Merge commit '053b5b8c4214ec379daac2cc2bf208c51cc1cd66' into main

Current Tink mirror in Fuchsia has missing include issue with the latest version of Clang. The missing include is #include <cstdlib> (https://github.com/tink-crypto/tink/blob/master/cc/util/secret_data_internal.h#L21)

Tink introduced it in https://github.com/tink-crypto/tink/commit/053b5b8c4214ec379daac2cc2bf208c51cc1cd66
We should at least uprev Tink to this commit to unblock the issue.

Command to uprev Tink:
```
git checkout origin/main -b ${USER}-merge
git merge 053b5b8c4214ec379daac2cc2bf208c51cc1cd66
python3 ./tools/convert_for_cobalt
fx format-code --files=${gn build files that convert_for_cobalt wrote}
git commit --no-verify
```

Locally patch to include missing absl/strings/str_cat.h header:
cc/mac/hmac_parameters.cc
cc/mac/aes_cmac_parameters.cc

Bug: 376297654

Change-Id: I7cc47556e5ddec6be01bd7a523f527abea47e20a
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/tink/+/1153894
Commit-Queue: Anivia Li <[email protected]>
Reviewed-by: Alex Pankhurst <[email protected]>
diff --git a/.bazelversion b/.bazelversion
index ac14c3d..09b254e 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 66f2ffc..2f37316 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,7 +1,6 @@
 ---
 name: Bug report
 about: Create a report to help us improve Tink
-assignees: 'thaidn, chuckx'
 
 ---
 
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 486da6f..100d12c 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,7 +1,6 @@
 ---
 name: Feature request
 about: Suggest an idea for Tink
-assignees: 'thaidn, chuckx'
 
 ---
 
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 49711e3..49269d9 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,7 +32,7 @@
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         # We must fetch at least the immediate parents so that if this is
         # a pull request then we can checkout the head.
@@ -45,7 +45,7 @@
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
+      uses: github/codeql-action/init@v2
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
+      uses: github/codeql-action/autobuild@v2
 
     # â„šī¸ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
+      uses: github/codeql-action/analyze@v2
diff --git a/CMakeLists.txt b/CMakeLists.txt
index dff0033..2e4f78f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
-cmake_minimum_required(VERSION 3.5)
-project(Tink VERSION 1.7.0 LANGUAGES CXX)
+cmake_minimum_required(VERSION 3.13)
+project(Tink VERSION 2.0.0 LANGUAGES CXX)
 
 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 
@@ -7,8 +7,6 @@
 option(TINK_USE_SYSTEM_OPENSSL "Build Tink linking to OpenSSL installed in the system" OFF)
 option(TINK_USE_INSTALLED_ABSEIL "Build Tink linking to Abseil installed in the system" OFF)
 option(TINK_USE_INSTALLED_GOOGLETEST "Build Tink linking to GTest installed in the system" OFF)
-option(TINK_USE_ABSL_STATUS "Compile Tink with absl::Status" OFF)
-option(TINK_USE_ABSL_STATUSOR "Compile Tink with absl::StatusOr" OFF)
 option(USE_ONLY_FIPS "Enables the FIPS only mode in Tink" OFF)
 
 set(CPACK_GENERATOR TGZ)
diff --git a/README.md b/README.md
index 74035b9..3ff7337 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,74 @@
 # Tink
 
-*A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.*
+*A multi-language, cross-platform library that provides cryptographic APIs that
+are secure, easy to use correctly, and hard(er) to misuse. See also:
+https://developers.google.com/tink*.
 
-https://developers.google.com/tink
+> **NOTE**: **Tink is moving!**
+>
+> As part of our roadmap we are splitting Tink into
+> [multiple GitHub repositories][split_repo_roadmap_url] that will be hosted at
+> [github.com/tink-crypto](https://github.com/tink-crypto) and will be
+> independently versioned.
+>
+> Roughly, we are going to create one repository per language, library extension
+> such as KMS (except Tink Python), and tools.
+>
+> A few important highlights:
+>
+> -   The migration will be done gradually over the course of 2023 with a new
+>     release from each of the new repositories. Releases will be announced in
+>     our [mailing list][tink_mailing_list_url].
+> -   We will keep updating each implementation/tool in
+>     [github.com/google/tink](https://github.com/google/tink) for a specified
+>     amount of time; migrated implementations/tools will eventually stop being
+>     updated on [github.com/google/tink](https://github.com/google/tink). The
+>     support window depends on the specific implementation, as shown in the
+>     table below.
+> -   New issues and pull requests should be created in the new repos.
+>
+> Below is the list of resulting repositories, migration timeline and expected
+> end of support.
+>
+> Tink implementation/extension         | New repository                                                                            | Migration status               | End of support in google/tink
+> ------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------ | -----------------------------
+> Tink Java                             | [tink-crypto/tink-java](https://github.com/tink-crypto/tink-java)                         | Complete (Q1 2023)             | Q3 2023
+> Tink Java AWS KMS extension           | [tink-crypto/tink-java-awskms](https://github.com/tink-crypto/tink-java-awskms)           | Complete (Q1 2023)             | Q3 2023
+> Tink Java Google Cloud KMS extension  | [tink-crypto/tink-java-gcpkms](https://github.com/tink-crypto/tink-java-gcpkms)           | Complete (Q1 2023)             | Q3 2023
+> Tink Java apps extension              | [tink-crypto/tink-java-apps](https://github.com/tink-crypto/tink-java-apps)               | Complete (Q1 2023)             | Q3 2023
+> Tink C++                              | [tink-crypto/tink-cc](https://github.com/tink-crypto/tink-cc)                             | Complete (Q2 2023)             | Q4 2023
+> Tink C++ AWS KMS extension            | [tink-crypto/tink-cc-awskms](https://github.com/tink-crypto/tink-cc-awskms)               | Complete (Q2 2023)             | Q4 2023
+> Tink C++ Google Cloud KMS extension   | [tink-crypto/tink-cc-gcpkms](https://github.com/tink-crypto/tink-cc-gcpkms)               | Complete (Q2 2023)             | Q4 2023
+> Tink Python                           | [tink-crypto/tink-py](https://github.com/tink-crypto/tink-py)                             | Not started (expected Q3 2023) | TBA
+> Tink Go                               | [tink-crypto/tink-go](https://github.com/tink-crypto/tink-go)                             | In progress (expected Q2 2023) | TBA
+> Tink Go AWS KMS extension             | [tink-crypto/tink-go-awskms](https://github.com/tink-crypto/tink-go-awskms)               | In progress (expected Q2 2023) | TBA
+> Tink Go Google Cloud KMS extension    | [tink-crypto/tink-go-gcpkms](https://github.com/tink-crypto/tink-go-gcpkms)               | In progress (expected Q2 2023) | TBA
+> Tink Go HashiCorp Vault KMS extension | [tink-crypto/tink-go-hcvault](https://github.com/tink-crypto/tink-go-hcvault)             | In progress (expected Q2 2023) | TBA
+> Tink Obj-C                            | [tink-crypto/tink-objc](https://github.com/tink-crypto/tink-objc)                         | Not started (expected Q4 2023) | TBA
+> Tink Tinkey                           | [tink-crypto/tink-tinkey](https://github.com/tink-crypto/tink-tinkey)                     | Complete (Q2 2023)             | Q4 2023
+> Tink cross language tests             | [tink-crypto/tink-cross-lang-tests](https://github.com/tink-crypto/tink-cross-lang-tests) | Not started (expected Q4 2023) | TBA
 
-**`Ubuntu`**                        | **`macOS`**
------------------------------------ | ---------------------------------
-[![Kokoro Ubuntu][ubuntu_badge]](#) | [![Kokoro macOS][macos_badge]](#)
+> **NOTE**: **We are removing Tink for JavaScript/TypeScript**
+>
+> We are removing the Tink JavaScript/TypeScript library from our current Github
+> repository (master branch). As part of our effort to migrate Tink to
+> https://github.com/tink-crypto, we will not release an individual
+> JavaScript/Typescript repository. Furthermore, the JavaScript/TypeScript
+> [directory](https://github.com/google/tink/tree/master/javascript) in the
+> current release branch (v1.7.0) will no longer be actively supported.
+>
+> _We aim to remove the JS/TS directory from the current Tink Github repository
+> (master branch) on **June 22, 2023**. We will also deprecate the Tink npm
+> package on this date._
+>
+> See [this](https://github.com/google/tink/issues/689) tracking issue for more
+> details.
+>
+> Feel free to use our [mailing list][tink_mailing_list_url] to raise any
+> questions, issues or concerns.
 
-[ubuntu_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-ubuntu.png
-[macos_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-macos.png
+[split_repo_roadmap_url]: https://developers.google.com/tink/roadmap#splitting_tink_into_multiple_github_repositories
+[tink_mailing_list_url]: https://groups.google.com/forum/#!forum/tink-users
 
 ## Index
 
@@ -25,9 +84,10 @@
 Using crypto in your application [shouldn't have to][devs_are_users_too_slides]
 feel like juggling chainsaws in the dark. Tink is a crypto library written by a
 group of cryptographers and security engineers at Google. It was born out of our
-extensive experience working with Google's product teams, [fixing weaknesses in
-implementations](https://github.com/google/wycheproof), and providing simple
-APIs that can be used safely without needing a crypto background.
+extensive experience working with Google's product teams,
+[fixing weaknesses in implementations](https://github.com/google/wycheproof),
+and providing simple APIs that can be used safely without needing a crypto
+background.
 
 Tink provides secure APIs that are easy to use correctly and hard(er) to misuse.
 It reduces common crypto pitfalls with user-centered design, careful
@@ -52,6 +112,15 @@
 released on 2022-08-09.
 
 Javascript/Typescript is in an alpha state and should only be used for testing.
+Please see the intent to remove statement
+[here](https://github.com/google/tink/issues/689).
+
+**`Ubuntu`**                        | **`macOS`**
+----------------------------------- | ---------------------------------
+[![Kokoro Ubuntu][ubuntu_badge]](#) | [![Kokoro macOS][macos_badge]](#)
+
+[ubuntu_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-ubuntu.png
+[macos_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-macos.png
 
 ## Getting started
 
@@ -130,12 +199,11 @@
 
 ## Contact and mailing list
 
-If you want to contribute, please read [CONTRIBUTING](docs/CONTRIBUTING.md)
-and send us pull requests. You can also report bugs or file feature requests.
+If you want to contribute, please read [CONTRIBUTING](docs/CONTRIBUTING.md) and
+send us pull requests. You can also report bugs or file feature requests.
 
 If you'd like to talk to the developers or get notified about major product
-updates, you may want to subscribe to our
-[mailing list](https://groups.google.com/forum/#!forum/tink-users).
+updates, you may want to subscribe to our [mailing list][tink_mailing_list_url].
 
 ## Maintainers
 
@@ -167,3 +235,5 @@
 -   Enzo Puig
 -   Veronika Slívová
 -   Paula Vidas
+-   Cathie Yun
+-   Federico Zalcberg
diff --git a/WORKSPACE b/WORKSPACE
deleted file mode 100644
index 5a9185b..0000000
--- a/WORKSPACE
+++ /dev/null
@@ -1,9 +0,0 @@
-workspace(name = "tink_base")
-
-load("@tink_base//:tink_base_deps.bzl", "tink_base_deps")
-
-tink_base_deps()
-
-load("@tink_base//:tink_base_deps_init.bzl", "tink_base_deps_init")
-
-tink_base_deps_init()
diff --git a/apps/.bazelversion b/apps/.bazelversion
deleted file mode 100644
index ac14c3d..0000000
--- a/apps/.bazelversion
+++ /dev/null
@@ -1 +0,0 @@
-5.1.1
diff --git a/apps/BUILD.bazel b/apps/BUILD.bazel
deleted file mode 100644
index 1a1b3a3..0000000
--- a/apps/BUILD.bazel
+++ /dev/null
@@ -1,3 +0,0 @@
-package(default_visibility = ["//:__subpackages__"])
-
-licenses(["notice"])
diff --git a/apps/README.md b/apps/README.md
deleted file mode 100644
index 70b329f..0000000
--- a/apps/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Tink Java Apps
-
-It contains extensions and applications of Tink Java.
diff --git a/apps/WORKSPACE b/apps/WORKSPACE
deleted file mode 100644
index 27aaa48..0000000
--- a/apps/WORKSPACE
+++ /dev/null
@@ -1,22 +0,0 @@
-workspace(name = "tink_apps")
-
-local_repository(
-    name = "tink_java",
-    path = "../java_src",
-)
-
-load("@tink_java//:tink_java_deps.bzl", "tink_java_deps", "TINK_MAVEN_ARTIFACTS")
-tink_java_deps()
-
-load("@tink_java//:tink_java_deps_init.bzl", "tink_java_deps_init")
-tink_java_deps_init()
-
-load("@rules_jvm_external//:defs.bzl", "maven_install")
-
-maven_install(
-    artifacts = TINK_MAVEN_ARTIFACTS,
-    repositories = [
-        "https://maven.google.com",
-        "https://repo1.maven.org/maven2",
-    ],
-)
diff --git a/apps/paymentmethodtoken/BUILD.bazel b/apps/paymentmethodtoken/BUILD.bazel
deleted file mode 100644
index f601945..0000000
--- a/apps/paymentmethodtoken/BUILD.bazel
+++ /dev/null
@@ -1,26 +0,0 @@
-load("@tink_java//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-gen_maven_jar_rules(
-    name = "maven",
-    doctitle = "Tink Cryptography API for Google Payment Method Token",
-    manifest_lines = [
-        "Automatic-Module-Name: com.google.crypto.tink.apps.paymentmethodtoken",
-    ],
-    root_packages = ["com.google.crypto.tink.apps.paymentmethodtoken"],
-    deps = [
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:google_payments_public_keys_manager",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_constants",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_decrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_encrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient_kem",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient_key_gen",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_sender",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_util",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:sender_intermediate_cert_factory",
-    ],
-)
diff --git a/apps/paymentmethodtoken/README.md b/apps/paymentmethodtoken/README.md
deleted file mode 100644
index 936026c..0000000
--- a/apps/paymentmethodtoken/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# An implementation of [Google Payment Method Token](https://developers.google.com/pay/api/payment-data-cryptography).
-
-## Latest release
-
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-paymentmethodtoken/1.7.0).
-
-The Maven group ID is `com.google.crypto.tink`, and the artifact ID is
-`apps-paymentmethodtoken`.
-
-To add a dependency using Maven:
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-paymentmethodtoken</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-## Snapshots
-
-Snapshots of this app built from the master branch are available through Maven
-using version `HEAD-SNAPSHOT`. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-paymentmethodtoken/HEAD-SNAPSHOT).
-
-To add a dependency using Maven:
-
-```xml
-<repositories>
-<repository>
-  <id>sonatype-snapshots</id>
-  <name>sonatype-snapshots</name>
-  <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
-  <snapshots>
-    <enabled>true</enabled>
-    <updatePolicy>always</updatePolicy>
-  </snapshots>
-  <releases>
-    <updatePolicy>always</updatePolicy>
-  </releases>
-</repository>
-</repositories>
-
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-paymentmethodtoken</artifactId>
-  <version>HEAD-SNAPSHOT</version>
-</dependency>
-```
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/BUILD.bazel b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/BUILD.bazel
deleted file mode 100644
index 434db56..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/BUILD.bazel
+++ /dev/null
@@ -1,125 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_library(
-    name = "google_payments_public_keys_manager",
-    srcs = ["GooglePaymentsPublicKeysManager.java"],
-    deps = [
-        "@maven//:com_google_http_client_google_http_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/util:keys_downloader",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_hybrid_decrypt",
-    srcs = ["PaymentMethodTokenHybridDecrypt.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_recipient_kem",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hkdf",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_sender",
-    srcs = ["PaymentMethodTokenSender.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_hybrid_encrypt",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_recipient_key_gen",
-    srcs = ["PaymentMethodTokenRecipientKeyGen.java"],
-    deps = [
-        ":payment_method_token_constants",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_constants",
-    srcs = ["PaymentMethodTokenConstants.java"],
-    deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:enums",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_recipient_kem",
-    srcs = ["PaymentMethodTokenRecipientKem.java"],
-)
-
-java_library(
-    name = "payment_method_token_hybrid_encrypt",
-    srcs = ["PaymentMethodTokenHybridEncrypt.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecies_hkdf_sender_kem",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_recipient",
-    srcs = ["PaymentMethodTokenRecipient.java"],
-    deps = [
-        ":google_payments_public_keys_manager",
-        ":payment_method_token_constants",
-        ":payment_method_token_hybrid_decrypt",
-        ":payment_method_token_recipient_kem",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:joda_time_joda_time",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "sender_intermediate_cert_factory",
-    srcs = ["SenderIntermediateCertFactory.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_util",
-    srcs = ["PaymentMethodTokenUtil.java"],
-    deps = [
-        ":payment_method_token_constants",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
-    ],
-)
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeysManager.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeysManager.java
deleted file mode 100644
index 0da08d2..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeysManager.java
+++ /dev/null
@@ -1,144 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.api.client.http.HttpTransport;
-import com.google.api.client.http.javanet.NetHttpTransport;
-import com.google.crypto.tink.util.KeysDownloader;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Thread-safe Google Payments public key manager.
- *
- * <p>For best performance, use the {@link GooglePaymentsPublicKeysManager#INSTANCE_PRODUCTION} for
- * production environment or {@link GooglePaymentsPublicKeysManager#INSTANCE_TEST} for test
- * environment.
- *
- * <p>If you need extra customizations for your use, we recommend you to use {@link
- * GooglePaymentsPublicKeysManager.Builder} to construct an instance and keep it as a singleton in a
- * static final variable across requests.
- *
- * <p>When initializing your server, we also recommend that you call {@link #refreshInBackground()}
- * to proactively fetch the keys.
- *
- * @since 1.0.0
- */
-public class GooglePaymentsPublicKeysManager {
-  /** Default HTTP transport used by this class. */
-  public static final NetHttpTransport DEFAULT_HTTP_TRANSPORT =
-      new NetHttpTransport.Builder().build();
-  /** URL to fetch keys for environment production. */
-  public static final String KEYS_URL_PRODUCTION =
-      "https://payments.developers.google.com/paymentmethodtoken/keys.json";
-  /** URL to fetch keys for environment test. */
-  public static final String KEYS_URL_TEST =
-      "https://payments.developers.google.com/paymentmethodtoken/test/keys.json";
-
-  private static final Executor DEFAULT_BACKGROUND_EXECUTOR = Executors.newCachedThreadPool();
-
-  private final KeysDownloader downloader;
-
-  /**
-   * Instance configured to talk to fetch keys from production environment (from {@link
-   * GooglePaymentsPublicKeysManager#KEYS_URL_PRODUCTION}).
-   */
-  public static final GooglePaymentsPublicKeysManager INSTANCE_PRODUCTION =
-      new GooglePaymentsPublicKeysManager(
-          DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, KEYS_URL_PRODUCTION);
-  /**
-   * Instance configured to talk to fetch keys from test environment (from {@link
-   * GooglePaymentsPublicKeysManager#KEYS_URL_TEST}).
-   */
-  public static final GooglePaymentsPublicKeysManager INSTANCE_TEST =
-      new GooglePaymentsPublicKeysManager(
-          DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, KEYS_URL_TEST);
-
-  GooglePaymentsPublicKeysManager(
-      Executor backgroundExecutor, HttpTransport httpTransport, String keysUrl) {
-    this.downloader =
-        new KeysDownloader.Builder()
-            .setUrl(keysUrl)
-            .setExecutor(backgroundExecutor)
-            .setHttpTransport(httpTransport)
-            .build();
-  }
-
-  HttpTransport getHttpTransport() {
-    return downloader.getHttpTransport();
-  }
-
-  String getUrl() {
-    return downloader.getUrl();
-  }
-
-  /**
-   * Returns a string containing a JSON with the Google public signing keys.
-   *
-   * <p>Meant to be called by {@link PaymentMethodTokenRecipient}.
-   */
-  String getTrustedSigningKeysJson() throws IOException {
-    return this.downloader.download();
-  }
-
-  /** Fetches keys in the background. */
-  public void refreshInBackground() {
-    downloader.refreshInBackground();
-  }
-
-  /**
-   * Builder for {@link GooglePaymentsPublicKeysManager}.
-   *
-   * @since 1.0.0
-   */
-  public static class Builder {
-    private HttpTransport httpTransport = DEFAULT_HTTP_TRANSPORT;
-    private String keysUrl = KEYS_URL_PRODUCTION;
-
-    public Builder setKeysUrl(String keysUrl) {
-      this.keysUrl = keysUrl;
-      return this;
-    }
-
-    /**
-     * Sets the HTTP transport.
-     *
-     * <p>You generally should not need to set a custom transport as the default transport {@link
-     * GooglePaymentsPublicKeysManager#DEFAULT_HTTP_TRANSPORT} should be suited for most use cases.
-     */
-    public Builder setHttpTransport(HttpTransport httpTransport) {
-      this.httpTransport = httpTransport;
-      return this;
-    }
-
-    public GooglePaymentsPublicKeysManager build() {
-      // If all parameters are equal to the existing singleton instances, returning them instead.
-      // This is more a safe guard if users of this class construct a new class and forget to
-      // save in a singleton.
-      for (GooglePaymentsPublicKeysManager instance :
-          Arrays.asList(INSTANCE_PRODUCTION, INSTANCE_TEST)) {
-        if (instance.getHttpTransport() == httpTransport && instance.getUrl().equals(keysUrl)) {
-          return instance;
-        }
-      }
-      return new GooglePaymentsPublicKeysManager(
-          DEFAULT_BACKGROUND_EXECUTOR, httpTransport, keysUrl);
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenConstants.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenConstants.java
deleted file mode 100644
index 416a426..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenConstants.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Enums.HashType;
-import java.nio.charset.StandardCharsets;
-
-/** Various constants. */
-final class PaymentMethodTokenConstants {
-  public static final String GOOGLE_SENDER_ID = "Google";
-  public static final String HMAC_SHA256_ALGO = "HmacSha256";
-  public static final byte[] HKDF_EMPTY_SALT = new byte[0];
-  public static final byte[] GOOGLE_CONTEXT_INFO_ECV1 = "Google".getBytes(StandardCharsets.UTF_8);
-  public static final String AES_CTR_ALGO = "AES/CTR/NoPadding";
-  // Zero IV is fine here because each encryption uses a unique key.
-  public static final byte[] AES_CTR_ZERO_IV = new byte[16];
-  public static final EllipticCurves.CurveType P256_CURVE_TYPE = EllipticCurves.CurveType.NIST_P256;
-  public static final EllipticCurves.PointFormatType UNCOMPRESSED_POINT_FORMAT =
-      EllipticCurves.PointFormatType.UNCOMPRESSED;
-  public static final String PROTOCOL_VERSION_EC_V1 = "ECv1";
-  public static final String PROTOCOL_VERSION_EC_V2 = "ECv2";
-  public static final String PROTOCOL_VERSION_EC_V2_SIGNING_ONLY = "ECv2SigningOnly";
-  public static final HashType ECDSA_HASH_SHA256 = HashType.SHA256;
-
-  public static final String JSON_ENCRYPTED_MESSAGE_KEY = "encryptedMessage";
-  public static final String JSON_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey";
-  public static final String JSON_INTERMEDIATE_SIGNING_KEY = "intermediateSigningKey";
-  public static final String JSON_KEY_EXPIRATION_KEY = "keyExpiration";
-  public static final String JSON_KEY_VALUE_KEY = "keyValue";
-  public static final String JSON_MESSAGE_EXPIRATION_KEY = "messageExpiration";
-  public static final String JSON_PROTOCOL_VERSION_KEY = "protocolVersion";
-  public static final String JSON_SIGNATURES_KEY = "signatures";
-  public static final String JSON_SIGNATURE_KEY = "signature";
-  public static final String JSON_SIGNED_KEY_KEY = "signedKey";
-  public static final String JSON_SIGNED_MESSAGE_KEY = "signedMessage";
-  public static final String JSON_TAG_KEY = "tag";
-
-  /** Represents configuration regarding each protocol version. */
-  enum ProtocolVersionConfig {
-    EC_V1(
-        /* protocolVersion= */ PROTOCOL_VERSION_EC_V1,
-        /* aesCtrKeySize= */ 128 / 8,
-        /* hmacSha256KeySize= */ 128 / 8,
-        /* isEncryptionRequired= */ true,
-        /* supportsIntermediateSigningKeys= */ false),
-    EC_V2(
-        /* protocolVersion= */ PROTOCOL_VERSION_EC_V2,
-        /* aesCtrKeySize= */ 256 / 8,
-        /* hmacSha256KeySize= */ 256 / 8,
-        /* isEncryptionRequired= */ true,
-        /* supportsIntermediateSigningKeys= */ true),
-    EC_V2_SIGNING_ONLY(
-        /* protocolVersion= */ PROTOCOL_VERSION_EC_V2_SIGNING_ONLY,
-        /* aesCtrKeySize= */ 256 / 8,
-        /* hmacSha256KeySize= */ 256 / 8,
-        /* isEncryptionRequired= */ false,
-        /* supportsIntermediateSigningKeys= */ true);
-
-    public final String protocolVersion;
-    public final int aesCtrKeySize;
-    public final int hmacSha256KeySize;
-    public final boolean isEncryptionRequired;
-    public final boolean supportsIntermediateSigningKeys;
-
-    ProtocolVersionConfig(
-        String protocolVersion,
-        int aesCtrKeySize,
-        int hmacSha256KeySize,
-        boolean isEncryptionRequired,
-        boolean supportsIntermediateSigningKeys) {
-      this.protocolVersion = protocolVersion;
-      this.aesCtrKeySize = aesCtrKeySize;
-      this.hmacSha256KeySize = hmacSha256KeySize;
-      this.isEncryptionRequired = isEncryptionRequired;
-      this.supportsIntermediateSigningKeys = supportsIntermediateSigningKeys;
-    }
-
-    public static ProtocolVersionConfig forProtocolVersion(String protocolVersion) {
-      for (ProtocolVersionConfig protocolVersionConfig : ProtocolVersionConfig.values()) {
-        if (protocolVersionConfig.protocolVersion.equals(protocolVersion)) {
-          return protocolVersionConfig;
-        }
-      }
-      throw new IllegalArgumentException("Unknown protocol version: " + protocolVersion);
-    }
-  }
-
-  private PaymentMethodTokenConstants() {}
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecrypt.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecrypt.java
deleted file mode 100644
index 6d2637a..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecrypt.java
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Hkdf;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.Arrays;
-
-/**
- * A {@link HybridDecrypt} implementation for the hybrid encryption used in <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- */
-class PaymentMethodTokenHybridDecrypt implements HybridDecrypt {
-  private final PaymentMethodTokenRecipientKem recipientKem;
-  private final ProtocolVersionConfig protocolVersionConfig;
-
-  PaymentMethodTokenHybridDecrypt(
-      final ECPrivateKey recipientPrivateKey, ProtocolVersionConfig protocolVersionConfig)
-      throws GeneralSecurityException {
-    this(
-        new PaymentMethodTokenRecipientKem() {
-          @Override
-          public byte[] computeSharedSecret(final byte[] ephemeralPublicKey)
-              throws GeneralSecurityException {
-            ECPublicKey publicKey =
-                EllipticCurves.getEcPublicKey(
-                    recipientPrivateKey.getParams(),
-                    PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-                    ephemeralPublicKey);
-            return EllipticCurves.computeSharedSecret(recipientPrivateKey, publicKey);
-          }
-        },
-        protocolVersionConfig);
-  }
-
-  PaymentMethodTokenHybridDecrypt(
-      final PaymentMethodTokenRecipientKem recipientKem,
-      ProtocolVersionConfig protocolVersionConfig) {
-    this.recipientKem = recipientKem;
-    this.protocolVersionConfig = protocolVersionConfig;
-  }
-
-  @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] contextInfo)
-      throws GeneralSecurityException {
-    try {
-      JsonObject json = JsonParser.parseString(new String(ciphertext, UTF_8)).getAsJsonObject();
-      validate(json);
-      byte[] demKey = kem(json, contextInfo);
-      return dem(json, demKey);
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("cannot decrypt; failed to parse JSON", e);
-    }
-  }
-
-  private byte[] kem(JsonObject json, final byte[] contextInfo) throws GeneralSecurityException {
-    int demKeySize = protocolVersionConfig.aesCtrKeySize + protocolVersionConfig.hmacSha256KeySize;
-    byte[] ephemeralPublicKey =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY).getAsString());
-    byte[] sharedSecret = recipientKem.computeSharedSecret(ephemeralPublicKey);
-    return Hkdf.computeEciesHkdfSymmetricKey(
-        ephemeralPublicKey,
-        sharedSecret,
-        PaymentMethodTokenConstants.HMAC_SHA256_ALGO,
-        PaymentMethodTokenConstants.HKDF_EMPTY_SALT,
-        contextInfo,
-        demKeySize);
-  }
-
-  private byte[] dem(JsonObject json, final byte[] demKey) throws GeneralSecurityException {
-    byte[] hmacSha256Key =
-        Arrays.copyOfRange(demKey, protocolVersionConfig.aesCtrKeySize, demKey.length);
-    byte[] encryptedMessage =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY).getAsString());
-    byte[] computedTag = PaymentMethodTokenUtil.hmacSha256(hmacSha256Key, encryptedMessage);
-    byte[] expectedTag =
-        Base64.decode(json.get(PaymentMethodTokenConstants.JSON_TAG_KEY).getAsString());
-    if (!Bytes.equal(expectedTag, computedTag)) {
-      throw new GeneralSecurityException("cannot decrypt; invalid MAC");
-    }
-    byte[] aesCtrKey = Arrays.copyOf(demKey, protocolVersionConfig.aesCtrKeySize);
-    return PaymentMethodTokenUtil.aesCtr(aesCtrKey, encryptedMessage);
-  }
-
-  private void validate(JsonObject payload) throws GeneralSecurityException {
-    if (!payload.has(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY)
-        || !payload.has(PaymentMethodTokenConstants.JSON_TAG_KEY)
-        || !payload.has(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY)
-        || payload.size() != 3) {
-      throw new GeneralSecurityException(
-          "The payload must contain exactly encryptedMessage, tag and ephemeralPublicKey");
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncrypt.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncrypt.java
deleted file mode 100644
index ff3a418..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncrypt.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EciesHkdfSenderKem;
-import com.google.gson.JsonObject;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPublicKey;
-import java.util.Arrays;
-
-/**
- * A {@link HybridEncrypt} implementation for the hybrid encryption used in <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- */
-class PaymentMethodTokenHybridEncrypt implements HybridEncrypt {
-  private final EciesHkdfSenderKem senderKem;
-  private final ProtocolVersionConfig protocolVersionConfig;
-
-  public PaymentMethodTokenHybridEncrypt(
-      final ECPublicKey recipientPublicKey, final ProtocolVersionConfig protocolVersionConfig) {
-    this.senderKem = new EciesHkdfSenderKem(recipientPublicKey);
-    this.protocolVersionConfig = protocolVersionConfig;
-  }
-
-  static String jsonEncodeCiphertext(byte[] ciphertext, byte[] tag, byte[] ephemeralPublicKey)
-      throws GeneralSecurityException {
-    JsonObject result = new JsonObject();
-    result.addProperty(
-        PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY, Base64.encode(ciphertext));
-    result.addProperty(
-        PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY, Base64.encode(ephemeralPublicKey));
-    result.addProperty(PaymentMethodTokenConstants.JSON_TAG_KEY, Base64.encode(tag));
-    StringWriter stringWriter = new StringWriter();
-    JsonWriter jsonWriter = new JsonWriter(stringWriter);
-    jsonWriter.setHtmlSafe(true);
-    try {
-      Streams.write(result, jsonWriter);
-      return stringWriter.toString();
-    } catch (IOException e) {
-      throw new GeneralSecurityException("cannot encrypt; JSON error", e);
-    }
-  }
-
-  @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] contextInfo)
-      throws GeneralSecurityException {
-    int symmetricKeySize =
-        protocolVersionConfig.aesCtrKeySize + protocolVersionConfig.hmacSha256KeySize;
-    EciesHkdfSenderKem.KemKey kemKey =
-        senderKem.generateKey(
-            PaymentMethodTokenConstants.HMAC_SHA256_ALGO,
-            PaymentMethodTokenConstants.HKDF_EMPTY_SALT,
-            contextInfo,
-            symmetricKeySize,
-            PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT);
-    byte[] aesCtrKey = Arrays.copyOf(kemKey.getSymmetricKey(), protocolVersionConfig.aesCtrKeySize);
-    byte[] ciphertext = PaymentMethodTokenUtil.aesCtr(aesCtrKey, plaintext);
-    byte[] hmacSha256Key =
-        Arrays.copyOfRange(
-            kemKey.getSymmetricKey(), protocolVersionConfig.aesCtrKeySize, symmetricKeySize);
-    byte[] tag = PaymentMethodTokenUtil.hmacSha256(hmacSha256Key, ciphertext);
-    byte[] ephemeralPublicKey = kemKey.getKemBytes();
-
-    String jsonEncodedCiphertext = jsonEncodeCiphertext(ciphertext, tag, ephemeralPublicKey);
-    return jsonEncodedCiphertext.getBytes(UTF_8);
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipient.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipient.java
deleted file mode 100644
index 5f6cfe7..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipient.java
+++ /dev/null
@@ -1,629 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaVerifyJce;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import org.joda.time.Instant;
-
-/**
- * An implementation of the recipient side of <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- *
- * <h3>Warning</h3>
- *
- * <p>This implementation only supports versions {@code ECv1}, {@code ECv2} and {@code
- * ECv2SigningOnly}.
- *
- * <h3>Typical usage</h3>
- *
- * <pre>{@code
- * PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder()
- *     .fetchSenderVerifyingKeysWith(
- *         GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION)
- *     .recipientId(recipientId)
- *     // Multiple recipient private keys can be added to support graceful key rotations
- *     .addRecipientPrivateKey(recipientPrivateKey1)
- *     .addRecipientPrivateKey(recipientPrivateKey2)
- *     .build();
- * String ciphertext = ...;
- * String plaintext = recipient.unseal(ciphertext);
- * }</pre>
- *
- * <h3>Custom decryption</h3>
- *
- * <p>Recipients that store private keys in HSM can provide implementations of {@link
- * PaymentMethodTokenRecipientKem} and configure Tink to use their custom code with {@link
- * PaymentMethodTokenRecipient.Builder#addRecipientKem}.
- *
- * <pre>{@code
- * PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder()
- *     .fetchSenderVerifyingKeysWith(
- *         GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION)
- *     .recipientId(recipientId)
- *     .addRecipientKem(new MyPaymentMethodTokenRecipientKem())
- *     .build();
- * String ciphertext = ...;
- * String plaintext = recipient.unseal(ciphertext);
- * }</pre>
- *
- * @see <a href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment
- *     Method Token standard</a>
- * @since 1.0.0
- */
-public final class PaymentMethodTokenRecipient {
-  private final String protocolVersion;
-  private final List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders;
-  private final List<HybridDecrypt> hybridDecrypters = new ArrayList<>();
-  private final String senderId;
-  private final String recipientId;
-
-  PaymentMethodTokenRecipient(
-      String protocolVersion,
-      List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders,
-      String senderId,
-      List<ECPrivateKey> recipientPrivateKeys,
-      List<PaymentMethodTokenRecipientKem> recipientKems,
-      String recipientId)
-      throws GeneralSecurityException {
-    if (!protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-        && !protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        && !protocolVersion.equals(
-            PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)) {
-      throw new IllegalArgumentException("invalid version: " + protocolVersion);
-    }
-    this.protocolVersion = protocolVersion;
-    if (senderVerifyingKeysProviders == null || senderVerifyingKeysProviders.isEmpty()) {
-      throw new IllegalArgumentException(
-          "must set at least one way to get sender's verifying key using"
-              + " Builder.fetchSenderVerifyingKeysWith or Builder.senderVerifyingKeys");
-    }
-    this.senderVerifyingKeysProviders = senderVerifyingKeysProviders;
-    this.senderId = senderId;
-
-    ProtocolVersionConfig protocolVersionConfig =
-        ProtocolVersionConfig.forProtocolVersion(protocolVersion);
-    if (protocolVersionConfig.isEncryptionRequired) {
-      if (recipientPrivateKeys.isEmpty() && recipientKems.isEmpty()) {
-        throw new IllegalArgumentException(
-            "must add at least one recipient's decrypting key using Builder.addRecipientPrivateKey "
-                + "or Builder.addRecipientKem");
-      }
-      for (ECPrivateKey privateKey : recipientPrivateKeys) {
-        hybridDecrypters.add(
-            new PaymentMethodTokenHybridDecrypt(privateKey, protocolVersionConfig));
-      }
-      for (PaymentMethodTokenRecipientKem kem : recipientKems) {
-        hybridDecrypters.add(new PaymentMethodTokenHybridDecrypt(kem, protocolVersionConfig));
-      }
-    } else {
-      if (!recipientPrivateKeys.isEmpty() || !recipientKems.isEmpty()) {
-        throw new IllegalArgumentException(
-            "must not set private decrypting key using Builder.addRecipientPrivateKey "
-                + "or Builder.addRecipientDecrypter");
-      }
-    }
-
-    if (recipientId == null) {
-      throw new IllegalArgumentException("must set recipient Id using Builder.recipientId");
-    }
-    this.recipientId = recipientId;
-  }
-
-  private PaymentMethodTokenRecipient(Builder builder) throws GeneralSecurityException {
-    this(
-        builder.protocolVersion,
-        builder.senderVerifyingKeysProviders,
-        builder.senderId,
-        builder.recipientPrivateKeys,
-        builder.recipientKems,
-        builder.recipientId);
-  }
-
-  /**
-   * Builder for {@link PaymentMethodTokenRecipient}.
-   *
-   * @since 1.0.0
-   */
-  public static class Builder {
-    private String protocolVersion = PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1;
-    private String senderId = PaymentMethodTokenConstants.GOOGLE_SENDER_ID;
-    private String recipientId = null;
-    private final List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders =
-        new ArrayList<SenderVerifyingKeysProvider>();
-    private final List<ECPrivateKey> recipientPrivateKeys = new ArrayList<ECPrivateKey>();
-    private final List<PaymentMethodTokenRecipientKem> recipientKems = new ArrayList<>();
-
-    public Builder() {}
-
-    /** Sets the protocolVersion. */
-    public Builder protocolVersion(String val) {
-      protocolVersion = val;
-      return this;
-    }
-
-    /** Sets the sender Id. */
-    public Builder senderId(String val) {
-      senderId = val;
-      return this;
-    }
-
-    /** Sets the recipient Id. */
-    public Builder recipientId(String val) {
-      recipientId = val;
-      return this;
-    }
-
-    /**
-     * Fetches verifying public keys of the sender using {@link GooglePaymentsPublicKeysManager}.
-     *
-     * <p>This is the preferred method of specifying the verifying public keys of the sender.
-     */
-    public Builder fetchSenderVerifyingKeysWith(
-        final GooglePaymentsPublicKeysManager googlePaymentsPublicKeysManager)
-        throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              try {
-                return parseTrustedSigningKeysJson(
-                    protocolVersion, googlePaymentsPublicKeysManager.getTrustedSigningKeysJson());
-              } catch (IOException e) {
-                throw new GeneralSecurityException("Failed to fetch keys!", e);
-              }
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Sets the trusted verifying public keys of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchSenderVerifyingKeysWith} passing it an instance of {@link
-     * GooglePaymentsPublicKeysManager}. It will take care of fetching fresh keys and caching in
-     * memory. Only use this method if you can't use {@link #fetchSenderVerifyingKeysWith} and be
-     * aware you will need to handle Google key rotations yourself.
-     *
-     * <p>The given string is a JSON object formatted like the following:
-     *
-     * <pre>
-     * {
-     *   "keys": [
-     *     {
-     *       "keyValue": "encoded public key",
-     *       "protocolVersion": "ECv1"
-     *     },
-     *     {
-     *       "keyValue": "encoded public key",
-     *       "protocolVersion": "ECv1"
-     *     },
-     *   ],
-     * }
-     * </pre>
-     *
-     * <p>Each public key will be a base64 (no wrapping, padded) version of the key encoded in ASN.1
-     * type SubjectPublicKeyInfo defined in the X.509 standard.
-     */
-    public Builder senderVerifyingKeys(final String trustedSigningKeysJson)
-        throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              return parseTrustedSigningKeysJson(protocolVersion, trustedSigningKeysJson);
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchSenderVerifyingKeysWith} passing it an instance of {@link
-     * GooglePaymentsPublicKeysManager}. It will take care of fetching fresh keys and caching in
-     * memory. Only use this method if you can't use {@link #fetchSenderVerifyingKeysWith} and be
-     * aware you will need to handle Google key rotations yourself.
-     *
-     * <p>The public key is a base64 (no wrapping, padded) version of the key encoded in ASN.1 type
-     * SubjectPublicKeyInfo defined in the X.509 standard.
-     *
-     * <p>Multiple keys may be added. This utility will then verify any message signed with any of
-     * the private keys corresponding to the public keys added. Adding multiple keys is useful for
-     * handling key rotation.
-     */
-    public Builder addSenderVerifyingKey(final String val) throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              return Collections.singletonList(PaymentMethodTokenUtil.x509EcPublicKey(val));
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchSenderVerifyingKeysWith} passing it an instance of {@link
-     * GooglePaymentsPublicKeysManager}. It will take care of fetching fresh keys and caching in
-     * memory. Only use this method if you can't use {@link #fetchSenderVerifyingKeysWith} and be
-     * aware you will need to handle Google key rotations yourself.
-     */
-    public Builder addSenderVerifyingKey(final ECPublicKey val) throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              return Collections.singletonList(val);
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds the decryption private key of the recipient.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder addRecipientPrivateKey(String val) throws GeneralSecurityException {
-      return addRecipientPrivateKey(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val));
-    }
-
-    /** Adds the decryption private key of the recipient. */
-    public Builder addRecipientPrivateKey(ECPrivateKey val) throws GeneralSecurityException {
-      recipientPrivateKeys.add(val);
-      return this;
-    }
-
-    /**
-     * Adds a custom {@link PaymentMethodTokenRecipientKem}.
-     *
-     * <p>This is useful for clients that store keys in an HSM and need a more control on how the
-     * key is used. If you are not using an HSM, you probably should just use {@link
-     * #addRecipientPrivateKey}.
-     *
-     * @since 1.1.0
-     */
-    public Builder addRecipientKem(PaymentMethodTokenRecipientKem kem) {
-      recipientKems.add(kem);
-      return this;
-    }
-
-    public PaymentMethodTokenRecipient build() throws GeneralSecurityException {
-      return new PaymentMethodTokenRecipient(this);
-    }
-  }
-
-  /**
-   * Unseal the given {@code sealedMessage} by performing the necessary signature verification and
-   * decryption (if required) steps based on the protocolVersion.
-   */
-  public String unseal(final String sealedMessage) throws GeneralSecurityException {
-    try {
-      if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) {
-        return unsealECV1(sealedMessage);
-      } else if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)) {
-        return unsealECV2(sealedMessage);
-      } else if (protocolVersion.equals(
-          PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)) {
-        return unsealECV2SigningOnly(sealedMessage);
-      }
-      throw new IllegalArgumentException("unsupported version: " + protocolVersion);
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("cannot unseal; invalid JSON message", e);
-    }
-  }
-
-  private String unsealECV1(String sealedMessage) throws GeneralSecurityException {
-    JsonObject jsonMsg = JsonParser.parseString(sealedMessage).getAsJsonObject();
-    validateECV1(jsonMsg);
-    String signedMessage = verifyECV1(jsonMsg);
-    String decryptedMessage = decrypt(signedMessage);
-    validateMessage(decryptedMessage);
-    return decryptedMessage;
-  }
-
-  private String unsealECV2(String sealedMessage) throws GeneralSecurityException {
-    JsonObject jsonMsg = JsonParser.parseString(sealedMessage).getAsJsonObject();
-    validateECV2(jsonMsg);
-    String signedMessage = verifyECV2(jsonMsg);
-    String decryptedMessage = decrypt(signedMessage);
-    validateMessage(decryptedMessage);
-    return decryptedMessage;
-  }
-
-  private String unsealECV2SigningOnly(String sealedMessage) throws GeneralSecurityException {
-    JsonObject jsonMsg = JsonParser.parseString(sealedMessage).getAsJsonObject();
-    validateECV2(jsonMsg);
-    String message = verifyECV2(jsonMsg);
-    validateMessage(message);
-    return message;
-  }
-
-  private String verifyECV1(final JsonObject jsonMsg) throws GeneralSecurityException {
-    byte[] signature =
-        Base64.decode(jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY).getAsString());
-    String signedMessage =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString();
-    byte[] signedBytes = getSignedBytes(protocolVersion, signedMessage);
-    verify(
-        protocolVersion,
-        senderVerifyingKeysProviders,
-        Collections.singletonList(signature),
-        signedBytes);
-    return signedMessage;
-  }
-
-  private String verifyECV2(final JsonObject jsonMsg) throws GeneralSecurityException {
-    byte[] signature =
-        Base64.decode(jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY).getAsString());
-    String signedMessage =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString();
-    byte[] signedBytes = getSignedBytes(protocolVersion, signedMessage);
-    verify(
-        protocolVersion,
-        verifyIntermediateSigningKey(jsonMsg),
-        Collections.singletonList(signature),
-        signedBytes);
-    return signedMessage;
-  }
-
-  private byte[] getSignedBytes(String protocolVersion, String signedMessage)
-      throws GeneralSecurityException {
-    return PaymentMethodTokenUtil.toLengthValue(
-        // The order of the parameters matters.
-        senderId, recipientId, protocolVersion, signedMessage);
-  }
-
-  private void validateMessage(String decryptedMessage) throws GeneralSecurityException {
-    JsonObject decodedMessage;
-
-    try {
-      decodedMessage = JsonParser.parseString(decryptedMessage).getAsJsonObject();
-    } catch (JsonParseException | IllegalStateException e) {
-      // Message wasn't a valid JSON, so nothing to validate.
-      return;
-    }
-
-    // If message expiration is present, checking it.
-    if (decodedMessage.has(PaymentMethodTokenConstants.JSON_MESSAGE_EXPIRATION_KEY)) {
-      long expirationInMillis =
-          Long.parseLong(
-              decodedMessage
-                  .get(PaymentMethodTokenConstants.JSON_MESSAGE_EXPIRATION_KEY)
-                  .getAsString());
-      if (expirationInMillis <= Instant.now().getMillis()) {
-        throw new GeneralSecurityException("expired payload");
-      }
-    }
-  }
-
-  private static void verify(
-      final String protocolVersion,
-      final List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders,
-      final List<byte[]> signatures,
-      final byte[] signedBytes)
-      throws GeneralSecurityException {
-    boolean verified = false;
-    for (SenderVerifyingKeysProvider verifyingKeysProvider : senderVerifyingKeysProviders) {
-      for (ECPublicKey publicKey : verifyingKeysProvider.get(protocolVersion)) {
-        EcdsaVerifyJce verifier =
-            new EcdsaVerifyJce(
-                publicKey, PaymentMethodTokenConstants.ECDSA_HASH_SHA256, EcdsaEncoding.DER);
-        for (byte[] signature : signatures) {
-          try {
-            verifier.verify(signature, signedBytes);
-            // No exception means the signature is valid.
-            verified = true;
-          } catch (GeneralSecurityException e) {
-            // ignored, try again
-          }
-        }
-      }
-    }
-    if (!verified) {
-      throw new GeneralSecurityException("cannot verify signature");
-    }
-  }
-
-  private String decrypt(String ciphertext) throws GeneralSecurityException {
-    for (HybridDecrypt hybridDecrypter : hybridDecrypters) {
-      try {
-        byte[] cleartext =
-            hybridDecrypter.decrypt(
-                ciphertext.getBytes(UTF_8), PaymentMethodTokenConstants.GOOGLE_CONTEXT_INFO_ECV1);
-        return new String(cleartext, UTF_8);
-      } catch (GeneralSecurityException e) {
-        // ignored, try again
-      }
-    }
-    throw new GeneralSecurityException("cannot decrypt");
-  }
-
-  private void validateECV1(final JsonObject jsonMsg) throws GeneralSecurityException {
-    if (!jsonMsg.has(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY)
-        || !jsonMsg.has(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY)
-        || !jsonMsg.has(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY)
-        || jsonMsg.size() != 3) {
-      throw new GeneralSecurityException(
-          "ECv1 message must contain exactly protocolVersion, signature and signedMessage");
-    }
-    String version =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY).getAsString();
-    if (!version.equals(protocolVersion)) {
-      throw new GeneralSecurityException("invalid version: " + version);
-    }
-  }
-
-  private void validateECV2(final JsonObject jsonMsg) throws GeneralSecurityException {
-    if (!jsonMsg.has(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY)
-        || !jsonMsg.has(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY)
-        || !jsonMsg.has(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY)
-        || !jsonMsg.has(PaymentMethodTokenConstants.JSON_INTERMEDIATE_SIGNING_KEY)
-        || jsonMsg.size() != 4) {
-      throw new GeneralSecurityException(
-          protocolVersion
-              + " message must contain exactly protocolVersion, intermediateSigningKey, "
-              + "signature and signedMessage");
-    }
-    String version =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY).getAsString();
-    if (!version.equals(protocolVersion)) {
-      throw new GeneralSecurityException("invalid version: " + version);
-    }
-  }
-
-  /**
-   * Verifies the intermediate key and returns a singleton list containing of {@code
-   * SenderVerifyingKeysProvider} that provides the verified key.
-   */
-  private List<SenderVerifyingKeysProvider> verifyIntermediateSigningKey(JsonObject jsonMsg)
-      throws GeneralSecurityException {
-    JsonObject intermediateSigningKey =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_INTERMEDIATE_SIGNING_KEY).getAsJsonObject();
-    validateIntermediateSigningKey(intermediateSigningKey);
-    ArrayList<byte[]> signatures = new ArrayList<>();
-    JsonArray signaturesJson =
-        intermediateSigningKey
-            .get(PaymentMethodTokenConstants.JSON_SIGNATURES_KEY)
-            .getAsJsonArray();
-    for (int i = 0; i < signaturesJson.size(); i++) {
-      signatures.add(Base64.decode(signaturesJson.get(i).getAsString()));
-    }
-    String signedKeyAsString =
-        intermediateSigningKey.get(PaymentMethodTokenConstants.JSON_SIGNED_KEY_KEY).getAsString();
-    byte[] signedBytes =
-        PaymentMethodTokenUtil.toLengthValue(
-            // The order of the parameters matters.
-            senderId, protocolVersion, signedKeyAsString);
-    verify(protocolVersion, senderVerifyingKeysProviders, signatures, signedBytes);
-    JsonObject signedKey = JsonParser.parseString(signedKeyAsString).getAsJsonObject();
-    validateSignedKey(signedKey);
-    final String key = signedKey.get(PaymentMethodTokenConstants.JSON_KEY_VALUE_KEY).getAsString();
-    SenderVerifyingKeysProvider provider =
-        new SenderVerifyingKeysProvider() {
-          @Override
-          public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-            if (PaymentMethodTokenRecipient.this.protocolVersion.equals(protocolVersion)) {
-              return Collections.singletonList(PaymentMethodTokenUtil.x509EcPublicKey(key));
-            } else {
-              return Collections.emptyList();
-            }
-          }
-        };
-    return Collections.singletonList(provider);
-  }
-
-  private JsonObject validateIntermediateSigningKey(final JsonObject intermediateSigningKey)
-      throws GeneralSecurityException {
-    if (!intermediateSigningKey.has(PaymentMethodTokenConstants.JSON_SIGNATURES_KEY)
-        || !intermediateSigningKey.has(PaymentMethodTokenConstants.JSON_SIGNED_KEY_KEY)
-        || intermediateSigningKey.size() != 2) {
-      throw new GeneralSecurityException(
-          "intermediateSigningKey must contain exactly signedKey and signatures");
-    }
-    return intermediateSigningKey;
-  }
-
-  private void validateSignedKey(final JsonObject signedKey) throws GeneralSecurityException {
-    // Note: allowing further keys to be added so we can extend the protocol if needed in the future
-    if (!signedKey.has(PaymentMethodTokenConstants.JSON_KEY_VALUE_KEY)
-        || !signedKey.has(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY)) {
-      throw new GeneralSecurityException(
-          "intermediateSigningKey.signedKey must contain keyValue and keyExpiration");
-    }
-
-    // If message expiration is present, checking it.
-    long expirationInMillis =
-        Long.parseLong(
-            signedKey.get(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY).getAsString());
-    if (expirationInMillis <= Instant.now().getMillis()) {
-      throw new GeneralSecurityException("expired intermediateSigningKey");
-    }
-  }
-
-  private static List<ECPublicKey> parseTrustedSigningKeysJson(
-      String protocolVersion, String trustedSigningKeysJson) throws GeneralSecurityException {
-    List<ECPublicKey> senderVerifyingKeys = new ArrayList<>();
-    try {
-      JsonArray keys =
-          JsonParser.parseString(trustedSigningKeysJson)
-              .getAsJsonObject()
-              .get("keys")
-              .getAsJsonArray();
-      for (int i = 0; i < keys.size(); i++) {
-        JsonObject key = keys.get(i).getAsJsonObject();
-        if (protocolVersion.equals(
-            key.get(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY).getAsString())) {
-
-          if (key.has(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY)) {
-            // If message expiration is present, checking it.
-            long expirationInMillis =
-                Long.parseLong(
-                    key.get(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY).getAsString());
-            if (expirationInMillis <= Instant.now().getMillis()) {
-              // Ignore expired keys
-              continue;
-            }
-          } else if (!protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) {
-            // keyExpiration is required in all versions except ECv1, so if it is missing we should
-            // skip using this key.
-            // In ECv1 the expiration is optional because it is assumed that the caller is
-            // respecting the HTTP cache headers and not using the trustedSigningKeysJson that are
-            // expired according to the headers.
-            continue;
-          }
-
-          senderVerifyingKeys.add(
-              PaymentMethodTokenUtil.x509EcPublicKey(key.get("keyValue").getAsString()));
-        }
-      }
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
-    }
-    if (senderVerifyingKeys.isEmpty()) {
-      throw new GeneralSecurityException("no trusted keys are available for this protocol version");
-    }
-    return senderVerifyingKeys;
-  }
-
-  private interface SenderVerifyingKeysProvider {
-    List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException;
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKem.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKem.java
deleted file mode 100644
index 47d322b..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKem.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import java.security.GeneralSecurityException;
-
-/**
- * Interface for recipient's key encapsulation mechanism (KEM).
- *
- * <p>Google Pay's tokens are encrypted using ECIES which is a hybrid encryption mode consisting of
- * two steps: key encapsulation mechanisam (KEM) using Elliptic Curve Diffie Hellman (ECDH) and HKDF
- * and data encapsulation mechanism (DEM) using AES-CTR-HMAC.
- *
- * <p>During encryption, the KEM step takes the recipient's public key and produces a DEM key and an
- * ephemeral public key. The DEM key is then used to encrypt the credit card data, and the ephemeral
- * public key is sent as the <b>ephemeralPublicKey</b> field of the payload.
- *
- * <p>To decrypt, the recipient must use their private key to compute an ECDH shared secret from the
- * ephemeral public key, and from that derive the DEM key using HKDF. If the recipient keeps the
- * private key in a HSM, they cannot load the private key in Tink, but they can implement this
- * interface and configure Tink to use their custom KEM implementation with {@link
- * PaymentMethodTokenRecipient.Builder#addRecipientKem}.
- *
- * @see <a href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment
- *     Method Token standard</a>
- * @since 1.1.0
- */
-public interface PaymentMethodTokenRecipientKem {
-  /**
-   * Computes a shared secret from the {@code ephemeralPublicKey}, using ECDH.
-   *
-   * <p>{@code ephemeralPublicKey} is a point on the elliptic curve defined in the <a
-   * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
-   * Token standard</a>, encoded in uncompressed point format. In version ECv1 and ECv2 of the
-   * standard, the elliptic curve is NIST P-256.
-   *
-   * <p>Note that you only needs to compute the shared secret, but you don't have to derive the DEM
-   * key with HKDF -- that process is handled by Tink.
-   */
-  byte[] computeSharedSecret(final byte[] ephemeralPublicKey) throws GeneralSecurityException;
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKeyGen.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKeyGen.java
deleted file mode 100644
index 2713854..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKeyGen.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPublicKey;
-
-/**
- * A util that generates key pairs for the recipient side of <a
- * href="https://developers.google.com/android-pay/integration/payment-token-cryptography">Google
- * Payment Method Token</a>.
- *
- * <h3>Usage</h3>
- *
- * <pre>
- * bazel build apps/paymentmethodtoken/...
- * ./bazel-bin/apps/paymentmethodtoken/recipientkeygen
- * </pre>
- *
- * <p>Running that command will generate a fresh key pair. The private/public key can be found in
- * private_key.bin/public_key.bin. The content of private_key.bin can be passed to {@link
- * PaymentMethodTokenRecipient.Builder#addRecipientPrivateKey} and the content of public_key.bin can
- * be passed to {@link PaymentMethodTokenSender.Builder#rawUncompressedRecipientPublicKey}.
- */
-public final class PaymentMethodTokenRecipientKeyGen {
-  private static final String PRIVATE_KEY_FILE = "private_key.bin";
-
-  private static final String PUBLIC_KEY_FILE = "public_key.bin";
-
-  private static void generateKey() throws GeneralSecurityException, IOException {
-    KeyPair keyPair = EllipticCurves.generateKeyPair(PaymentMethodTokenConstants.P256_CURVE_TYPE);
-    writeBase64(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded());
-
-    ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
-    writeBase64(
-        PUBLIC_KEY_FILE,
-        EllipticCurves.pointEncode(
-            PaymentMethodTokenConstants.P256_CURVE_TYPE,
-            PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-            publicKey.getW()));
-  }
-
-  private static void writeBase64(String pathname, byte[] content) throws IOException {
-    File out = new File(pathname);
-    if (out.exists()) {
-      System.out.println("Please make sure that " + pathname + " does not exist.");
-      System.exit(-1);
-    }
-    FileOutputStream stream = new FileOutputStream(out);
-    stream.write(Base64.encode(content, Base64.DEFAULT | Base64.NO_WRAP));
-    stream.close();
-  }
-
-  public static void main(String[] args) throws GeneralSecurityException, IOException {
-    System.out.println("Generating key....");
-    generateKey();
-    System.out.println("done.");
-  }
-
-  private PaymentMethodTokenRecipientKeyGen() {}
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSender.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSender.java
deleted file mode 100644
index 25d0fe2..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSender.java
+++ /dev/null
@@ -1,335 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaSignJce;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-
-/**
- * An implementation of the sender side of <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- *
- * <h3>Warning</h3>
- *
- * <p>This implementation supports only versions {@code ECv1} and {@code ECv2}.
- *
- * <h3>Usage</h3>
- *
- * <pre>{@code
- * PaymentMethodTokenSender sender = new PaymentMethodTokenSender.Builder()
- *    .senderId(senderId)
- *    .senderSigningKey(senderPrivateKey)
- *    .recipientId(recipientId)
- *    .recipientPublicKey(recipientPublicKey)
- *    .build();
- * String plaintext = "blah";
- * String ciphertext = sender.seal(plaintext);
- * }</pre>
- *
- * @see <a href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment
- *     Method Token standard</a>
- * @since 1.0.0
- */
-public final class PaymentMethodTokenSender {
-  private final String protocolVersion;
-  private final ProtocolVersionConfig protocolVersionConfig;
-  private final PublicKeySign signer;
-  private final String senderIntermediateCert;
-  private final String senderId;
-  private final String recipientId;
-
-  private HybridEncrypt hybridEncrypter;
-
-  PaymentMethodTokenSender(Builder builder) throws GeneralSecurityException {
-    switch (builder.protocolVersion) {
-      case PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1:
-        validateV1(builder);
-        break;
-      case PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2:
-        validateV2(builder);
-        break;
-      case PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY:
-        validateV2SigningOnly(builder);
-        break;
-      default:
-        throw new IllegalArgumentException("invalid version: " + builder.protocolVersion);
-    }
-
-    this.protocolVersion = builder.protocolVersion;
-    this.protocolVersionConfig = ProtocolVersionConfig.forProtocolVersion(protocolVersion);
-    this.signer =
-        new EcdsaSignJce(
-            builder.senderIntermediateSigningKey != null
-                ? builder.senderIntermediateSigningKey
-                : builder.senderSigningKey,
-            PaymentMethodTokenConstants.ECDSA_HASH_SHA256,
-            EcdsaEncoding.DER);
-    this.senderId = builder.senderId;
-    if (protocolVersionConfig.isEncryptionRequired) {
-      this.hybridEncrypter =
-          new PaymentMethodTokenHybridEncrypt(builder.recipientPublicKey, protocolVersionConfig);
-    }
-    if (builder.recipientId == null) {
-      throw new IllegalArgumentException("must set recipient Id using Builder.recipientId");
-    }
-    this.recipientId = builder.recipientId;
-    this.senderIntermediateCert = builder.senderIntermediateCert;
-  }
-
-  /**
-   * Builder for {@link PaymentMethodTokenSender}.
-   *
-   * @since 1.0.0
-   */
-  public static class Builder {
-    private String protocolVersion = PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1;
-    private String senderId = PaymentMethodTokenConstants.GOOGLE_SENDER_ID;
-    private String recipientId = null;
-    private ECPrivateKey senderSigningKey = null;
-    private ECPrivateKey senderIntermediateSigningKey = null;
-    private String senderIntermediateCert = null;
-    private ECPublicKey recipientPublicKey = null;
-
-    public Builder() {}
-
-    /** Sets the protocolVersion. */
-    public Builder protocolVersion(String val) {
-      protocolVersion = val;
-      return this;
-    }
-
-    /** Sets the sender Id. */
-    public Builder senderId(String val) {
-      senderId = val;
-      return this;
-    }
-
-    /** Sets the recipient Id. */
-    public Builder recipientId(String val) {
-      recipientId = val;
-      return this;
-    }
-
-    /**
-     * Sets the signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder senderSigningKey(String val) throws GeneralSecurityException {
-      senderSigningKey = PaymentMethodTokenUtil.pkcs8EcPrivateKey(val);
-      return this;
-    }
-
-    public Builder senderSigningKey(ECPrivateKey val) throws GeneralSecurityException {
-      senderSigningKey = val;
-      return this;
-    }
-
-    /**
-     * Sets the intermediate signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     *
-     * @since 1.1.0
-     */
-    public Builder senderIntermediateSigningKey(String val) throws GeneralSecurityException {
-      return senderIntermediateSigningKey(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val));
-    }
-
-    /**
-     * Sets the intermediate signing key of the sender.
-     *
-     * @since 1.1.0
-     */
-    public Builder senderIntermediateSigningKey(ECPrivateKey val) throws GeneralSecurityException {
-      senderIntermediateSigningKey = val;
-      return this;
-    }
-
-    /**
-     * JSON containing sender intermediate signing key and a signature of it by the sender signing
-     * key.
-     *
-     * <p>This can be generated by {@link SenderIntermediateCertFactory}.
-     *
-     * @since 1.1.0
-     */
-    public Builder senderIntermediateCert(String val) throws GeneralSecurityException {
-      this.senderIntermediateCert = val;
-      return this;
-    }
-
-    /**
-     * Sets the encryption public key of the recipient.
-     *
-     * <p>The public key is a base64 (no wrapping, padded) version of the key encoded in ASN.1 type
-     * SubjectPublicKeyInfo defined in the X.509 standard.
-     */
-    public Builder recipientPublicKey(String val) throws GeneralSecurityException {
-      recipientPublicKey = PaymentMethodTokenUtil.x509EcPublicKey(val);
-      return this;
-    }
-
-    public Builder recipientPublicKey(ECPublicKey val) throws GeneralSecurityException {
-      recipientPublicKey = val;
-      return this;
-    }
-
-    /**
-     * Sets the encryption public key of the recipient.
-     *
-     * <p>The public key must be formatted as base64 encoded uncompressed point format. This format
-     * is described in more detail in "Public Key Cryptography For The Financial Services Industry:
-     * The Elliptic Curve Digital Signature Algorithm (ECDSA)", ANSI X9.62, 1998
-     */
-    public Builder rawUncompressedRecipientPublicKey(String val) throws GeneralSecurityException {
-      recipientPublicKey = PaymentMethodTokenUtil.rawUncompressedEcPublicKey(val);
-      return this;
-    }
-
-    public PaymentMethodTokenSender build() throws GeneralSecurityException {
-      return new PaymentMethodTokenSender(this);
-    }
-  }
-
-  /** Seals the input message according to the Payment Method Token specification. */
-  public String seal(final String message) throws GeneralSecurityException {
-    if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-        || protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        || protocolVersion.equals(
-            PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)) {
-      return sealV1OrV2(message);
-    }
-    throw new GeneralSecurityException("Unsupported version: " + protocolVersion);
-  }
-
-  private String sealV1OrV2(final String message) throws GeneralSecurityException {
-    String signedMessage =
-        protocolVersionConfig.isEncryptionRequired
-            ? new String(
-                hybridEncrypter.encrypt(
-                    message.getBytes(UTF_8), PaymentMethodTokenConstants.GOOGLE_CONTEXT_INFO_ECV1),
-                UTF_8)
-            : message;
-    return signV1OrV2(signedMessage);
-  }
-
-  static String jsonEncodeSignedMessage(
-      String message, String protocolVersion, byte[] signature, String senderIntermediateCert)
-      throws GeneralSecurityException {
-    try {
-      JsonObject result = new JsonObject();
-      result.addProperty(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY, Base64.encode(signature));
-      if (senderIntermediateCert != null) {
-        result.add(
-            PaymentMethodTokenConstants.JSON_INTERMEDIATE_SIGNING_KEY,
-            JsonParser.parseString(senderIntermediateCert).getAsJsonObject());
-      }
-      result.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, protocolVersion);
-      result.addProperty(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY, message);
-      StringWriter stringWriter = new StringWriter();
-      JsonWriter jsonWriter = new JsonWriter(stringWriter);
-      jsonWriter.setHtmlSafe(true);
-      Streams.write(result, jsonWriter);
-      return stringWriter.toString();
-    } catch (JsonParseException | IllegalStateException | IOException e) {
-      throw new GeneralSecurityException("cannot seal; JSON error", e);
-    }
-  }
-
-  private String signV1OrV2(String message) throws GeneralSecurityException {
-    byte[] toSignBytes =
-        PaymentMethodTokenUtil.toLengthValue(
-            // The order of the parameters matters.
-            senderId, recipientId, protocolVersion, message);
-    byte[] signature = signer.sign(toSignBytes);
-    return jsonEncodeSignedMessage(message, protocolVersion, signature, senderIntermediateCert);
-  }
-
-  private static void validateV1(Builder builder) {
-    // ECv1 signed payloads directly.
-    if (builder.senderSigningKey == null) {
-      throw new IllegalArgumentException(
-          "must set sender's signing key using Builder.senderSigningKey");
-    }
-    if (builder.senderIntermediateSigningKey != null) {
-      throw new IllegalArgumentException(
-          "must not set sender's intermediate signing key using "
-              + "Builder.senderIntermediateSigningKey");
-    }
-    if (builder.senderIntermediateCert != null) {
-      throw new IllegalArgumentException(
-          "must not set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert");
-    }
-    if (builder.recipientPublicKey == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's public key using Builder.recipientPublicKey");
-    }
-  }
-
-  private static void validateV2(Builder builder) {
-    validateIntermediateSigningKeys(builder);
-    if (builder.recipientPublicKey == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's public key using Builder.recipientPublicKey");
-    }
-  }
-
-  private static void validateV2SigningOnly(Builder builder) {
-    validateIntermediateSigningKeys(builder);
-    if (builder.recipientPublicKey != null) {
-      throw new IllegalArgumentException(
-          "must not set recipient's public key using Builder.recipientPublicKey");
-    }
-  }
-
-  private static void validateIntermediateSigningKeys(Builder builder) {
-    // ECv2 and newer protocols use an intermediate signing key.
-    if (builder.senderSigningKey != null) {
-      throw new IllegalArgumentException(
-          "must not set sender's signing key using Builder.senderSigningKey");
-    }
-    if (builder.senderIntermediateSigningKey == null) {
-      throw new IllegalArgumentException(
-          "must set sender's intermediate signing key using "
-              + "Builder.senderIntermediateSigningKey");
-    }
-    if (builder.senderIntermediateCert == null) {
-      throw new IllegalArgumentException(
-          "must set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert");
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenUtil.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenUtil.java
deleted file mode 100644
index 185038d..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenUtil.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EngineFactory;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Various helpers. */
-final class PaymentMethodTokenUtil {
-  static ECPublicKey rawUncompressedEcPublicKey(String rawUncompressedPublicKey)
-      throws GeneralSecurityException {
-    return EllipticCurves.getEcPublicKey(
-        PaymentMethodTokenConstants.P256_CURVE_TYPE,
-        PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-        Base64.decode(rawUncompressedPublicKey));
-  }
-
-  static ECPublicKey x509EcPublicKey(String x509PublicKey) throws GeneralSecurityException {
-    return EllipticCurves.getEcPublicKey(Base64.decode(x509PublicKey));
-  }
-
-  static ECPrivateKey pkcs8EcPrivateKey(String pkcs8PrivateKey) throws GeneralSecurityException {
-    return EllipticCurves.getEcPrivateKey(Base64.decode(pkcs8PrivateKey));
-  }
-
-  static byte[] toLengthValue(String... chunks) throws GeneralSecurityException {
-    byte[] out = new byte[0];
-    for (String chunk : chunks) {
-      byte[] bytes = chunk.getBytes(StandardCharsets.UTF_8);
-      out = Bytes.concat(out, Bytes.intToByteArray(4, bytes.length));
-      out = Bytes.concat(out, bytes);
-    }
-    return out;
-  }
-
-  static byte[] aesCtr(final byte[] encryptionKey, final byte[] message)
-      throws GeneralSecurityException {
-    Cipher cipher = EngineFactory.CIPHER.getInstance(PaymentMethodTokenConstants.AES_CTR_ALGO);
-    cipher.init(
-        Cipher.ENCRYPT_MODE,
-        new SecretKeySpec(encryptionKey, "AES"),
-        new IvParameterSpec(PaymentMethodTokenConstants.AES_CTR_ZERO_IV));
-    return cipher.doFinal(message);
-  }
-
-  static byte[] hmacSha256(final byte[] macKey, final byte[] encryptedMessage)
-      throws GeneralSecurityException {
-    SecretKeySpec key = new SecretKeySpec(macKey, PaymentMethodTokenConstants.HMAC_SHA256_ALGO);
-    Mac mac = EngineFactory.MAC.getInstance(PaymentMethodTokenConstants.HMAC_SHA256_ALGO);
-    mac.init(key);
-    return mac.doFinal(encryptedMessage);
-  }
-
-  private PaymentMethodTokenUtil() {}
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactory.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactory.java
deleted file mode 100644
index 0e3bfc7..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactory.java
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaSignJce;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Creates a signed certificate with the intermediate signing keys used by the sender in certain
- * protocol versions.
- *
- * @since 1.1.0
- */
-public class SenderIntermediateCertFactory {
-  private final List<PublicKeySign> signers;
-  private final String intermediateSigningKey;
-  private final String protocolVersion;
-  private final String senderId;
-  private final long expiration;
-
-  private SenderIntermediateCertFactory(
-      String protocolVersion,
-      String senderId,
-      List<ECPrivateKey> senderSigningKeys,
-      String intermediateSigningKey,
-      long expiration)
-      throws GeneralSecurityException {
-    if (!ProtocolVersionConfig.forProtocolVersion(protocolVersion)
-        .supportsIntermediateSigningKeys) {
-      throw new IllegalArgumentException("invalid version: " + protocolVersion);
-    }
-    if (senderSigningKeys.isEmpty()) {
-      throw new IllegalArgumentException(
-          "must add at least one sender's signing key using Builder.addSenderSigningKey");
-    }
-    if (expiration == 0) {
-      throw new IllegalArgumentException("must set expiration using Builder.expiration");
-    }
-    if (expiration < 0) {
-      throw new IllegalArgumentException("invalid negative expiration");
-    }
-    this.protocolVersion = protocolVersion;
-    this.senderId = senderId;
-    this.signers = new ArrayList<>();
-    for (ECPrivateKey senderSigningKey : senderSigningKeys) {
-      this.signers.add(
-          new EcdsaSignJce(
-              senderSigningKey, PaymentMethodTokenConstants.ECDSA_HASH_SHA256, EcdsaEncoding.DER));
-    }
-    this.intermediateSigningKey = intermediateSigningKey;
-    this.expiration = expiration;
-  }
-
-  /**
-   * Builder for {@link SenderIntermediateCertFactory}.
-   *
-   * @since 1.1.0
-   */
-  public static class Builder {
-    private final List<ECPrivateKey> senderSigningKeys = new ArrayList<>();
-    private String intermediateSigningKey;
-    private String protocolVersion = PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2;
-    private String senderId = PaymentMethodTokenConstants.GOOGLE_SENDER_ID;
-    private long expiration;
-
-    public Builder() {}
-
-    /** Sets the protocolVersion. */
-    public Builder protocolVersion(String val) {
-      protocolVersion = val;
-      return this;
-    }
-
-    /** Sets the sender Id. */
-    public Builder senderId(String val) {
-      senderId = val;
-      return this;
-    }
-
-    /** Sets the expiration in millis since epoch. */
-    public Builder expiration(long millisSinceEpoch) {
-      expiration = millisSinceEpoch;
-      return this;
-    }
-
-    /**
-     * Adds a signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder addSenderSigningKey(String val) throws GeneralSecurityException {
-      return addSenderSigningKey(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val));
-    }
-
-    /**
-     * Adds a signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder addSenderSigningKey(ECPrivateKey val) throws GeneralSecurityException {
-      this.senderSigningKeys.add(val);
-      return this;
-    }
-
-    /**
-     * Sets the intermediate signing key being signed.
-     *
-     * <p>The public key specified here is a base64 (no wrapping, padded) version of the key encoded
-     * in ASN.1 type SubjectPublicKeyInfo defined in the X.509 standard.
-     */
-    public Builder senderIntermediateSigningKey(String val) throws GeneralSecurityException {
-      // Parsing to validate the format
-      PaymentMethodTokenUtil.x509EcPublicKey(val);
-      intermediateSigningKey = val;
-      return this;
-    }
-
-    public SenderIntermediateCertFactory build() throws GeneralSecurityException {
-      return new SenderIntermediateCertFactory(
-          protocolVersion, senderId, senderSigningKeys, intermediateSigningKey, expiration);
-    }
-  }
-
-  static String jsonEncodeSignedKey(String intermediateSigningKey, long expiration) {
-    try {
-      JsonObject jsonObj = new JsonObject();
-      jsonObj.addProperty(PaymentMethodTokenConstants.JSON_KEY_VALUE_KEY, intermediateSigningKey);
-      jsonObj.addProperty(
-          PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY, Long.toString(expiration));
-      StringWriter stringWriter = new StringWriter();
-      JsonWriter jsonWriter = new JsonWriter(stringWriter);
-      jsonWriter.setHtmlSafe(true);
-      Streams.write(jsonObj, jsonWriter);
-      return stringWriter.toString();
-    } catch (JsonParseException | IllegalStateException | IOException e) {
-      throw new AssertionError("Failed to perform JSON encoding", e);
-    }
-  }
-
-  static String jsonEncodeCertificate(String signedKey, ArrayList<String> signatures) {
-    try {
-      JsonArray jsonSignatures = new JsonArray();
-      for (String signature : signatures) {
-        jsonSignatures.add(signature);
-      }
-      JsonObject result = new JsonObject();
-      result.addProperty(PaymentMethodTokenConstants.JSON_SIGNED_KEY_KEY, signedKey);
-      result.add(PaymentMethodTokenConstants.JSON_SIGNATURES_KEY, jsonSignatures);
-      StringWriter stringWriter = new StringWriter();
-      JsonWriter jsonWriter = new JsonWriter(stringWriter);
-      jsonWriter.setHtmlSafe(true);
-      Streams.write(result, jsonWriter);
-      return stringWriter.toString();
-    } catch (JsonParseException | IllegalStateException | IOException e) {
-      throw new AssertionError("Failed to perform JSON encoding", e);
-    }
-  }
-
-  /**
-   * Creates the certificate.
-   *
-   * <p>This will return a serialized JSONObject in the following format:
-   *
-   * <pre>
-   *   {
-   *     // {
-   *     //   // A string that identifies this cert
-   *     //   "keyValue": "ZXBoZW1lcmFsUHVibGljS2V5"
-   *     //   // string (UTC milliseconds since epoch)
-   *     //   "expiration": "1520836260646",
-   *     // }
-   *     "signedKey": "... serialized JSON shown in comment above ...",
-   *     "signatures": ["signature1", "signature2", ...],
-   *   }
-   * </pre>
-   */
-  public String create() throws GeneralSecurityException {
-    String signedKey = jsonEncodeSignedKey(intermediateSigningKey, expiration);
-    byte[] toSignBytes =
-        PaymentMethodTokenUtil.toLengthValue(
-            // The order of the parameters matters.
-            senderId, protocolVersion, signedKey);
-    ArrayList<String> signatures = new ArrayList<>();
-    for (PublicKeySign signer : signers) {
-      byte[] signature = signer.sign(toSignBytes);
-      signatures.add(Base64.encode(signature));
-    }
-    return jsonEncodeCertificate(signedKey, signatures);
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/BUILD.bazel b/apps/paymentmethodtoken/src/test/BUILD.bazel
deleted file mode 100644
index c6f52d3..0000000
--- a/apps/paymentmethodtoken/src/test/BUILD.bazel
+++ /dev/null
@@ -1,43 +0,0 @@
-load("@tink_java//tools:gen_java_test_rules.bzl", "gen_java_test_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-# Tests
-
-java_library(
-    name = "generator_test",
-    testonly = 1,
-    srcs = glob([
-        "**/*.java",
-    ]),
-    deps = [
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:google_payments_public_keys_manager",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_constants",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_decrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_encrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient_kem",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_sender",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_util",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:sender_intermediate_cert_factory",
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:com_google_http_client_google_http_client",
-        "@maven//:joda_time_joda_time",
-        "@maven//:junit_junit",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-    ],
-)
-
-gen_java_test_rules(
-    test_files = glob(["**/*Test.java"],
-    ),
-    deps = [
-        ":generator_test",
-    ],
-)
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeyManagerTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeyManagerTest.java
deleted file mode 100644
index 59cf9ad..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeyManagerTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.fail;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link GooglePaymentsPublicKeysManager}. */
-@RunWith(JUnit4.class)
-public class GooglePaymentsPublicKeyManagerTest {
-  @Test
-  public void builderShouldReturnSingletonsWhenMatching() {
-    assertSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION,
-        new GooglePaymentsPublicKeysManager.Builder().build());
-    assertSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_TEST,
-        new GooglePaymentsPublicKeysManager.Builder()
-            .setKeysUrl(GooglePaymentsPublicKeysManager.KEYS_URL_TEST)
-            .build());
-  }
-
-  @Test
-  public void builderShouldReturnDifferentInstanceWhenNotMatchingSingletons() {
-    assertNotSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION,
-        new GooglePaymentsPublicKeysManager.Builder().setKeysUrl("https://abc").build());
-    assertNotSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_TEST,
-        new GooglePaymentsPublicKeysManager.Builder().setKeysUrl("https://abc").build());
-  }
-
-  @Test
-  public void builderShouldThrowIllegalArgumentExceptionWhenUrlIsNotHttps() {
-    try {
-      new GooglePaymentsPublicKeysManager.Builder().setKeysUrl("http://abc").build();
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException ex) {
-      // expected.
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodJsonEncodingTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodJsonEncodingTest.java
deleted file mode 100644
index 70b8ed4..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodJsonEncodingTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-
-import java.util.ArrayList;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for the exact Json-Encoding produced.
- *
- * These tests test implementation details. Do not depend on the this. For example, the particular
- * ordering of the elements or the particular character escaping used may change in the future.
- * */
-@RunWith(JUnit4.class)
-public final class PaymentMethodJsonEncodingTest {
-
-  @Test
-  public void testExactOutputOfJsonEncodeCiphertext() throws Exception {
-    byte[] ciphertext = "CiPhErTeXt".getBytes(UTF_8);
-    byte[] tag = "taaag".getBytes(UTF_8);
-    byte[] ephemeralPublicKey = "ephemeral Public Key".getBytes(UTF_8);
-
-    String jsonEncodedCiphertext =
-        PaymentMethodTokenHybridEncrypt.jsonEncodeCiphertext(ciphertext, tag, ephemeralPublicKey);
-
-    // JSONObject uses a HashMap, where the ordering is not defined. The ordering is however
-    // deterministic. And for jsonEncodeCiphertext, the order happens to be first "encryptedMessage"
-    // then "ephemeralPublicKey", and finally "tag". Also, JSONObject uses HTML-safe encoding.
-    assertEquals(
-        "{\"encryptedMessage\":\"Q2lQaEVyVGVYdA\\u003d\\u003d\",\"ephemeralPublicKey\":"
-            + "\"ZXBoZW1lcmFsIFB1YmxpYyBLZXk\\u003d\",\"tag\":\"dGFhYWc\\u003d\"}",
-        jsonEncodedCiphertext);
-  }
-
-  @Test
-  public void testExactOutputOfJsonEncodeSignedMessage() throws Exception {
-    String senderIntermediateCert =
-        "{\"signedKey\":\"{\\\"keyValue\\\":\\\"abcde\\\\u003d\\\\u003d\\\",\\\"keyExpiration\\\""
-            + ":\\\"1615299372858\\\"}\",\"signatures\":[\"fghijkl\\u003d\"]}";
-    String version = "ECv1";
-    String message =
-        "{\"encryptedMessage\":\"Q2lQaEVyVGVYdA\\u003d\\u003d\",\"ephemeralPublicKey\":\"ZXBoZW1l"
-            + "cmFsIFB1YmxpYyBLZXk\\u003d\",\"tag\":\"dGFhYWc\\u003d\"}";
-    byte[] signature = "the signature".getBytes(UTF_8);
-
-    String jsonEncodedSignedMessage =
-        PaymentMethodTokenSender.jsonEncodeSignedMessage(
-            message, version, signature, senderIntermediateCert);
-
-    String expected =
-        "{\"signature\":\"dGhlIHNpZ25hdHVyZQ\\u003d\\u003d\",\"intermediateSigningKey\":{\"signe"
-            + "dKey\":\"{\\\"keyValue\\\":\\\"abcde\\\\u003d\\\\u003d\\\",\\\"keyExpiration\\\":"
-            + "\\\"1615299372858\\\"}\",\"signatures\":[\"fghijkl\\u003d\"]},\"protocolVersion\""
-            + ":\"ECv1\",\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"Q2lQaEVyVGVYdA\\\\u00"
-            + "3d\\\\u003d\\\",\\\"ephemeralPublicKey\\\":\\\"ZXBoZW1lcmFsIFB1YmxpYyBLZXk\\\\u00"
-            + "3d\\\",\\\"tag\\\":\\\"dGFhYWc\\\\u003d\\\"}\"}";
-    assertEquals(expected, jsonEncodedSignedMessage);
-
-    String expected2 =
-        "{\"signature\":\"dGhlIHNpZ25hdHVyZQ\\u003d\\u003d\",\"protocolVersion\":\"ECv1\",\"sign"
-            + "edMessage\":\"{\\\"encryptedMessage\\\":\\\"Q2lQaEVyVGVYdA\\\\u003d\\\\u003d\\\","
-            + "\\\"ephemeralPublicKey\\\":\\\"ZXBoZW1lcmFsIFB1YmxpYyBLZXk\\\\u003d\\\",\\\"tag\\"
-            + "\":\\\"dGFhYWc\\\\u003d\\\"}\"}";
-
-    String jsonEncodedSignedMessage2 =
-        PaymentMethodTokenSender.jsonEncodeSignedMessage(message, version, signature, null);
-    assertEquals(expected2, jsonEncodedSignedMessage2);
-  }
-
-  @Test
-  public void testExactOutputOfJsonEncodedSignedKey() throws Exception {
-    String intermediateSigningKey =
-        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSA"
-            + "M43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-    long expiration = 1520836260646L;
-    assertEquals(
-        "{\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1"
-            + "TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\",\"keyExpiration\""
-            + ":\"1520836260646\"}",
-        SenderIntermediateCertFactory.jsonEncodeSignedKey(intermediateSigningKey, expiration));
-  }
-
-  @Test
-  public void testExactOutputOfJsonEncodeCertificate() throws Exception {
-    String intermediateSigningKey =
-        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSA"
-            + "M43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-    long expiration = 1520836260646L;
-    String signedKey =
-        SenderIntermediateCertFactory.jsonEncodeSignedKey(intermediateSigningKey, expiration);
-    ArrayList<String> signatures = new ArrayList<>();
-    signatures.add("iTzFvzFRxyCw==");
-    signatures.add("abcde090/+==");
-    signatures.add("xyz");
-    String expected =
-        "{\"signedKey\":\"{\\\"keyValue\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE"
-            + "/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRx"
-            + "yCw\\\\u003d\\\\u003d\\\",\\\"keyExpiration\\\":\\\"1520836260646\\\"}\",\"signatur"
-            + "es\":[\"iTzFvzFRxyCw\\u003d\\u003d\",\"abcde090/+\\u003d\\u003d\",\"xyz\"]}";
-    assertEquals(
-        expected, SenderIntermediateCertFactory.jsonEncodeCertificate(signedKey, signatures));
-  }
-
-  @Test
-  public void testExactOutputOfWeirdJsonEncodeCertificate() throws Exception {
-    String intermediateSigningKey =
-        "\"\\==";
-    long expiration = -123;
-    String signedKey =
-        SenderIntermediateCertFactory.jsonEncodeSignedKey(intermediateSigningKey, expiration);
-    ArrayList<String> signatures = new ArrayList<>();
-    signatures.add("");
-    signatures.add("\\\"/+==");
-    String expected =
-        "{\"signedKey\":\"{\\\"keyValue\\\":\\\"\\\\\\\"\\\\\\\\\\\\u003d\\\\u003d"
-            + "\\\",\\\"keyExpiration\\\":\\\"-123\\\"}\",\"signatures\":[\"\",\"\\\\\\\"/+\\u003d"
-            + "\\u003d\"]}";
-    assertEquals(
-        expected, SenderIntermediateCertFactory.jsonEncodeCertificate(signedKey, signatures));
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecryptTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecryptTest.java
deleted file mode 100644
index 387c3aa..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecryptTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Random;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECParameterSpec;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code PaymentMethodTokenHybridDecrypt}. */
-@RunWith(JUnit4.class)
-public class PaymentMethodTokenHybridDecryptTest {
-  @Test
-  public void testModifyDecrypt() throws Exception {
-    ECParameterSpec spec = EllipticCurves.getNistP256Params();
-    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
-    keyGen.initialize(spec);
-    KeyPair recipientKey = keyGen.generateKeyPair();
-    ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
-    ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
-
-    HybridEncrypt hybridEncrypt =
-        new PaymentMethodTokenHybridEncrypt(recipientPublicKey, ProtocolVersionConfig.EC_V1);
-    HybridDecrypt hybridDecrypt =
-        new PaymentMethodTokenHybridDecrypt(recipientPrivateKey, ProtocolVersionConfig.EC_V1);
-    testModifyDecrypt(hybridEncrypt, hybridDecrypt);
-  }
-
-  public void testModifyDecrypt(HybridEncrypt hybridEncrypt, HybridDecrypt hybridDecrypt)
-      throws Exception {
-    byte[] plaintext = Random.randBytes(111);
-    byte[] context = "context info".getBytes(UTF_8);
-
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, context);
-    byte[] decrypted = hybridDecrypt.decrypt(ciphertext, context);
-    assertArrayEquals(plaintext, decrypted);
-
-    JsonObject json = JsonParser.parseString(new String(ciphertext, UTF_8)).getAsJsonObject();
-
-    // Modify public key.
-    byte[] kem =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY).getAsString());
-    for (int bytes = 0; bytes < kem.length; bytes++) {
-      for (int bit = 0; bit < 8; bit++) {
-        byte[] modifiedPublicKey = Arrays.copyOf(kem, kem.length);
-        modifiedPublicKey[bytes] ^= (byte) (1 << bit);
-        json.addProperty(
-            PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY,
-            Base64.encode(modifiedPublicKey));
-        try {
-          hybridDecrypt.decrypt(json.toString().getBytes(UTF_8), context);
-          fail("Invalid ciphertext, should have thrown exception");
-        } catch (GeneralSecurityException expected) {
-          // Expected
-        }
-      }
-    }
-
-    // Modify payload.
-    byte[] payload =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY).getAsString());
-    for (int bytes = 0; bytes < payload.length; bytes++) {
-      for (int bit = 0; bit < 8; bit++) {
-        byte[] modifiedPayload = Arrays.copyOf(payload, payload.length);
-        modifiedPayload[bytes] ^= (byte) (1 << bit);
-        json.addProperty(
-            PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY, Base64.encode(modifiedPayload));
-        try {
-          hybridDecrypt.decrypt(json.toString().getBytes(UTF_8), context);
-          fail("Invalid ciphertext, should have thrown exception");
-        } catch (GeneralSecurityException expected) {
-          // Expected
-        }
-      }
-    }
-
-    // Modify context.
-    try {
-      hybridDecrypt.decrypt(ciphertext, Arrays.copyOf(context, context.length - 1));
-      fail("Invalid context, should have thrown exception");
-    } catch (GeneralSecurityException expected) {
-      // Expected
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncryptTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncryptTest.java
deleted file mode 100644
index 3fe9459..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncryptTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Random;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECParameterSpec;
-import java.util.Set;
-import java.util.TreeSet;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code PaymentMethodTokenHybridEncrypt}. */
-@RunWith(JUnit4.class)
-public class PaymentMethodTokenHybridEncryptTest {
-  @Test
-  public void testBasicMultipleEncrypts() throws Exception {
-    ECParameterSpec spec = EllipticCurves.getNistP256Params();
-    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
-    keyGen.initialize(spec);
-    KeyPair recipientKey = keyGen.generateKeyPair();
-    ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
-    ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
-
-    HybridEncrypt hybridEncrypt =
-        new PaymentMethodTokenHybridEncrypt(recipientPublicKey, ProtocolVersionConfig.EC_V1);
-    HybridDecrypt hybridDecrypt =
-        new PaymentMethodTokenHybridDecrypt(recipientPrivateKey, ProtocolVersionConfig.EC_V1);
-    testBasicMultipleEncrypts(hybridEncrypt, hybridDecrypt);
-  }
-
-  public void testBasicMultipleEncrypts(HybridEncrypt hybridEncrypt, HybridDecrypt hybridDecrypt)
-      throws Exception {
-    byte[] plaintext = Random.randBytes(111);
-    byte[] context = "context info".getBytes(StandardCharsets.UTF_8);
-    // Makes sure that the encryption is randomized.
-    Set<String> ciphertexts = new TreeSet<String>();
-    for (int j = 0; j < 100; j++) {
-      byte[] ciphertext = hybridEncrypt.encrypt(plaintext, context);
-      if (ciphertexts.contains(new String(ciphertext, StandardCharsets.UTF_8))) {
-        throw new GeneralSecurityException("Encryption is not randomized");
-      }
-      ciphertexts.add(new String(ciphertext, StandardCharsets.UTF_8));
-      byte[] decrypted = hybridDecrypt.decrypt(ciphertext, context);
-      assertArrayEquals(plaintext, decrypted);
-    }
-    assertEquals(100, ciphertexts.size());
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientTest.java
deleted file mode 100644
index 4bce9f5..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientTest.java
+++ /dev/null
@@ -1,1403 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.api.client.testing.http.MockHttpTransport;
-import com.google.api.client.testing.http.MockLowLevelHttpResponse;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import org.joda.time.Duration;
-import org.joda.time.Instant;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code PaymentMethodTokenRecipient}. */
-@RunWith(JUnit4.class)
-public class PaymentMethodTokenRecipientTest {
-
-  /**
-   * Sample merchant public key.
-   *
-   * <p>Corresponds to public key of {@link #MERCHANT_PRIVATE_KEY_PKCS8_BASE64}
-   *
-   * <p>Created with:
-   *
-   * <pre>
-   * openssl ec -in merchant-key.pem -pubout -text -noout 2> /dev/null | grep "pub:" -A5 \
-   *     | xxd -r -p | base64
-   * </pre>
-   */
-  private static final String MERCHANT_PUBLIC_KEY_BASE64 =
-      "BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y=";
-
-  /**
-   * Sample merchant private key.
-   *
-   * <p>Corresponds to the private key of {@link #MERCHANT_PUBLIC_KEY_BASE64}
-   *
-   * <pre>
-   * openssl pkcs8 -topk8 -inform PEM -outform PEM -in merchant-key.pem -nocrypt
-   * </pre>
-   */
-  private static final String MERCHANT_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCPSuFr4iSIaQprjj"
-          + "chHPyDu2NXFe0vDBoTpPkYaK9dehRANCAATnaFz/vQKuO90pxsINyVNWojabHfbx"
-          + "9qIJ6uD7Q7ZSxmtyo/Ez3/o2kDT8g0pIdyVIYktCsq65VoQIDWSh2Bdm";
-
-  /** An alternative merchant private key used during the tests. */
-  private static final String ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOUIzccyJ3rTx6SVm"
-          + "XrWdtwUP0NU26nvc8KIYw2GmYZKhRANCAAR5AjmTNAE93hQEQE+PryLlgr6Q7FXyN"
-          + "XoZRk+1Fikhq61mFhQ9s14MOwGBxd5O6Jwn/sdUrWxkYk3idtNEN1Rz";
-
-  /** Sample Google provided JSON with its public signing keys. */
-  private static final String GOOGLE_VERIFYING_PUBLIC_KEYS_JSON =
-      "{\n"
-          + "  \"keys\": [\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPYnHwS8uegWAewQtlxizmLFynw"
-          + "HcxRT1PK07cDA6/C4sXrVI1SzZCUx8U8S0LjMrT6ird/VW7be3Mz6t/srtRQ==\",\n"
-          + "      \"protocolVersion\": \"ECv1\"\n"
-          + "    },\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM"
-          + "43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2\"\n"
-          + "    },\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENXvYqxD5WayKYhuXQevdGdLA8i"
-          + "fV4LsRS2uKvFo8wwyiwgQHB9DiKzG6T/P1Fu9Bl7zWy/se5Dy4wk1mJoPuxg==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2SigningOnly\"\n"
-          + "    }\n"
-          + "  ]\n"
-          + "}";
-
-  /** Index within {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} of the ECv1 Google signing key. */
-  private static final int INDEX_OF_GOOGLE_SIGNING_EC_V1 = 0;
-
-  /** Index within {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} of the ECv2 Google signing key. */
-  private static final int INDEX_OF_GOOGLE_SIGNING_EC_V2 = 1;
-
-  /**
-   * Index within {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} of the ECv2SigningOnly Google signing
-   * key.
-   */
-  private static final int INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY = 2;
-
-  /**
-   * Sample Google private signing key for the ECv1 protocolVersion.
-   *
-   * <p>Corresponds to the ECv1 private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O"
-          + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF"
-          + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F";
-
-  /**
-   * Sample Google private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to ECv2 private key of the key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yR"
-          + "ydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google private signing key for the ECv2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to ECv2SigningOnly private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRi9hSdY+knJ08odnY"
-          + "tZFMRi7ZYeMoasAijLhD4GiQ1yhRANCAAQ1e9irEPlZrIpiG5dB690Z0sDy"
-          + "J9XguxFLa4q8WjzDDKLCBAcH0OIrMbpP8/UW70GXvNbL+x7kPLjCTWYmg+7G";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2SigningOnly protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-          "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8OaurwvbyYm8JWDgFPRTIDg0/"
-              + "kcQTFAQ4txi5IP0AyM1QiagwRhDUfjpqZkpw8xt/DXwyWYM0DdHqoeV"
-              + "TKqmYQ==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECv2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-          "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Jvpkq26tpZ0s"
-              + "TTZVh4teEI41SnJdmkBzM8VZ5ZirE2hRANCAATw5q6vC9vJibwlYOAU"
-              + "9FMgODT+RxBMUBDi3GLkg/QDIzVCJqDBGENR+OmpmSnDzG38NfDJZgz"
-              + "QN0eqh5VMqqZh";
-
-  private static final String RECIPIENT_ID = "someRecipient";
-
-  private static final String PLAINTEXT = "plaintext";
-
-  /**
-   * The result of {@link #PLAINTEXT} encrypted with {@link #MERCHANT_PRIVATE_KEY_PKCS8_BASE64} and
-   * signed with the only key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} using the ECv1
-   * protocolVersion.
-   */
-  private static final String CIPHERTEXT_EC_V1 =
-      "{"
-          + "\"protocolVersion\":\"ECv1\","
-          + "\"signedMessage\":"
-          + ("\"{"
-              + "\\\"tag\\\":\\\"ZVwlJt7dU8Plk0+r8rPF8DmPTvDiOA1UAoNjDV+SqDE\\\\u003d\\\","
-              + "\\\"ephemeralPublicKey\\\":\\\"BPhVspn70Zj2Kkgu9t8+ApEuUWsI/zos5whGCQBlgOkuYagOis7"
-              + "qsrcbQrcprjvTZO3XOU+Qbcc28FSgsRtcgQE\\\\u003d\\\","
-              + "\\\"encryptedMessage\\\":\\\"12jUObueVTdy\\\"}\",")
-          + "\"signature\":\"MEQCIDxBoUCoFRGReLdZ/cABlSSRIKoOEFoU3e27c14vMZtfAiBtX3pGMEpnw6mSAbnagC"
-          + "CgHlCk3NcFwWYEyxIE6KGZVA\\u003d\\u003d\"}";
-
-  private static final String ALTERNATE_PUBLIC_SIGNING_KEY =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEU8E6JppGKFG40r5dDU1idHRN52NuwsemFzXZh1oUqh3bGUPgPioH+RoW"
-          + "nmVSUQz1WfM2426w9f0GADuXzpUkcw==";
-
-  private static final class MyPaymentMethodTokenRecipientKem
-      implements PaymentMethodTokenRecipientKem {
-    private final ECPrivateKey privateKey;
-
-    public MyPaymentMethodTokenRecipientKem(String recipientPrivateKey)
-        throws GeneralSecurityException {
-      privateKey = PaymentMethodTokenUtil.pkcs8EcPrivateKey(recipientPrivateKey);
-    }
-
-    @Override
-    public byte[] computeSharedSecret(final byte[] ephemeralPublicKey)
-        throws GeneralSecurityException {
-      ECPublicKey publicKey =
-          EllipticCurves.getEcPublicKey(
-              privateKey.getParams(),
-              PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-              ephemeralPublicKey);
-      return EllipticCurves.computeSharedSecret(privateKey, publicKey);
-    }
-  }
-
-  @Test
-  public void testShouldDecryptECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldDecryptECV1WithNonStrictJsonEncoding() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-  String ciphertextEcV1WithNonStrictJsonEncoding =
-      "{"
-          + "# comment \n"   // python-style comment terminated with new line
-          + "protocolVersion:'ECv1',"   // protocolVersion has no quotes, ECv1 has single quotes
-          + "/* a comment */"   // c-style comment
-          + "\"signedMessage\"="  // use = instead of :
-          + "// another comment \n"   // c-style comment terminated with new line
-          + ("\"{"
-              + "\\\"tag\\\":\\\"ZVwlJt7dU8Plk0+r8rPF8DmPTvDiOA1UAoNjDV+SqDE\\\\u003d\\\","
-              + "\\\"ephemeralPublicKey\\\":\\\"BPhVspn70Zj2Kkgu9t8+ApEuUWsI/zos5whGCQBlgOkuYagOis7"
-              + "qsrcbQrcprjvTZO3XOU+Qbcc28FSgsRtcgQE\\\\u003d\\\","
-              + "\\\"encryptedMessage\\\":\\\"12jUObueVTdy\\\"}\";")  // ; instead of ,
-          + "\"signature\":\"MEQCIDxBoUCoFRGReLdZ/cABlSSRIKoOEFoU3e27c14vMZtfAiBtX3pGMEpnw6mSAbnagC"
-          + "CgHlCk3NcFwWYEyxIE6KGZVA\\u003d\\u003d\"}";
-
-    assertEquals(PLAINTEXT, recipient.unseal(ciphertextEcV1WithNonStrictJsonEncoding));
-  }
-
-  @Test
-  public void testShouldDecryptECV1WhenUsingCustomKem() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldDecryptECV1WhenFetchingSenderVerifyingKeys() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .fetchSenderVerifyingKeysWith(
-                new GooglePaymentsPublicKeysManager.Builder()
-                    .setHttpTransport(
-                        new MockHttpTransport.Builder()
-                            .setLowLevelHttpResponse(
-                                new MockLowLevelHttpResponse()
-                                    .setContent(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON))
-                            .build())
-                    .build())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldTryAllKeysToDecryptECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldTryAllCustomKemsToDecryptECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldFailIfDecryptingWithDifferentKeyECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot decrypt", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfDecryptingWithDifferentCustomKemECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot decrypt", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKeyECV1() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    trustedKeysJson
-        .get("keys")
-        .getAsJsonArray()
-        .get(INDEX_OF_GOOGLE_SIGNING_EC_V1)
-        .getAsJsonObject()
-        .addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryAllKeysToVerifySignatureECV1() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    JsonObject wrongKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject().deepCopy();
-    wrongKey.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(wrongKey);
-    newKeys.add(correctKey);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldFailIfSignedECV1WithKeyForWrongProtocolVersion() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKeyButWrongProtocol =
-        keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    correctKeyButWrongProtocol.addProperty(
-        PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv2");
-    JsonObject wrongKeyButRightProtocol = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    wrongKeyButRightProtocol.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    wrongKeyButRightProtocol.addProperty(
-        PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY,
-        PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(correctKeyButWrongProtocol);
-    newKeys.add(wrongKeyButRightProtocol);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfNoSigningKeysForProtocolVersion() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    key1.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv2");
-    JsonObject key2 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-
-    key2.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    key2.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv3");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    newKeys.add(key2);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChangedInECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    payload.addProperty(
-        PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY,
-        payload.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString() + " ");
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfWrongRecipientInECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId("not " + RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfECV1SetsWrongProtocolVersion() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    String invalidVersion = "ECv2";
-    payload.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, invalidVersion);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("invalid version: " + invalidVersion, e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfProtocolSetToAnInt() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    payload.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, 1);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      // expected
-    }
-  }
-
-  @Test
-  public void testShouldFailIfProtocolSetToAnFloat() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    payload.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, 1.1);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      // expected
-    }
-  }
-
-  @Test
-  public void testShouldSucceedIfMessageIsNotExpired() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject plaintext = new JsonObject();
-    plaintext.addProperty(
-        "messageExpiration",
-        // One day in the future
-        String.valueOf(Instant.now().plus(Duration.standardDays(1)).getMillis()));
-    plaintext.addProperty("someKey", "someValue");
-    String ciphertext = sender.seal(plaintext.toString());
-    JsonObject decrypted = JsonParser.parseString(recipient.unseal(ciphertext)).getAsJsonObject();
-
-    assertEquals("someValue", decrypted.get("someKey").getAsString());
-  }
-
-  @Test
-  public void testShouldFailIfMessageIsExpired() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject expired = new JsonObject();
-    expired.addProperty(
-        "messageExpiration",
-        // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-
-    String ciphertext = sender.seal(expired.toString());
-    try {
-      recipient.unseal(ciphertext);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired payload", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfTrustedKeyIsExpiredInECV1() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    key1.addProperty(
-        "keyExpiration", // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldSucceedIfKeyExpirationIsMissingInTrustedKeyIsExpiredForECV1()
-      throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    key1.remove("keyExpiration");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testUnsealECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testUnsealECV2WithCustomKem() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChangedInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    payload.addProperty(
-        PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY,
-        payload.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString() + " ");
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfECV2UseWrongSenderId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .senderId("not-" + PaymentMethodTokenConstants.GOOGLE_SENDER_ID)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKeyECV2() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    key1.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfTrustedKeyIsExpiredInECV2() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    key1.addProperty(
-        "keyExpiration", // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfKeyExpirationIsMissingInTrustedKeyECV2() throws Exception {
-    // Key expiration is required for V2
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    key1.remove("keyExpiration");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryAllKeysToVerifySignatureECV2() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    JsonObject wrongKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject().deepCopy();
-    wrongKey.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(wrongKey);
-    newKeys.add(correctKey);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedKeyWasChangedInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.addProperty(
-        "signedKey", intermediateSigningKey.get("signedKey").getAsString() + " ");
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignatureForSignedKeyIsIncorrectInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    JsonArray signatures = intermediateSigningKey.get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryVerifyingAllSignaturesForSignedKeyInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    JsonArray signatures = intermediateSigningKey.get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    newSignatures.add(correctSignature);
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldThrowIfECV2UseWrongRecipientId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId("not" + RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldAcceptNonExpiredECV2Message() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the future
-        String.valueOf(Instant.now().plus(Duration.standardDays(1)).getMillis()));
-    String plaintext = payload.toString();
-    assertEquals(plaintext, recipient.unseal(sealECV2(plaintext)));
-  }
-
-  @Test
-  public void testShouldFailIfECV2MessageIsExpired() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-
-    String ciphertext = sealECV2(payload.toString());
-    try {
-      recipient.unseal(ciphertext);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired payload", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfIntermediateSigningKeyIsExpiredInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    // Expiration date in the past.
-                    .expiration(Instant.now().minus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sender.seal(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired intermediateSigningKey", e.getMessage());
-    }
-  }
-
-  private static String sealECV2(String plaintext) throws GeneralSecurityException {
-    return new PaymentMethodTokenSender.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-        .senderIntermediateCert(
-            new SenderIntermediateCertFactory.Builder()
-                .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-                .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-                .senderIntermediateSigningKey(
-                    GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                .build()
-                .create())
-        .recipientId(RECIPIENT_ID)
-        .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-        .build()
-        .seal(plaintext);
-  }
-
-  @Test
-  public void testVerifyECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(signECV2SigningOnly(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChangedInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    payload.addProperty(
-        PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY,
-        payload.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString() + " ");
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfECV2SigningOnlyUseWrongSenderId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .senderId("not-" + PaymentMethodTokenConstants.GOOGLE_SENDER_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKeyECV2SigningOnly() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    key.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfTrustedKeyIsExpiredInECV2SigningOnly() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    key.addProperty(
-        "keyExpiration", // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfKeyExpirationIsMissingInTrustedKeyECV2SigningOnly() throws Exception {
-    // Key expiration is required for ECv2SigningOnly
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    key.remove("keyExpiration");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryAllKeysToVerifySignatureECV2SigningOnly() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    JsonObject wrongKey =
-        keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject().deepCopy();
-    wrongKey.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(wrongKey);
-    newKeys.add(correctKey);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(signECV2SigningOnly(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedKeyWasChangedInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.addProperty(
-        "signedKey", intermediateSigningKey.get("signedKey").getAsString() + " ");
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignatureForSignedKeyIsIncorrectInECV2SigningOnly()
-      throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    JsonArray signatures =
-        payload.get("intermediateSigningKey").getAsJsonObject().get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryVerifyingAllSignaturesForSignedKeyInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    JsonArray signatures =
-        payload.get("intermediateSigningKey").getAsJsonObject().get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    newSignatures.add(correctSignature);
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    assertEquals(PLAINTEXT, recipient.unseal(signECV2SigningOnly(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldThrowIfECV2SigningOnlyUseWrongRecipientId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId("not" + RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldAcceptNonExpiredECV2SigningOnlyMessage() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the future
-        String.valueOf(Instant.now().plus(Duration.standardDays(1)).getMillis()));
-    String plaintext = payload.toString();
-    assertEquals(plaintext, recipient.unseal(signECV2SigningOnly(plaintext)));
-  }
-
-  @Test
-  public void testShouldFailIfECV2SigningOnlyMessageIsExpired() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    String ciphertext = signECV2SigningOnly(payload.toString());
-    try {
-      recipient.unseal(ciphertext);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired payload", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfIntermediateSigningKeyIsExpiredInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(
-                        PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    // Expiration date in the past.
-                    .expiration(Instant.now().minus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(sender.seal(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired intermediateSigningKey", e.getMessage());
-    }
-  }
-
-  private static String signECV2SigningOnly(String plaintext) throws GeneralSecurityException {
-    return new PaymentMethodTokenSender.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-        .senderIntermediateSigningKey(
-            GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-        .senderIntermediateCert(
-            new SenderIntermediateCertFactory.Builder()
-                .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                .senderIntermediateSigningKey(
-                    GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                .build()
-                .create())
-        .recipientId(RECIPIENT_ID)
-        .build()
-        .seal(plaintext);
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSenderTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSenderTest.java
deleted file mode 100644
index 5ca98a0..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSenderTest.java
+++ /dev/null
@@ -1,577 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.subtle.EllipticCurves;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECParameterSpec;
-import org.joda.time.Duration;
-import org.joda.time.Instant;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code PaymentMethodTokenSender}. */
-@RunWith(JUnit4.class)
-public class PaymentMethodTokenSenderTest {
-  private static final String MERCHANT_PUBLIC_KEY_BASE64 =
-      "BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y=";
-  /**
-   * Created with:
-   *
-   * <pre>
-   * openssl pkcs8 -topk8 -inform PEM -outform PEM -in merchant-key.pem -nocrypt
-   * </pre>
-   */
-  private static final String MERCHANT_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCPSuFr4iSIaQprjj"
-          + "chHPyDu2NXFe0vDBoTpPkYaK9dehRANCAATnaFz/vQKuO90pxsINyVNWojabHfbx"
-          + "9qIJ6uD7Q7ZSxmtyo/Ez3/o2kDT8g0pIdyVIYktCsq65VoQIDWSh2Bdm";
-
-  /** Sample Google provided JSON with its public signing keys. */
-  private static final String GOOGLE_VERIFYING_PUBLIC_KEYS_JSON =
-      "{\n"
-          + "  \"keys\": [\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPYnHwS8uegWAewQtlxizmLFynw"
-          + "HcxRT1PK07cDA6/C4sXrVI1SzZCUx8U8S0LjMrT6ird/VW7be3Mz6t/srtRQ==\",\n"
-          + "      \"protocolVersion\": \"ECv1\"\n"
-          + "    },\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM"
-          + "43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2\"\n"
-          + "    },\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENXvYqxD5WayKYhuXQevdGdLA8i"
-          + "fV4LsRS2uKvFo8wwyiwgQHB9DiKzG6T/P1Fu9Bl7zWy/se5Dy4wk1mJoPuxg==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2SigningOnly\"\n"
-          + "    }\n"
-          + "  ]\n"
-          + "}";
-
-  /**
-   * Sample Google private signing key for the ECv1 protocolVersion.
-   *
-   * <p>Corresponds to the ECv1 private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O"
-          + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF"
-          + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F";
-
-  /**
-   * Sample Google private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to ECv2 private key of the key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yR"
-          + "ydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google private signing key for the ECV2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to ECV2SigningOnly private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRi9hSdY+knJ08odnY"
-          + "tZFMRi7ZYeMoasAijLhD4GiQ1yhRANCAAQ1e9irEPlZrIpiG5dB690Z0sDy"
-          + "J9XguxFLa4q8WjzDDKLCBAcH0OIrMbpP8/UW70GXvNbL+x7kPLjCTWYmg+7G";
-
-  /**
-   * Sample Google intermediate public signing key for the ECV2SigningOnly protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-          "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8OaurwvbyYm8JWDgFPRTIDg0/"
-              + "kcQTFAQ4txi5IP0AyM1QiagwRhDUfjpqZkpw8xt/DXwyWYM0DdHqoeV"
-              + "TKqmYQ==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECV2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-          "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Jvpkq26tpZ0s"
-              + "TTZVh4teEI41SnJdmkBzM8VZ5ZirE2hRANCAATw5q6vC9vJibwlYOAU"
-              + "9FMgODT+RxBMUBDi3GLkg/QDIzVCJqDBGENR+OmpmSnDzG38NfDJZgz"
-              + "QN0eqh5VMqqZh";
-
-  private static final String RECIPIENT_ID = "someRecipient";
-
-  @Test
-  public void testECV1WithPrecomputedKeys() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(sender.seal(plaintext)));
-  }
-
-  @Test
-  public void testECV1WithTestdataSeal() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    // Seal where "=" has been escaped by \u003d.
-    // - tag is "bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8=", = gets escapced by \u003d, so
-    //   signedMessage contains "tag": "bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8\u003d"
-    // - this gets escaped in the 2nd encoding with \ -> \\ and " -> \". So the final seal
-    //   contains \"tag\":\"bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8\\u003d\"
-    // Note that to encode this string in Java, we have to escape \ -> \\ and " -> \" again.
-    String testdataSeal =
-        "{\"signedMessage\":\"{\\\"tag\\\":\\\"bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8\\\\u003d"
-            + "\\\",\\\"ephemeralPublicKey\\\":\\\"BLyk1Iwcx+yRQbCSsZgAwb8s/ChNDTlcCovgL5/37qDdia3x"
-            + "PM5GUb+HmbW9bF/c12p0ySxtU/MDcNkJZ0nWCVs\\\\u003d\\\",\\\"encryptedMessage\\\":\\\"ue"
-            + "63Gg\\\\u003d\\\\u003d\\\"}\",\"protocolVersion\":\"ECv1\",\"signature\":\"MEQCIFFdW"
-            + "ve35+jh7CT9QC9W6Leqx32P41oNxG2NDm6PaY1fAiAKHvklWHDPiLKYPb0zyXRGIl6GYvfQ3LAl1z5sR3CbS"
-            + "w\\u003d\\u003d\"}";
-
-    // Seal where "=" has not been escaped, but " and \ have to be escaped as before.
-    String testdataSeal2 =
-        "{\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"Ih3e7g==\\\",\\\"tag\\\":\\\"GlZ332kL5u"
-            + "ZICWrCsSSQ6KrFFqQyKI84SH2Wh6UTv8c=\\\",\\\"ephemeralPublicKey\\\":\\\"BBb/VaSEYJphhs"
-            + "ma1X24QrjdFr/DHo7IDu8owi6NR0tW9p5F9jn6wxu5by5Rs/bYkVdr8HNT9+MEpBOnL7Si/dQ=\\\"}\",\""
-            + "protocolVersion\":\"ECv1\",\"signature\":\"MEYCIQD6rOV4l9pm/MDr2jPkqnU2GSHbbnUaLYQow"
-            + "/0Y+S1axAIhAKUUO7N9eoBNuXySDDWDYrdu7r0IHxeeFtkubDxhplYU\"}";
-
-    // Seal where "=" has only been escaped in the outer encoding:
-    // - tag is "Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE=", so
-    //   signedMessage contains "tag": "Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE="
-    // - this gets escaped in the 2nd encoding with \ -> \\, " -> \" and = -> \u003d. So the final
-    //   seal contains \"tag\":\"Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE\u003d\"
-    // Note that to encode this string in Java, we have to escape \ -> \\ and " -> \" again.
-    String testdataSeal3 =
-        "{\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"eTBsng\\u003d\\u003d\\\",\\\"tag\\\":\\"
-            + "\"Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE\\u003d\\\",\\\"ephemeralPublicKey\\\":"
-            + "\\\"BGPS6h2ddaOA+H71e28xZ2OQZBJuVrCK3qUXc6G6ykHTr8ab9pmS7B87n9jg8qwYAWpFcRNgC2fxnrPL"
-            + "+p2Gk5U\\u003d\\\"}\",\"protocolVersion\":\"ECv1\",\"signature\":\"MEUCIQC+8NTo1xbem"
-            + "5lPhXuEwcYc0W03jdj3Q1YNI4XvoVAbuAIgT5WXy1wZ2GNXoaLauNGo0iHeOjsZT3wYUziZPHkSAlk\\u003"
-            + "d\"}";
-
-    // Weird token where for no reason the characters { -> \u007b and s -> \u0073 have been escaped.
-    String testdataSeal4 =
-        "{\"\\u0073ignedMe\\u0073\\u0073age\":\"\\u007b\\\"encryptedMe\\\\u0073\\\\u0073age\\\":\\"
-            + "\"tfAPcA\\\\u003d\\\\u003d\\\",\\\"tag\\\":\\\"bB72KLdziA4Oca77w7g7a4fbKpgFXVrtUr56W"
-            + "NpIL6M\\\\u003d\\\",\\\"ephemeralPublicKey\\\":\\\"BKYBrgWkOP7gqCcIqRjT1t0HjMdbEIMeP"
-            + "82VtfC7IaCNbHgL9vojnQ/Yxy8Kutn+MCvG3gRdTxGN+Pwl+1QjM6w\\\\u003d\\\"}\",\"protocolVer"
-            + "\\u0073ion\":\"ECv1\",\"\\u0073ignature\":\"MEUCIHDW7JS51iy1LeyiNri7tNqe0HYdPwmDoi/M"
-            + "j2RD+3f1AiEA9FBSpqDhQzEvn849IrxXWQlIkuJdUfLE1NxCBQ8mCj0\\u003d\"}";
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(testdataSeal));
-    assertEquals(plaintext, recipient.unseal(testdataSeal2));
-    assertEquals(plaintext, recipient.unseal(testdataSeal3));
-    assertEquals(plaintext, recipient.unseal(testdataSeal4));
-  }
-
-  @Test
-  public void testECV2WithPrecomputedKeys() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(sender.seal(plaintext)));
-  }
-
-  @Test
-  public void testECV2SigningOnlyWithPrecomputedKeys() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(
-                        PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(sender.seal(plaintext)));
-  }
-
-  @Test
-  public void testShouldThrowWithUnsupportedProtocolVersion() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder().protocolVersion("ECv99").build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("invalid version: ECv99", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignedIntermediateSigningKeyIsSetForECV1() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .senderIntermediateCert(newSignedIntermediateSigningKey())
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfIntermediateSigningKeyIsSetForECV1() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set sender's intermediate signing key using "
-              + "Builder.senderIntermediateSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSenderSigningKeyIsSetForECV2() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set sender's signing key using Builder.senderSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignedIntermediateSigningKeyIsNotSetForECV2() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          // no calls to senderIntermediateCert
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfIntermediateSigningKeyIsNotSetForECV2() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          // no calls to senderIntermediateSigningKey
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set sender's intermediate signing key using Builder.senderIntermediateSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSenderSigningKeyIsNotSetForECV1() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          // no calls to senderSigningKey
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set sender's signing key using Builder.senderSigningKey", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSenderSigningKeyIsSetForECV2SigningOnly() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set sender's signing key using Builder.senderSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignedIntermediateSigningKeyIsNotSetForECV2SigningOnly()
-      throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          // no calls to senderIntermediateCert
-          .senderIntermediateSigningKey(
-              GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfRecipientPublicKeyIsSetForECV2SigningOnly() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          // no calls to senderIntermediateCert
-          .senderIntermediateSigningKey(
-              GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-          .senderIntermediateCert(
-              new SenderIntermediateCertFactory.Builder()
-                  .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                  .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                  .senderIntermediateSigningKey(
-                      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                  .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                  .build()
-                  .create())
-          .recipientId(RECIPIENT_ID)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set recipient's public key using Builder.recipientPublicKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfIntermediateSigningKeyIsNotSetForECV2SigningOnly() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          // no calls to senderIntermediateSigningKey
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set sender's intermediate signing key using Builder.senderIntermediateSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testSendReceive() throws Exception {
-    ECParameterSpec spec = EllipticCurves.getNistP256Params();
-    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
-    keyGen.initialize(spec);
-    String senderId = "foo";
-    String recipientId = "bar";
-
-    KeyPair senderKey = keyGen.generateKeyPair();
-    ECPublicKey senderPublicKey = (ECPublicKey) senderKey.getPublic();
-    ECPrivateKey senderPrivateKey = (ECPrivateKey) senderKey.getPrivate();
-
-    KeyPair recipientKey = keyGen.generateKeyPair();
-    ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
-    ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
-
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderId(senderId)
-            .senderSigningKey(senderPrivateKey)
-            .recipientId(recipientId)
-            .recipientPublicKey(recipientPublicKey)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderId(senderId)
-            .addSenderVerifyingKey(senderPublicKey)
-            .recipientId(recipientId)
-            .addRecipientPrivateKey(recipientPrivateKey)
-            .build();
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(sender.seal(plaintext)));
-  }
-
-  @Test
-  public void testWithKeysGeneratedByRecipientKeyGen() throws Exception {
-    ECParameterSpec spec = EllipticCurves.getNistP256Params();
-    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
-    keyGen.initialize(spec);
-    String senderId = "foo";
-    String recipientId = "bar";
-
-    KeyPair senderKey = keyGen.generateKeyPair();
-    ECPublicKey senderPublicKey = (ECPublicKey) senderKey.getPublic();
-    ECPrivateKey senderPrivateKey = (ECPrivateKey) senderKey.getPrivate();
-
-    // The keys here are generated by PaymentMethodTokenRecipientKeyGen.
-    String recipientPrivateKey =
-        "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCAP5/1502pXYdMion22yiWK"
-            + "GoTBJN/wAAfdjBU6puyEMw==";
-    String recipientPublicKey =
-        "BJ995jnw2Ppn4BMP/ZKtlTOOIBQC+/L3PDcFRjowZuCkRqUZ/kGWE8c+zimZNHOZPzLB"
-            + "NVGJ3V8M/fM4g4o02Mc=";
-
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderId(senderId)
-            .senderSigningKey(senderPrivateKey)
-            .recipientId(recipientId)
-            .rawUncompressedRecipientPublicKey(recipientPublicKey)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderId(senderId)
-            .addSenderVerifyingKey(senderPublicKey)
-            .recipientId(recipientId)
-            .addRecipientPrivateKey(recipientPrivateKey)
-            .build();
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(sender.seal(plaintext)));
-  }
-
-  private String newSignedIntermediateSigningKey() throws GeneralSecurityException {
-    return new SenderIntermediateCertFactory.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-        .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-        .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-        .build()
-        .create();
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactoryTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactoryTest.java
deleted file mode 100644
index 566ebde..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactoryTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import org.joda.time.Duration;
-import org.joda.time.Instant;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link SenderIntermediateCertFactory}. */
-@RunWith(JUnit4.class)
-public class SenderIntermediateCertFactoryTest {
-
-  /**
-   * Sample Google private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the private key encoded in PKCS8 format.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yR"
-          + "ydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-
-  @Test
-  public void shouldProduceSenderIntermediateCertJson() throws Exception {
-    String encoded =
-        new SenderIntermediateCertFactory.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-            .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-            .expiration(123456)
-            .build()
-            .create();
-
-    JsonObject decodedSignedIntermediateSigningKey =
-        JsonParser.parseString(encoded).getAsJsonObject();
-    assertTrue(decodedSignedIntermediateSigningKey.get("signedKey").isJsonPrimitive());
-    assertTrue(decodedSignedIntermediateSigningKey.get("signatures").isJsonArray());
-    assertEquals(2, decodedSignedIntermediateSigningKey.size());
-
-    JsonObject signedKey =
-        JsonParser.parseString(decodedSignedIntermediateSigningKey.get("signedKey").getAsString())
-            .getAsJsonObject();
-    assertTrue(signedKey.get("keyValue").getAsJsonPrimitive().isString());
-    assertTrue(signedKey.get("keyExpiration").getAsJsonPrimitive().isString());
-    assertEquals(2, signedKey.size());
-    assertEquals(
-        GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64,
-        signedKey.get("keyValue").getAsString());
-    assertEquals("123456", signedKey.get("keyExpiration").getAsString());
-    assertEquals(1, decodedSignedIntermediateSigningKey.get("signatures").getAsJsonArray().size());
-    assertFalse(
-        decodedSignedIntermediateSigningKey
-            .get("signatures")
-            .getAsJsonArray()
-            .get(0)
-            .getAsString()
-            .isEmpty());
-  }
-
-  @Test
-  public void shouldThrowIfExpirationNotSet() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-          .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-          // no expiration
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("must set expiration using Builder.expiration", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void shouldThrowIfExpirationIsNegative() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-          .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-          .expiration(-1)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("invalid negative expiration", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void shouldThrowIfNoSenderSigningKeyAdded() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-          // no call to addSenderSigningKey
-          .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must add at least one sender's signing key using Builder.addSenderSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void shouldSupportECV2SigningOnly() throws Exception {
-    new SenderIntermediateCertFactory.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-        .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-        .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-        .expiration(123456)
-        .build();
-  }
-
-  @Test
-  public void shouldThrowIfInvalidProtocolVersionSet() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("invalid version: ECv1", expected.getMessage());
-    }
-  }
-}
diff --git a/apps/rewardedads/BUILD.bazel b/apps/rewardedads/BUILD.bazel
deleted file mode 100644
index a8f1330..0000000
--- a/apps/rewardedads/BUILD.bazel
+++ /dev/null
@@ -1,15 +0,0 @@
-load("@tink_java//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-gen_maven_jar_rules(
-    name = "maven",
-    doctitle = "Tink Cryptography API for Google Mobile Rewarded Video Ads SSV",
-    manifest_lines = [
-        "Automatic-Module-Name: com.google.crypto.tink.apps.rewardedads",
-    ],
-    root_packages = ["com.google.crypto.tink.apps.rewardedads"],
-    deps = ["//rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads:rewarded_ads_verifier"],
-)
diff --git a/apps/rewardedads/README.md b/apps/rewardedads/README.md
deleted file mode 100644
index b92e374..0000000
--- a/apps/rewardedads/README.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# An implementation of Google AdMob Rewarded Ads
-
-This app implements the verifier side of Server-Side Verification of Google
-AdMob Rewarded Ads.
-
-## Latest Release
-
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-rewardedads/1.7.0).
-
-The Maven group ID is `com.google.crypto.tink`, and the artifact ID is
-`apps-rewardedads`.
-
-To add a dependency using Maven:
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-rewardedads</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-## Snapshots
-
-Snapshots of this app built from the master branch are available through Maven
-using version `HEAD-SNAPSHOT`. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-rewardedads/HEAD-SNAPSHOT).
-
-To add a dependency using Maven:
-
-```xml
-<repositories>
-<repository>
-  <id>sonatype-snapshots</id>
-  <name>sonatype-snapshots</name>
-  <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
-  <snapshots>
-    <enabled>true</enabled>
-    <updatePolicy>always</updatePolicy>
-  </snapshots>
-  <releases>
-    <updatePolicy>always</updatePolicy>
-  </releases>
-</repository>
-</repositories>
-
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-rewardedads</artifactId>
-  <version>HEAD-SNAPSHOT</version>
-</dependency>
-```
diff --git a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/BUILD.bazel b/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/BUILD.bazel
deleted file mode 100644
index a44132c..0000000
--- a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/BUILD.bazel
+++ /dev/null
@@ -1,17 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_library(
-    name = "rewarded_ads_verifier",
-    srcs = ["RewardedAdsVerifier.java"],
-    deps = [
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:com_google_http_client_google_http_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:enums",
-        "@tink_java//src/main/java/com/google/crypto/tink/util:keys_downloader",
-    ],
-)
diff --git a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java b/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java
deleted file mode 100644
index 85d26df..0000000
--- a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java
+++ /dev/null
@@ -1,325 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.rewardedads;
-
-import com.google.api.client.http.javanet.NetHttpTransport;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaVerifyJce;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.crypto.tink.subtle.Enums.HashType;
-import com.google.crypto.tink.util.KeysDownloader;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.charset.Charset;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPublicKey;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * An implementation of the verifier side of Server-Side Verification of Google AdMob Rewarded Ads.
- *
- * <p>Typical usage:
- *
- * <pre>{@code
- * RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
- *     .fetchVerifyingPublicKeysWith(
- *         RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
- *     .build();
- * String rewardUrl = ...;
- * verifier.verify(rewardUrl);
- * }</pre>
- *
- * <p>This usage ensures that you always have the latest public keys, even when the keys were
- * recently rotated. It will fetch and cache the latest public keys from {#PUBLIC_KEYS_URL_PROD}.
- * When the cache expires, it will re-fetch the public keys. When initializing your server, we also
- * recommend that you call {@link KeysDownloader#refreshInBackground()} of {@link
- * RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD} to proactively fetch the public keys.
- *
- * <p>If you've already downloaded the public keys and have other means to manage key rotation, you
- * can use {@link RewardedAdsVerifier.Builder#setVerifyingPublicKeys} to set the public keys. The
- * Builder also allows you to customize other properties.
- */
-public final class RewardedAdsVerifier {
-  private static final Charset UTF_8 = Charset.forName("UTF-8");
-
-  /** Default HTTP transport used by this class. */
-  private static final NetHttpTransport DEFAULT_HTTP_TRANSPORT =
-      new NetHttpTransport.Builder().build();
-
-  private static final Executor DEFAULT_BACKGROUND_EXECUTOR = Executors.newCachedThreadPool();
-  private final List<VerifyingPublicKeysProvider> verifyingPublicKeysProviders;
-
-  public static final String SIGNATURE_PARAM_NAME = "signature=";
-  public static final String KEY_ID_PARAM_NAME = "key_id=";
-
-  /** URL to fetch keys for environment production. */
-  public static final String PUBLIC_KEYS_URL_PROD =
-      "https://www.gstatic.com/admob/reward/verifier-keys.json";
-
-  /** URL to fetch keys for environment test. */
-  public static final String PUBLIC_KEYS_URL_TEST =
-      "https://www.gstatic.com/admob/reward/verifier-keys-test.json";
-
-  /**
-   * Instance configured to talk to fetch keys from production environment (from {@link
-   * #PUBLIC_KEYS_URL_PROD}).
-   */
-  public static final KeysDownloader KEYS_DOWNLOADER_INSTANCE_PROD =
-      new KeysDownloader(DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, PUBLIC_KEYS_URL_PROD);
-  /**
-   * Instance configured to talk to fetch keys from test environment (from {@link
-   * #PUBLIC_KEYS_URL_TEST}).
-   */
-  public static final KeysDownloader KEYS_DOWNLOADER_INSTANCE_TEST =
-      new KeysDownloader(DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, PUBLIC_KEYS_URL_TEST);
-
-  RewardedAdsVerifier(List<VerifyingPublicKeysProvider> verifyingPublicKeysProviders)
-      throws GeneralSecurityException {
-    if (verifyingPublicKeysProviders == null || verifyingPublicKeysProviders.isEmpty()) {
-      throw new IllegalArgumentException(
-          "must set at least one way to get verifying key using"
-              + " Builder.fetchVerifyingPublicKeysWith or Builder.setVerifyingPublicKeys");
-    }
-    this.verifyingPublicKeysProviders = verifyingPublicKeysProviders;
-  }
-
-  private RewardedAdsVerifier(Builder builder) throws GeneralSecurityException {
-    this(builder.verifyingPublicKeysProviders);
-  }
-
-  /**
-   * Verifies that {@code rewardUrl} has a valid signature.
-   *
-   * <p>This method requires that the name of the last two query parameters of {@code rewardUrl} are
-   * {@link #SIGNATURE_PARAM_NAME} and {@link #KEY_ID_PARAM_NAME} in that order.
-   */
-  public void verify(String rewardUrl) throws GeneralSecurityException {
-    URI uri;
-    try {
-      uri = new URI(rewardUrl);
-    } catch (URISyntaxException ex) {
-      throw new GeneralSecurityException(ex);
-    }
-    String queryString = uri.getQuery();
-    int i = queryString.indexOf(SIGNATURE_PARAM_NAME);
-    if (i <= 0 || queryString.charAt(i - 1) != '&') {
-      throw new GeneralSecurityException(
-          "signature and key id must be the last two query parameters");
-    }
-    byte[] tbsData =
-        queryString.substring(0, i - 1 /* i - 1 instead of i because of & */).getBytes(UTF_8);
-
-    String sigAndKeyId = queryString.substring(i);
-    i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);
-    if (i == -1 || sigAndKeyId.charAt(i - 1) != '&') {
-      throw new GeneralSecurityException(
-          "signature and key id must be the last two query parameters");
-    }
-    String sig =
-        sigAndKeyId.substring(
-            SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */);
-
-    // We don't have to check that keyId is the last parameter, because the long conversion would
-    // fail anyway if there's any trailing data.
-    try {
-      long keyId = Long.parseLong(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length()));
-      verify(tbsData, keyId, Base64.urlSafeDecode(sig));
-    } catch (NumberFormatException ex) {
-      throw new GeneralSecurityException("key_id must be a long");
-    }
-  }
-
-  private void verify(final byte[] tbs, long keyId, final byte[] signature)
-      throws GeneralSecurityException {
-    boolean foundKeyId = false;
-    for (VerifyingPublicKeysProvider provider : verifyingPublicKeysProviders) {
-      Map<Long, ECPublicKey> publicKeys = provider.get();
-      if (publicKeys.containsKey(keyId)) {
-        foundKeyId = true;
-        ECPublicKey publicKey = publicKeys.get(keyId);
-        EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER);
-        verifier.verify(signature, tbs);
-      }
-    }
-    if (!foundKeyId) {
-      throw new GeneralSecurityException("cannot find verifying key with key id: " + keyId);
-    }
-  }
-
-  /** Builder for RewardedAdsVerifier. */
-  public static class Builder {
-    private final List<VerifyingPublicKeysProvider> verifyingPublicKeysProviders =
-        new ArrayList<>();
-
-    public Builder() {}
-
-    /**
-     * Fetches verifying public keys of the sender using {@link KeysDownloader}.
-     *
-     * <p>This is the preferred method of specifying the verifying public keys.
-     */
-    public Builder fetchVerifyingPublicKeysWith(final KeysDownloader downloader)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              try {
-                return parsePublicKeysJson(downloader.download());
-              } catch (IOException e) {
-                throw new GeneralSecurityException("Failed to fetch keys!", e);
-              }
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Sets the trusted verifying public keys of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link
-     * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use
-     * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need
-     * to handle key rotations yourself.
-     *
-     * <p>The given string is a JSON object formatted like the following:
-     *
-     * <pre>
-     * {
-     *   "keys": [
-     *     {
-     *       keyId: 1916455855,
-     *       pem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUaWMKcBHWdhUE+DncSIHhFCLLEln\nUs0LB9oanZ4K/FNICIM8ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw==\n-----END PUBLIC KEY-----"
-     *       base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUaWMKcBHWdhUE+DncSIHhFCLLElnUs0LB9oanZ4K/FNICIM8ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
-     *     },
-     *     {
-     *       keyId: 3901585526,
-     *       pem: "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtxg2BsK/fllIeADtLspezS6YfHFWXZ8tiJncm8LDBa/NxEC84akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw==\n-----END PUBLIC KEY-----"
-     *       base64: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtxg2BsK/fllIeADtLspezS6YfHFWXZ8tiJncm8LDBa/NxEC84akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
-     *     },
-     *   ],
-     * }
-     * </pre>
-     *
-     * <p>Each public key will be a base64 (no wrapping, padded) version of the key encoded in ASN.1
-     * type SubjectPublicKeyInfo defined in the X.509 standard.
-     */
-    public Builder setVerifyingPublicKeys(final String publicKeysJson)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              return parsePublicKeysJson(publicKeysJson);
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link
-     * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use
-     * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need
-     * to handle Google key rotations yourself.
-     *
-     * <p>The public key is a base64 (no wrapping, padded) version of the key encoded in ASN.1 type
-     * SubjectPublicKeyInfo defined in the X.509 standard.
-     *
-     * <p>Multiple keys may be added. This utility will then verify any message signed with any of
-     * the private keys corresponding to the public keys added. Adding multiple keys is useful for
-     * handling key rotation.
-     */
-    public Builder addVerifyingPublicKey(final long keyId, final String val)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              return Collections.singletonMap(
-                  keyId, EllipticCurves.getEcPublicKey(Base64.decode(val)));
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link
-     * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use
-     * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need
-     * to handle Google key rotations yourself.
-     */
-    public Builder addVerifyingPublicKey(final long keyId, final ECPublicKey val)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              return Collections.singletonMap(keyId, val);
-            }
-          });
-      return this;
-    }
-
-    public RewardedAdsVerifier build() throws GeneralSecurityException {
-      return new RewardedAdsVerifier(this);
-    }
-  }
-
-  private static Map<Long, ECPublicKey> parsePublicKeysJson(String publicKeysJson)
-      throws GeneralSecurityException {
-    Map<Long, ECPublicKey> publicKeys = new HashMap<>();
-    try {
-      JsonArray keys =
-          JsonParser.parseString(publicKeysJson).getAsJsonObject().get("keys").getAsJsonArray();
-      for (int i = 0; i < keys.size(); i++) {
-        JsonObject key = keys.get(i).getAsJsonObject();
-        publicKeys.put(
-            key.get("keyId").getAsLong(),
-            EllipticCurves.getEcPublicKey(Base64.decode(key.get("base64").getAsString())));
-      }
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
-    }
-    if (publicKeys.isEmpty()) {
-      throw new GeneralSecurityException("no trusted keys are available for this protocol version");
-    }
-    return publicKeys;
-  }
-
-  private interface VerifyingPublicKeysProvider {
-    Map<Long, ECPublicKey> get() throws GeneralSecurityException;
-  }
-}
diff --git a/apps/rewardedads/src/test/BUILD.bazel b/apps/rewardedads/src/test/BUILD.bazel
deleted file mode 100644
index 244de24..0000000
--- a/apps/rewardedads/src/test/BUILD.bazel
+++ /dev/null
@@ -1,35 +0,0 @@
-load("@tink_java//tools:gen_java_test_rules.bzl", "gen_java_test_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-# Tests
-
-java_library(
-    name = "generator_test",
-    testonly = 1,
-    srcs = glob([
-        "**/*.java",
-    ]),
-    deps = [
-        "//rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads:rewarded_ads_verifier",
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:com_google_http_client_google_http_client",
-        "@maven//:junit_junit",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:enums",
-        "@tink_java//src/main/java/com/google/crypto/tink/util:keys_downloader",
-    ],
-)
-
-gen_java_test_rules(
-    test_files = glob([
-        "**/*Test.java",
-    ]),
-    deps = [
-        ":generator_test",
-    ],
-)
diff --git a/apps/rewardedads/src/test/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifierTest.java b/apps/rewardedads/src/test/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifierTest.java
deleted file mode 100644
index f0e636e..0000000
--- a/apps/rewardedads/src/test/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifierTest.java
+++ /dev/null
@@ -1,371 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.rewardedads;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.api.client.testing.http.MockHttpTransport;
-import com.google.api.client.testing.http.MockLowLevelHttpResponse;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaSignJce;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.crypto.tink.subtle.Enums.HashType;
-import com.google.crypto.tink.util.KeysDownloader;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.net.URI;
-import java.nio.charset.Charset;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code RewardedAdsVerifier}. */
-@RunWith(JUnit4.class)
-public class RewardedAdsVerifierTest {
-  private static final Charset UTF_8 = Charset.forName("UTF-8");
-
-  /** Sample Google provided JSON with its public signing keys. */
-  private static final String GOOGLE_VERIFYING_PUBLIC_KEYS_JSON =
-      "{\"keys\":[{"
-          + "\"keyId\":1234,"
-          + "\"base64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPYnHwS8uegWAewQt"
-          + "lxizmLFynwHcxRT1PK07cDA6/C4sXrVI1SzZCUx8U8S0LjMrT6ird/VW7be3Mz6t/srtRQ==\""
-          + "}]}";
-
-  /**
-   * Sample Google private signing key.
-   *
-   * <p>Corresponds to private key of the key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O"
-          + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF"
-          + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F";
-
-  // must match the value in GOOGLE_VERIFYING_PUBLIC_KEYS_JSON
-  private static final long KEY_ID = 1234;
-  private static final String REWARD_HOST_AND_PATH = "https://publisher.com/blah?";
-  private static final String REWARD_QUERY = "foo1=bar1&foo2=bar2";
-  private static final String REWARD_URL = REWARD_HOST_AND_PATH + REWARD_QUERY;
-
-  private static final String ALTERNATE_PUBLIC_SIGNING_KEY =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEU8E6JppGKFG40r5dDU1idHRN52NuwsemFzXZh1oUqh3bGUPgPioH+RoW"
-          + "nmVSUQz1WfM2426w9f0GADuXzpUkcw==";
-
-  private static String signUrl(String rewardUrl, String privateKey, long keyId) throws Exception {
-    EcdsaSignJce signer =
-        new EcdsaSignJce(
-            EllipticCurves.getEcPrivateKey(Base64.decode(privateKey)),
-            HashType.SHA256,
-            EcdsaEncoding.DER);
-    String queryString = new URI(rewardUrl).getQuery();
-    return buildUrl(rewardUrl, signer.sign(queryString.getBytes(UTF_8)), keyId);
-  }
-
-  private static String buildUrl(String rewardUrl, byte[] sig, long keyId) {
-    return new StringBuilder(rewardUrl)
-        .append("&")
-        .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-        .append(Base64.urlSafeEncode(sig))
-        .append("&")
-        .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-        .append(keyId)
-        .toString();
-  }
-
-  @Test
-  public void testShouldVerify() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID));
-  }
-
-  @Test
-  public void testShouldVerifyIfKeyIdIsLargerThanMaxInt() throws Exception {
-    long keyId = Integer.MAX_VALUE + 1;
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-
-    trustedKeysJson.getAsJsonArray("keys").get(0).getAsJsonObject().addProperty("keyId", keyId);
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(trustedKeysJson.toString())
-            .build();
-    verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, keyId));
-  }
-
-  @Test
-  public void testShouldVerifyWithEncodedUrl() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-
-    String rewardUrl = "https://publisher.com/path?foo=hello%20world&bar=user%40gmail.com";
-    String decodedQueryString = "foo=hello world&[email protected]";
-    EcdsaSignJce signer =
-        new EcdsaSignJce(
-            EllipticCurves.getEcPrivateKey(Base64.decode(GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64)),
-            HashType.SHA256,
-            EcdsaEncoding.DER);
-    byte[] signature = signer.sign(decodedQueryString.getBytes(UTF_8));
-    String signedUrl = buildUrl(rewardUrl, signature, KEY_ID);
-    verifier.verify(signedUrl);
-  }
-
-  @Test
-  public void testShouldDecryptV1WhenFetchingSenderVerifyingKeys() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .fetchVerifyingPublicKeysWith(
-                new KeysDownloader.Builder()
-                    .setHttpTransport(
-                        new MockHttpTransport.Builder()
-                            .setLowLevelHttpResponse(
-                                new MockLowLevelHttpResponse()
-                                    .setContent(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON))
-                            .build())
-                    .setUrl("https://someUrl" /* unused */)
-                    .build())
-            .build();
-
-    verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID));
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKey() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    trustedKeysJson
-        .getAsJsonArray("keys")
-        .get(0)
-        .getAsJsonObject()
-        .addProperty("base64", ALTERNATE_PUBLIC_SIGNING_KEY);
-
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(trustedKeysJson.toString())
-            .build();
-
-    try {
-      verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("Invalid signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfKeyIdNotFound() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-
-    try {
-      verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID + 1));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertTrue(e.getMessage().contains("cannot find verifying key with key id"));
-    }
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChanged() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    byte[] validSignedUrl =
-        signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID).getBytes(UTF_8);
-    for (int i = REWARD_HOST_AND_PATH.length(); i < REWARD_URL.length(); i++) {
-      byte[] modifiedUrl = Arrays.copyOf(validSignedUrl, validSignedUrl.length);
-      modifiedUrl[i] = (byte) (modifiedUrl[i] ^ 0xff);
-      try {
-        verifier.verify(new String(modifiedUrl, UTF_8));
-        fail("Expected GeneralSecurityException");
-      } catch (GeneralSecurityException e) {
-        assertEquals("Invalid signature", e.getMessage());
-      }
-    }
-  }
-
-  @Test
-  public void testShouldFailIfSignatureWasChanged() throws Exception {
-    EcdsaSignJce signer =
-        new EcdsaSignJce(
-            EllipticCurves.getEcPrivateKey(Base64.decode(GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64)),
-            HashType.SHA256,
-            EcdsaEncoding.DER);
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-
-    byte[] validSig = signer.sign(REWARD_URL.getBytes(UTF_8));
-    for (int i = 0; i < validSig.length; i++) {
-      byte[] modifiedSig = Arrays.copyOf(validSig, validSig.length);
-      modifiedSig[i] = (byte) (modifiedSig[i] ^ 0xff);
-      String modifiedUrl = buildUrl(REWARD_URL, modifiedSig, KEY_ID);
-      try {
-        verifier.verify(modifiedUrl);
-        fail("Expected GeneralSecurityException");
-      } catch (GeneralSecurityException e) {
-        // Expected.
-        System.out.println(e);
-      }
-    }
-  }
-
-  @Test
-  public void testShouldFailWithoutSignature() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(REWARD_URL);
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithoutKeyId() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNotTheLastParameters() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_HOST_AND_PATH)
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNotTheLastParameters2() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_HOST_AND_PATH)
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append("&")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("123")
-              .append("bar=baz")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNotTheLastParameters3() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append("&")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("123")
-              .append("&bar=baz") // this would be interpreted as part of the key ID
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("key_id must be a long", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNoAmpersand() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("123")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithKeyIdNotLong() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append("&")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("not_long")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("key_id must be a long", e.getMessage());
-    }
-  }
-}
diff --git a/apps/webpush/BUILD.bazel b/apps/webpush/BUILD.bazel
deleted file mode 100644
index 7260048..0000000
--- a/apps/webpush/BUILD.bazel
+++ /dev/null
@@ -1,20 +0,0 @@
-load("@tink_java//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-gen_maven_jar_rules(
-    name = "maven",
-    doctitle = "Tink Cryptography API for Message Encryption for Web Push (RFC 8291)",
-    manifest_lines = [
-        "Automatic-Module-Name: com.google.crypto.tink.apps.webpush",
-    ],
-    root_packages = ["com.google.crypto.tink.apps.webpush"],
-    deps = [
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_constants",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_decrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_encrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_util",
-    ],
-)
diff --git a/apps/webpush/README.md b/apps/webpush/README.md
deleted file mode 100644
index 1018f4d..0000000
--- a/apps/webpush/README.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# Message Encryption for Web Push
-
-This Tink app is an implementation of [RFC 8291 - Message Encryption for Web
-Push](https://tools.ietf.org/html/rfc8291).
-
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-webpush/1.7.0).
-
-## Installation
-
-To add a dependency using Maven:
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-webpush</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-To add a dependency using Gradle:
-
-```
-dependencies {
-  implementation 'com.google.crypto.tink:apps-webpush:1.7.0'
-}
-```
-
-## Encryption
-
-```java
-import com.google.crypto.tink.HybridEncrypt;
-import java.security.interfaces.ECPublicKey;
-
-ECPublicKey reicipientPublicKey = ...;
-byte[] authSecret = ...;
-HybridEncrypt hybridEncrypt = new WebPushHybridEncrypt.Builder()
-     .withAuthSecret(authSecret)
-     .withRecipientPublicKey(recipientPublicKey)
-     .build();
-byte[] plaintext = ...;
-byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo, must be null */);
-```
-
-## Decryption
-
-```java
-import com.google.crypto.tink.HybridDecrypt;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-
-ECPrivateKey recipientPrivateKey = ...;
-ECPublicKey  recipientPublicKey = ...;
-HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder()
-     .withAuthSecret(authSecret)
-     .withRecipientPublicKey(recipientPublicKey)
-     .withRecipientPrivateKey(recipientPrivateKey)
-     .build();
-byte[] ciphertext = ...;
-byte[] plaintext = hybridDecrypt.decrypt(ciphertext, /* contextInfo, must be null */);
-```
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/BUILD.bazel b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/BUILD.bazel
deleted file mode 100644
index 5d2afbb..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/BUILD.bazel
+++ /dev/null
@@ -1,44 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_library(
-    name = "web_push_hybrid_decrypt",
-    srcs = ["WebPushHybridDecrypt.java"],
-    deps = [
-        ":web_push_constants",
-        ":web_push_util",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
-    ],
-)
-
-java_library(
-    name = "web_push_util",
-    srcs = ["WebPushUtil.java"],
-    deps = [
-        ":web_push_constants",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hkdf",
-    ],
-)
-
-java_library(
-    name = "web_push_constants",
-    srcs = ["WebPushConstants.java"],
-    deps = ["@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves"],
-)
-
-java_library(
-    name = "web_push_hybrid_encrypt",
-    srcs = ["WebPushHybridEncrypt.java"],
-    deps = [
-        ":web_push_constants",
-        ":web_push_util",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
-    ],
-)
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushConstants.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushConstants.java
deleted file mode 100644
index c864021..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushConstants.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.subtle.EllipticCurves;
-import java.nio.charset.Charset;
-
-/** Various constants. */
-final class WebPushConstants {
-  static final Charset UTF_8 = Charset.forName("UTF-8");
-  static final int AUTH_SECRET_SIZE = 16;
-  static final int IKM_SIZE = 32;
-  static final int CEK_KEY_SIZE = 16;
-  static final int NONCE_SIZE = 12;
-  static final byte[] IKM_INFO =
-      new byte[] {'W', 'e', 'b', 'P', 'u', 's', 'h', ':', ' ', 'i', 'n', 'f', 'o', (byte) 0};
-  static final byte[] CEK_INFO =
-      new byte[] {
-        'C', 'o', 'n', 't', 'e', 'n', 't', '-', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', ':', ' ',
-        'a', 'e', 's', '1', '2', '8', 'g', 'c', 'm', (byte) 0
-      };
-  static final byte[] NONCE_INFO =
-      new byte[] {
-        'C', 'o', 'n', 't', 'e', 'n', 't', '-', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', ':', ' ',
-        'n', 'o', 'n', 'c', 'e', (byte) 0
-      };
-
-  static final int SALT_SIZE = 16;
-  static final int RECORD_SIZE_LEN = 4;
-  static final int PUBLIC_KEY_SIZE_LEN = 1;
-  static final int PUBLIC_KEY_SIZE = 65;
-  //   * salt:                    16
-  //   * record size:              4
-  //   * public key size:          1
-  //   * uncompressed public key: 65
-  //   * total                  : 86
-  static final int CONTENT_CODING_HEADER_SIZE =
-      SALT_SIZE + RECORD_SIZE_LEN + PUBLIC_KEY_SIZE_LEN + PUBLIC_KEY_SIZE;
-
-  // the byte 0x2 separating the payload and the padding
-  static final byte PADDING_DELIMITER_BYTE = (byte) 2;
-  static final int PADDING_DELIMETER_SIZE = 1;
-  static final int DEFAULT_PADDING_SIZE = 0;
-  static final int TAG_SIZE = 16;
-  //   * content coding header:   86
-  //   * padding delimeter:        1
-  //   * AES-GCM tag size:        16
-  //   * Total:                  103
-  static final int CIPHERTEXT_OVERHEAD =
-      CONTENT_CODING_HEADER_SIZE + PADDING_DELIMETER_SIZE + DEFAULT_PADDING_SIZE + TAG_SIZE;
-
-  static final int MAX_CIPHERTEXT_SIZE = 4096;
-
-  static final String HMAC_SHA256 = "HMACSHA256";
-  static final EllipticCurves.PointFormatType UNCOMPRESSED_POINT_FORMAT =
-      EllipticCurves.PointFormatType.UNCOMPRESSED;
-  static final EllipticCurves.CurveType NIST_P256_CURVE_TYPE = EllipticCurves.CurveType.NIST_P256;
-
-  private WebPushConstants() {}
-}
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecrypt.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecrypt.java
deleted file mode 100644
index 2062c54..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecrypt.java
+++ /dev/null
@@ -1,278 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EngineFactory;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECPoint;
-import java.util.Arrays;
-import javax.crypto.Cipher;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * A {@link HybridDecrypt} implementation for the hybrid encryption used in <a
- * href="https://tools.ietf.org/html/rfc8291">RFC 8291 - Web Push Message Encryption</a>.
- *
- * <h3>Ciphertext format</h3>
- *
- * <p>When used with <a href="https://tools.ietf.org/html/rfc8291#section-4">AES128-GCM content
- * encoding</a>, which is the only content encoding supported in this implementation, the ciphertext
- * is formatted according to RFC 8188 section 2, and looks as follows
- *
- * <pre>
- * // NOLINTNEXTLINE
- * +-----------+----------------+------------------+---------------------------------------------------
- * | salt (16) | recordsize (4) | publickeylen (1) | publickey (publickeylen) | aes128-gcm-ciphertext |
- * +-----------+----------------+------------------+---------------------------------------------------
- * </pre>
- *
- * <p>RFC 8188 divides messages into records which are encrypted independently. Web Push messages
- * cannot be longer than 3993 bytes, and are always encrypted in a single record with default size
- * of 4096 bytes. {@code aes128-gcm-ciphertext} is the encryption of the message padded with a
- * single byte of value {@code 0x02} (which indicates that this is the last and only record).
- *
- * <h3>Usage</h3>
- *
- * <pre>{@code
- * import com.google.crypto.tink.HybridDecrypt;
- * import com.google.crypto.tink.HybridEncrypt;
- * import java.security.interfaces.ECPrivateKey;
- * import java.security.interfaces.ECPublicKey;
- *
- * // Encryption.
- * ECPublicKey reicipientPublicKey = ...;
- * byte[] authSecret = ...;
- * HybridEncrypt hybridEncrypt = new WebPushHybridEncrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .build();
- * byte[] plaintext = ...;
- * byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
- *
- * // Decryption.
- * ECPrivateKey recipientPrivateKey = ...;
- * HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .withRecipientPrivateKey(recipientPrivateKey)
- *      .build();
- * byte[] plaintext = hybridDecrypt.decrypt(ciphertext, null);
- * }</pre>
- *
- * @since 1.1.0
- */
-public final class WebPushHybridDecrypt implements HybridDecrypt {
-  private final ECPrivateKey recipientPrivateKey;
-  private final byte[] recipientPublicKey;
-  private final byte[] authSecret;
-  private final int recordSize;
-
-  private WebPushHybridDecrypt(Builder builder) throws GeneralSecurityException {
-    if (builder.recipientPrivateKey == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's private key with Builder.withRecipientPrivateKey");
-    }
-    this.recipientPrivateKey = builder.recipientPrivateKey;
-
-    if (builder.recipientPublicKey == null
-        || builder.recipientPublicKey.length != WebPushConstants.PUBLIC_KEY_SIZE) {
-      throw new IllegalArgumentException(
-          "recipient public key must have " + WebPushConstants.PUBLIC_KEY_SIZE + " bytes");
-    }
-    this.recipientPublicKey = builder.recipientPublicKey;
-
-    if (builder.authSecret == null) {
-      throw new IllegalArgumentException("must set auth secret with Builder.withAuthSecret");
-    }
-    if (builder.authSecret.length != WebPushConstants.AUTH_SECRET_SIZE) {
-      throw new IllegalArgumentException(
-          "auth secret must have " + WebPushConstants.AUTH_SECRET_SIZE + " bytes");
-    }
-    this.authSecret = builder.authSecret;
-
-    if (builder.recordSize < WebPushConstants.CIPHERTEXT_OVERHEAD
-        || builder.recordSize > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-      throw new IllegalArgumentException(
-          String.format(
-              "invalid record size (%s); must be a number between [%s, %s]",
-              builder.recordSize,
-              WebPushConstants.CIPHERTEXT_OVERHEAD,
-              WebPushConstants.MAX_CIPHERTEXT_SIZE));
-    }
-    this.recordSize = builder.recordSize;
-  }
-
-  /**
-   * Builder for {@link WebPushHybridDecrypt}.
-   *
-   * @since 1.1.0
-   */
-  public static final class Builder {
-    private ECPrivateKey recipientPrivateKey = null;
-    private byte[] recipientPublicKey = null;
-    private byte[] authSecret = null;
-    private int recordSize = WebPushConstants.MAX_CIPHERTEXT_SIZE;
-
-    public Builder() {}
-
-    /**
-     * Sets the record size.
-     *
-     * <p>If set, this value must match the record size set with {@link
-     * WebPushHybridEncrypt.Builder#withRecordSize}.
-     *
-     * <p>If not set, a record size of 4096 bytes is used. This value should work for most users.
-     */
-    public Builder withRecordSize(int val) {
-      recordSize = val;
-      return this;
-    }
-
-    /** Sets the authentication secret. */
-    public Builder withAuthSecret(final byte[] val) {
-      authSecret = val.clone();
-      return this;
-    }
-
-    /** Sets the public key of the recipient. */
-    public Builder withRecipientPublicKey(ECPublicKey val) throws GeneralSecurityException {
-      recipientPublicKey =
-          EllipticCurves.pointEncode(
-              WebPushConstants.NIST_P256_CURVE_TYPE,
-              WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-              val.getW());
-      return this;
-    }
-
-    /**
-     * Sets the public key of the recipient.
-     *
-     * <p>The public key must be formatted as an uncompressed point format, i.e., it has {@code 65}
-     * bytes and the first byte must be {@code 0x04}.
-     */
-    public Builder withRecipientPublicKey(final byte[] val) {
-      recipientPublicKey = val.clone();
-      return this;
-    }
-
-    /** Sets the private key of the recipient. */
-    public Builder withRecipientPrivateKey(ECPrivateKey val) throws GeneralSecurityException {
-      recipientPrivateKey = val;
-      return this;
-    }
-
-    /**
-     * Sets the private key of the recipient.
-     *
-     * <p>The private key is the serialized bytes of the BigInteger returned by
-     * {@link ECPrivateKey#getS()}.
-     */
-    public Builder withRecipientPrivateKey(final byte[] val) throws GeneralSecurityException {
-      recipientPrivateKey =
-          EllipticCurves.getEcPrivateKey(WebPushConstants.NIST_P256_CURVE_TYPE, val);
-      return this;
-    }
-
-    public WebPushHybridDecrypt build() throws GeneralSecurityException {
-      return new WebPushHybridDecrypt(this);
-    }
-  }
-
-  @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] contextInfo /* unused */)
-      throws GeneralSecurityException {
-    if (contextInfo != null) {
-      throw new GeneralSecurityException("contextInfo must be null because it is unused");
-    }
-
-    if (ciphertext.length < WebPushConstants.CIPHERTEXT_OVERHEAD) {
-      throw new GeneralSecurityException("ciphertext too short");
-    }
-
-    // A push service is not required to support more than 4096 octets of
-    // payload body. See https://tools.ietf.org/html/rfc8291#section-4.0.
-    if (ciphertext.length > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-      throw new GeneralSecurityException("ciphertext too long");
-    }
-
-    // Unpacking.
-    ByteBuffer record = ByteBuffer.wrap(ciphertext);
-    byte[] salt = new byte[WebPushConstants.SALT_SIZE];
-    record.get(salt);
-
-    int recordSize = record.getInt();
-    if (recordSize != this.recordSize
-        || recordSize < ciphertext.length
-        || recordSize > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-      throw new GeneralSecurityException("invalid record size: " + recordSize);
-    }
-
-    int publicKeySize = (int) record.get();
-    if (publicKeySize != WebPushConstants.PUBLIC_KEY_SIZE) {
-      throw new GeneralSecurityException("invalid ephemeral public key size: " + publicKeySize);
-    }
-
-    byte[] asPublicKey = new byte[WebPushConstants.PUBLIC_KEY_SIZE];
-    record.get(asPublicKey);
-    ECPoint asPublicPoint =
-        EllipticCurves.pointDecode(
-            WebPushConstants.NIST_P256_CURVE_TYPE,
-            WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-            asPublicKey);
-
-    byte[] payload = new byte[ciphertext.length - WebPushConstants.CONTENT_CODING_HEADER_SIZE];
-    record.get(payload);
-
-    // See https://tools.ietf.org/html/rfc8291#section-3.4.
-    byte[] ecdhSecret = EllipticCurves.computeSharedSecret(recipientPrivateKey, asPublicPoint);
-    byte[] ikm = WebPushUtil.computeIkm(ecdhSecret, authSecret, recipientPublicKey, asPublicKey);
-    byte[] cek = WebPushUtil.computeCek(ikm, salt);
-    byte[] nonce = WebPushUtil.computeNonce(ikm, salt);
-
-    return decrypt(cek, nonce, payload);
-  }
-
-  private byte[] decrypt(final byte[] key, final byte[] nonce, final byte[] ciphertext)
-      throws GeneralSecurityException {
-    Cipher cipher = EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding");
-    GCMParameterSpec params = new GCMParameterSpec(8 * WebPushConstants.TAG_SIZE, nonce);
-    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), params);
-    byte[] plaintext = cipher.doFinal(ciphertext);
-    if (plaintext.length == 0) {
-      throw new GeneralSecurityException("decryption failed");
-    }
-    // Remove zero paddings.
-    int index = plaintext.length - 1;
-    while (index > 0) {
-      if (plaintext[index] != 0) {
-        break;
-      }
-      index--;
-    }
-
-    if (plaintext[index] != WebPushConstants.PADDING_DELIMITER_BYTE) {
-      throw new GeneralSecurityException("decryption failed");
-    }
-    return Arrays.copyOf(plaintext, index);
-  }
-}
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncrypt.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncrypt.java
deleted file mode 100644
index dfb7555..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncrypt.java
+++ /dev/null
@@ -1,259 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EngineFactory;
-import com.google.crypto.tink.subtle.Random;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECPoint;
-import javax.crypto.Cipher;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * A {@link HybridEncrypt} implementation for the hybrid encryption used in <a
- * href="https://tools.ietf.org/html/rfc8291">RFC 8291 - Web Push Message Encryption</a>.
- *
- * <h3>Ciphertext format</h3>
- *
- * <p>When used with <a href="https://tools.ietf.org/html/rfc8291#section-4">AES128-GCM content
- * encoding</a>, which is the only content encoding supported in this implementation, the ciphertext
- * is formatted according to RFC 8188 section 2, and looks as follows
- *
- * <pre>
- * // NOLINTNEXTLINE
- * +-----------+----------------+------------------+---------------------------------------------------
- * | salt (16) | recordsize (4) | publickeylen (1) | publickey (publickeylen) | aes128-gcm-ciphertext |
- * +-----------+----------------+------------------+---------------------------------------------------
- * </pre>
- *
- * <p>RFC 8188 divides messages into records which are encrypted independently. Web Push messages
- * cannot be longer than 3993 bytes, and are always encrypted in a single record with default size
- * of 4096 bytes. {@code aes128-gcm-ciphertext} is the encryption of the message padded with a
- * single byte of value {@code 0x02} (which indicates that this is the last and only record).
- *
- * <h3>Usage</h3>
- *
- * <pre>{@code
- * import com.google.crypto.tink.HybridDecrypt;
- * import com.google.crypto.tink.HybridEncrypt;
- * import java.security.interfaces.ECPrivateKey;
- * import java.security.interfaces.ECPublicKey;
- *
- * // Encryption.
- * ECPublicKey reicipientPublicKey = ...;
- * byte[] authSecret = ...;
- * HybridEncrypt hybridEncrypt = new WebPushHybridEncrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .build();
- * byte[] plaintext = ...;
- * byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
- *
- * // Decryption.
- * ECPrivateKey recipientPrivateKey = ...;
- * HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .withRecipientPrivateKey(recipientPrivateKey)
- *      .build();
- * byte[] plaintext = hybridDecrypt.decrypt(ciphertext, null);
- * }</pre>
- *
- * @since 1.1.0
- */
-public final class WebPushHybridEncrypt implements HybridEncrypt {
-  private final byte[] recipientPublicKey;
-  private final byte[] authSecret;
-  private final ECPoint recipientPublicPoint;
-  private final int recordSize;
-  private final int paddingSize;
-
-  private WebPushHybridEncrypt(Builder builder) throws GeneralSecurityException {
-    if (builder.recipientPublicKey == null || builder.recipientPublicPoint == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's public key with Builder.withRecipientPublicKey");
-    }
-    this.recipientPublicKey = builder.recipientPublicKey;
-    this.recipientPublicPoint = builder.recipientPublicPoint;
-
-    if (builder.authSecret == null) {
-      throw new IllegalArgumentException("must set auth secret with Builder.withAuthSecret");
-    }
-    if (builder.authSecret.length != WebPushConstants.AUTH_SECRET_SIZE) {
-      throw new IllegalArgumentException(
-          "auth secret must have " + WebPushConstants.AUTH_SECRET_SIZE + " bytes");
-    }
-    this.authSecret = builder.authSecret;
-    this.recordSize = builder.recordSize;
-    this.paddingSize = builder.paddingSize;
-  }
-
-  /**
-   * Builder for {@link WebPushHybridEncrypt}.
-   *
-   * @since 1.1.0
-   */
-  public static final class Builder {
-    private byte[] recipientPublicKey = null;
-    private ECPoint recipientPublicPoint = null;
-    private byte[] authSecret = null;
-    private int recordSize = WebPushConstants.MAX_CIPHERTEXT_SIZE;
-    private int paddingSize = WebPushConstants.DEFAULT_PADDING_SIZE;
-
-    public Builder() {}
-
-    /**
-     * Sets the record size.
-     *
-     * <p>If set, this value must match the record size set with {@link
-     * WebPushHybridEncrypt.Builder#withRecordSize}.
-     *
-     * <p>If not set, a record size of 4096 bytes is used. This value should work for most users.
-     */
-    public Builder withRecordSize(int val) {
-      if (val < WebPushConstants.CIPHERTEXT_OVERHEAD
-          || val > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-        throw new IllegalArgumentException(
-            String.format(
-                "invalid record size (%s); must be a number between [%s, %s]",
-                val, WebPushConstants.CIPHERTEXT_OVERHEAD, WebPushConstants.MAX_CIPHERTEXT_SIZE));
-      }
-
-      recordSize = val;
-      return this;
-    }
-
-    /**
-     * Sets the padding size which is default to 0.
-     *
-     * <p>The padding size cannot be larger than
-     */
-    public Builder withPaddingSize(int val) {
-      if (val < 0
-          || val > WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD) {
-        throw new IllegalArgumentException(
-            String.format(
-                "invalid padding size (%s); must be a number between [%s, %s]",
-                val,
-                0,
-                WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD));
-      }
-
-      paddingSize = val;
-      return this;
-    }
-
-    /** Sets the authentication secret. */
-    public Builder withAuthSecret(final byte[] val) {
-      authSecret = val.clone();
-      return this;
-    }
-
-    /** Sets the public key of the recipient. */
-    public Builder withRecipientPublicKey(ECPublicKey val) throws GeneralSecurityException {
-      recipientPublicPoint = val.getW();
-      recipientPublicKey =
-          EllipticCurves.pointEncode(
-              WebPushConstants.NIST_P256_CURVE_TYPE,
-              WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-              val.getW());
-
-      return this;
-    }
-
-    /**
-     * Sets the public key of the recipient.
-     *
-     * <p>The public key must be formatted as an uncompressed point format, i.e., it has {@code 65}
-     * bytes and the first byte must be {@code 0x04}.
-     */
-    public Builder withRecipientPublicKey(final byte[] val) throws GeneralSecurityException {
-      recipientPublicKey = val.clone();
-      recipientPublicPoint =
-          EllipticCurves.pointDecode(
-              WebPushConstants.NIST_P256_CURVE_TYPE,
-              WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-              recipientPublicKey);
-      return this;
-    }
-
-    public WebPushHybridEncrypt build() throws GeneralSecurityException {
-      return new WebPushHybridEncrypt(this);
-    }
-  }
-
-  @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] contextInfo /* unused */)
-      throws GeneralSecurityException {
-    if (contextInfo != null) {
-      throw new GeneralSecurityException("contextInfo must be null because it is unused");
-    }
-
-    if (plaintext.length > recordSize - paddingSize - WebPushConstants.CIPHERTEXT_OVERHEAD) {
-      throw new GeneralSecurityException(
-          String.format(
-              "plaintext too long; with record size = %d and padding size = %d, plaintext cannot"
-                  + " be longer than %d",
-              recordSize,
-              paddingSize,
-              recordSize - paddingSize - WebPushConstants.CIPHERTEXT_OVERHEAD));
-    }
-
-    // See https://tools.ietf.org/html/rfc8291#section-3.4.
-    KeyPair keyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) keyPair.getPrivate();
-    ECPublicKey ephemeralPublicKey = (ECPublicKey) keyPair.getPublic();
-    byte[] ecdhSecret =
-        EllipticCurves.computeSharedSecret(ephemeralPrivateKey, recipientPublicPoint);
-    byte[] ephemeralPublicKeyBytes =
-        EllipticCurves.pointEncode(
-            WebPushConstants.NIST_P256_CURVE_TYPE,
-            WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-            ephemeralPublicKey.getW());
-    byte[] ikm =
-        WebPushUtil.computeIkm(ecdhSecret, authSecret, recipientPublicKey, ephemeralPublicKeyBytes);
-    byte[] salt = Random.randBytes(WebPushConstants.SALT_SIZE);
-    byte[] cek = WebPushUtil.computeCek(ikm, salt);
-    byte[] nonce = WebPushUtil.computeNonce(ikm, salt);
-    return ByteBuffer.allocate(
-            WebPushConstants.CIPHERTEXT_OVERHEAD + plaintext.length + paddingSize)
-        .put(salt)
-        .putInt(recordSize)
-        .put((byte) WebPushConstants.PUBLIC_KEY_SIZE)
-        .put(ephemeralPublicKeyBytes)
-        .put(encrypt(cek, nonce, plaintext))
-        .array();
-  }
-
-  private byte[] encrypt(final byte[] key, final byte[] nonce, final byte[] plaintext)
-      throws GeneralSecurityException {
-    Cipher cipher = EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding");
-    GCMParameterSpec params = new GCMParameterSpec(8 * WebPushConstants.TAG_SIZE, nonce);
-    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), params);
-    byte[] paddedPlaintext = new byte[plaintext.length + 1 + paddingSize];
-    paddedPlaintext[plaintext.length] = WebPushConstants.PADDING_DELIMITER_BYTE;
-    System.arraycopy(plaintext, 0, paddedPlaintext, 0, plaintext.length);
-    return cipher.doFinal(paddedPlaintext);
-  }
-}
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushUtil.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushUtil.java
deleted file mode 100644
index c2fc72e..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushUtil.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.Hkdf;
-import java.security.GeneralSecurityException;
-
-/** Various helpers. */
-final class WebPushUtil {
-  public static byte[] computeIkm(
-      final byte[] ecdhSecret,
-      final byte[] authSecret,
-      final byte[] uaPublic,
-      final byte[] asPublic)
-      throws GeneralSecurityException {
-    byte[] keyInfo = Bytes.concat(WebPushConstants.IKM_INFO, uaPublic, asPublic);
-    return Hkdf.computeHkdf(
-        WebPushConstants.HMAC_SHA256,
-        ecdhSecret /* ikm */,
-        authSecret /* salt */,
-        keyInfo,
-        WebPushConstants.IKM_SIZE);
-  }
-
-  public static byte[] computeCek(final byte[] ikm, final byte[] salt)
-      throws GeneralSecurityException {
-    return Hkdf.computeHkdf(
-        WebPushConstants.HMAC_SHA256,
-        ikm,
-        salt,
-        WebPushConstants.CEK_INFO,
-        WebPushConstants.CEK_KEY_SIZE);
-  }
-
-  public static byte[] computeNonce(final byte[] ikm, final byte[] salt)
-      throws GeneralSecurityException {
-    return Hkdf.computeHkdf(
-        WebPushConstants.HMAC_SHA256,
-        ikm,
-        salt,
-        WebPushConstants.NONCE_INFO,
-        WebPushConstants.NONCE_SIZE);
-  }
-
-  private WebPushUtil() {}
-}
diff --git a/apps/webpush/src/test/BUILD.bazel b/apps/webpush/src/test/BUILD.bazel
deleted file mode 100644
index 309b3ad..0000000
--- a/apps/webpush/src/test/BUILD.bazel
+++ /dev/null
@@ -1,38 +0,0 @@
-load("@tink_java//tools:gen_java_test_rules.bzl", "gen_java_test_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-# Tests
-
-java_library(
-    name = "generator_test",
-    testonly = 1,
-    srcs = glob([
-        "**/*.java",
-    ]),
-    deps = [
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_constants",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_decrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_encrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_util",
-        "@maven//:junit_junit",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hex",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
-    ],
-)
-
-gen_java_test_rules(
-    test_files = glob([
-        "**/*Test.java",
-    ]),
-    deps = [
-        ":generator_test",
-    ],
-)
diff --git a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecryptTest.java b/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecryptTest.java
deleted file mode 100644
index f8a6987..0000000
--- a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecryptTest.java
+++ /dev/null
@@ -1,243 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.webpush;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Random;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code WebPushHybridDecrypt}. */
-@RunWith(JUnit4.class)
-public class WebPushHybridDecryptTest {
-  // Copied from https://tools.ietf.org/html/rfc8291#section-5.
-  private static final String PLAINTEXT = "V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24";
-  private static final String RECEIVER_PRIVATE_KEY = "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94";
-  private static final String RECEIVER_PUBLIC_KEY =
-      "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4";
-  private static final String AUTH_SECRET = "BTBZMqHH6r4Tts7J_aSIgg";
-  private static final String CIPHERTEXT =
-      "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27ml"
-          + "mlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPT"
-          + "pK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN";
-  private static final int RECORD_SIZE = 4096;
-
-  @Test
-  public void testWithRfc8291TestVector() throws Exception {
-    byte[] plaintext = Base64.urlSafeDecode(PLAINTEXT);
-    byte[] recipientPrivateKey = Base64.urlSafeDecode(RECEIVER_PRIVATE_KEY);
-    byte[] recipientPublicKey = Base64.urlSafeDecode(RECEIVER_PUBLIC_KEY);
-    byte[] authSecret = Base64.urlSafeDecode(AUTH_SECRET);
-    byte[] ciphertext = Base64.urlSafeDecode(CIPHERTEXT);
-
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(RECORD_SIZE)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(recipientPublicKey)
-            .withRecipientPrivateKey(recipientPrivateKey)
-            .build();
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-
-  @Test
-  public void testEncryptDecryptWithInvalidRecordSizes() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    // Test with out of range record sizes.
-    {
-      try {
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE + 1)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-        fail("Expected IllegalArgumentException");
-      } catch (IllegalArgumentException ex) {
-        // expected.
-      }
-
-      try {
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD - 1)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-
-      } catch (IllegalArgumentException ex) {
-        // expected.
-      }
-    }
-
-    // Test with random mismatched record size.
-    {
-      for (int i = 0; i < 50; i++) {
-        int recordSize =
-            WebPushConstants.CIPHERTEXT_OVERHEAD
-                + Random.randInt(
-                    WebPushConstants.MAX_CIPHERTEXT_SIZE
-                        - WebPushConstants.CIPHERTEXT_OVERHEAD
-                        - 1);
-        HybridEncrypt hybridEncrypt =
-            new WebPushHybridEncrypt.Builder()
-                .withRecordSize(recordSize)
-                .withAuthSecret(authSecret)
-                .withRecipientPublicKey(uaPublicKey)
-                .build();
-        HybridDecrypt hybridDecrypt =
-            new WebPushHybridDecrypt.Builder()
-                .withRecordSize(recordSize + 1)
-                .withAuthSecret(authSecret)
-                .withRecipientPublicKey(uaPublicKey)
-                .withRecipientPrivateKey(uaPrivateKey)
-                .build();
-        byte[] plaintext = Random.randBytes(recordSize - WebPushConstants.CIPHERTEXT_OVERHEAD);
-        byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-
-        try {
-          hybridDecrypt.decrypt(ciphertext, null /* contextInfo */);
-          fail("Expected GeneralSecurityException");
-        } catch (GeneralSecurityException ex) {
-          // expected.
-        }
-      }
-    }
-  }
-
-  @Test
-  public void testNonNullContextInfo() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = Random.randBytes(20);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-    try {
-      byte[] contextInfo = new byte[0];
-      hybridDecrypt.decrypt(ciphertext, contextInfo);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException ex) {
-      // expected;
-    }
-  }
-
-  @Test
-  public void testModifyCiphertext() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = Random.randBytes(20);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-
-    // Flipping bits.
-    for (int b = 0; b < ciphertext.length; b++) {
-      for (int bit = 0; bit < 8; bit++) {
-        byte[] modified = Arrays.copyOf(ciphertext, ciphertext.length);
-        modified[b] ^= (byte) (1 << bit);
-        try {
-          byte[] unused = hybridDecrypt.decrypt(modified, null /* contextInfo */);
-          fail("Decrypting modified ciphertext should fail");
-        } catch (GeneralSecurityException ex) {
-          // This is expected.
-        }
-      }
-    }
-
-    // Truncate the message.
-    for (int length = 0; length < ciphertext.length; length++) {
-      byte[] modified = Arrays.copyOf(ciphertext, length);
-      try {
-        byte[] unused = hybridDecrypt.decrypt(modified, null /* contextInfo */);
-        fail("Decrypting modified ciphertext should fail");
-      } catch (GeneralSecurityException ex) {
-        // This is expected.
-      }
-    }
-  }
-
-  @Test
-  public void testEncryptDecrypt_withPadding_shouldWork() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    int paddingSize = 20;
-    int plaintextSize = 20;
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withPaddingSize(paddingSize)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = Random.randBytes(plaintextSize);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-
-    assertEquals(
-        ciphertext.length, plaintext.length + paddingSize + WebPushConstants.CIPHERTEXT_OVERHEAD);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-}
diff --git a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncryptTest.java b/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncryptTest.java
deleted file mode 100644
index 7b84b84..0000000
--- a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncryptTest.java
+++ /dev/null
@@ -1,244 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.apps.webpush;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Hex;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.Set;
-import java.util.TreeSet;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code WebPushHybridEncrypt}. */
-@RunWith(JUnit4.class)
-public class WebPushHybridEncryptTest {
-  @Test
-  public void testEncryptDecrypt() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] uaPublicKeyBytes =
-        EllipticCurves.pointEncode(
-            WebPushConstants.NIST_P256_CURVE_TYPE,
-            WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-            uaPublicKey.getW());
-    byte[] authSecret = Random.randBytes(16);
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKeyBytes)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKeyBytes)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-
-    Set<String> salts = new TreeSet<>();
-    Set<String> ephemeralPublicKeys = new TreeSet<>();
-    Set<String> payloads = new TreeSet<>();
-    int numTests = 100;
-    if (TestUtil.isTsan()) {
-      numTests = 5;
-    }
-    for (int j = 0; j < numTests; j++) {
-      byte[] plaintext = Random.randBytes(j);
-      byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-      assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-      assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-
-      // Checks that the encryption is randomized.
-      ByteBuffer record = ByteBuffer.wrap(ciphertext);
-      byte[] salt = new byte[WebPushConstants.SALT_SIZE];
-      record.get(salt);
-      salts.add(Hex.encode(salt));
-
-      int unused1 = record.getInt();
-      int unused2 = (int) record.get();
-
-      byte[] ephemeralPublicKey = new byte[WebPushConstants.PUBLIC_KEY_SIZE];
-      record.get(ephemeralPublicKey);
-      ephemeralPublicKeys.add(Hex.encode(ephemeralPublicKey));
-
-      byte[] payload = new byte[ciphertext.length - WebPushConstants.CONTENT_CODING_HEADER_SIZE];
-      record.get(payload);
-      payloads.add(Hex.encode(payload));
-    }
-    assertEquals(numTests, salts.size());
-    assertEquals(numTests, ephemeralPublicKeys.size());
-    assertEquals(numTests, payloads.size());
-  }
-
-  @Test
-  public void testEncryptDecryptWithVaryingRecordSizes() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    int numTests = 100;
-    if (TestUtil.isTsan()) {
-      numTests = 5;
-    }
-    // Test with random, valid record sizes.
-    for (int i = 0; i < numTests; i++) {
-      int recordSize =
-          WebPushConstants.CIPHERTEXT_OVERHEAD
-              + Random.randInt(
-                  WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD);
-      HybridEncrypt hybridEncrypt =
-          new WebPushHybridEncrypt.Builder()
-              .withRecordSize(recordSize)
-              .withAuthSecret(authSecret)
-              .withRecipientPublicKey(uaPublicKey)
-              .build();
-      HybridDecrypt hybridDecrypt =
-          new WebPushHybridDecrypt.Builder()
-              .withRecordSize(recordSize)
-              .withAuthSecret(authSecret)
-              .withRecipientPublicKey(uaPublicKey)
-              .withRecipientPrivateKey(uaPrivateKey)
-              .build();
-
-      byte[] plaintext = Random.randBytes(recordSize - WebPushConstants.CIPHERTEXT_OVERHEAD);
-      byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-      assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-      assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-    }
-  }
-
-  @Test
-  public void testEncryptDecrypt_largestPossibleRecordSize() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-    // Test with largest possible record size.
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext =
-        Random.randBytes(
-            WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-    assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-
-  @Test
-  public void testEncryptDecrypt_smallestPossibleRecordSize() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-    // Test with smallest possible record size.
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = new byte[0];
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-    assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-
-  @Test
-  public void testEncryptDecrypt_outOfRangeRecordSize_throws() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    try {
-      new WebPushHybridEncrypt.Builder()
-          .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE + 1)
-          .withAuthSecret(authSecret)
-          .withRecipientPublicKey(uaPublicKey)
-          .build();
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException ex) {
-      // expected.
-    }
-
-    try {
-      new WebPushHybridEncrypt.Builder()
-          .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD - 1)
-          .withAuthSecret(authSecret)
-          .withRecipientPublicKey(uaPublicKey)
-          .build();
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException ex) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testNonNullContextInfo() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    byte[] plaintext = Random.randBytes(20);
-    byte[] contextInfo = new byte[0];
-
-    try {
-      byte[] unusedCiphertext = hybridEncrypt.encrypt(plaintext, contextInfo);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException ex) {
-      // expected;
-    }
-  }
-}
-
diff --git a/cc/.bazelrc b/cc/.bazelrc
index 94aad05..e2435c9 100644
--- a/cc/.bazelrc
+++ b/cc/.bazelrc
@@ -1,3 +1,7 @@
 # Fix for grpc build error on macOS.
 # See: https://github.com/bazelbuild/bazel/issues/4341
-build --copt -DGRPC_BAZEL_BUILD
+build --copt=-DGRPC_BAZEL_BUILD
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/cc/.bazelversion b/cc/.bazelversion
index ac14c3d..09b254e 100644
--- a/cc/.bazelversion
+++ b/cc/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/cc/BUILD.bazel b/cc/BUILD.bazel
index 5064fdb..c88dd8e 100644
--- a/cc/BUILD.bazel
+++ b/cc/BUILD.bazel
@@ -23,8 +23,6 @@
         "aead_key_templates.h",
         "binary_keyset_reader.h",
         "binary_keyset_writer.h",
-        "catalogue.h",
-        "config.h",
         "deterministic_aead.h",
         "deterministic_aead_config.h",
         "deterministic_aead_factory.h",
@@ -77,6 +75,7 @@
         ":input_stream",
         ":json_keyset_reader",
         ":json_keyset_writer",
+        ":key",
         ":key_manager",
         ":keyset_handle",
         ":keyset_manager",
@@ -396,41 +395,6 @@
 )
 
 cc_library(
-    name = "catalogue",
-    hdrs = ["catalogue.h"],
-    include_prefix = "tink",
-    deps = [
-        ":key_manager",
-        "//util:statusor",
-        "@com_google_absl//absl/base:core_headers",
-    ],
-)
-
-cc_library(
-    name = "config",
-    srcs = ["core/config.cc"],
-    hdrs = ["config.h"],
-    include_prefix = "tink",
-    deps = [
-        ":catalogue",
-        ":key_manager",
-        ":registry",
-        "//aead:aead_config",
-        "//daead:deterministic_aead_config",
-        "//hybrid:hybrid_config",
-        "//mac:mac_config",
-        "//proto:config_cc_proto",
-        "//signature:signature_config",
-        "//streamingaead:streaming_aead_config",
-        "//util:errors",
-        "//util:status",
-        "//util:statusor",
-        "@com_google_absl//absl/status",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
-cc_library(
     name = "crypto_format",
     srcs = ["core/crypto_format.cc"],
     hdrs = ["crypto_format.h"],
@@ -453,6 +417,7 @@
         "//proto:tink_cc_proto",
         "//util:errors",
         "//util:statusor",
+        "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
@@ -507,20 +472,54 @@
     visibility = ["//visibility:public"],
     deps = [
         ":aead",
+        ":configuration",
+        ":insecure_secret_key_access",
+        ":key",
+        ":key_gen_configuration",
         ":key_manager",
+        ":key_status",
         ":keyset_reader",
         ":keyset_writer",
         ":primitive_set",
         ":registry",
+        "//internal:configuration_impl",
+        "//internal:key_gen_configuration_impl",
         "//internal:key_info",
+        "//internal:key_status_util",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:util",
         "//proto:tink_cc_proto",
         "//util:errors",
         "//util:keyset_util",
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "keyset_handle_builder",
+    srcs = ["core/keyset_handle_builder.cc"],
+    hdrs = ["keyset_handle_builder.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":key",
+        ":key_status",
+        ":keyset_handle",
+        ":parameters",
+        "//internal:keyset_handle_builder_entry",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
     ],
 )
 
@@ -565,9 +564,9 @@
     include_prefix = "tink",
     visibility = ["//visibility:public"],
     deps = [
+        ":key_gen_configuration",
         ":keyset_handle",
-        ":keyset_reader",
-        ":registry",
+        "//internal:key_gen_configuration_impl",
         "//proto:tink_cc_proto",
         "//util:enums",
         "//util:errors",
@@ -703,6 +702,19 @@
 )
 
 cc_library(
+    name = "partial_key_access_token",
+    hdrs = ["partial_key_access_token.h"],
+    include_prefix = "tink",
+)
+
+cc_library(
+    name = "partial_key_access",
+    hdrs = ["partial_key_access.h"],
+    include_prefix = "tink",
+    deps = [":partial_key_access_token"],
+)
+
+cc_library(
     name = "secret_key_access_token",
     hdrs = ["secret_key_access_token.h"],
     include_prefix = "tink",
@@ -712,9 +724,31 @@
     name = "insecure_secret_key_access",
     hdrs = ["insecure_secret_key_access.h"],
     include_prefix = "tink",
+    visibility = ["//visibility:public"],
     deps = [":secret_key_access_token"],
 )
 
+cc_library(
+    name = "restricted_data",
+    srcs = ["core/restricted_data.cc"],
+    hdrs = ["restricted_data.h"],
+    include_prefix = "tink",
+    deps = [
+        ":secret_key_access_token",
+        "//subtle:random",
+        "//util:secret_data",
+        "@boringssl//:crypto",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
+cc_library(
+    name = "key_status",
+    hdrs = ["key_status.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+)
+
 # tests
 
 cc_test(
@@ -723,6 +757,7 @@
     srcs = ["core/version_test.cc"],
     deps = [
         ":version",
+        "//internal:util",
         "@com_google_googletest//:gtest_main",
     ],
 )
@@ -788,18 +823,6 @@
 )
 
 cc_test(
-    name = "config_test",
-    size = "small",
-    srcs = ["core/config_test.cc"],
-    deps = [
-        ":config",
-        ":mac",
-        "//proto:config_cc_proto",
-        "@com_google_googletest//:gtest_main",
-    ],
-)
-
-cc_test(
     name = "crypto_format_test",
     size = "small",
     srcs = ["core/crypto_format_test.cc"],
@@ -820,6 +843,8 @@
         ":core/key_manager_impl",
         ":json_keyset_reader",
         ":json_keyset_writer",
+        ":key_gen_configuration",
+        ":key_status",
         ":keyset_handle",
         ":primitive_set",
         ":primitive_wrapper",
@@ -827,11 +852,16 @@
         "//aead:aead_key_templates",
         "//aead:aead_wrapper",
         "//aead:aes_gcm_key_manager",
+        "//config:fips_140_2",
+        "//config:key_gen_fips_140_2",
         "//config:tink_config",
+        "//config/internal:global_registry",
+        "//internal:fips_utils",
+        "//internal:key_gen_configuration_impl",
+        "//proto:aes_gcm_siv_cc_proto",
         "//proto:tink_cc_proto",
         "//signature:ecdsa_sign_key_manager",
         "//signature:signature_key_templates",
-        "//util:protobuf_helper",
         "//util:status",
         "//util:test_keyset_handle",
         "//util:test_matchers",
@@ -843,6 +873,34 @@
 )
 
 cc_test(
+    name = "keyset_handle_builder_test",
+    srcs = ["core/keyset_handle_builder_test.cc"],
+    deps = [
+        ":insecure_secret_key_access",
+        ":key_status",
+        ":keyset_handle_builder",
+        ":partial_key_access",
+        "//config:tink_config",
+        "//internal:legacy_proto_key",
+        "//internal:legacy_proto_parameters",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//mac:aes_cmac_key",
+        "//mac:aes_cmac_parameters",
+        "//mac:mac_key_templates",
+        "//proto:aes_cmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:status",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
     name = "key_manager_test",
     size = "small",
     srcs = ["core/key_manager_test.cc"],
@@ -861,7 +919,6 @@
     size = "small",
     srcs = ["core/keyset_manager_test.cc"],
     deps = [
-        ":config",
         ":keyset_handle",
         ":keyset_manager",
         "//aead:aead_config",
@@ -893,9 +950,11 @@
     size = "small",
     srcs = ["core/primitive_set_test.cc"],
     deps = [
+        ":cleartext_keyset_handle",
         ":crypto_format",
         ":mac",
         ":primitive_set",
+        "//keyderivation:keyset_deriver",
         "//proto:tink_cc_proto",
         "//util:test_matchers",
         "//util:test_util",
@@ -999,3 +1058,82 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "core/partial_key_access_token_test",
+    srcs = ["core/partial_key_access_token_test.cc"],
+    deps = [
+        ":partial_key_access",
+        ":partial_key_access_token",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "core/restricted_data_test",
+    srcs = ["core/restricted_data_test.cc"],
+    deps = [
+        ":insecure_secret_key_access",
+        ":restricted_data",
+        "//subtle:random",
+        "//util:secret_data",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "proto_keyset_format",
+    srcs = ["proto_keyset_format.cc"],
+    hdrs = ["proto_keyset_format.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":binary_keyset_reader",
+        ":binary_keyset_writer",
+        ":cleartext_keyset_handle",
+        ":keyset_handle",
+        ":secret_key_access_token",
+        "//util:secret_data",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "proto_keyset_format_test",
+    srcs = ["proto_keyset_format_test.cc"],
+    deps = [
+        ":insecure_secret_key_access",
+        ":keyset_handle_builder",
+        ":mac",
+        ":proto_keyset_format",
+        "//config:tink_config",
+        "//internal:legacy_proto_parameters",
+        "//internal:proto_parameters_serialization",
+        "//mac:mac_key_templates",
+        "//signature:signature_key_templates",
+        "//util:secret_data",
+        "//util:test_matchers",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "configuration",
+    hdrs = ["configuration.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//internal:key_type_info_store",
+        "//internal:keyset_wrapper_store",
+    ],
+)
+
+cc_library(
+    name = "key_gen_configuration",
+    hdrs = ["key_gen_configuration.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = ["//internal:key_type_info_store"],
+)
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index 2ef21b2..e13bee0 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -181,19 +181,6 @@
   public_configs = [ "//third_party/tink:tink_config" ]
 }
 
-# CC Library : catalogue
-source_set("catalogue") {
-  configs += [ "//build/config:no_rtti" ]
-  configs -= [ "//build/config:no_rtti" ]
-  sources = [ "catalogue.h" ]
-  public_deps = [
-    ":key_manager",
-    "//third_party/abseil-cpp/absl/base:core_headers",
-    "//third_party/tink/cc/util:statusor",
-  ]
-  public_configs = [ "//third_party/tink:tink_config" ]
-}
-
 # CC Library : crypto_format
 source_set("crypto_format") {
   configs += [ "//build/config:no_rtti" ]
@@ -221,6 +208,7 @@
   ]
   public_deps = [
     ":crypto_format",
+    "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
     "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/status:status",
@@ -268,17 +256,30 @@
   ]
   public_deps = [
     ":aead",
+    ":configuration",
+    ":insecure_secret_key_access",
+    ":key",
+    ":key_gen_configuration",
     ":key_manager",
+    ":key_status",
     ":keyset_reader",
     ":keyset_writer",
     ":primitive_set",
     ":registry",
     "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
+    "//third_party/abseil-cpp/absl/log:check",
     "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc/internal:configuration_impl",
+    "//third_party/tink/cc/internal:key_gen_configuration_impl",
     "//third_party/tink/cc/internal:key_info",
+    "//third_party/tink/cc/internal:key_status_util",
+    "//third_party/tink/cc/internal:mutable_serialization_registry",
+    "//third_party/tink/cc/internal:proto_key_serialization",
+    "//third_party/tink/cc/internal:util",
     "//third_party/tink/cc/proto:tink_proto",
     "//third_party/tink/cc/util:errors",
     "//third_party/tink/cc/util:keyset_util",
@@ -433,3 +434,104 @@
   ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
+
+# CC Library : parameters
+source_set("parameters") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "parameters.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key
+source_set("key") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key.h" ]
+  public_deps = [
+    ":parameters",
+    "//third_party/abseil-cpp/absl/types:optional",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : partial_key_access_token
+source_set("partial_key_access_token") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "partial_key_access_token.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : partial_key_access
+source_set("partial_key_access") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "partial_key_access.h" ]
+  public_deps = [ ":partial_key_access_token" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : secret_key_access_token
+source_set("secret_key_access_token") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "secret_key_access_token.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : insecure_secret_key_access
+source_set("insecure_secret_key_access") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "insecure_secret_key_access.h" ]
+  public_deps = [ ":secret_key_access_token" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : restricted_data
+source_set("restricted_data") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "core/restricted_data.cc",
+    "restricted_data.h",
+  ]
+  public_deps = [
+    ":secret_key_access_token",
+    "//third_party/abseil-cpp/absl/log:check",
+    "//third_party/boringssl:crypto",
+    "//third_party/tink/cc/subtle:random",
+    "//third_party/tink/cc/util:secret_data",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_status
+source_set("key_status") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_status.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : configuration
+source_set("configuration") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "configuration.h" ]
+  public_deps = [
+    "//third_party/tink/cc/internal:key_type_info_store",
+    "//third_party/tink/cc/internal:keyset_wrapper_store",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_gen_configuration
+source_set("key_gen_configuration") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_gen_configuration.h" ]
+  public_deps = [ "//third_party/tink/cc/internal:key_type_info_store" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
diff --git a/cc/CMakeLists.txt b/cc/CMakeLists.txt
index a7defba..1cd07e4 100644
--- a/cc/CMakeLists.txt
+++ b/cc/CMakeLists.txt
@@ -6,6 +6,7 @@
 add_subdirectory(mac)
 add_subdirectory(monitoring)
 add_subdirectory(jwt)
+add_subdirectory(keyderivation)
 add_subdirectory(prf)
 add_subdirectory(signature)
 add_subdirectory(streamingaead)
@@ -16,12 +17,6 @@
 
 # Configuration settings for the build.
 
-if (TINK_USE_ABSL_STATUS)
-    target_compile_definitions(tink_util_status PUBLIC TINK_USE_ABSL_STATUS)
-endif()
-if (TINK_USE_ABSL_STATUSOR)
-    target_compile_definitions(tink_util_statusor INTERFACE TINK_USE_ABSL_STATUSOR)
-endif()
 if(USE_ONLY_FIPS)
     target_compile_definitions(tink_internal_fips_utils PUBLIC TINK_USE_ONLY_FIPS)
 endif()
@@ -39,9 +34,7 @@
     aead_key_templates.h
     binary_keyset_reader.h
     binary_keyset_writer.h
-    catalogue.h
     cleartext_keyset_handle.h
-    config.h
     deterministic_aead.h
     deterministic_aead_config.h
     deterministic_aead_factory.h
@@ -92,6 +85,7 @@
     tink::core::input_stream
     tink::core::json_keyset_reader
     tink::core::json_keyset_writer
+    tink::core::key
     tink::core::key_manager
     tink::core::keyset_handle
     tink::core::keyset_manager
@@ -373,39 +367,6 @@
 )
 
 tink_cc_library(
-  NAME catalogue
-  SRCS
-    catalogue.h
-  DEPS
-    tink::core::key_manager
-    absl::core_headers
-    tink::util::statusor
-)
-
-tink_cc_library(
-  NAME config
-  SRCS
-    core/config.cc
-    config.h
-  DEPS
-    tink::core::catalogue
-    tink::core::key_manager
-    tink::core::registry
-    absl::status
-    absl::strings
-    tink::aead::aead_config
-    tink::daead::deterministic_aead_config
-    tink::hybrid::hybrid_config
-    tink::mac::mac_config
-    tink::signature::signature_config
-    tink::streamingaead::streaming_aead_config
-    tink::util::errors
-    tink::util::status
-    tink::util::statusor
-    tink::proto::config_cc_proto
-)
-
-tink_cc_library(
   NAME crypto_format
   SRCS
     core/crypto_format.cc
@@ -424,6 +385,7 @@
     primitive_set.h
   DEPS
     tink::core::crypto_format
+    absl::core_headers
     absl::flat_hash_map
     absl::memory
     absl::status
@@ -472,23 +434,55 @@
     keyset_handle.h
   DEPS
     tink::core::aead
+    tink::core::configuration
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::key_gen_configuration
     tink::core::key_manager
+    tink::core::key_status
     tink::core::keyset_reader
     tink::core::keyset_writer
     tink::core::primitive_set
     tink::core::registry
     absl::core_headers
     absl::flat_hash_map
+    absl::check
     absl::memory
     absl::status
     absl::strings
+    absl::optional
+    tink::internal::configuration_impl
+    tink::internal::key_gen_configuration_impl
     tink::internal::key_info
+    tink::internal::key_status_util
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::util
     tink::util::errors
     tink::util::keyset_util
     tink::proto::tink_cc_proto
 )
 
 tink_cc_library(
+  NAME keyset_handle_builder
+  SRCS
+    core/keyset_handle_builder.cc
+    keyset_handle_builder.h
+  DEPS
+    tink::core::key
+    tink::core::key_status
+    tink::core::keyset_handle
+    tink::core::parameters
+    absl::check
+    absl::status
+    absl::strings
+    absl::optional
+    tink::internal::keyset_handle_builder_entry
+    tink::subtle::random
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
   NAME cleartext_keyset_handle
   SRCS
     core/cleartext_keyset_handle.cc
@@ -525,13 +519,13 @@
     core/keyset_manager.cc
     keyset_manager.h
   DEPS
+    tink::core::key_gen_configuration
     tink::core::keyset_handle
-    tink::core::keyset_reader
-    tink::core::registry
     absl::core_headers
     absl::memory
     absl::status
     absl::synchronization
+    tink::internal::key_gen_configuration_impl
     tink::util::enums
     tink::util::errors
     tink::util::status
@@ -658,6 +652,26 @@
 )
 
 tink_cc_library(
+  NAME partial_key_access_token
+  SRCS
+    partial_key_access_token.h
+)
+
+tink_cc_library(
+  NAME partial_key_access
+  SRCS
+    partial_key_access.h
+  DEPS
+    tink::core::partial_key_access_token
+)
+
+tink_cc_library(
+  NAME secret_key_access_token
+  SRCS
+    secret_key_access_token.h
+)
+
+tink_cc_library(
   NAME insecure_secret_key_access
   SRCS
     insecure_secret_key_access.h
@@ -666,9 +680,22 @@
 )
 
 tink_cc_library(
-  NAME secret_key_access_token
+  NAME restricted_data
   SRCS
-    secret_key_access_token.h
+    core/restricted_data.cc
+    restricted_data.h
+  DEPS
+    tink::core::secret_key_access_token
+    absl::check
+    crypto
+    tink::subtle::random
+    tink::util::secret_data
+)
+
+tink_cc_library(
+  NAME key_status
+  SRCS
+    key_status.h
 )
 
 # tests
@@ -680,6 +707,7 @@
   DEPS
     tink::core::version
     gmock
+    tink::internal::util
 )
 
 tink_cc_test(
@@ -739,17 +767,6 @@
 )
 
 tink_cc_test(
-  NAME config_test
-  SRCS
-    core/config_test.cc
-  DEPS
-    tink::core::config
-    tink::core::mac
-    gmock
-    tink::proto::config_cc_proto
-)
-
-tink_cc_test(
   NAME crypto_format_test
   SRCS
     core/crypto_format_test.cc
@@ -770,6 +787,8 @@
     tink::core::key_manager_impl
     tink::core::json_keyset_reader
     tink::core::json_keyset_writer
+    tink::core::key_gen_configuration
+    tink::core::key_status
     tink::core::keyset_handle
     tink::core::primitive_set
     tink::core::primitive_wrapper
@@ -779,14 +798,47 @@
     tink::aead::aead_key_templates
     tink::aead::aead_wrapper
     tink::aead::aes_gcm_key_manager
+    tink::config::fips_140_2
+    tink::config::key_gen_fips_140_2
     tink::config::tink_config
+    tink::config::internal::global_registry
+    tink::internal::fips_utils
+    tink::internal::key_gen_configuration_impl
     tink::signature::ecdsa_sign_key_manager
     tink::signature::signature_key_templates
-    tink::util::protobuf_helper
     tink::util::status
     tink::util::test_keyset_handle
     tink::util::test_matchers
     tink::util::test_util
+    tink::proto::aes_gcm_siv_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME keyset_handle_builder_test
+  SRCS
+    core/keyset_handle_builder_test.cc
+  DEPS
+    tink::core::insecure_secret_key_access
+    tink::core::key_status
+    tink::core::keyset_handle_builder
+    tink::core::partial_key_access
+    gmock
+    absl::status
+    absl::strings
+    tink::config::tink_config
+    tink::internal::legacy_proto_key
+    tink::internal::legacy_proto_parameters
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::mac::aes_cmac_key
+    tink::mac::aes_cmac_parameters
+    tink::mac::mac_key_templates
+    tink::subtle::random
+    tink::util::status
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_cmac_cc_proto
     tink::proto::tink_cc_proto
 )
 
@@ -808,7 +860,6 @@
   SRCS
     core/keyset_manager_test.cc
   DEPS
-    tink::core::config
     tink::core::keyset_handle
     tink::core::keyset_manager
     gmock
@@ -838,10 +889,12 @@
   SRCS
     core/primitive_set_test.cc
   DEPS
+    tink::core::cleartext_keyset_handle
     tink::core::crypto_format
     tink::core::mac
     tink::core::primitive_set
     gmock
+    tink::keyderivation::keyset_deriver
     tink::util::test_matchers
     tink::util::test_util
     tink::proto::tink_cc_proto
@@ -942,3 +995,78 @@
     tink::core::secret_key_access_testonly
     gmock
 )
+
+tink_cc_test(
+  NAME partial_key_access_token_test
+  SRCS
+    core/partial_key_access_token_test.cc
+  DEPS
+    tink::core::partial_key_access
+    tink::core::partial_key_access_token
+    gmock
+    absl::core_headers
+)
+
+tink_cc_test(
+  NAME restricted_data_test
+  SRCS
+    core/restricted_data_test.cc
+  DEPS
+    tink::core::insecure_secret_key_access
+    tink::core::restricted_data
+    gmock
+    tink::subtle::random
+    tink::util::secret_data
+)
+
+tink_cc_library(
+  NAME proto_keyset_format
+  SRCS
+    proto_keyset_format.cc
+    proto_keyset_format.h
+  DEPS
+    tink::core::binary_keyset_reader
+    tink::core::binary_keyset_writer
+    tink::core::cleartext_keyset_handle
+    tink::core::keyset_handle
+    tink::core::secret_key_access_token
+    absl::strings
+    tink::util::secret_data
+)
+
+tink_cc_test(
+  NAME proto_keyset_format_test
+  SRCS
+    proto_keyset_format_test.cc
+  DEPS
+    tink::core::insecure_secret_key_access
+    tink::core::keyset_handle_builder
+    tink::core::mac
+    tink::core::proto_keyset_format
+    gmock
+    absl::strings
+    tink::config::tink_config
+    tink::internal::legacy_proto_parameters
+    tink::internal::proto_parameters_serialization
+    tink::mac::mac_key_templates
+    tink::signature::signature_key_templates
+    tink::util::secret_data
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME configuration
+  SRCS
+    configuration.h
+  DEPS
+    tink::internal::key_type_info_store
+    tink::internal::keyset_wrapper_store
+)
+
+tink_cc_library(
+  NAME key_gen_configuration
+  SRCS
+    key_gen_configuration.h
+  DEPS
+    tink::internal::key_type_info_store
+)
diff --git a/cc/MODULE.bazel b/cc/MODULE.bazel
new file mode 100644
index 0000000..ab23a48
--- /dev/null
+++ b/cc/MODULE.bazel
@@ -0,0 +1,32 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tink C++ Bazel Module definition."""
+module(
+    name = "tink_cc",
+    version = "2.0.0",
+)
+
+bazel_dep(name = "rules_cc", version = "0.0.5")
+bazel_dep(name = "rules_proto", version = "5.3.0-21.7")
+bazel_dep(name = "platforms", version = "0.0.6")
+bazel_dep(name = "bazel_skylib", version = "1.3.0")
+bazel_dep(name = "googletest", version = "1.12.1", repo_name = "com_google_googletest")
+bazel_dep(name = "protobuf", version = "21.7", repo_name = "com_google_protobuf")
+bazel_dep(name = "boringssl", version = "0.0.0-20230215-5c22014")
+bazel_dep(name = "rapidjson", version = "1.1.0")
+bazel_dep(name = "abseil-cpp", version = "20230125.1", repo_name="com_google_absl")
+
+wycheproof_extension = use_extension("//:extensions.bzl", "wycheproof_extension")
+use_repo(wycheproof_extension, "wycheproof")
diff --git a/cc/WORKSPACE b/cc/WORKSPACE
index ca79c62..af5976a 100644
--- a/cc/WORKSPACE
+++ b/cc/WORKSPACE
@@ -13,9 +13,3 @@
 load("@tink_cc//:tink_cc_deps_init.bzl", "tink_cc_deps_init")
 
 tink_cc_deps_init()
-
-load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
-
-# Creates a default toolchain config for RBE. Use this as is if you are
-# using the rbe_ubuntu16_04 container, otherwise refer to RBE docs.
-rbe_autoconfig(name = "rbe_default")
diff --git a/cc/WORKSPACE.bzlmod b/cc/WORKSPACE.bzlmod
new file mode 100644
index 0000000..057d6aa
--- /dev/null
+++ b/cc/WORKSPACE.bzlmod
@@ -0,0 +1 @@
+# This replaces the content of the WORKSPACE file when using --enable_bzlmod.
diff --git a/cc/aead.h b/cc/aead.h
index 803fc51..45017d6 100644
--- a/cc/aead.h
+++ b/cc/aead.h
@@ -51,7 +51,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const = 0;
 
-  virtual ~Aead() {}
+  virtual ~Aead() = default;
 };
 
 }  // namespace tink
diff --git a/cc/aead/BUILD.bazel b/cc/aead/BUILD.bazel
index 28b3e8b..10648c4 100644
--- a/cc/aead/BUILD.bazel
+++ b/cc/aead/BUILD.bazel
@@ -214,6 +214,7 @@
         "//subtle:random",
         "//util:constants",
         "//util:enums",
+        "//util:input_stream_util",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -281,6 +282,7 @@
     deps = [
         "//:aead",
         "//:registry",
+        "//aead/internal:aead_util",
         "//proto:tink_cc_proto",
         "//util:status",
         "//util:statusor",
@@ -302,6 +304,7 @@
         "//:core/template_util",
         "//:kms_client",
         "//:kms_clients",
+        "//aead/internal:aead_util",
         "//internal:fips_utils",
         "//proto:kms_envelope_cc_proto",
         "//proto:tink_cc_proto",
@@ -329,6 +332,95 @@
     ],
 )
 
+cc_library(
+    name = "failing_aead",
+    testonly = 1,
+    srcs = ["failing_aead.cc"],
+    hdrs = ["failing_aead.h"],
+    include_prefix = "tink/aead",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:aead",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aead_parameters",
+    hdrs = ["aead_parameters.h"],
+    include_prefix = "tink/aead",
+    deps = ["//:parameters"],
+)
+
+cc_library(
+    name = "aead_key",
+    hdrs = ["aead_key.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aead_parameters",
+        "//:key",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aes_gcm_parameters",
+    srcs = ["aes_gcm_parameters.cc"],
+    hdrs = ["aes_gcm_parameters.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aead_parameters",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aes_gcm_key",
+    srcs = ["aes_gcm_key.cc"],
+    hdrs = ["aes_gcm_key.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aead_key",
+        ":aes_gcm_parameters",
+        "//:partial_key_access_token",
+        "//:restricted_data",
+        "//subtle:subtle_util",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "aes_gcm_proto_serialization",
+    srcs = ["aes_gcm_proto_serialization.cc"],
+    hdrs = ["aes_gcm_proto_serialization.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//internal:key_parser",
+        "//internal:key_serializer",
+        "//internal:mutable_serialization_registry",
+        "//internal:parameters_parser",
+        "//internal:parameters_serializer",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
 # tests
 
 cc_test(
@@ -373,11 +465,11 @@
         "//:primitive_set",
         "//:registry",
         "//config:tink_fips",
+        "//internal:fips_utils",
         "//proto:tink_cc_proto",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
-        "//util:test_util",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_googletest//:gtest_main",
@@ -516,7 +608,6 @@
     deps = [
         ":aes_ctr_hmac_aead_key_manager",
         "//:aead",
-        "//:mac",
         "//proto:aes_ctr_cc_proto",
         "//proto:aes_ctr_hmac_aead_cc_proto",
         "//proto:common_cc_proto",
@@ -526,6 +617,7 @@
         "//subtle:aead_test_util",
         "//subtle:aes_ctr_boringssl",
         "//util:enums",
+        "//util:istream_input_stream",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -589,7 +681,9 @@
         ":aead_key_templates",
         ":kms_envelope_aead",
         "//:aead",
+        "//:keyset_handle",
         "//:registry",
+        "//internal:ssl_util",
         "//mac:mac_key_templates",
         "//proto:aes_gcm_cc_proto",
         "//util:status",
@@ -599,6 +693,7 @@
         "@com_google_absl//absl/base:endian",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
 )
@@ -608,6 +703,7 @@
     size = "small",
     srcs = ["kms_envelope_aead_key_manager_test.cc"],
     deps = [
+        ":aead_config",
         ":aead_key_templates",
         ":aes_eax_key_manager",
         ":kms_envelope_aead",
@@ -616,9 +712,11 @@
         "//:kms_client",
         "//:kms_clients",
         "//:registry",
+        "//mac:mac_key_templates",
         "//proto:kms_envelope_cc_proto",
         "//proto:tink_cc_proto",
         "//subtle:aead_test_util",
+        "//util:fake_kms_client",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
@@ -650,19 +748,6 @@
     ],
 )
 
-cc_library(
-    name = "failing_aead",
-    testonly = 1,
-    srcs = ["failing_aead.cc"],
-    hdrs = ["failing_aead.h"],
-    include_prefix = "tink/aead",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//:aead",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
 cc_test(
     name = "failing_aead_test",
     srcs = ["failing_aead_test.cc"],
@@ -673,3 +758,51 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "aes_gcm_parameters_test",
+    srcs = ["aes_gcm_parameters_test.cc"],
+    deps = [
+        ":aes_gcm_parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_gcm_key_test",
+    srcs = ["aes_gcm_key_test.cc"],
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_gcm_proto_serialization_test",
+    size = "small",
+    srcs = ["aes_gcm_proto_serialization_test.cc"],
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        ":aes_gcm_proto_serialization",
+        "//:insecure_secret_key_access",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/aead/BUILD.gn b/cc/aead/BUILD.gn
index f4dc781..6eae32e 100644
--- a/cc/aead/BUILD.gn
+++ b/cc/aead/BUILD.gn
@@ -204,6 +204,7 @@
     "//third_party/tink/cc/subtle:random",
     "//third_party/tink/cc/util:constants",
     "//third_party/tink/cc/util:enums",
+    "//third_party/tink/cc/util:input_stream_util",
     "//third_party/tink/cc/util:secret_data",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
@@ -277,6 +278,7 @@
     "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/tink/cc:aead",
     "//third_party/tink/cc:registry",
+    "//third_party/tink/cc/aead/internal:aead_util",
     "//third_party/tink/cc/proto:tink_proto",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
@@ -302,6 +304,7 @@
     "//third_party/tink/cc:core/template_util",
     "//third_party/tink/cc:kms_client",
     "//third_party/tink/cc:kms_clients",
+    "//third_party/tink/cc/aead/internal:aead_util",
     "//third_party/tink/cc/internal:fips_utils",
     "//third_party/tink/cc/proto:kms_envelope_proto",
     "//third_party/tink/cc/proto:tink_proto",
diff --git a/cc/aead/CMakeLists.txt b/cc/aead/CMakeLists.txt
index c4adc4e..41264b9 100644
--- a/cc/aead/CMakeLists.txt
+++ b/cc/aead/CMakeLists.txt
@@ -199,6 +199,7 @@
     tink::subtle::random
     tink::util::constants
     tink::util::enums
+    tink::util::input_stream_util
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -266,6 +267,7 @@
     absl::strings
     tink::core::aead
     tink::core::registry
+    tink::aead::internal::aead_util
     tink::util::status
     tink::util::statusor
     tink::proto::tink_cc_proto
@@ -286,6 +288,7 @@
     tink::core::template_util
     tink::core::kms_client
     tink::core::kms_clients
+    tink::aead::internal::aead_util
     tink::internal::fips_utils
     tink::util::constants
     tink::util::status
@@ -304,6 +307,91 @@
     absl::strings
     tink::core::aead
     tink::util::statusor
+  TESTONLY
+)
+
+tink_cc_library(
+  NAME failing_aead
+  SRCS
+    failing_aead.cc
+    failing_aead.h
+  DEPS
+    absl::strings
+    tink::core::aead
+  TESTONLY
+)
+
+tink_cc_library(
+  NAME aead_parameters
+  SRCS
+    aead_parameters.h
+  DEPS
+    tink::core::parameters
+)
+
+tink_cc_library(
+  NAME aead_key
+  SRCS
+    aead_key.h
+  DEPS
+    tink::aead::aead_parameters
+    absl::strings
+    tink::core::key
+)
+
+tink_cc_library(
+  NAME aes_gcm_parameters
+  SRCS
+    aes_gcm_parameters.cc
+    aes_gcm_parameters.h
+  DEPS
+    tink::aead::aead_parameters
+    absl::strings
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_gcm_key
+  SRCS
+    aes_gcm_key.cc
+    aes_gcm_key.h
+  DEPS
+    tink::aead::aead_key
+    tink::aead::aes_gcm_parameters
+    absl::strings
+    absl::optional
+    tink::core::partial_key_access_token
+    tink::core::restricted_data
+    tink::subtle::subtle_util
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_gcm_proto_serialization
+  SRCS
+    aes_gcm_proto_serialization.cc
+    aes_gcm_proto_serialization.h
+  DEPS
+    tink::aead::aes_gcm_key
+    tink::aead::aes_gcm_parameters
+    absl::status
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::mutable_serialization_registry
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::util::status
+    tink::util::statusor
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::tink_cc_proto
 )
 
 # tests
@@ -351,10 +439,10 @@
     tink::core::primitive_set
     tink::core::registry
     tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
-    tink::util::test_util
     tink::proto::tink_cc_proto
 )
 
@@ -487,11 +575,11 @@
     gmock
     absl::status
     tink::core::aead
-    tink::core::mac
     tink::subtle::subtle
     tink::subtle::aead_test_util
     tink::subtle::aes_ctr_boringssl
     tink::util::enums
+    tink::util::istream_input_stream
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -558,8 +646,11 @@
     absl::endian
     absl::memory
     absl::status
+    absl::strings
     tink::core::aead
+    tink::core::keyset_handle
     tink::core::registry
+    tink::internal::ssl_util
     tink::mac::mac_key_templates
     tink::util::status
     tink::util::statusor
@@ -573,6 +664,7 @@
   SRCS
     kms_envelope_aead_key_manager_test.cc
   DEPS
+    tink::aead::aead_config
     tink::aead::aead_key_templates
     tink::aead::aes_eax_key_manager
     tink::aead::kms_envelope_aead
@@ -584,7 +676,9 @@
     tink::core::kms_client
     tink::core::kms_clients
     tink::core::registry
+    tink::mac::mac_key_templates
     tink::subtle::aead_test_util
+    tink::util::fake_kms_client
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
@@ -612,16 +706,6 @@
     tink::proto::tink_cc_proto
 )
 
-tink_cc_library(
-  NAME failing_aead
-  SRCS
-    failing_aead.cc
-    failing_aead.h
-  DEPS
-    absl::strings
-    tink::core::aead
-)
-
 tink_cc_test(
   NAME failing_aead_test
   SRCS
@@ -632,3 +716,50 @@
     absl::status
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME aes_gcm_parameters_test
+  SRCS
+    aes_gcm_parameters_test.cc
+  DEPS
+    tink::aead::aes_gcm_parameters
+    gmock
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_gcm_key_test
+  SRCS
+    aes_gcm_key_test.cc
+  DEPS
+    tink::aead::aes_gcm_key
+    tink::aead::aes_gcm_parameters
+    gmock
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_gcm_proto_serialization_test
+  SRCS
+    aes_gcm_proto_serialization_test.cc
+  DEPS
+    tink::aead::aes_gcm_key
+    tink::aead::aes_gcm_parameters
+    tink::aead::aes_gcm_proto_serialization
+    gmock
+    tink::core::insecure_secret_key_access
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::subtle::random
+    tink::util::test_matchers
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/aead/aead_config.cc b/cc/aead/aead_config.cc
index d627064..b5a84ca 100644
--- a/cc/aead/aead_config.cc
+++ b/cc/aead/aead_config.cc
@@ -34,15 +34,6 @@
 
 namespace crypto {
 namespace tink {
-
-using ::google::crypto::tink::RegistryConfig;
-
-// static
-const RegistryConfig& AeadConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
 // static
 util::Status AeadConfig::Register() {
   auto status = MacConfig::Register();
diff --git a/cc/aead/aead_config.h b/cc/aead/aead_config.h
index 6431d64..08cb196 100644
--- a/cc/aead/aead_config.h
+++ b/cc/aead/aead_config.h
@@ -34,14 +34,6 @@
 //
 class AeadConfig {
  public:
-  static constexpr char kCatalogueName[] = "TinkAead";
-  static constexpr char kPrimitiveName[] = "Aead";
-
-  // Returns config of Aead implementations supported
-  // in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers Aead primitive wrapper and key managers for all Aead key types
   // from the current Tink release.
   static crypto::tink::util::Status Register();
diff --git a/cc/aead/aead_config_test.cc b/cc/aead/aead_config_test.cc
index 726e3d1..4a7945d 100644
--- a/cc/aead/aead_config_test.cc
+++ b/cc/aead/aead_config_test.cc
@@ -29,27 +29,24 @@
 #include "tink/aead/aead_key_templates.h"
 #include "tink/aead/aes_gcm_key_manager.h"
 #include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/keyset_handle.h"
 #include "tink/primitive_set.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
-#include "tink/util/test_util.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-using ::crypto::tink::test::DummyAead;
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
-using ::google::crypto::tink::KeysetInfo;
-using ::google::crypto::tink::KeyStatusType;
+using ::crypto::tink::util::StatusOr;
 using ::google::crypto::tink::KeyTemplate;
-using ::google::crypto::tink::OutputPrefixType;
-using ::testing::Eq;
+using ::testing::IsNull;
 using ::testing::Not;
 using ::testing::Test;
 
@@ -80,35 +77,17 @@
 
   ASSERT_THAT(AeadConfig::Register(), IsOk());
 
-  KeysetInfo::KeyInfo key_info;
-  key_info.set_status(KeyStatusType::ENABLED);
-  key_info.set_key_id(1234);
-  key_info.set_output_prefix_type(OutputPrefixType::RAW);
-  auto primitive_set = absl::make_unique<PrimitiveSet<Aead>>();
-  ASSERT_THAT(primitive_set->set_primary(*primitive_set->AddPrimitive(
-                  absl::make_unique<DummyAead>("dummy"), key_info)),
-              IsOk());
-
-  util::StatusOr<std::unique_ptr<Aead>> primitive_result =
-      Registry::Wrap(std::move(primitive_set));
-
-  ASSERT_THAT(primitive_result, IsOk());
-  util::StatusOr<std::string> encryption_result =
-      (*primitive_result)->Encrypt("secret", "");
-  ASSERT_THAT(encryption_result, IsOk());
-
-  util::StatusOr<std::string> decryption_result =
-      DummyAead("dummy").Decrypt(*encryption_result, "");
-  ASSERT_THAT(decryption_result, IsOk());
-  EXPECT_THAT(*decryption_result, Eq("secret"));
-
-  decryption_result = DummyAead("dummy").Decrypt(*encryption_result, "wrong");
-  EXPECT_THAT(decryption_result, Not(IsOk()));
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(keyset_handle.status(), IsOk());
+  StatusOr<std::unique_ptr<Aead>> aead = (*keyset_handle)->GetPrimitive<Aead>();
+  ASSERT_THAT(aead.status(), IsOk());
+  ASSERT_THAT(*aead, Not(IsNull()));
 }
 
 // FIPS-only mode tests
 TEST_F(AeadConfigTest, RegisterNonFipsTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto.";
   }
 
@@ -128,7 +107,7 @@
 }
 
 TEST_F(AeadConfigTest, RegisterFipsValidTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto.";
   }
 
@@ -148,7 +127,7 @@
 }
 
 TEST_F(AeadConfigTest, RegisterFailsIfBoringCryptoNotAvailable) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Only supported in FIPS-only mode with BoringCrypto not available.";
   }
diff --git a/cc/aead/aead_key.h b/cc/aead/aead_key.h
new file mode 100644
index 0000000..869ddc8
--- /dev/null
+++ b/cc/aead/aead_key.h
@@ -0,0 +1,52 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AEAD_KEY_H_
+#define TINK_AEAD_AEAD_KEY_H_
+
+#include "absl/strings/string_view.h"
+#include "tink/aead/aead_parameters.h"
+#include "tink/key.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents a function to encrypt and decrypt data using authenticated
+// encryption with associated data (AEAD).
+class AeadKey : public Key {
+ public:
+  // Returns the bytes prefixed to every ciphertext generated by this key.
+  //
+  // In order to make key rotation more efficient, Tink allows every AEAD key to
+  // have an associated ciphertext output prefix. When decrypting a ciphertext,
+  // only keys with a matching prefix have to be tried.
+  //
+  // Note that a priori, the output prefix may not be unique in a keyset
+  // (i.e., different keys in a keyset may have the same prefix or one prefix
+  // may be a prefix of another). To avoid this, built-in Tink keys use the
+  // convention that the prefix is either '0x00<big endian key id>' or
+  // '0x01<big endian key id>'.
+  virtual absl::string_view GetOutputPrefix() const = 0;
+
+  const AeadParameters& GetParameters() const override = 0;
+
+  bool operator==(const Key& other) const override = 0;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AEAD_KEY_H_
diff --git a/cc/aead/aead_parameters.h b/cc/aead/aead_parameters.h
new file mode 100644
index 0000000..dc5caad
--- /dev/null
+++ b/cc/aead/aead_parameters.h
@@ -0,0 +1,32 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AEAD_PARAMETERS_H_
+#define TINK_AEAD_AEAD_PARAMETERS_H_
+
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes an `AeadKey` (e.g., key attributes), excluding the randomly chosen
+// key material.
+class AeadParameters : public Parameters {};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AEAD_PARAMETERS_H_
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager.cc b/cc/aead/aes_ctr_hmac_aead_key_manager.cc
index ffe7867..10a0634 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager.cc
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager.cc
@@ -34,6 +34,7 @@
 #include "tink/subtle/ind_cpa_cipher.h"
 #include "tink/subtle/random.h"
 #include "tink/util/enums.h"
+#include "tink/util/input_stream_util.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -52,12 +53,14 @@
 constexpr int kMinTagSizeInBytes = 10;
 }
 
-using crypto::tink::util::Enums;
-using crypto::tink::util::Status;
-using crypto::tink::util::StatusOr;
-using google::crypto::tink::AesCtrHmacAeadKey;
-using google::crypto::tink::AesCtrHmacAeadKeyFormat;
-using google::crypto::tink::HashType;
+using ::crypto::tink::util::Enums;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::AesCtrHmacAeadKey;
+using ::google::crypto::tink::AesCtrHmacAeadKeyFormat;
+using ::google::crypto::tink::AesCtrKey;
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HmacKey;
 
 StatusOr<AesCtrHmacAeadKey> AesCtrHmacAeadKeyManager::CreateKey(
     const AesCtrHmacAeadKeyFormat& aes_ctr_hmac_aead_key_format) const {
@@ -176,5 +179,53 @@
   return HmacKeyManager().ValidateKeyFormat(key_format.hmac_key_format());
 }
 
+// To ensure the resulting key can provide key commitment, the AES-CTR key must
+// be derived first, then the HMAC key. This avoids situation where it's
+// possible to brute force raw key material so that the 32th byte of the
+// keystream is a 0 Give party A a key with this raw key material, saying that
+// the size of the HMAC key is 32 bytes and the size of the AES key is 16 bytes.
+// Give party B a key with this raw key material, saying that the size of the
+// HMAC key is 31 bytes and the size of the AES key is 16 bytes. Since HMAC will
+// pad the key with zeroes, this leads to both parties using the same HMAC key,
+// but a different AES key (offset by 1 byte)
+StatusOr<AesCtrHmacAeadKey> AesCtrHmacAeadKeyManager::DeriveKey(
+    const AesCtrHmacAeadKeyFormat& key_format,
+    InputStream* input_stream) const {
+  Status status = ValidateKeyFormat(key_format);
+  if (!status.ok()) {
+    return status;
+  }
+  StatusOr<std::string> aes_ctr_randomness = ReadBytesFromStream(
+      key_format.aes_ctr_key_format().key_size(), input_stream);
+  if (!aes_ctr_randomness.ok()) {
+    if (absl::IsOutOfRange(aes_ctr_randomness.status())) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Could not get enough pseudorandomness from input stream");
+    }
+    return aes_ctr_randomness.status();
+  }
+  StatusOr<HmacKey> hmac_key =
+      HmacKeyManager().DeriveKey(key_format.hmac_key_format(), input_stream);
+  if (!hmac_key.ok()) {
+    return hmac_key.status();
+  }
+
+  google::crypto::tink::AesCtrHmacAeadKey key;
+  key.set_version(get_version());
+  *key.mutable_hmac_key() = hmac_key.value();
+
+  AesCtrKey* aes_ctr_key = key.mutable_aes_ctr_key();
+  aes_ctr_key->set_version(get_version());
+  aes_ctr_key->set_key_value(aes_ctr_randomness.value());
+  *aes_ctr_key->mutable_params() = key_format.aes_ctr_key_format().params();
+
+  status = ValidateKey(key);
+  if (!status.ok()) {
+    return status;
+  }
+  return key;
+}
+
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager.h b/cc/aead/aes_ctr_hmac_aead_key_manager.h
index a241a9c..e3b2f42 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager.h
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager.h
@@ -67,6 +67,10 @@
   CreateKey(const google::crypto::tink::AesCtrHmacAeadKeyFormat& key_format)
       const override;
 
+  crypto::tink::util::StatusOr<google::crypto::tink::AesCtrHmacAeadKey>
+  DeriveKey(const google::crypto::tink::AesCtrHmacAeadKeyFormat& key_format,
+            InputStream* input_stream) const override;
+
   internal::FipsCompatibility FipsStatus() const override {
     return internal::FipsCompatibility::kRequiresBoringCrypto;
   }
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc b/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc
index 6815ec3..b84711a 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc
@@ -19,6 +19,7 @@
 #include <stdint.h>
 
 #include <memory>
+#include <sstream>
 #include <string>
 #include <utility>
 
@@ -26,13 +27,13 @@
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "tink/aead.h"
-#include "tink/mac.h"
 #include "tink/subtle/aead_test_util.h"
 #include "tink/subtle/aes_ctr_boringssl.h"
 #include "tink/subtle/encrypt_then_authenticate.h"
 #include "tink/subtle/hmac_boringssl.h"
 #include "tink/subtle/ind_cpa_cipher.h"
 #include "tink/util/enums.h"
+#include "tink/util/istream_input_stream.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -48,11 +49,11 @@
 
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
+using ::crypto::tink::util::IstreamInputStream;
 using ::crypto::tink::util::StatusOr;
 using ::google::crypto::tink::AesCtrHmacAeadKey;
 using ::google::crypto::tink::AesCtrHmacAeadKeyFormat;
 using ::google::crypto::tink::HashType;
-using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
 using ::testing::Not;
 using ::testing::SizeIs;
@@ -146,14 +147,24 @@
   AesCtrHmacAeadKeyFormat key_format = CreateValidKeyFormat();
   for (int len = 0; len < 42; ++len) {
     key_format.mutable_aes_ctr_key_format()->set_key_size(len);
+    IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde0123456789abcdefghijklmnopqrztuvwxyz0123456789abcde01"
+      "23456789abcdefghijklmnopqrztuvwxyz0123456789abcde0123456789abcdefghi"
+      "jklmnopqrztuvwxyz")};
     if (len == 16 || len == 32) {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   IsOk())
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          IsOk());
     } else {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   Not(IsOk()))
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          Not(IsOk()));
     }
   }
 }
@@ -162,14 +173,24 @@
   AesCtrHmacAeadKeyFormat key_format = CreateValidKeyFormat();
   for (int len = 0; len < 42; ++len) {
     key_format.mutable_hmac_key_format()->set_key_size(len);
+    IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde0123456789abcdefghijklmnopqrztuvwxyz0123456789abcde01"
+      "23456789abcdefghijklmnopqrztuvwxyz0123456789abcde0123456789abcdefghi"
+      "jklmnopqrztuvwxyz")};
     if (len >= 16) {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   IsOk())
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          IsOk());
     } else {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   Not(IsOk()))
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          Not(IsOk()));
     }
   }
 }
@@ -222,6 +243,92 @@
               IsOk());
 }
 
+TEST(AesCtrHmacAeadKeyManagerTest, Derive16ByteKey) {
+  AesCtrHmacAeadKeyFormat key_format;
+  key_format.mutable_aes_ctr_key_format()->set_key_size(16);
+  key_format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  key_format.mutable_hmac_key_format()->set_key_size(16);
+  key_format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  key_format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  key_format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde_YELLOW_SUBMARINE_EXTRA")};
+
+  StatusOr<AesCtrHmacAeadKey> derived_key =
+      AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream);
+  ASSERT_THAT(derived_key, IsOk());
+  EXPECT_THAT(derived_key.value().aes_ctr_key().key_value(),
+              Eq("0123456789abcde_"));
+  EXPECT_THAT(derived_key.value().hmac_key().key_value(),
+              Eq("YELLOW_SUBMARINE"));
+  EXPECT_THAT(derived_key.value().hmac_key().params().hash(),
+              key_format.hmac_key_format().params().hash());
+  EXPECT_THAT(derived_key.value().hmac_key().params().tag_size(),
+              key_format.hmac_key_format().params().tag_size());
+  EXPECT_THAT(derived_key.value().aes_ctr_key().params().iv_size(),
+              Eq(key_format.aes_ctr_key_format().params().iv_size()));
+}
+
+TEST(AesCtrHmacAeadKeyManagerTest, Derive32ByteKey) {
+  AesCtrHmacAeadKeyFormat format;
+  format.mutable_aes_ctr_key_format()->set_key_size(32);
+  format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  format.mutable_hmac_key_format()->set_key_size(32);
+  format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde0123456789abcdef_YELLOW_SUBMARINE_YELLOW_SUBMARIN")};
+
+  StatusOr<AesCtrHmacAeadKey> derived_key =
+      AesCtrHmacAeadKeyManager().DeriveKey(format, &input_stream);
+  ASSERT_THAT(derived_key, IsOk());
+  EXPECT_THAT(derived_key.value().aes_ctr_key().key_value(),
+              Eq("0123456789abcde0123456789abcdef_"));
+  EXPECT_THAT(derived_key.value().hmac_key().key_value(),
+              Eq("YELLOW_SUBMARINE_YELLOW_SUBMARIN"));
+}
+
+TEST(AesCtrHmacAeadKeyManagerTest, DeriveKeyNotEnoughRandomnessForAesCtrKey) {
+  AesCtrHmacAeadKeyFormat format;
+  format.mutable_aes_ctr_key_format()->set_key_size(32);
+  format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  format.mutable_hmac_key_format()->set_key_size(32);
+  format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{
+      absl::make_unique<std::stringstream>("0123456789")};
+
+  ASSERT_THAT(
+      AesCtrHmacAeadKeyManager().DeriveKey(format, &input_stream).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesCtrHmacAeadKeyManagerTest, DeriveKeyNotEnoughRandomnessForHmacKey) {
+  AesCtrHmacAeadKeyFormat format;
+  format.mutable_aes_ctr_key_format()->set_key_size(16);
+  format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  format.mutable_hmac_key_format()->set_key_size(32);
+  format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{
+      absl::make_unique<std::stringstream>("YELLOW_SUBMARINE")};
+
+  ASSERT_THAT(
+      AesCtrHmacAeadKeyManager().DeriveKey(format, &input_stream).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/aead/aes_gcm_key.cc b/cc/aead/aes_gcm_key.cc
new file mode 100644
index 0000000..4e93443
--- /dev/null
+++ b/cc/aead/aes_gcm_key.cc
@@ -0,0 +1,107 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_key.h"
+
+#include <string>
+
+#include "absl/strings/escaping.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/subtle_util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::StatusOr<std::string> ComputeOutputPrefix(
+    const AesGcmParameters& parameters, absl::optional<int> id_requirement) {
+  switch (parameters.GetVariant()) {
+    case AesGcmParameters::Variant::kNoPrefix:
+      return std::string("");  // Empty prefix.
+    case AesGcmParameters::Variant::kCrunchy:
+      if (!id_requirement.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "id requirement must have value with kCrunchy or kLegacy");
+      }
+      return absl::StrCat(absl::HexStringToBytes("00"),
+                          subtle::BigEndian32(*id_requirement));
+    case AesGcmParameters::Variant::kTink:
+      if (!id_requirement.has_value()) {
+        return util::Status(absl::StatusCode::kInvalidArgument,
+                            "id requirement must have value with kTink");
+      }
+      return absl::StrCat(absl::HexStringToBytes("01"),
+                          subtle::BigEndian32(*id_requirement));
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat("Invalid variant: ", parameters.GetVariant()));
+  }
+}
+
+}  // namespace
+
+util::StatusOr<AesGcmKey> AesGcmKey::Create(const AesGcmParameters& parameters,
+                                            const RestrictedData& key_bytes,
+                                            absl::optional<int> id_requirement,
+                                            PartialKeyAccessToken token) {
+  if (parameters.KeySizeInBytes() != key_bytes.size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Key size does not match AES-GCM parameters");
+  }
+  if (parameters.HasIdRequirement() && !id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key without ID requirement with parameters with ID "
+        "requirement");
+  }
+  if (!parameters.HasIdRequirement() && id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key with ID requirement with parameters without ID "
+        "requirement");
+  }
+  util::StatusOr<std::string> output_prefix =
+      ComputeOutputPrefix(parameters, id_requirement);
+  if (!output_prefix.ok()) {
+    return output_prefix.status();
+  }
+  return AesGcmKey(parameters, key_bytes, id_requirement,
+                   *std::move(output_prefix));
+}
+
+bool AesGcmKey::operator==(const Key& other) const {
+  const AesGcmKey* that = dynamic_cast<const AesGcmKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (GetParameters() != that->GetParameters()) {
+    return false;
+  }
+  if (id_requirement_ != that->id_requirement_) {
+    return false;
+  }
+  return key_bytes_ == that->key_bytes_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_key.h b/cc/aead/aes_gcm_key.h
new file mode 100644
index 0000000..8159ea7
--- /dev/null
+++ b/cc/aead/aes_gcm_key.h
@@ -0,0 +1,84 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AES_GCM_KEY_H_
+#define TINK_AEAD_AES_GCM_KEY_H_
+
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aead_key.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents an AEAD that uses AES-GCM.
+class AesGcmKey : public AeadKey {
+ public:
+  // Copyable and movable.
+  AesGcmKey(const AesGcmKey& other) = default;
+  AesGcmKey& operator=(const AesGcmKey& other) = default;
+  AesGcmKey(AesGcmKey&& other) = default;
+  AesGcmKey& operator=(AesGcmKey&& other) = default;
+
+  // Creates a new AES-GCM key.  If the parameters specify a variant that uses
+  // a prefix, then the id is used to compute this prefix.
+  static util::StatusOr<AesGcmKey> Create(const AesGcmParameters& parameters,
+                                          const RestrictedData& key_bytes,
+                                          absl::optional<int> id_requirement,
+                                          PartialKeyAccessToken token);
+
+  // Returns the underlying AES key.
+  util::StatusOr<RestrictedData> GetKeyBytes(
+      PartialKeyAccessToken token) const {
+    return key_bytes_;
+  }
+
+  absl::string_view GetOutputPrefix() const override { return output_prefix_; }
+
+  const AesGcmParameters& GetParameters() const override { return parameters_; }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  AesGcmKey(const AesGcmParameters& parameters, const RestrictedData& key_bytes,
+            absl::optional<int> id_requirement,
+            std::string output_prefix)
+      : parameters_(parameters),
+        key_bytes_(key_bytes),
+        id_requirement_(id_requirement),
+        output_prefix_(std::move(output_prefix)) {}
+
+  AesGcmParameters parameters_;
+  RestrictedData key_bytes_;
+  absl::optional<int> id_requirement_;
+  std::string output_prefix_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AES_GCM_KEY_H_
diff --git a/cc/aead/aes_gcm_key_test.cc b/cc/aead/aes_gcm_key_test.cc
new file mode 100644
index 0000000..06a0a35
--- /dev/null
+++ b/cc/aead/aes_gcm_key_test.cc
@@ -0,0 +1,285 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_key.h"
+
+#include <string>
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesGcmParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using AesGcmKeyTest = TestWithParam<std::tuple<int, int, TestCase>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesGcmKeyTestSuite, AesGcmKeyTest,
+    Combine(Values(16, 24, 32), Range(12, 16),
+            Values(TestCase{AesGcmParameters::Variant::kTink, 0x02030400,
+                            std::string("\x01\x02\x03\x04\x00", 5)},
+                   TestCase{AesGcmParameters::Variant::kCrunchy, 0x01030005,
+                            std::string("\x00\x01\x03\x00\x05", 5)},
+                   TestCase{AesGcmParameters::Variant::kNoPrefix, absl::nullopt,
+                            ""})));
+
+TEST_P(AesGcmKeyTest, CreateSucceeds) {
+  int key_size;
+  int iv_and_tag_size;  // NOTE: There's no requirement for IV size == tag size.
+  TestCase test_case;
+  std::tie(key_size, iv_and_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  EXPECT_THAT(key->GetParameters(), Eq(*params));
+  EXPECT_THAT(key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(key->GetOutputPrefix(), Eq(test_case.output_prefix));
+}
+
+TEST(AesGcmKeyTest, CreateKeyWithMismatchedKeySizeFails) {
+  // Key size parameter is 32 bytes.
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  // Key material is 16 bytes (another valid key length).
+  RestrictedData mismatched_secret = RestrictedData(/*num_random_bytes=*/16);
+
+  EXPECT_THAT(AesGcmKey::Create(*params, mismatched_secret,
+                                /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmKeyTest, CreateKeyWithInvalidIdRequirementFails) {
+  util::StatusOr<AesGcmParameters> no_prefix_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .Build();
+  ASSERT_THAT(no_prefix_params, IsOk());
+
+  util::StatusOr<AesGcmParameters> tink_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  EXPECT_THAT(AesGcmKey::Create(*no_prefix_params, secret,
+                                /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(
+      AesGcmKey::Create(*tink_params, secret,
+                        /*id_requirement=*/absl::nullopt, GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmKeyTest, GetKeyBytes) {
+  int key_size;
+  int iv_and_tag_size;  // NOTE: There's no requirement for IV size == tag size.
+  TestCase test_case;
+  std::tie(key_size, iv_and_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  EXPECT_THAT(key->GetKeyBytes(GetPartialKeyAccess()), IsOkAndHolds(secret));
+}
+
+TEST_P(AesGcmKeyTest, KeyEquals) {
+  int key_size;
+  int iv_and_tag_size;  // NOTE: There's no requirement for IV size == tag size.
+  TestCase test_case;
+  std::tie(key_size, iv_and_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key == *other_key);
+  EXPECT_TRUE(*other_key == *key);
+  EXPECT_FALSE(*key != *other_key);
+  EXPECT_FALSE(*other_key != *key);
+}
+
+TEST(AesGcmKeyTest, DifferentVariantNotEqual) {
+  util::StatusOr<AesGcmParameters> crunchy_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kCrunchy)
+          .Build();
+  ASSERT_THAT(crunchy_params, IsOk());
+
+  util::StatusOr<AesGcmParameters> tink_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesGcmKey> key =
+      AesGcmKey::Create(*crunchy_params, secret, /*id_requirement=*/0x01020304,
+                        GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key =
+      AesGcmKey::Create(*tink_params, secret, /*id_requirement=*/0x01020304,
+                        GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesGcmKeyTest, DifferentSecretDataNotEqual) {
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret1 = RestrictedData(/*num_random_bytes=*/32);
+  RestrictedData secret2 = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret1, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key = AesGcmKey::Create(
+      *params, secret2, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesGcmKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key = AesGcmKey::Create(
+      *params, secret, /*id_requirement=*/0x02030405, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_parameters.cc b/cc/aead/aes_gcm_parameters.cc
new file mode 100644
index 0000000..d78d888
--- /dev/null
+++ b/cc/aead/aes_gcm_parameters.cc
@@ -0,0 +1,103 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_parameters.h"
+
+#include <set>
+
+#include "absl/strings/str_cat.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetKeySizeInBytes(
+    int key_size) {
+  key_size_in_bytes_ = key_size;
+  return *this;
+}
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetIvSizeInBytes(
+    int iv_size) {
+  iv_size_in_bytes_ = iv_size;
+  return *this;
+}
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetTagSizeInBytes(
+    int tag_size) {
+  tag_size_in_bytes_ = tag_size;
+  return *this;
+}
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetVariant(
+    Variant variant) {
+  variant_ = variant;
+  return *this;
+}
+
+util::StatusOr<AesGcmParameters> AesGcmParameters::Builder::Build() {
+  if (key_size_in_bytes_ != 16 && key_size_in_bytes_ != 24 &&
+      key_size_in_bytes_ != 32) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Key size should be 16, 24, or 32 bytes, got ",
+                     key_size_in_bytes_, " bytes."));
+  }
+  if (iv_size_in_bytes_ <= 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("IV size should be positive, got ",
+                                     iv_size_in_bytes_, " bytes."));
+  }
+  if (tag_size_in_bytes_ < 12 || tag_size_in_bytes_ > 16) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Tag size should be between 12 and 16 bytes, got ",
+                     tag_size_in_bytes_, " bytes."));
+  }
+  static const std::set<Variant>* supported_variants = new std::set<Variant>(
+      {Variant::kTink, Variant::kCrunchy, Variant::kNoPrefix});
+  if (supported_variants->find(variant_) == supported_variants->end()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create AES-GCM parameters with unknown variant.");
+  }
+  return AesGcmParameters(key_size_in_bytes_, iv_size_in_bytes_,
+                          tag_size_in_bytes_, variant_);
+}
+
+bool AesGcmParameters::operator==(const Parameters& other) const {
+  const AesGcmParameters* that = dynamic_cast<const AesGcmParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (key_size_in_bytes_ != that->key_size_in_bytes_) {
+    return false;
+  }
+  if (iv_size_in_bytes_ != that->iv_size_in_bytes_) {
+    return false;
+  }
+  if (tag_size_in_bytes_ != that->tag_size_in_bytes_) {
+    return false;
+  }
+  if (variant_ != that->variant_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_parameters.h b/cc/aead/aes_gcm_parameters.h
new file mode 100644
index 0000000..de87b2c
--- /dev/null
+++ b/cc/aead/aes_gcm_parameters.h
@@ -0,0 +1,105 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AES_GCM_PARAMETERS_H_
+#define TINK_AEAD_AES_GCM_PARAMETERS_H_
+
+#include "tink/aead/aead_parameters.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes the parameters of an `AesGcmKey`.
+class AesGcmParameters : public AeadParameters {
+ public:
+  // Description of the output prefix prepended to the ciphertext.
+  enum class Variant : int {
+    // Prepends '0x01<big endian key id>' to the ciphertext.
+    kTink = 1,
+    // Prepends '0x00<big endian key id>' to the ciphertext.
+    kCrunchy = 2,
+    // Does not prepend any prefix (i.e., keys must have no ID requirement).
+    kNoPrefix = 3,
+    // Added to guard from failures that may be caused by future expansions.
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Creates AES-GCM parameters instances.
+  class Builder {
+   public:
+    // Copyable and movable.
+    Builder(const Builder& other) = default;
+    Builder& operator=(const Builder& other) = default;
+    Builder(Builder&& other) = default;
+    Builder& operator=(Builder&& other) = default;
+
+    // Creates initially empty parameters builder.
+    Builder() = default;
+
+    Builder& SetKeySizeInBytes(int key_size);
+    Builder& SetIvSizeInBytes(int iv_size);
+    Builder& SetTagSizeInBytes(int tag_size);
+    Builder& SetVariant(Variant variant);
+
+    // Creates AES-GCM parameters object from this builder.
+    util::StatusOr<AesGcmParameters> Build();
+
+   private:
+    int key_size_in_bytes_;
+    int iv_size_in_bytes_;
+    int tag_size_in_bytes_;
+    Variant variant_;
+  };
+
+  // Copyable and movable.
+  AesGcmParameters(const AesGcmParameters& other) = default;
+  AesGcmParameters& operator=(const AesGcmParameters& other) = default;
+  AesGcmParameters(AesGcmParameters&& other) = default;
+  AesGcmParameters& operator=(AesGcmParameters&& other) = default;
+
+  int KeySizeInBytes() const { return key_size_in_bytes_; }
+
+  int IvSizeInBytes() const { return iv_size_in_bytes_; }
+
+  int TagSizeInBytes() const { return tag_size_in_bytes_; }
+
+  Variant GetVariant() const { return variant_; }
+
+  bool HasIdRequirement() const override {
+    return variant_ != Variant::kNoPrefix;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  AesGcmParameters(int key_size_in_bytes, int iv_size_in_bytes,
+                   int tag_size_in_bytes, Variant variant)
+      : key_size_in_bytes_(key_size_in_bytes),
+        iv_size_in_bytes_(iv_size_in_bytes),
+        tag_size_in_bytes_(tag_size_in_bytes),
+        variant_(variant) {}
+
+  int key_size_in_bytes_;
+  int iv_size_in_bytes_;
+  int tag_size_in_bytes_;
+  Variant variant_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AES_GCM_PARAMETERS_H_
diff --git a/cc/aead/aes_gcm_parameters_test.cc b/cc/aead/aes_gcm_parameters_test.cc
new file mode 100644
index 0000000..7238bb9
--- /dev/null
+++ b/cc/aead/aes_gcm_parameters_test.cc
@@ -0,0 +1,386 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_parameters.h"
+
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct BuildTestCase {
+  AesGcmParameters::Variant variant;
+  int key_size;
+  int iv_size;
+  int tag_size;
+  bool has_id_requirement;
+};
+
+using AesGcmParametersBuildTest = TestWithParam<BuildTestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesGcmParametersBuildTestSuite, AesGcmParametersBuildTest,
+    Values(BuildTestCase{AesGcmParameters::Variant::kTink, /*key_size=*/16,
+                         /*iv_size=*/12, /*tag_size=*/12,
+                         /*has_id_requirement=*/true},
+           BuildTestCase{AesGcmParameters::Variant::kCrunchy, /*key_size=*/24,
+                         /*iv_size=*/14, /*tag_size=*/14,
+                         /*has_id_requirement=*/true},
+           BuildTestCase{AesGcmParameters::Variant::kNoPrefix,
+                         /*key_size=*/32, /*iv_size=*/16, /*tag_size=*/16,
+                         /*has_id_requirement=*/false}));
+
+TEST_P(AesGcmParametersBuildTest, Build) {
+  BuildTestCase test_case = GetParam();
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  EXPECT_THAT(parameters->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(parameters->IvSizeInBytes(), Eq(test_case.iv_size));
+  EXPECT_THAT(parameters->TagSizeInBytes(), Eq(test_case.tag_size));
+  EXPECT_THAT(parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(parameters->HasIdRequirement(), Eq(test_case.has_id_requirement));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingVariantFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidVariantFails) {
+  EXPECT_THAT(
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::
+                          kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
+          .Build()
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingKeySizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidKeySizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(15)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(17)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(23)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(25)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(31)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(33)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingIvSizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidIvSizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(0)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingTagSizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidTagSizeFails) {
+  // Too small.
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(11)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big.
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(17)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, CopyConstructor) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  AesGcmParameters copy(*parameters);
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.IvSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.TagSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.GetVariant(), Eq(AesGcmParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+TEST(AesGcmParametersTest, CopyAssignment) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  AesGcmParameters copy = *parameters;
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.IvSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.TagSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.GetVariant(), Eq(AesGcmParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+using AesGcmParametersVariantTest =
+    TestWithParam<std::tuple<int, int, AesGcmParameters::Variant>>;
+
+INSTANTIATE_TEST_SUITE_P(AesGcmParametersVariantTestSuite,
+                         AesGcmParametersVariantTest,
+                         Combine(Values(16, 24, 32), Range(12, 16),
+                                 Values(AesGcmParameters::Variant::kTink,
+                                        AesGcmParameters::Variant::kCrunchy,
+                                        AesGcmParameters::Variant::kNoPrefix)));
+
+TEST_P(AesGcmParametersVariantTest, ParametersEquals) {
+  int key_size;
+  int iv_and_tag_size;
+  AesGcmParameters::Variant variant;
+  std::tie(key_size, iv_and_tag_size, variant) = GetParam();
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(variant)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(variant)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters == *other_parameters);
+  EXPECT_TRUE(*other_parameters == *parameters);
+  EXPECT_FALSE(*parameters != *other_parameters);
+  EXPECT_FALSE(*other_parameters != *parameters);
+}
+
+TEST(AesGcmParametersTest, KeySizeNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(24)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesGcmParametersTest, IvSizeNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesGcmParametersTest, TagSizeNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(14)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesGcmParametersTest, VariantNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_proto_serialization.cc b/cc/aead/aes_gcm_proto_serialization.cc
new file mode 100644
index 0000000..57272ff
--- /dev/null
+++ b/cc/aead/aes_gcm_proto_serialization.cc
@@ -0,0 +1,273 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_proto_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aes_gcm_key.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::OutputPrefixType;
+
+using AesGcmProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   AesGcmParameters>;
+using AesGcmProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<AesGcmParameters,
+                                       internal::ProtoParametersSerialization>;
+using AesGcmProtoKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, AesGcmKey>;
+using AesGcmProtoKeySerializerImpl =
+    internal::KeySerializerImpl<AesGcmKey, internal::ProtoKeySerialization>;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.AesGcmKey";
+
+util::StatusOr<AesGcmParameters::Variant> ToVariant(
+    OutputPrefixType output_prefix_type) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::LEGACY:
+       ABSL_FALLTHROUGH_INTENDED;  // Parse LEGACY output prefix as CRUNCHY.
+    case OutputPrefixType::CRUNCHY:
+      return AesGcmParameters::Variant::kCrunchy;
+    case OutputPrefixType::RAW:
+      return AesGcmParameters::Variant::kNoPrefix;
+    case OutputPrefixType::TINK:
+      return AesGcmParameters::Variant::kTink;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine AesGcmParameters::Variant");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    AesGcmParameters::Variant variant) {
+  switch (variant) {
+    case AesGcmParameters::Variant::kCrunchy:
+      return OutputPrefixType::CRUNCHY;
+    case AesGcmParameters::Variant::kNoPrefix:
+      return OutputPrefixType::RAW;
+    case AesGcmParameters::Variant::kTink:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine output prefix type");
+  }
+}
+
+// Legacy Tink AES-GCM key proto format assumes 12-byte random IVs and 16-byte
+// tags.
+util::Status ValidateParamsForProto(const AesGcmParameters& params) {
+  if (params.IvSizeInBytes() != 12) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Tink currently restricts AES-GCM IV size to 12 bytes.");
+  }
+  if (params.TagSizeInBytes() != 16) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Tink currently restricts AES-GCM tag size to 16 bytes.");
+  }
+  return util::OkStatus();
+}
+
+util::StatusOr<AesGcmParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesGcmParameters.");
+  }
+
+  AesGcmKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesGcmKeyFormat proto");
+  }
+  if (proto_key_format.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesGcmParameters::Variant> variant =
+      ToVariant(serialization.GetKeyTemplate().output_prefix_type());
+  if (!variant.ok()) return variant.status();
+
+  // Legacy Tink AES-GCM key proto format assumes 12-byte random IVs and 16-byte
+  // tags.
+  return AesGcmParameters::Builder()
+      .SetVariant(*variant)
+      .SetKeySizeInBytes(proto_key_format.key_size())
+      .SetIvSizeInBytes(12)
+      .SetTagSizeInBytes(16)
+      .Build();
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const AesGcmParameters& parameters) {
+  util::Status valid_params = ValidateParamsForProto(parameters);
+  if (!valid_params.ok()) return valid_params;
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  AesGcmKeyFormat proto_key_format;
+  proto_key_format.set_key_size(parameters.KeySizeInBytes());
+
+  return internal::ProtoParametersSerialization::Create(
+      kTypeUrl, *output_prefix_type, proto_key_format.SerializeAsString());
+}
+
+util::StatusOr<AesGcmKey> ParseKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesGcmKey.");
+  }
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+  google::crypto::tink::AesGcmKey proto_key;
+  RestrictedData restricted_data = serialization.SerializedKeyProto();
+  // OSS proto library complains if input is not converted to a string.
+  if (!proto_key.ParseFromString(
+          std::string(restricted_data.GetSecret(*token)))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesGcmKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesGcmParameters::Variant> variant =
+      ToVariant(serialization.GetOutputPrefixType());
+  if (!variant.ok()) return variant.status();
+
+  // Legacy AES-GCM key proto format assumes 12-byte random IVs and 16-byte
+  // tags.
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(*variant)
+          .SetKeySizeInBytes(proto_key.key_value().length())
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(16)
+          .Build();
+  if (!parameters.ok()) return parameters.status();
+
+  return AesGcmKey::Create(
+      *parameters, RestrictedData(proto_key.key_value(), *token),
+      serialization.IdRequirement(), GetPartialKeyAccess());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializeKey(
+    const AesGcmKey& key, absl::optional<SecretKeyAccessToken> token) {
+  util::Status valid_params = ValidateParamsForProto(key.GetParameters());
+  if (!valid_params.ok()) return valid_params;
+
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetKeyBytes(GetPartialKeyAccess());
+  if (!restricted_input.ok()) return restricted_input.status();
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+
+  google::crypto::tink::AesGcmKey proto_key;
+  proto_key.set_version(0);
+  // OSS proto library complains if input is not converted to a string.
+  proto_key.set_key_value(std::string(restricted_input->GetSecret(*token)));
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetParameters().GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  RestrictedData restricted_output =
+      RestrictedData(proto_key.SerializeAsString(), *token);
+  return internal::ProtoKeySerialization::Create(
+      kTypeUrl, restricted_output, google::crypto::tink::KeyData::SYMMETRIC,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+AesGcmProtoParametersParserImpl* AesGcmProtoParametersParser() {
+  static auto* parser =
+      new AesGcmProtoParametersParserImpl(kTypeUrl, ParseParameters);
+  return parser;
+}
+
+AesGcmProtoParametersSerializerImpl* AesGcmProtoParametersSerializer() {
+  static auto* serializer =
+      new AesGcmProtoParametersSerializerImpl(kTypeUrl, SerializeParameters);
+  return serializer;
+}
+
+AesGcmProtoKeyParserImpl* AesGcmProtoKeyParser() {
+  static auto* parser = new AesGcmProtoKeyParserImpl(kTypeUrl, ParseKey);
+  return parser;
+}
+
+AesGcmProtoKeySerializerImpl* AesGcmProtoKeySerializer() {
+  static auto* serializer = new AesGcmProtoKeySerializerImpl(SerializeKey);
+  return serializer;
+}
+
+}  // namespace
+
+util::Status RegisterAesGcmProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(AesGcmProtoParametersParser());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterParametersSerializer(AesGcmProtoParametersSerializer());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(AesGcmProtoKeyParser());
+  if (!status.ok()) return status;
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(AesGcmProtoKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_proto_serialization.h b/cc/aead/aes_gcm_proto_serialization.h
new file mode 100644
index 0000000..68a49f4
--- /dev/null
+++ b/cc/aead/aes_gcm_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AES_GCM_PROTO_SERIALIZATION_H_
+#define TINK_AEAD_AES_GCM_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for AES-GCM parameters and keys.
+crypto::tink::util::Status RegisterAesGcmProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AES_GCM_PROTO_SERIALIZATION_H_
diff --git a/cc/aead/aes_gcm_proto_serialization_test.cc b/cc/aead/aes_gcm_proto_serialization_test.cc
new file mode 100644
index 0000000..f19c820
--- /dev/null
+++ b/cc/aead/aes_gcm_proto_serialization_test.cc
@@ -0,0 +1,515 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_proto_serialization.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aes_gcm_key.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::subtle::Random;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesGcmParameters::Variant variant;
+  OutputPrefixType output_prefix_type;
+  int key_size;
+  int iv_size;
+  int tag_size;
+  absl::optional<int> id;
+  std::string output_prefix;
+};
+
+class AesGcmProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  void SetUp() override {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    AesGcmProtoSerializationTestSuite, AesGcmProtoSerializationTest,
+    Values(TestCase{AesGcmParameters::Variant::kTink, OutputPrefixType::TINK,
+                    /*key_size=*/16, /*iv_size=*/12, /*tag_size=*/16,
+                    /*id=*/0x02030400,
+                    /*output_prefix=*/std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{AesGcmParameters::Variant::kCrunchy,
+                    OutputPrefixType::CRUNCHY, /*key_size=*/16, /*iv_size=*/12,
+                    /*tag_size=*/16, /*id=*/0x01030005,
+                    /*output_prefix=*/std::string("\x00\x01\x03\x00\x05", 5)},
+           TestCase{AesGcmParameters::Variant::kNoPrefix, OutputPrefixType::RAW,
+                    /*key_size=*/32, /*iv_size=*/12, /*tag_size=*/16,
+                    /*id=*/absl::nullopt, /*output_prefix=*/""}));
+
+TEST_P(AesGcmProtoSerializationTest, ParseParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(test_case.key_size);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          test_case.output_prefix_type, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), test_case.id.has_value());
+
+  const AesGcmParameters* gcm_params =
+      dynamic_cast<const AesGcmParameters*>(params->get());
+  ASSERT_THAT(gcm_params, NotNull());
+  EXPECT_THAT(gcm_params->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(gcm_params->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(gcm_params->IvSizeInBytes(), Eq(test_case.iv_size));
+  EXPECT_THAT(gcm_params->TagSizeInBytes(), Eq(test_case.tag_size));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseParametersWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(16);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          OutputPrefixType::RAW, "invalid_serialization");
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseParametersWithUnkownOutputPrefix) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(16);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          OutputPrefixType::UNKNOWN_PREFIX,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseParametersWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(1);
+  key_format_proto.set_key_size(16);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          OutputPrefixType::RAW,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmProtoSerializationTest, SerializeParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(test_case.variant)
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+
+  const internal::ProtoParametersSerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoParametersSerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().type_url(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  AesGcmKeyFormat key_format;
+  ASSERT_THAT(
+      key_format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  EXPECT_THAT(key_format.key_size(), Eq(test_case.key_size));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeParametersWithDisallowedIvSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(14)
+          .SetTagSizeInBytes(16)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeParametersWithDisallowedTagSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(14)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmProtoSerializationTest, ParseKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, test_case.output_prefix_type, test_case.id);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key)->GetIdRequirement(), Eq(test_case.id));
+  EXPECT_THAT((*key)->GetParameters().HasIdRequirement(),
+              test_case.id.has_value());
+
+  util::StatusOr<AesGcmParameters> expected_parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(test_case.variant)
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .Build();
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  util::StatusOr<AesGcmKey> expected_key = AesGcmKey::Create(
+      *expected_parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(expected_key, IsOk());
+
+  EXPECT_THAT(**key, Eq(*expected_key));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseLegacyKeyAsCrunchy) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::LEGACY, /*id_requirement=*/123);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+
+  const AesGcmKey* aes_gcm_key = dynamic_cast<const AesGcmKey*>(key->get());
+  ASSERT_THAT(aes_gcm_key, NotNull());
+  EXPECT_THAT(aes_gcm_key->GetParameters().GetVariant(),
+              Eq(AesGcmParameters::Variant::kCrunchy));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, /*token=*/absl::nullopt);
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(1);  // Invalid version number.
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmProtoSerializationTest, SerializeKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(test_case.variant)
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(), Eq(test_case.id));
+
+  google::crypto::tink::AesGcmKey proto_key;
+  // OSS proto library complains if input is not converted to a string.
+  ASSERT_THAT(proto_key.ParseFromString(std::string(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get()))),
+              IsTrue());
+  EXPECT_THAT(proto_key.key_value().size(), Eq(test_case.key_size));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeKeyWithDisallowedIvSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(14)
+          .SetTagSizeInBytes(16)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeKeyWithDisallowedTagSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(14)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(16)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(*key, absl::nullopt);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/cord_aead.h b/cc/aead/cord_aead.h
index 46a60ce..e57ba5f 100644
--- a/cc/aead/cord_aead.h
+++ b/cc/aead/cord_aead.h
@@ -56,7 +56,7 @@
       absl::Cord ciphertext,
       absl::Cord associated_data) const = 0;
 
-  virtual ~CordAead() {}
+  virtual ~CordAead() = default;
 };
 
 }  // namespace tink
diff --git a/cc/aead/cord_aead_wrapper.cc b/cc/aead/cord_aead_wrapper.cc
index 0efb3a0..263b3fc 100644
--- a/cc/aead/cord_aead_wrapper.cc
+++ b/cc/aead/cord_aead_wrapper.cc
@@ -56,7 +56,7 @@
   crypto::tink::util::StatusOr<absl::Cord> Decrypt(
       absl::Cord ciphertext, absl::Cord associated_data) const override;
 
-  ~CordAeadSetWrapper() override {}
+  ~CordAeadSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<CordAead>> aead_set_;
diff --git a/cc/aead/failing_aead.cc b/cc/aead/failing_aead.cc
index 81835c5..9f6c0f8 100644
--- a/cc/aead/failing_aead.cc
+++ b/cc/aead/failing_aead.cc
@@ -15,8 +15,10 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/aead/failing_aead.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+
 #include "absl/strings/string_view.h"
 
 namespace crypto {
diff --git a/cc/aead/failing_aead.h b/cc/aead/failing_aead.h
index 70b80a9..88d8c81 100644
--- a/cc/aead/failing_aead.h
+++ b/cc/aead/failing_aead.h
@@ -16,6 +16,7 @@
 #ifndef TINK_AEAD_FAILING_AEAD_H_
 #define TINK_AEAD_FAILING_AEAD_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/aead/internal/BUILD.bazel b/cc/aead/internal/BUILD.bazel
index 0f7c72f..225bffc 100644
--- a/cc/aead/internal/BUILD.bazel
+++ b/cc/aead/internal/BUILD.bazel
@@ -11,6 +11,7 @@
         "//util:errors",
         "//util:statusor",
         "@boringssl//:crypto",
+        "@com_google_absl//absl/container:flat_hash_set",
         "@com_google_absl//absl/status",
     ],
 )
@@ -61,13 +62,11 @@
         "//internal:ssl_unique_ptr",
         "//subtle:random",
         "//subtle:subtle_util",
-        "//util:errors",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
         "@boringssl//:crypto",
         "@com_google_absl//absl/status",
-        "@com_google_absl//absl/strings",
         "@com_google_absl//absl/strings:cord",
     ],
 )
@@ -154,7 +153,7 @@
     name = "cord_aes_gcm_boringssl_test",
     size = "small",
     srcs = ["cord_aes_gcm_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_gcm"],
+    data = ["//testvectors:aes_gcm"],
     deps = [
         ":cord_aes_gcm_boringssl",
         "//subtle:aes_gcm_boringssl",
@@ -199,7 +198,7 @@
 cc_test(
     name = "zero_copy_aes_gcm_boringssl_test",
     srcs = ["zero_copy_aes_gcm_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_gcm"],
+    data = ["//testvectors:aes_gcm"],
     deps = [
         ":wycheproof_aead",
         ":zero_copy_aead",
@@ -235,21 +234,20 @@
     name = "ssl_aead_test",
     srcs = ["ssl_aead_test.cc"],
     data = [
-        "@wycheproof//testvectors:aes_gcm",
-        "@wycheproof//testvectors:aes_gcm_siv",
-        "@wycheproof//testvectors:chacha20_poly1305",
+        "//testvectors:aes_gcm",
+        "//testvectors:aes_gcm_siv",
+        "//testvectors:chacha20_poly1305",
     ],
     deps = [
         ":ssl_aead",
         ":wycheproof_aead",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//internal:ssl_util",
         "//subtle:subtle_util",
         "//util:secret_data",
         "//util:statusor",
         "//util:test_matchers",
         "@com_google_absl//absl/container:flat_hash_set",
-        "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_absl//absl/types:span",
@@ -265,6 +263,7 @@
         ":ssl_aead",
         "//config:tink_fips",
         "//internal:ssl_util",
+        "//internal:util",
         "//subtle:subtle_util",
         "//util:secret_data",
         "//util:statusor",
diff --git a/cc/aead/internal/BUILD.gn b/cc/aead/internal/BUILD.gn
index ffabdb7..1202a08 100644
--- a/cc/aead/internal/BUILD.gn
+++ b/cc/aead/internal/BUILD.gn
@@ -14,6 +14,7 @@
     "aead_util.h",
   ]
   public_deps = [
+    "//third_party/abseil-cpp/absl/container:flat_hash_set",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/boringssl:crypto",
     "//third_party/tink/cc/util:errors",
@@ -60,13 +61,11 @@
     ":aead_util",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/abseil-cpp/absl/strings:cord",
-    "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/boringssl:crypto",
     "//third_party/tink/cc/aead:cord_aead",
     "//third_party/tink/cc/internal:ssl_unique_ptr",
     "//third_party/tink/cc/subtle:random",
     "//third_party/tink/cc/subtle:subtle_util",
-    "//third_party/tink/cc/util:errors",
     "//third_party/tink/cc/util:secret_data",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
diff --git a/cc/aead/internal/CMakeLists.txt b/cc/aead/internal/CMakeLists.txt
index e7c1a07..217d2b4 100644
--- a/cc/aead/internal/CMakeLists.txt
+++ b/cc/aead/internal/CMakeLists.txt
@@ -6,6 +6,7 @@
     aead_util.cc
     aead_util.h
   DEPS
+    absl::flat_hash_set
     absl::status
     crypto
     tink::util::errors
@@ -21,6 +22,7 @@
     absl::strings
     tink::subtle::wycheproof_util
     tink::util::statusor
+  TESTONLY
 )
 
 tink_cc_library(
@@ -52,14 +54,12 @@
   DEPS
     tink::aead::internal::aead_util
     absl::status
-    absl::strings
     absl::cord
     crypto
     tink::aead::cord_aead
     tink::internal::ssl_unique_ptr
     tink::subtle::random
     tink::subtle::subtle_util
-    tink::util::errors
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -74,6 +74,7 @@
     gmock
     absl::strings
     tink::util::statusor
+  TESTONLY
 )
 
 tink_cc_library(
@@ -159,11 +160,10 @@
     tink::aead::internal::wycheproof_aead
     gmock
     absl::flat_hash_set
-    absl::memory
     absl::status
     absl::strings
     absl::span
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::internal::ssl_util
     tink::subtle::subtle_util
     tink::util::secret_data
@@ -241,6 +241,7 @@
     absl::span
     tink::config::tink_fips
     tink::internal::ssl_util
+    tink::internal::util
     tink::subtle::subtle_util
     tink::util::secret_data
     tink::util::statusor
diff --git a/cc/aead/internal/aead_from_zero_copy_test.cc b/cc/aead/internal/aead_from_zero_copy_test.cc
index 2fe4486..9693661 100644
--- a/cc/aead/internal/aead_from_zero_copy_test.cc
+++ b/cc/aead/internal/aead_from_zero_copy_test.cc
@@ -49,7 +49,7 @@
 
 TEST(AeadFromZeroCopyTest, EncryptSucceeds) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxEncryptionSize(kPlaintext.size()))
       .WillOnce(Return(kCiphertext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Encrypt(kPlaintext, kAssociatedData, _))
@@ -66,7 +66,7 @@
 
 TEST(AeadFromZeroCopyTest, EncryptFailsIfZeroCopyEncryptFails) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxEncryptionSize(kPlaintext.size()))
       .WillOnce(Return(kCiphertext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Encrypt(kPlaintext, kAssociatedData, _))
@@ -79,7 +79,7 @@
 
 TEST(AeadFromZeroCopyTest, DecryptSucceeds) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxDecryptionSize(kCiphertext.size()))
       .WillOnce(Return(kPlaintext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Decrypt(kCiphertext, kAssociatedData, _))
@@ -96,7 +96,7 @@
 
 TEST(AeadFromZeroCopyTest, EncryptFailsIfZeroCopyDecryptFails) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxDecryptionSize(kCiphertext.size()))
       .WillOnce(Return(kPlaintext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Decrypt(kCiphertext, kAssociatedData, _))
diff --git a/cc/aead/internal/aead_util.cc b/cc/aead/internal/aead_util.cc
index bfe0719..22c0bce 100644
--- a/cc/aead/internal/aead_util.cc
+++ b/cc/aead/internal/aead_util.cc
@@ -15,6 +15,8 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/aead/internal/aead_util.h"
 
+#include <string>
+
 #include "absl/status/status.h"
 #include "openssl/evp.h"
 #include "tink/util/errors.h"
@@ -24,6 +26,18 @@
 namespace tink {
 namespace internal {
 
+bool IsSupportedKmsEnvelopeAeadDekKeyType(absl::string_view key_type) {
+  static const auto *kSupportedDekKeyTypes =
+      new absl::flat_hash_set<std::string>({
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+          "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+          "type.googleapis.com/google.crypto.tink.AesEaxKey",
+          "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+      });
+  return kSupportedDekKeyTypes->contains(key_type);
+}
+
 util::StatusOr<const EVP_CIPHER *> GetAesGcmCipherForKeySize(
     uint32_t key_size_in_bytes) {
   switch (key_size_in_bytes) {
diff --git a/cc/aead/internal/aead_util.h b/cc/aead/internal/aead_util.h
index c8c84d2..094b22d 100644
--- a/cc/aead/internal/aead_util.h
+++ b/cc/aead/internal/aead_util.h
@@ -16,6 +16,9 @@
 #ifndef TINK_AEAD_INTERNAL_AEAD_UTIL_H_
 #define TINK_AEAD_INTERNAL_AEAD_UTIL_H_
 
+#include <string>
+
+#include "absl/container/flat_hash_set.h"
 #include "openssl/evp.h"
 #include "tink/util/statusor.h"
 
@@ -23,6 +26,8 @@
 namespace tink {
 namespace internal {
 
+bool IsSupportedKmsEnvelopeAeadDekKeyType(absl::string_view key_type);
+
 // Returns a pointer to an AES-GCM EVP_CIPHER for the given key size.
 util::StatusOr<const EVP_CIPHER *> GetAesGcmCipherForKeySize(
     uint32_t key_size_in_bytes);
diff --git a/cc/aead/internal/aead_util_test.cc b/cc/aead/internal/aead_util_test.cc
index ea3941a..83c717f 100644
--- a/cc/aead/internal/aead_util_test.cc
+++ b/cc/aead/internal/aead_util_test.cc
@@ -27,6 +27,8 @@
 
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::IsOkAndHolds;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
 using ::testing::Not;
 
 TEST(AeadUtilTest, GetAesGcmCipherForKeySize) {
@@ -42,6 +44,15 @@
   }
 }
 
+TEST(AeadUtilTest, SupportedKmsEnvelopeAeadDekKeyTypes) {
+  EXPECT_THAT(IsSupportedKmsEnvelopeAeadDekKeyType(
+                  "type.googleapis.com/google.crypto.tink.AesGcmKey"),
+              IsTrue());
+  EXPECT_THAT(IsSupportedKmsEnvelopeAeadDekKeyType(
+                  "type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey"),
+              IsFalse());
+}
+
 #ifdef OPENSSL_IS_BORINGSSL
 
 TEST(AeadUtilTest, GetAesAeadForKeySize) {
diff --git a/cc/aead/internal/cord_aes_gcm_boringssl.cc b/cc/aead/internal/cord_aes_gcm_boringssl.cc
index 321e9bf..4356bee 100644
--- a/cc/aead/internal/cord_aes_gcm_boringssl.cc
+++ b/cc/aead/internal/cord_aes_gcm_boringssl.cc
@@ -17,22 +17,18 @@
 #include "tink/aead/internal/cord_aes_gcm_boringssl.h"
 
 #include <cstdint>
-#include <iterator>
 #include <memory>
 #include <string>
 #include <utility>
-#include <vector>
 
 #include "absl/status/status.h"
 #include "absl/strings/cord.h"
 #include "openssl/evp.h"
-#include "openssl/err.h"
 #include "tink/aead/cord_aead.h"
 #include "tink/aead/internal/aead_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/random.h"
 #include "tink/subtle/subtle_util.h"
-#include "tink/util/errors.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -47,8 +43,8 @@
 
 // Set the IV `iv` for the given `context`. if `encryption` is true, set the
 // context for encryption, and for decryption otherwise.
-util::Status SetIv(EVP_CIPHER_CTX* context, absl::string_view iv,
-                   bool encryption) {
+util::Status SetIvAndDirection(EVP_CIPHER_CTX* context, absl::string_view iv,
+                               bool encryption) {
   const int encryption_flag = encryption ? 1 : 0;
   // Set the IV size.
   if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, iv.size(),
@@ -67,28 +63,100 @@
   return util::OkStatus();
 }
 
+#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_NUMBER < 0x30000000L
+// Returns a new EVP_CIPHER_CTX for encryption (`encryption` == true) or
+// decryption (`encryption` == false). It tries to skip part of the
+// initialization copying `partial_context`.
+util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> NewContextFromPartial(
+    EVP_CIPHER_CTX* partial_context, absl::string_view iv, bool encryption) {
+  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
+  if (context == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "EVP_CIPHER_CTX_new failed");
+  }
+  // Try making a copy of `partial_context` to skip some pre-computations.
+  //
+  // NOTE: With BoringSSL and OpenSSL 1.1.1 EVP_CIPHER_CTX_copy makes a copy
+  // of the `cipher_data` field of `context` as well, which contains the key
+  // material and IV (see [1] and [2]).
+  //
+  // [1]https://github.com/google/boringssl/blob/4c8bcf0da2951cacd8ed8eaa7fd2df4b22fca23b/crypto/fipsmodule/cipher/cipher.c#L116
+  // [2]https://github.com/openssl/openssl/blob/830bf8e1e4749ad65c51b6a1d0d769ae689404ba/crypto/evp/evp_enc.c#L703
+  if (EVP_CIPHER_CTX_copy(context.get(), partial_context) <= 0) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "EVP_CIPHER_CTX_copy failed");
+  }
+  util::Status res =
+      SetIvAndDirection(context.get(), iv, /*encryption=*/encryption);
+  if (!res.ok()) {
+    return res;
+  }
+  return std::move(context);
+}
+#else
+// Returns a new EVP_CIPHER_CTX for encryption (`encryption` == true) or
+// decryption (`encryption` == false) with given `key` and `iv`.
+//
+// NOTE: Copying the context fails with OpenSSL 3.0, which doesn't provide a
+// `dupctx` function for aead ciphers (see [1], [2]).
+//
+// [1]https://github.com/openssl/openssl/blob/eb52450f5151e8e78743ab05de21a344823316f5/crypto/evp/evp_enc.c#L1427
+// [2]https://github.com/openssl/openssl/blob/cac250755efd0c40cc6127a0e4baceb8d226c7e3/providers/implementations/include/prov/ciphercommon_aead.h#L30
+util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> NewContext(
+    const util::SecretData& key, absl::string_view iv, bool encryption) {
+  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
+  if (context == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "EVP_CIPHER_CTX_new failed");
+  }
+  util::StatusOr<const EVP_CIPHER*> cipher =
+      internal::GetAesGcmCipherForKeySize(key.size());
+  if (!cipher.ok()) {
+    return cipher.status();
+  }
+  if (EVP_CipherInit_ex(context.get(), *cipher, /*impl=*/nullptr,
+                        reinterpret_cast<const uint8_t*>(key.data()),
+                        /*iv=*/nullptr, /*enc=*/1) <= 0) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Context initialization failed");
+  }
+  util::Status res =
+      SetIvAndDirection(context.get(), iv, /*encryption=*/encryption);
+  if (!res.ok()) {
+    return res;
+  }
+  return std::move(context);
+}
+#endif
+
 }  // namespace
 
-util::StatusOr<std::unique_ptr<CordAead> > CordAesGcmBoringSsl::New(
-    util::SecretData key_value) {
+util::StatusOr<std::unique_ptr<CordAead>> CordAesGcmBoringSsl::New(
+    const util::SecretData& key_value) {
   util::StatusOr<const EVP_CIPHER*> cipher =
       internal::GetAesGcmCipherForKeySize(key_value.size());
   if (!cipher.ok()) {
     return cipher.status();
   }
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-  // Initialize the cipher now to have some precomputations on the key. The
-  // direction (enc/dec) is not important since it will be overwritten later.
-  if (EVP_CipherInit_ex(context.get(), *cipher, /*engine=*/nullptr,
+  internal::SslUniquePtr<EVP_CIPHER_CTX> partial_context(EVP_CIPHER_CTX_new());
+  // Initialize a partial context for the cipher to allow OpenSSL/BoringSSL
+  // making some precomputations on the key. Encrypt and Decrypt will try making
+  // a copy of this context to avoid doing the same initializations again and to
+  // guarantee thread safety.
+  //
+  // NOTE: It doesn't matter at this point if we set the direction to encryption
+  // or decryption, it will be overwritten later any time we call
+  // EVP_CipherInit_ex.
+  if (EVP_CipherInit_ex(partial_context.get(), *cipher, /*engine=*/nullptr,
                         reinterpret_cast<const uint8_t*>(&key_value[0]),
                         /*iv=*/nullptr, /*enc=*/1) <= 0) {
     return util::Status(absl::StatusCode::kInternal,
                         "Context initialization failed");
   }
 
-  std::unique_ptr<CordAead> aead =
-      absl::WrapUnique(new CordAesGcmBoringSsl(std::move(context)));
+  std::unique_ptr<CordAead> aead = absl::WrapUnique(
+      new CordAesGcmBoringSsl(std::move(partial_context), key_value));
   return std::move(aead);
 }
 
@@ -96,18 +164,21 @@
     absl::Cord plaintext, absl::Cord associated_data) const {
   std::string iv = subtle::Random::GetRandomBytes(kIvSizeInBytes);
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-  EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-  util::Status res = SetIv(context.get(), iv, /*encryption=*/true);
-  if (!res.ok()) {
-    return res;
+#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_NUMBER < 0x30000000L
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContextFromPartial(partial_context_.get(), iv, /*encryption=*/true);
+#else
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContext(key_, iv, /*encryption=*/true);
+#endif
+  if (!context.ok()) {
+    return context.status();
   }
 
   int len = 0;
   // Process AAD.
   for (auto ad_chunk : associated_data.Chunks()) {
-    if (!EVP_EncryptUpdate(context.get(), /*out=*/nullptr, &len,
+    if (!EVP_EncryptUpdate(context->get(), /*out=*/nullptr, &len,
                            reinterpret_cast<const uint8_t*>(ad_chunk.data()),
                            ad_chunk.size())) {
       return util::Status(absl::StatusCode::kInternal, "Encryption failed");
@@ -124,7 +195,7 @@
 
   for (auto plaintext_chunk : plaintext.Chunks()) {
     if (!EVP_EncryptUpdate(
-            context.get(),
+            context->get(),
             reinterpret_cast<uint8_t*>(&(buffer[ciphertext_buffer_offset])),
             &len, reinterpret_cast<const uint8_t*>(plaintext_chunk.data()),
             plaintext_chunk.size())) {
@@ -132,13 +203,14 @@
     }
     ciphertext_buffer_offset += plaintext_chunk.size();
   }
-  if (!EVP_EncryptFinal_ex(context.get(), nullptr, &len)) {
+  if (!EVP_EncryptFinal_ex(context->get(), nullptr, &len)) {
     return util::Status(absl::StatusCode::kInternal, "Encryption failed");
   }
 
   std::string tag;
   subtle::ResizeStringUninitialized(&tag, kTagSizeInBytes);
-  if (!EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_GCM_GET_TAG, kTagSizeInBytes,
+  if (!EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_GCM_GET_TAG,
+                           kTagSizeInBytes,
                            reinterpret_cast<uint8_t*>(&tag[0]))) {
     return util::Status(absl::StatusCode::kInternal, "Encryption failed");
   }
@@ -157,23 +229,26 @@
     return util::Status(absl::StatusCode::kInternal, "Ciphertext too short");
   }
 
-  // First bytes contain IV.
+  // First bytes contain the IV.
   std::string iv = std::string(ciphertext.Subcord(0, kIvSizeInBytes));
   absl::Cord raw_ciphertext = ciphertext.Subcord(
       kIvSizeInBytes, ciphertext.size() - kIvSizeInBytes - kTagSizeInBytes);
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-  EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-  util::Status res = SetIv(context.get(), iv, /*encryption=*/false);
-  if (!res.ok()) {
-    return res;
+#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_NUMBER < 0x30000000L
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContextFromPartial(partial_context_.get(), iv, /*encryption=*/false);
+#else
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContext(key_, iv, /*encryption=*/false);
+#endif
+  if (!context.ok()) {
+    return context.status();
   }
 
   int len = 0;
   // Process associated data.
   for (auto ad_chunk : associated_data.Chunks()) {
-    if (!EVP_DecryptUpdate(context.get(), nullptr, &len,
+    if (!EVP_DecryptUpdate(context->get(), /*out=*/nullptr, &len,
                            reinterpret_cast<const uint8_t*>(ad_chunk.data()),
                            ad_chunk.size())) {
       return util::Status(absl::StatusCode::kInternal, "Decryption failed");
@@ -192,7 +267,7 @@
       });
 
   for (auto ct_chunk : raw_ciphertext.Chunks()) {
-    if (!EVP_DecryptUpdate(context.get(),
+    if (!EVP_DecryptUpdate(context->get(),
                            reinterpret_cast<uint8_t*>(
                                &plaintext_buffer[plaintext_buffer_offset]),
                            &len,
@@ -207,13 +282,13 @@
   std::string tag = std::string(
       ciphertext.Subcord(ciphertext.size() - kTagSizeInBytes, kTagSizeInBytes));
 
-  if (!EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_GCM_SET_TAG, kTagSizeInBytes,
-                           &tag[0])) {
+  if (!EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_GCM_SET_TAG,
+                           kTagSizeInBytes, &tag[0])) {
     return util::Status(absl::StatusCode::kInternal,
                         "Could not set authentication tag");
   }
   // Verify authentication tag.
-  if (!EVP_DecryptFinal_ex(context.get(), nullptr, &len)) {
+  if (!EVP_DecryptFinal_ex(context->get(), nullptr, &len)) {
     return util::Status(absl::StatusCode::kInternal, "Authentication failed");
   }
   return result;
diff --git a/cc/aead/internal/cord_aes_gcm_boringssl.h b/cc/aead/internal/cord_aes_gcm_boringssl.h
index 9dcd288..273b523 100644
--- a/cc/aead/internal/cord_aes_gcm_boringssl.h
+++ b/cc/aead/internal/cord_aes_gcm_boringssl.h
@@ -20,12 +20,10 @@
 #include <memory>
 #include <utility>
 
-#include "absl/strings/string_view.h"
 #include "openssl/evp.h"
 #include "tink/aead/cord_aead.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/util/secret_data.h"
-#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
 namespace crypto {
@@ -35,7 +33,7 @@
 class CordAesGcmBoringSsl : public CordAead {
  public:
   static crypto::tink::util::StatusOr<std::unique_ptr<CordAead>> New(
-      util::SecretData key_value);
+      const util::SecretData& key_value);
 
   crypto::tink::util::StatusOr<absl::Cord> Encrypt(
       absl::Cord plaintext, absl::Cord associated_data) const override;
@@ -44,10 +42,15 @@
       absl::Cord ciphertext, absl::Cord associated_data) const override;
 
  private:
-  explicit CordAesGcmBoringSsl(internal::SslUniquePtr<EVP_CIPHER_CTX> context)
-      : context_(std::move(context)) {}
+  explicit CordAesGcmBoringSsl(
+      internal::SslUniquePtr<EVP_CIPHER_CTX> partial_context,
+      const util::SecretData& key)
+      : partial_context_(std::move(partial_context)), key_(key) {}
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context_;
+  // Partially-initialized EVP_CIPHER_CTX context that is copied for every
+  // Encrypt/Decrypt operation.
+  internal::SslUniquePtr<EVP_CIPHER_CTX> partial_context_;
+  util::SecretData key_;
 };
 
 }  // namespace internal
diff --git a/cc/aead/internal/ssl_aead.cc b/cc/aead/internal/ssl_aead.cc
index 4b62a8a..c8b2629 100644
--- a/cc/aead/internal/ssl_aead.cc
+++ b/cc/aead/internal/ssl_aead.cc
@@ -44,36 +44,10 @@
 
 ABSL_CONST_INIT const int kXchacha20Poly1305TagSizeInBytes = 16;
 ABSL_CONST_INIT const int kAesGcmTagSizeInBytes = 16;
+ABSL_CONST_INIT const int kAesGcmSivTagSizeInBytes = 16;
 
 namespace {
 
-// Sets `iv` to the given `context`, as well as the "direction"
-// (encrypt/decrypt) based on `encryption`.
-util::Status SetIvAndDirection(EVP_CIPHER_CTX *context, absl::string_view iv,
-                               bool encryption) {
-  if (context == nullptr) {
-    return util::Status(absl::StatusCode::kInternal,
-                        "Context must not be null");
-  }
-  const int encryption_flag = encryption ? 1 : 0;
-  // Set the size for IV first, then set the IV bytes.
-  if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_AEAD_SET_IVLEN, iv.size(),
-                          /*ptr=*/nullptr) <= 0) {
-    return util::Status(absl::StatusCode::kInternal, "Setting IV size failed");
-  }
-  if (EVP_CipherInit_ex(context, /*cipher=*/nullptr, /*impl=*/nullptr,
-                        /*key=*/nullptr,
-                        reinterpret_cast<const uint8_t *>(iv.data()),
-                        /*enc=*/encryption_flag) <= 0) {
-    return util::Status(
-        absl::StatusCode::kInternal,
-        absl::StrCat("Failed to initialize the context with IV of size ",
-                     iv.size(), " and for ",
-                     encryption ? "encryption" : "decryption"));
-  }
-  return util::OkStatus();
-}
-
 // Encrypts/Decrypts `data` and writes the result into `out`. The direction
 // (encrypt/decrypt) is given by `context`. `out` is assumed to be large enough
 // to hold the encrypted/decrypted content.
@@ -108,9 +82,9 @@
 
 class OpenSslOneShotAeadImpl : public SslOneShotAead {
  public:
-  OpenSslOneShotAeadImpl(internal::SslUniquePtr<EVP_CIPHER_CTX> context,
-                         size_t tag_size)
-      : context_(std::move(context)), tag_size_(tag_size) {}
+  explicit OpenSslOneShotAeadImpl(const util::SecretData &key,
+                                  const EVP_CIPHER *cipher, size_t tag_size)
+      : key_(key), cipher_(cipher), tag_size_(tag_size) {}
 
   util::StatusOr<int64_t> Encrypt(absl::string_view plaintext,
                                   absl::string_view associated_data,
@@ -140,25 +114,15 @@
                        associated_data.size()));
     }
 
-    // For thread safety we copy the context and only then set the IV. This
-    // allows to allocate an AesGcmBoringSsl cipher, and initialize the context
-    // to force precomputation on the key, and only then set a different IV
-    // for each call to `Encrypt`.
-    internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-    // This makes a copy of the `cipher_data` field of the context too, which
-    // contains the key material and IV (see
-    // https://github.com/google/boringssl/blob/master/crypto/fipsmodule/cipher/cipher.c#L116).
-    EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-    util::Status res =
-        SetIvAndDirection(context.get(), iv, /*encryption=*/true);
-    if (!res.ok()) {
-      return res;
+    util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+        GetContext(iv, /*encryption=*/true);
+    if (!context.ok()) {
+      return context.status();
     }
 
     // Set the associated data.
     int len = 0;
-    if (EVP_EncryptUpdate(context.get(), /*out=*/nullptr, &len,
+    if (EVP_EncryptUpdate(context->get(), /*out=*/nullptr, &len,
                           reinterpret_cast<const uint8_t *>(ad.data()),
                           ad.size()) <= 0) {
       return util::Status(absl::StatusCode::kInternal,
@@ -166,18 +130,18 @@
     }
 
     util::StatusOr<int64_t> raw_ciphertext_bytes =
-        UpdateCipher(context.get(), plaintext_data, out);
+        UpdateCipher(context->get(), plaintext_data, out);
     if (!raw_ciphertext_bytes.ok()) {
       return raw_ciphertext_bytes.status();
     }
 
-    if (EVP_EncryptFinal_ex(context.get(), /*out=*/nullptr, &len) <= 0) {
+    if (EVP_EncryptFinal_ex(context->get(), /*out=*/nullptr, &len) <= 0) {
       return util::Status(absl::StatusCode::kInternal, "Finalization failed");
     }
 
     // Write the tag after the ciphertext.
     absl::Span<char> tag = out.subspan(*raw_ciphertext_bytes, tag_size_);
-    if (EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_AEAD_GET_TAG, tag_size_,
+    if (EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_AEAD_GET_TAG, tag_size_,
                             reinterpret_cast<uint8_t *>(tag.data())) <= 0) {
       return util::Status(absl::StatusCode::kInternal, "Failed to get the tag");
     }
@@ -218,18 +182,15 @@
                        associated_data.size()));
     }
 
-    internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-    EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-    util::Status res =
-        SetIvAndDirection(context.get(), iv, /*encryption=*/false);
-    if (!res.ok()) {
-      return res;
+    util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+        GetContext(iv, /*encryption=*/false);
+    if (!context.ok()) {
+      return context.status();
     }
 
     int len = 0;
     // Add the associated data.
-    if (EVP_DecryptUpdate(context.get(), /*out=*/nullptr, &len,
+    if (EVP_DecryptUpdate(context->get(), /*out=*/nullptr, &len,
                           reinterpret_cast<const uint8_t *>(ad.data()),
                           ad.size()) <= 0) {
       return util::Status(absl::StatusCode::kInternal,
@@ -246,7 +207,7 @@
     auto tag = std::string(ciphertext.substr(raw_ciphertext_size, tag_size_));
 
     // Set the tag.
-    if (EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_AEAD_SET_TAG, tag_size_,
+    if (EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_AEAD_SET_TAG, tag_size_,
                             reinterpret_cast<uint8_t *>(&tag[0])) <= 0) {
       return util::Status(absl::StatusCode::kInternal,
                           "Could not set authentication tag");
@@ -267,12 +228,12 @@
         absl::MakeCleanup([out] { OPENSSL_cleanse(out.data(), out.size()); });
 
     util::StatusOr<int64_t> written_bytes =
-        UpdateCipher(context.get(), raw_ciphertext, out_buffer);
+        UpdateCipher(context->get(), raw_ciphertext, out_buffer);
     if (!written_bytes.ok()) {
       return written_bytes.status();
     }
 
-    if (!EVP_DecryptFinal_ex(context.get(), /*out=*/nullptr, &len)) {
+    if (!EVP_DecryptFinal_ex(context->get(), /*out=*/nullptr, &len)) {
       return util::Status(absl::StatusCode::kInternal, "Authentication failed");
     }
 
@@ -281,20 +242,70 @@
     return *written_bytes;
   }
 
+  int64_t CiphertextSize(int64_t plaintext_length) const override {
+    return plaintext_length + tag_size_;
+  }
+
+  int64_t PlaintextSize(int64_t ciphertext_length) const override {
+    if (ciphertext_length < tag_size_) {
+      return 0;
+    }
+    return ciphertext_length - tag_size_;
+  }
+
  private:
-  const internal::SslUniquePtr<EVP_CIPHER_CTX> context_;
+  // Returns a new EVP_CIPHER_CTX for encryption (`ecryption` == true) or
+  // decryption (`encryption` == false).
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> GetContext(
+      absl::string_view iv, bool encryption) const {
+    internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
+    if (context == nullptr) {
+      return util::Status(absl::StatusCode::kInternal,
+                          "EVP_CIPHER_CTX_new failed");
+    }
+    const int encryption_flag = encryption ? 1 : 0;
+    if (EVP_CipherInit_ex(context.get(), cipher_, /*impl=*/nullptr,
+                          /*key=*/nullptr, /*iv=*/nullptr,
+                          encryption_flag) <= 0) {
+      return util::Status(
+          absl::StatusCode::kInternal,
+          absl::StrCat("Failed initializializing context for ",
+                       encryption ? "encryption" : "decryption"));
+    }
+    // Set the size for IV first, then set the IV bytes.
+    if (EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_AEAD_SET_IVLEN, iv.size(),
+                            /*ptr=*/nullptr) <= 0) {
+      return util::Status(
+          absl::StatusCode::kInternal,
+          absl::StrCat("Failed stting size of the IV to ", iv.size()));
+    }
+    if (EVP_CipherInit_ex(context.get(), /*cipher=*/nullptr, /*impl=*/nullptr,
+                          reinterpret_cast<const uint8_t *>(key_.data()),
+                          reinterpret_cast<const uint8_t *>(iv.data()),
+                          encryption_flag) <= 0) {
+      return util::Status(
+          absl::StatusCode::kInternal,
+          absl::StrCat("Failed to set key of size ", key_.size(),
+                       "and IV of size ", iv.size()));
+    }
+
+    return std::move(context);
+  }
+
+  const util::SecretData key_;
+  const EVP_CIPHER *cipher_;
   const size_t tag_size_;
 };
 
 #ifdef OPENSSL_IS_BORINGSSL
 
-// Implementation of the one-shot AEAD cypter. This is purposely internal to an
-// anonymous namespace to disallow direct use of this class other than through
-// the Create* functions below.
+// Implementation of the one-shot AEAD cypter. This is purposely internal to
+// an anonymous namespace to disallow direct use of this class other than
+// through the Create* functions below.
 class BoringSslOneShotAeadImpl : public SslOneShotAead {
  public:
-  BoringSslOneShotAeadImpl(internal::SslUniquePtr<EVP_AEAD_CTX> context,
-                           size_t tag_size)
+  explicit BoringSslOneShotAeadImpl(
+      internal::SslUniquePtr<EVP_AEAD_CTX> context, size_t tag_size)
       : context_(std::move(context)), tag_size_(tag_size) {}
 
   util::StatusOr<int64_t> Encrypt(absl::string_view plaintext,
@@ -388,70 +399,24 @@
     return out_len;
   }
 
+  int64_t CiphertextSize(int64_t plaintext_length) const override {
+    return plaintext_length + tag_size_;
+  }
+
+  int64_t PlaintextSize(int64_t ciphertext_length) const override {
+    if (ciphertext_length < tag_size_) {
+      return 0;
+    }
+    return ciphertext_length - tag_size_;
+  }
+
+ private:
   const internal::SslUniquePtr<EVP_AEAD_CTX> context_;
   const size_t tag_size_;
 };
 
 #endif
 
-#ifdef OPENSSL_IS_BORINGSSL
-// Always use the BoringSSL APIs when available.
-using SslOneShotAeadImpl = BoringSslOneShotAeadImpl;
-#else
-using SslOneShotAeadImpl = OpenSslOneShotAeadImpl;
-#endif
-
-// One shot implementing AES-GCM.
-class SslAesGcmOneShotAead : public SslOneShotAeadImpl {
- public:
-#ifdef OPENSSL_IS_BORINGSSL
-  explicit SslAesGcmOneShotAead(internal::SslUniquePtr<EVP_AEAD_CTX> context)
-      : BoringSslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#else
-  explicit SslAesGcmOneShotAead(internal::SslUniquePtr<EVP_CIPHER_CTX> context)
-      : SslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#endif
-
-  int64_t CiphertextSize(int64_t plaintext_length) const override {
-    return plaintext_length + kAesGcmTagSizeInBytes;
-  }
-
-  int64_t PlaintextSize(int64_t ciphertext_length) const override {
-    if (ciphertext_length < kAesGcmTagSizeInBytes) {
-      return 0;
-    }
-    return ciphertext_length - kAesGcmTagSizeInBytes;
-  }
-};
-
-// One shot implementing AES-GCM-SIV.
-using SslAesGcmSivOneShotAead = SslAesGcmOneShotAead;
-
-// One shot implementing XCHACHA-POLY-1305.
-class SslXchacha20Poly1305OneShotAead : public SslOneShotAeadImpl {
- public:
-#ifdef OPENSSL_IS_BORINGSSL
-  explicit SslXchacha20Poly1305OneShotAead(
-      internal::SslUniquePtr<EVP_AEAD_CTX> context)
-      : BoringSslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#else
-  explicit SslXchacha20Poly1305OneShotAead(
-      internal::SslUniquePtr<EVP_CIPHER_CTX> context)
-      : SslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#endif
-
-  int64_t CiphertextSize(int64_t plaintext_length) const override {
-    return plaintext_length + kXchacha20Poly1305TagSizeInBytes;
-  }
-
-  int64_t PlaintextSize(int64_t ciphertext_length) const override {
-    if (ciphertext_length < kXchacha20Poly1305TagSizeInBytes) {
-      return 0;
-    }
-    return ciphertext_length - kXchacha20Poly1305TagSizeInBytes;
-  }
-};
-
 }  // namespace
 
 util::StatusOr<std::unique_ptr<SslOneShotAead>> CreateAesGcmOneShotCrypter(
@@ -470,6 +435,8 @@
         absl::StatusCode::kInternal,
         absl::StrCat("EVP_AEAD_CTX_new failed: ", internal::GetSslErrors()));
   }
+  return {absl::make_unique<BoringSslOneShotAeadImpl>(std::move(context),
+                                                      kAesGcmTagSizeInBytes)};
 #else
   util::StatusOr<const EVP_CIPHER *> aead_cipher =
       GetAesGcmCipherForKeySize(key.size());
@@ -477,25 +444,9 @@
     return aead_cipher.status();
   }
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-
-  if (context == nullptr) {
-    return util::Status(absl::StatusCode::kInternal,
-                        "EVP_CIPHER_CTX_new failed");
-  }
-
-  // Initialize the context for the cipher having OpenSSL to make some
-  // precomputations on the key. It doesn't matter at this point if we set
-  // encryption or decryption, it will be overwritten later on anyways any
-  // time we call EVP_CipherInit_ex.
-  if (EVP_CipherInit_ex(context.get(), *aead_cipher, /*impl=*/nullptr,
-                        reinterpret_cast<const uint8_t *>(&key[0]),
-                        /*iv=*/nullptr, /*enc=*/1) <= 0) {
-    return util::Status(absl::StatusCode::kInternal,
-                        "Context initialization failed");
-  }
+  return absl::make_unique<OpenSslOneShotAeadImpl>(key, *aead_cipher,
+                                                   kAesGcmTagSizeInBytes);
 #endif
-  return {absl::make_unique<SslAesGcmOneShotAead>(std::move(context))};
 }
 
 util::StatusOr<std::unique_ptr<SslOneShotAead>> CreateAesGcmSivOneShotCrypter(
@@ -513,7 +464,8 @@
                         absl::StrCat("EVP_AEAD_CTX_new initialization Failed: ",
                                      internal::GetSslErrors()));
   }
-  return {absl::make_unique<SslAesGcmSivOneShotAead>(std::move(context))};
+  return {absl::make_unique<BoringSslOneShotAeadImpl>(
+      std::move(context), kAesGcmSivTagSizeInBytes)};
 #else
   return util::Status(absl::StatusCode::kUnimplemented,
                       "AES-GCM-SIV is unimplemented for OpenSSL");
@@ -538,8 +490,8 @@
                         absl::StrCat("EVP_AEAD_CTX_new initialization Failed: ",
                                      internal::GetSslErrors()));
   }
-  return {
-      absl::make_unique<SslXchacha20Poly1305OneShotAead>(std::move(context))};
+  return {absl::make_unique<BoringSslOneShotAeadImpl>(
+      std::move(context), kXchacha20Poly1305TagSizeInBytes)};
 #else
   return util::Status(absl::StatusCode::kUnimplemented,
                       "Xchacha20-Poly1305 is unimplemented for OpenSSL");
diff --git a/cc/aead/internal/ssl_aead.h b/cc/aead/internal/ssl_aead.h
index 6ff877a..3fdc158 100644
--- a/cc/aead/internal/ssl_aead.h
+++ b/cc/aead/internal/ssl_aead.h
@@ -28,9 +28,10 @@
 namespace tink {
 namespace internal {
 
+// Tag sizes.
 ABSL_CONST_INIT extern const int kXchacha20Poly1305TagSizeInBytes;
-// Tag size for both AES-GCM and AES-GCM-SIV.
 ABSL_CONST_INIT extern const int kAesGcmTagSizeInBytes;
+ABSL_CONST_INIT extern const int kAesGcmSivTagSizeInBytes;
 
 // Interface for one-shot AEAD crypters.
 class SslOneShotAead {
diff --git a/cc/aead/internal/ssl_aead_large_inputs_test.cc b/cc/aead/internal/ssl_aead_large_inputs_test.cc
index 1ca2400..9427b78 100644
--- a/cc/aead/internal/ssl_aead_large_inputs_test.cc
+++ b/cc/aead/internal/ssl_aead_large_inputs_test.cc
@@ -33,6 +33,7 @@
 #include "tink/aead/internal/ssl_aead.h"
 #include "tink/config/tink_fips.h"
 #include "tink/internal/ssl_util.h"
+#include "tink/internal/util.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/statusor.h"
@@ -87,6 +88,9 @@
 
 // Encrypt/decrypt with an input larger than a MAX int.
 TEST_P(SslOneShotAeadLargeInputsTest, EncryptDecryptLargeInput) {
+  if (IsWindows()) {
+    GTEST_SKIP() << "Skipping on Windows: this currently times out";
+  }
   const int64_t buff_size =
       static_cast<int64_t>(std::numeric_limits<int>::max()) + 1024;
   std::string large_input(buff_size, '0');
diff --git a/cc/aead/internal/ssl_aead_test.cc b/cc/aead/internal/ssl_aead_test.cc
index 375e9cd..23fd35b 100644
--- a/cc/aead/internal/ssl_aead_test.cc
+++ b/cc/aead/internal/ssl_aead_test.cc
@@ -34,7 +34,7 @@
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "tink/aead/internal/wycheproof_aead.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/ssl_util.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/secret_data.h"
@@ -524,7 +524,7 @@
 }
 
 TEST(SslOneShotAeadTest, AesGcmTestFipsOnly) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (IsFipsModeEnabled() && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test should not run in FIPS mode when BoringCrypto is "
                     "unavailable.";
   }
@@ -539,7 +539,7 @@
 }
 
 TEST(SslOneShotAeadTest, AesGcmTestTestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!IsFipsModeEnabled() || IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
diff --git a/cc/aead/internal/zero_copy_aead_wrapper.cc b/cc/aead/internal/zero_copy_aead_wrapper.cc
index 3ed95cc..b1e8114 100644
--- a/cc/aead/internal/zero_copy_aead_wrapper.cc
+++ b/cc/aead/internal/zero_copy_aead_wrapper.cc
@@ -60,7 +60,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  ~ZeroCopyAeadSetWrapper() override {}
+  ~ZeroCopyAeadSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<ZeroCopyAead>> aead_set_;
diff --git a/cc/aead/internal/zero_copy_aead_wrapper.h b/cc/aead/internal/zero_copy_aead_wrapper.h
index 1964a6d..beca0aa 100644
--- a/cc/aead/internal/zero_copy_aead_wrapper.h
+++ b/cc/aead/internal/zero_copy_aead_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_AEAD_INTERNAL_ZERO_COPY_AEAD_WRAPPER_H_
 #define TINK_AEAD_INTERNAL_ZERO_COPY_AEAD_WRAPPER_H_
 
+#include <memory>
+
 #include "tink/aead.h"
 #include "tink/aead/internal/zero_copy_aead.h"
 #include "tink/primitive_set.h"
@@ -46,4 +48,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_AEAD_AEAD_WRAPPER_H_
+#endif  // TINK_AEAD_INTERNAL_ZERO_COPY_AEAD_WRAPPER_H_
diff --git a/cc/aead/internal/zero_copy_aead_wrapper_test.cc b/cc/aead/internal/zero_copy_aead_wrapper_test.cc
index 489b7de..b4e2218 100644
--- a/cc/aead/internal/zero_copy_aead_wrapper_test.cc
+++ b/cc/aead/internal/zero_copy_aead_wrapper_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/aead/internal/zero_copy_aead_wrapper.h"
 
+#include <cstring>
 #include <memory>
 #include <string>
 #include <utility>
diff --git a/cc/aead/kms_envelope_aead.cc b/cc/aead/kms_envelope_aead.cc
index 887c253..8f36b0d 100644
--- a/cc/aead/kms_envelope_aead.cc
+++ b/cc/aead/kms_envelope_aead.cc
@@ -27,6 +27,7 @@
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "tink/aead.h"
+#include "tink/aead/internal/aead_util.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -60,6 +61,11 @@
 util::StatusOr<std::unique_ptr<Aead>> KmsEnvelopeAead::New(
     const google::crypto::tink::KeyTemplate& dek_template,
     std::unique_ptr<Aead> remote_aead) {
+  if (!internal::IsSupportedKmsEnvelopeAeadDekKeyType(
+          dek_template.type_url())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "unsupported key type");
+  }
   if (remote_aead == nullptr) {
     return util::Status(absl::StatusCode::kInvalidArgument,
                         "remote_aead must be non-null");
diff --git a/cc/aead/kms_envelope_aead.h b/cc/aead/kms_envelope_aead.h
index 1fd9d89..acfe066 100644
--- a/cc/aead/kms_envelope_aead.h
+++ b/cc/aead/kms_envelope_aead.h
@@ -58,7 +58,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  ~KmsEnvelopeAead() override {}
+  ~KmsEnvelopeAead() override = default;
 
  private:
   KmsEnvelopeAead(const google::crypto::tink::KeyTemplate& dek_template,
diff --git a/cc/aead/kms_envelope_aead_key_manager.h b/cc/aead/kms_envelope_aead_key_manager.h
index 04b9e31..69c99d1 100644
--- a/cc/aead/kms_envelope_aead_key_manager.h
+++ b/cc/aead/kms_envelope_aead_key_manager.h
@@ -25,6 +25,7 @@
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "tink/aead.h"
+#include "tink/aead/internal/aead_util.h"
 #include "tink/core/key_type_manager.h"
 #include "tink/core/template_util.h"
 #include "tink/internal/fips_utils.h"
@@ -75,6 +76,11 @@
       return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
                                         "Missing kek_uri.");
     }
+    if (!internal::IsSupportedKmsEnvelopeAeadDekKeyType(
+            format.dek_template().type_url())) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "unsupported dek key type");
+    }
     return util::OkStatus();
   }
 
diff --git a/cc/aead/kms_envelope_aead_key_manager_test.cc b/cc/aead/kms_envelope_aead_key_manager_test.cc
index 7b32aa1..4cee9c1 100644
--- a/cc/aead/kms_envelope_aead_key_manager_test.cc
+++ b/cc/aead/kms_envelope_aead_key_manager_test.cc
@@ -26,13 +26,16 @@
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "tink/aead.h"
+#include "tink/aead/aead_config.h"
 #include "tink/aead/aead_key_templates.h"
 #include "tink/aead/aes_eax_key_manager.h"
 #include "tink/aead/kms_envelope_aead.h"
 #include "tink/kms_client.h"
 #include "tink/kms_clients.h"
+#include "tink/mac/mac_key_templates.h"
 #include "tink/registry.h"
 #include "tink/subtle/aead_test_util.h"
+#include "tink/util/fake_kms_client.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
@@ -46,8 +49,9 @@
 using ::crypto::tink::test::DummyAead;
 using ::crypto::tink::test::DummyKmsClient;
 using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
 using ::crypto::tink::test::StatusIs;
-using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::KeyTemplate;
 using ::google::crypto::tink::KmsEnvelopeAeadKey;
 using ::google::crypto::tink::KmsEnvelopeAeadKeyFormat;
 using ::testing::Eq;
@@ -118,7 +122,15 @@
 
 TEST(KmsEnvelopeAeadKeyManagerTest, ValidateKeyFormatNoTemplate) {
   KmsEnvelopeAeadKeyFormat key_format;
-  *key_format.mutable_dek_template() = AeadKeyTemplates::Aes128Eax();
+  key_format.set_kek_uri("Some uri");
+  EXPECT_THAT(KmsEnvelopeAeadKeyManager().ValidateKeyFormat(key_format),
+              Not(IsOk()));
+}
+
+TEST(KmsEnvelopeAeadKeyManagerTest, ValidateKeyFormatInvalidDekTemplate) {
+  KmsEnvelopeAeadKeyFormat key_format;
+  key_format.set_kek_uri("Some uri");
+  *key_format.mutable_dek_template() = MacKeyTemplates::HmacSha256();
   EXPECT_THAT(KmsEnvelopeAeadKeyManager().ValidateKeyFormat(key_format),
               Not(IsOk()));
 }
@@ -229,6 +241,54 @@
               IsOk());
 }
 
+class KmsEnvelopeAeadKeyManagerDekTemplatesTest
+    : public testing::TestWithParam<KeyTemplate> {
+  void SetUp() override { ASSERT_THAT(AeadConfig::Register(), IsOk()); }
+};
+
+TEST_P(KmsEnvelopeAeadKeyManagerDekTemplatesTest, EncryptDecryp) {
+  util::StatusOr<std::string> kek_uri_result =
+      test::FakeKmsClient::CreateFakeKeyUri();
+  ASSERT_THAT(kek_uri_result, IsOk());
+  std::string kek_uri = kek_uri_result.value();
+  util::Status register_fake_kms_client_status =
+      test::FakeKmsClient::RegisterNewClient(kek_uri, /*credentials_path=*/"");
+  ASSERT_THAT(register_fake_kms_client_status, IsOk());
+
+  KeyTemplate dek_template = GetParam();
+  KeyTemplate env_template =
+      AeadKeyTemplates::KmsEnvelopeAead(kek_uri, dek_template);
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(env_template);
+  ASSERT_THAT(handle, IsOk());
+  util::StatusOr<std::unique_ptr<Aead>> envelope_aead =
+      (*handle)->GetPrimitive<Aead>();
+  ASSERT_THAT(envelope_aead, IsOk());
+
+  std::string plaintext = "plaintext";
+  std::string associated_data = "associated_data";
+  util::StatusOr<std::string> ciphertext =
+      (*envelope_aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  util::StatusOr<std::string> decrypted =
+      (*envelope_aead)->Decrypt(ciphertext.value(), associated_data);
+  EXPECT_THAT(decrypted, IsOkAndHolds(plaintext));
+
+  std::string invalid_associated_data = "invalid_associated_data";
+  util::StatusOr<std::string> decrypted_with_invalid_associated_data =
+      (*envelope_aead)->Decrypt(ciphertext.value(), invalid_associated_data);
+  EXPECT_THAT(decrypted_with_invalid_associated_data.status(), Not(IsOk()));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    KmsEnvelopeAeadKeyManagerDekTemplatesTest,
+    KmsEnvelopeAeadKeyManagerDekTemplatesTest,
+    testing::Values(AeadKeyTemplates::Aes128Gcm(),
+                    AeadKeyTemplates::Aes256Gcm(),
+                    AeadKeyTemplates::Aes128CtrHmacSha256(),
+                    AeadKeyTemplates::Aes128Eax(),
+                    AeadKeyTemplates::Aes128GcmNoPrefix()));
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/aead/kms_envelope_aead_test.cc b/cc/aead/kms_envelope_aead_test.cc
index 673a1b5..ef8395d 100644
--- a/cc/aead/kms_envelope_aead_test.cc
+++ b/cc/aead/kms_envelope_aead_test.cc
@@ -21,15 +21,18 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/base/internal/endian.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
+#include "absl/strings/string_view.h"
 #include "tink/aead.h"
 #include "tink/aead/aead_config.h"
 #include "tink/aead/aead_key_templates.h"
+#include "tink/keyset_handle.h"
 #include "tink/mac/mac_key_templates.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
@@ -37,147 +40,249 @@
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 #include "proto/aes_gcm.pb.h"
+#include "tink/internal/ssl_util.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-using crypto::tink::test::DummyAead;
-using crypto::tink::test::IsOk;
-using crypto::tink::test::StatusIs;
-using testing::HasSubstr;
+using ::crypto::tink::Aead;
+using ::crypto::tink::test::DummyAead;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyTemplate;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::Not;
+using ::testing::SizeIs;
+using ::testing::Test;
 
+constexpr int kEncryptedDekPrefixSize = 4;
+constexpr absl::string_view kRemoteAeadName = "kms-backed-aead";
 
-TEST(KmsEnvelopeAeadTest, BasicEncryptDecrypt) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
+class KmsEnvelopeAeadTest : public Test {
+ protected:
+  void TearDown() override { Registry::Reset(); }
+};
 
-  auto dek_template = AeadKeyTemplates::Aes128Eax();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
+TEST_F(KmsEnvelopeAeadTest, EncryptDecryptSucceed) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
 
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result, IsOk());
-  auto aead = std::move(aead_result.value());
+  // Use an AES-128-GCM primitive as the remote one.
+  util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(keyset_handle, IsOk());
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Eax();
+  util::StatusOr<std::unique_ptr<Aead>> remote_aead =
+      (*keyset_handle)->GetPrimitive<Aead>();
+
+  util::StatusOr<std::unique_ptr<Aead>> envelope_aead =
+      KmsEnvelopeAead::New(dek_template, *std::move(remote_aead));
+  ASSERT_THAT(envelope_aead, IsOk());
+
   std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto encrypt_result = aead->Encrypt(message, aad);
-  EXPECT_THAT(encrypt_result, IsOk());
-  auto decrypt_result = aead->Decrypt(encrypt_result.value(), aad);
-  EXPECT_THAT(decrypt_result, IsOk());
-  EXPECT_EQ(decrypt_result.value(), message);
+  std::string aad = "Some associated data.";
+  util::StatusOr<std::string> encrypt_result =
+      (*envelope_aead)->Encrypt(message, aad);
+  ASSERT_THAT(encrypt_result, IsOk());
+  util::StatusOr<std::string> decrypt_result =
+      (*envelope_aead)->Decrypt(encrypt_result.value(), aad);
+  EXPECT_THAT(decrypt_result, IsOkAndHolds(message));
 }
 
-TEST(KmsEnvelopeAeadTest, NullAead) {
-  auto dek_template = AeadKeyTemplates::Aes128Eax();
-  auto aead_result = KmsEnvelopeAead::New(dek_template, nullptr);
-  EXPECT_THAT(aead_result.status(), StatusIs(absl::StatusCode::kInvalidArgument,
-                                             HasSubstr("non-null")));
-}
-
-TEST(KmsEnvelopeAeadTest, MissingDekKeyManager) {
-  Registry::Reset();
-  auto dek_template = AeadKeyTemplates::Aes128Eax();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result.status(),
-              StatusIs(absl::StatusCode::kNotFound, HasSubstr("AesEaxKey")));
-}
-
-TEST(KmsEnvelopeAeadTest, WrongDekPrimitive) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
-  auto dek_template = MacKeyTemplates::HmacSha256();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result.status(),
-              StatusIs(absl::StatusCode::kInvalidArgument,
-                       HasSubstr("not among supported primitives")));
-}
-
-TEST(KmsEnvelopeAeadTest, DecryptionErrors) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
-
-  auto dek_template = AeadKeyTemplates::Aes128Gcm();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result, IsOk());
-  auto aead = std::move(aead_result.value());
-  std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto encrypt_result = aead->Encrypt(message, aad);
-  EXPECT_THAT(encrypt_result, IsOk());
-  auto ct = encrypt_result.value();
-
-  // Empty ciphertext.
-  auto decrypt_result = aead->Decrypt("", aad);
+TEST_F(KmsEnvelopeAeadTest, NewFailsIfReamoteAeadIsNull) {
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Eax();
   EXPECT_THAT(
-      decrypt_result.status(),
+      KmsEnvelopeAead::New(dek_template, /*remote_aead=*/nullptr).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("non-null")));
+}
+
+TEST_F(KmsEnvelopeAeadTest, NewFailsIfDekKeyManagerIsNotRegistered) {
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Eax();
+  auto remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  EXPECT_THAT(
+      KmsEnvelopeAead::New(dek_template, std::move(remote_aead)).status(),
+      StatusIs(absl::StatusCode::kNotFound, HasSubstr("AesEaxKey")));
+}
+
+TEST_F(KmsEnvelopeAeadTest, NewFailsIfUsingDekTemplateOfUnsupportedKeyType) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+  KeyTemplate dek_template = MacKeyTemplates::HmacSha256();
+  auto remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  EXPECT_THAT(
+      KmsEnvelopeAead::New(dek_template, std::move(remote_aead)).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("unsupported key type")));
+}
+
+TEST_F(KmsEnvelopeAeadTest, DecryptFailsWithInvalidCiphertextOrAad) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Gcm();
+  auto remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
+  ASSERT_THAT(aead, IsOk());
+
+  std::string message = "Some data to encrypt.";
+  std::string aad = "Some associated data.";
+  util::StatusOr<std::string> encrypt_result = (*aead)->Encrypt(message, aad);
+  ASSERT_THAT(encrypt_result, IsOk());
+  auto ciphertext = absl::string_view(*encrypt_result);
+
+  // Ciphertext has size zero or smaller than 4 bytes.
+  EXPECT_THAT(
+      (*aead)->Decrypt(/*ciphertext=*/"", aad).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("too short")));
+  EXPECT_THAT(
+      (*aead)->Decrypt(/*ciphertext=*/"sh", aad).status(),
       StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("too short")));
 
-  // Short ciphertext.
-  decrypt_result = aead->Decrypt("sh", aad);
+  // Ciphertext is smaller than the size of the key.
+  const int dek_encrypted_key_size = absl::big_endian::Load32(
+      reinterpret_cast<const uint8_t*>(ciphertext.data()));
+  // We leave only key size and key truncated by one.
   EXPECT_THAT(
-      decrypt_result.status(),
-      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("too short")));
-
-  // Truncated ciphertext.
-  decrypt_result = aead->Decrypt(ct.substr(2), aad);
-  EXPECT_THAT(
-      decrypt_result.status(),
+      (*aead)
+          ->Decrypt(ciphertext.substr(0, 4 + dek_encrypted_key_size - 1), aad)
+          .status(),
       StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("invalid")));
 
-  // Corrupted ciphertext.
-  auto ct_copy = ct;
-  ct_copy[4] = 'a';  // corrupt serialized DEK.
-  decrypt_result = aead->Decrypt(ct_copy, aad);
+  std::string corrupted_ciphertext = *encrypt_result;
+  // Corrupt the serialized DEK.
+  corrupted_ciphertext[4] = 'a';
   EXPECT_THAT(
-      decrypt_result.status(),
+      (*aead)->Decrypt(corrupted_ciphertext, aad).status(),
       StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("invalid")));
 
   // Wrong associated data.
-  decrypt_result = aead->Decrypt(ct, "wrong aad");
-  EXPECT_THAT(decrypt_result.status(),
+  EXPECT_THAT((*aead)->Decrypt(ciphertext, "wrong aad").status(),
               StatusIs(absl::StatusCode::kInternal,
                        HasSubstr("Authentication failed")));
 }
 
-TEST(KmsEnvelopeAeadTest, KeyFormat) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
+TEST_F(KmsEnvelopeAeadTest, DekMaintainsCorrectKeyFormat) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
 
-  auto dek_template = AeadKeyTemplates::Aes128Gcm();
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Gcm();
+  auto kms_remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      KmsEnvelopeAead::New(dek_template, std::move(kms_remote_aead));
+  ASSERT_THAT(aead, IsOk());
 
-  // Construct a remote AEAD which uses same key template for this test.
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-
-  // Create envelope AEAD and encrypt some data.
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result, IsOk());
-
-  auto aead = std::move(aead_result.value());
   std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto encrypt_result = aead->Encrypt(message, aad);
-  EXPECT_THAT(encrypt_result, IsOk());
-  auto ct = encrypt_result.value();
+  std::string aad = "Some associated data.";
+  util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(message, aad);
+  ASSERT_THAT(ciphertext, IsOk());
 
-  // Recover DEK from ciphertext
-  auto enc_dek_size =
-      absl::big_endian::Load32(reinterpret_cast<const uint8_t*>(ct.data()));
-
-  remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-  auto dek_decrypt_result =
-      remote_aead->Decrypt(ct.substr(4, enc_dek_size), "");
+  // Recover DEK from ciphertext (see
+  // https://developers.google.com/tink/wire-format#envelope_encryption).
+  auto enc_dek_size = absl::big_endian::Load32(
+      reinterpret_cast<const uint8_t*>(ciphertext->data()));
+  DummyAead remote_aead = DummyAead(kRemoteAeadName);
+  absl::string_view encrypted_dek =
+      absl::string_view(*ciphertext)
+          .substr(kEncryptedDekPrefixSize, enc_dek_size);
+  util::StatusOr<std::string> dek_proto_bytes =
+      remote_aead.Decrypt(encrypted_dek,
+                          /*associated_data=*/"");
+  ASSERT_THAT(dek_proto_bytes, IsOk());
 
   // Check if we can deserialize a GCM key proto from the decrypted DEK.
   google::crypto::tink::AesGcmKey key;
-  EXPECT_THAT(key.ParseFromString(dek_decrypt_result.value()), true);
-  EXPECT_THAT(key.key_value().size(), testing::Eq(16));
+  EXPECT_TRUE(key.ParseFromString(dek_proto_bytes.value()));
+  EXPECT_THAT(key.key_value(), SizeIs(16));
 }
 
+TEST_F(KmsEnvelopeAeadTest, MultipleEncryptionsProduceDifferentDeks) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Gcm();
+  auto kms_remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      KmsEnvelopeAead::New(dek_template, std::move(kms_remote_aead));
+  ASSERT_THAT(aead, IsOk());
+
+  std::string message = "Some data to encrypt.";
+  std::string aad = "Some associated data.";
+
+  constexpr int kNumIterations = 2;
+  std::vector<google::crypto::tink::AesGcmKey> ciphertexts;
+  ciphertexts.reserve(kNumIterations);
+  for (int i = 0; i < kNumIterations; i++) {
+    util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(message, aad);
+    ASSERT_THAT(ciphertext, IsOk());
+
+    auto enc_dek_size = absl::big_endian::Load32(
+        reinterpret_cast<const uint8_t*>(ciphertext->data()));
+    DummyAead remote_aead = DummyAead(kRemoteAeadName);
+    util::StatusOr<std::string> dek_proto_bytes = remote_aead.Decrypt(
+        ciphertext->substr(kEncryptedDekPrefixSize, enc_dek_size),
+        /*associated_data=*/"");
+    ASSERT_THAT(dek_proto_bytes, IsOk());
+
+    google::crypto::tink::AesGcmKey key;
+    ASSERT_TRUE(key.ParseFromString(dek_proto_bytes.value()));
+    ASSERT_THAT(key.key_value(), SizeIs(16));
+    ciphertexts.push_back(key);
+  }
+
+  for (int i = 0; i < ciphertexts.size() - 1; i++) {
+    for (int j = i + 1; j < ciphertexts.size(); j++) {
+      EXPECT_THAT(ciphertexts[i].SerializeAsString(),
+                  Not(Eq(ciphertexts[j].SerializeAsString())));
+    }
+  }
+}
+
+class KmsEnvelopeAeadDekTemplatesTest
+    : public testing::TestWithParam<KeyTemplate> {
+  void SetUp() override { ASSERT_THAT(AeadConfig::Register(), IsOk()); }
+};
+
+TEST_P(KmsEnvelopeAeadDekTemplatesTest, EncryptDecrypt) {
+  // Use an AES-128-GCM primitive as the remote AEAD.
+  util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(keyset_handle, IsOk());
+  util::StatusOr<std::unique_ptr<Aead>> remote_aead =
+      (*keyset_handle)->GetPrimitive<Aead>();
+
+  KeyTemplate dek_template = GetParam();
+  util::StatusOr<std::unique_ptr<Aead>> envelope_aead =
+      KmsEnvelopeAead::New(dek_template, *std::move(remote_aead));
+  ASSERT_THAT(envelope_aead, IsOk());
+
+  std::string plaintext = "plaintext";
+  std::string associated_data = "associated_data";
+  util::StatusOr<std::string> ciphertext =
+      (*envelope_aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  util::StatusOr<std::string> decrypted =
+      (*envelope_aead)->Decrypt(ciphertext.value(), associated_data);
+  EXPECT_THAT(decrypted, IsOkAndHolds(plaintext));
+}
+
+std::vector<KeyTemplate> GetTestTemplates() {
+  std::vector<KeyTemplate> templates = {
+    AeadKeyTemplates::Aes128Gcm(),
+    AeadKeyTemplates::Aes256Gcm(),
+    AeadKeyTemplates::Aes128CtrHmacSha256(),
+    AeadKeyTemplates::Aes128Eax(),
+    AeadKeyTemplates::Aes128GcmNoPrefix()
+  };
+  if (internal::IsBoringSsl()) {
+    templates.push_back(AeadKeyTemplates::XChaCha20Poly1305());
+    templates.push_back(AeadKeyTemplates::Aes256GcmSiv());
+  }
+  return templates;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    KmsEnvelopeAeadDekTemplatesTest, KmsEnvelopeAeadDekTemplatesTest,
+    testing::ValuesIn(GetTestTemplates()));
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/binary_keyset_reader.h b/cc/binary_keyset_reader.h
index 01d4220..7b718c1 100644
--- a/cc/binary_keyset_reader.h
+++ b/cc/binary_keyset_reader.h
@@ -18,6 +18,7 @@
 #define TINK_BINARY_KEYSET_READER_H_
 
 #include <istream>
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/binary_keyset_writer.h b/cc/binary_keyset_writer.h
index 802ba41..5e5a0dc 100644
--- a/cc/binary_keyset_writer.h
+++ b/cc/binary_keyset_writer.h
@@ -17,6 +17,7 @@
 #ifndef TINK_BINARY_KEYSET_WRITER_H_
 #define TINK_BINARY_KEYSET_WRITER_H_
 
+#include <memory>
 #include <ostream>
 #include <utility>
 
@@ -38,7 +39,7 @@
       std::unique_ptr<std::ostream> destination_stream);
 
   crypto::tink::util::Status
-  Write(const google::crypto::tink::Keyset& keyset) override;;
+  Write(const google::crypto::tink::Keyset& keyset) override;
 
   crypto::tink::util::Status
   Write(const google::crypto::tink::EncryptedKeyset& encrypted_keyset) override;
diff --git a/cc/catalogue.h b/cc/catalogue.h
deleted file mode 100644
index 0452c4c..0000000
--- a/cc/catalogue.h
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_CATALOGUE_H_
-#define TINK_CATALOGUE_H_
-
-#include <string>
-
-#include "absl/base/macros.h"
-#include "tink/key_manager.h"
-#include "tink/util/statusor.h"
-
-namespace crypto {
-namespace tink {
-
-// This class is deprecated. We don't support catalogues anymore.
-template <class P>
-class ABSL_DEPRECATED("Catalogues are not supported anymore.") Catalogue {
- public:
-  // Returns a key manager for the given 'type_url', 'primitive_name',
-  // and version at least 'min_version' (if any found).
-  // Caller owns the returned manager.
-  virtual crypto::tink::util::StatusOr<std::unique_ptr<KeyManager<P>>>
-  GetKeyManager(const std::string& type_url, const std::string& primitive_name,
-                uint32_t min_version) const = 0;
-
-  virtual ~Catalogue() {}
-};
-
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_CATALOGUE_H_
diff --git a/cc/config.h b/cc/config.h
deleted file mode 100644
index 099a448..0000000
--- a/cc/config.h
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_CONFIG_H_
-#define TINK_CONFIG_H_
-
-#include <string>
-
-#include "absl/status/status.h"
-#include "absl/strings/ascii.h"
-#include "tink/aead/aead_config.h"
-#include "tink/catalogue.h"
-#include "tink/daead/deterministic_aead_config.h"
-#include "tink/hybrid/hybrid_config.h"
-#include "tink/key_manager.h"
-#include "tink/mac/mac_config.h"
-#include "tink/registry.h"
-#include "tink/signature/signature_config.h"
-#include "tink/streamingaead/streaming_aead_config.h"
-#include "tink/util/errors.h"
-#include "tink/util/status.h"
-#include "proto/config.pb.h"
-
-namespace crypto {
-namespace tink {
-
-// Static methods for handling of Tink configurations.
-//
-// Configurations, i.e., collections of key types and their corresponding key
-// managers supported by a specific run-time environment enable control
-// of Tink setup via JSON-formatted config files that determine which key types
-// are supported, and provide a mechanism for deprecation of obsolete/outdated
-// cryptographic schemes (see tink/proto/config.proto for more info).
-//
-// Example usage:
-//
-// RegistryConfig registry_config = ...;
-// auto status = Config::Register(registry_config);
-//
-class Config {
- public:
-  // Returns a KeyTypeEntry for Tink key types with the specified parameters.
-  static std::unique_ptr<google::crypto::tink::KeyTypeEntry>
-  GetTinkKeyTypeEntry(const std::string& catalogue_name,
-                      const std::string& primitive_name,
-                      const std::string& key_proto_name,
-                      int key_manager_version, bool new_key_allowed);
-
-  // Registers a key manager according to the specification in 'entry'.
-  template <class P>
-  static crypto::tink::util::Status Register(
-      const google::crypto::tink::KeyTypeEntry& entry);
-
-  // Registers key managers and primitive wrappers according to the
-  // specification in 'config'.
-  static crypto::tink::util::Status Register(
-      const google::crypto::tink::RegistryConfig& config);
-
- private:
-  static crypto::tink::util::Status Validate(
-      const google::crypto::tink::KeyTypeEntry& entry);
-};
-
-///////////////////////////////////////////////////////////////////////////////
-// Implementation details of templated methods.
-
-// static
-template <class P>
-crypto::tink::util::Status Config::Register(
-    const google::crypto::tink::KeyTypeEntry& entry) {
-  util::Status status;
-  std::string primitive_name = absl::AsciiStrToLower(entry.primitive_name());
-
-  if (primitive_name == "mac") {
-    status = MacConfig::Register();
-  } else if (primitive_name == "aead") {
-    status = AeadConfig::Register();
-  } else if (primitive_name == "deterministicaead") {
-    status = DeterministicAeadConfig::Register();
-  } else if (primitive_name == "hybridencrypt" ||
-             primitive_name == "hybriddecrypt") {
-    status = HybridConfig::Register();
-  } else if (primitive_name == "publickeysign" ||
-             primitive_name == "publickeyverify") {
-    status = SignatureConfig::Register();
-  } else if (primitive_name == "streamingaead") {
-    status = StreamingAeadConfig::Register();
-  } else {
-    status = util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("Non-standard primitive '", entry.primitive_name(),
-                     "', call Registry::RegisterKeyManager "
-                     "and Registry::"
-                     "RegisterPrimitiveWrapper directly."));
-  }
-  if (!status.ok()) return status;
-  return util::OkStatus();
-}
-
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_CONFIG_H_
diff --git a/cc/config/BUILD.bazel b/cc/config/BUILD.bazel
index 8ec12df..8d77a3e 100644
--- a/cc/config/BUILD.bazel
+++ b/cc/config/BUILD.bazel
@@ -11,7 +11,6 @@
     include_prefix = "tink/config",
     visibility = ["//visibility:public"],
     deps = [
-        "//:config",
         "//:key_manager",
         "//:registry",
         "//daead:deterministic_aead_config",
@@ -38,28 +37,6 @@
     build_setting_default = False,
 )
 
-bool_flag(
-    name = "tink_use_absl_status",
-    build_setting_default = False,
-)
-
-config_setting(
-    name = "absl_status_enabled",
-    flag_values = {"//config:tink_use_absl_status": "True"},
-    visibility = ["//visibility:public"],
-)
-
-bool_flag(
-    name = "tink_use_absl_statusor",
-    build_setting_default = False,
-)
-
-config_setting(
-    name = "absl_statusor_enabled",
-    flag_values = {"//config:tink_use_absl_statusor": "True"},
-    visibility = ["//visibility:public"],
-)
-
 cc_library(
     name = "tink_fips",
     srcs = ["tink_fips.cc"],
@@ -75,6 +52,60 @@
     ],
 )
 
+cc_library(
+    name = "fips_140_2",
+    srcs = ["fips_140_2.cc"],
+    hdrs = ["fips_140_2.h"],
+    include_prefix = "tink/config",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:configuration",
+        "//aead:aead_wrapper",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:configuration_impl",
+        "//internal:fips_utils",
+        "//mac:hmac_key_manager",
+        "//mac:mac_wrapper",
+        "//mac/internal:chunked_mac_wrapper",
+        "//prf:hmac_prf_key_manager",
+        "//prf:prf_set_wrapper",
+        "//signature:ecdsa_sign_key_manager",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:public_key_sign_wrapper",
+        "//signature:public_key_verify_wrapper",
+        "//signature:rsa_ssa_pkcs1_sign_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_sign_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
+cc_library(
+    name = "key_gen_fips_140_2",
+    srcs = ["key_gen_fips_140_2.cc"],
+    hdrs = ["key_gen_fips_140_2.h"],
+    include_prefix = "tink/config",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:key_gen_configuration",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:fips_utils",
+        "//internal:key_gen_configuration_impl",
+        "//mac:hmac_key_manager",
+        "//prf:hmac_prf_key_manager",
+        "//signature:ecdsa_sign_key_manager",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:rsa_ssa_pkcs1_sign_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_sign_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
 # tests
 
 cc_test(
@@ -84,7 +115,6 @@
     deps = [
         ":tink_config",
         "//:aead",
-        "//:config",
         "//:deterministic_aead",
         "//:hybrid_decrypt",
         "//:hybrid_encrypt",
@@ -127,3 +157,50 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "fips_140_2_test",
+    srcs = ["fips_140_2_test.cc"],
+    deps = [
+        ":fips_140_2",
+        "//aead:aead_key_templates",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:configuration_impl",
+        "//internal:fips_utils",
+        "//internal:key_type_info_store",
+        "//mac:aes_cmac_key_manager",
+        "//mac:hmac_key_manager",
+        "//prf:hmac_prf_key_manager",
+        "//proto:tink_cc_proto",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "//util:test_keyset_handle",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "key_gen_fips_140_2_test",
+    srcs = ["key_gen_fips_140_2_test.cc"],
+    deps = [
+        ":key_gen_fips_140_2",
+        "//aead:aead_key_templates",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:fips_utils",
+        "//internal:key_gen_configuration_impl",
+        "//mac:aes_cmac_key_manager",
+        "//mac:hmac_key_manager",
+        "//prf:hmac_prf_key_manager",
+        "//proto:tink_cc_proto",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/config/CMakeLists.txt b/cc/config/CMakeLists.txt
index d479a98..010958c 100644
--- a/cc/config/CMakeLists.txt
+++ b/cc/config/CMakeLists.txt
@@ -1,5 +1,7 @@
 tink_module(config)
 
+add_subdirectory(internal)
+
 tink_cc_library(
   NAME tink_config
   SRCS
@@ -7,7 +9,6 @@
     tink_config.h
   DEPS
     absl::core_headers
-    tink::core::config
     tink::core::key_manager
     tink::core::registry
     tink::daead::deterministic_aead_config
@@ -41,6 +42,56 @@
     tink::util::status
 )
 
+tink_cc_library(
+  NAME fips_140_2
+  SRCS
+    fips_140_2.cc
+    fips_140_2.h
+  DEPS
+    absl::check
+    tink::core::configuration
+    tink::aead::aead_wrapper
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::configuration_impl
+    tink::internal::fips_utils
+    tink::mac::hmac_key_manager
+    tink::mac::mac_wrapper
+    tink::mac::internal::chunked_mac_wrapper
+    tink::prf::hmac_prf_key_manager
+    tink::prf::prf_set_wrapper
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::public_key_sign_wrapper
+    tink::signature::public_key_verify_wrapper
+    tink::signature::rsa_ssa_pkcs1_sign_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_sign_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::signature::ecdsa_sign_key_manager
+)
+
+tink_cc_library(
+  NAME key_gen_fips_140_2
+  SRCS
+    key_gen_fips_140_2.cc
+    key_gen_fips_140_2.h
+  DEPS
+    absl::check
+    tink::core::key_gen_configuration
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::fips_utils
+    tink::internal::key_gen_configuration_impl
+    tink::mac::hmac_key_manager
+    tink::prf::hmac_prf_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::rsa_ssa_pkcs1_sign_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_sign_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::signature::ecdsa_sign_key_manager
+)
+
 # tests
 
 tink_cc_test(
@@ -53,7 +104,6 @@
     absl::status
     tink::core::cc
     tink::core::aead
-    tink::core::config
     tink::core::deterministic_aead
     tink::core::hybrid_decrypt
     tink::core::hybrid_encrypt
@@ -89,3 +139,50 @@
     tink::util::status
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME fips_140_2_test
+  SRCS
+    fips_140_2_test.cc
+  DEPS
+    tink::config::fips_140_2
+    gmock
+    tink::aead::aead_key_templates
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::configuration_impl
+    tink::internal::fips_utils
+    tink::internal::key_type_info_store
+    tink::mac::aes_cmac_key_manager
+    tink::mac::hmac_key_manager
+    tink::prf::hmac_prf_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::util::test_keyset_handle
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME key_gen_fips_140_2_test
+  SRCS
+    key_gen_fips_140_2_test.cc
+  DEPS
+    tink::config::key_gen_fips_140_2
+    gmock
+    tink::aead::aead_key_templates
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::fips_utils
+    tink::internal::key_gen_configuration_impl
+    tink::mac::aes_cmac_key_manager
+    tink::mac::hmac_key_manager
+    tink::prf::hmac_prf_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/config/fips_140_2.cc b/cc/config/fips_140_2.cc
new file mode 100644
index 0000000..0eafff5
--- /dev/null
+++ b/cc/config/fips_140_2.cc
@@ -0,0 +1,134 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/fips_140_2.h"
+
+#include "absl/log/check.h"
+#include "tink/aead/aead_wrapper.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/configuration.h"
+#include "tink/internal/configuration_impl.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/mac/hmac_key_manager.h"
+#include "tink/mac/internal/chunked_mac_wrapper.h"
+#include "tink/mac/mac_wrapper.h"
+#include "tink/prf/hmac_prf_key_manager.h"
+#include "tink/prf/prf_set_wrapper.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/signature/public_key_sign_wrapper.h"
+#include "tink/signature/public_key_verify_wrapper.h"
+#include "tink/signature/rsa_ssa_pkcs1_sign_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_sign_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
+#include "tink/signature/ecdsa_sign_key_manager.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::Status AddMac(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<MacWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<internal::ChunkedMacWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  return internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<HmacKeyManager>(), config);
+}
+
+util::Status AddAead(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<AeadWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<AesCtrHmacAeadKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  return internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<AesGcmKeyManager>(), config);
+}
+
+util::Status AddPrf(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<PrfSetWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  return internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<HmacPrfKeyManager>(), config);
+}
+
+util::Status AddSignature(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<PublicKeySignWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<PublicKeyVerifyWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::ConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<EcdsaSignKeyManager>(),
+      absl::make_unique<EcdsaVerifyKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::ConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<RsaSsaPssSignKeyManager>(),
+      absl::make_unique<RsaSsaPssVerifyKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  return internal::ConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<RsaSsaPkcs1SignKeyManager>(),
+      absl::make_unique<RsaSsaPkcs1VerifyKeyManager>(), config);
+}
+
+}  // namespace
+
+const Configuration& ConfigFips140_2() {
+  static const Configuration* instance = [] {
+    internal::SetFipsRestricted();
+
+    static Configuration* config = new Configuration();
+    CHECK_OK(AddMac(*config));
+    CHECK_OK(AddAead(*config));
+    CHECK_OK(AddPrf(*config));
+    CHECK_OK(AddSignature(*config));
+
+    return config;
+  }();
+  return *instance;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/fips_140_2.h b/cc/config/fips_140_2.h
new file mode 100644
index 0000000..24fa9b1
--- /dev/null
+++ b/cc/config/fips_140_2.h
@@ -0,0 +1,33 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_CONFIG_FIPS_140_2_H_
+#define TINK_CONFIG_FIPS_140_2_H_
+
+#include "tink/configuration.h"
+
+namespace crypto {
+namespace tink {
+
+// Configuration used to generate primitives using FIPS 140-2-compliant key
+// types. Importing this Configuration restricts Tink to FIPS globally and
+// requires BoringSSL to be built with the BoringCrypto module.
+const Configuration& ConfigFips140_2();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_CONFIG_FIPS_140_2_H_
diff --git a/cc/config/fips_140_2_test.cc b/cc/config/fips_140_2_test.cc
new file mode 100644
index 0000000..7f134c9
--- /dev/null
+++ b/cc/config/fips_140_2_test.cc
@@ -0,0 +1,142 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/fips_140_2.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/internal/configuration_impl.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/key_type_info_store.h"
+#include "tink/mac/aes_cmac_key_manager.h"
+#include "tink/mac/hmac_key_manager.h"
+#include "tink/prf/hmac_prf_key_manager.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
+#include "tink/util/test_keyset_handle.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+
+class Fips1402Test : public ::testing::Test {
+ protected:
+  void TearDown() override { internal::UnSetFipsRestricted(); }
+};
+
+TEST_F(Fips1402Test, ConfigFips1402) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::ConfigurationImpl::GetKeyTypeInfoStore(ConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+
+  EXPECT_THAT((*store)->Get(HmacKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(AesCtrHmacAeadKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(AesGcmKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(HmacPrfKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(EcdsaVerifyKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(RsaSsaPssVerifyKeyManager().get_key_type()),
+              IsOk());
+  EXPECT_THAT((*store)->Get(RsaSsaPkcs1VerifyKeyManager().get_key_type()),
+              IsOk());
+}
+
+TEST_F(Fips1402Test, ConfigFips1402FailsInNonFipsMode) {
+  if (internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in non-FIPS mode";
+  }
+
+  EXPECT_DEATH_IF_SUPPORTED(
+      ConfigFips140_2(), "BoringSSL not built with the BoringCrypto module.");
+}
+
+TEST_F(Fips1402Test, NonFipsTypeNotPresent) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::ConfigurationImpl::GetKeyTypeInfoStore(ConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+  EXPECT_THAT((*store)->Get(AesCmacKeyManager().get_key_type()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(Fips1402Test, NewKeyDataAndGetPrimitive) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  // TODO(b/265705174): Replace with KeysetHandle::GenerateNew once that takes a
+  // config parameter.
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::ConfigurationImpl::GetKeyTypeInfoStore(ConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+  KeyTemplate templ = AeadKeyTemplates::Aes128Gcm();
+  util::StatusOr<internal::KeyTypeInfoStore::Info*> info =
+      (*store)->Get(templ.type_url());
+  ASSERT_THAT(info, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeyData>> key_data =
+      (*info)->key_factory().NewKeyData(templ.value());
+  ASSERT_THAT(key_data, IsOk());
+
+  Keyset keyset;
+  uint32_t key_id = 0;
+  test::AddKeyData(**key_data, key_id, OutputPrefixType::TINK,
+                   KeyStatusType::ENABLED, &keyset);
+  keyset.set_primary_key_id(key_id);
+
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      handle->GetPrimitive<Aead>(ConfigFips140_2());
+  EXPECT_THAT(aead, IsOk());
+
+  std::string plaintext = "plaintext";
+  std::string ad = "ad";
+  util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(plaintext, ad);
+  ASSERT_THAT(ciphertext, IsOk());
+
+  util::StatusOr<std::string> decrypted = (*aead)->Decrypt(*ciphertext, ad);
+  EXPECT_THAT(decrypted, IsOkAndHolds(plaintext));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/internal/BUILD.bazel b/cc/config/internal/BUILD.bazel
new file mode 100644
index 0000000..6d94c3a
--- /dev/null
+++ b/cc/config/internal/BUILD.bazel
@@ -0,0 +1,29 @@
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "global_registry",
+    srcs = ["global_registry.cc"],
+    hdrs = ["global_registry.h"],
+    include_prefix = "tink/config/internal",
+    deps = [
+        "//:key_gen_configuration",
+        "//internal:key_gen_configuration_impl",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
+cc_test(
+    name = "global_registry_test",
+    srcs = ["global_registry_test.cc"],
+    deps = [
+        ":global_registry",
+        "//:keyset_handle",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/config/internal/CMakeLists.txt b/cc/config/internal/CMakeLists.txt
new file mode 100644
index 0000000..3e19a05
--- /dev/null
+++ b/cc/config/internal/CMakeLists.txt
@@ -0,0 +1,26 @@
+tink_module(config::internal)
+
+tink_cc_library(
+  NAME global_registry
+  SRCS
+    global_registry.cc
+    global_registry.h
+  DEPS
+    absl::check
+    tink::core::key_gen_configuration
+    tink::internal::key_gen_configuration_impl
+)
+
+tink_cc_test(
+  NAME global_registry_test
+  SRCS
+    global_registry_test.cc
+  DEPS
+    tink::config::internal::global_registry
+    gmock
+    absl::status
+    tink::core::keyset_handle
+    tink::util::test_matchers
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/config/internal/global_registry.cc b/cc/config/internal/global_registry.cc
new file mode 100644
index 0000000..e1e6907
--- /dev/null
+++ b/cc/config/internal/global_registry.cc
@@ -0,0 +1,38 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/internal/global_registry.h"
+
+#include "absl/log/check.h"
+#include "tink/internal/key_gen_configuration_impl.h"
+#include "tink/key_gen_configuration.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+const KeyGenConfiguration& KeyGenConfigGlobalRegistry() {
+  static const KeyGenConfiguration* instance = [] {
+    static KeyGenConfiguration* config = new KeyGenConfiguration();
+    CHECK_OK(KeyGenConfigurationImpl::SetGlobalRegistryMode(*config));
+    return config;
+  }();
+  return *instance;
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/internal/global_registry.h b/cc/config/internal/global_registry.h
new file mode 100644
index 0000000..e1c8996
--- /dev/null
+++ b/cc/config/internal/global_registry.h
@@ -0,0 +1,34 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_CONFIG_INTERNAL_GLOBAL_REGISTRY_H_
+#define TINK_CONFIG_INTERNAL_GLOBAL_REGISTRY_H_
+
+#include "tink/key_gen_configuration.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// TODO(b/265705174): Move to public API after API review.
+// Used to generate keys using the global crypto::tink::Registry.
+const crypto::tink::KeyGenConfiguration& KeyGenConfigGlobalRegistry();
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_CONFIG_INTERNAL_GLOBAL_REGISTRY_H_
diff --git a/cc/config/internal/global_registry_test.cc b/cc/config/internal/global_registry_test.cc
new file mode 100644
index 0000000..b5196f3
--- /dev/null
+++ b/cc/config/internal/global_registry_test.cc
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/internal/global_registry.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/test_matchers.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+
+class FakePrimitive {
+ public:
+  explicit FakePrimitive(std::string s) : s_(s) {}
+  std::string get() { return s_; }
+
+ private:
+  std::string s_;
+};
+
+class FakeKeyTypeManager
+    : public KeyTypeManager<AesGcmKey, AesGcmKeyFormat, List<FakePrimitive>> {
+ public:
+  class FakePrimitiveFactory : public PrimitiveFactory<FakePrimitive> {
+   public:
+    util::StatusOr<std::unique_ptr<FakePrimitive>> Create(
+        const AesGcmKey& key) const override {
+      return absl::make_unique<FakePrimitive>(key.key_value());
+    }
+  };
+
+  FakeKeyTypeManager()
+      : KeyTypeManager(absl::make_unique<FakePrimitiveFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::SYMMETRIC;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const AesGcmKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateKeyFormat(
+      const AesGcmKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  util::StatusOr<AesGcmKey> CreateKey(
+      const AesGcmKeyFormat& key_format) const override {
+    return AesGcmKey();
+  }
+
+  util::StatusOr<AesGcmKey> DeriveKey(
+      const AesGcmKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return AesGcmKey();
+  }
+
+ private:
+  const std::string key_type_ =
+      "type.googleapis.com/google.crypto.tink.AesGcmKey";
+};
+
+TEST(GlobalRegistryTest, KeyGenConfigGlobalRegistry) {
+  Registry::Reset();
+
+  KeyTemplate templ;
+  templ.set_type_url("type.googleapis.com/google.crypto.tink.AesGcmKey");
+  templ.set_output_prefix_type(OutputPrefixType::TINK);
+
+  EXPECT_THAT(
+      KeysetHandle::GenerateNew(templ, KeyGenConfigGlobalRegistry()).status(),
+      StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<FakeKeyTypeManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  EXPECT_THAT(
+      KeysetHandle::GenerateNew(templ, KeyGenConfigGlobalRegistry()).status(),
+      IsOk());
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/key_gen_fips_140_2.cc b/cc/config/key_gen_fips_140_2.cc
new file mode 100644
index 0000000..1c260be
--- /dev/null
+++ b/cc/config/key_gen_fips_140_2.cc
@@ -0,0 +1,95 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/key_gen_fips_140_2.h"
+
+#include "absl/log/check.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/key_gen_configuration_impl.h"
+#include "tink/key_gen_configuration.h"
+#include "tink/mac/hmac_key_manager.h"
+#include "tink/prf/hmac_prf_key_manager.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_sign_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_sign_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
+#include "tink/signature/ecdsa_sign_key_manager.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::Status AddMac(KeyGenConfiguration& config) {
+  return internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<HmacKeyManager>(), config);
+}
+
+util::Status AddAead(KeyGenConfiguration& config) {
+  util::Status status = internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<AesCtrHmacAeadKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  return internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<AesGcmKeyManager>(), config);
+}
+
+util::Status AddPrf(KeyGenConfiguration& config) {
+  return internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<HmacPrfKeyManager>(), config);
+}
+
+util::Status AddSignature(KeyGenConfiguration& config) {
+  util::Status status =
+      internal::KeyGenConfigurationImpl::AddAsymmetricKeyManagers(
+          absl::make_unique<EcdsaSignKeyManager>(),
+          absl::make_unique<EcdsaVerifyKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::KeyGenConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<RsaSsaPssSignKeyManager>(),
+      absl::make_unique<RsaSsaPssVerifyKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  return internal::KeyGenConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<RsaSsaPkcs1SignKeyManager>(),
+      absl::make_unique<RsaSsaPkcs1VerifyKeyManager>(), config);
+}
+
+}  // namespace
+
+const KeyGenConfiguration& KeyGenConfigFips140_2() {
+  static const KeyGenConfiguration* instance = [] {
+    internal::SetFipsRestricted();
+
+    static KeyGenConfiguration* config = new KeyGenConfiguration();
+    CHECK_OK(AddMac(*config));
+    CHECK_OK(AddAead(*config));
+    CHECK_OK(AddPrf(*config));
+    CHECK_OK(AddSignature(*config));
+
+    return config;
+  }();
+  return *instance;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/key_gen_fips_140_2.h b/cc/config/key_gen_fips_140_2.h
new file mode 100644
index 0000000..a22e7ed
--- /dev/null
+++ b/cc/config/key_gen_fips_140_2.h
@@ -0,0 +1,33 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_CONFIG_KEY_GEN_FIPS_140_2_H_
+#define TINK_CONFIG_KEY_GEN_FIPS_140_2_H_
+
+#include "tink/key_gen_configuration.h"
+
+namespace crypto {
+namespace tink {
+
+// KeyGenConfiguration used to generate keys using FIPS 140-2-compliant key
+// types. Importing this KeyGenConfiguration restricts Tink to FIPS globally and
+// requires BoringSSL to be built with the BoringCrypto module.
+const KeyGenConfiguration& KeyGenConfigFips140_2();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_CONFIG_KEY_GEN_FIPS_140_2_H_
diff --git a/cc/config/key_gen_fips_140_2_test.cc b/cc/config/key_gen_fips_140_2_test.cc
new file mode 100644
index 0000000..3b9994c
--- /dev/null
+++ b/cc/config/key_gen_fips_140_2_test.cc
@@ -0,0 +1,118 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/key_gen_fips_140_2.h"
+
+#include <memory>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/key_gen_configuration_impl.h"
+#include "tink/mac/aes_cmac_key_manager.h"
+#include "tink/mac/hmac_key_manager.h"
+#include "tink/prf/hmac_prf_key_manager.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::KeyTemplate;
+
+class KeyGenFips1402Test : public testing::Test {
+ protected:
+  void TearDown() override { internal::UnSetFipsRestricted(); }
+};
+
+TEST_F(KeyGenFips1402Test, KeyGenConfigFips1402) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::KeyGenConfigurationImpl::GetKeyTypeInfoStore(
+          KeyGenConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+
+  EXPECT_THAT((*store)->Get(HmacKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(AesCtrHmacAeadKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(AesGcmKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(HmacPrfKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(EcdsaVerifyKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(RsaSsaPssVerifyKeyManager().get_key_type()),
+              IsOk());
+  EXPECT_THAT((*store)->Get(RsaSsaPkcs1VerifyKeyManager().get_key_type()),
+              IsOk());
+}
+
+TEST_F(KeyGenFips1402Test, KeyGenConfigFips1402FailsInNonFipsMode) {
+  if (internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in non-FIPS mode";
+  }
+
+  EXPECT_DEATH_IF_SUPPORTED(
+      KeyGenConfigFips140_2(),
+      "BoringSSL not built with the BoringCrypto module.");
+}
+
+TEST_F(KeyGenFips1402Test, NonFipsTypeNotPresent) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::KeyGenConfigurationImpl::GetKeyTypeInfoStore(
+          KeyGenConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+  EXPECT_THAT((*store)->Get(AesCmacKeyManager().get_key_type()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(KeyGenFips1402Test, NewKeyData) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  // TODO(b/265705174): Replace with KeysetHandle::GenerateNew once that takes a
+  // config parameter.
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::KeyGenConfigurationImpl::GetKeyTypeInfoStore(
+          KeyGenConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+  KeyTemplate templ = AeadKeyTemplates::Aes128Gcm();
+  util::StatusOr<internal::KeyTypeInfoStore::Info*> info =
+      (*store)->Get(templ.type_url());
+  ASSERT_THAT(info, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeyData>> key_data =
+      (*info)->key_factory().NewKeyData(templ.value());
+  EXPECT_THAT(key_data, IsOk());
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/tink_config.cc b/cc/config/tink_config.cc
index d909ab8..5210bfa 100644
--- a/cc/config/tink_config.cc
+++ b/cc/config/tink_config.cc
@@ -16,7 +16,6 @@
 
 #include "tink/config/tink_config.h"
 
-#include "tink/config.h"
 #include "tink/daead/deterministic_aead_config.h"
 #include "tink/hybrid/hybrid_config.h"
 #include "tink/key_manager.h"
diff --git a/cc/config/tink_config_test.cc b/cc/config/tink_config_test.cc
index 3cd046d..e1cd175 100644
--- a/cc/config/tink_config_test.cc
+++ b/cc/config/tink_config_test.cc
@@ -20,7 +20,6 @@
 #include "absl/status/status.h"
 #include "tink/aead.h"
 #include "tink/aead/aes_gcm_key_manager.h"
-#include "tink/config.h"
 #include "tink/deterministic_aead.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/hybrid_encrypt.h"
diff --git a/cc/config/tink_fips_test.cc b/cc/config/tink_fips_test.cc
index 61dc06d..ec15ca7 100644
--- a/cc/config/tink_fips_test.cc
+++ b/cc/config/tink_fips_test.cc
@@ -65,7 +65,7 @@
 }
 
 TEST(TinkFipsTest, CompatibilityChecksWithBoringCrypto) {
-  if (!FIPS_mode()) {
+  if (!internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test only run if BoringCrypto module is available.";
   }
 
@@ -88,7 +88,7 @@
 }
 
 TEST(TinkFipsTest, CompatibilityChecksWithoutBoringCrypto) {
-  if (FIPS_mode()) {
+  if (internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test only run if BoringCrypto module is not available.";
   }
 
diff --git a/cc/configuration.h b/cc/configuration.h
new file mode 100644
index 0000000..a087737
--- /dev/null
+++ b/cc/configuration.h
@@ -0,0 +1,55 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_CONFIGURATION_H_
+#define TINK_CONFIGURATION_H_
+
+#include "tink/internal/key_type_info_store.h"
+#include "tink/internal/keyset_wrapper_store.h"
+
+namespace crypto {
+namespace tink {
+
+namespace internal {
+class ConfigurationImpl;
+}
+
+// Configuration used to generate primitives using stored primitive wrappers and
+// key type managers.
+class Configuration {
+ public:
+  Configuration() = default;
+
+  // Not copyable or movable.
+  Configuration(const Configuration&) = delete;
+  Configuration& operator=(const Configuration&) = delete;
+
+ private:
+  friend class internal::ConfigurationImpl;
+
+  // When true, Configuration is in global registry mode. For `some_fn(config)`
+  // with a `config` parameter, this indicates to `some_fn` to use
+  // crypto::tink::Registry directly.
+  bool global_registry_mode_ = false;
+
+  crypto::tink::internal::KeyTypeInfoStore key_type_info_store_;
+  crypto::tink::internal::KeysetWrapperStore keyset_wrapper_store_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_CONFIGURATION_H_
diff --git a/cc/core/cleartext_keyset_handle.cc b/cc/core/cleartext_keyset_handle.cc
index 989f95e..9e000ff 100644
--- a/cc/core/cleartext_keyset_handle.cc
+++ b/cc/core/cleartext_keyset_handle.cc
@@ -20,6 +20,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/container/flat_hash_map.h"
 #include "absl/status/status.h"
@@ -41,14 +42,23 @@
     std::unique_ptr<KeysetReader> reader,
     const absl::flat_hash_map<std::string, std::string>&
         monitoring_annotations) {
-  auto keyset_result = reader->Read();
+  util::StatusOr<std::unique_ptr<Keyset>> keyset_result = reader->Read();
   if (!keyset_result.ok()) {
     return ToStatusF(absl::StatusCode::kInvalidArgument,
                      "Error reading keyset data: %s",
                      keyset_result.status().message());
   }
+  util::StatusOr<std::vector<std::shared_ptr<const KeysetHandle::Entry>>>
+      entries = KeysetHandle::GetEntriesFromKeyset(**keyset_result);
+  if (!entries.ok()) {
+    return entries.status();
+  }
+  if (entries->size() != (*keyset_result)->key_size()) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Error converting keyset proto into key entries.");
+  }
   std::unique_ptr<KeysetHandle> handle(new KeysetHandle(
-      std::move(keyset_result.value()), monitoring_annotations));
+      std::move(keyset_result.value()), *entries, monitoring_annotations));
   return std::move(handle);
 }
 
diff --git a/cc/core/cleartext_keyset_handle_test.cc b/cc/core/cleartext_keyset_handle_test.cc
index d172ccf..0ff416c 100644
--- a/cc/core/cleartext_keyset_handle_test.cc
+++ b/cc/core/cleartext_keyset_handle_test.cc
@@ -29,7 +29,6 @@
 #include "tink/util/test_util.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddRawKey;
 using crypto::tink::test::AddTinkKey;
 
@@ -49,9 +48,9 @@
 TEST_F(CleartextKeysetHandleTest, testRead) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   {  // Reader that reads a valid keyset.
@@ -76,9 +75,9 @@
 TEST_F(CleartextKeysetHandleTest, testWrite) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
 
diff --git a/cc/core/config.cc b/cc/core/config.cc
deleted file mode 100644
index fe5cae2..0000000
--- a/cc/core/config.cc
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/config.h"
-
-#include <memory>
-#include <string>
-
-#include "absl/status/status.h"
-#include "absl/strings/ascii.h"
-#include "absl/strings/str_cat.h"
-#include "tink/util/errors.h"
-#include "tink/util/status.h"
-#include "tink/util/statusor.h"
-#include "proto/config.pb.h"
-
-using google::crypto::tink::KeyTypeEntry;
-
-namespace crypto {
-namespace tink {
-
-// static
-std::unique_ptr<google::crypto::tink::KeyTypeEntry> Config::GetTinkKeyTypeEntry(
-    const std::string& catalogue_name, const std::string& primitive_name,
-    const std::string& key_proto_name, int key_manager_version,
-    bool new_key_allowed) {
-  std::string prefix = "type.googleapis.com/google.crypto.tink.";
-  std::unique_ptr<KeyTypeEntry> entry(new KeyTypeEntry());
-  entry->set_catalogue_name(catalogue_name);
-  entry->set_primitive_name(primitive_name);
-  entry->set_type_url(prefix.append(key_proto_name));
-  entry->set_key_manager_version(key_manager_version);
-  entry->set_new_key_allowed(new_key_allowed);
-  return entry;
-}
-
-// static
-crypto::tink::util::Status Config::Validate(const KeyTypeEntry& entry) {
-  if (entry.type_url().empty()) {
-    return util::Status(absl::StatusCode::kInvalidArgument,
-                        "Missing type_url.");
-  }
-  if (entry.primitive_name().empty()) {
-    return util::Status(absl::StatusCode::kInvalidArgument,
-                        "Missing primitive_name.");
-  }
-  if (entry.catalogue_name().empty()) {
-    return util::Status(absl::StatusCode::kInvalidArgument,
-                        "Missing catalogue_name.");
-  }
-  return util::OkStatus();
-}
-
-// static
-util::Status Config::Register(
-    const google::crypto::tink::RegistryConfig& config) {
-  util::Status status;
-  status = MacConfig::Register();
-  if (!status.ok()) return status;
-  status = AeadConfig::Register();
-  if (!status.ok()) return status;
-  status = DeterministicAeadConfig::Register();
-  if (!status.ok()) return status;
-  status = HybridConfig::Register();
-  if (!status.ok()) return status;
-  status = SignatureConfig::Register();
-  if (!status.ok()) return status;
-  status = StreamingAeadConfig::Register();
-  if (!status.ok()) return status;
-  return util::OkStatus();
-}
-
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/core/config_test.cc b/cc/core/config_test.cc
deleted file mode 100644
index 2633475..0000000
--- a/cc/core/config_test.cc
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-#include "tink/config.h"
-
-#include "gtest/gtest.h"
-#include "tink/mac.h"
-#include "proto/config.pb.h"
-
-using google::crypto::tink::KeyTypeEntry;
-
-namespace crypto {
-namespace tink {
-namespace {
-
-class ConfigTest : public ::testing::Test {
-};
-
-TEST_F(ConfigTest, testValidation) {
-  KeyTypeEntry entry;
-
-  auto status = Config::Register<Mac>(entry);
-  EXPECT_FALSE(status.ok());
-  EXPECT_EQ(absl::StatusCode::kInvalidArgument, status.code());
-
-  entry.set_type_url("some key type");
-  entry.set_catalogue_name("some catalogue");
-  status = Config::Register<Mac>(entry);
-  EXPECT_FALSE(status.ok());
-  EXPECT_EQ(absl::StatusCode::kInvalidArgument, status.code());
-
-  entry.set_primitive_name("some primitive");
-  status = Config::Register<Mac>(entry);
-  EXPECT_FALSE(status.ok());
-  EXPECT_EQ(absl::StatusCode::kInvalidArgument, status.code());
-}
-
-
-// TODO(przydatek): add more tests.
-
-}  // namespace
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/core/json_keyset_reader_test.cc b/cc/core/json_keyset_reader_test.cc
index dfd5048..5a3702a 100644
--- a/cc/core/json_keyset_reader_test.cc
+++ b/cc/core/json_keyset_reader_test.cc
@@ -27,7 +27,6 @@
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/substitute.h"
-#include "tink/util/protobuf_helper.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 #include "proto/aes_eax.pb.h"
@@ -312,11 +311,11 @@
   EXPECT_THAT(keyset->primary_key_id(), Eq(4294967275));
 }
 
-TEST_F(JsonKeysetReaderTest, ReadNegativeKeyId) {
+TEST_F(JsonKeysetReaderTest, RejectsNegativeKeyIds) {
   std::string json_serialization =
       absl::Substitute(R"(
       {
-         "primaryKeyId": -21,
+         "primaryKeyId": 711,
          "key":[
             {
                "keyData":{
@@ -349,6 +348,44 @@
   EXPECT_THAT(read_result, Not(IsOk()));
 }
 
+TEST_F(JsonKeysetReaderTest, RejectsKeyIdLargerThanUint32) {
+  // 4294967296 = 2^32, which is too large for uint32.
+  std::string json_serialization =
+      absl::Substitute(R"(
+      {
+         "primaryKeyId": 711,
+         "key":[
+            {
+               "keyData":{
+                  "typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey",
+                  "keyMaterialType":"SYMMETRIC",
+                  "value": "$0"
+               },
+               "outputPrefixType":"TINK",
+               "keyId": 4294967296,
+               "status":"ENABLED"
+            },
+            {
+               "keyData":{
+                  "typeUrl":"type.googleapis.com/google.crypto.tink.AesEaxKey",
+                  "keyMaterialType":"SYMMETRIC",
+                  "value":"$1"
+               },
+               "outputPrefixType":"RAW",
+               "keyId":711,
+               "status":"ENABLED"
+            }
+         ]
+      })",
+                       absl::Base64Escape(gcm_key_.SerializeAsString()),
+                       absl::Base64Escape(eax_key_.SerializeAsString()));
+  auto reader_result = JsonKeysetReader::New(json_serialization);
+  ASSERT_THAT(reader_result, IsOk());
+  auto reader = std::move(reader_result.value());
+  auto read_result = reader->Read();
+  EXPECT_THAT(read_result, Not(IsOk()));
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/core/key_manager_impl.h b/cc/core/key_manager_impl.h
index ae0de69..c47c951 100644
--- a/cc/core/key_manager_impl.h
+++ b/cc/core/key_manager_impl.h
@@ -17,6 +17,7 @@
 #define TINK_CORE_KEY_MANAGER_IMPL_H_
 
 #include <functional>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/core/key_type_manager.h b/cc/core/key_type_manager.h
index a2a8c6f..0ca2836 100644
--- a/cc/core/key_type_manager.h
+++ b/cc/core/key_type_manager.h
@@ -42,7 +42,7 @@
 template <typename KeyProto, typename KeyFormatProto>
 class InternalKeyFactory {
  public:
-  virtual ~InternalKeyFactory() {}
+  virtual ~InternalKeyFactory() = default;
 
   // Validates a key format proto.  KeyFormatProtos
   // on which this function returns a non-ok status will not be passed to
@@ -69,7 +69,7 @@
 template <typename KeyProto>
 class InternalKeyFactory<KeyProto, void> {
  public:
-  virtual ~InternalKeyFactory() {}
+  virtual ~InternalKeyFactory() = default;
 };
 
 }  // namespace internal
@@ -112,7 +112,7 @@
   template <typename Primitive>
   class PrimitiveFactory {
    public:
-    virtual ~PrimitiveFactory() {}
+    virtual ~PrimitiveFactory() = default;
     virtual crypto::tink::util::StatusOr<std::unique_ptr<Primitive>> Create(
         const KeyProto& key) const = 0;
   };
diff --git a/cc/core/key_type_manager_test.cc b/cc/core/key_type_manager_test.cc
index 921aea5..f5f8dfb 100644
--- a/cc/core/key_type_manager_test.cc
+++ b/cc/core/key_type_manager_test.cc
@@ -38,7 +38,6 @@
 
 namespace {
 
-using ::crypto::tink::test::StatusIs;
 using ::google::crypto::tink::AesGcmKey;
 using ::google::crypto::tink::AesGcmKeyFormat;
 using ::testing::Eq;
diff --git a/cc/core/keyset_handle.cc b/cc/core/keyset_handle.cc
index 3b75cfb..eded473 100644
--- a/cc/core/keyset_handle.cc
+++ b/cc/core/keyset_handle.cc
@@ -12,19 +12,32 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //
-///////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
 #include "tink/keyset_handle.h"
 
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/container/flat_hash_map.h"
+#include "absl/log/check.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "tink/aead.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/key_gen_configuration_impl.h"
 #include "tink/internal/key_info.h"
+#include "tink/internal/key_status_util.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/util.h"
+#include "tink/key_gen_configuration.h"
+#include "tink/key_status.h"
 #include "tink/keyset_reader.h"
 #include "tink/keyset_writer.h"
 #include "tink/registry.h"
@@ -36,7 +49,9 @@
 using google::crypto::tink::KeyData;
 using google::crypto::tink::Keyset;
 using google::crypto::tink::KeysetInfo;
+using google::crypto::tink::KeyStatusType;
 using google::crypto::tink::KeyTemplate;
+using google::crypto::tink::OutputPrefixType;
 
 namespace crypto {
 namespace tink {
@@ -83,8 +98,145 @@
   return util::OkStatus();
 }
 
+util::StatusOr<internal::ProtoKeySerialization> ToProtoKeySerialization(
+    Keyset::Key key) {
+  absl::optional<int> id_requirement = absl::nullopt;
+  if (key.output_prefix_type() != OutputPrefixType::RAW) {
+    id_requirement = key.key_id();
+  }
+
+  return internal::ProtoKeySerialization::Create(
+      key.key_data().type_url(),
+      RestrictedData(key.key_data().value(), InsecureSecretKeyAccess::Get()),
+      key.key_data().key_material_type(), key.output_prefix_type(),
+      id_requirement);
+}
+
 }  // anonymous namespace
 
+util::Status KeysetHandle::ValidateAt(int index) const {
+  const Keyset::Key& proto_key = get_keyset().key(index);
+  OutputPrefixType output_prefix_type = proto_key.output_prefix_type();
+  absl::optional<int> id_requirement = absl::nullopt;
+  if (output_prefix_type != OutputPrefixType::RAW) {
+    id_requirement = proto_key.key_id();
+  }
+
+  if (!internal::IsPrintableAscii(proto_key.key_data().type_url())) {
+    return util::Status(absl::StatusCode::kFailedPrecondition,
+                        "Non-printable ASCII character in type URL.");
+  }
+
+  util::StatusOr<KeyStatus> key_status =
+      internal::FromKeyStatusType(proto_key.status());
+  if (!key_status.ok()) return key_status.status();
+
+  return util::OkStatus();
+}
+
+util::Status KeysetHandle::Validate() const {
+  int num_primary = 0;
+  const Keyset& keyset = get_keyset();
+
+  for (int i = 0; i < size(); ++i) {
+    util::Status status = ValidateAt(i);
+    if (!status.ok()) return status;
+
+    Keyset::Key proto_key = keyset.key(i);
+    if (proto_key.key_id() == keyset.primary_key_id()) {
+      ++num_primary;
+      if (proto_key.status() != KeyStatusType::ENABLED) {
+        return util::Status(absl::StatusCode::kFailedPrecondition,
+                            "Keyset has primary that is not enabled");
+      }
+    }
+  }
+
+  if (num_primary < 1) {
+    return util::Status(absl::StatusCode::kFailedPrecondition,
+                        "Keyset has no primary");
+  }
+  if (num_primary > 1) {
+    return util::Status(absl::StatusCode::kFailedPrecondition,
+                        "Keyset has more than one primary");
+  }
+
+  return util::OkStatus();
+}
+
+KeysetHandle::Entry KeysetHandle::GetPrimary() const {
+  util::Status validation = Validate();
+  CHECK_OK(validation);
+
+  const Keyset& keyset = get_keyset();
+  for (int i = 0; i < keyset.key_size(); ++i) {
+    if (keyset.key(i).key_id() == keyset.primary_key_id()) {
+      return (*this)[i];
+    }
+  }
+
+  // Since keyset handle was validated, it should have a valid primary key.
+  internal::LogFatal("Keyset handle should have a valid primary key.");
+}
+
+KeysetHandle::Entry KeysetHandle::operator[](int index) const {
+  CHECK(index >= 0 && index < size())
+      << "Invalid index " << index << " for keyset of size " << size();
+
+  if (!entries_.empty() && entries_.size() > index) {
+    return *entries_[index];
+  }
+  // Since `entries_` has not been populated, the entry must be created on
+  // demand from the key proto entry at `index` in `keyset_`. This special
+  // case will no longer be necessary after `keyset_` has been removed from the
+  // `KeysetHandle` class.
+  //
+  // TODO(b/277792846): Remove after transition to rely solely on
+  // `KeysetHandle::Entry`.
+  return CreateEntryAt(index);
+}
+
+KeysetHandle::Entry KeysetHandle::CreateEntryAt(int index) const {
+  CHECK(index >= 0 && index < size())
+      << "Invalid index " << index << " for keyset of size " << size();
+
+  util::Status validation = ValidateAt(index);
+  CHECK_OK(validation);
+
+  Keyset keyset = get_keyset();
+  util::StatusOr<Entry> entry =
+      CreateEntry(keyset.key(index), keyset.primary_key_id());
+  // Status should be OK since this keyset handle has been validated.
+  CHECK_OK(entry.status());
+  return *entry;
+}
+
+util::StatusOr<KeysetHandle::Entry> KeysetHandle::CreateEntry(
+    const Keyset::Key& proto_key, uint32_t primary_key_id) {
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      ToProtoKeySerialization(proto_key);
+  if (!serialization.ok()) {
+    return serialization.status();
+  }
+
+  util::StatusOr<std::shared_ptr<const Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .ParseKeyWithLegacyFallback(*serialization,
+                                      InsecureSecretKeyAccess::Get());
+  if (!key.ok()) {
+    return key.status();
+  }
+
+  util::StatusOr<KeyStatus> key_status =
+      internal::FromKeyStatusType(proto_key.status());
+  if (!key_status.ok()) {
+    return key_status.status();
+  }
+
+  return Entry(*std::move(key), *key_status, proto_key.key_id(),
+               proto_key.key_id() == primary_key_id);
+}
+
 util::StatusOr<std::unique_ptr<KeysetHandle>> KeysetHandle::Read(
     std::unique_ptr<KeysetReader> reader, const Aead& master_key_aead,
     const absl::flat_hash_map<std::string, std::string>&
@@ -114,8 +266,17 @@
                      "Error decrypting encrypted keyset: %s",
                      keyset_result.status().message());
   }
-  return absl::WrapUnique(
-      new KeysetHandle(*std::move(keyset_result), monitoring_annotations));
+  util::StatusOr<std::vector<std::shared_ptr<const Entry>>> entries =
+      GetEntriesFromKeyset(**keyset_result);
+  if (!entries.ok()) {
+    return entries.status();
+  }
+  if (entries->size() != (*keyset_result)->key_size()) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Error converting keyset proto into key entries.");
+  }
+  return absl::WrapUnique(new KeysetHandle(*std::move(keyset_result), *entries,
+                                           monitoring_annotations));
 }
 
 util::StatusOr<std::unique_ptr<KeysetHandle>> KeysetHandle::ReadNoSecret(
@@ -131,8 +292,17 @@
   if (!validation.ok()) {
     return validation;
   }
+  util::StatusOr<std::vector<std::shared_ptr<const Entry>>> entries =
+      GetEntriesFromKeyset(keyset);
+  if (!entries.ok()) {
+    return entries.status();
+  }
+  if (entries->size() != keyset.key_size()) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Error converting keyset proto into key entries.");
+  }
   return absl::WrapUnique(
-      new KeysetHandle(std::move(keyset), monitoring_annotations));
+      new KeysetHandle(std::move(keyset), *entries, monitoring_annotations));
 }
 
 util::Status KeysetHandle::Write(KeysetWriter* writer,
@@ -169,17 +339,30 @@
 }
 
 util::StatusOr<std::unique_ptr<KeysetHandle>> KeysetHandle::GenerateNew(
-    const KeyTemplate& key_template,
+    const KeyTemplate& key_template, const KeyGenConfiguration& config,
     const absl::flat_hash_map<std::string, std::string>&
         monitoring_annotations) {
-  Keyset keyset;
+  auto handle =
+      absl::WrapUnique(new KeysetHandle(Keyset(), monitoring_annotations));
   util::StatusOr<uint32_t> const result =
-      AddToKeyset(key_template, /*as_primary=*/true, &keyset);
+      handle->AddKey(key_template, /*as_primary=*/true, config);
   if (!result.ok()) {
     return result.status();
   }
-  return absl::WrapUnique(
-      new KeysetHandle(std::move(keyset), monitoring_annotations));
+  return std::move(handle);
+}
+
+util::StatusOr<std::unique_ptr<KeysetHandle>> KeysetHandle::GenerateNew(
+    const KeyTemplate& key_template,
+    const absl::flat_hash_map<std::string, std::string>&
+        monitoring_annotations) {
+  KeyGenConfiguration config;
+  util::Status status =
+      internal::KeyGenConfigurationImpl::SetGlobalRegistryMode(config);
+  if (!status.ok()) {
+    return status;
+  }
+  return GenerateNew(key_template, config, monitoring_annotations);
 }
 
 util::StatusOr<std::unique_ptr<Keyset::Key>> ExtractPublicKey(
@@ -206,44 +389,98 @@
     public_keyset->add_key()->Swap(public_key_result.value().get());
   }
   public_keyset->set_primary_key_id(get_keyset().primary_key_id());
+  util::StatusOr<std::vector<std::shared_ptr<const Entry>>> entries =
+      GetEntriesFromKeyset(*public_keyset);
+  if (!entries.ok()) {
+    return entries.status();
+  }
+  if (entries->size() != public_keyset->key_size()) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Error converting keyset proto into key entries.");
+  }
   std::unique_ptr<KeysetHandle> handle(
-      new KeysetHandle(std::move(public_keyset)));
+      new KeysetHandle(std::move(public_keyset), *entries));
   return std::move(handle);
 }
 
 crypto::tink::util::StatusOr<uint32_t> KeysetHandle::AddToKeyset(
     const google::crypto::tink::KeyTemplate& key_template, bool as_primary,
-    Keyset* keyset) {
+    const KeyGenConfiguration& config, Keyset* keyset) {
   if (key_template.output_prefix_type() ==
       google::crypto::tink::OutputPrefixType::UNKNOWN_PREFIX) {
     return util::Status(absl::StatusCode::kInvalidArgument,
                         "key template has unknown prefix");
   }
-  auto key_data_result = Registry::NewKeyData(key_template);
-  if (!key_data_result.ok()) return key_data_result.status();
-  auto key_data = std::move(key_data_result.value());
+
+  // Generate new key data.
+  util::StatusOr<std::unique_ptr<KeyData>> key_data;
+  if (internal::KeyGenConfigurationImpl::GetGlobalRegistryMode(config)) {
+    key_data = Registry::NewKeyData(key_template);
+  } else {
+    util::StatusOr<const internal::KeyTypeInfoStore*> key_type_info_store =
+        internal::KeyGenConfigurationImpl::GetKeyTypeInfoStore(config);
+    if (!key_type_info_store.ok()) {
+      return key_type_info_store.status();
+    }
+    util::StatusOr<const internal::KeyTypeInfoStore::Info*> key_type_info =
+        (*key_type_info_store)->Get(key_template.type_url());
+    if (!key_type_info.ok()) {
+      return key_type_info.status();
+    }
+    key_data = (*key_type_info)->key_factory().NewKeyData(key_template.value());
+  }
+  if (!key_data.ok()) {
+    return key_data.status();
+  }
+
+  // Add and fill in new key in `keyset`.
   Keyset::Key* key = keyset->add_key();
-  uint32_t key_id = GenerateUnusedKeyId(*keyset);
-  *(key->mutable_key_data()) = *key_data;
-  key->set_status(google::crypto::tink::KeyStatusType::ENABLED);
-  key->set_key_id(key_id);
+  *(key->mutable_key_data()) = *std::move(key_data).value();
+  key->set_status(KeyStatusType::ENABLED);
   key->set_output_prefix_type(key_template.output_prefix_type());
+
+  uint32_t key_id = GenerateUnusedKeyId(*keyset);
+  key->set_key_id(key_id);
   if (as_primary) {
     keyset->set_primary_key_id(key_id);
   }
   return key_id;
 }
 
+crypto::tink::util::StatusOr<uint32_t> KeysetHandle::AddKey(
+    const google::crypto::tink::KeyTemplate& key_template, bool as_primary,
+    const KeyGenConfiguration& config) {
+  util::StatusOr<uint32_t> id =
+      AddToKeyset(key_template, as_primary, config, &keyset_);
+  if (!id.ok()) {
+    return id.status();
+  }
+  util::StatusOr<const Entry> entry = CreateEntry(
+      keyset_.key(keyset_.key_size() - 1), keyset_.primary_key_id());
+  if (!entry.ok()) {
+    return entry.status();
+  }
+  entries_.push_back(std::make_shared<const Entry>(*entry));
+  return *id;
+}
+
 KeysetInfo KeysetHandle::GetKeysetInfo() const {
   return KeysetInfoFromKeyset(get_keyset());
 }
 
-KeysetHandle::KeysetHandle(Keyset keyset) : keyset_(std::move(keyset)) {}
-
-KeysetHandle::KeysetHandle(std::unique_ptr<Keyset> keyset)
-    : keyset_(std::move(*keyset)) {}
-
-const Keyset& KeysetHandle::get_keyset() const { return keyset_; }
+util::StatusOr<std::vector<std::shared_ptr<const KeysetHandle::Entry>>>
+KeysetHandle::GetEntriesFromKeyset(const Keyset& keyset) {
+  std::vector<std::shared_ptr<const Entry>> entries;
+  for (const Keyset::Key& key : keyset.key()) {
+    util::StatusOr<const Entry> entry =
+        CreateEntry(key, keyset.primary_key_id());
+    if (!entry.ok()) {
+      return entry.status();
+    }
+    entries.push_back(std::make_shared<const Entry>(*entry));
+  }
+  return entries;
+}
 
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/core/keyset_handle_builder.cc b/cc/core/keyset_handle_builder.cc
new file mode 100644
index 0000000..223321b
--- /dev/null
+++ b/cc/core/keyset_handle_builder.cc
@@ -0,0 +1,198 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyset_handle_builder.h"
+
+#include <iostream>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/log/check.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/key_status.h"
+#include "tink/keyset_handle.h"
+#include "tink/subtle/random.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::Keyset;
+
+void SetBuilderEntryAttributes(KeyStatus status, bool is_primary,
+                               absl::optional<int> id,
+                               KeysetHandleBuilder::Entry* entry) {
+  entry->SetStatus(status);
+  if (is_primary) {
+    entry->SetPrimary();
+  } else {
+    entry->UnsetPrimary();
+  }
+  if (id.has_value()) {
+    entry->SetFixedId(*id);
+  } else {
+    entry->SetRandomId();
+  }
+}
+
+}  // namespace
+
+KeysetHandleBuilder::KeysetHandleBuilder(const KeysetHandle& handle) {
+  for (int i = 0; i < handle.size(); ++i) {
+    KeysetHandle::Entry entry = handle[i];
+    KeysetHandleBuilder::Entry builder_entry =
+        KeysetHandleBuilder::Entry::CreateFromKey(
+            std::move(entry.key_), entry.GetStatus(), entry.IsPrimary());
+    AddEntry(std::move(builder_entry));
+  }
+}
+
+KeysetHandleBuilder::Entry KeysetHandleBuilder::Entry::CreateFromKey(
+    std::shared_ptr<const Key> key, KeyStatus status, bool is_primary) {
+  absl::optional<int> id_requirement = key->GetIdRequirement();
+  auto imported_entry = absl::make_unique<internal::KeyEntry>(std::move(key));
+  KeysetHandleBuilder::Entry entry(std::move(imported_entry));
+  SetBuilderEntryAttributes(status, is_primary, id_requirement, &entry);
+  return entry;
+}
+
+KeysetHandleBuilder::Entry KeysetHandleBuilder::Entry::CreateFromParams(
+    std::shared_ptr<const Parameters> parameters, KeyStatus status,
+    bool is_primary, absl::optional<int> id) {
+  auto generated_entry =
+      absl::make_unique<internal::ParametersEntry>(std::move(parameters));
+  KeysetHandleBuilder::Entry entry(std::move(generated_entry));
+  SetBuilderEntryAttributes(status, is_primary, id, &entry);
+  return entry;
+}
+
+util::StatusOr<int> KeysetHandleBuilder::NextIdFromKeyIdStrategy(
+    internal::KeyIdStrategy strategy, const std::set<int>& ids_so_far) {
+  if (strategy.strategy == internal::KeyIdStrategyEnum::kFixedId) {
+    if (!strategy.id_requirement.has_value()) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Missing fixed id with fixed id strategy.");
+    }
+    return *strategy.id_requirement;
+  }
+  if (strategy.strategy == internal::KeyIdStrategyEnum::kRandomId) {
+    int id = 0;
+    while (id == 0 || ids_so_far.find(id) != ids_so_far.end()) {
+      id = subtle::Random::GetRandomUInt32();
+    }
+    return id;
+  }
+  return util::Status(absl::StatusCode::kInvalidArgument,
+                      "Invalid key id strategy.");
+}
+
+void KeysetHandleBuilder::ClearPrimary() {
+  for (KeysetHandleBuilder::Entry& entry : entries_) {
+    entry.UnsetPrimary();
+  }
+}
+
+KeysetHandleBuilder& KeysetHandleBuilder::AddEntry(
+    KeysetHandleBuilder::Entry entry) {
+  CHECK(!entry.added_to_builder_)
+      << "Keyset handle builder entry already added to a builder.";
+  entry.added_to_builder_ = true;
+  if (entry.IsPrimary()) {
+    ClearPrimary();
+  }
+  entries_.push_back(std::move(entry));
+  return *this;
+}
+
+KeysetHandleBuilder& KeysetHandleBuilder::RemoveEntry(int index) {
+  CHECK(index >= 0 && index < entries_.size())
+      << "Keyset handle builder entry removal index out of range.";
+  entries_.erase(entries_.begin() + index);
+  return *this;
+}
+
+util::Status KeysetHandleBuilder::CheckIdAssignments() {
+  // We only want random id entries after fixed id entries. Otherwise, we might
+  // randomly pick an id that is later specified as a fixed id.
+  for (int i = 0; i < entries_.size() - 1; ++i) {
+    if (entries_[i].HasRandomId() && !entries_[i + 1].HasRandomId()) {
+      return util::Status(absl::StatusCode::kFailedPrecondition,
+                          "Entries with random ids may only be followed "
+                          "by other entries with random ids.");
+    }
+  }
+  return util::OkStatus();
+}
+
+util::StatusOr<KeysetHandle> KeysetHandleBuilder::Build() {
+  if (build_called_) {
+      return util::Status(
+          absl::StatusCode::kFailedPrecondition,
+          "KeysetHandleBuilder::Build may only be called once");
+  }
+  build_called_ = true;
+  Keyset keyset;
+  absl::optional<int> primary_id = absl::nullopt;
+
+  util::Status assigned_ids_status = CheckIdAssignments();
+  if (!assigned_ids_status.ok()) return assigned_ids_status;
+
+  std::set<int> ids_so_far;
+  for (KeysetHandleBuilder::Entry& entry : entries_) {
+    util::StatusOr<int> id =
+        NextIdFromKeyIdStrategy(entry.GetKeyIdStrategy(), ids_so_far);
+    if (!id.ok()) return id.status();
+
+    if (ids_so_far.find(*id) != ids_so_far.end()) {
+      return util::Status(
+          absl::StatusCode::kAlreadyExists,
+          absl::StrFormat("Next id %d is already used in the keyset.", *id));
+    }
+    ids_so_far.insert(*id);
+
+    util::StatusOr<Keyset::Key> key = entry.CreateKeysetKey(*id);
+    if (!key.ok()) return key.status();
+
+    *keyset.add_key() = *key;
+    if (entry.IsPrimary()) {
+      if (primary_id.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInternal,
+            "Primary is already set in this keyset (should never happen since "
+            "primary is cleared when a new primary is added).");
+      }
+      primary_id = *id;
+    }
+  }
+
+  if (!primary_id.has_value()) {
+    return util::Status(absl::StatusCode::kFailedPrecondition,
+                        "No primary set in this keyset.");
+  }
+  keyset.set_primary_key_id(*primary_id);
+  util::StatusOr<std::vector<std::shared_ptr<const KeysetHandle::Entry>>>
+      entries = KeysetHandle::GetEntriesFromKeyset(keyset);
+  return KeysetHandle(keyset, *std::move(entries));
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/core/keyset_handle_builder_test.cc b/cc/core/keyset_handle_builder_test.cc
new file mode 100644
index 0000000..3d29593
--- /dev/null
+++ b/cc/core/keyset_handle_builder_test.cc
@@ -0,0 +1,838 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyset_handle_builder.h"
+
+#include <memory>
+#include <ostream>
+#include <set>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/config/tink_config.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/legacy_proto_key.h"
+#include "tink/internal/legacy_proto_parameters.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/key_status.h"
+#include "tink/mac/aes_cmac_key.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/mac/mac_key_templates.h"
+#include "tink/partial_key_access.h"
+#include "tink/subtle/random.h"
+#include "tink/util/status.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/aes_cmac.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::AddTinkKey;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesCmacParams;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::SizeIs;
+using ::testing::Test;
+
+class KeysetHandleBuilderTest : public Test {
+ protected:
+  void SetUp() override {
+    util::Status status = TinkConfig::Register();
+    ASSERT_TRUE(status.ok()) << status;
+  }
+};
+
+using KeysetHandleBuilderDeathTest = KeysetHandleBuilderTest;
+
+util::StatusOr<internal::LegacyProtoParameters> CreateLegacyProtoParameters(
+    KeyTemplate key_template) {
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(key_template);
+  if (!serialization.ok()) return serialization.status();
+
+  return internal::LegacyProtoParameters(*serialization);
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildWithSingleKey) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+          /*id=*/123);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+  EXPECT_THAT(*handle, SizeIs(1));
+
+  EXPECT_THAT((*handle)[0].GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT((*handle)[0].GetId(), Eq(123));
+  EXPECT_THAT((*handle)[0].IsPrimary(), IsTrue());
+  EXPECT_THAT((*handle)[0].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildWithMultipleKeys) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry0 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kDestroyed,
+          /*is_primary=*/false,
+          /*id=*/123);
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+          /*id=*/456);
+
+  KeysetHandleBuilder::Entry entry2 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kDisabled,
+          /*is_primary=*/false, /*id=*/789);
+
+  util::StatusOr<KeysetHandle> handle = KeysetHandleBuilder()
+                                            .AddEntry(std::move(entry0))
+                                            .AddEntry(std::move(entry1))
+                                            .AddEntry(std::move(entry2))
+                                            .Build();
+  ASSERT_THAT(handle.status(), IsOk());
+  EXPECT_THAT(*handle, SizeIs(3));
+
+  EXPECT_THAT((*handle)[0].GetStatus(), Eq(KeyStatus::kDestroyed));
+  EXPECT_THAT((*handle)[0].GetId(), Eq(123));
+  EXPECT_THAT((*handle)[0].IsPrimary(), IsFalse());
+  EXPECT_THAT((*handle)[0].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+
+  EXPECT_THAT((*handle)[1].GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT((*handle)[1].GetId(), Eq(456));
+  EXPECT_THAT((*handle)[1].IsPrimary(), IsTrue());
+  EXPECT_THAT((*handle)[1].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+
+  EXPECT_THAT((*handle)[2].GetStatus(), Eq(KeyStatus::kDisabled));
+  EXPECT_THAT((*handle)[2].GetId(), Eq(789));
+  EXPECT_THAT((*handle)[2].IsPrimary(), IsFalse());
+  EXPECT_THAT((*handle)[2].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildCopy) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry0 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kDestroyed,
+          /*is_primary=*/false,
+          /*id=*/123);
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+          /*id=*/456);
+
+  KeysetHandleBuilder::Entry entry2 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kDisabled,
+          /*is_primary=*/false, /*id=*/789);
+
+  util::StatusOr<KeysetHandle> handle = KeysetHandleBuilder()
+                                            .AddEntry(std::move(entry0))
+                                            .AddEntry(std::move(entry1))
+                                            .AddEntry(std::move(entry2))
+                                            .Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  util::StatusOr<KeysetHandle> copy = KeysetHandleBuilder(*handle).Build();
+  ASSERT_THAT(copy.status(), IsOk());
+  EXPECT_THAT(copy->size(), Eq(3));
+
+  EXPECT_THAT((*copy)[0].GetStatus(), Eq(KeyStatus::kDestroyed));
+  EXPECT_THAT((*copy)[0].GetId(), Eq(123));
+  EXPECT_THAT((*copy)[0].IsPrimary(), IsFalse());
+  EXPECT_THAT((*copy)[0].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+
+  EXPECT_THAT((*copy)[1].GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT((*copy)[1].GetId(), Eq(456));
+  EXPECT_THAT((*copy)[1].IsPrimary(), IsTrue());
+  EXPECT_THAT((*copy)[1].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+
+  EXPECT_THAT((*copy)[2].GetStatus(), Eq(KeyStatus::kDisabled));
+  EXPECT_THAT((*copy)[2].GetId(), Eq(789));
+  EXPECT_THAT((*copy)[2].IsPrimary(), IsFalse());
+  EXPECT_THAT((*copy)[2].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+}
+
+TEST_F(KeysetHandleBuilderTest, IsPrimary) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(*parameters,
+                                                           KeyStatus::kEnabled,
+                                                           /*is_primary=*/false,
+                                                           /*id=*/123);
+  EXPECT_THAT(entry.IsPrimary(), IsFalse());
+
+  entry.SetPrimary();
+  EXPECT_THAT(entry.IsPrimary(), IsTrue());
+}
+
+TEST_F(KeysetHandleBuilderTest, SetAndGetStatus) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false,
+          /*id=*/123);
+
+  entry.SetStatus(KeyStatus::kDisabled);
+  EXPECT_THAT(entry.GetStatus(), Eq(KeyStatus::kDisabled));
+  entry.SetStatus(KeyStatus::kEnabled);
+  EXPECT_THAT(entry.GetStatus(), Eq(KeyStatus::kEnabled));
+  entry.SetStatus(KeyStatus::kDestroyed);
+  EXPECT_THAT(entry.GetStatus(), Eq(KeyStatus::kDestroyed));
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildWithRandomId) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry primary =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true);
+
+  KeysetHandleBuilder builder;
+  builder.AddEntry(std::move(primary));
+
+  int num_non_primary_entries = 1 << 16;
+  for (int i = 0; i < num_non_primary_entries; ++i) {
+    KeysetHandleBuilder::Entry non_primary =
+        KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+            *parameters, KeyStatus::kEnabled, /*is_primary=*/false);
+    builder.AddEntry(std::move(non_primary));
+  }
+
+  util::StatusOr<KeysetHandle> handle = builder.Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  std::set<int> ids;
+  for (int i = 0; i < handle->size(); ++i) {
+    ids.insert((*handle)[i].GetId());
+  }
+  EXPECT_THAT(ids, SizeIs(num_non_primary_entries + 1));
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildWithRandomIdAfterFixedId) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry fixed =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/123);
+
+  KeysetHandleBuilder::Entry random =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false);
+
+  util::StatusOr<KeysetHandle> handle = KeysetHandleBuilder()
+                                            .AddEntry(std::move(fixed))
+                                            .AddEntry(std::move(random))
+                                            .Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  EXPECT_THAT(*handle, SizeIs(2));
+  EXPECT_THAT((*handle)[0].GetId(), Eq(123));
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildWithFixedIdAfterRandomIdFails) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry random =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false);
+
+  KeysetHandleBuilder::Entry fixed =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/123);
+
+  util::StatusOr<KeysetHandle> handle = KeysetHandleBuilder()
+                                            .AddEntry(std::move(random))
+                                            .AddEntry(std::move(fixed))
+                                            .Build();
+  ASSERT_THAT(handle.status(), StatusIs(absl::StatusCode::kFailedPrecondition));
+}
+
+TEST_F(KeysetHandleBuilderDeathTest, AddEntryToAnotherBuilderCrashes) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/123);
+
+  KeysetHandleBuilder builder0;
+  builder0.AddEntry(std::move(entry));
+  KeysetHandleBuilder builder1;
+  EXPECT_DEATH_IF_SUPPORTED(
+      builder1.AddEntry(std::move(builder0[0])),
+      "Keyset handle builder entry already added to a builder.");
+}
+
+TEST_F(KeysetHandleBuilderDeathTest, ReAddEntryToSameBuilderCrashes) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/123);
+
+  KeysetHandleBuilder builder;
+  builder.AddEntry(std::move(entry));
+  EXPECT_DEATH_IF_SUPPORTED(
+      builder.AddEntry(std::move(builder[0])),
+      "Keyset handle builder entry already added to a builder.");
+}
+
+TEST_F(KeysetHandleBuilderDeathTest,
+       AddDereferencedEntryToAnotherBuilderCrashes) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/123);
+
+  KeysetHandleBuilder builder0;
+  builder0.AddEntry(std::move(entry));
+  KeysetHandleBuilder builder1;
+  EXPECT_DEATH_IF_SUPPORTED(
+      builder1.AddEntry(std::move(*&(builder0[0]))),
+      "Keyset handle builder entry already added to a builder.");
+}
+
+TEST_F(KeysetHandleBuilderTest, RemoveEntry) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry0 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false, /*id=*/123);
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/456);
+
+  util::StatusOr<KeysetHandle> handle0 = KeysetHandleBuilder()
+                                             .AddEntry(std::move(entry0))
+                                             .AddEntry(std::move(entry1))
+                                             .Build();
+  ASSERT_THAT(handle0.status(), IsOk());
+  ASSERT_THAT(*handle0, SizeIs(2));
+
+  util::StatusOr<KeysetHandle> handle1 =
+      KeysetHandleBuilder(*handle0).RemoveEntry(0).Build();
+  ASSERT_THAT(handle1.status(), IsOk());
+  ASSERT_THAT(*handle1, SizeIs(1));
+
+  EXPECT_THAT((*handle1)[0].GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT((*handle1)[0].GetId(), Eq(456));
+  EXPECT_THAT((*handle1)[0].IsPrimary(), IsTrue());
+  EXPECT_THAT((*handle1)[0].GetKey()->GetParameters().HasIdRequirement(),
+              IsTrue());
+}
+
+TEST_F(KeysetHandleBuilderDeathTest, RemoveOutofRangeIndexEntryCrashes) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true, /*id=*/123);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  EXPECT_DEATH_IF_SUPPORTED(
+      KeysetHandleBuilder(*handle).RemoveEntry(1),
+      "Keyset handle builder entry removal index out of range.");
+}
+
+TEST_F(KeysetHandleBuilderTest, Size) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry0 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kDestroyed,
+          /*is_primary=*/false,
+          /*id=*/123);
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+          /*id=*/456);
+
+  KeysetHandleBuilder builder;
+  ASSERT_THAT(builder, SizeIs(0));
+  builder.AddEntry(std::move(entry0));
+  ASSERT_THAT(builder, SizeIs(1));
+  builder.AddEntry(std::move(entry1));
+  EXPECT_THAT(builder, SizeIs(2));
+}
+
+TEST_F(KeysetHandleBuilderTest, NoPrimaryFails) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry0 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false,
+          /*id=*/123);
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false,
+          /*id=*/456);
+
+  util::StatusOr<KeysetHandle> handle = KeysetHandleBuilder()
+                                            .AddEntry(std::move(entry0))
+                                            .AddEntry(std::move(entry1))
+                                            .Build();
+  ASSERT_THAT(handle.status(), StatusIs(absl::StatusCode::kFailedPrecondition));
+}
+
+TEST_F(KeysetHandleBuilderTest, RemovePrimaryFails) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry0 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+          /*id=*/123);
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/false,
+          /*id=*/456);
+
+  util::StatusOr<KeysetHandle> handle = KeysetHandleBuilder()
+                                            .AddEntry(std::move(entry0))
+                                            .AddEntry(std::move(entry1))
+                                            .RemoveEntry(0)
+                                            .Build();
+  ASSERT_THAT(handle.status(), StatusIs(absl::StatusCode::kFailedPrecondition));
+}
+
+TEST_F(KeysetHandleBuilderTest, AddPrimaryClearsOtherPrimary) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder builder;
+  builder.AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+      *parameters, KeyStatus::kEnabled,
+      /*is_primary=*/true,
+      /*id=*/123));
+  builder.AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+      *parameters, KeyStatus::kEnabled,
+      /*is_primary=*/true,
+      /*id=*/456));
+
+  ASSERT_THAT(builder[0].IsPrimary(), IsFalse());
+  ASSERT_THAT(builder[1].IsPrimary(), IsTrue());
+}
+
+TEST_F(KeysetHandleBuilderTest, NoIdStrategySucceeds) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle, IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, DuplicateId) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled,
+              /*is_primary=*/true,
+              /*id=*/123))
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled,
+              /*is_primary=*/false,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle.status(), StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromParams) {
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromParams(
+          absl::make_unique<AesCmacParameters>(std::move(*params)),
+          KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromLegacyKey) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::DISABLED,
+             KeyData::SYMMETRIC, &keyset);
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          key.key_data().type_url(),
+          RestrictedData(key.SerializeAsString(),
+                         InsecureSecretKeyAccess::Get()),
+          key.key_data().key_material_type(), key.output_prefix_type(),
+          key.key_id());
+
+  util::StatusOr<internal::LegacyProtoKey> proto_key =
+      internal::LegacyProtoKey::Create(*serialization,
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(proto_key.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry = KeysetHandleBuilder::Entry::CreateFromKey(
+      absl::make_unique<internal::LegacyProtoKey>(std::move(*proto_key)),
+      KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromKey) {
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(32);
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret, /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry = KeysetHandleBuilder::Entry::CreateFromKey(
+      absl::make_unique<AesCmacKey>(std::move(*key)), KeyStatus::kEnabled,
+      /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromCopyableKey) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::DISABLED,
+             KeyData::SYMMETRIC, &keyset);
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          key.key_data().type_url(),
+          RestrictedData(key.SerializeAsString(),
+                         InsecureSecretKeyAccess::Get()),
+          key.key_data().key_material_type(), key.output_prefix_type(),
+          key.key_id());
+
+  util::StatusOr<internal::LegacyProtoKey> proto_key =
+      internal::LegacyProtoKey::Create(*serialization,
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(proto_key.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableKey(
+          *proto_key, KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromParameters) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromParams(
+          absl::make_unique<internal::LegacyProtoParameters>(*parameters),
+          KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromCopyableParameters) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, UsePrimitiveFromLegacyProtoParams) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Mac>> mac = handle->GetPrimitive<Mac>();
+  ASSERT_THAT(mac.status(), IsOk());
+  util::StatusOr<std::string> tag = (*mac)->ComputeMac("some input");
+  ASSERT_THAT(tag.status(), IsOk());
+  util::Status verified = (*mac)->VerifyMac(*tag, "some input");
+  EXPECT_THAT(verified, IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, UsePrimitiveFromParams) {
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromParams(
+          absl::make_unique<AesCmacParameters>(std::move(*params)),
+          KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Mac>> mac = handle->GetPrimitive<Mac>();
+  ASSERT_THAT(mac.status(), IsOk());
+  util::StatusOr<std::string> tag = (*mac)->ComputeMac("some input");
+  ASSERT_THAT(tag.status(), IsOk());
+  util::Status verified = (*mac)->VerifyMac(*tag, "some input");
+  EXPECT_THAT(verified, IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, UsePrimitiveFromLegacyProtoKey) {
+  AesCmacParams params;
+  params.set_tag_size(16);
+  google::crypto::tink::AesCmacKey key;
+  *key.mutable_params() = params;
+  key.set_version(0);
+  key.set_key_value(subtle::Random::GetRandomBytes(32));
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey",
+          RestrictedData(key.SerializeAsString(),
+                         InsecureSecretKeyAccess::Get()),
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/123);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<internal::LegacyProtoKey> proto_key =
+      internal::LegacyProtoKey::Create(*serialization,
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(proto_key.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableKey(
+          *proto_key, KeyStatus::kEnabled, /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Mac>> mac = handle->GetPrimitive<Mac>();
+  ASSERT_THAT(mac.status(), IsOk());
+  util::StatusOr<std::string> tag = (*mac)->ComputeMac("some input");
+  ASSERT_THAT(tag.status(), IsOk());
+  util::Status verified = (*mac)->VerifyMac(*tag, "some input");
+  EXPECT_THAT(verified, IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, UsePrimitiveFromKey) {
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(32);
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret, /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry = KeysetHandleBuilder::Entry::CreateFromKey(
+      absl::make_unique<AesCmacKey>(std::move(*key)), KeyStatus::kEnabled,
+      /*is_primary=*/true);
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder().AddEntry(std::move(entry)).Build();
+  ASSERT_THAT(handle.status(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Mac>> mac = handle->GetPrimitive<Mac>();
+  ASSERT_THAT(mac.status(), IsOk());
+  util::StatusOr<std::string> tag = (*mac)->ComputeMac("some input");
+  ASSERT_THAT(tag.status(), IsOk());
+  util::Status verified = (*mac)->VerifyMac(*tag, "some input");
+  EXPECT_THAT(verified, IsOk());
+}
+
+TEST_F(KeysetHandleBuilderTest, BuildTwiceFails) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters(MacKeyTemplates::AesCmac());
+  ASSERT_THAT(parameters.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry =
+      KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+          *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+          /*id=*/123);
+
+  KeysetHandleBuilder builder;
+  builder.AddEntry(std::move(entry));
+
+  EXPECT_THAT(builder.Build(), IsOk());
+  EXPECT_THAT(builder.Build().status(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+}
+
+TEST_F(KeysetHandleBuilderTest, UsePrimitivesFromSplitKeyset) {
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *params, KeyStatus::kEnabled, /*is_primary=*/false))
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *params, KeyStatus::kEnabled, /*is_primary=*/true))
+          .Build();
+  ASSERT_THAT(handle, IsOkAndHolds(SizeIs(2)));
+
+  util::StatusOr<KeysetHandle> handle0 =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromKey(
+              (*handle)[0].GetKey(), KeyStatus::kEnabled,
+              /*is_primary=*/true))
+          .Build();
+  ASSERT_THAT(handle0, IsOkAndHolds(SizeIs(1)));
+  ASSERT_THAT((*handle)[0].GetId(), Eq((*handle0)[0].GetId()));
+
+  util::StatusOr<KeysetHandle> handle1 =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromKey(
+              (*handle)[1].GetKey(), KeyStatus::kEnabled,
+              /*is_primary=*/true))
+          .Build();
+  ASSERT_THAT(handle1, IsOkAndHolds(SizeIs(1)));
+  ASSERT_THAT((*handle)[1].GetId(), Eq((*handle1)[0].GetId()));
+
+  util::StatusOr<std::unique_ptr<Mac>> mac0 = handle0->GetPrimitive<Mac>();
+  ASSERT_THAT(mac0.status(), IsOk());
+  util::StatusOr<std::string> tag0 = (*mac0)->ComputeMac("some input");
+  ASSERT_THAT(tag0.status(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Mac>> mac1 = handle1->GetPrimitive<Mac>();
+  ASSERT_THAT(mac1.status(), IsOk());
+  util::StatusOr<std::string> tag1 = (*mac1)->ComputeMac("some other input");
+  ASSERT_THAT(tag1.status(), IsOk());
+
+  // Use original keyset to verify tags computed from new keysets.
+  util::StatusOr<std::unique_ptr<Mac>> mac = handle->GetPrimitive<Mac>();
+  ASSERT_THAT(mac.status(), IsOk());
+  EXPECT_THAT((*mac)->VerifyMac(*tag0, "some input"), IsOk());
+  EXPECT_THAT((*mac)->VerifyMac(*tag1, "some other input"), IsOk());
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/core/keyset_handle_test.cc b/cc/core/keyset_handle_test.cc
index ae81673..539dc2c 100644
--- a/cc/core/keyset_handle_test.cc
+++ b/cc/core/keyset_handle_test.cc
@@ -33,105 +33,123 @@
 #include "tink/binary_keyset_reader.h"
 #include "tink/binary_keyset_writer.h"
 #include "tink/cleartext_keyset_handle.h"
+#include "tink/config/fips_140_2.h"
+#include "tink/config/internal/global_registry.h"
+#include "tink/config/key_gen_fips_140_2.h"
 #include "tink/config/tink_config.h"
 #include "tink/core/key_manager_impl.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/key_gen_configuration_impl.h"
 #include "tink/json_keyset_reader.h"
 #include "tink/json_keyset_writer.h"
+#include "tink/key_gen_configuration.h"
+#include "tink/key_status.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
 #include "tink/signature/ecdsa_sign_key_manager.h"
 #include "tink/signature/signature_key_templates.h"
-#include "tink/util/protobuf_helper.h"
 #include "tink/util/status.h"
 #include "tink/util/test_keyset_handle.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
+#include "proto/aes_gcm_siv.pb.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
 namespace tink {
 
-using crypto::tink::TestKeysetHandle;
-using crypto::tink::test::AddKeyData;
-using crypto::tink::test::AddLegacyKey;
-using crypto::tink::test::AddRawKey;
-using crypto::tink::test::AddTinkKey;
-using crypto::tink::test::DummyAead;
-using crypto::tink::test::IsOk;
-using crypto::tink::test::StatusIs;
-using google::crypto::tink::EcdsaKeyFormat;
-using google::crypto::tink::EncryptedKeyset;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::Keyset;
-using google::crypto::tink::KeyStatusType;
-using google::crypto::tink::KeyTemplate;
-using google::crypto::tink::OutputPrefixType;
+using ::crypto::tink::TestKeysetHandle;
+using ::crypto::tink::test::AddKeyData;
+using ::crypto::tink::test::AddLegacyKey;
+using ::crypto::tink::test::AddRawKey;
+using ::crypto::tink::test::AddTinkKey;
+using ::crypto::tink::test::DummyAead;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::AesGcmSivKey;
+using ::google::crypto::tink::EcdsaKeyFormat;
+using ::google::crypto::tink::EncryptedKeyset;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
 using ::testing::_;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
 using ::testing::Not;
+using ::testing::SizeIs;
 
 namespace {
 
 class KeysetHandleTest : public ::testing::Test {
  protected:
   void SetUp() override {
+    Registry::Reset();
     auto status = TinkConfig::Register();
     ASSERT_TRUE(status.ok()) << status;
+
+    internal::UnSetFipsRestricted();
   }
 };
 
-// Dummy key factory that is required to create a key manager.
-class DummyAeadKeyFactory : public KeyFactory {
+using KeysetHandleDeathTest = KeysetHandleTest;
+
+// Fake AEAD key type manager for testing.
+class FakeAeadKeyManager
+    : public KeyTypeManager<AesGcmKey, AesGcmKeyFormat, List<Aead>> {
  public:
-  explicit DummyAeadKeyFactory(absl::string_view key_type)
-      : key_type_(key_type) {}
+  class AeadFactory : public PrimitiveFactory<Aead> {
+   public:
+    explicit AeadFactory(absl::string_view key_type) : key_type_(key_type) {}
 
-  util::StatusOr<std::unique_ptr<portable_proto::MessageLite>> NewKey(
-      const portable_proto::MessageLite& key_format) const override {
-    return util::Status(absl::StatusCode::kUnimplemented, "Unimplemented");
-  }
+    util::StatusOr<std::unique_ptr<Aead>> Create(
+        const AesGcmKey& key) const override {
+      return {absl::make_unique<DummyAead>(key_type_)};
+    }
 
-  util::StatusOr<std::unique_ptr<portable_proto::MessageLite>> NewKey(
-      absl::string_view serialized_key_format) const override {
-    return util::Status(absl::StatusCode::kUnimplemented, "Unimplemented");
-  }
+   private:
+    const std::string key_type_;
+  };
 
-  util::StatusOr<std::unique_ptr<KeyData>> NewKeyData(
-      absl::string_view serialized_key_format) const override {
-    auto key_data = absl::make_unique<KeyData>();
-    key_data->set_type_url(key_type_);
-    std::string serialized_key_format_str(serialized_key_format);
-    key_data->set_value(serialized_key_format_str);
-    return std::move(key_data);
-  }
-
- private:
-  const std::string key_type_;
-};
-
-// Fake Aead key manager for testing.
-class FakeAeadKeyManager : public KeyManager<Aead> {
- public:
   explicit FakeAeadKeyManager(absl::string_view key_type)
-      : key_type_(key_type), key_factory_(key_type) {}
+      : KeyTypeManager(absl::make_unique<AeadFactory>(key_type)),
+        key_type_(key_type) {}
 
-  util::StatusOr<std::unique_ptr<Aead>> GetPrimitive(
-      const KeyData& key) const override {
-    return {absl::make_unique<DummyAead>(key_type_)};
-  }
-
-  util::StatusOr<std::unique_ptr<Aead>> GetPrimitive(
-      const portable_proto::MessageLite& key) const override {
-    return util::Status(absl::StatusCode::kUnknown,
-                        "DummyAeadKeyFactory cannot construct an aead");
+  google::crypto::tink::KeyData::KeyMaterialType key_material_type()
+      const override {
+    return google::crypto::tink::KeyData::SYMMETRIC;
   }
 
   uint32_t get_version() const override { return 0; }
+
   const std::string& get_key_type() const override { return key_type_; }
-  const KeyFactory& get_key_factory() const override { return key_factory_; }
+
+  crypto::tink::util::Status ValidateKey(const AesGcmKey& key) const override {
+    return util::OkStatus();
+  }
+
+  crypto::tink::util::Status ValidateKeyFormat(
+      const AesGcmKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  crypto::tink::util::StatusOr<AesGcmKey> CreateKey(
+      const AesGcmKeyFormat& key_format) const override {
+    return AesGcmKey();
+  }
+
+  crypto::tink::util::StatusOr<AesGcmKey> DeriveKey(
+      const AesGcmKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return AesGcmKey();
+  }
 
  private:
   const std::string key_type_;
-  const DummyAeadKeyFactory key_factory_;
 };
 
 class MockAeadPrimitiveWrapper : public PrimitiveWrapper<Aead, Aead> {
@@ -145,9 +163,9 @@
 Keyset GetTestKeyset() {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   return keyset;
@@ -157,9 +175,9 @@
 Keyset GetPublicTestKeyset() {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PUBLIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::REMOTE, &keyset);
   keyset.set_primary_key_id(42);
   return keyset;
@@ -168,9 +186,9 @@
 TEST_F(KeysetHandleTest, ReadEncryptedKeysetBinary) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -265,12 +283,12 @@
   Registry::Reset();
   ASSERT_THAT(Registry::RegisterPrimitiveWrapper(std::move(primitive_wrapper)),
               IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>("some key type"),
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<FakeAeadKeyManager>("some_key_type"),
                   /*new_key_allowed=*/true),
               IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>("some other key type"),
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<FakeAeadKeyManager>("some_other_key_type"),
                   /*new_key_allowed=*/true),
               IsOk());
 
@@ -283,9 +301,9 @@
 TEST_F(KeysetHandleTest, ReadEncryptedKeysetJson) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -366,9 +384,9 @@
   // Prepare a valid keyset handle
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   auto reader =
@@ -406,9 +424,9 @@
 TEST_F(KeysetHandleTest, ReadEncryptedKeysetWithAssociatedDataGoodKeyset) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -458,12 +476,12 @@
   Registry::Reset();
   ASSERT_THAT(Registry::RegisterPrimitiveWrapper(std::move(primitive_wrapper)),
               IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>("some key type"),
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<FakeAeadKeyManager>("some_key_type"),
                   /*new_key_allowed=*/true),
               IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>("some other key type"),
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<FakeAeadKeyManager>("some_other_key_type"),
                   /*new_key_allowed=*/true),
               IsOk());
 
@@ -476,9 +494,9 @@
 TEST_F(KeysetHandleTest, ReadEncryptedKeysetWithAssociatedDataWrongAad) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   DummyAead aead("dummy aead 42");
@@ -497,9 +515,9 @@
 TEST_F(KeysetHandleTest, ReadEncryptedKeysetWithAssociatedDataEmptyAad) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   DummyAead aead("dummy aead 42");
@@ -518,9 +536,9 @@
   // Prepare a valid keyset handle
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   auto reader =
@@ -556,55 +574,94 @@
   EXPECT_EQ(absl::StatusCode::kInvalidArgument, status.code());
 }
 
-TEST_F(KeysetHandleTest, GenerateNewKeysetHandle) {
-  const google::crypto::tink::KeyTemplate* key_templates[] = {
+TEST_F(KeysetHandleTest, GenerateNew) {
+  const google::crypto::tink::KeyTemplate* templates[] = {
       &AeadKeyTemplates::Aes128Gcm(),
       &AeadKeyTemplates::Aes256Gcm(),
       &AeadKeyTemplates::Aes128CtrHmacSha256(),
       &AeadKeyTemplates::Aes256CtrHmacSha256(),
   };
-  for (auto templ : key_templates) {
-    auto handle_result = KeysetHandle::GenerateNew(*templ);
-    EXPECT_TRUE(handle_result.ok())
-        << "Failed for template:\n " << templ->SerializeAsString()
-        << "\n with status: "<< handle_result.status();
+  KeyGenConfiguration config;
+  ASSERT_THAT(internal::KeyGenConfigurationImpl::SetGlobalRegistryMode(config),
+              IsOk());
+  for (auto templ : templates) {
+    EXPECT_THAT(KeysetHandle::GenerateNew(*templ).status(), IsOk());
+    EXPECT_THAT(KeysetHandle::GenerateNew(*templ, config).status(), IsOk());
   }
 }
 
+TEST_F(KeysetHandleTest, GenerateNewWithBespokeConfig) {
+  KeyGenConfiguration config;
+  EXPECT_THAT(
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), config).status(),
+      StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), config),
+              IsOk());
+  EXPECT_THAT(
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), config).status(),
+      IsOk());
+}
+
+TEST_F(KeysetHandleTest, GenerateNewWithGlobalRegistryConfig) {
+  KeyGenConfiguration config;
+  ASSERT_THAT(internal::KeyGenConfigurationImpl::SetGlobalRegistryMode(config),
+              IsOk());
+  EXPECT_THAT(KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), config),
+              IsOk());
+}
+
 TEST_F(KeysetHandleTest, GenerateNewWithAnnotations) {
   const absl::flat_hash_map<std::string, std::string> kAnnotations = {
       {"key1", "value1"}, {"key2", "value2"}};
 
-  // The template used doesn't make any different w.r.t. annotations.
-  util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+  // `handle` depends on the global registry.
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
       KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), kAnnotations);
-  ASSERT_THAT(keyset_handle, IsOk());
-  auto primitive_wrapper = absl::make_unique<MockAeadPrimitiveWrapper>();
-  absl::flat_hash_map<std::string, std::string> generated_annotations;
-  EXPECT_CALL(*primitive_wrapper, Wrap(_))
-      .WillOnce(
-          [&generated_annotations](
-              std::unique_ptr<PrimitiveSet<Aead>> generated_primitive_set) {
-            generated_annotations = generated_primitive_set->get_annotations();
-            std::unique_ptr<Aead> aead = absl::make_unique<DummyAead>("");
-            return aead;
-          });
-  Registry::Reset();
-  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(std::move(primitive_wrapper)),
-              IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>(
-                      "type.googleapis.com/google.crypto.tink.AesGcmKey"),
-                  true),
-              IsOk());
+  ASSERT_THAT(handle, IsOk());
 
-  EXPECT_THAT((*keyset_handle)->GetPrimitive<Aead>(), IsOk());
-  EXPECT_EQ(generated_annotations, kAnnotations);
-  // This is needed to cleanup mocks.
-  Registry::Reset();
+  // `config_handle` uses a config that depends on the global registry.
+  KeyGenConfiguration config;
+  ASSERT_THAT(internal::KeyGenConfigurationImpl::SetGlobalRegistryMode(config),
+              IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> config_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), config,
+                                kAnnotations);
+  ASSERT_THAT(config_handle, IsOk());
+
+  for (KeysetHandle h : {**handle, **config_handle}) {
+    auto primitive_wrapper = absl::make_unique<MockAeadPrimitiveWrapper>();
+    absl::flat_hash_map<std::string, std::string> generated_annotations;
+    EXPECT_CALL(*primitive_wrapper, Wrap(_))
+        .WillOnce(
+            [&generated_annotations](
+                std::unique_ptr<PrimitiveSet<Aead>> generated_primitive_set) {
+              generated_annotations =
+                  generated_primitive_set->get_annotations();
+              std::unique_ptr<Aead> aead = absl::make_unique<DummyAead>("");
+              return aead;
+            });
+
+    Registry::Reset();
+    ASSERT_THAT(
+        Registry::RegisterPrimitiveWrapper(std::move(primitive_wrapper)),
+        IsOk());
+    ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                    absl::make_unique<FakeAeadKeyManager>(
+                        "type.googleapis.com/google.crypto.tink.AesGcmKey"),
+                    /*new_key_allowed=*/true),
+                IsOk());
+
+    EXPECT_THAT(h.GetPrimitive<Aead>(), IsOk());
+    EXPECT_EQ(generated_annotations, kAnnotations);
+
+    // This is needed to cleanup mocks.
+    Registry::Reset();
+  }
 }
 
-TEST_F(KeysetHandleTest, GenerateNewKeysetHandleErrors) {
+TEST_F(KeysetHandleTest, GenerateNewErrors) {
   KeyTemplate templ;
   templ.set_type_url("type.googleapis.com/some.unknown.KeyType");
   templ.set_output_prefix_type(OutputPrefixType::TINK);
@@ -621,7 +678,6 @@
   EXPECT_FALSE(handle_result.ok());
 }
 
-
 void CompareKeyMetadata(const Keyset::Key& expected,
                         const Keyset::Key& actual) {
   EXPECT_EQ(expected.status(), actual.status());
@@ -630,9 +686,9 @@
 }
 
 TEST_F(KeysetHandleTest, GetPublicKeysetHandle) {
-  { // A keyset with a single key.
-    auto handle_result = KeysetHandle::GenerateNew(
-        SignatureKeyTemplates::EcdsaP256());
+  {  // A keyset with a single key.
+    auto handle_result =
+        KeysetHandle::GenerateNew(SignatureKeyTemplates::EcdsaP256());
     ASSERT_TRUE(handle_result.ok()) << handle_result.status();
     auto handle = std::move(handle_result.value());
     auto public_handle_result = handle->GetPublicKeysetHandle();
@@ -646,7 +702,7 @@
     EXPECT_EQ(KeyData::ASYMMETRIC_PUBLIC,
               public_keyset.key(0).key_data().key_material_type());
   }
-  { // A keyset with multiple keys.
+  {  // A keyset with multiple keys.
     EcdsaSignKeyManager key_manager;
     Keyset keyset;
     int key_count = 3;
@@ -684,9 +740,9 @@
 }
 
 TEST_F(KeysetHandleTest, GetPublicKeysetHandleErrors) {
-  { // A keyset with a single key.
-    auto handle_result = KeysetHandle::GenerateNew(
-        AeadKeyTemplates::Aes128Eax());
+  {  // A keyset with a single key.
+    auto handle_result =
+        KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Eax());
     ASSERT_TRUE(handle_result.ok()) << handle_result.status();
     auto handle = std::move(handle_result.value());
     auto public_handle_result = handle->GetPublicKeysetHandle();
@@ -694,7 +750,7 @@
     EXPECT_PRED_FORMAT2(testing::IsSubstring, "ASYMMETRIC_PRIVATE",
                         std::string(public_handle_result.status().message()));
   }
-  { // A keyset with multiple keys.
+  {  // A keyset with multiple keys.
     Keyset keyset;
 
     EcdsaKeyFormat ecdsa_key_format;
@@ -760,6 +816,84 @@
   EXPECT_EQ(aead->Decrypt(raw_encryption, aad).value(), plaintext);
 }
 
+TEST_F(KeysetHandleTest, GetPrimitiveWithBespokeConfigSucceeds) {
+  KeyGenConfiguration key_gen_config;
+  ASSERT_THAT(internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), key_gen_config),
+              IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), key_gen_config);
+  ASSERT_THAT(handle, IsOk());
+
+  Configuration config;
+  ASSERT_THAT(internal::ConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), config),
+              IsOk());
+  ASSERT_THAT(internal::ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<AeadWrapper>(), config),
+              IsOk());
+
+  EXPECT_THAT((*handle)->GetPrimitive<Aead>(config).status(), IsOk());
+}
+
+TEST_F(KeysetHandleTest, GetPrimitiveWithBespokeConfigFailsIfEmpty) {
+  KeyGenConfiguration key_gen_config;
+  ASSERT_THAT(internal::KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), key_gen_config),
+              IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(), key_gen_config);
+  ASSERT_THAT(handle, IsOk());
+
+  Configuration config;
+  EXPECT_THAT((*handle)->GetPrimitive<Aead>(config).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(KeysetHandleTest, GetPrimitiveWithGlobalRegistryConfig) {
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(),
+                                internal::KeyGenConfigGlobalRegistry());
+  ASSERT_THAT(handle, IsOk());
+
+  // TODO(b/265705174): Replace with ConfigGlobalRegistry instance.
+  Configuration config;
+  ASSERT_THAT(internal::ConfigurationImpl::SetGlobalRegistryMode(config),
+              IsOk());
+  EXPECT_THAT((*handle)->GetPrimitive<Aead>(config), IsOk());
+}
+
+TEST_F(KeysetHandleTest, GetPrimitiveWithConfigFips1402) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm(),
+                                KeyGenConfigFips140_2());
+  ASSERT_THAT(handle, IsOk());
+  EXPECT_THAT((*handle)->GetPrimitive<Aead>(ConfigFips140_2()), IsOk());
+}
+
+TEST_F(KeysetHandleTest, GetPrimitiveWithConfigFips1402FailsWithNonFipsHandle) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  Keyset keyset;
+  AesGcmSivKey key_proto;
+  *key_proto.mutable_key_value() = subtle::Random::GetRandomBytes(16);
+  test::AddTinkKey(AeadKeyTemplates::Aes256GcmSiv().type_url(), /*key_id=*/13,
+                   key_proto, KeyStatusType::ENABLED, KeyData::SYMMETRIC,
+                   &keyset);
+  keyset.set_primary_key_id(13);
+
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  EXPECT_THAT(handle->GetPrimitive<Aead>(ConfigFips140_2()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
 // Tests that GetPrimitive(nullptr) fails with a non-ok status.
 TEST_F(KeysetHandleTest, GetPrimitiveNullptrKeyManager) {
   Keyset keyset;
@@ -803,9 +937,9 @@
 TEST_F(KeysetHandleTest, ReadNoSecret) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PUBLIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::REMOTE, &keyset);
   keyset.set_primary_key_id(42);
   auto handle_result = KeysetHandle::ReadNoSecret(keyset.SerializeAsString());
@@ -841,12 +975,12 @@
   Registry::Reset();
   ASSERT_THAT(Registry::RegisterPrimitiveWrapper(std::move(primitive_wrapper)),
               IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>("some key type"),
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<FakeAeadKeyManager>("some_key_type"),
                   /*new_key_allowed=*/true),
               IsOk());
-  ASSERT_THAT(Registry::RegisterKeyManager(
-                  absl::make_unique<FakeAeadKeyManager>("some other key type"),
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<FakeAeadKeyManager>("some_other_key_type"),
                   /*new_key_allowed=*/true),
               IsOk());
 
@@ -859,7 +993,7 @@
 TEST_F(KeysetHandleTest, ReadNoSecretFailForTypeUnknown) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::UNKNOWN_KEYMATERIAL, &keyset);
   keyset.set_primary_key_id(42);
   auto result = KeysetHandle::ReadNoSecret(keyset.SerializeAsString());
@@ -869,7 +1003,7 @@
 TEST_F(KeysetHandleTest, ReadNoSecretFailForTypeSymmetric) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
   auto result = KeysetHandle::ReadNoSecret(keyset.SerializeAsString());
@@ -879,7 +1013,7 @@
 TEST_F(KeysetHandleTest, ReadNoSecretFailForTypeAssymmetricPrivate) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PRIVATE, &keyset);
   keyset.set_primary_key_id(42);
   auto result = KeysetHandle::ReadNoSecret(keyset.SerializeAsString());
@@ -889,13 +1023,13 @@
 TEST_F(KeysetHandleTest, ReadNoSecretFailForHidden) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PUBLIC, &keyset);
   for (int i = 0; i < 10; ++i) {
     AddTinkKey(absl::StrCat("more key type", i), i, key, KeyStatusType::ENABLED,
                KeyData::ASYMMETRIC_PUBLIC, &keyset);
   }
-  AddRawKey("some other key type", 10, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 10, key, KeyStatusType::ENABLED,
             KeyData::ASYMMETRIC_PRIVATE, &keyset);
   for (int i = 0; i < 10; ++i) {
     AddRawKey(absl::StrCat("more key type", i + 100), i + 100, key,
@@ -916,9 +1050,9 @@
 TEST_F(KeysetHandleTest, WriteNoSecret) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PUBLIC, &keyset);
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED,
             KeyData::REMOTE, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -935,7 +1069,7 @@
 TEST_F(KeysetHandleTest, WriteNoSecretFailForTypeUnknown) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::UNKNOWN_KEYMATERIAL, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -952,7 +1086,7 @@
 TEST_F(KeysetHandleTest, WriteNoSecretFailForTypeSymmetric) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -969,7 +1103,7 @@
 TEST_F(KeysetHandleTest, WriteNoSecretFailForTypeAssymmetricPrivate) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PRIVATE, &keyset);
   keyset.set_primary_key_id(42);
 
@@ -986,13 +1120,13 @@
 TEST_F(KeysetHandleTest, WriteNoSecretFailForHidden) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PUBLIC, &keyset);
   for (int i = 0; i < 10; ++i) {
     AddTinkKey(absl::StrCat("more key type", i), i, key, KeyStatusType::ENABLED,
                KeyData::ASYMMETRIC_PUBLIC, &keyset);
   }
-  AddRawKey("some other key type", 10, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 10, key, KeyStatusType::ENABLED,
             KeyData::ASYMMETRIC_PRIVATE, &keyset);
   for (int i = 0; i < 10; ++i) {
     AddRawKey(absl::StrCat("more key type", i + 100), i + 100, key,
@@ -1014,13 +1148,13 @@
 TEST_F(KeysetHandleTest, GetKeysetInfo) {
   Keyset keyset;
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED,
              KeyData::ASYMMETRIC_PUBLIC, &keyset);
   for (int i = 0; i < 10; ++i) {
     AddTinkKey(absl::StrCat("more key type", i), i, key, KeyStatusType::ENABLED,
                KeyData::ASYMMETRIC_PUBLIC, &keyset);
   }
-  AddRawKey("some other key type", 10, key, KeyStatusType::ENABLED,
+  AddRawKey("some_other_key_type", 10, key, KeyStatusType::ENABLED,
             KeyData::ASYMMETRIC_PRIVATE, &keyset);
   for (int i = 0; i < 10; ++i) {
     AddRawKey(absl::StrCat("more key type", i + 100), i + 100, key,
@@ -1042,6 +1176,192 @@
   }
 }
 
+TEST_F(KeysetHandleTest, GetEntryFromSingleKeyKeyset) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(11);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(handle->Validate(), IsOk());
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  ASSERT_THAT(handle->ValidateAt(0), IsOk());
+  KeysetHandle::Entry entry = (*handle)[0];
+
+  EXPECT_THAT(entry.GetId(), Eq(11));
+  EXPECT_THAT(entry.GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT(entry.IsPrimary(), IsTrue());
+  EXPECT_THAT(entry.GetKey()->GetIdRequirement(), Eq(11));
+  EXPECT_THAT(entry.GetKey()->GetParameters().HasIdRequirement(), IsTrue());
+}
+
+TEST_F(KeysetHandleTest, GetEntryFromMultipleKeyKeyset) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddRawKey("first_key_type", 11, key, KeyStatusType::DISABLED,
+            KeyData::SYMMETRIC, &keyset);
+  AddTinkKey("second_key_type", 22, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  AddRawKey("third_key_type", 33, key, KeyStatusType::DESTROYED,
+            KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(22);
+
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(handle->Validate(), IsOk());
+  ASSERT_THAT(*handle, SizeIs(3));
+
+  ASSERT_THAT(handle->ValidateAt(0), IsOk());
+  KeysetHandle::Entry entry0 = (*handle)[0];
+  EXPECT_THAT(entry0.GetId(), Eq(11));
+  EXPECT_THAT(entry0.GetStatus(), Eq(KeyStatus::kDisabled));
+  EXPECT_THAT(entry0.IsPrimary(), IsFalse());
+  EXPECT_THAT(entry0.GetKey()->GetIdRequirement(), Eq(absl::nullopt));
+  EXPECT_THAT(entry0.GetKey()->GetParameters().HasIdRequirement(), IsFalse());
+
+  ASSERT_THAT(handle->ValidateAt(1), IsOk());
+  KeysetHandle::Entry entry1 = (*handle)[1];
+  EXPECT_THAT(entry1.GetId(), Eq(22));
+  EXPECT_THAT(entry1.GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT(entry1.IsPrimary(), IsTrue());
+  EXPECT_THAT(entry1.GetKey()->GetIdRequirement(), Eq(22));
+  EXPECT_THAT(entry1.GetKey()->GetParameters().HasIdRequirement(), IsTrue());
+
+  ASSERT_THAT(handle->ValidateAt(2), IsOk());
+  KeysetHandle::Entry entry2 = (*handle)[2];
+  EXPECT_THAT(entry2.GetId(), Eq(33));
+  EXPECT_THAT(entry2.GetStatus(), Eq(KeyStatus::kDestroyed));
+  EXPECT_THAT(entry2.IsPrimary(), IsFalse());
+  EXPECT_THAT(entry2.GetKey()->GetIdRequirement(), Eq(absl::nullopt));
+  EXPECT_THAT(entry2.GetKey()->GetParameters().HasIdRequirement(), IsFalse());
+}
+
+TEST_F(KeysetHandleDeathTest, EntryWithIndexOutOfBoundsCrashes) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(11);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(handle->Validate(), IsOk());
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  EXPECT_DEATH_IF_SUPPORTED((*handle)[-1],
+                            "Invalid index -1 for keyset of size 1");
+  EXPECT_DEATH_IF_SUPPORTED((*handle)[1],
+                            "Invalid index 1 for keyset of size 1");
+}
+
+TEST_F(KeysetHandleDeathTest, EntryWithUnknownStatusFails) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::UNKNOWN_STATUS,
+             KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(11);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  EXPECT_THAT(handle->Validate(), StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(handle->ValidateAt(0),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_DEATH_IF_SUPPORTED((*handle)[0], "Invalid key status type.");
+}
+
+TEST_F(KeysetHandleDeathTest, EntryWithUnprintableTypeUrlFails) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddRawKey("invalid key type url with spaces", 11, key, KeyStatusType::ENABLED,
+            KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(11);
+
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  EXPECT_THAT(handle->Validate(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(handle->ValidateAt(0),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_DEATH_IF_SUPPORTED((*handle)[0],
+                            "Non-printable ASCII character in type URL.");
+}
+
+TEST_F(KeysetHandleTest, GetPrimary) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  AddTinkKey("first_key_type", 22, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  AddTinkKey("first_key_type", 33, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(33);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(handle->Validate(), IsOk());
+  ASSERT_THAT(*handle, SizeIs(3));
+
+  util::StatusOr<KeysetHandle::Entry> primary = handle->GetPrimary();
+  ASSERT_THAT(primary, IsOk());
+
+  EXPECT_THAT(primary->GetId(), Eq(33));
+  EXPECT_THAT(primary->GetStatus(), Eq(KeyStatus::kEnabled));
+  EXPECT_THAT(primary->IsPrimary(), IsTrue());
+}
+
+TEST_F(KeysetHandleDeathTest, NonexistentPrimaryFails) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  EXPECT_THAT(handle->Validate(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_DEATH_IF_SUPPORTED(handle->GetPrimary(), "Keyset has no primary");
+}
+
+TEST_F(KeysetHandleDeathTest, MultiplePrimariesFail) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  AddTinkKey("second_key_type", 11, key, KeyStatusType::ENABLED,
+             KeyData::SYMMETRIC, &keyset);
+  // Multiple primaries since two distinct keys share the same key id.
+  keyset.set_primary_key_id(11);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(*handle, SizeIs(2));
+
+  EXPECT_THAT(handle->Validate(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_DEATH_IF_SUPPORTED(handle->GetPrimary(),
+                            "Keyset has more than one primary");
+}
+
+TEST_F(KeysetHandleDeathTest, GetDisabledPrimaryFails) {
+  Keyset keyset;
+  Keyset::Key key;
+  AddTinkKey("first_key_type", 11, key, KeyStatusType::DISABLED,
+             KeyData::SYMMETRIC, &keyset);
+  keyset.set_primary_key_id(11);
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  ASSERT_THAT(*handle, SizeIs(1));
+
+  EXPECT_THAT(handle->Validate(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_DEATH_IF_SUPPORTED(handle->GetPrimary(),
+                            "Keyset has primary that is not enabled");
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/core/keyset_manager.cc b/cc/core/keyset_manager.cc
index 6ca880a..947b3bd 100644
--- a/cc/core/keyset_manager.cc
+++ b/cc/core/keyset_manager.cc
@@ -17,14 +17,13 @@
 #include "tink/keyset_manager.h"
 
 #include <memory>
-#include <random>
 #include <utility>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
+#include "tink/internal/key_gen_configuration_impl.h"
+#include "tink/key_gen_configuration.h"
 #include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
-#include "tink/registry.h"
 #include "tink/util/enums.h"
 #include "tink/util/errors.h"
 #include "tink/util/status.h"
@@ -34,12 +33,12 @@
 namespace crypto {
 namespace tink {
 
+using ::crypto::tink::util::Enums;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
 using google::crypto::tink::Keyset;
 using google::crypto::tink::KeyStatusType;
 using google::crypto::tink::KeyTemplate;
-using crypto::tink::util::Enums;
-using crypto::tink::util::Status;
-using crypto::tink::util::StatusOr;
 
 // static
 StatusOr<std::unique_ptr<KeysetManager>> KeysetManager::New(
@@ -71,17 +70,22 @@
   return Add(key_template, false);
 }
 
-crypto::tink::util::StatusOr<uint32_t> KeysetManager::Add(
+StatusOr<uint32_t> KeysetManager::Add(
     const google::crypto::tink::KeyTemplate& key_template, bool as_primary) {
+  KeyGenConfiguration config;
+  Status status =
+      internal::KeyGenConfigurationImpl::SetGlobalRegistryMode(config);
+  if (!status.ok()) {
+    return status;
+  }
   absl::MutexLock lock(&keyset_mutex_);
-  return KeysetHandle::AddToKeyset(key_template, as_primary, &keyset_);
+  return KeysetHandle::AddToKeyset(key_template, as_primary, config, &keyset_);
 }
 
 StatusOr<uint32_t> KeysetManager::Rotate(const KeyTemplate& key_template) {
   return Add(key_template, true);
 }
 
-
 Status KeysetManager::Enable(uint32_t key_id) {
   absl::MutexLock lock(&keyset_mutex_);
   for (auto& key : *(keyset_.mutable_key())) {
@@ -129,8 +133,7 @@
                      "Cannot delete primary key (key_id %u).", key_id);
   }
   auto key_field = keyset_.mutable_key();
-  for (auto key_iter = key_field->begin();
-       key_iter != key_field->end();
+  for (auto key_iter = key_field->begin(); key_iter != key_field->end();
        key_iter++) {
     auto key = *key_iter;
     if (key.key_id() == key_id) {
@@ -184,7 +187,6 @@
                    "No key with key_id %u found in the keyset.", key_id);
 }
 
-
 int KeysetManager::KeyCount() const {
   absl::MutexLock lock(&keyset_mutex_);
   return keyset_.key_size();
diff --git a/cc/core/keyset_manager_test.cc b/cc/core/keyset_manager_test.cc
index e60838e..230d660 100644
--- a/cc/core/keyset_manager_test.cc
+++ b/cc/core/keyset_manager_test.cc
@@ -20,14 +20,11 @@
 #include "gtest/gtest.h"
 #include "tink/aead/aead_config.h"
 #include "tink/aead/aes_gcm_key_manager.h"
-#include "tink/config.h"
 #include "tink/keyset_handle.h"
 #include "tink/util/test_keyset_handle.h"
 #include "proto/aes_gcm.pb.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
-
 using google::crypto::tink::AesGcmKeyFormat;
 using google::crypto::tink::KeyData;
 using google::crypto::tink::KeyStatusType;
diff --git a/cc/core/partial_key_access_token_test.cc b/cc/core/partial_key_access_token_test.cc
new file mode 100644
index 0000000..41087e5
--- /dev/null
+++ b/cc/core/partial_key_access_token_test.cc
@@ -0,0 +1,52 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <utility>
+
+#include "tink/partial_key_access_token.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/base/attributes.h"
+#include "tink/partial_key_access.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+TEST(PartialKeyAccessTokenTest, CopyConstructor) {
+  PartialKeyAccessToken token = GetPartialKeyAccess();
+  PartialKeyAccessToken copy ABSL_ATTRIBUTE_UNUSED(token);
+}
+
+TEST(PartialKeyAccessTokenTest, CopyAssignment) {
+  PartialKeyAccessToken token = GetPartialKeyAccess();
+  PartialKeyAccessToken copy ABSL_ATTRIBUTE_UNUSED = token;
+}
+
+TEST(PartialKeyAccessTokenTest, MoveConstructor) {
+  PartialKeyAccessToken token = GetPartialKeyAccess();
+  PartialKeyAccessToken move ABSL_ATTRIBUTE_UNUSED(std::move(token));
+}
+
+TEST(PartialKeyAccessTokenTest, MoveAssignment) {
+  PartialKeyAccessToken token = GetPartialKeyAccess();
+  PartialKeyAccessToken move ABSL_ATTRIBUTE_UNUSED = std::move(token);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/core/primitive_set_test.cc b/cc/core/primitive_set_test.cc
index 3f3ebdf..ea877bd 100644
--- a/cc/core/primitive_set_test.cc
+++ b/cc/core/primitive_set_test.cc
@@ -24,7 +24,9 @@
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "tink/cleartext_keyset_handle.h"
 #include "tink/crypto_format.h"
+#include "tink/keyderivation/keyset_deriver.h"
 #include "tink/mac.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
@@ -32,20 +34,21 @@
 
 using ::crypto::tink::test::DummyMac;
 using ::crypto::tink::test::IsOk;
+using ::google::crypto::tink::Keyset;
 using ::google::crypto::tink::KeysetInfo;
 using ::google::crypto::tink::KeyStatusType;
 using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::SizeIs;
 using ::testing::UnorderedElementsAreArray;
 
 namespace crypto {
 namespace tink {
 namespace {
 
-class PrimitiveSetTest : public ::testing::Test {
-};
+class PrimitiveSetTest : public ::testing::Test {};
 
-void add_primitives(PrimitiveSet<Mac>* primitive_set,
-                    int key_id_offset,
+void add_primitives(PrimitiveSet<Mac>* primitive_set, int key_id_offset,
                     int primitives_count) {
   for (int i = 0; i < primitives_count; i++) {
     int key_id = key_id_offset + i;
@@ -59,8 +62,20 @@
   }
 }
 
-void access_primitives(PrimitiveSet<Mac>* primitive_set,
-                       int key_id_offset,
+void add_primitives(PrimitiveSet<Mac>::Builder* primitive_set_builder,
+                    int key_id_offset, int primitives_count) {
+  for (int i = 0; i < primitives_count; i++) {
+    int key_id = key_id_offset + i;
+    KeysetInfo::KeyInfo key_info;
+    key_info.set_output_prefix_type(OutputPrefixType::TINK);
+    key_info.set_key_id(key_id);
+    key_info.set_status(KeyStatusType::ENABLED);
+    std::unique_ptr<Mac> mac(new DummyMac("dummy MAC"));
+    primitive_set_builder->AddPrimitive(std::move(mac), key_info);
+  }
+}
+
+void access_primitives(PrimitiveSet<Mac>* primitive_set, int key_id_offset,
                        int primitives_count) {
   for (int i = 0; i < primitives_count; i++) {
     int key_id = key_id_offset + i;
@@ -76,17 +91,24 @@
 }
 
 TEST_F(PrimitiveSetTest, ConcurrentOperations) {
-  PrimitiveSet<Mac> mac_set;
+  PrimitiveSet<Mac>::Builder mac_set_builder;
   int offset_a = 100;
   int offset_b = 150;
   int count = 100;
 
   // Add some primitives.
-  std::thread add_primitives_a(add_primitives, &mac_set, offset_a, count);
-  std::thread add_primitives_b(add_primitives, &mac_set, offset_b, count);
+  // See go/totw/133 on why we use a lambda here.
+  std::thread add_primitives_a(
+      [&]() { add_primitives(&mac_set_builder, offset_a, count); });
+  std::thread add_primitives_b(
+      [&]() { add_primitives(&mac_set_builder, offset_b, count); });
   add_primitives_a.join();
   add_primitives_b.join();
 
+  auto mac_set_result = std::move(mac_set_builder).Build();
+  ASSERT_TRUE(mac_set_result.ok()) << mac_set_result.status();
+  PrimitiveSet<Mac> mac_set = std::move(mac_set_result.value());
+
   // Access primitives.
   std::thread access_primitives_a(access_primitives, &mac_set, offset_a, count);
   std::thread access_primitives_b(access_primitives, &mac_set, offset_b, count);
@@ -137,7 +159,7 @@
   key_2.set_key_id(key_id_2);
   key_2.set_status(KeyStatusType::ENABLED);
 
-  uint32_t key_id_3 = key_id_2;    // same id as key_2
+  uint32_t key_id_3 = key_id_2;  // same id as key_2
   KeysetInfo::KeyInfo key_3;
   key_3.set_output_prefix_type(OutputPrefixType::TINK);
   key_3.set_key_id(key_id_3);
@@ -155,7 +177,408 @@
   key_5.set_key_id(key_id_5);
   key_5.set_status(KeyStatusType::ENABLED);
 
-  uint32_t key_id_6 = key_id_1;    // same id as key_1
+  uint32_t key_id_6 = key_id_1;  // same id as key_1
+  KeysetInfo::KeyInfo key_6;
+  key_6.set_output_prefix_type(OutputPrefixType::TINK);
+  key_6.set_key_id(key_id_6);
+  key_6.set_status(KeyStatusType::ENABLED);
+
+  PrimitiveSet<Mac>::Builder primitive_set_builder;
+
+  // Add all the primitives.
+  auto primitive_set_result = PrimitiveSet<Mac>::Builder{}
+                                  .AddPrimitive(std::move(mac_1), key_1)
+                                  .AddPrimitive(std::move(mac_2), key_2)
+                                  .AddPrimaryPrimitive(std::move(mac_3), key_3)
+                                  .AddPrimitive(std::move(mac_4), key_4)
+                                  .AddPrimitive(std::move(mac_5), key_5)
+                                  .AddPrimitive(std::move(mac_6), key_6)
+                                  .Build();
+
+  ASSERT_TRUE(primitive_set_result.ok()) << primitive_set_result.status();
+  PrimitiveSet<Mac> primitive_set = std::move(primitive_set_result.value());
+
+  std::string data = "some data";
+
+  {  // Check the primary.
+    auto primary = primitive_set.get_primary();
+    EXPECT_FALSE(primary == nullptr);
+    EXPECT_EQ(KeyStatusType::ENABLED, primary->get_status());
+    EXPECT_EQ(DummyMac(mac_name_3).ComputeMac(data).value(),
+              primary->get_primitive().ComputeMac(data).value());
+  }
+
+  {  // Check raw primitives.
+    auto& primitives = *(primitive_set.get_raw_primitives().value());
+    EXPECT_EQ(2, primitives.size());
+    EXPECT_EQ(DummyMac(mac_name_4).ComputeMac(data).value(),
+              primitives[0]->get_primitive().ComputeMac(data).value());
+    EXPECT_EQ(KeyStatusType::ENABLED, primitives[0]->get_status());
+    EXPECT_EQ(key_4.key_id(), primitives[0]->get_key_id());
+    EXPECT_EQ(OutputPrefixType::RAW, primitives[0]->get_output_prefix_type());
+    EXPECT_EQ(DummyMac(mac_name_5).ComputeMac(data).value(),
+              primitives[1]->get_primitive().ComputeMac(data).value());
+    EXPECT_EQ(KeyStatusType::ENABLED, primitives[1]->get_status());
+    EXPECT_EQ(key_5.key_id(), primitives[1]->get_key_id());
+    EXPECT_EQ(OutputPrefixType::RAW, primitives[1]->get_output_prefix_type());
+  }
+
+  {  // Check Tink primitives.
+    std::string prefix = CryptoFormat::GetOutputPrefix(key_1).value();
+    auto& primitives = *(primitive_set.get_primitives(prefix).value());
+    EXPECT_EQ(2, primitives.size());
+    EXPECT_EQ(DummyMac(mac_name_1).ComputeMac(data).value(),
+              primitives[0]->get_primitive().ComputeMac(data).value());
+    EXPECT_EQ(KeyStatusType::ENABLED, primitives[0]->get_status());
+    EXPECT_EQ(key_1.key_id(), primitives[0]->get_key_id());
+    EXPECT_EQ(OutputPrefixType::TINK, primitives[0]->get_output_prefix_type());
+    EXPECT_EQ(DummyMac(mac_name_6).ComputeMac(data).value(),
+              primitives[1]->get_primitive().ComputeMac(data).value());
+    EXPECT_EQ(KeyStatusType::ENABLED, primitives[1]->get_status());
+    EXPECT_EQ(key_1.key_id(), primitives[1]->get_key_id());
+    EXPECT_EQ(OutputPrefixType::TINK, primitives[1]->get_output_prefix_type());
+  }
+
+  {  // Check another Tink primitive.
+    std::string prefix = CryptoFormat::GetOutputPrefix(key_3).value();
+    auto& primitives = *(primitive_set.get_primitives(prefix).value());
+    EXPECT_EQ(1, primitives.size());
+    EXPECT_EQ(DummyMac(mac_name_3).ComputeMac(data).value(),
+              primitives[0]->get_primitive().ComputeMac(data).value());
+    EXPECT_EQ(KeyStatusType::ENABLED, primitives[0]->get_status());
+    EXPECT_EQ(key_3.key_id(), primitives[0]->get_key_id());
+    EXPECT_EQ(OutputPrefixType::TINK, primitives[0]->get_output_prefix_type());
+  }
+
+  {  // Check legacy primitive.
+    std::string prefix = CryptoFormat::GetOutputPrefix(key_2).value();
+    auto& primitives = *(primitive_set.get_primitives(prefix).value());
+    EXPECT_EQ(1, primitives.size());
+    EXPECT_EQ(DummyMac(mac_name_2).ComputeMac(data).value(),
+              primitives[0]->get_primitive().ComputeMac(data).value());
+    EXPECT_EQ(KeyStatusType::ENABLED, primitives[0]->get_status());
+    EXPECT_EQ(key_2.key_id(), primitives[0]->get_key_id());
+    EXPECT_EQ(OutputPrefixType::LEGACY,
+              primitives[0]->get_output_prefix_type());
+  }
+}
+
+TEST_F(PrimitiveSetTest, PrimaryKeyWithIdCollisions) {
+  std::string mac_name_1 = "MAC#1";
+  std::string mac_name_2 = "MAC#2";
+
+  uint32_t key_id_1 = 1234543;
+  KeysetInfo::KeyInfo key_info_1;
+  key_info_1.set_key_id(key_id_1);
+  key_info_1.set_status(KeyStatusType::ENABLED);
+
+  uint32_t key_id_2 = key_id_1;  // same id as key_2
+  KeysetInfo::KeyInfo key_info_2;
+  key_info_2.set_key_id(key_id_2);
+  key_info_2.set_status(KeyStatusType::ENABLED);
+
+  {  // Test with RAW-keys.
+    std::unique_ptr<Mac> mac_1(new DummyMac(mac_name_1));
+    std::unique_ptr<Mac> mac_2(new DummyMac(mac_name_2));
+    key_info_1.set_output_prefix_type(OutputPrefixType::RAW);
+    key_info_2.set_output_prefix_type(OutputPrefixType::RAW);
+    PrimitiveSet<Mac>::Builder primitive_set_builder;
+
+    // Add the first primitive, and set it as primary.
+    primitive_set_builder.AddPrimaryPrimitive(std::move(mac_1), key_info_1);
+
+    auto primitive_set_result = std::move(primitive_set_builder).Build();
+    ASSERT_TRUE(primitive_set_result.ok()) << primitive_set_result.status();
+    PrimitiveSet<Mac> primitive_set = std::move(primitive_set_result.value());
+
+    std::string identifier = "";
+    const auto& primitives =
+        *(primitive_set.get_primitives(identifier).value());
+    EXPECT_EQ(1, primitives.size());
+    EXPECT_EQ(primitive_set.get_primary(), primitives[0].get());
+  }
+
+  {  // Test with TINK-keys.
+    std::unique_ptr<Mac> mac_1(new DummyMac(mac_name_1));
+    std::unique_ptr<Mac> mac_2(new DummyMac(mac_name_2));
+    key_info_1.set_output_prefix_type(OutputPrefixType::TINK);
+    key_info_2.set_output_prefix_type(OutputPrefixType::TINK);
+    PrimitiveSet<Mac>::Builder primitive_set_builder;
+
+    // Add the first primitive, and set it as primary.
+    primitive_set_builder.AddPrimaryPrimitive(std::move(mac_1), key_info_1);
+
+    auto primitive_set_result = std::move(primitive_set_builder).Build();
+    ASSERT_TRUE(primitive_set_result.ok()) << primitive_set_result.status();
+    PrimitiveSet<Mac> primitive_set = std::move(primitive_set_result.value());
+    std::string identifier = CryptoFormat::GetOutputPrefix(key_info_1).value();
+    const auto& primitives =
+        *(primitive_set.get_primitives(identifier).value());
+    EXPECT_EQ(1, primitives.size());
+    EXPECT_EQ(primitive_set.get_primary(), primitives[0].get());
+  }
+
+  {  // Test with LEGACY-keys.
+    std::unique_ptr<Mac> mac_1(new DummyMac(mac_name_1));
+    std::unique_ptr<Mac> mac_2(new DummyMac(mac_name_2));
+    key_info_1.set_output_prefix_type(OutputPrefixType::LEGACY);
+    key_info_2.set_output_prefix_type(OutputPrefixType::LEGACY);
+    PrimitiveSet<Mac>::Builder primitive_set_builder;
+
+    // Add the first primitive, and set it as primary.
+    primitive_set_builder.AddPrimaryPrimitive(std::move(mac_1), key_info_1);
+
+    auto primitive_set_result = std::move(primitive_set_builder).Build();
+    ASSERT_TRUE(primitive_set_result.ok()) << primitive_set_result.status();
+    PrimitiveSet<Mac> primitive_set = std::move(primitive_set_result.value());
+    std::string identifier = CryptoFormat::GetOutputPrefix(key_info_1).value();
+    const auto& primitives =
+        *(primitive_set.get_primitives(identifier).value());
+    EXPECT_EQ(1, primitives.size());
+    EXPECT_EQ(primitive_set.get_primary(), primitives[0].get());
+  }
+}
+
+TEST_F(PrimitiveSetTest, DisabledKey) {
+  std::string mac_name_1 = "MAC#1";
+  std::unique_ptr<Mac> mac_1(new DummyMac(mac_name_1));
+
+  uint32_t key_id_1 = 1234543;
+  KeysetInfo::KeyInfo key_info_1;
+  key_info_1.set_output_prefix_type(OutputPrefixType::TINK);
+  key_info_1.set_key_id(key_id_1);
+  key_info_1.set_status(KeyStatusType::DISABLED);
+
+  // Add all the primitives.
+  auto add_primitive_result = PrimitiveSet<Mac>::Builder{}
+                                  .AddPrimitive(std::move(mac_1), key_info_1)
+                                  .Build();
+  EXPECT_FALSE(add_primitive_result.ok());
+}
+
+KeysetInfo::KeyInfo CreateKey(uint32_t key_id,
+                              OutputPrefixType output_prefix_type,
+                              KeyStatusType key_status,
+                              absl::string_view type_url) {
+  KeysetInfo::KeyInfo key_info;
+  key_info.set_output_prefix_type(output_prefix_type);
+  key_info.set_key_id(key_id);
+  key_info.set_status(key_status);
+  std::string type_url_str(type_url);
+  key_info.set_type_url(type_url_str);
+  return key_info;
+}
+
+// Struct to hold MAC, Id and type_url.
+struct MacIdAndTypeUrl {
+  std::string mac;
+  std::string id;
+  std::string type_url;
+};
+
+bool operator==(const MacIdAndTypeUrl& first, const MacIdAndTypeUrl& other) {
+  return first.mac == other.mac && first.id == other.id &&
+         first.type_url == other.type_url;
+}
+
+TEST_F(PrimitiveSetTest, GetAll) {
+  auto pset_result =
+      PrimitiveSet<Mac>::Builder{}
+          .AddPrimitive(
+              absl::make_unique<DummyMac>("MAC1"),
+              CreateKey(0x01010101, OutputPrefixType::TINK,
+                        KeyStatusType::ENABLED, /*type_url=*/
+                        "type.googleapis.com/google.crypto.tink.HmacKey"))
+          .AddPrimitive(
+              absl::make_unique<DummyMac>("MAC2"),
+              CreateKey(0x02020202, OutputPrefixType::TINK,
+                        KeyStatusType::ENABLED, /*type_url=*/
+                        "type.googleapis.com/google.crypto.tink.HmacKey"))
+          // Add primitive and make it primary.
+          .AddPrimaryPrimitive(
+              absl::make_unique<DummyMac>("MAC3"),
+              CreateKey(0x02020202, OutputPrefixType::TINK,
+                        KeyStatusType::ENABLED, /*type_url=*/
+                        "type.googleapis.com/google.crypto.tink.AesCmacKey"))
+          .AddPrimitive(
+              absl::make_unique<DummyMac>("MAC4"),
+              CreateKey(0x02020202, OutputPrefixType::RAW,
+                        KeyStatusType::ENABLED, /*type_url=*/
+                        "type.googleapis.com/google.crypto.tink.AesCmacKey"))
+          .AddPrimitive(
+              absl::make_unique<DummyMac>("MAC5"),
+              CreateKey(0x01010101, OutputPrefixType::TINK,
+                        KeyStatusType::ENABLED, /*type_url=*/
+                        "type.googleapis.com/google.crypto.tink.AesCmacKey"))
+          .Build();
+
+  ASSERT_TRUE(pset_result.ok()) << pset_result.status();
+  PrimitiveSet<Mac> pset = std::move(pset_result.value());
+
+  std::vector<MacIdAndTypeUrl> mac_id_and_type;
+  for (auto* entry : pset.get_all()) {
+    auto mac_or = entry->get_primitive().ComputeMac("");
+    ASSERT_THAT(mac_or, IsOk());
+    mac_id_and_type.push_back({mac_or.value(), entry->get_identifier(),
+                               std::string(entry->get_key_type_url())});
+  }
+
+  // In the following id part, the first byte is 1 for Tink.
+  std::vector<MacIdAndTypeUrl> expected_result = {
+      {/*mac=*/"13:0:DummyMac:MAC1", /*id=*/absl::StrCat("\1\1\1\1\1"),
+       /*type_url=*/"type.googleapis.com/google.crypto.tink.HmacKey"},
+      {/*mac=*/"13:0:DummyMac:MAC2", /*id=*/absl::StrCat("\1\2\2\2\2"),
+       /*type_url=*/"type.googleapis.com/google.crypto.tink.HmacKey"},
+      {/*mac=*/"13:0:DummyMac:MAC3", /*id=*/absl::StrCat("\1\2\2\2\2"),
+       /*type_url=*/"type.googleapis.com/google.crypto.tink.AesCmacKey"},
+      {/*mac=*/"13:0:DummyMac:MAC4", /*id=*/"",
+       /*type_url=*/"type.googleapis.com/google.crypto.tink.AesCmacKey"},
+      {/*mac=*/"13:0:DummyMac:MAC5", /*id=*/absl::StrCat("\1\1\1\1\1"),
+       /*type_url=*/"type.googleapis.com/google.crypto.tink.AesCmacKey"}};
+
+  EXPECT_THAT(mac_id_and_type, UnorderedElementsAreArray(expected_result));
+}
+
+class FakeDeriver : public KeysetDeriver {
+ public:
+  explicit FakeDeriver() = default;
+  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> DeriveKeyset(
+      absl::string_view salt) const override {
+    Keyset keyset;
+    return CleartextKeysetHandle::GetKeysetHandle(keyset);
+  }
+};
+
+TEST_F(PrimitiveSetTest, GetAllInKeysetOrder) {
+  auto pset = absl::make_unique<PrimitiveSet<KeysetDeriver>>();
+  std::vector<KeysetInfo::KeyInfo> key_infos;
+
+  KeysetInfo::KeyInfo key_info;
+  key_info.set_key_id(1010101);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::RAW);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+  ASSERT_THAT(pset->AddPrimitive(absl::make_unique<FakeDeriver>(), key_info),
+              IsOk());
+  key_infos.push_back(key_info);
+
+  key_info.set_key_id(2020202);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::LEGACY);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+  ASSERT_THAT(pset->AddPrimitive(absl::make_unique<FakeDeriver>(), key_info),
+              IsOk());
+  key_infos.push_back(key_info);
+
+  key_info.set_key_id(3030303);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::TINK);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+  ASSERT_THAT(pset->AddPrimitive(absl::make_unique<FakeDeriver>(), key_info),
+              IsOk());
+  key_infos.push_back(key_info);
+
+  std::vector<PrimitiveSet<KeysetDeriver>::Entry<KeysetDeriver>*> entries =
+      pset->get_all_in_keyset_order();
+  ASSERT_THAT(entries, SizeIs(key_infos.size()));
+
+  for (int i = 0; i < entries.size(); i++) {
+    EXPECT_THAT(entries[i]->get_identifier(),
+                Eq(*CryptoFormat::GetOutputPrefix(key_infos[i])));
+    EXPECT_THAT(entries[i]->get_status(), Eq(KeyStatusType::ENABLED));
+    EXPECT_THAT(entries[i]->get_key_id(), Eq(key_infos[i].key_id()));
+    EXPECT_THAT(entries[i]->get_output_prefix_type(),
+                Eq(key_infos[i].output_prefix_type()));
+    EXPECT_THAT(entries[i]->get_key_type_url(), Eq(key_infos[i].type_url()));
+  }
+}
+
+TEST_F(PrimitiveSetTest, LegacyConcurrentOperations) {
+  PrimitiveSet<Mac> mac_set;
+  int offset_a = 100;
+  int offset_b = 150;
+  int count = 100;
+
+  // Add some primitives.
+  std::thread add_primitives_a(
+      [&]() { add_primitives(&mac_set, offset_a, count); });
+  std::thread add_primitives_b(
+      [&]() { add_primitives(&mac_set, offset_b, count); });
+  add_primitives_a.join();
+  add_primitives_b.join();
+
+  // Access primitives.
+  std::thread access_primitives_a(access_primitives, &mac_set, offset_a, count);
+  std::thread access_primitives_b(access_primitives, &mac_set, offset_b, count);
+  access_primitives_a.join();
+  access_primitives_b.join();
+
+  // Verify the common key ids added by both threads.
+  for (int key_id = offset_a; key_id < offset_b + count; key_id++) {
+    KeysetInfo::KeyInfo key_info;
+    key_info.set_output_prefix_type(OutputPrefixType::TINK);
+    key_info.set_key_id(key_id);
+    key_info.set_status(KeyStatusType::ENABLED);
+    std::string prefix = CryptoFormat::GetOutputPrefix(key_info).value();
+    auto get_result = mac_set.get_primitives(prefix);
+    EXPECT_TRUE(get_result.ok()) << get_result.status();
+    auto macs = get_result.value();
+    if (key_id >= offset_b && key_id < offset_a + count) {
+      EXPECT_EQ(2, macs->size());  // overlapping key_id range
+    } else {
+      EXPECT_EQ(1, macs->size());
+    }
+  }
+}
+
+TEST_F(PrimitiveSetTest, LegacyBasic) {
+  std::string mac_name_1 = "MAC#1";
+  std::unique_ptr<Mac> mac_1(new DummyMac(mac_name_1));
+  std::string mac_name_2 = "MAC#2";
+  std::unique_ptr<Mac> mac_2(new DummyMac(mac_name_2));
+  std::string mac_name_3 = "MAC#3";
+  std::unique_ptr<Mac> mac_3(new DummyMac(mac_name_3));
+  std::string mac_name_4 = "MAC#3";
+  std::unique_ptr<Mac> mac_4(new DummyMac(mac_name_4));
+  std::string mac_name_5 = "MAC#3";
+  std::unique_ptr<Mac> mac_5(new DummyMac(mac_name_5));
+  std::string mac_name_6 = "MAC#3";
+  std::unique_ptr<Mac> mac_6(new DummyMac(mac_name_6));
+
+  uint32_t key_id_1 = 1234543;
+  KeysetInfo::KeyInfo key_1;
+  key_1.set_output_prefix_type(OutputPrefixType::TINK);
+  key_1.set_key_id(key_id_1);
+  key_1.set_status(KeyStatusType::ENABLED);
+
+  uint32_t key_id_2 = 7213743;
+  KeysetInfo::KeyInfo key_2;
+  key_2.set_output_prefix_type(OutputPrefixType::LEGACY);
+  key_2.set_key_id(key_id_2);
+  key_2.set_status(KeyStatusType::ENABLED);
+
+  uint32_t key_id_3 = key_id_2;  // same id as key_2
+  KeysetInfo::KeyInfo key_3;
+  key_3.set_output_prefix_type(OutputPrefixType::TINK);
+  key_3.set_key_id(key_id_3);
+  key_3.set_status(KeyStatusType::ENABLED);
+
+  uint32_t key_id_4 = 947327;
+  KeysetInfo::KeyInfo key_4;
+  key_4.set_output_prefix_type(OutputPrefixType::RAW);
+  key_4.set_key_id(key_id_4);
+  key_4.set_status(KeyStatusType::ENABLED);
+
+  uint32_t key_id_5 = 529472;
+  KeysetInfo::KeyInfo key_5;
+  key_5.set_output_prefix_type(OutputPrefixType::RAW);
+  key_5.set_key_id(key_id_5);
+  key_5.set_status(KeyStatusType::ENABLED);
+
+  uint32_t key_id_6 = key_id_1;  // same id as key_1
   KeysetInfo::KeyInfo key_6;
   key_6.set_output_prefix_type(OutputPrefixType::TINK);
   key_6.set_key_id(key_id_6);
@@ -260,7 +683,7 @@
   }
 }
 
-TEST_F(PrimitiveSetTest, PrimaryKeyWithIdCollisions) {
+TEST_F(PrimitiveSetTest, LegacyPrimaryKeyWithIdCollisions) {
   std::string mac_name_1 = "MAC#1";
   std::string mac_name_2 = "MAC#2";
 
@@ -269,7 +692,7 @@
   key_info_1.set_key_id(key_id_1);
   key_info_1.set_status(KeyStatusType::ENABLED);
 
-  uint32_t key_id_2 = key_id_1;    // same id as key_2
+  uint32_t key_id_2 = key_id_1;  // same id as key_2
   KeysetInfo::KeyInfo key_info_2;
   key_info_2.set_key_id(key_id_2);
   key_info_2.set_status(KeyStatusType::ENABLED);
@@ -362,7 +785,7 @@
   }
 }
 
-TEST_F(PrimitiveSetTest, DisabledKey) {
+TEST_F(PrimitiveSetTest, LegacyDisabledKey) {
   std::string mac_name_1 = "MAC#1";
   std::unique_ptr<Mac> mac_1(new DummyMac(mac_name_1));
 
@@ -379,32 +802,7 @@
   EXPECT_FALSE(add_primitive_result.ok());
 }
 
-KeysetInfo::KeyInfo CreateKey(uint32_t key_id,
-                              OutputPrefixType output_prefix_type,
-                              KeyStatusType key_status,
-                              absl::string_view type_url) {
-  KeysetInfo::KeyInfo key_info;
-  key_info.set_output_prefix_type(output_prefix_type);
-  key_info.set_key_id(key_id);
-  key_info.set_status(key_status);
-  std::string type_url_str(type_url);
-  key_info.set_type_url(type_url_str);
-  return key_info;
-}
-
-// Struct to hold MAC, Id and type_url.
-struct MacIdAndTypeUrl {
-  std::string mac;
-  std::string id;
-  std::string type_url;
-};
-
-bool operator==(const MacIdAndTypeUrl& first, const MacIdAndTypeUrl& other) {
-  return first.mac == other.mac && first.id == other.id &&
-         first.type_url == other.type_url;
-}
-
-TEST_F(PrimitiveSetTest, GetAll) {
+TEST_F(PrimitiveSetTest, LegacyGetAll) {
   PrimitiveSet<Mac> pset;
   EXPECT_THAT(
       pset.AddPrimitive(
diff --git a/cc/core/private_key_manager_impl.h b/cc/core/private_key_manager_impl.h
index 5d2d6ae..63a3e93 100644
--- a/cc/core/private_key_manager_impl.h
+++ b/cc/core/private_key_manager_impl.h
@@ -16,6 +16,7 @@
 #ifndef TINK_CORE_PRIVATE_KEY_MANAGER_IMPL_H_
 #define TINK_CORE_PRIVATE_KEY_MANAGER_IMPL_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/core/private_key_manager_impl_test.cc b/cc/core/private_key_manager_impl_test.cc
index 512531e..a874884 100644
--- a/cc/core/private_key_manager_impl_test.cc
+++ b/cc/core/private_key_manager_impl_test.cc
@@ -43,17 +43,22 @@
 using ::google::crypto::tink::EcdsaPrivateKey;
 using ::google::crypto::tink::EcdsaPublicKey;
 using ::google::crypto::tink::EcdsaSignatureEncoding;
-using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Return;
 
+}  // namespace
+
 // Placeholders for the primitives. We don't really want to test anything with
 // these except that things compile and List<PrivatePrimitive> is never confused
 // with List<PublicPrimitive> in private_key_manager_impl.
+// NOTE: These are outside of the anonymous namespace to allow compiling with
+// MSVC.
 class PrivatePrimitive {};
 class PublicPrimitive {};
 
+namespace {
+
 class ExamplePrivateKeyTypeManager
     : public PrivateKeyTypeManager<EcdsaPrivateKey, EcdsaKeyFormat,
                                    EcdsaPublicKey, List<PrivatePrimitive>> {
diff --git a/cc/core/restricted_data.cc b/cc/core/restricted_data.cc
new file mode 100644
index 0000000..4f66030
--- /dev/null
+++ b/cc/core/restricted_data.cc
@@ -0,0 +1,45 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/restricted_data.h"
+
+#include <iostream>
+
+#include "absl/log/check.h"
+#include "openssl/crypto.h"
+#include "tink/subtle/random.h"
+#include "tink/util/secret_data.h"
+
+namespace crypto {
+namespace tink {
+
+RestrictedData::RestrictedData(int64_t num_random_bytes) {
+  CHECK_GE(num_random_bytes, 0)
+      << "Cannot generate a negative number of random bytes.\n";
+  secret_ = util::SecretDataFromStringView(
+      subtle::Random::GetRandomBytes(num_random_bytes));
+}
+
+bool RestrictedData::operator==(const RestrictedData& other) const {
+  if (secret_.size() != other.secret_.size()) {
+    return false;
+  }
+  return CRYPTO_memcmp(secret_.data(), other.secret_.data(), secret_.size()) ==
+         0;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/core/restricted_data_test.cc b/cc/core/restricted_data_test.cc
new file mode 100644
index 0000000..4012563
--- /dev/null
+++ b/cc/core/restricted_data_test.cc
@@ -0,0 +1,117 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/restricted_data.h"
+
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/subtle/random.h"
+#include "tink/util/secret_data.h"
+
+namespace crypto {
+namespace tink {
+
+using ::crypto::tink::subtle::Random;
+using ::testing::Eq;
+using ::testing::SizeIs;
+
+TEST(RestrictedDataTest, CreateAndGetSecret) {
+  const std::string secret = Random::GetRandomBytes(32);
+  RestrictedData data(secret, InsecureSecretKeyAccess::Get());
+
+  EXPECT_THAT(data.GetSecret(InsecureSecretKeyAccess::Get()), Eq(secret));
+}
+
+TEST(RestrictedDataTest, GenerateRandomAndSize) {
+  RestrictedData data(/*num_random_bytes=*/32);
+
+  EXPECT_THAT(data.GetSecret(InsecureSecretKeyAccess::Get()), SizeIs(32));
+  EXPECT_THAT(data.size(), Eq(32));
+}
+
+TEST(RestrictedDataTest, GenerateRandomNegative) {
+  EXPECT_DEATH_IF_SUPPORTED(
+      RestrictedData(/*num_random_bytes=*/-1),
+      "Cannot generate a negative number of random bytes.\n");
+}
+
+TEST(RestrictedDataTest, Equals) {
+  const std::string secret = Random::GetRandomBytes(32);
+  RestrictedData data(secret, InsecureSecretKeyAccess::Get());
+  RestrictedData same_data(secret, InsecureSecretKeyAccess::Get());
+
+  EXPECT_TRUE(data == same_data);
+  EXPECT_TRUE(same_data == data);
+  EXPECT_FALSE(data != same_data);
+  EXPECT_FALSE(same_data != data);
+}
+
+TEST(RestrictedDataTest, NotEquals) {
+  RestrictedData data(
+      util::SecretDataAsStringView(Random::GetRandomKeyBytes(32)),
+      InsecureSecretKeyAccess::Get());
+  RestrictedData diff_data(
+      util::SecretDataAsStringView(Random::GetRandomKeyBytes(32)),
+      InsecureSecretKeyAccess::Get());
+
+  EXPECT_TRUE(data != diff_data);
+  EXPECT_TRUE(diff_data != data);
+  EXPECT_FALSE(data == diff_data);
+  EXPECT_FALSE(diff_data == data);
+}
+
+TEST(RestrictedDataTest, CopyConstructor) {
+  RestrictedData data(/*num_random_bytes=*/32);
+  RestrictedData copy(data);
+
+  EXPECT_THAT(copy, SizeIs(32));
+  EXPECT_THAT(copy.GetSecret(InsecureSecretKeyAccess::Get()),
+              Eq(data.GetSecret(InsecureSecretKeyAccess::Get())));
+}
+
+TEST(RestrictedDataTest, CopyAssignment) {
+  RestrictedData data(/*num_random_bytes=*/32);
+  RestrictedData copy = data;
+
+  EXPECT_THAT(copy, SizeIs(32));
+  EXPECT_THAT(copy.GetSecret(InsecureSecretKeyAccess::Get()),
+              Eq(copy.GetSecret(InsecureSecretKeyAccess::Get())));
+}
+
+TEST(RestrictedDataTest, MoveConstructor) {
+  const std::string secret = Random::GetRandomBytes(32);
+  RestrictedData data(secret, InsecureSecretKeyAccess::Get());
+  RestrictedData move(std::move(data));
+
+  EXPECT_THAT(move, SizeIs(32));
+  EXPECT_THAT(move.GetSecret(InsecureSecretKeyAccess::Get()), Eq(secret));
+}
+
+TEST(RestrictedDataTest, MoveAssignment) {
+  const std::string secret = Random::GetRandomBytes(32);
+  RestrictedData data(secret, InsecureSecretKeyAccess::Get());
+  RestrictedData move = std::move(data);
+
+  EXPECT_THAT(move, SizeIs(32));
+  EXPECT_THAT(move.GetSecret(InsecureSecretKeyAccess::Get()), Eq(secret));
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/core/version_test.cc b/cc/core/version_test.cc
index d25babd..3a4bac5 100644
--- a/cc/core/version_test.cc
+++ b/cc/core/version_test.cc
@@ -20,12 +20,16 @@
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "tink/internal/util.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-TEST(VersionTest, testVersionFormat) {
+using ::testing::AnyOf;
+using ::testing::MatchesRegex;
+
+TEST(VersionTest, VersionHasCorrectFormat) {
   // The regex represents Semantic Versioning syntax (www.semver.org),
   // i.e. three dot-separated numbers, with an optional suffix
   // that starts with a hyphen, to cover alpha/beta releases and
@@ -33,8 +37,16 @@
   //   1.2.3
   //   1.2.3-beta
   //   1.2.3-RC1
-  std::string version_regex = "[0-9]+[.][0-9]+[.][0-9]+(-[A-Za-z0-9]+)?";
-  EXPECT_THAT(Version::kTinkVersion, testing::MatchesRegex(version_regex));
+  if (crypto::tink::internal::IsWindows()) {
+    // Using the syntax in
+    // https://github.com/google/googletest/blob/main/docs/advanced.md#regular-expression-syntax.
+    EXPECT_THAT(Version::kTinkVersion,
+                AnyOf(MatchesRegex(R"regex(\d+\.\d+\.\d+)regex"),
+                      MatchesRegex(R"regex(\d+\.\d+\.\d+-\w+)regex")));
+  } else {
+    std::string version_regex = "[0-9]+[.][0-9]+[.][0-9]+(-[A-Za-z0-9]+)?";
+    EXPECT_THAT(Version::kTinkVersion, testing::MatchesRegex(version_regex));
+  }
 }
 
 }  // namespace
diff --git a/cc/daead/BUILD.bazel b/cc/daead/BUILD.bazel
index 2a4c933..e6bb7b0 100644
--- a/cc/daead/BUILD.bazel
+++ b/cc/daead/BUILD.bazel
@@ -56,9 +56,9 @@
     visibility = ["//visibility:public"],
     deps = [
         ":aes_siv_key_manager",
+        ":aes_siv_proto_serialization",
         ":deterministic_aead_wrapper",
         "//:registry",
-        "//config:config_util",
         "//config:tink_fips",
         "//proto:config_cc_proto",
         "//util:status",
@@ -97,6 +97,91 @@
     ],
 )
 
+cc_library(
+    name = "failing_daead",
+    srcs = ["failing_daead.cc"],
+    hdrs = ["failing_daead.h"],
+    include_prefix = "tink/daead",
+    deps = [
+        "//:deterministic_aead",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "deterministic_aead_parameters",
+    hdrs = ["deterministic_aead_parameters.h"],
+    include_prefix = "tink/daead",
+    deps = ["//:parameters"],
+)
+
+cc_library(
+    name = "deterministic_aead_key",
+    hdrs = ["deterministic_aead_key.h"],
+    include_prefix = "tink/daead",
+    deps = [
+        ":deterministic_aead_parameters",
+        "//:key",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aes_siv_parameters",
+    srcs = ["aes_siv_parameters.cc"],
+    hdrs = ["aes_siv_parameters.h"],
+    include_prefix = "tink/daead",
+    deps = [
+        ":deterministic_aead_parameters",
+        "//util:statusor",
+    ],
+)
+
+cc_library(
+    name = "aes_siv_key",
+    srcs = ["aes_siv_key.cc"],
+    hdrs = ["aes_siv_key.h"],
+    include_prefix = "tink/daead",
+    deps = [
+        ":aes_siv_parameters",
+        ":deterministic_aead_key",
+        "//:partial_key_access_token",
+        "//:restricted_data",
+        "//subtle:subtle_util",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "aes_siv_proto_serialization",
+    srcs = ["aes_siv_proto_serialization.cc"],
+    hdrs = ["aes_siv_proto_serialization.h"],
+    include_prefix = "tink/daead",
+    deps = [
+        ":aes_siv_key",
+        ":aes_siv_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//internal:key_parser",
+        "//internal:key_serializer",
+        "//internal:mutable_serialization_registry",
+        "//internal:parameters_parser",
+        "//internal:parameters_serializer",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_siv_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
 # tests
 
 cc_test(
@@ -143,14 +228,20 @@
     srcs = ["deterministic_aead_config_test.cc"],
     tags = ["fips"],
     deps = [
+        ":aes_siv_key",
         ":aes_siv_key_manager",
+        ":aes_siv_parameters",
         ":deterministic_aead_config",
         ":deterministic_aead_key_templates",
-        "//:config",
         "//:deterministic_aead",
+        "//:insecure_secret_key_access",
         "//:keyset_handle",
+        "//:partial_key_access",
         "//:registry",
         "//config:tink_fips",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
         "//util:status",
         "//util:test_matchers",
         "//util:test_util",
@@ -195,17 +286,6 @@
     ],
 )
 
-cc_library(
-    name = "failing_daead",
-    srcs = ["failing_daead.cc"],
-    hdrs = ["failing_daead.h"],
-    include_prefix = "tink/daead",
-    deps = [
-        "//:deterministic_aead",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
 cc_test(
     name = "failing_daead_test",
     srcs = ["failing_daead_test.cc"],
@@ -216,3 +296,51 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "aes_siv_parameters_test",
+    srcs = ["aes_siv_parameters_test.cc"],
+    deps = [
+        ":aes_siv_parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_siv_key_test",
+    srcs = ["aes_siv_key_test.cc"],
+    deps = [
+        ":aes_siv_key",
+        ":aes_siv_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_siv_proto_serialization_test",
+    size = "small",
+    srcs = ["aes_siv_proto_serialization_test.cc"],
+    deps = [
+        ":aes_siv_key",
+        ":aes_siv_parameters",
+        ":aes_siv_proto_serialization",
+        "//:insecure_secret_key_access",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_siv_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/daead/CMakeLists.txt b/cc/daead/CMakeLists.txt
index 4876217..95a2b78 100644
--- a/cc/daead/CMakeLists.txt
+++ b/cc/daead/CMakeLists.txt
@@ -53,11 +53,11 @@
     deterministic_aead_config.h
   DEPS
     tink::daead::aes_siv_key_manager
+    tink::daead::aes_siv_proto_serialization
     tink::daead::deterministic_aead_wrapper
     absl::core_headers
     absl::memory
     tink::core::registry
-    tink::config::config_util
     tink::config::tink_fips
     tink::util::status
     tink::proto::config_cc_proto
@@ -90,6 +90,87 @@
     tink::proto::tink_cc_proto
 )
 
+tink_cc_library(
+  NAME failing_daead
+  SRCS
+    failing_daead.cc
+    failing_daead.h
+  DEPS
+    absl::strings
+    tink::core::deterministic_aead
+)
+
+tink_cc_library(
+  NAME deterministic_aead_parameters
+  SRCS
+    deterministic_aead_parameters.h
+  DEPS
+    tink::core::parameters
+)
+
+tink_cc_library(
+  NAME deterministic_aead_key
+  SRCS
+    deterministic_aead_key.h
+  DEPS
+    tink::daead::deterministic_aead_parameters
+    absl::strings
+    tink::core::key
+)
+
+tink_cc_library(
+  NAME aes_siv_parameters
+  SRCS
+    aes_siv_parameters.cc
+    aes_siv_parameters.h
+  DEPS
+    tink::daead::deterministic_aead_parameters
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_siv_key
+  SRCS
+    aes_siv_key.cc
+    aes_siv_key.h
+  DEPS
+    tink::daead::aes_siv_parameters
+    tink::daead::deterministic_aead_key
+    absl::strings
+    absl::optional
+    tink::core::partial_key_access_token
+    tink::core::restricted_data
+    tink::subtle::subtle_util
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_siv_proto_serialization
+  SRCS
+    aes_siv_proto_serialization.cc
+    aes_siv_proto_serialization.h
+  DEPS
+    tink::daead::aes_siv_key
+    tink::daead::aes_siv_parameters
+    absl::status
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::mutable_serialization_registry
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::util::status
+    tink::util::statusor
+    tink::proto::aes_siv_cc_proto
+    tink::proto::tink_cc_proto
+)
+
 # tests
 
 tink_cc_test(
@@ -133,16 +214,22 @@
   SRCS
     deterministic_aead_config_test.cc
   DEPS
+    tink::daead::aes_siv_key
     tink::daead::aes_siv_key_manager
+    tink::daead::aes_siv_parameters
     tink::daead::deterministic_aead_config
     tink::daead::deterministic_aead_key_templates
     gmock
     absl::status
-    tink::core::config
     tink::core::deterministic_aead
+    tink::core::insecure_secret_key_access
     tink::core::keyset_handle
+    tink::core::partial_key_access
     tink::core::registry
     tink::config::tink_fips
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
     tink::util::status
     tink::util::test_matchers
     tink::util::test_util
@@ -182,16 +269,6 @@
     tink::proto::tink_cc_proto
 )
 
-tink_cc_library(
-  NAME failing_daead
-  SRCS
-    failing_daead.cc
-    failing_daead.h
-  DEPS
-    absl::strings
-    tink::core::deterministic_aead
-)
-
 tink_cc_test(
   NAME failing_daead_test
   SRCS
@@ -202,3 +279,50 @@
     absl::status
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME aes_siv_parameters_test
+  SRCS
+    aes_siv_parameters_test.cc
+  DEPS
+    tink::daead::aes_siv_parameters
+    gmock
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_siv_key_test
+  SRCS
+    aes_siv_key_test.cc
+  DEPS
+    tink::daead::aes_siv_key
+    tink::daead::aes_siv_parameters
+    gmock
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_siv_proto_serialization_test
+  SRCS
+    aes_siv_proto_serialization_test.cc
+  DEPS
+    tink::daead::aes_siv_key
+    tink::daead::aes_siv_parameters
+    tink::daead::aes_siv_proto_serialization
+    gmock
+    tink::core::insecure_secret_key_access
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::subtle::random
+    tink::util::test_matchers
+    tink::proto::aes_siv_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/daead/aes_siv_key.cc b/cc/daead/aes_siv_key.cc
new file mode 100644
index 0000000..00582b2
--- /dev/null
+++ b/cc/daead/aes_siv_key.cc
@@ -0,0 +1,107 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/daead/aes_siv_key.h"
+
+#include <string>
+
+#include "absl/strings/escaping.h"
+#include "absl/types/optional.h"
+#include "tink/daead/aes_siv_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/subtle_util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::StatusOr<std::string> ComputeOutputPrefix(
+    const AesSivParameters& parameters, absl::optional<int> id_requirement) {
+  switch (parameters.GetVariant()) {
+    case AesSivParameters::Variant::kNoPrefix:
+      return std::string("");  // Empty prefix.
+    case AesSivParameters::Variant::kCrunchy:
+      if (!id_requirement.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "id requirement must have value with kCrunchy or kLegacy");
+      }
+      return absl::StrCat(absl::HexStringToBytes("00"),
+                          subtle::BigEndian32(*id_requirement));
+    case AesSivParameters::Variant::kTink:
+      if (!id_requirement.has_value()) {
+        return util::Status(absl::StatusCode::kInvalidArgument,
+                            "id requirement must have value with kTink");
+      }
+      return absl::StrCat(absl::HexStringToBytes("01"),
+                          subtle::BigEndian32(*id_requirement));
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat("Invalid variant: ", parameters.GetVariant()));
+  }
+}
+
+}  // namespace
+
+util::StatusOr<AesSivKey> AesSivKey::Create(const AesSivParameters& parameters,
+                                            const RestrictedData& key_bytes,
+                                            absl::optional<int> id_requirement,
+                                            PartialKeyAccessToken token) {
+  if (parameters.KeySizeInBytes() != key_bytes.size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Key size does not match AES-SIV parameters");
+  }
+  if (parameters.HasIdRequirement() && !id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key without ID requirement with parameters with ID "
+        "requirement");
+  }
+  if (!parameters.HasIdRequirement() && id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key with ID requirement with parameters without ID "
+        "requirement");
+  }
+  util::StatusOr<std::string> output_prefix =
+      ComputeOutputPrefix(parameters, id_requirement);
+  if (!output_prefix.ok()) {
+    return output_prefix.status();
+  }
+  return AesSivKey(parameters, key_bytes, id_requirement,
+                   *std::move(output_prefix));
+}
+
+bool AesSivKey::operator==(const Key& other) const {
+  const AesSivKey* that = dynamic_cast<const AesSivKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (GetParameters() != that->GetParameters()) {
+    return false;
+  }
+  if (id_requirement_ != that->id_requirement_) {
+    return false;
+  }
+  return key_bytes_ == that->key_bytes_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/daead/aes_siv_key.h b/cc/daead/aes_siv_key.h
new file mode 100644
index 0000000..8ca060d
--- /dev/null
+++ b/cc/daead/aes_siv_key.h
@@ -0,0 +1,83 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_DAEAD_AES_SIV_KEY_H_
+#define TINK_DAEAD_AES_SIV_KEY_H_
+
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/daead/aes_siv_parameters.h"
+#include "tink/daead/deterministic_aead_key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents a Deterministic AEAD that uses AES-SIV.
+class AesSivKey : public DeterministicAeadKey {
+ public:
+  // Copyable and movable.
+  AesSivKey(const AesSivKey& other) = default;
+  AesSivKey& operator=(const AesSivKey& other) = default;
+  AesSivKey(AesSivKey&& other) = default;
+  AesSivKey& operator=(AesSivKey&& other) = default;
+
+  // Creates a new AES-SIV key.  If the parameters specify a variant that uses
+  // a prefix, then the id is used to compute this prefix.
+  static util::StatusOr<AesSivKey> Create(const AesSivParameters& parameters,
+                                          const RestrictedData& key_bytes,
+                                          absl::optional<int> id_requirement,
+                                          PartialKeyAccessToken token);
+
+  // Returns the underlying AES-SIV key.
+  util::StatusOr<RestrictedData> GetKeyBytes(
+      PartialKeyAccessToken token) const {
+    return key_bytes_;
+  }
+
+  absl::string_view GetOutputPrefix() const override { return output_prefix_; }
+
+  const AesSivParameters& GetParameters() const override { return parameters_; }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  AesSivKey(const AesSivParameters& parameters, const RestrictedData& key_bytes,
+            absl::optional<int> id_requirement, std::string output_prefix)
+      : parameters_(parameters),
+        key_bytes_(key_bytes),
+        id_requirement_(id_requirement),
+        output_prefix_(std::move(output_prefix)) {}
+
+  AesSivParameters parameters_;
+  RestrictedData key_bytes_;
+  absl::optional<int> id_requirement_;
+  std::string output_prefix_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_DAEAD_AES_SIV_KEY_H_
diff --git a/cc/daead/aes_siv_key_manager.h b/cc/daead/aes_siv_key_manager.h
index 4d2429c..29aa509 100644
--- a/cc/daead/aes_siv_key_manager.h
+++ b/cc/daead/aes_siv_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_DAEAD_AES_SIV_KEY_MANAGER_H_
 #define TINK_DAEAD_AES_SIV_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/daead/aes_siv_key_manager_test.cc b/cc/daead/aes_siv_key_manager_test.cc
index f00daec..aebb705 100644
--- a/cc/daead/aes_siv_key_manager_test.cc
+++ b/cc/daead/aes_siv_key_manager_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/daead/aes_siv_key_manager.h"
 
+#include <sstream>
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
diff --git a/cc/daead/aes_siv_key_test.cc b/cc/daead/aes_siv_key_test.cc
new file mode 100644
index 0000000..294525e
--- /dev/null
+++ b/cc/daead/aes_siv_key_test.cc
@@ -0,0 +1,230 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/daead/aes_siv_key.h"
+
+#include <string>
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/types/optional.h"
+#include "tink/daead/aes_siv_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesSivParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using AesSivKeyTest = TestWithParam<std::tuple<int, TestCase>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesSivKeyTestSuite, AesSivKeyTest,
+    Combine(Values(32, 48, 64),
+            Values(TestCase{AesSivParameters::Variant::kTink, 0x02030400,
+                            std::string("\x01\x02\x03\x04\x00", 5)},
+                   TestCase{AesSivParameters::Variant::kCrunchy, 0x01030005,
+                            std::string("\x00\x01\x03\x00\x05", 5)},
+                   TestCase{AesSivParameters::Variant::kNoPrefix, absl::nullopt,
+                            ""})));
+
+TEST_P(AesSivKeyTest, CreateSucceeds) {
+  int key_size;
+  TestCase test_case;
+  std::tie(key_size, test_case) = GetParam();
+
+  util::StatusOr<AesSivParameters> params =
+      AesSivParameters::Create(key_size, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  EXPECT_THAT(key->GetParameters(), Eq(*params));
+  EXPECT_THAT(key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(key->GetOutputPrefix(), Eq(test_case.output_prefix));
+}
+
+TEST(AesSivKeyTest, CreateKeyWithMismatchedKeySizeFails) {
+  // Key size parameter is 64 bytes.
+  util::StatusOr<AesSivParameters> params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  // Key material is 32 bytes (another valid key length).
+  RestrictedData mismatched_secret = RestrictedData(/*num_random_bytes=*/32);
+
+  EXPECT_THAT(AesSivKey::Create(*params, mismatched_secret,
+                                /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesSivKeyTest, CreateKeyWithInvalidIdRequirementFails) {
+  util::StatusOr<AesSivParameters> no_prefix_params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kNoPrefix);
+  ASSERT_THAT(no_prefix_params, IsOk());
+
+  util::StatusOr<AesSivParameters> tink_params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/64);
+
+  EXPECT_THAT(AesSivKey::Create(*no_prefix_params, secret,
+                                /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(
+      AesSivKey::Create(*tink_params, secret,
+                        /*id_requirement=*/absl::nullopt, GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesSivKeyTest, GetKeyBytes) {
+  int key_size;
+  TestCase test_case;
+  std::tie(key_size, test_case) = GetParam();
+
+  util::StatusOr<AesSivParameters> params =
+      AesSivParameters::Create(key_size, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT(key->GetKeyBytes(GetPartialKeyAccess()), IsOkAndHolds(secret));
+}
+
+TEST_P(AesSivKeyTest, KeyEquals) {
+  int key_size;
+  TestCase test_case;
+  std::tie(key_size, test_case) = GetParam();
+
+  util::StatusOr<AesSivParameters> params =
+      AesSivParameters::Create(key_size, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesSivKey> other_key = AesSivKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key == *other_key);
+  EXPECT_TRUE(*other_key == *key);
+  EXPECT_FALSE(*key != *other_key);
+  EXPECT_FALSE(*other_key != *key);
+}
+
+TEST(AesSivKeyTest, DifferentVariantNotEqual) {
+  util::StatusOr<AesSivParameters> crunchy_params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kCrunchy);
+  ASSERT_THAT(crunchy_params, IsOk());
+
+  util::StatusOr<AesSivParameters> tink_params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/64);
+
+  util::StatusOr<AesSivKey> key =
+      AesSivKey::Create(*crunchy_params, secret, /*id_requirement=*/0x01020304,
+                        GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesSivKey> other_key =
+      AesSivKey::Create(*tink_params, secret, /*id_requirement=*/0x01020304,
+                        GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesSivKeyTest, DifferentSecretDataNotEqual) {
+  util::StatusOr<AesSivParameters> params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret1 = RestrictedData(/*num_random_bytes=*/64);
+  RestrictedData secret2 = RestrictedData(/*num_random_bytes=*/64);
+
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *params, secret1, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesSivKey> other_key = AesSivKey::Create(
+      *params, secret2, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesSivKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<AesSivParameters> params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/64);
+
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *params, secret, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesSivKey> other_key = AesSivKey::Create(
+      *params, secret, /*id_requirement=*/0x02030405, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/daead/aes_siv_parameters.cc b/cc/daead/aes_siv_parameters.cc
new file mode 100644
index 0000000..996be60
--- /dev/null
+++ b/cc/daead/aes_siv_parameters.cc
@@ -0,0 +1,58 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/daead/aes_siv_parameters.h"
+
+#include <set>
+
+namespace crypto {
+namespace tink {
+
+util::StatusOr<AesSivParameters> AesSivParameters::Create(int key_size_in_bytes,
+                                                          Variant variant) {
+  if (key_size_in_bytes != 32 && key_size_in_bytes != 48 &&
+      key_size_in_bytes != 64) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Key size should be 32, 48, or 64 bytes, got ",
+                     key_size_in_bytes, " bytes."));
+  }
+  static const std::set<Variant>* supported_variants = new std::set<Variant>(
+      {Variant::kTink, Variant::kCrunchy, Variant::kNoPrefix});
+  if (supported_variants->find(variant) == supported_variants->end()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create AES-SIV parameters with unknown variant.");
+  }
+  return AesSivParameters(key_size_in_bytes, variant);
+}
+
+bool AesSivParameters::operator==(const Parameters& other) const {
+  const AesSivParameters* that = dynamic_cast<const AesSivParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (key_size_in_bytes_ != that->key_size_in_bytes_) {
+    return false;
+  }
+  if (variant_ != that->variant_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/daead/aes_siv_parameters.h b/cc/daead/aes_siv_parameters.h
new file mode 100644
index 0000000..3d828b9
--- /dev/null
+++ b/cc/daead/aes_siv_parameters.h
@@ -0,0 +1,73 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_DAEAD_AES_SIV_PARAMETERS_H_
+#define TINK_DAEAD_AES_SIV_PARAMETERS_H_
+
+#include "tink/daead/deterministic_aead_parameters.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes the parameters of an `AesSivKey`.
+class AesSivParameters : public DeterministicAeadParameters {
+ public:
+  // Description of the output prefix prepended to the ciphertext.
+  enum class Variant : int {
+    // Prepends '0x01<big endian key id>' to the ciphertext.
+    kTink = 1,
+    // Prepends '0x00<big endian key id>' to the ciphertext.
+    kCrunchy = 2,
+    // Does not prepend any prefix (i.e., keys must have no ID requirement).
+    kNoPrefix = 3,
+    // Added to guard from failures that may be caused by future expansions.
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Copyable and movable.
+  AesSivParameters(const AesSivParameters& other) = default;
+  AesSivParameters& operator=(const AesSivParameters& other) = default;
+  AesSivParameters(AesSivParameters&& other) = default;
+  AesSivParameters& operator=(AesSivParameters&& other) = default;
+
+  // Creates `AesSivParameters` object from `key_size_in_bytes` and `variant`.
+  // Only allows 32-, 48-, and 64-byte key sizes as specified in RFC 5297.
+  static util::StatusOr<AesSivParameters> Create(int key_size_in_bytes,
+                                                 Variant variant);
+
+  int KeySizeInBytes() const { return key_size_in_bytes_; }
+
+  Variant GetVariant() const { return variant_; }
+
+  bool HasIdRequirement() const override {
+    return variant_ != Variant::kNoPrefix;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  AesSivParameters(int key_size_in_bytes, Variant variant)
+      : key_size_in_bytes_(key_size_in_bytes), variant_(variant) {}
+
+  int key_size_in_bytes_;
+  Variant variant_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_DAEAD_AES_SIV_PARAMETERS_H_
diff --git a/cc/daead/aes_siv_parameters_test.cc b/cc/daead/aes_siv_parameters_test.cc
new file mode 100644
index 0000000..ac57baf
--- /dev/null
+++ b/cc/daead/aes_siv_parameters_test.cc
@@ -0,0 +1,182 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/daead/aes_siv_parameters.h"
+
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct CreateTestCase {
+  AesSivParameters::Variant variant;
+  int key_size;
+  bool has_id_requirement;
+};
+
+using AesSivParametersBuildTest = TestWithParam<CreateTestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesSivParametersBuildTestSuite, AesSivParametersBuildTest,
+    Values(CreateTestCase{AesSivParameters::Variant::kTink, /*key_size=*/32,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{AesSivParameters::Variant::kCrunchy, /*key_size=*/48,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{AesSivParameters::Variant::kNoPrefix, /*key_size=*/64,
+                          /*has_id_requirement=*/false}));
+
+TEST_P(AesSivParametersBuildTest, Create) {
+  CreateTestCase test_case = GetParam();
+
+  util::StatusOr<AesSivParameters> parameters =
+      AesSivParameters::Create(test_case.key_size, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  EXPECT_THAT(parameters->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(parameters->HasIdRequirement(), Eq(test_case.has_id_requirement));
+}
+
+TEST(AesSivParametersTest, CreateWithInvalidVariantFails) {
+  EXPECT_THAT(AesSivParameters::Create(
+                  /*key_size_in_bytes=*/64,
+                  AesSivParameters::Variant::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesSivParametersTest, CreateWithInvalidKeySizeFails) {
+  EXPECT_THAT(AesSivParameters::Create(/*key_size_in_bytes=*/31,
+                                       AesSivParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesSivParameters::Create(/*key_size_in_bytes=*/33,
+                                       AesSivParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesSivParameters::Create(/*key_size_in_bytes=*/47,
+                                       AesSivParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesSivParameters::Create(/*key_size_in_bytes=*/49,
+                                       AesSivParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesSivParameters::Create(/*key_size_in_bytes=*/63,
+                                       AesSivParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesSivParameters::Create(/*key_size_in_bytes=*/65,
+                                       AesSivParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesSivParametersTest, CopyConstructor) {
+  util::StatusOr<AesSivParameters> parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  AesSivParameters copy(*parameters);
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(64));
+  EXPECT_THAT(copy.GetVariant(), Eq(AesSivParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+TEST(AesSivParametersTest, CopyAssignment) {
+  util::StatusOr<AesSivParameters> parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  AesSivParameters copy = *parameters;
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(64));
+  EXPECT_THAT(copy.GetVariant(), Eq(AesSivParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+using AesSivParametersVariantTest =
+    TestWithParam<std::tuple<int, AesSivParameters::Variant>>;
+
+INSTANTIATE_TEST_SUITE_P(AesSivParametersVariantTestSuite,
+                         AesSivParametersVariantTest,
+                         Combine(Values(32, 48, 64),
+                                 Values(AesSivParameters::Variant::kTink,
+                                        AesSivParameters::Variant::kCrunchy,
+                                        AesSivParameters::Variant::kNoPrefix)));
+
+TEST_P(AesSivParametersVariantTest, ParametersEquals) {
+  int key_size;
+  AesSivParameters::Variant variant;
+  std::tie(key_size, variant) = GetParam();
+
+  util::StatusOr<AesSivParameters> parameters =
+      AesSivParameters::Create(key_size, variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesSivParameters> other_parameters =
+      AesSivParameters::Create(key_size, variant);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters == *other_parameters);
+  EXPECT_TRUE(*other_parameters == *parameters);
+  EXPECT_FALSE(*parameters != *other_parameters);
+  EXPECT_FALSE(*other_parameters != *parameters);
+}
+
+TEST(AesSivParametersTest, KeySizeNotEqual) {
+  util::StatusOr<AesSivParameters> parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/48, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesSivParameters> other_parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesSivParametersTest, VariantNotEqual) {
+  util::StatusOr<AesSivParameters> parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesSivParameters> other_parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kNoPrefix);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/daead/aes_siv_proto_serialization.cc b/cc/daead/aes_siv_proto_serialization.cc
new file mode 100644
index 0000000..c1711c5
--- /dev/null
+++ b/cc/daead/aes_siv_proto_serialization.cc
@@ -0,0 +1,237 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/daead/aes_siv_proto_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/daead/aes_siv_key.h"
+#include "tink/daead/aes_siv_parameters.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/aes_siv.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::AesSivKeyFormat;
+using ::google::crypto::tink::OutputPrefixType;
+
+using AesSivProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   AesSivParameters>;
+using AesSivProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<AesSivParameters,
+                                       internal::ProtoParametersSerialization>;
+using AesSivProtoKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, AesSivKey>;
+using AesSivProtoKeySerializerImpl =
+    internal::KeySerializerImpl<AesSivKey, internal::ProtoKeySerialization>;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.AesSivKey";
+
+util::StatusOr<AesSivParameters::Variant> ToVariant(
+    OutputPrefixType output_prefix_type) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::LEGACY:
+      ABSL_FALLTHROUGH_INTENDED;  // Parse LEGACY output prefix as CRUNCHY.
+    case OutputPrefixType::CRUNCHY:
+      return AesSivParameters::Variant::kCrunchy;
+    case OutputPrefixType::RAW:
+      return AesSivParameters::Variant::kNoPrefix;
+    case OutputPrefixType::TINK:
+      return AesSivParameters::Variant::kTink;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine AesSivParameters::Variant");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    AesSivParameters::Variant variant) {
+  switch (variant) {
+    case AesSivParameters::Variant::kCrunchy:
+      return OutputPrefixType::CRUNCHY;
+    case AesSivParameters::Variant::kNoPrefix:
+      return OutputPrefixType::RAW;
+    case AesSivParameters::Variant::kTink:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine output prefix type");
+  }
+}
+
+util::StatusOr<AesSivParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesSivParameters.");
+  }
+
+  AesSivKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesSivKeyFormat proto");
+  }
+  if (proto_key_format.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesSivParameters::Variant> variant =
+      ToVariant(serialization.GetKeyTemplate().output_prefix_type());
+  if (!variant.ok()) return variant.status();
+
+  return AesSivParameters::Create(proto_key_format.key_size(), *variant);
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const AesSivParameters& parameters) {
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  AesSivKeyFormat proto_key_format;
+  proto_key_format.set_key_size(parameters.KeySizeInBytes());
+
+  return internal::ProtoParametersSerialization::Create(
+      kTypeUrl, *output_prefix_type, proto_key_format.SerializeAsString());
+}
+
+util::StatusOr<AesSivKey> ParseKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesSivKey.");
+  }
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+  google::crypto::tink::AesSivKey proto_key;
+  RestrictedData restricted_data = serialization.SerializedKeyProto();
+  // OSS proto library complains if input is not converted to a string.
+  if (!proto_key.ParseFromString(
+          std::string(restricted_data.GetSecret(*token)))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesSivKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesSivParameters::Variant> variant =
+      ToVariant(serialization.GetOutputPrefixType());
+  if (!variant.ok()) return variant.status();
+
+  util::StatusOr<AesSivParameters> parameters =
+      AesSivParameters::Create(proto_key.key_value().length(), *variant);
+  if (!parameters.ok()) return parameters.status();
+
+  return AesSivKey::Create(
+      *parameters, RestrictedData(proto_key.key_value(), *token),
+      serialization.IdRequirement(), GetPartialKeyAccess());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializeKey(
+    const AesSivKey& key, absl::optional<SecretKeyAccessToken> token) {
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetKeyBytes(GetPartialKeyAccess());
+  if (!restricted_input.ok()) return restricted_input.status();
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+
+  google::crypto::tink::AesSivKey proto_key;
+  proto_key.set_version(0);
+  // OSS proto library complains if input is not converted to a string.
+  proto_key.set_key_value(std::string(restricted_input->GetSecret(*token)));
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetParameters().GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  RestrictedData restricted_output =
+      RestrictedData(proto_key.SerializeAsString(), *token);
+  return internal::ProtoKeySerialization::Create(
+      kTypeUrl, restricted_output, google::crypto::tink::KeyData::SYMMETRIC,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+AesSivProtoParametersParserImpl* AesSivProtoParametersParser() {
+  static auto* parser =
+      new AesSivProtoParametersParserImpl(kTypeUrl, ParseParameters);
+  return parser;
+}
+
+AesSivProtoParametersSerializerImpl* AesSivProtoParametersSerializer() {
+  static auto* serializer =
+      new AesSivProtoParametersSerializerImpl(kTypeUrl, SerializeParameters);
+  return serializer;
+}
+
+AesSivProtoKeyParserImpl* AesSivProtoKeyParser() {
+  static auto* parser = new AesSivProtoKeyParserImpl(kTypeUrl, ParseKey);
+  return parser;
+}
+
+AesSivProtoKeySerializerImpl* AesSivProtoKeySerializer() {
+  static auto* serializer = new AesSivProtoKeySerializerImpl(SerializeKey);
+  return serializer;
+}
+
+}  // namespace
+
+util::Status RegisterAesSivProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(AesSivProtoParametersParser());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterParametersSerializer(AesSivProtoParametersSerializer());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(AesSivProtoKeyParser());
+  if (!status.ok()) return status;
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(AesSivProtoKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/daead/aes_siv_proto_serialization.h b/cc/daead/aes_siv_proto_serialization.h
new file mode 100644
index 0000000..daff867
--- /dev/null
+++ b/cc/daead/aes_siv_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_DAEAD_AES_SIV_PROTO_SERIALIZATION_H_
+#define TINK_DAEAD_AES_SIV_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for AES-SIV parameters and keys.
+crypto::tink::util::Status RegisterAesSivProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_DAEAD_AES_SIV_PROTO_SERIALIZATION_H_
diff --git a/cc/daead/aes_siv_proto_serialization_test.cc b/cc/daead/aes_siv_proto_serialization_test.cc
new file mode 100644
index 0000000..1f6a45a
--- /dev/null
+++ b/cc/daead/aes_siv_proto_serialization_test.cc
@@ -0,0 +1,395 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/daead/aes_siv_proto_serialization.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/daead/aes_siv_key.h"
+#include "tink/daead/aes_siv_parameters.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
+#include "proto/aes_siv.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::subtle::Random;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesSivKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesSivParameters::Variant variant;
+  OutputPrefixType output_prefix_type;
+  int key_size;
+  absl::optional<int> id;
+  std::string output_prefix;
+};
+
+class AesSivProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  void SetUp() override {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    AesSivProtoSerializationTestSuite, AesSivProtoSerializationTest,
+    Values(TestCase{AesSivParameters::Variant::kTink, OutputPrefixType::TINK,
+                    /*key_size=*/32, /*id=*/0x02030400,
+                    /*output_prefix=*/std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{AesSivParameters::Variant::kCrunchy,
+                    OutputPrefixType::CRUNCHY, /*key_size=*/48,
+                    /*id=*/0x01030005,
+                    /*output_prefix=*/std::string("\x00\x01\x03\x00\x05", 5)},
+           TestCase{AesSivParameters::Variant::kNoPrefix, OutputPrefixType::RAW,
+                    /*key_size=*/64, /*id=*/absl::nullopt,
+                    /*output_prefix=*/""}));
+
+TEST_P(AesSivProtoSerializationTest, ParseParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  AesSivKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(test_case.key_size);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey",
+          test_case.output_prefix_type, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), test_case.id.has_value());
+
+  const AesSivParameters* siv_params =
+      dynamic_cast<const AesSivParameters*>(params->get());
+  ASSERT_THAT(siv_params, NotNull());
+  EXPECT_THAT(siv_params->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(siv_params->KeySizeInBytes(), Eq(test_case.key_size));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseParametersWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  AesSivKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey",
+          OutputPrefixType::RAW, "invalid_serialization");
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseParametersWithUnkownOutputPrefix) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  AesSivKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey",
+          OutputPrefixType::UNKNOWN_PREFIX,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseParametersWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  AesSivKeyFormat key_format_proto;
+  key_format_proto.set_version(1);
+  key_format_proto.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey",
+          OutputPrefixType::RAW, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesSivProtoSerializationTest, SerializeParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  util::StatusOr<AesSivParameters> parameters =
+      AesSivParameters::Create(test_case.key_size, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesSivKey"));
+
+  const internal::ProtoParametersSerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoParametersSerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().type_url(),
+              Eq("type.googleapis.com/google.crypto.tink.AesSivKey"));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  AesSivKeyFormat key_format;
+  ASSERT_THAT(
+      key_format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  EXPECT_THAT(key_format.key_size(), Eq(test_case.key_size));
+}
+
+TEST_P(AesSivProtoSerializationTest, ParseKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  google::crypto::tink::AesSivKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey", serialized_key,
+          KeyData::SYMMETRIC, test_case.output_prefix_type, test_case.id);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key)->GetIdRequirement(), Eq(test_case.id));
+  EXPECT_THAT((*key)->GetParameters().HasIdRequirement(),
+              test_case.id.has_value());
+
+  util::StatusOr<AesSivParameters> expected_parameters =
+      AesSivParameters::Create(test_case.key_size, test_case.variant);
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  util::StatusOr<AesSivKey> expected_key = AesSivKey::Create(
+      *expected_parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(expected_key, IsOk());
+
+  EXPECT_THAT(**key, Eq(*expected_key));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseLegacyKeyAsCrunchy) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(64);
+  google::crypto::tink::AesSivKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::LEGACY, /*id_requirement=*/123);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+
+  const AesSivKey* aes_siv_key = dynamic_cast<const AesSivKey*>(key->get());
+  ASSERT_THAT(aes_siv_key, NotNull());
+  EXPECT_THAT(aes_siv_key->GetParameters().GetVariant(),
+              Eq(AesSivParameters::Variant::kCrunchy));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(64);
+  google::crypto::tink::AesSivKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, /*token=*/absl::nullopt);
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesSivProtoSerializationTest, ParseKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(64);
+  google::crypto::tink::AesSivKey key_proto;
+  key_proto.set_version(1);  // Invalid version number.
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesSivProtoSerializationTest, SerializeKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  util::StatusOr<AesSivParameters> parameters =
+      AesSivParameters::Create(test_case.key_size, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesSivKey"));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(),
+              Eq("type.googleapis.com/google.crypto.tink.AesSivKey"));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(), Eq(test_case.id));
+
+  google::crypto::tink::AesSivKey proto_key;
+  // OSS proto library complains if input is not converted to a string.
+  ASSERT_THAT(proto_key.ParseFromString(std::string(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get()))),
+              IsTrue());
+  EXPECT_THAT(proto_key.key_value().size(), Eq(test_case.key_size));
+}
+
+TEST_F(AesSivProtoSerializationTest, SerializeKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesSivProtoSerialization(), IsOk());
+
+  util::StatusOr<AesSivParameters> parameters = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(64);
+  util::StatusOr<AesSivKey> key = AesSivKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(*key, absl::nullopt);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/daead/deterministic_aead_config.cc b/cc/daead/deterministic_aead_config.cc
index 749f8cf..5c1a229 100644
--- a/cc/daead/deterministic_aead_config.cc
+++ b/cc/daead/deterministic_aead_config.cc
@@ -17,26 +17,17 @@
 #include "tink/daead/deterministic_aead_config.h"
 
 #include "absl/memory/memory.h"
-#include "tink/config/config_util.h"
 #include "tink/config/tink_fips.h"
 #include "tink/daead/aes_siv_key_manager.h"
+#include "tink/daead/aes_siv_proto_serialization.h"
 #include "tink/daead/deterministic_aead_wrapper.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
-#include "proto/config.pb.h"
-
-using google::crypto::tink::RegistryConfig;
 
 namespace crypto {
 namespace tink {
 
 // static
-const RegistryConfig& DeterministicAeadConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
-// static
 util::Status DeterministicAeadConfig::Register() {
   // Currently there are no FIPS-validated deterministic AEAD key managers
   // available, therefore none will be registered in FIPS only mode.
@@ -49,6 +40,9 @@
       absl::make_unique<AesSivKeyManager>(), true);
   if (!status.ok()) return status;
 
+  status = RegisterAesSivProtoSerialization();
+  if (!status.ok()) return status;
+
   // Register primitive wrapper.
   return Registry::RegisterPrimitiveWrapper(
       absl::make_unique<DeterministicAeadWrapper>());
diff --git a/cc/daead/deterministic_aead_config.h b/cc/daead/deterministic_aead_config.h
index 1441fdd..3fdd199 100644
--- a/cc/daead/deterministic_aead_config.h
+++ b/cc/daead/deterministic_aead_config.h
@@ -35,14 +35,6 @@
 //
 class DeterministicAeadConfig {
  public:
-  static constexpr char kCatalogueName[] = "TinkDeterministicAead";
-  static constexpr char kPrimitiveName[] = "DeterministicAead";
-
-  // Returns config of DeterministicAead implementations supported
-  // in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers DeterministicAead primitive wrapper and key managers for all
   // DeterministicAead key types from the current Tink release.
   static crypto::tink::util::Status Register();
diff --git a/cc/daead/deterministic_aead_config_test.cc b/cc/daead/deterministic_aead_config_test.cc
index 1a59d14..ba833d1 100644
--- a/cc/daead/deterministic_aead_config_test.cc
+++ b/cc/daead/deterministic_aead_config_test.cc
@@ -17,17 +17,24 @@
 #include "tink/daead/deterministic_aead_config.h"
 
 #include <list>
+#include <memory>
 #include <utility>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/config.h"
 #include "tink/config/tink_fips.h"
+#include "tink/daead/aes_siv_key.h"
 #include "tink/daead/aes_siv_key_manager.h"
+#include "tink/daead/aes_siv_parameters.h"
 #include "tink/daead/deterministic_aead_key_templates.h"
 #include "tink/deterministic_aead.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
 #include "tink/keyset_handle.h"
+#include "tink/partial_key_access.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/test_matchers.h"
@@ -40,11 +47,16 @@
 using ::crypto::tink::test::DummyDeterministicAead;
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
 using ::testing::Eq;
 
 class DeterministicAeadConfigTest : public ::testing::Test {
  protected:
-  void SetUp() override { Registry::Reset(); }
+  void SetUp() override {
+    Registry::Reset();
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
 };
 
 TEST_F(DeterministicAeadConfigTest, Basic) {
@@ -121,6 +133,98 @@
   }
 }
 
+TEST_F(DeterministicAeadConfigTest, AesSivProtoParamsSerializationRegistered) {
+  if (IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  util::StatusOr<internal::ProtoParametersSerialization>
+      proto_params_serialization =
+          internal::ProtoParametersSerialization::Create(
+              DeterministicAeadKeyTemplates::Aes256Siv());
+  ASSERT_THAT(proto_params_serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *proto_params_serialization);
+  ASSERT_THAT(parsed_params.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  util::StatusOr<AesSivParameters> params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_params =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(*params);
+  ASSERT_THAT(serialized_params.status(),
+              StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(DeterministicAeadConfig::Register(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_params2 =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *proto_params_serialization);
+  ASSERT_THAT(parsed_params2, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_params2 =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(*params);
+  ASSERT_THAT(serialized_params2, IsOk());
+}
+
+TEST_F(DeterministicAeadConfigTest, AesSivProtoKeySerializationRegistered) {
+  if (IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  google::crypto::tink::AesSivKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(subtle::Random::GetRandomBytes(64));
+
+  util::StatusOr<internal::ProtoKeySerialization> proto_key_serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesSivKey",
+          RestrictedData(key_proto.SerializeAsString(),
+                         InsecureSecretKeyAccess::Get()),
+          KeyData::SYMMETRIC, OutputPrefixType::TINK, /*id_requirement=*/123);
+  ASSERT_THAT(proto_key_serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *proto_key_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  util::StatusOr<AesSivParameters> params = AesSivParameters::Create(
+      /*key_size_in_bytes=*/64, AesSivParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  util::StatusOr<AesSivKey> key =
+      AesSivKey::Create(*params,
+                        RestrictedData(subtle::Random::GetRandomBytes(64),
+                                       InsecureSecretKeyAccess::Get()),
+                        /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_key =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialized_key.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(DeterministicAeadConfig::Register(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key2 =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *proto_key_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key2, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_key2 =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialized_key2, IsOk());
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/daead/deterministic_aead_factory.cc b/cc/daead/deterministic_aead_factory.cc
index bdd23ed..8ae089a 100644
--- a/cc/daead/deterministic_aead_factory.cc
+++ b/cc/daead/deterministic_aead_factory.cc
@@ -16,6 +16,8 @@
 
 #include "tink/daead/deterministic_aead_factory.h"
 
+#include <memory>
+
 #include "tink/daead/deterministic_aead_wrapper.h"
 #include "tink/deterministic_aead.h"
 #include "tink/key_manager.h"
diff --git a/cc/daead/deterministic_aead_factory.h b/cc/daead/deterministic_aead_factory.h
index 5359a29..dcd365b 100644
--- a/cc/daead/deterministic_aead_factory.h
+++ b/cc/daead/deterministic_aead_factory.h
@@ -17,6 +17,8 @@
 #ifndef TINK_DAEAD_DETERMINISTIC_AEAD_FACTORY_H_
 #define TINK_DAEAD_DETERMINISTIC_AEAD_FACTORY_H_
 
+#include <memory>
+
 #include "absl/base/macros.h"
 #include "tink/deterministic_aead.h"
 #include "tink/key_manager.h"
diff --git a/cc/daead/deterministic_aead_factory_test.cc b/cc/daead/deterministic_aead_factory_test.cc
index 78cc363..ae6ac6d 100644
--- a/cc/daead/deterministic_aead_factory_test.cc
+++ b/cc/daead/deterministic_aead_factory_test.cc
@@ -31,7 +31,6 @@
 #include "tink/util/test_util.h"
 #include "proto/aes_siv.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddRawKey;
 using crypto::tink::test::AddTinkKey;
 using google::crypto::tink::AesSivKeyFormat;
diff --git a/cc/daead/deterministic_aead_key.h b/cc/daead/deterministic_aead_key.h
new file mode 100644
index 0000000..1534754
--- /dev/null
+++ b/cc/daead/deterministic_aead_key.h
@@ -0,0 +1,52 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_DAEAD_DETERMINISTIC_AEAD_KEY_H_
+#define TINK_DAEAD_DETERMINISTIC_AEAD_KEY_H_
+
+#include "absl/strings/string_view.h"
+#include "tink/daead/deterministic_aead_parameters.h"
+#include "tink/key.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents a function to encrypt and decrypt data using deterministic
+// authenticated encryption with associated data (Deterministic AEAD).
+class DeterministicAeadKey : public Key {
+ public:
+  // Returns the bytes prefixed to every ciphertext generated by this key.
+  //
+  // In order to make key rotation more efficient, Tink allows every
+  // Deterministic AEAD key to have an associated ciphertext output prefix. When
+  // decrypting a ciphertext, only keys with a matching prefix have to be tried.
+  //
+  // Note that a priori, the output prefix may not be unique in a keyset
+  // (i.e., different keys in a keyset may have the same prefix or one prefix
+  // may be a prefix of another). To avoid this, built-in Tink keys use the
+  // convention that the prefix is either '0x00<big endian key id>' or
+  // '0x01<big endian key id>'.
+  virtual absl::string_view GetOutputPrefix() const = 0;
+
+  const DeterministicAeadParameters& GetParameters() const override = 0;
+
+  bool operator==(const Key& other) const override = 0;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_DAEAD_DETERMINISTIC_AEAD_KEY_H_
diff --git a/cc/daead/deterministic_aead_parameters.h b/cc/daead/deterministic_aead_parameters.h
new file mode 100644
index 0000000..53b746a
--- /dev/null
+++ b/cc/daead/deterministic_aead_parameters.h
@@ -0,0 +1,32 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_DAEAD_DETERMINISTIC_AEAD_PARAMETERS_H_
+#define TINK_DAEAD_DETERMINISTIC_AEAD_PARAMETERS_H_
+
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes a `DeterministicAeadKey` (e.g., key attributes), excluding the
+// randomly chosen key material.
+class DeterministicAeadParameters : public Parameters {};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_DAEAD_DETERMINISTIC_AEAD_PARAMETERS_H_
diff --git a/cc/daead/deterministic_aead_wrapper.cc b/cc/daead/deterministic_aead_wrapper.cc
index 3db1e99..13cc57f 100644
--- a/cc/daead/deterministic_aead_wrapper.cc
+++ b/cc/daead/deterministic_aead_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/daead/deterministic_aead_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -70,7 +71,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  ~DeterministicAeadSetWrapper() override {}
+  ~DeterministicAeadSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<DeterministicAead>> daead_set_;
diff --git a/cc/daead/deterministic_aead_wrapper.h b/cc/daead/deterministic_aead_wrapper.h
index ea95b94..77dd3d7 100644
--- a/cc/daead/deterministic_aead_wrapper.h
+++ b/cc/daead/deterministic_aead_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_DAEAD_DETERMINISTIC_AEAD_WRAPPER_H_
 #define TINK_DAEAD_DETERMINISTIC_AEAD_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/deterministic_aead.h"
 #include "tink/primitive_set.h"
diff --git a/cc/daead/failing_daead.cc b/cc/daead/failing_daead.cc
index 61b30f9..b3fa26f 100644
--- a/cc/daead/failing_daead.cc
+++ b/cc/daead/failing_daead.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/daead/failing_daead.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/daead/failing_daead.h b/cc/daead/failing_daead.h
index 551ad4b..74c7b50 100644
--- a/cc/daead/failing_daead.h
+++ b/cc/daead/failing_daead.h
@@ -16,6 +16,7 @@
 #ifndef TINK_DAEAD_FAILING_DAEAD_H_
 #define TINK_DAEAD_FAILING_DAEAD_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/daead/subtle/aead_or_daead.cc b/cc/daead/subtle/aead_or_daead.cc
index c8c8b57..419c333 100644
--- a/cc/daead/subtle/aead_or_daead.cc
+++ b/cc/daead/subtle/aead_or_daead.cc
@@ -16,6 +16,7 @@
 
 #include "tink/daead/subtle/aead_or_daead.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/daead/subtle/aead_or_daead_test.cc b/cc/daead/subtle/aead_or_daead_test.cc
index 8244b72..a70e204 100644
--- a/cc/daead/subtle/aead_or_daead_test.cc
+++ b/cc/daead/subtle/aead_or_daead_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/daead/subtle/aead_or_daead.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/deterministic_aead.h b/cc/deterministic_aead.h
index fa73762..310f72f 100644
--- a/cc/deterministic_aead.h
+++ b/cc/deterministic_aead.h
@@ -53,7 +53,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const = 0;
 
-  virtual ~DeterministicAead() {}
+  virtual ~DeterministicAead() = default;
 };
 
 }  // namespace tink
diff --git a/cc/examples/.bazelrc b/cc/examples/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/cc/examples/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/cc/examples/.bazelversion b/cc/examples/.bazelversion
index ac14c3d..09b254e 100644
--- a/cc/examples/.bazelversion
+++ b/cc/examples/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/cc/examples/CMakeLists.txt b/cc/examples/CMakeLists.txt
new file mode 100644
index 0000000..a85f910
--- /dev/null
+++ b/cc/examples/CMakeLists.txt
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.13)
+
+project(Examples CXX)
+
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(CMAKE_BUILD_TYPE Release)
+
+# Import Tink as an in-tree dependency.
+add_subdirectory(../.. tink)
+
+# Make sure we have bash.
+find_program(BASH_PROGRAM bash REQUIRED)
+
+# Include path at the base of the examples folder.
+set(TINK_EXAMPLES_INCLUDE_PATH "${CMAKE_SOURCE_DIR}")
+
+include(FetchContent)
+
+FetchContent_Declare(
+  googletest
+  URL       https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz
+  URL_HASH  SHA256=b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5
+)
+
+FetchContent_GetProperties(googletest)
+if(NOT googletest_POPULATED)
+  FetchContent_Populate(googletest)
+  add_subdirectory(
+    ${googletest_SOURCE_DIR}
+    ${googletest_BINARY_DIR}
+    EXCLUDE_FROM_ALL)
+endif()
+
+enable_testing()
+
+add_subdirectory(aead)
+add_subdirectory(digital_signatures)
+add_subdirectory(hybrid_encryption)
+add_subdirectory(jwt)
+add_subdirectory(mac)
+add_subdirectory(util)
+add_subdirectory(walkthrough)
diff --git a/cc/examples/MODULE.bazel b/cc/examples/MODULE.bazel
new file mode 100644
index 0000000..654c59f
--- /dev/null
+++ b/cc/examples/MODULE.bazel
@@ -0,0 +1,11 @@
+"""Module definition for Tink C++ Examples."""
+# Omitting `version` because this is not meant to be depended on by other
+# modules.
+module(name = "tink_cc_examples")
+
+# Use local tink_cc.
+bazel_dep(name = "tink_cc", version = "")
+local_path_override(module_name = "tink_cc", path = "../")
+
+bazel_dep(name = "googletest", version = "1.12.1", repo_name = "com_google_googletest")
+bazel_dep(name = "abseil-cpp", version = "20230125.1", repo_name="com_google_absl")
diff --git a/cc/examples/WORKSPACE b/cc/examples/WORKSPACE
index b7fff2b..5cdbbc9 100644
--- a/cc/examples/WORKSPACE
+++ b/cc/examples/WORKSPACE
@@ -1,4 +1,4 @@
-workspace(name = "examples_cc")
+workspace(name = "tink_cc_examples")
 
 # The local_repository() rule is used below because the workspaces referenced
 # are all located within the Tink git repository.
diff --git a/cc/examples/WORKSPACE.bzlmod b/cc/examples/WORKSPACE.bzlmod
new file mode 100644
index 0000000..057d6aa
--- /dev/null
+++ b/cc/examples/WORKSPACE.bzlmod
@@ -0,0 +1 @@
+# This replaces the content of the WORKSPACE file when using --enable_bzlmod.
diff --git a/cc/examples/aead/BUILD.bazel b/cc/examples/aead/BUILD.bazel
index 80727bb..fa7907a 100644
--- a/cc/examples/aead/BUILD.bazel
+++ b/cc/examples/aead/BUILD.bazel
@@ -11,14 +11,12 @@
     name = "aead_cli",
     srcs = ["aead_cli.cc"],
     deps = [
+        "//util",
         "@com_google_absl//absl/flags:flag",
         "@com_google_absl//absl/flags:parse",
-        "@com_google_absl//absl/memory",
-        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/strings",
         "@tink_cc//:aead",
-        "@tink_cc//:cleartext_keyset_handle",
-        "@tink_cc//:json_keyset_reader",
         "@tink_cc//:keyset_handle",
         "@tink_cc//:keyset_reader",
         "@tink_cc//aead:aead_config",
diff --git a/cc/examples/aead/CMakeLists.txt b/cc/examples/aead/CMakeLists.txt
new file mode 100644
index 0000000..ae04ba6
--- /dev/null
+++ b/cc/examples/aead/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_executable(aead_cli aead_cli.cc)
+target_include_directories(aead_cli PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(aead_cli
+  tink::static
+  absl::check
+  absl::flags_parse
+  util)
+
+add_test(
+  NAME aead_cli_test
+  COMMAND "${BASH_PROGRAM}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/aead_cli_test.sh"
+    "${CMAKE_CURRENT_BINARY_DIR}/aead_cli"
+    "${CMAKE_CURRENT_SOURCE_DIR}/aead_test_keyset.json")
diff --git a/cc/examples/aead/aead_cli.cc b/cc/examples/aead/aead_cli.cc
index 028f9a1..36e3225 100644
--- a/cc/examples/aead/aead_cli.cc
+++ b/cc/examples/aead/aead_cli.cc
@@ -15,184 +15,117 @@
 ///////////////////////////////////////////////////////////////////////////////
 // [START aead-example]
 // A command-line utility for testing Tink AEAD.
-#include <fstream>
 #include <iostream>
 #include <memory>
-#include <sstream>
+#include <ostream>
 #include <string>
-#include <utility>
 
 #include "absl/flags/flag.h"
 #include "absl/flags/parse.h"
-#include "absl/memory/memory.h"
-#include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
+#include "absl/log/check.h"
 #include "absl/strings/string_view.h"
 #include "tink/aead.h"
 #include "tink/aead/aead_config.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "tink/json_keyset_reader.h"
+#include "util/util.h"
 #include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
 #include "tink/util/status.h"
 
 ABSL_FLAG(std::string, keyset_filename, "", "Keyset file in JSON format");
 ABSL_FLAG(std::string, mode, "", "Mode of operation {encrypt|decrypt}");
 ABSL_FLAG(std::string, input_filename, "", "Filename to operate on");
 ABSL_FLAG(std::string, output_filename, "", "Output file name");
-ABSL_FLAG(std::string, associated_data, "", "Associated data for AEAD");
+ABSL_FLAG(std::string, associated_data, "",
+          "Associated data for AEAD (default: empty");
 
 namespace {
 
 using ::crypto::tink::Aead;
 using ::crypto::tink::AeadConfig;
-using ::crypto::tink::CleartextKeysetHandle;
-using ::crypto::tink::JsonKeysetReader;
 using ::crypto::tink::KeysetHandle;
-using ::crypto::tink::KeysetReader;
 using ::crypto::tink::util::Status;
 using ::crypto::tink::util::StatusOr;
 
 constexpr absl::string_view kEncrypt = "encrypt";
 constexpr absl::string_view kDecrypt = "decrypt";
 
-// Creates a KeysetReader that reads a JSON-formatted keyset
-// from the given file.
-StatusOr<std::unique_ptr<KeysetReader>> GetJsonKeysetReader(
-    const std::string& filename) {
-  std::clog << "Creating a JsonKeysetReader...\n";
-  auto key_input_stream = absl::make_unique<std::ifstream>();
-  key_input_stream->open(filename, std::ifstream::in);
-  return JsonKeysetReader::New(std::move(key_input_stream));
-}
-
-// Creates a KeysetHandle that for a keyset read from the given file,
-// which is expected to contain a JSON-formatted keyset.
-StatusOr<std::unique_ptr<KeysetHandle>> ReadKeyset(
-    const std::string& filename) {
-  StatusOr<std::unique_ptr<KeysetReader>> keyset_reader =
-      GetJsonKeysetReader(filename);
-  if (!keyset_reader.ok()) {
-    return keyset_reader.status();
-  }
-  return CleartextKeysetHandle::Read(*std::move(keyset_reader));
-}
-
-// Reads `filename` and returns the read content as a string, or an error status
-// if the file does not exist.
-StatusOr<std::string> Read(const std::string& filename) {
-  std::clog << "Reading the input...\n";
-  std::ifstream input_stream;
-  input_stream.open(filename, std::ifstream::in);
-  if (!input_stream.is_open()) {
-    return Status(absl::StatusCode::kInternal,
-                  absl::StrCat("Error opening input file ", filename));
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  return input.str();
-}
-
-// Writes the given `data_to_write` to the specified file `filename`.
-Status Write(const std::string& data_to_write, const std::string& filename) {
-  std::clog << "Writing the output...\n";
-  std::ofstream output_stream(filename,
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    return Status(absl::StatusCode::kInternal,
-                  absl::StrCat("Error opening output file ", filename));
-  }
-  output_stream << data_to_write;
-  return crypto::tink::util::OkStatus();
+void ValidateParams() {
+  // [START_EXCLUDE]
+  CHECK(absl::GetFlag(FLAGS_mode) == kEncrypt ||
+        absl::GetFlag(FLAGS_mode) == kDecrypt)
+      << "Invalid mode; must be `encrypt` or `decrypt`";
+  CHECK(!absl::GetFlag(FLAGS_keyset_filename).empty())
+      << "Keyset file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_input_filename).empty())
+      << "Input file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_output_filename).empty())
+      << "Output file must be specified";
+  // [END_EXCLUDE]
 }
 
 }  // namespace
 
+namespace tink_cc_examples {
+
+// AEAD example CLI implementation.
+Status AeadCli(absl::string_view mode, const std::string& keyset_filename,
+               const std::string& input_filename,
+               const std::string& output_filename,
+               absl::string_view associated_data) {
+  Status result = AeadConfig::Register();
+  if (!result.ok()) return result;
+
+  // Read the keyset from file.
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      ReadJsonCleartextKeyset(keyset_filename);
+  if (!keyset_handle.ok()) return keyset_handle.status();
+
+  // Get the primitive.
+  StatusOr<std::unique_ptr<Aead>> aead = (*keyset_handle)->GetPrimitive<Aead>();
+  if (!aead.ok()) return aead.status();
+
+  // Read the input.
+  StatusOr<std::string> input_file_content = ReadFile(input_filename);
+  if (!input_file_content.ok()) return input_file_content.status();
+
+  // Compute the output.
+  std::string output;
+  if (mode == kEncrypt) {
+    StatusOr<std::string> encrypt_result =
+        (*aead)->Encrypt(*input_file_content, associated_data);
+    if (!encrypt_result.ok()) return encrypt_result.status();
+    output = encrypt_result.value();
+  } else {  // operation == kDecrypt.
+    StatusOr<std::string> decrypt_result =
+        (*aead)->Decrypt(*input_file_content, associated_data);
+    if (!decrypt_result.ok()) return decrypt_result.status();
+    output = decrypt_result.value();
+  }
+
+  // Write the output to the output file.
+  return WriteToFile(output, output_filename);
+}
+
+}  // namespace tink_cc_examples
+
 int main(int argc, char** argv) {
   absl::ParseCommandLine(argc, argv);
 
+  ValidateParams();
+
   std::string mode = absl::GetFlag(FLAGS_mode);
   std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
   std::string input_filename = absl::GetFlag(FLAGS_input_filename);
   std::string output_filename = absl::GetFlag(FLAGS_output_filename);
   std::string associated_data = absl::GetFlag(FLAGS_associated_data);
 
-  if (mode.empty()) {
-    std::cerr << "Mode must be specified with --mode=<" << kEncrypt << "|"
-              << kDecrypt << ">." << std::endl;
-    exit(1);
-  }
-
-  if (mode != kEncrypt && mode != kDecrypt) {
-    std::cerr << "Unknown mode '" << mode << "'; "
-              << "Expected either " << kEncrypt << " or " << kDecrypt << "."
-              << std::endl;
-    exit(1);
-  }
   std::clog << "Using keyset from file " << keyset_filename << " to AEAD-"
             << mode << " file " << input_filename << " with associated data '"
             << associated_data << "'." << std::endl;
   std::clog << "The resulting output will be written to " << output_filename
             << std::endl;
 
-  Status result = AeadConfig::Register();
-  if (!result.ok()) {
-    std::cerr << result.message() << std::endl;
-    exit(1);
-  }
-
-  // Read the keyset from file.
-  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
-      ReadKeyset(keyset_filename);
-  if (!keyset_handle.ok()) {
-    std::cerr << keyset_handle.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Get the primitive.
-  StatusOr<std::unique_ptr<Aead>> aead_primitive =
-      (*keyset_handle)->GetPrimitive<Aead>();
-  if (!aead_primitive.ok()) {
-    std::cerr << aead_primitive.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Read the input.
-  StatusOr<std::string> input_file_content = Read(input_filename);
-  if (!input_file_content.ok()) {
-    std::cerr << input_file_content.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Compute the output.
-  std::clog << mode << "ing...\n";
-  std::string output;
-  if (mode == kEncrypt) {
-    StatusOr<std::string> encrypt_result =
-        (*aead_primitive)->Encrypt(*input_file_content, associated_data);
-    if (!encrypt_result.ok()) {
-      std::cerr << encrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = encrypt_result.value();
-  } else {  // operation == kDecrypt.
-    StatusOr<std::string> decrypt_result =
-        (*aead_primitive)->Decrypt(*input_file_content, associated_data);
-    if (!decrypt_result.ok()) {
-      std::cerr << decrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = decrypt_result.value();
-  }
-
-  // Write the output to the output file.
-  Status write_result = Write(output, output_filename);
-  if (!write_result.ok()) {
-    std::cerr << write_result.message() << std::endl;
-    exit(1);
-  }
-
-  std::clog << "All done." << std::endl;
+  CHECK_OK(tink_cc_examples::AeadCli(mode, keyset_filename, input_filename,
+                                     output_filename, associated_data));
   return 0;
 }
 // [END aead-example]
diff --git a/cc/examples/aead/aead_cli_test.sh b/cc/examples/aead/aead_cli_test.sh
index ad5579e..d1d2627 100755
--- a/cc/examples/aead/aead_cli_test.sh
+++ b/cc/examples/aead/aead_cli_test.sh
@@ -20,6 +20,8 @@
 # Tests for Tink CC AEAD.
 #############################################################################
 
+: "${TEST_TMPDIR:=$(mktemp -d)}"
+
 readonly CLI="$1"
 readonly KEYSET_FILE="$2"
 readonly DATA_FILE="${TEST_TMPDIR}/example_data.txt"
@@ -44,7 +46,7 @@
 }
 
 #######################################
-# Asserts that the outcome of the latest test command was the expected one.
+# Asserts that the outcome of the latest test command is 0.
 #
 # If not, it terminates the test execution.
 #
@@ -52,23 +54,29 @@
 #   TEST_STATUS
 #   TEST_NAME
 #   TEST_CASE
-# Arguments:
-#   expected_outcome: The expected outcome.
 #######################################
-_assert_test_command_outcome() {
-  expected_outcome="$1"
-  if (( TEST_STATUS != expected_outcome )); then
-      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
-      exit 1
+assert_command_succeeded() {
+  if (( TEST_STATUS != 0 )); then
+    echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+    exit 1
   fi
 }
 
-assert_command_succeeded() {
-  _assert_test_command_outcome 0
-}
-
+#######################################
+# Asserts that the outcome of the latest test command is not 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
 assert_command_failed() {
-  _assert_test_command_outcome 1
+  if (( TEST_STATUS == 0 )); then
+      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+      exit 1
+  fi
 }
 
 #######################################
diff --git a/cc/examples/aead/aead_test_keyset.json b/cc/examples/aead/aead_test_keyset.json
index ff0aa56..204a2b4 100644
--- a/cc/examples/aead/aead_test_keyset.json
+++ b/cc/examples/aead/aead_test_keyset.json
@@ -1,12 +1,15 @@
 {
-  "primaryKeyId":1931667682,
-  "key":[{
-    "keyData":{
-      "typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey",
-      "value":"GhD+9l0RANZjzZEZ8PDp7LRW",
-      "keyMaterialType":"SYMMETRIC"},
-    "status":"ENABLED",
-    "keyId":1931667682,
-    "outputPrefixType":"TINK"
-  }]
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
 }
diff --git a/cc/examples/digital_signatures/BUILD.bazel b/cc/examples/digital_signatures/BUILD.bazel
index 3cc0bf2..a6adfaa 100644
--- a/cc/examples/digital_signatures/BUILD.bazel
+++ b/cc/examples/digital_signatures/BUILD.bazel
@@ -2,19 +2,11 @@
 
 licenses(["notice"])
 
-cc_library(
-    name = "util",
-    srcs = ["util.cc"],
-    hdrs = ["util.h"],
-    deps = [
-        "@tink_cc//:binary_keyset_reader",
-        "@tink_cc//:binary_keyset_writer",
-        "@tink_cc//:cleartext_keyset_handle",
-        "@tink_cc//:keyset_handle",
-        "@tink_cc//:keyset_reader",
-        "@tink_cc//:keyset_writer",
-        "@tink_cc//config:tink_config",
-        "@tink_cc//util:status",
+filegroup(
+    name = "digital_signature_keyset",
+    srcs = [
+        "digital_signature_private_keyset.json",
+        "digital_signature_public_keyset.json",
     ],
 )
 
@@ -22,10 +14,15 @@
     name = "digital_signatures_cli",
     srcs = ["digital_signatures_cli.cc"],
     deps = [
-        ":util",
+        "//util",
+        "@com_google_absl//absl/flags:flag",
+        "@com_google_absl//absl/flags:parse",
+        "@com_google_absl//absl/log:check",
+        "@tink_cc//:keyset_handle",
         "@tink_cc//:public_key_sign",
         "@tink_cc//:public_key_verify",
-        "@tink_cc//signature:signature_key_templates",
+        "@tink_cc//signature:signature_config",
+        "@tink_cc//util:status",
     ],
 )
 
@@ -35,6 +32,10 @@
     srcs = ["digital_signatures_cli_test.sh"],
     args = [
         "$(rootpath :digital_signatures_cli)",
+        "$(rootpaths :digital_signature_keyset)",
     ],
-    data = [":digital_signatures_cli"],
+    data = [
+        ":digital_signature_keyset",
+        ":digital_signatures_cli",
+    ],
 )
diff --git a/cc/examples/digital_signatures/CMakeLists.txt b/cc/examples/digital_signatures/CMakeLists.txt
new file mode 100644
index 0000000..8a14e9e
--- /dev/null
+++ b/cc/examples/digital_signatures/CMakeLists.txt
@@ -0,0 +1,17 @@
+add_executable(digital_signatures_cli digital_signatures_cli.cc)
+target_include_directories(digital_signatures_cli PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(digital_signatures_cli
+  tink::static
+  absl::check
+  absl::flags_parse
+  util)
+
+add_test(
+  NAME digital_signatures_cli_test
+  COMMAND "${BASH_PROGRAM}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/digital_signatures_cli_test.sh"
+    "${CMAKE_CURRENT_BINARY_DIR}/digital_signatures_cli"
+    "${CMAKE_CURRENT_SOURCE_DIR}/digital_signature_private_keyset.json"
+    "${CMAKE_CURRENT_SOURCE_DIR}/digital_signature_public_keyset.json")
diff --git a/cc/examples/digital_signatures/README.md b/cc/examples/digital_signatures/README.md
deleted file mode 100644
index 4e9558a..0000000
--- a/cc/examples/digital_signatures/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# C++ Digital Signatures CLI
-
-This is a command-line tool that can generate
-[Digital Signature](../../../docs/PRIMITIVES.md#digital-signatures)
-keys, and create and verify digital signatures.
-
-It demonstrates the basic steps of using Tink, namely loading key material,
-obtaining a primitive, and using the primitive to do crypto.
-
-## Build and Run
-
-### Bazel
-
-```shell
-git clone https://github.com/google/tink
-cd tink/cc/examples
-bazel build ...
-echo "a message" > message.txt
-./bazel-bin/digital_signatures/digital_signatures_cli gen-private-key private_keyset.bin
-./bazel-bin/digital_signatures/digital_signatures_cli get-public-key private_keyset.bin \
-    public_keyset.bin
-./bazel-bin/digital_signatures/digital_signatures_cli sign private_keyset.bin \
-    message.txt signature.bin
-./bazel-bin/digital_signatures/digital_signatures_cli verify public_keyset.bin \
-    message.txt signature.bin result.txt
-cat result.txt
-```
diff --git a/cc/examples/digital_signatures/digital_signature_private_keyset.json b/cc/examples/digital_signatures/digital_signature_private_keyset.json
new file mode 100644
index 0000000..215dad2
--- /dev/null
+++ b/cc/examples/digital_signatures/digital_signature_private_keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 1487078030,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+        "value": "Ek0SBggDEAIYAhohANUKuRXZHBD8rPcB5M6+pmgVSjk3gLSD/htdVvbrfbnPIiAXepWekQPRS74qUTMEwN6nXeizXucBxDk0SoKoeqShOBogbJEwIZASdx42tIitAe8UoBxWyi11Mq+HnWNtcQWkG18=",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 1487078030,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/digital_signatures/digital_signature_public_keyset.json b/cc/examples/digital_signatures/digital_signature_public_keyset.json
new file mode 100644
index 0000000..01ebcfa
--- /dev/null
+++ b/cc/examples/digital_signatures/digital_signature_public_keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 1487078030,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+        "value": "EgYIAxACGAIaIQDVCrkV2RwQ/Kz3AeTOvqZoFUo5N4C0g/4bXVb26325zyIgF3qVnpED0Uu+KlEzBMDep13os17nAcQ5NEqCqHqkoTg=",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 1487078030,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/digital_signatures/digital_signatures_cli.cc b/cc/examples/digital_signatures/digital_signatures_cli.cc
index a6a2a79..caa37cc 100644
--- a/cc/examples/digital_signatures/digital_signatures_cli.cc
+++ b/cc/examples/digital_signatures/digital_signatures_cli.cc
@@ -13,246 +13,123 @@
 // limitations under the License.
 //
 ///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for generating Digital Signatures keys, and creating
-// and verifying digital signatures.
-//
-// The first argument is the operation and it should be one of the following:
-// gen-private-key get-public-key sign verify.
-// Additional arguments depend on the operation.
-//
-// gen-private-key
-//   Generates a new private keyset using the RsaSsaPkcs13072Sha256F4 template.
-//   It requires 1 additional argument:
-//     output-file: name of the file for the resulting output
-//
-// get-public-key
-//   Extracts a public keyset associated with the given private keyset.
-//   It requires 2 additional arguments:
-//     private-keyset-file: name of the file with the private keyset
-//     output-file: name of the file for the resulting output
-//
-// sign
-//   Signs the message using the given private keyset.
-//   It requires 3 additional arguments:
-//     private-keyset-file: name of the file with the private keyset
-//     message-file: name of the file with the message
-//     output-file: name of the file for the resulting output
-//
-// verify
-//   Verifies the signature of the message using the given public keyset.
-//   It requires 4 additional arguments:
-//     public-keyset-file: name of the file with the public keyset
-//     message-file: name of the file with the message
-//     signature-file: name of the file with the signature
-//     output-file: name of the file for the resulting output (valid/invalid)
-
+// [START digital-signature-example]
+// A utility for signing and verifying files using digital signatures.
 #include <iostream>
+#include <memory>
+#include <ostream>
 #include <string>
-#include <utility>
 
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+#include "absl/log/check.h"
+#include "util/util.h"
+#include "tink/keyset_handle.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
-#include "tink/signature/signature_key_templates.h"
-#include "digital_signatures/util.h"
+#include "tink/signature/signature_config.h"
+#include "tink/util/status.h"
 
-// Prints usage info.
-void PrintUsageInfo() {
-  std::clog << "Usage: operation arguments\n"
-            << "where operation is one of the following:\n"
-            << "  gen-private-key get-public-key sign verify\n"
-            << "and, depending on the operation, arguments are:\n"
-            << "  gen-private-key: output-file\n"
-            << "  get-public-key: private-keyset-file output-file\n"
-            << "  sign: private-keyset-file message-file output-file\n"
-            << "  verify: public-keyset-file message-file signature-file "
-            << "output-file" << std::endl;
+ABSL_FLAG(std::string, keyset_filename, "", "Keyset file in JSON format");
+ABSL_FLAG(std::string, mode, "", "Mode of operation (sign|verify)");
+ABSL_FLAG(std::string, input_filename, "", "Filename to operate on");
+ABSL_FLAG(std::string, signature_filename, "", "Path to the signature file");
+
+namespace {
+
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::PublicKeySign;
+using ::crypto::tink::PublicKeyVerify;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+
+constexpr absl::string_view kSign = "sign";
+constexpr absl::string_view kVerify = "verify";
+
+void ValidateParams() {
+  // [START_EXCLUDE]
+  CHECK(absl::GetFlag(FLAGS_mode) == kSign ||
+        absl::GetFlag(FLAGS_mode) == kVerify)
+      << "Invalid mode; must be `" << kSign << "` or `" << kVerify << "`"
+      << std::endl;
+  CHECK(!absl::GetFlag(FLAGS_keyset_filename).empty())
+      << "Keyset file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_input_filename).empty())
+      << "Input file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_signature_filename).empty())
+      << "Signature file must be specified";
+  // [END_EXCLUDE]
 }
 
-// Generates a new private keyset using the RsaSsaPkcs13072Sha256F4 template
-// and writes it to the output file.
-void GeneratePrivateKey(const std::string& output_filename) {
-  std::clog << "Generating a new private keyset.." << std::endl;
+}  // namespace
 
-  auto key_template =
-      crypto::tink::SignatureKeyTemplates::RsaSsaPkcs13072Sha256F4();
-  auto new_keyset_handle_result =
-      crypto::tink::KeysetHandle::GenerateNew(key_template);
-  if (!new_keyset_handle_result.ok()) {
-    std::clog << "Generating new keyset failed: "
-              << new_keyset_handle_result.status().message() << std::endl;
-    exit(1);
+namespace tink_cc_examples {
+
+// Digital signature example CLI implementation.
+Status DigitalSignatureCli(absl::string_view mode,
+                           const std::string& keyset_filename,
+                           const std::string& input_filename,
+                           const std::string& signature_filename) {
+  Status result = crypto::tink::SignatureConfig::Register();
+  if (!result.ok()) return result;
+
+  // Read the keyset from file.
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      ReadJsonCleartextKeyset(keyset_filename);
+  if (!keyset_handle.ok()) return keyset_handle.status();
+
+  // Read the input.
+  StatusOr<std::string> input_file_content = ReadFile(input_filename);
+  if (!input_file_content.ok()) return input_file_content.status();
+
+  if (mode == kSign) {
+    StatusOr<std::unique_ptr<PublicKeySign>> public_key_sign =
+        (*keyset_handle)->GetPrimitive<PublicKeySign>();
+    if (!public_key_sign.ok()) return public_key_sign.status();
+
+    StatusOr<std::string> signature =
+        (*public_key_sign)->Sign(*input_file_content);
+    if (!signature.ok()) return signature.status();
+
+    return WriteToFile(*signature, signature_filename);
+  } else {  // mode == kVerify
+    StatusOr<std::unique_ptr<PublicKeyVerify>> public_key_verify =
+        (*keyset_handle)->GetPrimitive<PublicKeyVerify>();
+    if (!public_key_verify.ok()) return public_key_verify.status();
+
+    // Read the signature.
+    StatusOr<std::string> signature_file_content = ReadFile(signature_filename);
+    if (!signature_file_content.ok()) return signature_file_content.status();
+
+    return (*public_key_verify)
+        ->Verify(*signature_file_content, *input_file_content);
   }
-  auto keyset_handle = std::move(new_keyset_handle_result.value());
-
-  std::clog << "Writing the keyset to file " << output_filename
-            << "..." << std::endl;
-
-  Util::WriteKeyset(keyset_handle, output_filename);
 }
 
-// Extracts a public keyset associated with the given private keyset
-// and writes it to the output file.
-void ExtractPublicKey(const std::string& private_keyset_filename,
-                      const std::string& output_filename) {
-  std::clog << "Extracting a public keyset associated with the private "
-            << "keyset from file " << private_keyset_filename << "..."
-            << std::endl;
-
-  auto private_keyset_handle = Util::ReadKeyset(private_keyset_filename);
-
-  auto new_keyset_handle_result =
-      private_keyset_handle->GetPublicKeysetHandle();
-  if (!new_keyset_handle_result.ok()) {
-    std::clog << "Getting the keyset failed: "
-              << new_keyset_handle_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto public_keyset_handle = std::move(new_keyset_handle_result.value());
-
-  std::clog << "Writing the keyset to file " << output_filename
-            << "..." << std::endl;
-
-  Util::WriteKeyset(public_keyset_handle, output_filename);
-}
-
-// Signs the message using the given private keyset
-// and writes the signature to the output file.
-void Sign(const std::string& keyset_filename,
-          const std::string& message_filename,
-          const std::string& output_filename) {
-  auto keyset_handle = Util::ReadKeyset(keyset_filename);
-
-  auto primitive_result =
-      keyset_handle->GetPrimitive<crypto::tink::PublicKeySign>();
-  if (!primitive_result.ok()) {
-    std::clog << "Getting PublicKeySign-primitive from the factory failed: "
-              << primitive_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto public_key_sign = std::move(primitive_result.value());
-
-  std::clog << "Signing message from file " << message_filename
-            << " using private keyset from file " << keyset_filename
-            << "..." << std::endl;
-
-  std::string message = Util::Read(message_filename);
-
-  auto sign_result = public_key_sign->Sign(message);
-  if (!sign_result.ok()) {
-    std::clog << "Error while signing the message: "
-              << sign_result.status().message() << std::endl;
-    exit(1);
-  }
-  std::string signature = sign_result.value();
-
-  std::clog << "Writing the resulting signature to file " << output_filename
-            << "..." << std::endl;
-
-  Util::Write(signature, output_filename);
-}
-
-// Verifies the signature of the message using the given public keyset
-// and writes the result to the output file.
-void Verify(const std::string& keyset_filename,
-            const std::string& message_filename,
-            const std::string& signature_filename,
-            const std::string& output_filename) {
-  auto keyset_handle = Util::ReadKeyset(keyset_filename);
-
-  auto primitive_result =
-      keyset_handle->GetPrimitive<crypto::tink::PublicKeyVerify>();
-  if (!primitive_result.ok()) {
-    std::clog << "Getting PublicKeyVerify-primitive from the factory "
-              << "failed: " << primitive_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto public_key_verify = std::move(primitive_result.value());
-
-  std::clog << "Verifying signature from file " << signature_filename
-            << " of the message from file " << message_filename
-            << " using public keyset from file " << keyset_filename
-            << "..." << std::endl;
-
-  std::string signature = Util::Read(signature_filename);
-  std::string message = Util::Read(message_filename);
-
-  std::string result;
-  auto verify_status = public_key_verify->Verify(signature, message);
-  if (!verify_status.ok()) {
-    std::clog << "Error while verifying the signature: "
-              << verify_status.message() << std::endl;
-    result = "invalid";
-  } else {
-    result = "valid";
-  }
-
-  std::clog << "Writing the result to file " << output_filename
-            << "..." << std::endl;
-
-  Util::Write(result, output_filename);
-}
+}  // namespace tink_cc_examples
 
 int main(int argc, char** argv) {
-  if (argc == 1) {
-    PrintUsageInfo();
-    exit(1);
+  absl::ParseCommandLine(argc, argv);
+
+  ValidateParams();
+
+  std::string mode = absl::GetFlag(FLAGS_mode);
+  std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
+  std::string input_filename = absl::GetFlag(FLAGS_input_filename);
+  std::string signature_filename = absl::GetFlag(FLAGS_signature_filename);
+
+  std::clog << "Using keyset in " << keyset_filename << " to " << mode;
+  if (mode == kSign) {
+    std::clog << " file " << input_filename
+              << "; the resulting signature is written to "
+              << signature_filename << std::endl;
+  } else {  // mode == kVerify
+    std::clog << " the signature in " << signature_filename
+              << " over the content of " << input_filename << std::endl;
   }
 
-  Util::InitTink();
-
-  std::string operation = argv[1];
-
-  if (operation == "gen-private-key") {
-    if (argc != 3) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string output_filename = argv[2];
-
-    GeneratePrivateKey(output_filename);
-  } else if (operation == "get-public-key") {
-    if (argc != 4) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string private_keyset_filename = argv[2];
-    std::string output_filename = argv[3];
-
-    ExtractPublicKey(private_keyset_filename, output_filename);
-  } else if (operation == "sign") {
-    if (argc != 5) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string keyset_filename = argv[2];
-    std::string message_filename = argv[3];
-    std::string output_filename = argv[4];
-
-    Sign(keyset_filename, message_filename, output_filename);
-  } else if (operation == "verify") {
-    if (argc != 6) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string keyset_filename = argv[2];
-    std::string message_filename = argv[3];
-    std::string signature_filename = argv[4];
-    std::string output_filename = argv[5];
-
-    Verify(keyset_filename, message_filename, signature_filename,
-           output_filename);
-  } else {
-    std::clog << "Unknown operation. Supported operations are: "
-              << "gen-private-key get-public-key sign verify" << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Done!" << std::endl;
-
+  CHECK_OK(tink_cc_examples::DigitalSignatureCli(
+      mode, keyset_filename, input_filename, signature_filename));
   return 0;
 }
+// [END digital-signature-example]
diff --git a/cc/examples/digital_signatures/digital_signatures_cli_test.sh b/cc/examples/digital_signatures/digital_signatures_cli_test.sh
index c83e92c..b84f3df 100755
--- a/cc/examples/digital_signatures/digital_signatures_cli_test.sh
+++ b/cc/examples/digital_signatures/digital_signatures_cli_test.sh
@@ -14,114 +14,166 @@
 # limitations under the License.
 ################################################################################
 
+set -euo pipefail
 
 #############################################################################
-#### Tests for digital_signatures_cli binary.
-
-SIGNATURE_CLI="$1"
-
-PRIVATE_KEYSET_FILE="$TEST_TMPDIR/private_keyset.bin"
-PUBLIC_KEYSET_FILE="$TEST_TMPDIR/public_keyset.bin"
-MESSAGE_FILE="$TEST_TMPDIR/message.txt"
-SIGNATURE_FILE="$TEST_TMPDIR/signature.bin"
-RESULT_FILE="$TEST_TMPDIR/result.txt"
-
-OTHER_PRIVATE_KEYSET_FILE="$TEST_TMPDIR/other_private_keyset.bin"
-OTHER_PUBLIC_KEYSET_FILE="$TEST_TMPDIR/other_public_keyset.bin"
-OTHER_MESSAGE_FILE="$TEST_TMPDIR/other_message.txt"
-
-echo "This is a message." > $MESSAGE_FILE
-echo "This is a different message." > $OTHER_MESSAGE_FILE
-
+# Tests for Tink C++ digital signature example.
 #############################################################################
-#### Helper function that checks if values are equal.
 
-assert_equal() {
-  if [ "$1" == "$2" ]; then
-    echo "+++ Success: values are equal."
-  else
-    echo "--- Failure: values are different. Expected: [$1], actual: [$2]."
+: "${TEST_TMPDIR:=$(mktemp -d)}"
+
+readonly CLI="$1"
+readonly PRIVATE_KEYSET_FILE="$2"
+readonly PUBLIC_KEYSET_FILE="$3"
+readonly MESSAGE_FILE="${TEST_TMPDIR}/message.txt"
+readonly SIGNATURE_FILE="${TEST_TMPDIR}/signature.txt"
+readonly TEST_NAME="TinkCcExamplesDigitalSignaturesTest"
+
+echo "This is some message to be signed." > "${MESSAGE_FILE}"
+
+#######################################
+# A helper function for getting the return code of a command that may fail.
+# Temporarily disables error safety and stores return value in TEST_STATUS.
+#
+# Globals:
+#   TEST_STATUS
+# Arguments:
+#   Command to execute.
+#######################################
+test_command() {
+  set +e
+  "$@"
+  TEST_STATUS=$?
+  set -e
+}
+
+#######################################
+# Asserts that the outcome of the latest test command is 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+assert_command_succeeded() {
+  if (( TEST_STATUS != 0 )); then
+    echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
     exit 1
   fi
 }
 
-#############################################################################
-#### All good, everything should work.
-test_name="all_good"
-echo "+++ Starting test $test_name..."
+#######################################
+# Asserts that the outcome of the latest test command is not 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+assert_command_failed() {
+  if (( TEST_STATUS == 0 )); then
+      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+      exit 1
+  fi
+}
 
-#### Generate a private key and get a public key.
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
+#######################################
+# Starts a new test case; records the test case name to TEST_CASE.
+#
+# Globals:
+#   TEST_NAME
+#   TEST_CASE
+# Arguments:
+#   test_case: The name of the test case.
+#######################################
+start_test_case() {
+  TEST_CASE="$1"
+  echo "[ RUN      ] ${TEST_NAME}.${TEST_CASE}"
+}
 
-#### Sign the message.
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE || exit 1
-
-#### Verify the signature.
-$SIGNATURE_CLI verify $PUBLIC_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE $RESULT_FILE || exit 1
-
-#### Check that the signature is valid.
-RESULT=$(<$RESULT_FILE)
-assert_equal "valid" "$RESULT"
+#######################################
+# Ends a test case printing a success message.
+#
+# Globals:
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+end_test_case() {
+  echo "[       OK ] ${TEST_NAME}.${TEST_CASE}"
+}
 
 #############################################################################
-#### Bad private key when getting the public key.
-test_name="get_public_key_with_bad_private_key"
-echo "+++ Starting test $test_name..."
 
-echo "abcd" >> $PRIVATE_KEYSET_FILE
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE
+start_test_case "sign_verify_all_good"
 
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
+# Sign.
+test_command "${CLI}" \
+  --mode sign \
+  --keyset_filename "${PRIVATE_KEYSET_FILE}" \
+  --input_filename "${MESSAGE_FILE}" \
+  --signature_filename "${SIGNATURE_FILE}"
+assert_command_succeeded
+
+# Verify.
+test_command "${CLI}" \
+  --mode verify \
+  --keyset_filename "${PUBLIC_KEYSET_FILE}" \
+  --input_filename "${MESSAGE_FILE}" \
+  --signature_filename "${SIGNATURE_FILE}"
+assert_command_succeeded
+
+end_test_case
 
 #############################################################################
-#### Different public key when verifying a signature.
-test_name="verify_with_different_public_key"
-echo "+++ Starting test $test_name..."
 
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI gen-private-key $OTHER_PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $OTHER_PRIVATE_KEYSET_FILE $OTHER_PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE || exit 1
-$SIGNATURE_CLI verify $OTHER_PUBLIC_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE $RESULT_FILE || exit 1
+start_test_case "verify_fails_with_modified_signature"
 
-RESULT=$(<$RESULT_FILE)
-assert_equal "invalid" "$RESULT"
+# Sign.
+test_command "${CLI}" \
+  --mode sign \
+  --keyset_filename "${PRIVATE_KEYSET_FILE}" \
+  --input_filename "${MESSAGE_FILE}" \
+  --signature_filename "${SIGNATURE_FILE}"
+assert_command_succeeded
+
+# Modify signature.
+echo "modified" >> "${SIGNATURE_FILE}"
+
+# Verify.
+test_command "${CLI}" \
+  --mode verify \
+  --keyset_filename "${PUBLIC_KEYSET_FILE}" \
+  --input_filename "${MESSAGE_FILE}" \
+  --signature_filename "${SIGNATURE_FILE}"
+assert_command_failed
+
+end_test_case
 
 #############################################################################
-#### Different message when verifying a signature.
-test_name="verify_with_different_message"
-echo "+++ Starting test $test_name..."
 
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE || exit 1
-$SIGNATURE_CLI verify $PUBLIC_KEYSET_FILE $OTHER_MESSAGE_FILE $SIGNATURE_FILE $RESULT_FILE || exit 1
+start_test_case "verify_fails_with_modified_message"
 
-RESULT=$(<$RESULT_FILE)
-assert_equal "invalid" "$RESULT"
+# Sign.
+test_command "${CLI}" \
+  --mode sign \
+  --keyset_filename "${PRIVATE_KEYSET_FILE}" \
+  --input_filename "${MESSAGE_FILE}" \
+  --signature_filename "${SIGNATURE_FILE}"
+assert_command_succeeded
 
-#############################################################################
-#### Sign with wrong key.
-test_name="sign_with_wrong_key"
-echo "+++ Starting test $test_name..."
+# Modify message.
+echo "modified" >> "${MESSAGE_FILE}"
 
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PUBLIC_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE
+# Verify.
+test_command "${CLI}" \
+  --mode verify \
+  --keyset_filename "${PUBLIC_KEYSET_FILE}" \
+  --input_filename "${MESSAGE_FILE}" \
+  --signature_filename "${SIGNATURE_FILE}"
+assert_command_failed
 
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
-
-#############################################################################
-#### Verify with wrong key.
-test_name="verify_with_wrong_key"
-echo "+++ Starting test $test_name..."
-
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE || exit 1
-$SIGNATURE_CLI verify $PRIVATE_KEYSET_FILE $MESSAGE_FILE $SIGNATURE_FILE $RESULT_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
+end_test_case
diff --git a/cc/examples/digital_signatures/util.cc b/cc/examples/digital_signatures/util.cc
deleted file mode 100644
index 4ba9a10..0000000
--- a/cc/examples/digital_signatures/util.cc
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "digital_signatures/util.h"
-
-#include <fstream>
-#include <iostream>
-#include <string>
-#include <utility>
-
-#include "tink/binary_keyset_reader.h"
-#include "tink/binary_keyset_writer.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "tink/config.h"
-#include "tink/config/tink_config.h"
-#include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
-#include "tink/keyset_writer.h"
-#include "tink/util/status.h"
-
-using crypto::tink::BinaryKeysetReader;
-using crypto::tink::BinaryKeysetWriter;
-using crypto::tink::CleartextKeysetHandle;
-using crypto::tink::KeysetHandle;
-using crypto::tink::KeysetReader;
-using crypto::tink::KeysetWriter;
-using crypto::tink::TinkConfig;
-
-// static
-std::unique_ptr<KeysetReader> Util::GetBinaryKeysetReader(
-    const std::string& filename) {
-  std::unique_ptr<std::ifstream> keyset_stream(new std::ifstream());
-  keyset_stream->open(filename, std::ifstream::in);
-  auto keyset_reader_result = BinaryKeysetReader::New(std::move(keyset_stream));
-  if (!keyset_reader_result.ok()) {
-    std::clog << "Creation of the BinaryKeysetReader failed: "
-              << keyset_reader_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_reader_result.value());
-}
-
-// static
-std::unique_ptr<KeysetWriter> Util::GetBinaryKeysetWriter(
-    const std::string& filename) {
-  std::unique_ptr<std::ofstream> keyset_stream(new std::ofstream());
-  keyset_stream->open(filename, std::ofstream::out);
-  auto keyset_writer_result = BinaryKeysetWriter::New(std::move(keyset_stream));
-  if (!keyset_writer_result.ok()) {
-    std::clog << "Creation of the BinaryKeysetWriter failed: "
-              << keyset_writer_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_writer_result.value());
-}
-
-// static
-std::unique_ptr<KeysetHandle> Util::ReadKeyset(const std::string& filename) {
-  auto keyset_reader = GetBinaryKeysetReader(filename);
-  auto keyset_handle_result =
-      CleartextKeysetHandle::Read(std::move(keyset_reader));
-  if (!keyset_handle_result.ok()) {
-    std::clog << "Reading the keyset failed: "
-              << keyset_handle_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_handle_result.value());
-}
-
-// static
-void Util::WriteKeyset(const std::unique_ptr<KeysetHandle>& keyset_handle,
-                       const std::string& filename) {
-  auto keyset_writer = GetBinaryKeysetWriter(filename);
-  auto status = CleartextKeysetHandle::Write(keyset_writer.get(),
-                                             *keyset_handle);
-  if (!status.ok()) {
-    std::clog << "Writing the keyset failed: " << status.message() << std::endl;
-    exit(1);
-  }
-}
-
-// static
-void Util::InitTink() {
-  auto status = TinkConfig::Register();
-  if (!status.ok()) {
-    std::clog << "Initialization of Tink failed: " << status.message()
-              << std::endl;
-    exit(1);
-  }
-}
-
-// static
-std::string Util::Read(const std::string& filename) {
-  std::ifstream input_stream;
-  input_stream.open(filename, std::ifstream::in);
-  if (!input_stream.is_open()) {
-    std::clog << "Error opening input file " << filename << std::endl;
-    exit(1);
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  input_stream.close();
-  return input.str();
-}
-
-// static
-void Util::Write(const std::string& output, const std::string& filename) {
-  std::ofstream output_stream(filename,
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    std::clog << "Error opening output file " << filename << std::endl;
-    exit(1);
-  }
-  output_stream << output;
-  output_stream.close();
-}
diff --git a/cc/examples/digital_signatures/util.h b/cc/examples/digital_signatures/util.h
deleted file mode 100644
index de3bf60..0000000
--- a/cc/examples/digital_signatures/util.h
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_EXAMPLES_DIGITAL_SIGNATURES_UTIL_H_
-#define TINK_EXAMPLES_DIGITAL_SIGNATURES_UTIL_H_
-
-#include <fstream>
-#include <iostream>
-#include <string>
-
-#include "tink/keyset_handle.h"
-
-// Helper functions for Digital Signatures CLI
-class Util {
- public:
-  // Returns a BinaryKeysetReader that reads from the specified file.
-  // In case of errors writes a log message and aborts.
-  static std::unique_ptr<crypto::tink::KeysetReader> GetBinaryKeysetReader(
-      const std::string& filename);
-
-  // Returns a BinaryKeysetWriter that writes from the specified file.
-  // In case of errors writes a log message and aborts.
-  static std::unique_ptr<crypto::tink::KeysetWriter> GetBinaryKeysetWriter(
-      const std::string& filename);
-
-  // Reads a keyset from the specified file.
-  // In case of errors writes a log message and aborts.
-  static std::unique_ptr<crypto::tink::KeysetHandle> ReadKeyset(
-      const std::string& filename);
-
-  // Writes the keyset to the specified file.
-  // In case of errors writes a log message and aborts.
-  static void WriteKeyset(
-      const std::unique_ptr<crypto::tink::KeysetHandle>& keyset_handle,
-      const std::string& filename);
-
-  // Initializes Tink registry.
-  // In case of errors writes a log message and aborts.
-  static void InitTink();
-
-  // Reads the specified file and returns the contents as a string.
-  // In case of errors writes a log message and aborts.
-  static std::string Read(const std::string& filename);
-
-  // Writes the given 'output' to the specified file.
-  // In case of errors writes a log message and aborts.
-  static void Write(const std::string& output, const std::string& filename);
-};
-
-#endif  // TINK_EXAMPLES_DIGITAL_SIGNATURES_UTIL_H_
diff --git a/cc/examples/helloworld/BUILD.bazel b/cc/examples/helloworld/BUILD.bazel
deleted file mode 100644
index f6b6efb..0000000
--- a/cc/examples/helloworld/BUILD.bazel
+++ /dev/null
@@ -1,32 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-exports_files(["aes128_gcm_test_keyset_json.txt"])
-
-cc_binary(
-    name = "hello_world",
-    srcs = ["hello_world.cc"],
-    deps = [
-        "@tink_cc",
-        "@tink_cc//:cleartext_keyset_handle",
-        "@tink_cc//config:tink_config",
-        "@tink_cc//util:status",
-    ],
-)
-
-sh_test(
-    name = "hello_world_test",
-    size = "small",
-    srcs = [
-        "hello_world_test.sh",
-    ],
-    args = [
-        "$(rootpath :hello_world)",
-        "$(rootpath :aes128_gcm_test_keyset_json.txt)",
-    ],
-    data = [
-        ":aes128_gcm_test_keyset_json.txt",
-        ":hello_world",
-    ],
-)
diff --git a/cc/examples/helloworld/CMakeLists.txt b/cc/examples/helloworld/CMakeLists.txt
deleted file mode 100644
index 32932a5..0000000
--- a/cc/examples/helloworld/CMakeLists.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# 3.5 is the minimum version supported by Tink and its dependencies.
-cmake_minimum_required(VERSION 3.5)
-project(TinkHelloWorld CXX)
-
-set(CMAKE_BUILD_TYPE Release)
-
-# Import Tink as an in-tree dependency.
-add_subdirectory(../../.. tink)
-
-add_executable(hello_world hello_world.cc)
-target_link_libraries(hello_world tink::static)
diff --git a/cc/examples/helloworld/CMakeLists_for_CMakeBuildTest.txt b/cc/examples/helloworld/CMakeLists_for_CMakeBuildTest.txt
deleted file mode 100644
index d8a0778..0000000
--- a/cc/examples/helloworld/CMakeLists_for_CMakeBuildTest.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-cmake_minimum_required(VERSION 3.5)
-project(HelloWorld CXX)
-set(CMAKE_CXX_STANDARD 11)
-
-add_subdirectory(third_party/tink)
-
-add_executable(hello_world hello_world.cc)
-target_link_libraries(hello_world tink::static)
diff --git a/cc/examples/helloworld/README.md b/cc/examples/helloworld/README.md
deleted file mode 100644
index 92952a5..0000000
--- a/cc/examples/helloworld/README.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# C++ Hello World
-
-This is a command-line tool that can encrypt and decrypt small files using
-[AEAD (Authenticated Encryption with Associated Data)](https://developers.google.com/tink/aead).
-
-It demonstrates the basic steps of using Tink, namely loading key material,
-obtaining a primitive, and using the primitive to do crypto.
-
-## Build and Run
-
-### Bazel
-
-```shell
-# Build the code.
-git clone https://github.com/google/tink
-cd tink/cc/examples
-bazel build ...
-
-# Create some input.
-echo "some plaintext" > foo.txt
-
-# Encrypt.
-./bazel-bin/helloworld/hello_world \
-  ./helloworld/aes128_gcm_test_keyset_json.txt \
-  encrypt \
-  foo.txt \
-  "some aad" \
-  foo.encrypted
-
-# Decrypt.
-./bazel-bin/helloworld/hello_world \
-  ./helloworld/aes128_gcm_test_keyset_json.txt \
-  decrypt \
-  foo.encrypted \
-  "some aad" \
-  foo-decrypted.tx
-
-# Inspect the output.
-cat foo-decrypted.txt
-```
-
-### CMake
-
-```shell
-
-# Clone the Tink repository.
-git clone https://github.com/google/tink
-cd tink/cc/examples/helloworld
-
-# Build the hello world example.
-(
-  mkdir build && cd build
-  # Note: Specify -DTINK_USE_SYSTEM_OPENSSL=ON if you want to build Tink against
-  # OpenSSL rather than BoringSSL. CMake will search for a suitable OpenSSL
-  # library in the system's library path.
-  cmake .. -DCMAKE_CXX_STANDARD=11
-  make -j"$(nproc)"
-)
-
-# Create some input.
-echo "some plaintext" > foo.txt
-
-./build/hello_world \
-  aes128_gcm_test_keyset_json.txt \
-  encrypt \
-  foo.txt \
-  "some aad" \
-  foo.txt.encrypted
-
-./build/hello_world \
-  aes128_gcm_test_keyset_json.txt \
-  decrypt \
-  foo.txt.encrypted \
-  "some aad" \
-  foo.txt.decrypted
-
-if [[ "$(diff foo.txt foo.txt.decrypted)" == "" ]]; then
-  echo "File correctly encrypted and decrypted"
-else
-  echo "Failed to encrypt-and-decrypt. Diff:"
-  diff foo.txt foo.txt.decrypted
-fi
-```
-
-The `cmake_build_test.sh` script will run an encrypt-then-decrypt test with
-AEAD. You can build Tink against OpenSSL by passing the `--openssl` option --
-the script will download, build and install OpenSSL in a temporary directory.
diff --git a/cc/examples/helloworld/aes128_gcm_test_keyset_json.txt b/cc/examples/helloworld/aes128_gcm_test_keyset_json.txt
deleted file mode 100644
index 456c46a..0000000
--- a/cc/examples/helloworld/aes128_gcm_test_keyset_json.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-    "primaryKeyId": 515279322,
-    "key": [{
-        "keyData": {
-            "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
-            "keyMaterialType": "SYMMETRIC",
-            "value": "GhCS/1+ejWpx68NfGt6ziYHd"
-        },
-        "outputPrefixType": "TINK",
-        "keyId": 515279322,
-        "status": "ENABLED"
-    }]
-}
diff --git a/cc/examples/helloworld/cmake_build_test.sh b/cc/examples/helloworld/cmake_build_test.sh
deleted file mode 100755
index 2bbbcab..0000000
--- a/cc/examples/helloworld/cmake_build_test.sh
+++ /dev/null
@@ -1,210 +0,0 @@
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-#!/bin/bash
-
-set -e
-
-readonly TINK_USE_CXX_STANDARD=11
-
-# Test for using Tink in a CMake project.
-
-if [[ -z "${TEST_TMPDIR}" ]]; then
-  echo "Error: TEST_TMPDIR must be set to a temporary working directory."
-  exit 1
-fi
-
-if [[ -z "${TEST_SRCDIR}" ]]; then
-  echo "Error: TEST_SRCDIR must be set to Tink's parent directory."
-  exit 1
-fi
-
-# XDG_CACHE_HOME must be set for a successful build of BoringSSL.
-export XDG_CACHE_HOME="${TEST_TMPDIR}/cache"
-TEST_DATA_DIR="${TEST_SRCDIR}/tink/cc/examples/helloworld"
-CMAKE_LISTS_FILE="${TEST_DATA_DIR}/CMakeLists_for_CMakeBuildTest.txt"
-HELLO_WORLD_SRC="${TEST_DATA_DIR}/hello_world.cc"
-KEYSET_FILE="${TEST_DATA_DIR}/aes128_gcm_test_keyset_json.txt"
-
-PROJECT_DIR="${TEST_TMPDIR}/my_project"
-PLAINTEXT_FILE="${TEST_TMPDIR}/example_plaintext.txt"
-CIPHERTEXT_FILE="${TEST_TMPDIR}/ciphertext.bin"
-DECRYPTED_FILE="${TEST_TMPDIR}/decrypted.txt"
-AAD_TEXT="some associated data"
-
-# If "true" build and install OpenSSL and build Tink against it.
-USE_OPENSSL="false"
-# If "true" build and install Abseil and build Tink against it.
-USE_INSTALLED_ABSEIL="false"
-
-# Parse parameters.
-while [[ "$#" -gt 0 ]]; do
-  case "$1" in
-    --openssl)
-      USE_OPENSSL="true"
-      shift
-      ;;
-    # Use prebuilt static libraries of Abseil.
-    --use_installed_abseil)
-      USE_INSTALLED_ABSEIL="true"
-      shift
-      ;;
-    *)
-      echo "Unknown parameter - $1"
-      exit 1
-  esac
-done
-
-#######################################
-# Install a given version of OpenSSL in a temporary directory
-#
-# Adds the directory to PATH and sets OPENSSL_ROOT_DIR accordingly.
-#
-# Globals:
-#   OPENSSL_ROOT_DIR Gets populated with OpenSSL temporary directory.
-#   PATH Gets updated with bin's path.
-# Arguments:
-#   openssl_version Version of OpenSSL to install, e.g., 1.1.1l.
-#######################################
-install_openssl() {
-  local openssl_version="$1"
-
-  local openssl_name="openssl-${openssl_version}"
-  local openssl_archive="${openssl_name}.tar.gz"
-  local openssl_url="https://www.openssl.org/source/${openssl_archive}"
-  local openssl_sha256="$(curl -sS https://www.openssl.org/source/${openssl_archive}.sha256)"
-
-  local -r openssl_tmpdir="$(mktemp -dt tink-openssl.XXXXXX)"
-  (
-    cd "${openssl_tmpdir}"
-    curl -OLsS "${openssl_url}"
-    echo "${openssl_sha256} ${openssl_archive}" | sha256sum -c
-
-    tar xzf "${openssl_archive}"
-    cd "${openssl_name}"
-    ./config --prefix="${openssl_tmpdir}" --openssldir="${openssl_tmpdir}"
-    make -j"$(nproc)"
-    make install
-  )
-  export OPENSSL_ROOT_DIR="${openssl_tmpdir}"
-  export PATH="${openssl_tmpdir}/bin:${PATH}"
-}
-
-#######################################
-# Install Abeseil into a temporary folder.
-#
-# Globals:
-#   ABSEIL_INSTALL_PATH Gets populated with Abseil's installation path.
-# Arguments:
-#   abseil_commit Abseils commit to lookup.
-#######################################
-install_abseil() {
-  local -r abseil_commit="$1"
-  local -r abseil_tmpdir="$(mktemp -dt tink-abseil.XXXXXX)"
-  local -r abseil_install_dir="${abseil_tmpdir}/install"
-  (
-    cd "${abseil_tmpdir}"
-    mkdir "install"
-    curl -OLsS "https://github.com/abseil/abseil-cpp/archive/${abseil_commit}.zip"
-    unzip "${abseil_commit}.zip" && cd "abseil-cpp-${abseil_commit}"
-    mkdir build && cd build
-    cmake .. \
-      -DCMAKE_INSTALL_PREFIX="${abseil_install_dir}" \
-      -DCMAKE_CXX_STANDARD="${TINK_USE_CXX_STANDARD}"
-    cmake --build . --target install
-  )
-  export ABSEIL_INSTALL_PATH="${abseil_install_dir}"
-}
-
-
-#######################################
-# Builds the hello world project
-#
-# Globals:
-#   USE_OPENSSL if "true" install OpenSSL and build against it.
-#   USE_INSTALLED_ABSEIL if "true" install Abseil and build against it.
-# Arguments:
-#   None
-#######################################
-build_hello_world() {
-  local cmake_parameters=(
-    -DCMAKE_CXX_STANDARD="${TINK_USE_CXX_STANDARD}"
-  )
-  if [[ "${USE_OPENSSL}" == "true" ]]; then
-    # Install OpenSSL in a temporary directory.
-    install_openssl "1.1.1l"
-    cmake_parameters+=( -DTINK_USE_SYSTEM_OPENSSL=ON )
-  fi
-  if [[ "${USE_INSTALLED_ABSEIL}" == "true" ]]; then
-    # Commit from 2021-12-03
-    install_abseil "9336be04a242237cd41a525bedfcf3be1bb55377"
-    cmake_parameters+=( -DCMAKE_PREFIX_PATH="${ABSEIL_INSTALL_PATH}" )
-    cmake_parameters+=( -DTINK_USE_INSTALLED_ABSEIL=ON )
-  fi
-  readonly cmake_parameters
-  (
-    mkdir build && cd build
-    cmake --version
-    cmake .. "${cmake_parameters[@]}"
-    make -j"$(nproc)"
-  )
-}
-
-main() {
-  # Create necessary directories, and create a symlink to Tink in the
-  # "my_project" directory.
-  mkdir -p "${XDG_CACHE_HOME}"
-  mkdir -p "${PROJECT_DIR}" "${PROJECT_DIR}/third_party"
-  ln -s "${TEST_SRCDIR}/tink" "${PROJECT_DIR}/third_party/tink"
-
-  # Copy "my_project" files.
-  cp "${HELLO_WORLD_SRC}" "${KEYSET_FILE}" "${PROJECT_DIR}"
-  cp "${CMAKE_LISTS_FILE}" "${PROJECT_DIR}/CMakeLists.txt"
-
-  # Move into the newly populated project directory.
-  cd "${PROJECT_DIR}"
-
-  # Build the project. This will produce ./build/hello_world.
-  build_hello_world
-
-  # Create a plaintext.
-  echo "This is some message to be encrypted." > "${PLAINTEXT_FILE}"
-
-  # Run encryption & decryption.
-  ./build/hello_world \
-    "${KEYSET_FILE}" \
-    encrypt \
-    "${PLAINTEXT_FILE}" \
-    "${AAD_TEXT}" \
-    "${CIPHERTEXT_FILE}"
-
-  ./build/hello_world \
-    "${KEYSET_FILE}" \
-    decrypt \
-    "${CIPHERTEXT_FILE}" \
-    "${AAD_TEXT}" \
-    "${DECRYPTED_FILE}"
-
-  # Check that decryption is correct.
-  diff -q "${DECRYPTED_FILE}" "${PLAINTEXT_FILE}"
-  if [ $? -ne 0 ]; then
-    echo "--- Failure: the decrypted file differs from the original plaintext."
-    diff "${DECRYPTED_FILE}" "${PLAINTEXT_FILE}"
-    exit 1
-  fi
-  echo "+++ Success: decryption was correct."
-}
-
-main
diff --git a/cc/examples/helloworld/hello_world.cc b/cc/examples/helloworld/hello_world.cc
deleted file mode 100644
index 6653b2c..0000000
--- a/cc/examples/helloworld/hello_world.cc
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for testing Tink-primitives.
-// It requires 5 arguments:
-//   keyset-file:  name of the file with the keyset to be used for encryption
-//   operation: the actual AEAD-operation, i.e. "encrypt" or "decrypt"
-//   input-file:  name of the file with input (plaintext for encryption, or
-//                or ciphertext for decryption)
-//   associated-data:  a string to be used as assciated data
-//   output-file:  name of the file for the resulting output
-
-#include <fstream>
-#include <iostream>
-#include <sstream>
-#include <string>
-#include <utility>
-
-#include "tink/aead.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "tink/config.h"
-#include "tink/config/tink_config.h"
-#include "tink/json_keyset_reader.h"
-#include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
-
-namespace {
-
-// Helper functions.
-// Upon failure each function writes an error message, and terminates.
-
-// Initializes Tink.
-void InitTink() {
-  std::clog << "Initializing Tink...\n";
-  auto status = crypto::tink::TinkConfig::Register();
-  if (!status.ok()) {
-    std::clog << "Initialization of Tink failed: " << status.message()
-              << std::endl;
-    exit(1);
-  }
-}
-
-// Creates a KeysetReader that reads a JSON-formatted keyset
-// from the given file.
-std::unique_ptr<crypto::tink::KeysetReader> GetJsonKeysetReader(
-    const std::string& filename) {
-  std::clog << "Creating a JsonKeysetReader...\n";
-  std::unique_ptr<std::ifstream> keyset_stream(new std::ifstream());
-  keyset_stream->open(filename, std::ifstream::in);
-  auto keyset_reader_result =
-      crypto::tink::JsonKeysetReader::New(std::move(keyset_stream));
-  if (!keyset_reader_result.ok()) {
-    std::clog << "Creation of the reader failed: "
-              << keyset_reader_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_reader_result.value());
-}
-
-// Creates a KeysetHandle that for a keyset read from the given file,
-// which is expected to contain a JSON-formatted keyset.
-std::unique_ptr<crypto::tink::KeysetHandle> ReadKeyset(
-    const std::string& filename) {
-  auto keyset_reader = GetJsonKeysetReader(filename);
-  auto keyset_handle_result =
-      crypto::tink::CleartextKeysetHandle::Read(std::move(keyset_reader));
-  if (!keyset_handle_result.ok()) {
-    std::clog << "Reading the keyset failed: "
-              << keyset_handle_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_handle_result.value());
-}
-
-// Reads the specified file and returns the read content as a string.
-std::string Read(const std::string& filename) {
-  std::clog << "Reading the input...\n";
-  std::ifstream input_stream;
-  input_stream.open(filename, std::ifstream::in);
-  if (!input_stream.is_open()) {
-    std::clog << "Error opening input file " << filename << std::endl;
-    exit(1);
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  input_stream.close();
-  return input.str();
-}
-
-// Writes the given string to the specified file.
-void Write(const std::string& output, const std::string& filename) {
-  std::clog << "Writing the output...\n";
-  std::ofstream output_stream(filename,
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    std::clog << "Error opening output file " << filename << std::endl;
-    exit(1);
-  }
-  output_stream << output;
-  output_stream.close();
-}
-
-}  // namespace
-
-int main(int argc, char** argv) {
-  if (argc != 6) {
-    std::clog << "Usage: " << argv[0]
-         << "  keyset-file operation input-file associated-data output-file\n";
-    exit(1);
-  }
-
-  std::string keyset_filename(argv[1]);
-  std::string operation(argv[2]);
-  std::string input_filename(argv[3]);
-  std::string associated_data(argv[4]);
-  std::string output_filename(argv[5]);
-  if (!(operation == "encrypt" || operation == "decrypt")) {
-    std::clog << "Unknown operation '" << operation << "'.\n"
-              << "Expected 'encrypt' or 'decrypt'.\n";
-    exit(1);
-  }
-  std::clog << "Using keyset from file " << keyset_filename
-            << " to AEAD-" << operation
-            << " file "<< input_filename
-            << " with associated data '" << associated_data << "'.\n"
-            << "The resulting output will be written to file "
-            << output_filename << std::endl;
-
-  // Init Tink;
-  InitTink();
-
-  // Read the keyset.
-  auto keyset_handle = ReadKeyset(keyset_filename);
-
-  // Get the primitive.
-  auto primitive_result = keyset_handle->GetPrimitive<crypto::tink::Aead>();
-  if (!primitive_result.ok()) {
-    std::clog << "Getting AEAD-primitive from the factory failed: "
-              << primitive_result.status().message() << std::endl;
-    exit(1);
-  }
-  std::unique_ptr<crypto::tink::Aead> aead =
-      std::move(primitive_result.value());
-
-  // Read the input.
-  std::string input = Read(input_filename);
-
-  // Compute the output.
-  std::clog << operation << "ing...\n";
-  std::string output;
-  if (operation == "encrypt") {
-    auto encrypt_result = aead->Encrypt(input, associated_data);
-    if (!encrypt_result.ok()) {
-      std::clog << "Error while encrypting the input:"
-                << encrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = encrypt_result.value();
-  } else {  // operation == "decrypt"
-    auto decrypt_result = aead->Decrypt(input, associated_data);
-    if (!decrypt_result.ok()) {
-      std::clog << "Error while decrypting the input:"
-                << decrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = decrypt_result.value();
-  }
-
-  // Write the output to the output file.
-  Write(output, output_filename);
-
-  std::clog << "All done.\n";
-  return 0;
-}
diff --git a/cc/examples/helloworld/hello_world_test.sh b/cc/examples/helloworld/hello_world_test.sh
deleted file mode 100755
index aaf5dfb..0000000
--- a/cc/examples/helloworld/hello_world_test.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-#!/bin/bash
-
-#############################################################################
-##### Tests for hello_world binary.
-
-HELLO_WORLD_CLI="$1"
-KEYSET_FILE="$2"
-PLAINTEXT_FILE="$TEST_TMPDIR/example_plaintext.txt"
-CIPHERTEXT_FILE="$TEST_TMPDIR/ciphertext.bin"
-DECRYPTED_FILE="$TEST_TMPDIR/decrypted.txt"
-AAD_TEXT="some associated data"
-
-#############################################################################
-
-##### Create a plaintext.
-echo "This is some message to be encrypted." > $PLAINTEXT_FILE
-
-##### Run encryption & decryption.
-$HELLO_WORLD_CLI $KEYSET_FILE encrypt $PLAINTEXT_FILE "$AAD_TEXT" $CIPHERTEXT_FILE
-$HELLO_WORLD_CLI $KEYSET_FILE decrypt $CIPHERTEXT_FILE "$AAD_TEXT" $DECRYPTED_FILE
-
-##### Check that decryption is correct.
-diff -q $DECRYPTED_FILE $PLAINTEXT_FILE
-if [ $? -ne 0 ]; then
-  echo "--- Failure: the decrypted file differs from the original plaintext."
-  diff $DECRYPTED_FILE $PLAINTEXT_FILE
-  exit 1
-fi
-echo "+++ Success: decryption was correct."
diff --git a/cc/examples/hybrid_encryption/BUILD.bazel b/cc/examples/hybrid_encryption/BUILD.bazel
index 39c43b7..dc2f7c7 100644
--- a/cc/examples/hybrid_encryption/BUILD.bazel
+++ b/cc/examples/hybrid_encryption/BUILD.bazel
@@ -2,69 +2,21 @@
 
 licenses(["notice"])
 
-cc_library(
-    name = "util",
-    srcs = ["util.cc"],
-    hdrs = ["util.h"],
-    deps = [
-        "@tink_cc//:binary_keyset_reader",
-        "@tink_cc//:binary_keyset_writer",
-        "@tink_cc//:cleartext_keyset_handle",
-        "@tink_cc//:keyset_handle",
-        "@tink_cc//:keyset_reader",
-        "@tink_cc//:keyset_writer",
-        "@tink_cc//config:tink_config",
-        "@tink_cc//util:status",
-    ],
-)
-
-cc_binary(
-    name = "hybrid_encryption_cli",
-    srcs = ["hybrid_encryption_cli.cc"],
-    deps = [
-        ":util",
-        "@tink_cc//:hybrid_decrypt",
-        "@tink_cc//:hybrid_encrypt",
-        "@tink_cc//hybrid:hybrid_key_templates",
-    ],
-)
-
-sh_test(
-    name = "hybrid_encryption_cli_test",
-    size = "small",
-    srcs = ["hybrid_encryption_cli_test.sh"],
-    args = [
-        "$(rootpath :hybrid_encryption_cli)",
-    ],
-    data = [":hybrid_encryption_cli"],
-)
-
-filegroup(
-    name = "hybrid_test_keyset",
-    srcs = [
-        "hybrid_test_private_keyset.json",
-        "hybrid_test_public_keyset.json",
-    ],
-)
-
 cc_binary(
     name = "hybrid_cli",
     srcs = ["hybrid_cli.cc"],
-    data = [":hybrid_test_keyset"],
     deps = [
+        "//util",
         "@com_google_absl//absl/flags:flag",
         "@com_google_absl//absl/flags:parse",
-        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
-        "@tink_cc//:cleartext_keyset_handle",
         "@tink_cc//:hybrid_decrypt",
         "@tink_cc//:hybrid_encrypt",
-        "@tink_cc//:json_keyset_reader",
         "@tink_cc//:keyset_handle",
-        "@tink_cc//:keyset_reader",
-        "@tink_cc//config:tink_config",
         "@tink_cc//hybrid:hpke_config",
+        "@tink_cc//hybrid:hybrid_config",
         "@tink_cc//util:status",
     ],
 )
@@ -75,10 +27,10 @@
     srcs = ["hybrid_cli_test.sh"],
     args = [
         "$(rootpath :hybrid_cli)",
-        "$(rootpaths :hybrid_test_keyset)",
+        "$(rootpaths //hybrid_encryption/testdata:hpke_test_keyset)",
     ],
     data = [
         ":hybrid_cli",
-        ":hybrid_test_keyset",
+        "//hybrid_encryption/testdata:hpke_test_keyset",
     ],
 )
diff --git a/cc/examples/hybrid_encryption/CMakeLists.txt b/cc/examples/hybrid_encryption/CMakeLists.txt
new file mode 100644
index 0000000..99b6157
--- /dev/null
+++ b/cc/examples/hybrid_encryption/CMakeLists.txt
@@ -0,0 +1,20 @@
+add_executable(hybrid_cli hybrid_cli.cc)
+target_include_directories(hybrid_cli PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(hybrid_cli
+  tink::static
+  absl::check
+  absl::flags_parse
+  util)
+# Tink CMake's configuration doesn't expose tink::core::hpke_config. Remove
+# HPKE from this example when building with CMake.
+target_compile_definitions(hybrid_cli PRIVATE TINK_EXAMPLES_EXCLUDE_HPKE)
+
+add_test(
+  NAME hybrid_cli_test
+  COMMAND "${BASH_PROGRAM}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/hybrid_cli_test.sh"
+    "${CMAKE_CURRENT_BINARY_DIR}/hybrid_cli"
+    "${CMAKE_CURRENT_SOURCE_DIR}/testdata/hybrid_test_private_keyset.json"
+    "${CMAKE_CURRENT_SOURCE_DIR}/testdata/hybrid_test_public_keyset.json")
diff --git a/cc/examples/hybrid_encryption/README.md b/cc/examples/hybrid_encryption/README.md
deleted file mode 100644
index b72cb9d..0000000
--- a/cc/examples/hybrid_encryption/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# C++ Hybrid Encryption CLI
-
-This is a command-line tool that can generate
-[Hybrid Encryption](../../../docs/PRIMITIVES.md#hybrid_encryption) keys, and
-encrypt and decrypt using Hybrid encryption.
-
-It demonstrates the basic steps of using Tink, namely loading key material,
-obtaining a primitive, and using the primitive to do crypto.
-
-## Build and Run
-
-### Bazel
-
-```shell
-git clone https://github.com/google/tink
-cd tink/cc/examples
-bazel build ...
-echo "a message" > message.txt
-echo "context" > context_info.txt
-./bazel-bin/hybrid_encryption/hybrid_encryption_cli gen-private-key private_keyset.bin
-./bazel-bin/hybrid_encryption/hybrid_encryption_cli get-public-key private_keyset.bin \
-    public_keyset.bin
-./bazel-bin/hybrid_encryption/hybrid_encryption_cli encrypt public_keyset.bin \
-    message.txt context_info.txt encrypted_message.bin
-./bazel-bin/hybrid_encryption/hybrid_encryption_cli decrypt private_keyset.bin \
-    encrypted_message.bin context_info.txt decrypted_message.txt
-```
diff --git a/cc/examples/hybrid_encryption/hybrid_cli.cc b/cc/examples/hybrid_encryption/hybrid_cli.cc
index fbf958b..8c4239e 100644
--- a/cc/examples/hybrid_encryption/hybrid_cli.cc
+++ b/cc/examples/hybrid_encryption/hybrid_cli.cc
@@ -15,27 +15,23 @@
 ///////////////////////////////////////////////////////////////////////////////
 // [START hybrid-example]
 // A command-line utility for testing Tink Hybrid Encryption.
-
-#include <fstream>
 #include <iostream>
 #include <memory>
-#include <sstream>
+#include <ostream>
 #include <string>
-#include <utility>
 
 #include "absl/flags/flag.h"
 #include "absl/flags/parse.h"
-#include "absl/memory/memory.h"
-#include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
+#include "absl/log/check.h"
 #include "absl/strings/string_view.h"
-#include "tink/cleartext_keyset_handle.h"
+#include "util/util.h"
+#ifndef TINK_EXAMPLES_EXCLUDE_HPKE
 #include "tink/hybrid/hpke_config.h"
+#endif
+#include "tink/hybrid/hybrid_config.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/hybrid_encrypt.h"
-#include "tink/json_keyset_reader.h"
 #include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
 #include "tink/util/status.h"
 
 ABSL_FLAG(std::string, keyset_filename, "", "Keyset file in JSON format");
@@ -47,163 +43,107 @@
 
 namespace {
 
-using ::crypto::tink::CleartextKeysetHandle;
 using ::crypto::tink::HybridDecrypt;
 using ::crypto::tink::HybridEncrypt;
-using ::crypto::tink::JsonKeysetReader;
 using ::crypto::tink::KeysetHandle;
-using ::crypto::tink::KeysetReader;
 using ::crypto::tink::util::Status;
 using ::crypto::tink::util::StatusOr;
 
 constexpr absl::string_view kEncrypt = "encrypt";
 constexpr absl::string_view kDecrypt = "decrypt";
 
-// Creates a KeysetReader that reads a JSON-formatted keyset
-// from the given file.
-StatusOr<std::unique_ptr<KeysetReader>> GetJsonKeysetReader(
-    const std::string& filename) {
-  std::clog << "Creating a JsonKeysetReader...\n";
-  auto key_input_stream = absl::make_unique<std::ifstream>();
-  key_input_stream->open(filename, std::ifstream::in);
-  return JsonKeysetReader::New(std::move(key_input_stream));
-}
-
-// Creates a KeysetHandle that for a keyset read from the given file,
-// which is expected to contain a JSON-formatted keyset.
-StatusOr<std::unique_ptr<KeysetHandle>> ReadKeyset(
-    const std::string& filename) {
-  StatusOr<std::unique_ptr<KeysetReader>> keyset_reader =
-      GetJsonKeysetReader(filename);
-  if (!keyset_reader.ok()) {
-    return keyset_reader.status();
-  }
-  return CleartextKeysetHandle::Read(*std::move(keyset_reader));
-}
-
-// Reads `filename` and returns the read content as a string, or an error status
-// if the file does not exist.
-StatusOr<std::string> Read(const std::string& filename) {
-  std::clog << "Reading the input...\n";
-  std::ifstream input_stream;
-  input_stream.open(filename, std::ifstream::in);
-  if (!input_stream.is_open()) {
-    return Status(absl::StatusCode::kInternal,
-                  absl::StrCat("Error opening input file ", filename));
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  return input.str();
-}
-
-// Writes the given `data_to_write` to the specified file `filename`.
-Status Write(const std::string& data_to_write, const std::string& filename) {
-  std::clog << "Writing the output...\n";
-  std::ofstream output_stream(filename,
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    return Status(absl::StatusCode::kInternal,
-                  absl::StrCat("Error opening output file ", filename));
-  }
-  output_stream << data_to_write;
-  return crypto::tink::util::OkStatus();
+void ValidateParams() {
+  // [START_EXCLUDE]
+  CHECK(absl::GetFlag(FLAGS_mode) == kEncrypt ||
+        absl::GetFlag(FLAGS_mode) == kDecrypt)
+      << "Invalid mode; must be `encrypt` or `decrypt`";
+  CHECK(!absl::GetFlag(FLAGS_keyset_filename).empty())
+      << "Keyset file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_input_filename).empty())
+      << "Input file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_output_filename).empty())
+      << "Output file must be specified";
+  // [END_EXCLUDE]
 }
 
 }  // namespace
 
+namespace tink_cc_examples {
+
+Status HybridCli(absl::string_view mode, const std::string& keyset_filename,
+                 const std::string& input_filename,
+                 const std::string& output_filename,
+                 absl::string_view context_info) {
+  Status result = crypto::tink::HybridConfig::Register();
+  if (!result.ok()) return result;
+#ifndef TINK_EXAMPLES_EXCLUDE_HPKE
+  // HPKE isn't supported when using OpenSSL as a backend.
+  result = crypto::tink::RegisterHpke();
+  if (!result.ok()) return result;
+#endif
+
+  // Read the keyset from file.
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      ReadJsonCleartextKeyset(keyset_filename);
+  if (!keyset_handle.ok()) return keyset_handle.status();
+
+  // Read the input.
+  StatusOr<std::string> input_file_content = ReadFile(input_filename);
+  if (!input_file_content.ok()) return input_file_content.status();
+
+  // Compute the output.
+  std::string output;
+  if (mode == kEncrypt) {
+    // Get the hybrid encryption primitive.
+    StatusOr<std::unique_ptr<HybridEncrypt>> hybrid_encrypt_primitive =
+        (*keyset_handle)->GetPrimitive<HybridEncrypt>();
+    if (!hybrid_encrypt_primitive.ok()) {
+      return hybrid_encrypt_primitive.status();
+    }
+    // Generate the ciphertext.
+    StatusOr<std::string> encrypt_result =
+        (*hybrid_encrypt_primitive)->Encrypt(*input_file_content, context_info);
+    if (!encrypt_result.ok()) return encrypt_result.status();
+    output = encrypt_result.value();
+  } else {  // operation == kDecrypt.
+    // Get the hybrid decryption primitive.
+    StatusOr<std::unique_ptr<HybridDecrypt>> hybrid_decrypt_primitive =
+        (*keyset_handle)->GetPrimitive<HybridDecrypt>();
+    if (!hybrid_decrypt_primitive.ok()) {
+      return hybrid_decrypt_primitive.status();
+    }
+    // Recover the plaintext.
+    StatusOr<std::string> decrypt_result =
+        (*hybrid_decrypt_primitive)->Decrypt(*input_file_content, context_info);
+    if (!decrypt_result.ok()) return decrypt_result.status();
+    output = decrypt_result.value();
+  }
+
+  // Write the output to the output file.
+  return WriteToFile(output, output_filename);
+}
+
+}  // namespace tink_cc_examples
+
 int main(int argc, char** argv) {
   absl::ParseCommandLine(argc, argv);
 
+  ValidateParams();
+
   std::string mode = absl::GetFlag(FLAGS_mode);
   std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
   std::string input_filename = absl::GetFlag(FLAGS_input_filename);
   std::string output_filename = absl::GetFlag(FLAGS_output_filename);
   std::string context_info = absl::GetFlag(FLAGS_context_info);
 
-  if (mode.empty()) {
-    std::cerr << "Mode must be specified with --mode=<" << kEncrypt << "|"
-              << kDecrypt << ">." << std::endl;
-    exit(1);
-  }
-
-  if (mode != kEncrypt && mode != kDecrypt) {
-    std::cerr << "Unknown mode '" << mode << "'; "
-              << "Expected either " << kEncrypt << " or " << kDecrypt << "."
-              << std::endl;
-    exit(1);
-  }
   std::clog << "Using keyset from file " << keyset_filename << " to hybrid "
             << mode << " file " << input_filename << " with context info '"
             << context_info << "'." << std::endl;
   std::clog << "The resulting output will be written to " << output_filename
             << std::endl;
 
-  Status result = crypto::tink::RegisterHpke();
-  if (!result.ok()) {
-    std::cerr << result.message() << std::endl;
-    exit(1);
-  }
-
-  // Read the keyset from file.
-  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
-      ReadKeyset(keyset_filename);
-  if (!keyset_handle.ok()) {
-    std::cerr << keyset_handle.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Read the input.
-  StatusOr<std::string> input_file_content = Read(input_filename);
-  if (!input_file_content.ok()) {
-    std::cerr << input_file_content.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Compute the output.
-  std::clog << mode << "ing...\n";
-  std::string output;
-  if (mode == kEncrypt) {
-    // Get the hybrid encryption primitive.
-    StatusOr<std::unique_ptr<HybridEncrypt>> hybrid_encrypt_primitive =
-        (*keyset_handle)->GetPrimitive<HybridEncrypt>();
-    if (!hybrid_encrypt_primitive.ok()) {
-      std::cerr << hybrid_encrypt_primitive.status().message() << std::endl;
-      exit(1);
-    }
-    // Generate the ciphertext.
-    StatusOr<std::string> encrypt_result =
-        (*hybrid_encrypt_primitive)->Encrypt(*input_file_content, context_info);
-    if (!encrypt_result.ok()) {
-      std::cerr << encrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = encrypt_result.value();
-  } else {  // operation == kDecrypt.
-    // Get the hybrid decryption primitive.
-    StatusOr<std::unique_ptr<HybridDecrypt>> hybrid_decrypt_primitive =
-        (*keyset_handle)->GetPrimitive<HybridDecrypt>();
-    if (!hybrid_decrypt_primitive.ok()) {
-      std::cerr << hybrid_decrypt_primitive.status().message() << std::endl;
-      exit(1);
-    }
-    // Recover the plaintext.
-    StatusOr<std::string> decrypt_result =
-        (*hybrid_decrypt_primitive)->Decrypt(*input_file_content, context_info);
-    if (!decrypt_result.ok()) {
-      std::cerr << decrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = decrypt_result.value();
-  }
-
-  // Write the output to the output file.
-  Status write_result = Write(output, output_filename);
-  if (!write_result.ok()) {
-    std::cerr << write_result.message() << std::endl;
-    exit(1);
-  }
-
-  std::clog << "All done." << std::endl;
+  CHECK_OK(tink_cc_examples::HybridCli(mode, keyset_filename, input_filename,
+                                       output_filename, context_info));
   return 0;
 }
 // [END hybrid-example]
diff --git a/cc/examples/hybrid_encryption/hybrid_cli_test.sh b/cc/examples/hybrid_encryption/hybrid_cli_test.sh
index 22142c7..1909902 100755
--- a/cc/examples/hybrid_encryption/hybrid_cli_test.sh
+++ b/cc/examples/hybrid_encryption/hybrid_cli_test.sh
@@ -20,6 +20,8 @@
 # Tests for Tink C++ hybrid encryption example.
 #############################################################################
 
+: "${TEST_TMPDIR:=$(mktemp -d)}"
+
 readonly CLI="$1"
 readonly PRIVATE_KEYSET_FILE="$2"
 readonly PUBLIC_KEYSET_FILE="$3"
@@ -45,7 +47,7 @@
 }
 
 #######################################
-# Asserts that the outcome of the latest test command was the expected one.
+# Asserts that the outcome of the latest test command is 0.
 #
 # If not, it terminates the test execution.
 #
@@ -53,23 +55,29 @@
 #   TEST_STATUS
 #   TEST_NAME
 #   TEST_CASE
-# Arguments:
-#   expected_outcome: The expected outcome.
 #######################################
-_assert_test_command_outcome() {
-  expected_outcome="$1"
-  if (( TEST_STATUS != expected_outcome )); then
-      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
-      exit 1
+assert_command_succeeded() {
+  if (( TEST_STATUS != 0 )); then
+    echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+    exit 1
   fi
 }
 
-assert_command_succeeded() {
-  _assert_test_command_outcome 0
-}
-
+#######################################
+# Asserts that the outcome of the latest test command is not 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
 assert_command_failed() {
-  _assert_test_command_outcome 1
+  if (( TEST_STATUS == 0 )); then
+      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+      exit 1
+  fi
 }
 
 #######################################
diff --git a/cc/examples/hybrid_encryption/hybrid_encryption_cli.cc b/cc/examples/hybrid_encryption/hybrid_encryption_cli.cc
deleted file mode 100644
index 36dd752..0000000
--- a/cc/examples/hybrid_encryption/hybrid_encryption_cli.cc
+++ /dev/null
@@ -1,266 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for generating Hybrid Encryption keys, and encrypting
-// and decrypting files using hybrid encryption.
-//
-// The first argument is the operation and it should be one of the following:
-// gen-private-key get-public-key encrypt decrypt.
-// Additional arguments depend on the operation.
-//
-// gen-private-key
-//   Generates a new private keyset using the RsaSsaPkcs13072Sha256F4 template.
-//   It requires 1 additional argument:
-//     output-file: name of the file for the resulting output
-//
-// get-public-key
-//   Extracts a public keyset associated with the given private keyset.
-//   It requires 2 additional arguments:
-//     private-keyset-file: name of the file with the private keyset
-//     output-file: name of the file for the resulting output
-//
-// encrypt
-//   Encrypts the message using the given public keyset.
-//   It requires 4 additional arguments:
-//     public-keyset-file: name of the file with the public keyset
-//     message-file: name of the file with the message
-//     context-info-file: name of the file with the context-info,
-//                        can also be an empty file
-//     output-file: name of the file for the resulting output
-//
-// decrypt
-//   Decrypts the encrypted message using the given private keyset.
-//   It requires 4 additional arguments:
-//     private-keyset-file: name of the file with the private keyset
-//     encrypted-message-file: name of the file with the message
-//     context-info-file: name of the file with the context-info
-//     output-file: name of the file for the decrypted message
-
-#include <iostream>
-#include <string>
-#include <utility>
-
-#include "tink/hybrid/hybrid_key_templates.h"
-#include "tink/hybrid_decrypt.h"
-#include "tink/hybrid_encrypt.h"
-#include "hybrid_encryption/util.h"
-
-// Prints usage info.
-void PrintUsageInfo() {
-  std::clog << "Usage: operation arguments\n"
-            << "where operation is one of the following:\n"
-            << "  gen-private-key get-public-key encrypt decrypt\n"
-            << "and, depending on the operation, arguments are:\n"
-            << "  gen-private-key: output-file\n"
-            << "  get-public-key: private-keyset-file output-file\n"
-            << "  encrypt: public-keyset-file message-file context-info-file"
-            << " output-file\n"
-            << "  decrypt: private-keyset-file encrypted-file context-info-file"
-            << " output-file"
-            << std::endl;
-}
-
-// Generates a new private keyset using the EciesP256HkdfHmacSha256Aes128Gcm
-// template and writes it to the output file.
-void GeneratePrivateKey(const std::string& output_filename) {
-  std::clog << "Generating a new private keyset.." << std::endl;
-
-  auto key_template =
-      crypto::tink::HybridKeyTemplates::EciesP256HkdfHmacSha256Aes128Gcm();
-  auto new_keyset_handle_result =
-      crypto::tink::KeysetHandle::GenerateNew(key_template);
-  if (!new_keyset_handle_result.ok()) {
-    std::clog << "Generating new keyset failed: "
-              << new_keyset_handle_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto keyset_handle = std::move(new_keyset_handle_result.value());
-
-  std::clog << "Writing the keyset to file " << output_filename << "..."
-            << std::endl;
-
-  Util::WriteKeyset(keyset_handle, output_filename);
-}
-
-// Extracts a public keyset associated with the given private keyset
-// and writes it to the output file.
-void ExtractPublicKey(const std::string& private_keyset_filename,
-                      const std::string& output_filename) {
-  std::clog << "Extracting a public keyset associated with the private "
-            << "keyset from file " << private_keyset_filename << "..."
-            << std::endl;
-
-  auto private_keyset_handle = Util::ReadKeyset(private_keyset_filename);
-
-  auto new_keyset_handle_result =
-      private_keyset_handle->GetPublicKeysetHandle();
-  if (!new_keyset_handle_result.ok()) {
-    std::clog << "Getting the keyset failed: "
-              << new_keyset_handle_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto public_keyset_handle = std::move(new_keyset_handle_result.value());
-
-  std::clog << "Writing the keyset to file " << output_filename << "..."
-            << std::endl;
-
-  Util::WriteKeyset(public_keyset_handle, output_filename);
-}
-
-// Encrypts the message using the given public keyset
-// and writes the encrypted message to the output file.
-void Encrypt(const std::string& keyset_filename,
-             const std::string& message_filename,
-             const std::string& context_info_filename,
-             const std::string& output_filename) {
-  auto keyset_handle = Util::ReadKeyset(keyset_filename);
-
-  auto primitive_result =
-      keyset_handle->GetPrimitive<crypto::tink::HybridEncrypt>();
-  if (!primitive_result.ok()) {
-    std::clog << "Getting HybridEncryption-primitive from the factory failed: "
-              << primitive_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto hybrid_encrypt = std::move(primitive_result.value());
-
-  std::clog << "Encrypting message from file " << message_filename
-            << " using public keyset from file " << keyset_filename << "..."
-            << std::endl;
-
-  std::string message = Util::Read(message_filename);
-  std::string context_info = Util::Read(context_info_filename);
-
-  auto encrypt_result = hybrid_encrypt->Encrypt(message, context_info);
-  if (!encrypt_result.ok()) {
-    std::clog << "Error while encrypting the message: "
-              << encrypt_result.status().message() << std::endl;
-    exit(1);
-  }
-  std::string encrypted_message = encrypt_result.value();
-
-  std::clog << "Writing the resulting encrypted message to file "
-            << output_filename << "..." << std::endl;
-
-  Util::Write(encrypted_message, output_filename);
-}
-
-// Decrypts the encrypted message using the given private keyset
-// and writes the result to the output file.
-void Decrypt(const std::string& keyset_filename,
-             const std::string& message_filename,
-             const std::string& context_info_filename,
-             const std::string& output_filename) {
-  auto keyset_handle = Util::ReadKeyset(keyset_filename);
-
-  auto primitive_result =
-      keyset_handle->GetPrimitive<crypto::tink::HybridDecrypt>();
-  if (!primitive_result.ok()) {
-    std::clog << "Getting HybridDecrypt-primitive from the factory "
-              << "failed: " << primitive_result.status().message() << std::endl;
-    exit(1);
-  }
-  auto hybrid_decrypt = std::move(primitive_result.value());
-
-  std::clog << "Decrypting the encrypted file " << message_filename
-            << " to the file " << output_filename
-            << " using private keyset from file " << keyset_filename << "..."
-            << std::endl;
-
-  std::string message = Util::Read(message_filename);
-  std::string context_info = Util::Read(context_info_filename);
-
-  std::string result;
-  auto decrypt_status = hybrid_decrypt->Decrypt(message, context_info);
-  if (!decrypt_status.ok()) {
-    std::clog << "Error while decrypting the file: "
-              << decrypt_status.status().message() << std::endl;
-    exit(1);
-  }
-
-  std::string decrypted_message = decrypt_status.value();
-
-  std::clog << "Writing the resulting decrypted message to file "
-            << output_filename << "..." << std::endl;
-
-  Util::Write(decrypted_message, output_filename);
-}
-
-int main(int argc, char** argv) {
-  if (argc == 1) {
-    PrintUsageInfo();
-    exit(1);
-  }
-
-  Util::InitTink();
-
-  std::string operation = argv[1];
-
-  if (operation == "gen-private-key") {
-    if (argc != 3) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string output_filename = argv[2];
-
-    GeneratePrivateKey(output_filename);
-  } else if (operation == "get-public-key") {
-    if (argc != 4) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string private_keyset_filename = argv[2];
-    std::string output_filename = argv[3];
-
-    ExtractPublicKey(private_keyset_filename, output_filename);
-  } else if (operation == "encrypt") {
-    if (argc != 6) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string keyset_filename = argv[2];
-    std::string message_filename = argv[3];
-    std::string context_info_filename = argv[4];
-    std::string output_filename = argv[5];
-
-    Encrypt(keyset_filename, message_filename, context_info_filename,
-            output_filename);
-  } else if (operation == "decrypt") {
-    if (argc != 6) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string keyset_filename = argv[2];
-    std::string message_filename = argv[3];
-    std::string context_info_filename = argv[4];
-    std::string output_filename = argv[5];
-
-    Decrypt(keyset_filename, message_filename, context_info_filename,
-            output_filename);
-  } else {
-    std::clog << "Unknown operation. Supported operations are: "
-              << "gen-private-key get-public-key encrypt decrypt" << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Done!" << std::endl;
-
-  return 0;
-}
diff --git a/cc/examples/hybrid_encryption/hybrid_encryption_cli_test.sh b/cc/examples/hybrid_encryption/hybrid_encryption_cli_test.sh
deleted file mode 100755
index d60d681..0000000
--- a/cc/examples/hybrid_encryption/hybrid_encryption_cli_test.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-#############################################################################
-#### Tests for hybrid_encrypt_cli binary.
-
-HYBRID_CLI="$1"
-
-PRIVATE_KEYSET_FILE="$TEST_TMPDIR/private_keyset.bin"
-PUBLIC_KEYSET_FILE="$TEST_TMPDIR/public_keyset.bin"
-MESSAGE_FILE="$TEST_TMPDIR/message.txt"
-CONTEXT_INFO_FILE="$TEST_TMPDIR/context_info.txt"
-ENCRYPTED_MESSAGE_FILE="$TEST_TMPDIR/encrypted_message.bin"
-DECRYPTED_MESSAGE_FILE="$TEST_TMPDIR/decrypted_message.txt"
-RESULT_FILE="$TEST_TMPDIR/result.txt"
-
-OTHER_PRIVATE_KEYSET_FILE="$TEST_TMPDIR/other_private_keyset.bin"
-OTHER_PUBLIC_KEYSET_FILE="$TEST_TMPDIR/other_public_keyset.bin"
-OTHER_MESSAGE_FILE="$TEST_TMPDIR/other_message.txt"
-
-echo "This is a message." > $MESSAGE_FILE
-echo "This is a different message." > $OTHER_MESSAGE_FILE
-echo "context" > $CONTEXT_INFO_FILE
-echo "different context" > $OTHER_CONTEXT_INFO_FILE
-
-#############################################################################
-#### Helper function that checks if values are equal.
-
-assert_equal() {
-  if [ "$1" == "$2" ]; then
-    echo "+++ Success: values are equal."
-  else
-    echo "--- Failure: values are different. Expected: [$1], actual: [$2]."
-    exit 1
-  fi
-}
-
-#############################################################################
-#### All good, everything should work.
-test_name="all_good"
-echo "+++ Starting test $test_name..."
-
-#### Generate a private key and get a public key.
-$HYBRID_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$HYBRID_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-
-#### Encrypt the message.
-$HYBRID_CLI encrypt $PUBLIC_KEYSET_FILE $MESSAGE_FILE $CONTEXT_INFO_FILE $ENCRYPTED_MESSAGE_FILE || exit 1
-
-#### Decrypt the encrypted message.
-$HYBRID_CLI decrypt $PRIVATE_KEYSET_FILE $ENCRYPTED_MESSAGE_FILE $CONTEXT_INFO_FILE $DECRYPTED_MESSAGE_FILE || exit 1
-
-#### Check that the decrypted message is same as original message.
-DECRYPTED_MESSAGE=$(<$DECRYPTED_MESSAGE_FILE)
-ORIGINAL_MESSAGE=$(<$MESSAGE_FILE)
-assert_equal "$ORIGINAL_MESSAGE" "$DECRYPTED_MESSAGE"
-
-#############################################################################
-#### Bad private key when getting the public key.
-test_name="get_public_key_with_bad_private_key"
-echo "+++ Starting test $test_name..."
-
-echo "abcd" >> $PRIVATE_KEYSET_FILE
-$HYBRID_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
-
-#############################################################################
-#### Decrypting a bad encrypted file.
-test_name="decrypt_a_bad_file"
-echo "+++ Starting test $test_name..."
-
-$HYBRID_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$HYBRID_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$HYBRID_CLI encrypt $PUBLIC_KEYSET_FILE $MESSAGE_FILE $CONTEXT_INFO_FILE $ENCRYPTED_MESSAGE_FILE || exit 1
-$HYBRID_CLI decrypt $PRIVATE_KEYSET_FILE $OTHER_MESSAGE_FILE $CONTEXT_INFO_FILE $RESULT_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
-
-#############################################################################
-#### Encrypt with wrong key.
-test_name="encrypt_with_wrong_key"
-echo "+++ Starting test $test_name..."
-
-$HYBRID_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$HYBRID_CLI encrypt $PRIVATE_KEYSET_FILE $MESSAGE_FILE $ENCRYPTED_MESSAGE_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
-
-#############################################################################
-#### Decrypt with wrong key.
-test_name="decrypt_with_wrong_key"
-echo "+++ Starting test $test_name..."
-
-$HYBRID_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$HYBRID_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$HYBRID_CLI encrypt $PUBLIC_KEYSET_FILE $MESSAGE_FILE $CONTEXT_INFO_FILE $ENCRYPTED_MESSAGE_FILE || exit 1
-$HYBRID_CLI decrypt $PUBLIC_KEYSET_FILE $ENCRYPTED_MESSAGE_FILE $CONTEXT_INFO_FILE $RESULT_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
-
-#############################################################################
-#### Decrypt with different context.
-test_name="decrypt_with_different_context"
-echo "+++ Starting test $test_name..."
-
-$HYBRID_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$HYBRID_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$HYBRID_CLI encrypt $PUBLIC_KEYSET_FILE $MESSAGE_FILE $CONTEXT_INFO_FILE $ENCRYPTED_MESSAGE_FILE || exit 1
-$HYBRID_CLI decrypt $PRIVATE_KEYSET_FILE $ENCRYPTED_MESSAGE_FILE $OTHER_CONTEXT_INFO_FILE $RESULT_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
diff --git a/cc/examples/hybrid_encryption/testdata/BUILD.bazel b/cc/examples/hybrid_encryption/testdata/BUILD.bazel
new file mode 100644
index 0000000..323479b
--- /dev/null
+++ b/cc/examples/hybrid_encryption/testdata/BUILD.bazel
@@ -0,0 +1,19 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+filegroup(
+    name = "hpke_test_keyset",
+    srcs = [
+        "hpke_test_private_keyset.json",
+        "hpke_test_public_keyset.json",
+    ],
+)
+
+filegroup(
+    name = "hybrid_test_keyset",
+    srcs = [
+        "hybrid_test_private_keyset.json",
+        "hybrid_test_public_keyset.json",
+    ],
+)
diff --git a/cc/examples/hybrid_encryption/hybrid_test_private_keyset.json b/cc/examples/hybrid_encryption/testdata/hpke_test_private_keyset.json
similarity index 100%
rename from cc/examples/hybrid_encryption/hybrid_test_private_keyset.json
rename to cc/examples/hybrid_encryption/testdata/hpke_test_private_keyset.json
diff --git a/cc/examples/hybrid_encryption/hybrid_test_public_keyset.json b/cc/examples/hybrid_encryption/testdata/hpke_test_public_keyset.json
similarity index 100%
rename from cc/examples/hybrid_encryption/hybrid_test_public_keyset.json
rename to cc/examples/hybrid_encryption/testdata/hpke_test_public_keyset.json
diff --git a/cc/examples/hybrid_encryption/testdata/hybrid_test_private_keyset.json b/cc/examples/hybrid_encryption/testdata/hybrid_test_private_keyset.json
new file mode 100644
index 0000000..b237360
--- /dev/null
+++ b/cc/examples/hybrid_encryption/testdata/hybrid_test_private_keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 548859458,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey",
+        "value": "EowBEkQKBAgCEAMSOhI4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAEYARohAKjjAxgGmD9j90UyzNunoC04kWqaWiXGFRhOYfLS7Z2tIiEAhqqb+D0Din92zHwGQefzui0hma5khIZQCWyWHHVgNpsaIBQrEEuEn3hClVKM+4bsvmaUOqFYMbl7E6lNFJzbr+lp",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 548859458,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/hybrid_encryption/testdata/hybrid_test_public_keyset.json b/cc/examples/hybrid_encryption/testdata/hybrid_test_public_keyset.json
new file mode 100644
index 0000000..ddb1095
--- /dev/null
+++ b/cc/examples/hybrid_encryption/testdata/hybrid_test_public_keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 548859458,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey",
+        "value": "EkQKBAgCEAMSOhI4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAEYARohAKjjAxgGmD9j90UyzNunoC04kWqaWiXGFRhOYfLS7Z2tIiEAhqqb+D0Din92zHwGQefzui0hma5khIZQCWyWHHVgNps=",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 548859458,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/hybrid_encryption/util.cc b/cc/examples/hybrid_encryption/util.cc
deleted file mode 100644
index 3a9760f..0000000
--- a/cc/examples/hybrid_encryption/util.cc
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "hybrid_encryption/util.h"
-
-#include <fstream>
-#include <iostream>
-#include <string>
-#include <utility>
-
-#include "tink/binary_keyset_reader.h"
-#include "tink/binary_keyset_writer.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "tink/config.h"
-#include "tink/config/tink_config.h"
-#include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
-#include "tink/keyset_writer.h"
-#include "tink/util/status.h"
-
-using crypto::tink::BinaryKeysetReader;
-using crypto::tink::BinaryKeysetWriter;
-using crypto::tink::CleartextKeysetHandle;
-using crypto::tink::KeysetHandle;
-using crypto::tink::KeysetReader;
-using crypto::tink::KeysetWriter;
-using crypto::tink::TinkConfig;
-
-// static
-std::unique_ptr<KeysetReader> Util::GetBinaryKeysetReader(
-    const std::string& filename) {
-  std::unique_ptr<std::ifstream> keyset_stream(new std::ifstream());
-  keyset_stream->open(filename, std::ifstream::in);
-  auto keyset_reader_result = BinaryKeysetReader::New(std::move(keyset_stream));
-  if (!keyset_reader_result.ok()) {
-    std::clog << "Creation of the BinaryKeysetReader failed: "
-              << keyset_reader_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_reader_result.value());
-}
-
-// static
-std::unique_ptr<KeysetWriter> Util::GetBinaryKeysetWriter(
-    const std::string& filename) {
-  std::unique_ptr<std::ofstream> keyset_stream(new std::ofstream());
-  keyset_stream->open(filename, std::ofstream::out);
-  auto keyset_writer_result = BinaryKeysetWriter::New(std::move(keyset_stream));
-  if (!keyset_writer_result.ok()) {
-    std::clog << "Creation of the BinaryKeysetWriter failed: "
-              << keyset_writer_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_writer_result.value());
-}
-
-// static
-std::unique_ptr<KeysetHandle> Util::ReadKeyset(const std::string& filename) {
-  auto keyset_reader = GetBinaryKeysetReader(filename);
-  auto keyset_handle_result =
-      CleartextKeysetHandle::Read(std::move(keyset_reader));
-  if (!keyset_handle_result.ok()) {
-    std::clog << "Reading the keyset failed: "
-              << keyset_handle_result.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(keyset_handle_result.value());
-}
-
-// static
-void Util::WriteKeyset(const std::unique_ptr<KeysetHandle>& keyset_handle,
-                       const std::string& filename) {
-  auto keyset_writer = GetBinaryKeysetWriter(filename);
-  auto status =
-      CleartextKeysetHandle::Write(keyset_writer.get(), *keyset_handle);
-  if (!status.ok()) {
-    std::clog << "Writing the keyset failed: " << status.message() << std::endl;
-    exit(1);
-  }
-}
-
-// static
-void Util::InitTink() {
-  auto status = TinkConfig::Register();
-  if (!status.ok()) {
-    std::clog << "Initialization of Tink failed: " << status.message()
-              << std::endl;
-    exit(1);
-  }
-}
-
-// static
-std::string Util::Read(const std::string& filename) {
-  std::ifstream input_stream;
-  input_stream.open(filename, std::ifstream::in);
-  if (!input_stream.is_open()) {
-    std::clog << "Error opening input file " << filename << std::endl;
-    exit(1);
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  input_stream.close();
-  return input.str();
-}
-
-// static
-void Util::Write(const std::string& output, const std::string& filename) {
-  std::ofstream output_stream(filename,
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    std::clog << "Error opening output file " << filename << std::endl;
-    exit(1);
-  }
-  output_stream << output;
-  output_stream.close();
-}
diff --git a/cc/examples/hybrid_encryption/util.h b/cc/examples/hybrid_encryption/util.h
deleted file mode 100644
index 072fb14..0000000
--- a/cc/examples/hybrid_encryption/util.h
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_EXAMPLES_HYBRID_ENCRYPTION_UTIL_H_
-#define TINK_EXAMPLES_HYBRID_ENCRYPTION_UTIL_H_
-
-#include <fstream>
-#include <iostream>
-#include <string>
-
-#include "tink/keyset_handle.h"
-
-// Helper functions for Digital Signatures CLI
-class Util {
- public:
-  // Returns a BinaryKeysetReader that reads from the specified file.
-  // In case of errors writes a log message and aborts.
-  static std::unique_ptr<crypto::tink::KeysetReader> GetBinaryKeysetReader(
-      const std::string& filename);
-
-  // Returns a BinaryKeysetWriter that writes from the specified file.
-  // In case of errors writes a log message and aborts.
-  static std::unique_ptr<crypto::tink::KeysetWriter> GetBinaryKeysetWriter(
-      const std::string& filename);
-
-  // Reads a keyset from the specified file.
-  // In case of errors writes a log message and aborts.
-  static std::unique_ptr<crypto::tink::KeysetHandle> ReadKeyset(
-      const std::string& filename);
-
-  // Writes the keyset to the specified file.
-  // In case of errors writes a log message and aborts.
-  static void WriteKeyset(
-      const std::unique_ptr<crypto::tink::KeysetHandle>& keyset_handle,
-      const std::string& filename);
-
-  // Initializes Tink registry.
-  // In case of errors writes a log message and aborts.
-  static void InitTink();
-
-  // Reads the specified file and returns the contents as a string.
-  // In case of errors writes a log message and aborts.
-  static std::string Read(const std::string& filename);
-
-  // Writes the given 'output' to the specified file.
-  // In case of errors writes a log message and aborts.
-  static void Write(const std::string& output, const std::string& filename);
-};
-
-#endif  // TINK_EXAMPLES_HYBRID_ENCRYPTION_UTIL_H_
diff --git a/cc/examples/jwt/BUILD.bazel b/cc/examples/jwt/BUILD.bazel
index 5c38728..e35bad5 100644
--- a/cc/examples/jwt/BUILD.bazel
+++ b/cc/examples/jwt/BUILD.bazel
@@ -2,19 +2,11 @@
 
 licenses(["notice"])
 
-cc_library(
-    name = "util",
-    srcs = ["util.cc"],
-    hdrs = ["util.h"],
-    deps = [
-        "@com_google_absl//absl/strings",
-        "@tink_cc//:binary_keyset_reader",
-        "@tink_cc//:binary_keyset_writer",
-        "@tink_cc//:cleartext_keyset_handle",
-        "@tink_cc//:keyset_handle",
-        "@tink_cc//:keyset_reader",
-        "@tink_cc//:keyset_writer",
-        "@tink_cc//util:status",
+filegroup(
+    name = "jwt_signature_keysets",
+    srcs = [
+        "jwt_signature_private_keyset.json",
+        "jwt_signature_public_keyset.json",
     ],
 )
 
@@ -22,14 +14,17 @@
     name = "jwt_signature_cli",
     srcs = ["jwt_signature_cli.cc"],
     deps = [
-        ":util",
-        "@tink_cc//jwt:jwt_key_templates",
+        "//util",
+        "@com_google_absl//absl/flags:flag",
+        "@com_google_absl//absl/flags:parse",
+        "@com_google_absl//absl/log:check",
+        "@tink_cc//:keyset_handle",
         "@tink_cc//jwt:jwt_public_key_sign",
         "@tink_cc//jwt:jwt_public_key_verify",
         "@tink_cc//jwt:jwt_signature_config",
         "@tink_cc//jwt:jwt_validator",
         "@tink_cc//jwt:raw_jwt",
-        "@tink_cc//jwt:verified_jwt",
+        "@tink_cc//util:status",
     ],
 )
 
@@ -39,6 +34,10 @@
     srcs = ["jwt_signature_cli_test.sh"],
     args = [
         "$(rootpath :jwt_signature_cli)",
+        "$(rootpaths :jwt_signature_keysets)",
     ],
-    data = [":jwt_signature_cli"],
+    data = [
+        ":jwt_signature_cli",
+        ":jwt_signature_keysets",
+    ],
 )
diff --git a/cc/examples/jwt/CMakeLists.txt b/cc/examples/jwt/CMakeLists.txt
new file mode 100644
index 0000000..3bced16
--- /dev/null
+++ b/cc/examples/jwt/CMakeLists.txt
@@ -0,0 +1,18 @@
+add_executable(jwt_signature_cli jwt_signature_cli.cc)
+target_include_directories(jwt_signature_cli PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(jwt_signature_cli
+  tink::static
+  tink::jwt::jwt_signature_config
+  absl::check
+  absl::flags_parse
+  util)
+
+add_test(
+  NAME jwt_signature_cli_test
+  COMMAND "${BASH_PROGRAM}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwt_signature_cli_test.sh"
+    "${CMAKE_CURRENT_BINARY_DIR}/jwt_signature_cli"
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwt_signature_private_keyset.json"
+    "${CMAKE_CURRENT_SOURCE_DIR}/jwt_signature_public_keyset.json")
diff --git a/cc/examples/jwt/README.md b/cc/examples/jwt/README.md
deleted file mode 100644
index 6867d75..0000000
--- a/cc/examples/jwt/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# C++ JWT Signatures CLI
-
-This is a command-line utility for generating JSON Web Token (JWT) keys, and
-creating and verifying JWTs.
-
-It demonstrates the basic steps of using Tink, namely loading key material,
-obtaining a primitive, and using the primitive to do crypto.
-
-## Build and Run
-
-### Bazel
-
-```shell
-git clone https://github.com/google/tink
-cd tink/cc/examples
-bazel build ...
-./bazel-bin/jwt/jwt_signature_cli gen-private-key private_keyset.bin
-./bazel-bin/jwt/jwt_signature_cli get-public-key private_keyset.bin \
-    public_keyset.bin
-./bazel-bin/jwt/jwt_signature_cli sign private_keyset.bin \
-    my-audience token.txt
-./bazel-bin/jwt/jwt_signature_cli verify public_keyset.bin \
-    my-audience token.txt result.txt
-cat result.txt
-```
diff --git a/cc/examples/jwt/jwt_signature_cli.cc b/cc/examples/jwt/jwt_signature_cli.cc
index 8ffc229..34f6b05 100644
--- a/cc/examples/jwt/jwt_signature_cli.cc
+++ b/cc/examples/jwt/jwt_signature_cli.cc
@@ -13,276 +13,131 @@
 // limitations under the License.
 //
 ///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for generating JSON Web Token (JWT) keys, and creating
-// and verifying JWTs.
-//
-// The first argument is the operation and it should be one of the following:
-// gen-private-key get-public-key sign verify.
-// Additional arguments depend on the operation.
-//
-// gen-private-key
-//   Generates a new private keyset using JwtRs256_2048_F4_Template.
-//   It requires 1 additional argument:
-//     output-file: name of the file for the resulting output
-//
-// get-public-key
-//   Extracts a public keyset associated with the given private keyset.
-//   It requires 2 additional arguments:
-//     private-keyset-file: name of the file with the private keyset
-//     output-file: name of the file for the resulting output
-//
-// sign
-//   Generates and signs a token using the given private keyset.
-//   It requires 3 additional arguments:
-//     private-keyset-file: name of the file with the private keyset
-//     audience: audience claim to be put in the token.
-//     output-file: name of the file for the resulting output
-//
-// verify
-//   Verifies a token using the given public keyset.
-//   It requires 4 additional arguments:
-//     public-keyset-file: name of the file with the public keyset
-//     audience: expected audience in the token
-//     token-file: name of the file with the token
-//     output-file: name of the file for the resulting output (valid/invalid)
-
+// [START jwt-example]
+// A utility for creating, signing and verifying JSON Web Tokens (JWT).
 #include <iostream>
+#include <memory>
+#include <ostream>
 #include <string>
+#include <utility>
 
-#include "tink/jwt/jwt_key_templates.h"
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+#include "absl/log/check.h"
+#include "util/util.h"
 #include "tink/jwt/jwt_public_key_sign.h"
 #include "tink/jwt/jwt_public_key_verify.h"
 #include "tink/jwt/jwt_signature_config.h"
 #include "tink/jwt/jwt_validator.h"
 #include "tink/jwt/raw_jwt.h"
-#include "tink/jwt/verified_jwt.h"
-#include "jwt/util.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/status.h"
 
-// Prints usage info.
-using crypto::tink::JwtPublicKeySign;
-using crypto::tink::JwtPublicKeyVerify;
-using crypto::tink::JwtValidator;
-using crypto::tink::KeysetHandle;
-using crypto::tink::RawJwt;
-using crypto::tink::RawJwtBuilder;
+ABSL_FLAG(std::string, keyset_filename, "", "Keyset file in JSON format");
+ABSL_FLAG(std::string, mode, "", "Mode of operation (sign|verify)");
+ABSL_FLAG(std::string, audience, "", "Expected audience in the token");
+ABSL_FLAG(std::string, token_filename, "", "Path to the token file");
 
-void PrintUsageInfo() {
-  std::clog << "Usage: operation arguments\n"
-            << "where operation is one of the following:\n"
-            << "  gen-private-key get-public-key sign verify\n"
-            << "and, depending on the operation, arguments are:\n"
-            << "  gen-private-key: output-file\n"
-            << "  get-public-key: private-keyset-file output-file\n"
-            << "  sign: private-keyset-file audience output-file\n"
-            << "  verify: public-keyset-file audience token-file "
-            << "output-file" << std::endl;
+namespace {
+
+using ::crypto::tink::JwtPublicKeySign;
+using ::crypto::tink::JwtPublicKeyVerify;
+using ::crypto::tink::JwtValidator;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::RawJwt;
+using ::crypto::tink::RawJwtBuilder;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+
+constexpr absl::string_view kSign = "sign";
+constexpr absl::string_view kVerify = "verify";
+
+void ValidateParams() {
+  // [START_EXCLUDE]
+  CHECK(absl::GetFlag(FLAGS_mode) == kSign ||
+        absl::GetFlag(FLAGS_mode) == kVerify)
+      << "Invalid mode; must be `" << kSign << "` or `" << kVerify << "`"
+      << std::endl;
+  CHECK(!absl::GetFlag(FLAGS_keyset_filename).empty())
+      << "Keyset file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_audience).empty())
+      << "Expected audience in the token must be specified";
+  CHECK(!absl::GetFlag(FLAGS_token_filename).empty())
+      << "Token file must be specified";
+  // [END_EXCLUDE]
 }
 
-// Generates a new private keyset using JwtRs256_2048_F4_Template and writes it
-// to the output file.
-void GeneratePrivateKey(absl::string_view output_filename) {
-  std::clog << "Generating a new private keyset.." << std::endl;
+}  // namespace
 
-  auto key_template = crypto::tink::JwtRs256_2048_F4_Template();
-  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
-      crypto::tink::KeysetHandle::GenerateNew(key_template);
-  if (!keyset_handle.ok()) {
-    std::clog << "Generating new keyset failed: "
-              << keyset_handle.status().message() << std::endl;
-    exit(1);
+namespace tink_cc_examples {
+
+// JWT example CLI implementation.
+Status JwtCli(absl::string_view mode, const std::string& keyset_filename,
+              absl::string_view audience, const std::string& token_filename) {
+  Status result = crypto::tink::JwtSignatureRegister();
+  if (!result.ok()) return result;
+
+  // Read the keyset from file.
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      ReadJsonCleartextKeyset(keyset_filename);
+  if (!keyset_handle.ok()) return keyset_handle.status();
+
+  if (mode == kSign) {
+    StatusOr<RawJwt> raw_jwt =
+        RawJwtBuilder()
+            .AddAudience(audience)
+            .SetExpiration(absl::Now() + absl::Seconds(100))
+            .Build();
+    if (!raw_jwt.ok()) return raw_jwt.status();
+    StatusOr<std::unique_ptr<JwtPublicKeySign>> jwt_signer =
+        (*keyset_handle)->GetPrimitive<JwtPublicKeySign>();
+    if (!jwt_signer.ok()) return jwt_signer.status();
+
+    StatusOr<std::string> token = (*jwt_signer)->SignAndEncode(*raw_jwt);
+    if (!token.ok()) return token.status();
+
+    return WriteToFile(*token, token_filename);
+  } else {  // mode == kVerify
+    // Read the token.
+    StatusOr<std::string> token = ReadFile(token_filename);
+    if (!token.ok()) return token.status();
+
+    StatusOr<JwtValidator> validator =
+        crypto::tink::JwtValidatorBuilder().ExpectAudience(audience).Build();
+    if (!validator.ok()) return validator.status();
+
+    StatusOr<std::unique_ptr<JwtPublicKeyVerify>> jwt_verifier =
+        (*keyset_handle)->GetPrimitive<JwtPublicKeyVerify>();
+    if (!jwt_verifier.ok()) return jwt_verifier.status();
+
+    return (*jwt_verifier)->VerifyAndDecode(*token, *validator).status();
   }
-  std::clog << "Writing the keyset to file " << output_filename
-            << "..." << std::endl;
-
-  WriteKeyset(**keyset_handle, output_filename);
 }
 
-// Extracts a public keyset associated with the given private keyset
-// and writes it to the output file.
-void ExtractPublicKey(absl::string_view private_keyset_filename,
-                      absl::string_view output_filename) {
-  std::clog << "Extracting a public keyset associated with the private "
-            << "keyset from file " << private_keyset_filename << "..."
-            << std::endl;
-
-  std::unique_ptr<crypto::tink::KeysetHandle> private_keyset_handle =
-      ReadKeyset(private_keyset_filename);
-
-  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
-      public_keyset_handle = private_keyset_handle->GetPublicKeysetHandle();
-  if (!public_keyset_handle.ok()) {
-    std::clog << "Getting the keyset failed: "
-              << public_keyset_handle.status().message() << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Writing the keyset to file " << output_filename
-            << "..." << std::endl;
-
-  WriteKeyset(**public_keyset_handle, output_filename);
-}
-
-// Creates and signs a token with the given audience claim using the given
-// private keyset and writes the signature to the output file.
-void Sign(absl::string_view keyset_filename, absl::string_view audience,
-          absl::string_view output_filename) {
-  std::unique_ptr<crypto::tink::KeysetHandle> keyset_handle =
-      ReadKeyset(keyset_filename);
-
-  crypto::tink::util::StatusOr<std::unique_ptr<JwtPublicKeySign>>
-      jwt_public_key_sign = keyset_handle->GetPrimitive<JwtPublicKeySign>();
-  if (!jwt_public_key_sign.ok()) {
-    std::clog << "Getting JwtPublicKeySign-primitive from the factory failed: "
-              << jwt_public_key_sign.status().message() << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Generating a token with audience '" << audience
-            << "' using private keyset from file " << keyset_filename << "..."
-            << std::endl;
-
-  crypto::tink::util::StatusOr<RawJwt> raw_jwt =
-      RawJwtBuilder()
-          .AddAudience(audience)
-          .SetExpiration(absl::Now() + absl::Seconds(100))
-          .Build();
-  if (!raw_jwt.ok()) {
-    std::clog << "Building RawJwt failed: " << raw_jwt.status().message()
-              << std::endl;
-    exit(1);
-  }
-
-  crypto::tink::util::StatusOr<std::string> token =
-      (*jwt_public_key_sign)->SignAndEncode(*raw_jwt);
-  if (!token.ok()) {
-    std::clog << "Error while generating the token: "
-              << token.status().message() << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Writing the resulting token to file " << output_filename
-            << "..." << std::endl;
-
-  WriteFile(*token, output_filename);
-}
-
-// Verifies a token using the given public keyset and writes the result to the
-// output file.
-void Verify(absl::string_view keyset_filename,
-            absl::string_view expected_audience,
-            absl::string_view token_filename,
-            absl::string_view output_filename) {
-  std::unique_ptr<KeysetHandle> keyset_handle = ReadKeyset(keyset_filename);
-
-  crypto::tink::util::StatusOr<std::unique_ptr<JwtPublicKeyVerify>> verifier =
-      keyset_handle->GetPrimitive<crypto::tink::JwtPublicKeyVerify>();
-  if (!verifier.ok()) {
-    std::clog << "Getting JwtPublicKeyVerify-primitive from the factory "
-              << "failed: " << verifier.status().message() << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Verifying token from file " << token_filename
-            << " with expected audience '" << expected_audience
-            << "' using public keyset from file " << keyset_filename << "..."
-            << std::endl;
-
-  std::string token = ReadFile(token_filename);
-
-  crypto::tink::util::StatusOr<JwtValidator> validator =
-      crypto::tink::JwtValidatorBuilder()
-          .ExpectAudience(expected_audience)
-          .Build();
-  if (!validator.ok()) {
-    std::clog << "Building validator failed: " << validator.status().message()
-              << std::endl;
-    exit(1);
-  }
-
-  std::string result;
-  crypto::tink::util::StatusOr<crypto::tink::VerifiedJwt> verified_jwt =
-      (*verifier)->VerifyAndDecode(token, *validator);
-  if (!verified_jwt.ok()) {
-    std::clog << "Error while verifying the token: "
-              << verified_jwt.status().message() << std::endl;
-    result = "invalid";
-  } else {
-    absl::Duration ttl = *verified_jwt->GetExpiration() - absl::Now();
-    std::clog << "Token is valid for " << ttl << "." << std::endl;
-    result = "valid";
-  }
-
-  std::clog << "Writing the result to file " << output_filename
-            << "..." << std::endl;
-
-  WriteFile(result, output_filename);
-}
+}  // namespace tink_cc_examples
 
 int main(int argc, char** argv) {
-  if (argc == 1) {
-    PrintUsageInfo();
-    exit(1);
-  }
+  absl::ParseCommandLine(argc, argv);
 
-  crypto::tink::util::Status status = crypto::tink::JwtSignatureRegister();
-  if (!status.ok()) {
-    std::clog << "JwtSignatureRegister() failed: " << status.message()
+  ValidateParams();
+
+  std::string mode = absl::GetFlag(FLAGS_mode);
+  std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
+  std::string audience = absl::GetFlag(FLAGS_audience);
+  std::string token_filename = absl::GetFlag(FLAGS_token_filename);
+
+  std::clog << "Using keyset in " << keyset_filename << " to ";
+  if (mode == kSign) {
+    std::clog << " generate and sign a token using audience '" << audience
+              << "'; the resulting signature is written to " << token_filename
               << std::endl;
-    exit(1);
+  } else {  // mode == kVerify
+    std::clog << " verify a token with expected audience '" << audience
+              << std::endl;
   }
 
-  std::string operation = argv[1];
-
-  if (operation == "gen-private-key") {
-    if (argc != 3) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string output_filename = argv[2];
-
-    GeneratePrivateKey(output_filename);
-  } else if (operation == "get-public-key") {
-    if (argc != 4) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string private_keyset_filename = argv[2];
-    std::string output_filename = argv[3];
-
-    ExtractPublicKey(private_keyset_filename, output_filename);
-  } else if (operation == "sign") {
-    if (argc != 5) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string keyset_filename = argv[2];
-    std::string audience = argv[3];
-    std::string output_filename = argv[4];
-
-    Sign(keyset_filename, audience, output_filename);
-  } else if (operation == "verify") {
-    if (argc != 6) {
-      PrintUsageInfo();
-      exit(1);
-    }
-
-    std::string keyset_filename = argv[2];
-    std::string audience = argv[3];
-    std::string token_filename = argv[4];
-    std::string output_filename = argv[5];
-
-    Verify(keyset_filename, audience, token_filename, output_filename);
-  } else {
-    std::clog << "Unknown operation. Supported operations are: "
-              << "gen-private-key get-public-key sign verify" << std::endl;
-    exit(1);
-  }
-
-  std::clog << "Done!" << std::endl;
-
+  CHECK_OK(tink_cc_examples::JwtCli(mode, keyset_filename, audience,
+                                    token_filename));
   return 0;
 }
+// [END jwt-example]
diff --git a/cc/examples/jwt/jwt_signature_cli_test.sh b/cc/examples/jwt/jwt_signature_cli_test.sh
index 5376edb..16999c6 100755
--- a/cc/examples/jwt/jwt_signature_cli_test.sh
+++ b/cc/examples/jwt/jwt_signature_cli_test.sh
@@ -14,124 +14,166 @@
 # limitations under the License.
 ################################################################################
 
+set -euo pipefail
 
 #############################################################################
-#### Tests for digital_signatures_cli binary.
-
-SIGNATURE_CLI="$1"
-
-PRIVATE_KEYSET_FILE="$TEST_TMPDIR/private_keyset.bin"
-PUBLIC_KEYSET_FILE="$TEST_TMPDIR/public_keyset.bin"
-TOKEN_FILE="$TEST_TMPDIR/token.bin"
-RESULT_FILE="$TEST_TMPDIR/result.txt"
-
-OTHER_PRIVATE_KEYSET_FILE="$TEST_TMPDIR/other_private_keyset.bin"
-OTHER_PUBLIC_KEYSET_FILE="$TEST_TMPDIR/other_public_keyset.bin"
-INVALID_TOKEN_FILE="$TEST_TMPDIR/invalid_token.txt"
-
-echo "invalid.token.file" > $INVALID_TOKEN_FILE
-
+# Tests for Tink C++ JWT signature example.
 #############################################################################
-#### Helper function that checks if values are equal.
 
-assert_equal() {
-  if [ "$1" == "$2" ]; then
-    echo "+++ Success: values are equal."
-  else
-    echo "--- Failure: values are different. Expected: [$1], actual: [$2]."
+: "${TEST_TMPDIR:=$(mktemp -d)}"
+
+readonly CLI="$1"
+readonly PRIVATE_KEYSET_FILE="$2"
+readonly PUBLIC_KEYSET_FILE="$3"
+readonly TOKEN_FILE="${TEST_TMPDIR}/token.json"
+readonly TEST_NAME="TinkCcExamplesJwtSignatureTest"
+
+readonly AUDIENCE="JWT audience"
+
+#######################################
+# A helper function for getting the return code of a command that may fail.
+# Temporarily disables error safety and stores return value in TEST_STATUS.
+#
+# Globals:
+#   TEST_STATUS
+# Arguments:
+#   Command to execute.
+#######################################
+test_command() {
+  set +e
+  "$@"
+  TEST_STATUS=$?
+  set -e
+}
+
+#######################################
+# Asserts that the outcome of the latest test command is 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+assert_command_succeeded() {
+  if (( TEST_STATUS != 0 )); then
+    echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
     exit 1
   fi
 }
 
-#############################################################################
-#### All good, everything should work.
-test_name="all_good"
-echo "+++ Starting test $test_name..."
+#######################################
+# Asserts that the outcome of the latest test command is not 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+assert_command_failed() {
+  if (( TEST_STATUS == 0 )); then
+      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+      exit 1
+  fi
+}
 
-#### Generate a private key and get a public key.
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
+#######################################
+# Starts a new test case; records the test case name to TEST_CASE.
+#
+# Globals:
+#   TEST_NAME
+#   TEST_CASE
+# Arguments:
+#   test_case: The name of the test case.
+#######################################
+start_test_case() {
+  TEST_CASE="$1"
+  echo "[ RUN      ] ${TEST_NAME}.${TEST_CASE}"
+}
 
-#### Sign the message.
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE audience $TOKEN_FILE || exit 1
-
-#### Verify the signature.
-$SIGNATURE_CLI verify $PUBLIC_KEYSET_FILE audience $TOKEN_FILE $RESULT_FILE || exit 1
-
-#### Check that the signature is valid.
-RESULT=$(<$RESULT_FILE)
-assert_equal "valid" "$RESULT"
+#######################################
+# Ends a test case printing a success message.
+#
+# Globals:
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+end_test_case() {
+  echo "[       OK ] ${TEST_NAME}.${TEST_CASE}"
+}
 
 #############################################################################
-#### Bad private key when getting the public key.
-test_name="get_public_key_with_bad_private_key"
-echo "+++ Starting test $test_name..."
 
-echo "abcd" >> $PRIVATE_KEYSET_FILE
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE
+start_test_case "sign_verify_all_good"
 
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
+# Sign.
+test_command "${CLI}" \
+  --mode sign \
+  --keyset_filename "${PRIVATE_KEYSET_FILE}" \
+  --audience "${AUDIENCE}" \
+  --token_filename "${TOKEN_FILE}"
+assert_command_succeeded
+
+# Verify.
+test_command "${CLI}" \
+  --mode verify \
+  --keyset_filename "${PUBLIC_KEYSET_FILE}" \
+  --audience "${AUDIENCE}" \
+  --token_filename "${TOKEN_FILE}"
+assert_command_succeeded
+
+end_test_case
 
 #############################################################################
-#### Signature verification fails because the public key is wrong.
-test_name="verify_with_different_public_key"
-echo "+++ Starting test $test_name..."
 
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI gen-private-key $OTHER_PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $OTHER_PRIVATE_KEYSET_FILE $OTHER_PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE audience $TOKEN_FILE || exit 1
-$SIGNATURE_CLI verify $OTHER_PUBLIC_KEYSET_FILE audience $TOKEN_FILE $RESULT_FILE || exit 1
+start_test_case "verify_fails_with_invalid_token"
 
-RESULT=$(<$RESULT_FILE)
-assert_equal "invalid" "$RESULT"
+# Sign.
+test_command "${CLI}" \
+  --mode sign \
+  --keyset_filename "${PRIVATE_KEYSET_FILE}" \
+  --audience "${AUDIENCE}" \
+  --token_filename "${TOKEN_FILE}"
+assert_command_succeeded
+
+# Invalid token.
+echo "modified" >> "${TOKEN_FILE}"
+
+# Verify.
+test_command "${CLI}" \
+  --mode verify \
+  --keyset_filename "${PUBLIC_KEYSET_FILE}" \
+  --audience "${AUDIENCE}" \
+  --token_filename "${TOKEN_FILE}"
+assert_command_failed
+
+end_test_case
 
 #############################################################################
-#### Verification fails because the audience is wrong.
-test_name="verify_with_different_message"
-echo "+++ Starting test $test_name..."
 
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE audience $TOKEN_FILE || exit 1
-$SIGNATURE_CLI verify $PUBLIC_KEYSET_FILE other_audience $TOKEN_FILE $RESULT_FILE || exit 1
+start_test_case "verify_fails_with_invalid_audience"
 
-RESULT=$(<$RESULT_FILE)
-assert_equal "invalid" "$RESULT"
+# Sign.
+test_command "${CLI}" \
+  --mode sign \
+  --keyset_filename "${PRIVATE_KEYSET_FILE}" \
+  --audience "${AUDIENCE}" \
+  --token_filename "${TOKEN_FILE}"
+assert_command_succeeded
 
-#############################################################################
-#### Verification fails because the token ist invalid.
-test_name="verify_with_different_message"
-echo "+++ Starting test $test_name..."
+# Modify audience.
+readonly INVALID_AUDIENCE="invalid audience"
 
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI verify $PUBLIC_KEYSET_FILE audience $INVALID_TOKEN_FILE $RESULT_FILE || exit 1
+# Verify.
+test_command "${CLI}" \
+  --mode verify \
+  --keyset_filename "${PUBLIC_KEYSET_FILE}" \
+  --audience "${INVALID_AUDIENCE}" \
+  --token_filename "${TOKEN_FILE}"
+assert_command_failed
 
-RESULT=$(<$RESULT_FILE)
-assert_equal "invalid" "$RESULT"
+end_test_case
 
-#############################################################################
-#### Signing fails because we use the wrong key type.
-test_name="sign_with_wrong_key"
-echo "+++ Starting test $test_name..."
-
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI get-public-key $PRIVATE_KEYSET_FILE $PUBLIC_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PUBLIC_KEYSET_FILE audience $TOKEN_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
-
-#############################################################################
-#### Verification fails because we use the wrong key type.
-test_name="verify_with_wrong_key"
-echo "+++ Starting test $test_name..."
-
-$SIGNATURE_CLI gen-private-key $PRIVATE_KEYSET_FILE || exit 1
-$SIGNATURE_CLI sign $PRIVATE_KEYSET_FILE audience $TOKEN_FILE || exit 1
-$SIGNATURE_CLI verify $PRIVATE_KEYSET_FILE audience $TOKEN_FILE $RESULT_FILE
-
-EXIT_VALUE="$?"
-assert_equal 1 "$EXIT_VALUE"
diff --git a/cc/examples/jwt/jwt_signature_private_keyset.json b/cc/examples/jwt/jwt_signature_private_keyset.json
new file mode 100644
index 0000000..be5f4be
--- /dev/null
+++ b/cc/examples/jwt/jwt_signature_private_keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 185188009,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey",
+        "value": "EosCEAEagQIAs9iifvWObNLbP+x7zupVIYTdHKba4VFgJEnnGtIII21R+KGddTdvNGAokd4GPrFk1GDPitHrAAoW1+NWrafsEUi2J9Sy3uwEyarsKDggewoBCNg2fcWAiZXplPjUyTlhrLvTuyrcL/mGPy+ib7bdmov+D2EP+rKUH6/ydtQGiyHRR3uurTUWfrMD1/6WaBVfngpy5Pxs2nuHXRmBHQKWmPfvErgr4abdjhKDaWIuxzSise1CSAbiWTNcxpIuFYZgPjgQzpqeh93LUXIX9YJds/bhHtXqRdxk6yTisloHOZETItK/rHCCE25dLkkaJ2Li7AtnJdBc6tEUNiuFj2JCjSIDAQABGoACT2lWxwySaQbp/N3lBUZ/dJ+AKsiaWWdfNmbTfwpCwbHhwhFKv5lMpynWgCIzS7d0uDpPKhLq20eZMpaVjXRaTn92vzuyB7DbpFiukkvGO839CvS9iueMjDP/weHlwzxtHqKJKVoRg7WAS6Iy7XUngLhT5GKNdbsooJ1GSKXyhbgWyMcspKSQe4lZXUntVMK5z4iLNmcQwsBp8yM55mZra13TXowob/E/wd+tGiABCn6CDt8G1gXzWDaoF2tt6WhSGZbXUVGagmoea/BWeAuJyKSSi5h+uPpc5SPhGvyKfSEVaCs2QeM7/UIXhzAcx2j/VqySb6y9EbSiJfy8vr49QSKBAQD+AbFCGHd9kZ5LIQrfe9caOxS9pQPdFkBJESw0C3x2uBIg8awiQsuVXMeEgyGLyWBZoi2x98OMSR9OzCuSLtb7Nv0Wqn0LUj4WPRdmg//uLeD3O2rcVRIR4db/B8WvXnK2uQsqwGDyh4BepGvprXQPYMX2uwnBGL2ccS2De53HJSqBAQC1QfOi4egjmlmXqJLpISUSN1NixkIi8EJHaZZ0YrbaRrEyiJczthcazNHFt6gzgOcosFaKaZeqps4Tet+5NgS7eh7RzLQ2+cfT4ewpT2ExJ4NsOy8XDqD6GRjliLxjGAoUf24s3B+3LLACPiQjeeZGJP0ivh384WabyXXxRgHFSTKBAQChl7gKIYCbHPHEQAAnzyQ4Js/6GinMFCTPlyI09f23lUDLPpRQs4fKvNydO8Myp+ko/NjvOH1qGPbW7WLmu+++n+wA6HNmqWqgQTtK170Q7JULE/zWsTQutitN0cb82yxFfJFTIFJM2NFc5GNWpSeJxPoMDk+VTcUK6qGW3SSyFTqBAQCeaPFA3SZAV1kNjio2zNzVOr0JijOqzUdfmgv/03Xy9e1POMjMTMuMhIygu42o1XMwwEwh037Vicp4g96aw3cHUgc1XC30DgByUPRQdit/BgV5xY+2GvbdHKoBkKrz/8Jvf58OXaLqN4frrdtvlc2GaDVC89zJcUR3ym3lW0WY4UKBAQD6MCruwXaxXJMxjtlH1YT5ow4R5neeiswNfGj4Ta/WbWyiVA60zpdNbGqH+etmiHY8+aBb/H4O9+JhOcBtlMLN4UlK1jg8wPSemZjsIPiUZXHkeIUa2RTUSz90wgz7aOqC0lYsLLFaJNWs54fC9LpZ0JzoqYDI8iDPnlE7xaag9g==",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 185188009,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/jwt/jwt_signature_public_keyset.json b/cc/examples/jwt/jwt_signature_public_keyset.json
new file mode 100644
index 0000000..d6fa045
--- /dev/null
+++ b/cc/examples/jwt/jwt_signature_public_keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 185188009,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey",
+        "value": "EAEagQIAs9iifvWObNLbP+x7zupVIYTdHKba4VFgJEnnGtIII21R+KGddTdvNGAokd4GPrFk1GDPitHrAAoW1+NWrafsEUi2J9Sy3uwEyarsKDggewoBCNg2fcWAiZXplPjUyTlhrLvTuyrcL/mGPy+ib7bdmov+D2EP+rKUH6/ydtQGiyHRR3uurTUWfrMD1/6WaBVfngpy5Pxs2nuHXRmBHQKWmPfvErgr4abdjhKDaWIuxzSise1CSAbiWTNcxpIuFYZgPjgQzpqeh93LUXIX9YJds/bhHtXqRdxk6yTisloHOZETItK/rHCCE25dLkkaJ2Li7AtnJdBc6tEUNiuFj2JCjSIDAQAB",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 185188009,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/jwt/util.cc b/cc/examples/jwt/util.cc
deleted file mode 100644
index a40266b..0000000
--- a/cc/examples/jwt/util.cc
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "jwt/util.h"
-
-#include <fstream>
-#include <iostream>
-#include <string>
-#include <utility>
-
-#include "absl/strings/string_view.h"
-#include "tink/binary_keyset_reader.h"
-#include "tink/binary_keyset_writer.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
-#include "tink/keyset_writer.h"
-#include "tink/util/status.h"
-
-using crypto::tink::BinaryKeysetReader;
-using crypto::tink::BinaryKeysetWriter;
-using crypto::tink::CleartextKeysetHandle;
-using crypto::tink::KeysetHandle;
-using crypto::tink::KeysetReader;
-using crypto::tink::KeysetWriter;
-
-namespace {
-
-// Returns a BinaryKeysetReader that reads from the specified file.
-// In case of errors writes a log message and aborts.
-std::unique_ptr<KeysetReader> GetBinaryKeysetReader(
-    absl::string_view filename) {
-  std::unique_ptr<std::ifstream> keyset_stream(new std::ifstream());
-  keyset_stream->open(std::string(filename), std::ifstream::in);
-  crypto::tink::util::StatusOr<std::unique_ptr<KeysetReader>> keyset_reader =
-      BinaryKeysetReader::New(std::move(keyset_stream));
-  if (!keyset_reader.ok()) {
-    std::clog << "Creation of the BinaryKeysetReader failed: "
-              << keyset_reader.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(*keyset_reader);
-}
-
-// Returns a BinaryKeysetWriter that writes from the specified file.
-// In case of errors writes a log message and aborts.
-std::unique_ptr<KeysetWriter> GetBinaryKeysetWriter(
-    absl::string_view filename) {
-  std::unique_ptr<std::ofstream> keyset_stream(new std::ofstream());
-  keyset_stream->open(std::string(filename), std::ofstream::out);
-  crypto::tink::util::StatusOr<std::unique_ptr<BinaryKeysetWriter>>
-      keyset_writer = BinaryKeysetWriter::New(std::move(keyset_stream));
-  if (!keyset_writer.ok()) {
-    std::clog << "Creation of the BinaryKeysetWriter failed: "
-              << keyset_writer.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(*keyset_writer);
-}
-
-}  // namespace
-
-std::unique_ptr<KeysetHandle> ReadKeyset(absl::string_view filename) {
-  std::unique_ptr<crypto::tink::KeysetReader> keyset_reader =
-      GetBinaryKeysetReader(filename);
-  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
-      CleartextKeysetHandle::Read(std::move(keyset_reader));
-  if (!keyset_handle.ok()) {
-    std::clog << "Reading the keyset failed: "
-              << keyset_handle.status().message() << std::endl;
-    exit(1);
-  }
-  return std::move(*keyset_handle);
-}
-
-void WriteKeyset(const crypto::tink::KeysetHandle& keyset_handle,
-                 absl::string_view filename) {
-  std::unique_ptr<crypto::tink::KeysetWriter> keyset_writer =
-      GetBinaryKeysetWriter(filename);
-  auto status =
-      CleartextKeysetHandle::Write(keyset_writer.get(), keyset_handle);
-  if (!status.ok()) {
-    std::clog << "Writing the keyset failed: " << status.message() << std::endl;
-    exit(1);
-  }
-}
-
-std::string ReadFile(absl::string_view filename) {
-  std::ifstream input_stream;
-  input_stream.open(std::string(filename), std::ifstream::in);
-  if (!input_stream.is_open()) {
-    std::clog << "Error opening input file " << std::string(filename)
-              << std::endl;
-    exit(1);
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  input_stream.close();
-  return input.str();
-}
-
-void WriteFile(absl::string_view output, absl::string_view filename) {
-  std::ofstream output_stream(std::string(filename),
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    std::clog << "Error opening output file " << std::string(filename)
-              << std::endl;
-    exit(1);
-  }
-  output_stream << std::string(output);
-  output_stream.close();
-}
diff --git a/cc/examples/jwt/util.h b/cc/examples/jwt/util.h
deleted file mode 100644
index 9427faa..0000000
--- a/cc/examples/jwt/util.h
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_EXAMPLES_JWT_UTIL_H_
-#define TINK_EXAMPLES_JWT_UTIL_H_
-
-#include <fstream>
-#include <iostream>
-#include <string>
-
-#include "absl/strings/string_view.h"
-#include "tink/keyset_handle.h"
-
-// Helper functions for JWT Signature CLI
-
-// Reads a keyset from the specified file.
-// In case of errors writes a log message and aborts.
-std::unique_ptr<crypto::tink::KeysetHandle> ReadKeyset(
-    absl::string_view filename);
-
-// Writes the keyset to the specified file.
-// In case of errors writes a log message and aborts.
-void WriteKeyset(const crypto::tink::KeysetHandle& keyset_handle,
-                 absl::string_view filename);
-
-// Reads the specified file and returns the contents as a string.
-// In case of errors writes a log message and aborts.
-std::string ReadFile(absl::string_view filename);
-
-// Writes the given 'output' to the specified file.
-// In case of errors writes a log message and aborts.
-void WriteFile(absl::string_view output, absl::string_view filename);
-
-#endif  // TINK_EXAMPLES_JWT_UTIL_H_
diff --git a/cc/examples/key_derivation/BUILD.bazel b/cc/examples/key_derivation/BUILD.bazel
new file mode 100644
index 0000000..9f0bcd6
--- /dev/null
+++ b/cc/examples/key_derivation/BUILD.bazel
@@ -0,0 +1,34 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_binary(
+    name = "key_derivation_cli",
+    srcs = ["key_derivation_cli.cc"],
+    deps = [
+        "//util",
+        "@com_google_absl//absl/flags:flag",
+        "@com_google_absl//absl/flags:parse",
+        "@com_google_absl//absl/log:check",
+        "@tink_cc//:aead",
+        "@tink_cc//:keyset_handle",
+        "@tink_cc//aead:aead_config",
+        "@tink_cc//keyderivation:key_derivation_config",
+        "@tink_cc//keyderivation:keyset_deriver",
+        "@tink_cc//util:status",
+    ],
+)
+
+sh_test(
+    name = "key_derivation_cli_test",
+    size = "small",
+    srcs = ["key_derivation_cli_test.sh"],
+    args = [
+        "$(rootpath :key_derivation_cli)",
+        "$(rootpaths :keyset.json)",
+    ],
+    data = [
+        ":key_derivation_cli",
+        ":keyset.json",
+    ],
+)
diff --git a/cc/examples/key_derivation/CMakeLists.txt b/cc/examples/key_derivation/CMakeLists.txt
new file mode 100644
index 0000000..d81719b
--- /dev/null
+++ b/cc/examples/key_derivation/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_executable(key_derivation_cli key_derivation_cli.cc)
+target_include_directories(key_derivation_cli PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(key_derivation_cli
+  tink::static
+  absl::check
+  absl::flags_parse
+  util)
+
+add_test(
+  NAME key_derivation_cli_test
+  COMMAND "${BASH_PROGRAM}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/key_derivation_cli_test.sh"
+    "${CMAKE_CURRENT_BINARY_DIR}/key_derivation_cli"
+    "${CMAKE_CURRENT_SOURCE_DIR}/keyset.json"
diff --git a/cc/examples/key_derivation/key_derivation_cli.cc b/cc/examples/key_derivation/key_derivation_cli.cc
new file mode 100644
index 0000000..bca4ad1
--- /dev/null
+++ b/cc/examples/key_derivation/key_derivation_cli.cc
@@ -0,0 +1,143 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+// [START key-derivation-example]
+// A command-line utility for testing Tink Key Derivation.
+#include <iostream>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+#include "absl/log/check.h"
+#include "tink/aead.h"
+#include "tink/aead/aead_config.h"
+#include "util/util.h"
+#include "tink/keyderivation/key_derivation_config.h"
+#include "tink/keyderivation/keyset_deriver.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/status.h"
+
+ABSL_FLAG(std::string, keyset_filename, "",
+          "File in JSON format containing keyset that derives an AEAD keyset");
+ABSL_FLAG(std::string, salt_filename, "", "Salt file name");
+ABSL_FLAG(std::string, derived_keyset_filename, "", "Derived keyset file name");
+
+namespace {
+
+using ::crypto::tink::Aead;
+using ::crypto::tink::AeadConfig;
+using ::crypto::tink::KeyDerivationConfig;
+using ::crypto::tink::KeysetDeriver;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::util::OkStatus;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+
+void ValidateParams() {
+  // [START_EXCLUDE]
+  CHECK(!absl::GetFlag(FLAGS_keyset_filename).empty())
+      << "Keyset file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_salt_filename).empty())
+      << "Input file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_derived_keyset_filename).empty())
+      << "Output file must be specified";
+  // [END_EXCLUDE]
+}
+
+// Verifies `handle` contains a valid AEAD primitive.
+Status VerifyDerivedAeadKeyset(const KeysetHandle& handle) {
+  // [START_EXCLUDE]
+  StatusOr<std::unique_ptr<Aead>> aead = handle.GetPrimitive<Aead>();
+  if (!aead.ok()) return aead.status();
+
+  std::string plaintext = "plaintext";
+  std::string ad = "ad";
+  StatusOr<std::string> ciphertext = (*aead)->Encrypt(plaintext, ad);
+  if (!ciphertext.ok()) return ciphertext.status();
+
+  StatusOr<std::string> got = (*aead)->Decrypt(*ciphertext, ad);
+  if (!got.ok()) return got.status();
+
+  if (*got != plaintext) {
+    return Status(
+        absl::StatusCode::kInternal,
+        "AEAD obtained from derived keyset failed to decrypt correctly");
+  }
+  return OkStatus();
+  // [END_EXCLUDE]
+}
+
+}  // namespace
+
+namespace tink_cc_examples {
+
+Status KeyDerivationCli(const std::string& keyset_filename,
+                        const std::string& salt_filename,
+                        const std::string& derived_keyset_filename) {
+  Status result = KeyDerivationConfig::Register();
+  if (!result.ok()) return result;
+  result = AeadConfig::Register();
+  if (!result.ok()) return result;
+
+  // Read keyset from file.
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      ReadJsonCleartextKeyset(keyset_filename);
+  if (!keyset_handle.ok()) return keyset_handle.status();
+
+  // Get the primitive.
+  StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      (*keyset_handle)->GetPrimitive<KeysetDeriver>();
+  if (!deriver.ok()) return deriver.status();
+
+  // Read the salt.
+  StatusOr<std::string> salt_file_content = ReadFile(salt_filename);
+  if (!salt_file_content.ok()) return salt_file_content.status();
+
+  // Derive new keyset.
+  StatusOr<std::unique_ptr<KeysetHandle>> derived_handle =
+      (*deriver)->DeriveKeyset(*salt_file_content);
+  if (!derived_handle.ok()) return derived_handle.status();
+
+  Status status = VerifyDerivedAeadKeyset(**derived_handle);
+  if (!status.ok()) return status;
+
+  return WriteJsonCleartextKeyset(derived_keyset_filename, **derived_handle);
+}
+
+}  // namespace tink_cc_examples
+
+int main(int argc, char** argv) {
+  absl::ParseCommandLine(argc, argv);
+
+  ValidateParams();
+
+  std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
+  std::string salt_filename = absl::GetFlag(FLAGS_salt_filename);
+  std::string derived_keyset_filename =
+      absl::GetFlag(FLAGS_derived_keyset_filename);
+
+  std::clog << "Using keyset from file " << keyset_filename
+            << " to derive a new AEAD keyset with the salt in file "
+            << salt_filename << "." << std::endl;
+  std::clog << "The resulting derived keyset will be written to "
+            << derived_keyset_filename << "." << std::endl;
+
+  CHECK_OK(tink_cc_examples::KeyDerivationCli(keyset_filename, salt_filename,
+                                              derived_keyset_filename));
+  return 0;
+}
+// [END key-derivation-example]
diff --git a/cc/examples/key_derivation/key_derivation_cli_test.sh b/cc/examples/key_derivation/key_derivation_cli_test.sh
new file mode 100755
index 0000000..c59e7a7
--- /dev/null
+++ b/cc/examples/key_derivation/key_derivation_cli_test.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+set -euo pipefail
+
+#############################################################################
+# Tests for Tink C++ Key Derivation example.
+#############################################################################
+
+: "${TEST_TMPDIR:=$(mktemp -d)}"
+
+readonly CLI="$1"
+readonly KEYSET_FILE="$2"
+readonly SALT_FILE="${TEST_TMPDIR}/salt.txt"
+readonly DERIVED_KEYSET_FILE="${TEST_TMPDIR}/derived_keyset.json"
+readonly TEST_NAME="TinkCcExamplesKeyDerivationTest"
+
+echo "This is the salt used to derive keys." > "${SALT_FILE}"
+
+#######################################
+# A helper function for getting the return code of a command that may fail.
+# Temporarily disables error safety and stores return value in TEST_STATUS.
+#
+# Globals:
+#   TEST_STATUS
+# Arguments:
+#   Command to execute.
+#######################################
+test_command() {
+  set +e
+  "$@"
+  TEST_STATUS=$?
+  set -e
+}
+
+#######################################
+# Asserts that the outcome of the latest test command is 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+assert_command_succeeded() {
+  if (( TEST_STATUS != 0 )); then
+    echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+    exit 1
+  fi
+}
+
+#######################################
+# Asserts that the outcome of the latest test command is not 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+assert_command_failed() {
+  if (( TEST_STATUS == 0 )); then
+      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+      exit 1
+  fi
+}
+
+#######################################
+# Starts a new test case; records the test case name to TEST_CASE.
+#
+# Globals:
+#   TEST_NAME
+#   TEST_CASE
+# Arguments:
+#   test_case: The name of the test case.
+#######################################
+start_test_case() {
+  TEST_CASE="$1"
+  echo "[ RUN      ] ${TEST_NAME}.${TEST_CASE}"
+}
+
+#######################################
+# Ends a test case printing a success message.
+#
+# Globals:
+#   TEST_NAME
+#   TEST_CASE
+#######################################
+end_test_case() {
+  echo "[       OK ] ${TEST_NAME}.${TEST_CASE}"
+}
+
+#############################################################################
+
+start_test_case "derive_key"
+
+test_command "${CLI}" \
+  --keyset_filename "${KEYSET_FILE}" \
+  --salt_filename "${SALT_FILE}" \
+  --derived_keyset_filename "${DERIVED_KEYSET_FILE}"
+assert_command_succeeded
+
+end_test_case
diff --git a/cc/examples/key_derivation/keyset.json b/cc/examples/key_derivation/keyset.json
new file mode 100644
index 0000000..8703b1a
--- /dev/null
+++ b/cc/examples/key_derivation/keyset.json
@@ -0,0 +1,15 @@
+{
+  "primaryKeyId": 1746379508,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey",
+        "value": "El0KMXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkhrZGZQcmZLZXkSJhICCAMaIHq3492RGOyzGsJTQh6Xi6noTDSrPQxULHuBqB10zMUCGAEaOgo4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAE=",
+        "keyMaterialType": "SYMMETRIC"
+      },
+      "status": "ENABLED",
+      "keyId": 1746379508,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
diff --git a/cc/examples/mac/BUILD.bazel b/cc/examples/mac/BUILD.bazel
index 456fd7c..7d3b7c1 100644
--- a/cc/examples/mac/BUILD.bazel
+++ b/cc/examples/mac/BUILD.bazel
@@ -11,17 +11,14 @@
     name = "mac_cli",
     srcs = ["mac_cli.cc"],
     deps = [
+        "//util",
         "@com_google_absl//absl/flags:flag",
         "@com_google_absl//absl/flags:parse",
-        "@com_google_absl//absl/memory",
-        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/strings",
         "@tink_cc//:cleartext_keyset_handle",
-        "@tink_cc//:json_keyset_reader",
         "@tink_cc//:keyset_handle",
-        "@tink_cc//:keyset_reader",
         "@tink_cc//:mac",
-        "@tink_cc//config:tink_config",
         "@tink_cc//mac:mac_config",
         "@tink_cc//util:status",
     ],
diff --git a/cc/examples/mac/CMakeLists.txt b/cc/examples/mac/CMakeLists.txt
new file mode 100644
index 0000000..194d7d6
--- /dev/null
+++ b/cc/examples/mac/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_executable(mac_cli mac_cli.cc)
+target_include_directories(mac_cli PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(mac_cli
+  tink::static
+  absl::check
+  absl::flags_parse
+  util)
+
+add_test(
+  NAME mac_cli_test
+  COMMAND "${BASH_PROGRAM}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/mac_cli_test.sh"
+    "${CMAKE_CURRENT_BINARY_DIR}/mac_cli"
+    "${CMAKE_CURRENT_SOURCE_DIR}/mac_test_keyset.json")
diff --git a/cc/examples/mac/mac_cli.cc b/cc/examples/mac/mac_cli.cc
index 10cd06d..3a5407f 100644
--- a/cc/examples/mac/mac_cli.cc
+++ b/cc/examples/mac/mac_cli.cc
@@ -14,25 +14,22 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 // [START mac-example]
-// A command-line utility for testing Tink MAC.
+// A command-line utility for showcasing using the Tink MAC primitive.
 
 #include <fstream>
 #include <iostream>
 #include <memory>
+#include <ostream>
 #include <sstream>
 #include <string>
 #include <utility>
 
 #include "absl/flags/flag.h"
 #include "absl/flags/parse.h"
-#include "absl/memory/memory.h"
-#include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
+#include "absl/log/check.h"
 #include "absl/strings/string_view.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "tink/json_keyset_reader.h"
+#include "util/util.h"
 #include "tink/keyset_handle.h"
-#include "tink/keyset_reader.h"
 #include "tink/mac.h"
 #include "tink/mac/mac_config.h"
 #include "tink/util/status.h"
@@ -44,10 +41,7 @@
 
 namespace {
 
-using ::crypto::tink::CleartextKeysetHandle;
-using ::crypto::tink::JsonKeysetReader;
 using ::crypto::tink::KeysetHandle;
-using ::crypto::tink::KeysetReader;
 using ::crypto::tink::Mac;
 using ::crypto::tink::MacConfig;
 using ::crypto::tink::util::Status;
@@ -56,151 +50,89 @@
 constexpr absl::string_view kCompute = "compute";
 constexpr absl::string_view kVerify = "verify";
 
-// Creates a KeysetReader that reads a JSON-formatted keyset
-// from the given file.
-StatusOr<std::unique_ptr<KeysetReader>> GetJsonKeysetReader(
-    const std::string& filename) {
-  std::clog << "Creating a JsonKeysetReader...\n";
-  auto key_input_stream = absl::make_unique<std::ifstream>();
-  key_input_stream->open(filename, std::ifstream::in);
-  return JsonKeysetReader::New(std::move(key_input_stream));
-}
-
-// Creates a KeysetHandle that for a keyset read from the given file,
-// which is expected to contain a JSON-formatted keyset.
-StatusOr<std::unique_ptr<KeysetHandle>> ReadKeyset(
-    const std::string& filename) {
-  StatusOr<std::unique_ptr<KeysetReader>> keyset_reader =
-      GetJsonKeysetReader(filename);
-  if (!keyset_reader.ok()) {
-    return keyset_reader.status();
-  }
-  return CleartextKeysetHandle::Read(*std::move(keyset_reader));
-}
-
-// Reads `filename` and returns the read content as a string, or an error status
-// if the file does not exist.
-StatusOr<std::string> Read(const std::string& filename) {
-  std::clog << "Reading the input...\n";
-  std::ifstream input_stream;
-  input_stream.open(filename, std::ifstream::in);
-  if (!input_stream.is_open()) {
-    return Status(absl::StatusCode::kInternal,
-                  absl::StrCat("Error opening input file ", filename));
-  }
-  std::stringstream input;
-  input << input_stream.rdbuf();
-  return input.str();
-}
-
-// Writes the given `data_to_write` to the specified file `filename`.
-Status Write(const std::string& data_to_write, const std::string& filename) {
-  std::clog << "Writing the output...\n";
-  std::ofstream output_stream(filename,
-                              std::ofstream::out | std::ofstream::binary);
-  if (!output_stream.is_open()) {
-    return Status(absl::StatusCode::kInternal,
-                  absl::StrCat("Error opening output file ", filename));
-  }
-  output_stream << data_to_write;
-  return crypto::tink::util::OkStatus();
+void ValidateParams() {
+  // [START_EXCLUDE]
+  CHECK(absl::GetFlag(FLAGS_mode) == kCompute ||
+        absl::GetFlag(FLAGS_mode) == kVerify)
+      << "Invalid mode; must be `" << kCompute << "` or `" << kVerify << "`";
+  CHECK(!absl::GetFlag(FLAGS_keyset_filename).empty())
+      << "Keyset file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_data_filename).empty())
+      << "Data file must be specified";
+  CHECK(!absl::GetFlag(FLAGS_tag_filename).empty())
+      << "Tag file must be specified";
+  // [END_EXCLUDE]
 }
 
 }  // namespace
 
+namespace tink_cc_examples {
+
+// MAC example CLI implementation.
+Status MacCli(absl::string_view mode, const std::string keyset_filename,
+              const std::string& data_filename,
+              const std::string& tag_filename) {
+  Status result = MacConfig::Register();
+  if (!result.ok()) return result;
+
+  // Read the keyset from file.
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      ReadJsonCleartextKeyset(keyset_filename);
+  if (!keyset_handle.ok()) return keyset_handle.status();
+
+  // Get the primitive.
+  StatusOr<std::unique_ptr<Mac>> mac_primitive =
+      (*keyset_handle)->GetPrimitive<Mac>();
+  if (!mac_primitive.ok()) return mac_primitive.status();
+
+  // Read the input.
+  StatusOr<std::string> data_file_content = ReadFile(data_filename);
+  if (!data_file_content.ok()) return data_file_content.status();
+
+  std::string output;
+  if (mode == kCompute) {
+    // Compute authentication tag.
+    StatusOr<std::string> compute_result =
+        (*mac_primitive)->ComputeMac(*data_file_content);
+    if (!compute_result.ok()) return compute_result.status();
+    // Write out the authentication tag to tag file.
+    return WriteToFile(*compute_result, tag_filename);
+  } else {  // operation == kVerify.
+    // Read the authentication tag from tag file.
+    StatusOr<std::string> tag_result = ReadFile(tag_filename);
+    if (!tag_result.ok()) {
+      std::cerr << tag_result.status().message() << std::endl;
+      exit(1);
+    }
+    // Verify authentication tag.
+    Status verify_result =
+        (*mac_primitive)->VerifyMac(*tag_result, *data_file_content);
+    if (verify_result.ok()) std::clog << "Verification succeeded!" << std::endl;
+    return verify_result;
+  }
+}
+
+}  // namespace tink_cc_examples
+
 int main(int argc, char** argv) {
   absl::ParseCommandLine(argc, argv);
 
+  ValidateParams();
+
   std::string mode = absl::GetFlag(FLAGS_mode);
   std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
   std::string data_filename = absl::GetFlag(FLAGS_data_filename);
   std::string tag_filename = absl::GetFlag(FLAGS_tag_filename);
 
-  if (mode.empty()) {
-    std::cerr << "Mode must be specified with --mode=<" << kCompute << "|"
-              << kVerify << ">." << std::endl;
-    exit(1);
-  }
-
-  if (mode != kCompute && mode != kVerify) {
-    std::cerr << "Unknown mode '" << mode << "'; "
-              << "Expected either " << kCompute << " or " << kVerify << "."
-              << std::endl;
-    exit(1);
-  }
-
-  const std::string tag_file_action =
-      (mode == kCompute) ? "written to" : "read from";
   std::clog << "Using keyset from file '" << keyset_filename << "' to " << mode
             << " authentication tag from file '" << tag_filename
             << "' for data file '" << data_filename << "'." << std::endl;
-  std::clog << "The tag will be " << tag_file_action << " file '"
+  std::clog << "The tag will be "
+            << ((mode == kCompute) ? "written to" : "read from") << " file '"
             << tag_filename << "'." << std::endl;
 
-  Status result = MacConfig::Register();
-  if (!result.ok()) {
-    std::cerr << result.message() << std::endl;
-    exit(1);
-  }
-
-  // Read the keyset from file.
-  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
-      ReadKeyset(keyset_filename);
-  if (!keyset_handle.ok()) {
-    std::cerr << keyset_handle.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Get the primitive.
-  StatusOr<std::unique_ptr<Mac>> mac_primitive =
-      (*keyset_handle)->GetPrimitive<Mac>();
-  if (!mac_primitive.ok()) {
-    std::cerr << mac_primitive.status().message() << std::endl;
-    exit(1);
-  }
-
-  // Read the input.
-  StatusOr<std::string> data_file_content = Read(data_filename);
-  if (!data_file_content.ok()) {
-    std::cerr << data_file_content.status().message() << std::endl;
-    exit(1);
-  }
-
-  std::string output;
-  if (mode == kCompute) {
-    // Compute authentication tag.
-    std::clog << "Computing tag...\n";
-    StatusOr<std::string> compute_result =
-        (*mac_primitive)->ComputeMac(*data_file_content);
-    if (!compute_result.ok()) {
-      std::cerr << compute_result.status().message() << std::endl;
-      exit(1);
-    }
-    // Write out the authentication tag to tag file.
-    Status write_result = Write(*compute_result, tag_filename);
-    if (!write_result.ok()) {
-      std::cerr << write_result.message() << std::endl;
-      exit(1);
-    }
-  } else {  // operation == kVerify.
-    // Read the authentication tag from tag file.
-    StatusOr<std::string> tag_result = Read(tag_filename);
-    if (!tag_result.ok()) {
-      std::cerr << tag_result.status().message() << std::endl;
-      exit(1);
-    }
-    // Verify authentication tag.
-    std::clog << "Verifying tag...\n";
-    Status verify_result =
-        (*mac_primitive)->VerifyMac(*tag_result, *data_file_content);
-    if (!verify_result.ok()) {
-      std::cerr << verify_result.message() << std::endl;
-      exit(1);
-    }
-    std::clog << "verification succeeded" << std::endl;
-  }
-
-  std::clog << "All done." << std::endl;
+  CHECK_OK(tink_cc_examples::MacCli(mode, keyset_filename, data_filename,
+                                    tag_filename));
   return 0;
 }
 // [END mac-example]
diff --git a/cc/examples/mac/mac_cli_test.sh b/cc/examples/mac/mac_cli_test.sh
index c4600c5..34f88f3 100755
--- a/cc/examples/mac/mac_cli_test.sh
+++ b/cc/examples/mac/mac_cli_test.sh
@@ -20,6 +20,8 @@
 # Tests for Tink CC MAC.
 #############################################################################
 
+: "${TEST_TMPDIR:=$(mktemp -d)}"
+
 readonly CLI="$1"
 readonly KEYSET_FILE="$2"
 readonly DATA_FILE="${TEST_TMPDIR}/example_data.txt"
@@ -44,7 +46,7 @@
 }
 
 #######################################
-# Asserts that the outcome of the latest test command was the expected one.
+# Asserts that the outcome of the latest test command is 0.
 #
 # If not, it terminates the test execution.
 #
@@ -52,23 +54,29 @@
 #   TEST_STATUS
 #   TEST_NAME
 #   TEST_CASE
-# Arguments:
-#   expected_outcome: The expected outcome.
 #######################################
-_assert_test_command_outcome() {
-  expected_outcome="$1"
-  if (( TEST_STATUS != expected_outcome )); then
-      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
-      exit 1
+assert_command_succeeded() {
+  if (( TEST_STATUS != 0 )); then
+    echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+    exit 1
   fi
 }
 
-assert_command_succeeded() {
-  _assert_test_command_outcome 0
-}
-
+#######################################
+# Asserts that the outcome of the latest test command is not 0.
+#
+# If not, it terminates the test execution.
+#
+# Globals:
+#   TEST_STATUS
+#   TEST_NAME
+#   TEST_CASE
+#######################################
 assert_command_failed() {
-  _assert_test_command_outcome 1
+  if (( TEST_STATUS == 0 )); then
+      echo "[   FAILED ] ${TEST_NAME}.${TEST_CASE}"
+      exit 1
+  fi
 }
 
 #######################################
diff --git a/cc/examples/mac/mac_test_keyset.json b/cc/examples/mac/mac_test_keyset.json
index d4ef7e1..b40e989 100644
--- a/cc/examples/mac/mac_test_keyset.json
+++ b/cc/examples/mac/mac_test_keyset.json
@@ -1,13 +1,15 @@
 {
     "primaryKeyId": 691856985,
-    "key": [{
+    "key": [
+      {
         "keyData": {
-            "typeUrl": "type.googleapis.com/google.crypto.tink.HmacKey",
-            "keyMaterialType": "SYMMETRIC",
-            "value": "EgQIAxAgGiDZsmkTufMG/XlKlk9m7bqxustjUPT2YULEVm8mOp2mSA\u003d\u003d"
+          "typeUrl": "type.googleapis.com/google.crypto.tink.HmacKey",
+          "keyMaterialType": "SYMMETRIC",
+          "value": "EgQIAxAgGiDZsmkTufMG/XlKlk9m7bqxustjUPT2YULEVm8mOp2mSA=="
         },
         "outputPrefixType": "TINK",
         "keyId": 691856985,
         "status": "ENABLED"
-    }]
-}
+      }
+    ]
+  }
diff --git a/cc/examples/util/BUILD.bazel b/cc/examples/util/BUILD.bazel
new file mode 100644
index 0000000..b872c23
--- /dev/null
+++ b/cc/examples/util/BUILD.bazel
@@ -0,0 +1,20 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "util",
+    srcs = ["util.cc"],
+    hdrs = ["util.h"],
+    deps = [
+        "@com_google_absl//absl/memory",
+        "@tink_cc//:cleartext_keyset_handle",
+        "@tink_cc//:json_keyset_reader",
+        "@tink_cc//:json_keyset_writer",
+        "@tink_cc//:keyset_handle",
+        "@tink_cc//:keyset_reader",
+        "@tink_cc//:keyset_writer",
+        "@tink_cc//util:status",
+        "@tink_cc//util:statusor",
+    ],
+)
diff --git a/cc/examples/util/CMakeLists.txt b/cc/examples/util/CMakeLists.txt
new file mode 100644
index 0000000..6e5a02c
--- /dev/null
+++ b/cc/examples/util/CMakeLists.txt
@@ -0,0 +1,5 @@
+add_library(util util.cc util.h)
+target_include_directories(util PUBLIC
+  "${CMAKE_CURRENT_SOURCE_DIR}"
+  "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(util tink::static)
diff --git a/cc/examples/util/util.cc b/cc/examples/util/util.cc
new file mode 100644
index 0000000..ca4a3d5
--- /dev/null
+++ b/cc/examples/util/util.cc
@@ -0,0 +1,105 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#include "util/util.h"
+
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "absl/memory/memory.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/json_keyset_reader.h"
+#include "tink/json_keyset_writer.h"
+#include "tink/keyset_handle.h"
+#include "tink/keyset_reader.h"
+#include "tink/keyset_writer.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace tink_cc_examples {
+namespace {
+
+using ::crypto::tink::JsonKeysetReader;
+using ::crypto::tink::JsonKeysetWriter;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::KeysetReader;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+
+// Creates a KeysetReader that reads a JSON-formatted keyset
+// from the given file.
+StatusOr<std::unique_ptr<KeysetReader>> GetJsonKeysetReader(
+    const std::string& filename) {
+  auto input_stream = absl::make_unique<std::ifstream>();
+  input_stream->open(filename, std::ifstream::in);
+  return JsonKeysetReader::New(std::move(input_stream));
+}
+
+StatusOr<std::unique_ptr<JsonKeysetWriter>> GetJsonKeysetWriter(
+    const std::string& filename) {
+  auto output_stream = absl::make_unique<std::ofstream>();
+  output_stream->open(filename, std::ofstream::out);
+  return JsonKeysetWriter::New(std::move(output_stream));
+}
+
+}  // namespace
+
+StatusOr<std::unique_ptr<KeysetHandle>> ReadJsonCleartextKeyset(
+    const std::string& filename) {
+  StatusOr<std::unique_ptr<KeysetReader>> keyset_reader =
+      GetJsonKeysetReader(filename);
+  if (!keyset_reader.ok()) return keyset_reader.status();
+  return crypto::tink::CleartextKeysetHandle::Read(*std::move(keyset_reader));
+}
+
+Status WriteJsonCleartextKeyset(const std::string& filename,
+                                const KeysetHandle& keyset_handle) {
+  StatusOr<std::unique_ptr<JsonKeysetWriter>> keyset_writer =
+      GetJsonKeysetWriter(filename);
+  if (!keyset_writer.ok()) return keyset_writer.status();
+  return crypto::tink::CleartextKeysetHandle::Write(keyset_writer->get(),
+                                                    keyset_handle);
+}
+
+StatusOr<std::string> ReadFile(const std::string& filename) {
+  std::ifstream input_stream;
+  input_stream.open(filename, std::ifstream::in);
+  if (!input_stream.is_open()) {
+    return Status(absl::StatusCode::kInternal,
+                  absl::StrCat("Error opening input file ", filename));
+  }
+  std::stringstream input;
+  input << input_stream.rdbuf();
+  return input.str();
+}
+
+Status WriteToFile(const std::string& data_to_write,
+                   const std::string& filename) {
+  std::ofstream output_stream(filename,
+                              std::ofstream::out | std::ofstream::binary);
+  if (!output_stream.is_open()) {
+    return Status(absl::StatusCode::kInternal,
+                  absl::StrCat("Error opening output file ", filename));
+  }
+  output_stream << data_to_write;
+  return crypto::tink::util::OkStatus();
+}
+
+}  // namespace tink_cc_examples
diff --git a/cc/examples/util/util.h b/cc/examples/util/util.h
new file mode 100644
index 0000000..3f25a31
--- /dev/null
+++ b/cc/examples/util/util.h
@@ -0,0 +1,49 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_UTIL_UTIL_H_
+#define TINK_EXAMPLES_UTIL_UTIL_H_
+
+#include <memory>
+#include <string>
+
+#include "tink/keyset_handle.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace tink_cc_examples {
+
+// Reads a keyset from the given file `filename` which is expected to contain a
+// JSON-formatted keyset.
+crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>>
+ReadJsonCleartextKeyset(const std::string& filename);
+
+// Writes `keyset_handle` to the file `filename` formatted with JSON in
+// cleartext.
+crypto::tink::util::Status WriteJsonCleartextKeyset(
+    const std::string& filename,
+    const crypto::tink::KeysetHandle& keyset_handle);
+
+// Reads `filename` and returns the read content as a string, or an error status
+// if the file does not exist.
+crypto::tink::util::StatusOr<std::string> ReadFile(const std::string& filename);
+
+// Writes the given `data_to_write` to the specified file `filename`.
+crypto::tink::util::Status WriteToFile(const std::string& data_to_write,
+                                       const std::string& filename);
+
+}  // namespace tink_cc_examples
+
+#endif  // TINK_EXAMPLES_UTIL_UTIL_H_
diff --git a/cc/examples/walkthrough/BUILD.bazel b/cc/examples/walkthrough/BUILD.bazel
new file mode 100644
index 0000000..69d2010
--- /dev/null
+++ b/cc/examples/walkthrough/BUILD.bazel
@@ -0,0 +1,165 @@
+"""Walkthrough examples for using Tink."""
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "test_util",
+    testonly = 1,
+    srcs = ["test_util.cc"],
+    hdrs = ["test_util.h"],
+    deps = [
+        ":load_cleartext_keyset",
+        "@com_google_absl//absl/strings",
+        "@tink_cc",
+    ],
+)
+
+cc_library(
+    name = "create_keyset",
+    srcs = ["create_keyset.cc"],
+    hdrs = ["create_keyset.h"],
+    deps = [
+        "@tink_cc",
+        "@tink_cc//:keyset_handle",
+        "@tink_cc//util:statusor",
+    ],
+)
+
+cc_test(
+    name = "create_keyset_test",
+    srcs = ["create_keyset_test.cc"],
+    deps = [
+        ":create_keyset",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc",
+        "@tink_cc//util:test_matchers",
+    ],
+)
+
+cc_library(
+    name = "load_cleartext_keyset",
+    srcs = ["load_cleartext_keyset.cc"],
+    hdrs = ["load_cleartext_keyset.h"],
+    deps = [
+        "@com_google_absl//absl/strings",
+        "@tink_cc",
+        "@tink_cc//:cleartext_keyset_handle",
+    ],
+)
+
+cc_test(
+    name = "load_cleartext_keyset_test",
+    srcs = ["load_cleartext_keyset_test.cc"],
+    deps = [
+        ":load_cleartext_keyset",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc",
+        "@tink_cc//util:test_matchers",
+    ],
+)
+
+cc_library(
+    name = "load_encrypted_keyset",
+    srcs = ["load_encrypted_keyset.cc"],
+    hdrs = ["load_encrypted_keyset.h"],
+    deps = [
+        "@com_google_absl//absl/strings",
+        "@tink_cc",
+    ],
+)
+
+cc_test(
+    name = "load_encrypted_keyset_test",
+    srcs = ["load_encrypted_keyset_test.cc"],
+    deps = [
+        ":load_cleartext_keyset",
+        ":load_encrypted_keyset",
+        ":test_util",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc",
+        "@tink_cc//util:test_matchers",
+    ],
+)
+
+cc_library(
+    name = "write_keyset",
+    srcs = ["write_keyset.cc"],
+    hdrs = ["write_keyset.h"],
+    deps = [
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@tink_cc",
+    ],
+)
+
+cc_test(
+    name = "write_keyset_test",
+    srcs = ["write_keyset_test.cc"],
+    deps = [
+        ":load_cleartext_keyset",
+        ":load_encrypted_keyset",
+        ":test_util",
+        ":write_keyset",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc",
+        "@tink_cc//util:test_matchers",
+    ],
+)
+
+cc_library(
+    name = "obtain_and_use_a_primitive",
+    srcs = ["obtain_and_use_a_primitive.cc"],
+    hdrs = ["obtain_and_use_a_primitive.h"],
+    deps = [
+        "@com_google_absl//absl/strings",
+        "@tink_cc",
+    ],
+)
+
+cc_test(
+    name = "obtain_and_use_a_primitive_test",
+    srcs = ["obtain_and_use_a_primitive_test.cc"],
+    deps = [
+        ":load_cleartext_keyset",
+        ":obtain_and_use_a_primitive",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc",
+        "@tink_cc//util:test_matchers",
+    ],
+)
+
+cc_library(
+    name = "write_cleartext_keyset",
+    srcs = ["write_cleartext_keyset.cc"],
+    hdrs = ["write_cleartext_keyset.h"],
+    deps = [
+        "@com_google_absl//absl/strings",
+        "@tink_cc",
+        "@tink_cc//:cleartext_keyset_handle",
+    ],
+)
+
+cc_test(
+    name = "write_cleartext_keyset_test",
+    srcs = ["write_cleartext_keyset_test.cc"],
+    deps = [
+        ":load_cleartext_keyset",
+        ":write_cleartext_keyset",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc",
+        "@tink_cc//util:test_matchers",
+    ],
+)
diff --git a/cc/examples/walkthrough/CMakeLists.txt b/cc/examples/walkthrough/CMakeLists.txt
new file mode 100644
index 0000000..db128e5
--- /dev/null
+++ b/cc/examples/walkthrough/CMakeLists.txt
@@ -0,0 +1,70 @@
+# Library targets.
+
+add_library(create_keyset create_keyset.cc create_keyset.h)
+target_include_directories(create_keyset PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(create_keyset tink::static)
+
+add_library(load_cleartext_keyset load_cleartext_keyset.cc load_cleartext_keyset.h)
+target_include_directories(load_cleartext_keyset PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(load_cleartext_keyset tink::static)
+
+add_library(test_util test_util.cc test_util.h)
+target_include_directories(test_util PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(test_util load_cleartext_keyset tink::static)
+
+add_library(load_encrypted_keyset load_encrypted_keyset.cc load_encrypted_keyset.h)
+target_include_directories(load_encrypted_keyset PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(load_encrypted_keyset tink::static)
+
+add_library(write_keyset write_keyset.cc write_keyset.h)
+target_include_directories(write_keyset PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(write_keyset load_cleartext_keyset tink::static)
+
+add_library(obtain_and_use_a_primitive obtain_and_use_a_primitive.cc obtain_and_use_a_primitive.h)
+target_include_directories(obtain_and_use_a_primitive PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(obtain_and_use_a_primitive tink::static)
+
+add_library(write_cleartext_keyset write_cleartext_keyset.cc write_cleartext_keyset.h)
+target_include_directories(write_cleartext_keyset PUBLIC
+    "${CMAKE_CURRENT_SOURCE_DIR}"
+    "${TINK_EXAMPLES_INCLUDE_PATH}")
+target_link_libraries(write_cleartext_keyset tink::static)
+
+# Test targets.
+# NOTE: gmock and gtest_main are already exported by Tink.
+
+add_executable(create_keyset_test create_keyset_test.cc)
+add_test(NAME create_keyset_test COMMAND create_keyset_test WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(create_keyset_test create_keyset gmock gtest_main)
+
+add_executable(load_cleartext_keyset_test load_cleartext_keyset_test.cc)
+add_test(NAME load_cleartext_keyset_test COMMAND load_cleartext_keyset_test WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(load_cleartext_keyset_test load_cleartext_keyset gmock gtest_main)
+
+add_executable(load_encrypted_keyset_test load_encrypted_keyset_test.cc)
+add_test(NAME load_encrypted_keyset_test COMMAND load_encrypted_keyset_test WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(load_encrypted_keyset_test test_util load_encrypted_keyset load_cleartext_keyset gmock gtest_main)
+
+add_executable(write_keyset_test write_keyset_test.cc)
+add_test(NAME write_keyset_test COMMAND write_keyset_test WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(write_keyset_test test_util write_keyset load_cleartext_keyset load_encrypted_keyset gmock gtest_main)
+
+add_executable(obtain_and_use_a_primitive_test obtain_and_use_a_primitive_test.cc)
+add_test(NAME obtain_and_use_a_primitive_test COMMAND obtain_and_use_a_primitive_test WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(obtain_and_use_a_primitive_test obtain_and_use_a_primitive load_cleartext_keyset gmock gtest_main)
+
+add_executable(write_cleartext_keyset_test write_cleartext_keyset_test.cc)
+add_test(NAME write_cleartext_keyset_test COMMAND write_cleartext_keyset_test WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+target_link_libraries(write_cleartext_keyset_test write_cleartext_keyset load_cleartext_keyset gmock gtest_main)
diff --git a/cc/examples/walkthrough/create_keyset.cc b/cc/examples/walkthrough/create_keyset.cc
new file mode 100644
index 0000000..d02f24b
--- /dev/null
+++ b/cc/examples/walkthrough/create_keyset.cc
@@ -0,0 +1,46 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/create_keyset.h"
+
+// [START tink_walkthrough_create_keyset]
+#include <memory>
+
+#include "tink/aead/aead_key_templates.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::KeyTemplate;
+
+// Creates a keyset with a single AES128-GCM key and return a handle to it.
+//
+// Prerequisites for this example:
+//  - Register AEAD implementations of Tink.
+StatusOr<std::unique_ptr<KeysetHandle>> CreateAead128GcmKeyset() {
+  // Tink provides pre-baked templates. For example, we generate a key template
+  // for AES128-GCM.
+  KeyTemplate key_template = crypto::tink::AeadKeyTemplates::Aes128Gcm();
+  // This will generate a new keyset with only *one* key and return a keyset
+  // handle to it.
+  return KeysetHandle::GenerateNew(key_template);
+}
+
+}  // namespace tink_walkthrough
+// [END tink_walkthrough_create_keyset]
diff --git a/cc/examples/walkthrough/create_keyset.h b/cc/examples/walkthrough/create_keyset.h
new file mode 100644
index 0000000..96e2a41
--- /dev/null
+++ b/cc/examples/walkthrough/create_keyset.h
@@ -0,0 +1,32 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_CREATE_KEYSET_H_
+#define TINK_EXAMPLES_WALKTHROUGH_CREATE_KEYSET_H_
+
+#include <memory>
+
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+// Creates a keyset with a single AES128-GCM key and return a handle to it.
+crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>>
+CreateAead128GcmKeyset();
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_CREATE_KEYSET_H_
diff --git a/cc/examples/walkthrough/create_keyset_test.cc b/cc/examples/walkthrough/create_keyset_test.cc
new file mode 100644
index 0000000..7970c13
--- /dev/null
+++ b/cc/examples/walkthrough/create_keyset_test.cc
@@ -0,0 +1,67 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/create_keyset.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/aead/aead_config.h"
+#include "tink/registry.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace tink_walkthrough {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::util::StatusOr;
+using ::testing::Not;
+using ::testing::Test;
+
+class CreateAead128GcmKeysetTest : public Test {
+ public:
+  void TearDown() override { crypto::tink::Registry::Reset(); }
+};
+
+TEST_F(CreateAead128GcmKeysetTest,
+       CreateAead128GcmKeysetFailsIfAeadNotRegistered) {
+  EXPECT_THAT(CreateAead128GcmKeyset(), Not(IsOk()));
+}
+
+TEST_F(CreateAead128GcmKeysetTest, CreateAead128GcmKeysetSucceeds) {
+  ASSERT_THAT(crypto::tink::AeadConfig::Register(), IsOk());
+  StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>> keyset_handle =
+      CreateAead128GcmKeyset();
+  ASSERT_THAT(keyset_handle, IsOk());
+  constexpr absl::string_view plaintext = "Some plaintext";
+  constexpr absl::string_view associated_data = "Some associated_data";
+  StatusOr<std::unique_ptr<crypto::tink::Aead>> aead =
+      (*keyset_handle)->GetPrimitive<crypto::tink::Aead>();
+  ASSERT_THAT(aead, IsOk());
+  StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT((*aead)->Decrypt(*ciphertext, associated_data),
+              IsOkAndHolds(plaintext));
+}
+
+}  // namespace
+}  // namespace tink_walkthrough
diff --git a/cc/examples/walkthrough/load_cleartext_keyset.cc b/cc/examples/walkthrough/load_cleartext_keyset.cc
new file mode 100644
index 0000000..5ea603b
--- /dev/null
+++ b/cc/examples/walkthrough/load_cleartext_keyset.cc
@@ -0,0 +1,55 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/load_cleartext_keyset.h"
+
+// [START tink_walkthrough_load_cleartext_keyset]
+#include <iostream>
+#include <memory>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/json_keyset_reader.h"
+#include "tink/keyset_handle.h"
+#include "tink/keyset_reader.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::util::StatusOr;
+
+// Loads a JSON-serialized unencrypted keyset `serialized_keyset` and returns a
+// KeysetHandle.
+//
+// Prerequisites for this example:
+//  - Create an plaintext keyset in JSON, for example, using Tinkey:
+//
+//    tinkey create-key --key-template AES256_GCM \
+//      --out-format json --out keyset.json
+//
+StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>> LoadKeyset(
+    absl::string_view serialized_keyset) {
+  // To load a serialized keyset we need a JSON keyset reader.
+  StatusOr<std::unique_ptr<crypto::tink::KeysetReader>> reader =
+      crypto::tink::JsonKeysetReader::New(serialized_keyset);
+  if (!reader.ok()) return reader.status();
+  // Parse and obtain the keyset using the reader.
+  return crypto::tink::CleartextKeysetHandle::Read(*std::move(reader));
+}
+
+}  // namespace tink_walkthrough
+// [END tink_walkthrough_load_cleartext_keyset]
diff --git a/cc/examples/walkthrough/load_cleartext_keyset.h b/cc/examples/walkthrough/load_cleartext_keyset.h
new file mode 100644
index 0000000..b4e1e42
--- /dev/null
+++ b/cc/examples/walkthrough/load_cleartext_keyset.h
@@ -0,0 +1,34 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_LOAD_CLEARTEXT_KEYSET_H_
+#define TINK_EXAMPLES_WALKTHROUGH_LOAD_CLEARTEXT_KEYSET_H_
+
+#include <memory>
+
+#include "absl/strings/string_view.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+// Loads a JSON-serialized unencrypted keyset `serialized_keyset` and returns a
+// KeysetHandle.
+crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>>
+LoadKeyset(absl::string_view serialized_keyset);
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_LOAD_CLEARTEXT_KEYSET_H_
diff --git a/cc/examples/walkthrough/load_cleartext_keyset_test.cc b/cc/examples/walkthrough/load_cleartext_keyset_test.cc
new file mode 100644
index 0000000..ac8e5fa
--- /dev/null
+++ b/cc/examples/walkthrough/load_cleartext_keyset_test.cc
@@ -0,0 +1,77 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/load_cleartext_keyset.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead/aead_config.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace tink_walkthrough {
+namespace {
+
+constexpr absl::string_view kSerializedKeyset = R"json({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+})json";
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::crypto::tink::util::StatusOr;
+
+TEST(LoadKeysetTest, LoadKeysetFailsWithInvalidKeyset) {
+  EXPECT_THAT(LoadKeyset("Invalid").status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(LoadKeysetTest, LoadKeysetSucceeds) {
+  StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>> keyset_handle =
+      LoadKeyset(kSerializedKeyset);
+  ASSERT_THAT(keyset_handle, IsOk());
+  ASSERT_THAT(crypto::tink::AeadConfig::Register(), IsOk());
+  // Make sure we can extract the Aead primitive and encrypt/decrypt with it.
+  constexpr absl::string_view plaintext = "Some plaintext";
+  constexpr absl::string_view associated_data = "Some associated_data";
+  StatusOr<std::unique_ptr<crypto::tink::Aead>> aead =
+      (*keyset_handle)->GetPrimitive<crypto::tink::Aead>();
+  ASSERT_THAT(aead, IsOk());
+  StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT((*aead)->Decrypt(*ciphertext, associated_data),
+              IsOkAndHolds(plaintext));
+}
+
+}  // namespace
+}  // namespace tink_walkthrough
diff --git a/cc/examples/walkthrough/load_encrypted_keyset.cc b/cc/examples/walkthrough/load_encrypted_keyset.cc
new file mode 100644
index 0000000..11679a7
--- /dev/null
+++ b/cc/examples/walkthrough/load_encrypted_keyset.cc
@@ -0,0 +1,72 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/load_encrypted_keyset.h"
+
+// [START tink_walkthrough_load_encrypted_keyset]
+#include <iostream>
+#include <memory>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/json_keyset_reader.h"
+#include "tink/keyset_handle.h"
+#include "tink/keyset_reader.h"
+#include "tink/kms_client.h"
+#include "tink/kms_clients.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::util::StatusOr;
+
+// Loads a JSON-serialized keyset encrypted with a KSM
+// `serialized_encrypted_keyset`. The decryption uses the KMS master key
+// `master_key_uri`.
+//
+// Prerequisites for this example:
+//  - Register AEAD implementations of Tink.
+//  - Register a KMS client for the given URI prefix using KmsClients::Add.
+//  - Create a KMS encrypted keyset, for example using Tinkey with Cloud KMS:
+//
+//    tinkey create-keyset --key-template AES128_GCM \
+//      --out-format json --out encrypted_aead_keyset.json \
+//      --master-key-uri gcp-kms://<KMS key uri> \
+//      --credentials gcp_credentials.json
+//
+StatusOr<std::unique_ptr<KeysetHandle>> LoadKeyset(
+    absl::string_view serialized_encrypted_keyset,
+    absl::string_view master_key_uri) {
+  // Get a KMS client for the given key URI.
+  StatusOr<const crypto::tink::KmsClient*> kms_client =
+      crypto::tink::KmsClients::Get(master_key_uri);
+  if (!kms_client.ok()) return kms_client.status();
+  // A KmsClient can return an Aead primitive.
+  StatusOr<std::unique_ptr<crypto::tink::Aead>> kms_aead =
+      (*kms_client)->GetAead(master_key_uri);
+  if (!kms_aead.ok()) return kms_aead.status();
+  // Use a JSON reader to read the encrypted keyset.
+  StatusOr<std::unique_ptr<crypto::tink::KeysetReader>> reader =
+      crypto::tink::JsonKeysetReader::New(serialized_encrypted_keyset);
+  if (!reader.ok()) return reader.status();
+  // Decrypt using the KMS, parse the keyset and retuns a handle to it.
+  return KeysetHandle::Read(*std::move(reader), **kms_aead);
+}
+
+}  // namespace tink_walkthrough
+// [END tink_walkthrough_load_encrypted_keyset]
diff --git a/cc/examples/walkthrough/load_encrypted_keyset.h b/cc/examples/walkthrough/load_encrypted_keyset.h
new file mode 100644
index 0000000..0c271fd
--- /dev/null
+++ b/cc/examples/walkthrough/load_encrypted_keyset.h
@@ -0,0 +1,36 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_LOAD_ENCRYPTED_KEYSET_H_
+#define TINK_EXAMPLES_WALKTHROUGH_LOAD_ENCRYPTED_KEYSET_H_
+
+#include <memory>
+
+#include "absl/strings/string_view.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+// Loads a JSON-serialized keyset encrypted with a KSM
+// `serialized_encrypted_keyset`. The decryption uses the KMS master key
+// `master_key_uri`.
+crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>>
+LoadKeyset(absl::string_view serialized_encrypted_keyset,
+           absl::string_view master_key_uri);
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_LOAD_ENCRYPTED_KEYSET_H_
diff --git a/cc/examples/walkthrough/load_encrypted_keyset_test.cc b/cc/examples/walkthrough/load_encrypted_keyset_test.cc
new file mode 100644
index 0000000..4a1c42a
--- /dev/null
+++ b/cc/examples/walkthrough/load_encrypted_keyset_test.cc
@@ -0,0 +1,168 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/load_encrypted_keyset.h"
+
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/aead/aead_config.h"
+#include "walkthrough/load_cleartext_keyset.h"
+#include "walkthrough/test_util.h"
+#include "tink/kms_clients.h"
+#include "tink/registry.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace tink_walkthrough {
+namespace {
+
+constexpr absl::string_view kSerializedMasterKeyKeyset = R"json({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+})json";
+
+constexpr absl::string_view kSerializedKeysetToEncrypt = R"json({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GhD+9l0RANZjzZEZ8PDp7LRW"
+      },
+      "keyId": 1931667682,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 1931667682
+})json";
+
+// Encryption of kSerializedKeysetToEncrypt using kSerializedMasterKeyKeyset.
+constexpr absl::string_view kEncryptedKeyset = R"json({
+  "encryptedKeyset": "ARGMSWi6YHyZ/Oqxl00XSq631a0q2UPmf+rCvCIAggSZrwCmxFF797MpY0dqgaXu1fz2eQ8zFNhlyTXv9kwg1kY6COpyhY/68zNBUkyKX4CharLYfpg1LgRl+6rMzIQa0XDHh7ZDmp1CevzecZIKnG83uDRHxxSv3h8c/Kc="
+})json";
+
+constexpr absl::string_view kFakeKmsKeyUri = "fake://some_key";
+
+using ::crypto::tink::Aead;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::Registry;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+using ::testing::Environment;
+
+// Test environment used to register KMS clients only once for the whole test.
+class LoadKeysetTestEnvironment : public Environment {
+ public:
+  ~LoadKeysetTestEnvironment() override = default;
+
+  // Register FakeKmsClient and AlwaysFailingFakeKmsClient.
+  void SetUp() override {
+    auto fake_kms =
+        absl::make_unique<FakeKmsClient>(kSerializedMasterKeyKeyset);
+    ASSERT_THAT(crypto::tink::KmsClients::Add(std::move(fake_kms)), IsOk());
+    auto failing_kms = absl::make_unique<AlwaysFailingFakeKmsClient>();
+    ASSERT_THAT(crypto::tink::KmsClients::Add(std::move(failing_kms)), IsOk());
+  }
+};
+
+// Unused.
+Environment *const test_env =
+    testing::AddGlobalTestEnvironment(new LoadKeysetTestEnvironment());
+
+class LoadKeysetTest : public ::testing::Test {
+ public:
+  void TearDown() override { Registry::Reset(); }
+};
+
+TEST_F(LoadKeysetTest, LoadKeysetFailsWhenNoKmsRegistered) {
+  StatusOr<std::unique_ptr<KeysetHandle>> expected_keyset =
+      LoadKeyset(kEncryptedKeyset, /*master_key_uri=*/"other_kms://some_key");
+  EXPECT_THAT(expected_keyset.status(), StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(LoadKeysetTest, LoadKeysetFailsWhenKmsClientFails) {
+  StatusOr<std::unique_ptr<KeysetHandle>> expected_keyset =
+      LoadKeyset(kEncryptedKeyset, /*master_key_uri=*/"failing://some_key");
+  EXPECT_THAT(expected_keyset.status(),
+              StatusIs(absl::StatusCode::kUnimplemented));
+}
+
+TEST_F(LoadKeysetTest, LoadKeysetFailsWhenAeadNotRegistered) {
+  StatusOr<std::unique_ptr<KeysetHandle>> expected_keyset =
+      LoadKeyset(kEncryptedKeyset, kFakeKmsKeyUri);
+  EXPECT_THAT(expected_keyset.status(), StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(LoadKeysetTest, LoadKeysetFailsWhenInvalidKeyset) {
+  ASSERT_THAT(crypto::tink::AeadConfig::Register(), IsOk());
+  StatusOr<std::unique_ptr<KeysetHandle>> expected_keyset =
+      LoadKeyset("invalid", kFakeKmsKeyUri);
+  EXPECT_THAT(expected_keyset.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  Registry::Reset();
+}
+
+TEST_F(LoadKeysetTest, LoadKeysetSucceeds) {
+  ASSERT_THAT(crypto::tink::AeadConfig::Register(), IsOk());
+  StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      LoadKeyset(kEncryptedKeyset, kFakeKmsKeyUri);
+  ASSERT_THAT(handle, IsOk());
+  StatusOr<std::unique_ptr<Aead>> aead = (*handle)->GetPrimitive<Aead>();
+  ASSERT_THAT(aead, IsOk());
+
+  StatusOr<std::unique_ptr<KeysetHandle>> expected_keyset =
+      LoadKeyset(kSerializedKeysetToEncrypt);
+  ASSERT_THAT(expected_keyset, IsOk());
+  StatusOr<std::unique_ptr<Aead>> expected_aead =
+      (*expected_keyset)->GetPrimitive<Aead>();
+  ASSERT_THAT(expected_aead, IsOk());
+
+  std::string associated_data = "Some associated data";
+  std::string plaintext = "Some plaintext";
+
+  StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT((*expected_aead)->Decrypt(*ciphertext, associated_data),
+              IsOkAndHolds(plaintext));
+}
+
+}  // namespace
+}  // namespace tink_walkthrough
diff --git a/cc/examples/walkthrough/obtain_and_use_a_primitive.cc b/cc/examples/walkthrough/obtain_and_use_a_primitive.cc
new file mode 100644
index 0000000..3f4ede4
--- /dev/null
+++ b/cc/examples/walkthrough/obtain_and_use_a_primitive.cc
@@ -0,0 +1,71 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/obtain_and_use_a_primitive.h"
+
+// [START tink_walkthrough_obtain_and_use_a_primitive]
+#include <iostream>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::Aead;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::util::StatusOr;
+
+// AEAD encrypts `plaintext` with `associated_data` and the primary key in
+// `keyset_handle`.
+//
+// Prerequisites for this example:
+//  - Register AEAD implementations of Tink.
+//  - Create a keyset and get a handle to it.
+StatusOr<std::string> AeadEncrypt(const KeysetHandle& keyset_handle,
+                                  absl::string_view palintext,
+                                  absl::string_view associated_data) {
+  // To facilitate key rotation, GetPrimitive returns an Aead primitive that
+  // "wraps" multiple Aead primitives in the keyset. When encrypting it uses the
+  // primary key.
+  StatusOr<std::unique_ptr<Aead>> aead = keyset_handle.GetPrimitive<Aead>();
+  if (!aead.ok()) return aead.status();
+  return (*aead)->Encrypt(palintext, associated_data);
+}
+
+// AEAD decrypts `ciphertext` with `associated_data` and the correct key in
+// `keyset_handle`.
+//
+// Prerequisites for this example:
+//  - Register AEAD implementations of Tink.
+//  - Create a keyset and get a handle to it.
+StatusOr<std::string> AeadDecrypt(const KeysetHandle& keyset_handle,
+                                  absl::string_view ciphertext,
+                                  absl::string_view associated_data) {
+  // To facilitate key rotation, GetPrimitive returns an Aead primitive that
+  // "wraps" multiple Aead primitives in the keyset. When decrypting it uses the
+  // key that was used to encrypt using the key ID contained in the ciphertext.
+  StatusOr<std::unique_ptr<Aead>> aead = keyset_handle.GetPrimitive<Aead>();
+  if (!aead.ok()) return aead.status();
+  return (*aead)->Decrypt(ciphertext, associated_data);
+}
+
+}  // namespace tink_walkthrough
+// [END tink_walkthrough_obtain_and_use_a_primitive]
diff --git a/cc/examples/walkthrough/obtain_and_use_a_primitive.h b/cc/examples/walkthrough/obtain_and_use_a_primitive.h
new file mode 100644
index 0000000..1f36018
--- /dev/null
+++ b/cc/examples/walkthrough/obtain_and_use_a_primitive.h
@@ -0,0 +1,42 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_OBTAIN_AND_USE_A_PRIMITIVE_H_
+#define TINK_EXAMPLES_WALKTHROUGH_OBTAIN_AND_USE_A_PRIMITIVE_H_
+
+#include <memory>
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+// AEAD encrypts `plaintext` with `associated_data` and the primary key in
+// `keyset_handle`.
+crypto::tink::util::StatusOr<std::string> AeadEncrypt(
+    const crypto::tink::KeysetHandle& keyset_handle,
+    absl::string_view palintext, absl::string_view associated_data);
+
+// AEAD decrypts `ciphertext` with `associated_data` and the correct key in
+// `keyset_handle`.
+crypto::tink::util::StatusOr<std::string> AeadDecrypt(
+    const crypto::tink::KeysetHandle& keyset_handle,
+    absl::string_view ciphertext, absl::string_view associated_data);
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_OBTAIN_AND_USE_A_PRIMITIVE_H_
diff --git a/cc/examples/walkthrough/obtain_and_use_a_primitive_test.cc b/cc/examples/walkthrough/obtain_and_use_a_primitive_test.cc
new file mode 100644
index 0000000..95b3894
--- /dev/null
+++ b/cc/examples/walkthrough/obtain_and_use_a_primitive_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/obtain_and_use_a_primitive.h"
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead/aead_config.h"
+#include "walkthrough/load_cleartext_keyset.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace tink_walkthrough {
+namespace {
+
+constexpr absl::string_view kSerializedKeyset = R"string({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+})string";
+
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::util::StatusOr;
+
+TEST(LoadKeysetTest, EncryptDecrypt) {
+  ASSERT_THAT(crypto::tink::AeadConfig::Register(), IsOk());
+  StatusOr<std::unique_ptr<KeysetHandle>> master_key_keyset =
+      LoadKeyset(kSerializedKeyset);
+  ASSERT_THAT(master_key_keyset, IsOk());
+  constexpr absl::string_view kPlaintext = "Some data";
+  constexpr absl::string_view kAssociatedData = "Some associated data";
+  StatusOr<std::string> ciphertext =
+      AeadEncrypt(**master_key_keyset, kPlaintext, kAssociatedData);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT(AeadDecrypt(**master_key_keyset, *ciphertext, kAssociatedData),
+              IsOkAndHolds(kPlaintext));
+}
+
+}  // namespace
+}  // namespace tink_walkthrough
diff --git a/cc/examples/walkthrough/test_util.cc b/cc/examples/walkthrough/test_util.cc
new file mode 100644
index 0000000..3cb7c49
--- /dev/null
+++ b/cc/examples/walkthrough/test_util.cc
@@ -0,0 +1,53 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/test_util.h"
+
+#include <memory>
+
+#include "absl/strings/match.h"
+#include "walkthrough/load_cleartext_keyset.h"
+#include "tink/keyset_handle.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::Aead;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+
+bool FakeKmsClient::DoesSupport(absl::string_view key_uri) const {
+  return absl::StartsWith(key_uri, "fake://");
+}
+
+StatusOr<std::unique_ptr<Aead>> FakeKmsClient::GetAead(
+    absl::string_view key_uri) const {
+  StatusOr<std::unique_ptr<KeysetHandle>> master_key_keyset =
+      LoadKeyset(serialized_master_key_keyset_);
+  if (!master_key_keyset.ok()) return master_key_keyset.status();
+  return (*master_key_keyset)->GetPrimitive<Aead>();
+}
+
+bool AlwaysFailingFakeKmsClient::DoesSupport(absl::string_view key_uri) const {
+  return absl::StartsWith(key_uri, "failing://");
+}
+
+StatusOr<std::unique_ptr<Aead>> AlwaysFailingFakeKmsClient::GetAead(
+    absl::string_view key_uri) const {
+  return Status(absl::StatusCode::kUnimplemented, "Unimplemented");
+}
+
+}  // namespace tink_walkthrough
diff --git a/cc/examples/walkthrough/test_util.h b/cc/examples/walkthrough/test_util.h
new file mode 100644
index 0000000..f5f7312
--- /dev/null
+++ b/cc/examples/walkthrough/test_util.h
@@ -0,0 +1,57 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_TEST_UTIL_H_
+#define TINK_EXAMPLES_WALKTHROUGH_TEST_UTIL_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/kms_client.h"
+#include "tink/util/statusor.h"
+
+namespace tink_walkthrough {
+
+// A fake KmsClient that for every key URI always returns an aead from
+// kSerializedMasterKeyKeyset.
+class FakeKmsClient : public crypto::tink::KmsClient {
+ public:
+  explicit FakeKmsClient(absl::string_view serialized_master_key_keyset)
+      : serialized_master_key_keyset_(serialized_master_key_keyset) {}
+
+  bool DoesSupport(absl::string_view key_uri) const override;
+
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::Aead>> GetAead(
+      absl::string_view key_uri) const override;
+
+ private:
+  std::string serialized_master_key_keyset_;
+};
+
+// A fake KmsClient that always fails to return an AEAD.
+class AlwaysFailingFakeKmsClient : public crypto::tink::KmsClient {
+ public:
+  bool DoesSupport(absl::string_view key_uri) const override;
+
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::Aead>> GetAead(
+      absl::string_view key_uri) const override;
+};
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_TEST_UTIL_H_
diff --git a/cc/examples/walkthrough/write_cleartext_keyset.cc b/cc/examples/walkthrough/write_cleartext_keyset.cc
new file mode 100644
index 0000000..0cdc856
--- /dev/null
+++ b/cc/examples/walkthrough/write_cleartext_keyset.cc
@@ -0,0 +1,56 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/write_cleartext_keyset.h"
+
+// [START tink_walkthrough_write_keyset]
+#include <memory>
+#include <ostream>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/json_keyset_writer.h"
+#include "tink/keyset_handle.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::JsonKeysetWriter;
+using ::crypto::tink::util::StatusOr;
+
+// Writes a `keyset` to `output_stream` as a plaintext JSON format.
+//
+// Warning: Storing keys in cleartext is not recommended. We recommend using a
+// Key Management Service to protect your keys. See
+// https://github.com/google/tink/blob/master/cc/examples/walkthrough/write_keyset.cc
+// for an example, and
+// https://developers.google.com/tink/key-management-overview for more info on
+// how to use a KMS with Tink.
+//
+// Prerequisites for this example:
+//  - Create a keyset and obtain a KeysetHandle to it.
+crypto::tink::util::Status WriteKeyset(
+    const crypto::tink::KeysetHandle& keyset,
+    std::unique_ptr<std::ostream> output_stream) {
+  StatusOr<std::unique_ptr<JsonKeysetWriter>> keyset_writer =
+      JsonKeysetWriter::New(std::move(output_stream));
+  if (!keyset_writer.ok()) return keyset_writer.status();
+  return crypto::tink::CleartextKeysetHandle::Write((keyset_writer)->get(),
+                                                    keyset);
+}
+
+}  // namespace tink_walkthrough
+// [END tink_walkthrough_write_keyset]
diff --git a/cc/examples/walkthrough/write_cleartext_keyset.h b/cc/examples/walkthrough/write_cleartext_keyset.h
new file mode 100644
index 0000000..f0f2b8e
--- /dev/null
+++ b/cc/examples/walkthrough/write_cleartext_keyset.h
@@ -0,0 +1,37 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_WRITE_CLEARTEXT_KEYSET_H_
+#define TINK_EXAMPLES_WALKTHROUGH_WRITE_CLEARTEXT_KEYSET_H_
+
+#include <memory>
+#include <ostream>
+
+#include "tink/keyset_handle.h"
+
+namespace tink_walkthrough {
+
+// Writes a `keyset` to `output_stream` as a plaintext JSON format.
+//
+// Warning: Storing keys in cleartext is not recommended. We recommend using a
+// Key Management Service to protect your keys. See
+// https://developers.google.com/tink/key-management-overview.
+crypto::tink::util::Status WriteKeyset(
+    const crypto::tink::KeysetHandle& keyset,
+    std::unique_ptr<std::ostream> output_stream);
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_WRITE_CLEARTEXT_KEYSET_H_
diff --git a/cc/examples/walkthrough/write_cleartext_keyset_test.cc b/cc/examples/walkthrough/write_cleartext_keyset_test.cc
new file mode 100644
index 0000000..af229a2
--- /dev/null
+++ b/cc/examples/walkthrough/write_cleartext_keyset_test.cc
@@ -0,0 +1,89 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#include "walkthrough/write_cleartext_keyset.h"
+
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/aead/aead_config.h"
+#include "walkthrough/load_cleartext_keyset.h"
+#include "tink/util/test_matchers.h"
+
+namespace tink_walkthrough {
+namespace {
+
+using ::crypto::tink::Aead;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::util::StatusOr;
+
+constexpr absl::string_view kSerializedKeyset = R"string({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+})string";
+
+TEST(WriteCleartextKeysetTest, WriteKeysetSerializesCorrectly) {
+  ASSERT_THAT(crypto::tink::AeadConfig::Register(), IsOk());
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset =
+      LoadKeyset(kSerializedKeyset);
+
+  std::stringbuf buffer;
+  auto output_stream = absl::make_unique<std::ostream>(&buffer);
+  ASSERT_THAT(WriteKeyset(**keyset, std::move(output_stream)), IsOk());
+
+  StatusOr<std::unique_ptr<Aead>> aead = (*keyset)->GetPrimitive<Aead>();
+
+  // Make sure the encrypted keyset was written correctly by loading it and
+  // trying to decrypt ciphertext.
+  StatusOr<std::unique_ptr<KeysetHandle>> loaded_keyset =
+      LoadKeyset(buffer.str());
+  ASSERT_THAT(loaded_keyset, IsOk());
+  StatusOr<std::unique_ptr<Aead>> loaded_keyset_aead =
+      (*loaded_keyset)->GetPrimitive<Aead>();
+  ASSERT_THAT(loaded_keyset_aead, IsOk());
+
+  constexpr absl::string_view kPlaintext = "Some plaintext";
+  constexpr absl::string_view kAssociatedData = "Some associated data";
+
+  StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(kPlaintext, kAssociatedData);
+  EXPECT_THAT((*loaded_keyset_aead)->Decrypt(*ciphertext, kAssociatedData),
+              IsOkAndHolds(kPlaintext));
+  ciphertext = (*loaded_keyset_aead)->Encrypt(kPlaintext, kAssociatedData);
+  EXPECT_THAT((*aead)->Decrypt(*ciphertext, kAssociatedData),
+              IsOkAndHolds(kPlaintext));
+}
+
+}  // namespace
+}  // namespace tink_walkthrough
diff --git a/cc/examples/walkthrough/write_keyset.cc b/cc/examples/walkthrough/write_keyset.cc
new file mode 100644
index 0000000..ddaf807
--- /dev/null
+++ b/cc/examples/walkthrough/write_keyset.cc
@@ -0,0 +1,65 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/write_keyset.h"
+
+// [START tink_walkthrough_write_keyset]
+#include <fstream>
+#include <memory>
+#include <ostream>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/json_keyset_writer.h"
+#include "tink/keyset_handle.h"
+#include "tink/kms_client.h"
+#include "tink/kms_clients.h"
+
+namespace tink_walkthrough {
+
+using ::crypto::tink::JsonKeysetWriter;
+using ::crypto::tink::util::StatusOr;
+
+// Writes a `keyset` to `output_stream` in JSON format; the keyset is encrypted
+// through a KMS service using the KMS key `master_kms_key_uri`.
+//
+// Prerequisites for this example:
+//  - Register AEAD implementations of Tink.
+//  - Register a KMS client that can use `master_kms_key_uri`.
+//  - Create a keyset and obtain a KeysetHandle to it.
+crypto::tink::util::Status WriteEncryptedKeyset(
+    const crypto::tink::KeysetHandle& keyset,
+    std::unique_ptr<std::ostream> output_stream,
+    absl::string_view master_kms_key_uri) {
+  // Create a writer that will write the keyset to output_stream as JSON.
+  StatusOr<std::unique_ptr<JsonKeysetWriter>> writer =
+      JsonKeysetWriter::New(std::move(output_stream));
+  if (!writer.ok()) return writer.status();
+  // Get a KMS client for the given key URI.
+  StatusOr<const crypto::tink::KmsClient*> kms_client =
+      crypto::tink::KmsClients::Get(master_kms_key_uri);
+  if (!kms_client.ok()) return kms_client.status();
+  // Get an Aead primitive that uses the KMS service to encrypt/decrypt.
+  StatusOr<std::unique_ptr<crypto::tink::Aead>> kms_aead =
+      (*kms_client)->GetAead(master_kms_key_uri);
+  if (!kms_aead.ok()) return kms_aead.status();
+  return keyset.Write(writer->get(), **kms_aead);
+}
+
+}  // namespace tink_walkthrough
+// [END tink_walkthrough_write_keyset]
diff --git a/cc/examples/walkthrough/write_keyset.h b/cc/examples/walkthrough/write_keyset.h
new file mode 100644
index 0000000..323328c
--- /dev/null
+++ b/cc/examples/walkthrough/write_keyset.h
@@ -0,0 +1,35 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_EXAMPLES_WALKTHROUGH_WRITE_KEYSET_H_
+#define TINK_EXAMPLES_WALKTHROUGH_WRITE_KEYSET_H_
+
+#include <memory>
+#include <ostream>
+
+#include "tink/keyset_handle.h"
+
+namespace tink_walkthrough {
+
+// Writes a `keyset` to `output_stream` in JSON format; the keyset is encrypted
+// through a KMS service using the KMS key `master_kms_key_uri`.
+crypto::tink::util::Status WriteEncryptedKeyset(
+    const crypto::tink::KeysetHandle& keyset,
+    std::unique_ptr<std::ostream> output_stream,
+    absl::string_view master_kms_key_uri);
+
+}  // namespace tink_walkthrough
+
+#endif  // TINK_EXAMPLES_WALKTHROUGH_WRITE_KEYSET_H_
diff --git a/cc/examples/walkthrough/write_keyset_test.cc b/cc/examples/walkthrough/write_keyset_test.cc
new file mode 100644
index 0000000..4b82df4
--- /dev/null
+++ b/cc/examples/walkthrough/write_keyset_test.cc
@@ -0,0 +1,162 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "walkthrough/write_keyset.h"
+
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/aead.h"
+#include "tink/aead/aead_config.h"
+#include "walkthrough/load_cleartext_keyset.h"
+#include "walkthrough/load_encrypted_keyset.h"
+#include "walkthrough/test_util.h"
+#include "tink/kms_clients.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace tink_walkthrough {
+namespace {
+
+constexpr absl::string_view kSerializedMasterKeyKeyset = R"json({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+})json";
+
+constexpr absl::string_view kSerializedKeysetToEncrypt = R"json({
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GhD+9l0RANZjzZEZ8PDp7LRW"
+      },
+      "keyId": 1931667682,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 1931667682
+})json";
+
+using ::crypto::tink::Aead;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+using ::testing::Not;
+
+Status InitFakeKms() {
+  static Status* status = new Status([]() {
+    Status status = crypto::tink::AeadConfig::Register();
+    if (!status.ok()) {
+      return status;
+    }
+    return crypto::tink::KmsClients::Add(
+        absl::make_unique<FakeKmsClient>(kSerializedMasterKeyKeyset));
+  }());
+  return *status;
+}
+
+class WriteKeysetTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    ASSERT_THAT(InitFakeKms(), IsOk());
+    StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle_to_encrypt =
+        LoadKeyset(kSerializedKeysetToEncrypt);
+    ASSERT_THAT(keyset_handle_to_encrypt, IsOk());
+    keyset_handle_to_encrypt_ = std::move(*keyset_handle_to_encrypt);
+  }
+
+  std::unique_ptr<KeysetHandle> keyset_handle_to_encrypt_;
+};
+
+TEST_F(WriteKeysetTest, WriteEncryptedKeysetFailsWithNullOutputStream) {
+  EXPECT_THAT(WriteEncryptedKeyset(*keyset_handle_to_encrypt_, nullptr,
+                                   /*master_kms_key_uri=*/"fake://some_key"),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(WriteKeysetTest, WriteEncryptedKeysetFailsWhenStreamFails) {
+  auto output_stream = absl::make_unique<std::ostream>(nullptr);
+  EXPECT_THAT(
+      WriteEncryptedKeyset(*keyset_handle_to_encrypt_, std::move(output_stream),
+                           /*master_kms_key_uri=*/"fake://some_key"),
+      Not(IsOk()));
+}
+
+TEST_F(WriteKeysetTest, WriteEncryptedKeysetFailsNoKmsAvailable) {
+  std::stringbuf buffer;
+  auto output_stream = absl::make_unique<std::ostream>(&buffer);
+  EXPECT_THAT(WriteEncryptedKeyset(
+                  *keyset_handle_to_encrypt_, std::move(output_stream),
+                  /*master_kms_key_uri=*/"does_not_exist://does_not_exist"),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(WriteKeysetTest, WriteEncryptedKeysetWithValidInputs) {
+  std::stringbuf buffer;
+  auto output_stream = absl::make_unique<std::ostream>(&buffer);
+  constexpr absl::string_view master_kms_key_uri = "fake://some_key";
+  ASSERT_THAT(
+      WriteEncryptedKeyset(*keyset_handle_to_encrypt_, std::move(output_stream),
+                           master_kms_key_uri),
+      IsOk());
+  StatusOr<std::unique_ptr<Aead>> expected_aead =
+      keyset_handle_to_encrypt_->GetPrimitive<Aead>();
+  ASSERT_THAT(expected_aead, IsOk());
+  constexpr absl::string_view associated_data = "Some associated data";
+  constexpr absl::string_view plaintext = "Some plaintext";
+
+  StatusOr<std::string> ciphertext =
+      (*expected_aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+
+  // Make sure the encrypted keyset was written correctly by loading it and
+  // trying to decrypt ciphertext.
+  StatusOr<std::unique_ptr<KeysetHandle>> loaded_keyset =
+      LoadKeyset(buffer.str(), master_kms_key_uri);
+  ASSERT_THAT(loaded_keyset, IsOk());
+  StatusOr<std::unique_ptr<Aead>> loaded_keyset_aead =
+      (*loaded_keyset)->GetPrimitive<Aead>();
+  ASSERT_THAT(loaded_keyset_aead, IsOk());
+  EXPECT_THAT((*loaded_keyset_aead)->Decrypt(*ciphertext, associated_data),
+              IsOkAndHolds(plaintext));
+}
+
+}  // namespace
+}  // namespace tink_walkthrough
diff --git a/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.cc b/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.cc
index badab1d..3f6139d 100644
--- a/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.cc
+++ b/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.h"
 
+#include <memory>
+#include <sstream>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.h b/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.h
index 202df39..f48aba9 100644
--- a/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.h
+++ b/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.h
@@ -37,7 +37,7 @@
       std::unique_ptr<const Cecpq2AeadHkdfDemHelper>>
   New(const google::crypto::tink::KeyTemplate& dem_key_template);
 
-  virtual ~Cecpq2AeadHkdfDemHelper() {}
+  virtual ~Cecpq2AeadHkdfDemHelper() = default;
 
   // Creates and returns a new AeadOrDaead object that uses
   // a 32-bytes or greater high-entropy seed to generate a key.
diff --git a/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper_test.cc b/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper_test.cc
index d9c9c33..b6ca7bd 100644
--- a/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper_test.cc
+++ b/cc/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/cecpq2_aead_hkdf_dem_helper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/cecpq2_hybrid_key_templates.cc b/cc/experimental/pqcrypto/kem/cecpq2_hybrid_key_templates.cc
index c51a426..f6a24f7 100644
--- a/cc/experimental/pqcrypto/kem/cecpq2_hybrid_key_templates.cc
+++ b/cc/experimental/pqcrypto/kem/cecpq2_hybrid_key_templates.cc
@@ -29,7 +29,6 @@
 namespace tink {
 namespace {
 
-using google::crypto::tink::Cecpq2AeadHkdfKeyFormat;
 using google::crypto::tink::EcPointFormat;
 using google::crypto::tink::EllipticCurveType;
 using google::crypto::tink::HashType;
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt.cc b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt.cc
index 933eb4a..2ed06d8 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt.cc
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt_test.cc b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt_test.cc
index d141836..ffa659a 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt_test.cc
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt_test.cc
@@ -16,8 +16,10 @@
 
 #include "tink/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_decrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt.cc b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt.cc
index eb0c665..9d405b9 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt.cc
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt_test.cc b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt_test.cc
index 42d5b03..299e2c2 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt_test.cc
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/subtle/cecpq2_aead_hkdf_hybrid_encrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_recipient_kem_boringssl.cc b/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_recipient_kem_boringssl.cc
index f8e7baa..5eca870 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_recipient_kem_boringssl.cc
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_recipient_kem_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_recipient_kem_boringssl.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/memory/memory.h"
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.cc b/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.cc
index a67c8b5..7f21162 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.cc
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.h b/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.h
index e24e8b8..be57c87 100644
--- a/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.h
+++ b/cc/experimental/pqcrypto/kem/subtle/cecpq2_hkdf_sender_kem_boringssl.h
@@ -89,9 +89,8 @@
   // curves is trivial.
   static crypto::tink::util::StatusOr<
       std::unique_ptr<const Cecpq2HkdfSenderKemBoringSsl>>
-  New(EllipticCurveType curve, const absl::string_view ec_pubx,
-      const absl::string_view ec_puby,
-      const absl::string_view marshalled_hrss_pub);
+  New(EllipticCurveType curve, absl::string_view ec_pubx,
+      absl::string_view ec_puby, absl::string_view marshalled_hrss_pub);
 
   // Generates ephemeral key pairs, computes ECC's shared secret based on
   // generated ephemeral key and recipient's public key, generate a random
@@ -114,9 +113,8 @@
   // must be a big-endian byte array, and recipient's HRSS public key.
   static crypto::tink::util::StatusOr<
       std::unique_ptr<const Cecpq2HkdfSenderKemBoringSsl>>
-  New(EllipticCurveType curve, const absl::string_view pubx,
-      const absl::string_view puby,
-      const absl::string_view marshalled_hrss_pub);
+  New(EllipticCurveType curve, absl::string_view pubx, absl::string_view puby,
+      absl::string_view marshalled_hrss_pub);
 
   // Generates an ephemeral X25519 key pair, computes the X25519's shared secret
   // based on the ephemeral key and recipient's public key, generates a random
@@ -136,8 +134,7 @@
   // curve is not provided as a parameter here because the curve validation has
   // already been made in the New() method defined above.
   explicit Cecpq2HkdfX25519SenderKemBoringSsl(
-      const absl::string_view peer_ec_pubx,
-      const absl::string_view marshalled_hrss_pub);
+      absl::string_view peer_ec_pubx, absl::string_view marshalled_hrss_pub);
 
   // X25519 and HRSS public key containers. We note that the BoringSSL
   // implementation of HRSS requires that the HRSS public key is stored in the
diff --git a/cc/experimental/pqcrypto/kem/util/test_util.cc b/cc/experimental/pqcrypto/kem/util/test_util.cc
index ff4e43b..d019189 100644
--- a/cc/experimental/pqcrypto/kem/util/test_util.cc
+++ b/cc/experimental/pqcrypto/kem/util/test_util.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/kem/util/test_util.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/kem/util/test_util_test.cc b/cc/experimental/pqcrypto/kem/util/test_util_test.cc
index ee7c3ca..98c48d6 100644
--- a/cc/experimental/pqcrypto/kem/util/test_util_test.cc
+++ b/cc/experimental/pqcrypto/kem/util/test_util_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/kem/util/test_util.h"
 
+#include <vector>
+
 #include "gtest/gtest.h"
 #include "tink/aead/aes_gcm_key_manager.h"
 #include "tink/hybrid_encrypt.h"
diff --git a/cc/experimental/pqcrypto/signature/dilithium_key_template.cc b/cc/experimental/pqcrypto/signature/dilithium_key_template.cc
index adbaf94..14f52a3 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_key_template.cc
+++ b/cc/experimental/pqcrypto/signature/dilithium_key_template.cc
@@ -40,7 +40,7 @@
 using google::crypto::tink::KeyTemplate;
 using google::crypto::tink::OutputPrefixType;
 
-KeyTemplate* NewDilithiumKeyTemplate(int32 key_size,
+KeyTemplate* NewDilithiumKeyTemplate(int32_t key_size,
                                      DilithiumSeedExpansion seed_expansion) {
   KeyTemplate* key_template = new KeyTemplate;
   key_template->set_type_url(
diff --git a/cc/experimental/pqcrypto/signature/dilithium_key_template.h b/cc/experimental/pqcrypto/signature/dilithium_key_template.h
index cce59e2..c3acff2 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_key_template.h
+++ b/cc/experimental/pqcrypto/signature/dilithium_key_template.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_EXPERIMENTAL_SIGNATURE_DILITHIUM_KEY_TEMPLATE_H_
-#define TINK_EXPERIMENTAL_SIGNATURE_DILITHIUM_KEY_TEMPLATE_H_
+#ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_KEY_TEMPLATE_H_
+#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_KEY_TEMPLATE_H_
 
 #include "proto/tink.pb.h"
 
@@ -38,4 +38,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_EXPERIMENTAL_SIGNATURE_DILITHIUM_KEY_TEMPLATE_H_
+#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_KEY_TEMPLATE_H_
diff --git a/cc/experimental/pqcrypto/signature/dilithium_key_template_test.cc b/cc/experimental/pqcrypto/signature/dilithium_key_template_test.cc
index f4b2858..ac8c9c8 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_key_template_test.cc
+++ b/cc/experimental/pqcrypto/signature/dilithium_key_template_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/dilithium_key_template.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -50,7 +51,7 @@
 
 struct DilithiumKeyTemplateTestCase {
   std::string test_name;
-  int32 key_size;
+  int32_t key_size;
   DilithiumSeedExpansion seed_expansion;
   KeyTemplate key_template;
 };
diff --git a/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.cc b/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.cc
index 3acb09b..2ff7e1b 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.cc
+++ b/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.cc
@@ -16,6 +16,9 @@
 
 #include "tink/experimental/pqcrypto/signature/dilithium_sign_key_manager.h"
 
+#include <memory>
+#include <utility>
+
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
diff --git a/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.h b/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.h
index 6e83592..4f5abb0 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.h
+++ b/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_SIGN_KEY_MANAGER_H_
 #define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager_test.cc b/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager_test.cc
index 92f5423..bca0d26 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager_test.cc
+++ b/cc/experimental/pqcrypto/signature/dilithium_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/dilithium_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -70,7 +71,7 @@
 
 // Helper function that returns a valid dilithium key format.
 StatusOr<DilithiumKeyFormat> CreateValidKeyFormat(
-    int32 private_key_size, DilithiumSeedExpansion seed_expansion) {
+    int32_t private_key_size, DilithiumSeedExpansion seed_expansion) {
   DilithiumKeyFormat key_format;
   DilithiumParams* params = key_format.mutable_params();
   params->set_key_size(private_key_size);
diff --git a/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.cc b/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.cc
index 740e6f5..66d3c75 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.cc
+++ b/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/signature/dilithium_verify_key_manager.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
diff --git a/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.h b/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.h
index b139ab8..4893220 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.h
+++ b/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager.h
@@ -14,9 +14,10 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SUBTLE_DILITHIUM_VERIFY_KEY_MANAGER_H_
-#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SUBTLE_DILITHIUM_VERIFY_KEY_MANAGER_H_
+#ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_VERIFY_KEY_MANAGER_H_
+#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
@@ -71,4 +72,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SUBTLE_DILITHIUM_VERIFY_KEY_MANAGER_H_
+#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_DILITHIUM_VERIFY_KEY_MANAGER_H_
diff --git a/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager_test.cc b/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager_test.cc
index e9c1695..a093fea 100644
--- a/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager_test.cc
+++ b/cc/experimental/pqcrypto/signature/dilithium_verify_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/dilithium_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -70,7 +71,7 @@
 
 // Helper function that returns a valid dilithium private key.
 StatusOr<DilithiumPrivateKey> CreateValidPrivateKey(
-    int32 private_key_size, DilithiumSeedExpansion seed_expansion) {
+    int32_t private_key_size, DilithiumSeedExpansion seed_expansion) {
   DilithiumKeyFormat key_format;
   DilithiumParams* params = key_format.mutable_params();
   params->set_key_size(private_key_size);
@@ -81,7 +82,7 @@
 
 // Helper function that returns a valid dilithium public key.
 StatusOr<DilithiumPublicKey> CreateValidPublicKey(
-    int32 private_key_size, DilithiumSeedExpansion seed_expansion) {
+    int32_t private_key_size, DilithiumSeedExpansion seed_expansion) {
   StatusOr<DilithiumPrivateKey> private_key =
       CreateValidPrivateKey(private_key_size, seed_expansion);
 
diff --git a/cc/experimental/pqcrypto/signature/falcon_key_template.cc b/cc/experimental/pqcrypto/signature/falcon_key_template.cc
index 668259b..bd33a70 100644
--- a/cc/experimental/pqcrypto/signature/falcon_key_template.cc
+++ b/cc/experimental/pqcrypto/signature/falcon_key_template.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/signature/falcon_key_template.h"
 
+#include <memory>
+
 #include "tink/experimental/pqcrypto/signature/subtle/falcon_subtle_utils.h"
 #include "tink/util/constants.h"
 #include "proto/experimental/pqcrypto/falcon.pb.h"
diff --git a/cc/experimental/pqcrypto/signature/falcon_key_template_test.cc b/cc/experimental/pqcrypto/signature/falcon_key_template_test.cc
index 8c9c167..2af3a5c 100644
--- a/cc/experimental/pqcrypto/signature/falcon_key_template_test.cc
+++ b/cc/experimental/pqcrypto/signature/falcon_key_template_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/falcon_key_template.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.cc b/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.cc
index 9d1d3ed..0b431c5 100644
--- a/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.cc
+++ b/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/signature/falcon_sign_key_manager.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
diff --git a/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.h b/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.h
index 0ce6cc2..32a6f14 100644
--- a/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.h
+++ b/cc/experimental/pqcrypto/signature/falcon_sign_key_manager.h
@@ -17,7 +17,7 @@
 #ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_FALCON_SIGN_KEY_MANAGER_H_
 #define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_FALCON_SIGN_KEY_MANAGER_H_
 
-
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/experimental/pqcrypto/signature/falcon_sign_key_manager_test.cc b/cc/experimental/pqcrypto/signature/falcon_sign_key_manager_test.cc
index eccd0bb..06ff16a 100644
--- a/cc/experimental/pqcrypto/signature/falcon_sign_key_manager_test.cc
+++ b/cc/experimental/pqcrypto/signature/falcon_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/falcon_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -54,7 +55,7 @@
 using FalconSignKeyManagerTest = testing::TestWithParam<FalconTestCase>;
 
 // Helper function that returns a valid falcon key format.
-StatusOr<FalconKeyFormat> CreateValidKeyFormat(int32 private_key_size) {
+StatusOr<FalconKeyFormat> CreateValidKeyFormat(int32_t private_key_size) {
   FalconKeyFormat key_format;
   key_format.set_key_size(private_key_size);
 
diff --git a/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.cc b/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.cc
index 26dab4b..125c802 100644
--- a/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.cc
+++ b/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/signature/falcon_verify_key_manager.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
diff --git a/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.h b/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.h
index 9956700..dabe237 100644
--- a/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.h
+++ b/cc/experimental/pqcrypto/signature/falcon_verify_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_FALCON_VERIFY_KEY_MANAGER_H_
 #define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_FALCON_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/experimental/pqcrypto/signature/falcon_verify_key_manager_test.cc b/cc/experimental/pqcrypto/signature/falcon_verify_key_manager_test.cc
index c486288..3f89de5 100644
--- a/cc/experimental/pqcrypto/signature/falcon_verify_key_manager_test.cc
+++ b/cc/experimental/pqcrypto/signature/falcon_verify_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/falcon_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -54,7 +55,7 @@
 using FalconVerifyKeyManagerTest = testing::TestWithParam<FalconTestCase>;
 
 // Helper function that returns a valid falcon private key.
-StatusOr<FalconPrivateKey> CreateValidPrivateKey(int32 private_key_size) {
+StatusOr<FalconPrivateKey> CreateValidPrivateKey(int32_t private_key_size) {
   FalconKeyFormat key_format;
   key_format.set_key_size(private_key_size);
 
@@ -62,7 +63,7 @@
 }
 
 // Helper function that returns a valid falcon public key.
-StatusOr<FalconPublicKey> CreateValidPublicKey(int32 private_key_size) {
+StatusOr<FalconPublicKey> CreateValidPublicKey(int32_t private_key_size) {
   StatusOr<FalconPrivateKey> private_key =
       CreateValidPrivateKey(private_key_size);
 
diff --git a/cc/experimental/pqcrypto/signature/signature_config_test.cc b/cc/experimental/pqcrypto/signature/signature_config_test.cc
index 2526126..11605e1 100644
--- a/cc/experimental/pqcrypto/signature/signature_config_test.cc
+++ b/cc/experimental/pqcrypto/signature/signature_config_test.cc
@@ -19,13 +19,13 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/config/tink_fips.h"
 #include "tink/experimental/pqcrypto/signature/dilithium_sign_key_manager.h"
 #include "tink/experimental/pqcrypto/signature/dilithium_verify_key_manager.h"
-#include "tink/experimental/pqcrypto/signature/sphincs_sign_key_manager.h"
-#include "tink/experimental/pqcrypto/signature/sphincs_verify_key_manager.h"
 #include "tink/experimental/pqcrypto/signature/falcon_sign_key_manager.h"
 #include "tink/experimental/pqcrypto/signature/falcon_verify_key_manager.h"
+#include "tink/experimental/pqcrypto/signature/sphincs_sign_key_manager.h"
+#include "tink/experimental/pqcrypto/signature/sphincs_verify_key_manager.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
 #include "tink/registry.h"
@@ -45,7 +45,7 @@
 };
 
 TEST_F(PcqSignatureConfigTest, CheckDilithium) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used";
   }
 
@@ -69,7 +69,7 @@
 }
 
 TEST_F(PcqSignatureConfigTest, CheckSphincs) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used";
   }
 
@@ -93,7 +93,7 @@
 }
 
 TEST_F(PcqSignatureConfigTest, CheckFalcon) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used";
   }
 
diff --git a/cc/experimental/pqcrypto/signature/signature_config_util_test.cc b/cc/experimental/pqcrypto/signature/signature_config_util_test.cc
index 13bc155..f72e470 100644
--- a/cc/experimental/pqcrypto/signature/signature_config_util_test.cc
+++ b/cc/experimental/pqcrypto/signature/signature_config_util_test.cc
@@ -14,16 +14,16 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#include "tink/experimental/pqcrypto/signature/signature_config.h"
-
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
-#include "tink/config/tink_fips.h"
 #include "tink/experimental/pqcrypto/signature/dilithium_sign_key_manager.h"
 #include "tink/experimental/pqcrypto/signature/dilithium_verify_key_manager.h"
+#include "tink/experimental/pqcrypto/signature/signature_config.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
 #include "tink/registry.h"
@@ -45,7 +45,7 @@
 };
 
 TEST_F(PcqSignatureConfigTest, CheckStatus) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used";
   }
 
@@ -55,7 +55,7 @@
 // Tests that the PublicKeySignWrapper has been properly registered and we
 // can wrap primitives.
 TEST_F(PcqSignatureConfigTest, PublicKeySignWrapperRegistered) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used";
   }
 
@@ -91,7 +91,7 @@
 // Tests that the PublicKeyVerifyWrapper has been properly registered and we
 // can wrap primitives.
 TEST_F(PcqSignatureConfigTest, PublicKeyVerifyWrapperRegistered) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used";
   }
 
diff --git a/cc/experimental/pqcrypto/signature/sphincs_key_template.cc b/cc/experimental/pqcrypto/signature/sphincs_key_template.cc
index 038b9c2..fb07ad9 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_key_template.cc
+++ b/cc/experimental/pqcrypto/signature/sphincs_key_template.cc
@@ -72,7 +72,7 @@
 using ::google::crypto::tink::SphincsSignatureType;
 using ::google::crypto::tink::SphincsVariant;
 
-KeyTemplate* NewSphincsKeyTemplate(int32 private_key_size,
+KeyTemplate* NewSphincsKeyTemplate(int32_t private_key_size,
                                    SphincsHashType hash_type,
                                    SphincsVariant variant,
                                    SphincsSignatureType type) {
diff --git a/cc/experimental/pqcrypto/signature/sphincs_key_template_test.cc b/cc/experimental/pqcrypto/signature/sphincs_key_template_test.cc
index 071feb4..90cfdd4 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_key_template_test.cc
+++ b/cc/experimental/pqcrypto/signature/sphincs_key_template_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/sphincs_key_template.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.cc b/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.cc
index c481ae4..b5087ad 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.cc
+++ b/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/signature/sphincs_sign_key_manager.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
diff --git a/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.h b/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.h
index 03655e7..9c9d096 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.h
+++ b/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SPHINCS_SIGN_KEY_MANAGER_H_
 #define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SPHINCS_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager_test.cc b/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager_test.cc
index 7005f48..4b15c60 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager_test.cc
+++ b/cc/experimental/pqcrypto/signature/sphincs_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/sphincs_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -104,7 +105,7 @@
 using SphincsSignKeyManagerTest = testing::TestWithParam<SphincsTestCase>;
 
 // Helper function that returns a valid sphincs key format.
-StatusOr<SphincsKeyFormat> CreateValidKeyFormat(int32 private_key_size,
+StatusOr<SphincsKeyFormat> CreateValidKeyFormat(int32_t private_key_size,
                                                 SphincsHashType hash_type,
                                                 SphincsVariant variant,
                                                 SphincsSignatureType type) {
diff --git a/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.cc b/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.cc
index 438ccf3..0acf19c 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.cc
+++ b/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/experimental/pqcrypto/signature/sphincs_verify_key_manager.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
diff --git a/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.h b/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.h
index d0d2283..70fe0b3 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.h
+++ b/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SPHINCS_VERIFY_KEY_MANAGER_H_
 #define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SPHINCS_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager_test.cc b/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager_test.cc
index b17f57c..f8b8d56 100644
--- a/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager_test.cc
+++ b/cc/experimental/pqcrypto/signature/sphincs_verify_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/sphincs_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -102,7 +103,7 @@
 using SphincsVerifyKeyManagerTest = testing::TestWithParam<SphincsTestCase>;
 
 // Helper function that returns a valid sphincs private key.
-StatusOr<SphincsPrivateKey> CreateValidPrivateKey(int32 private_key_size,
+StatusOr<SphincsPrivateKey> CreateValidPrivateKey(int32_t private_key_size,
                                                   SphincsHashType hash_type,
                                                   SphincsVariant variant,
                                                   SphincsSignatureType type) {
@@ -117,7 +118,7 @@
 }
 
 // Helper function that returns a valid sphincs public key.
-StatusOr<SphincsPublicKey> CreateValidPublicKey(int32 private_key_size,
+StatusOr<SphincsPublicKey> CreateValidPublicKey(int32_t private_key_size,
                                                 SphincsHashType hash_type,
                                                 SphincsVariant variant,
                                                 SphincsSignatureType type) {
diff --git a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign.cc b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign.cc
index 6fd6d82..3e7e66a 100644
--- a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign.cc
@@ -20,6 +20,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <iterator>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign_test.cc b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign_test.cc
index a0dcba5..fb45802 100644
--- a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/dilithium_avx2_sign.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify.cc b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify.cc
index b12c44f..d984a75 100644
--- a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify.cc
@@ -19,6 +19,7 @@
 #include <algorithm>
 #include <cstddef>
 #include <iterator>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify_test.cc b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify_test.cc
index 6d90fc3..9caf4d1 100644
--- a/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/dilithium_avx2_verify.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/falcon_sign.cc b/cc/experimental/pqcrypto/signature/subtle/falcon_sign.cc
index 63196a2..af83e92 100644
--- a/cc/experimental/pqcrypto/signature/subtle/falcon_sign.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/falcon_sign.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/falcon_sign.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/falcon_sign_test.cc b/cc/experimental/pqcrypto/signature/subtle/falcon_sign_test.cc
index 639a69a..a23a1f6 100644
--- a/cc/experimental/pqcrypto/signature/subtle/falcon_sign_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/falcon_sign_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/falcon_sign.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils.h b/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils.h
index 302803a..81b1e08 100644
--- a/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils.h
+++ b/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils.h
@@ -102,7 +102,7 @@
 // This is an utility function that generates a new Falcon key pair.
 // This function is expected to be called from a key manager class.
 crypto::tink::util::StatusOr<FalconKeyPair> GenerateFalconKeyPair(
-    int32 private_key_size);
+    int32_t private_key_size);
 
 // Validates whether the private key size is safe to use for falcon signature.
 crypto::tink::util::Status ValidateFalconPrivateKeySize(int32_t key_size);
diff --git a/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils_test.cc b/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils_test.cc
index 0b05363..552a463 100644
--- a/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/falcon_subtle_utils_test.cc
@@ -19,6 +19,7 @@
 #include <climits>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/experimental/pqcrypto/signature/subtle/falcon_verify.cc b/cc/experimental/pqcrypto/signature/subtle/falcon_verify.cc
index 1284c30..6ad208f 100644
--- a/cc/experimental/pqcrypto/signature/subtle/falcon_verify.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/falcon_verify.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/falcon_verify.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/falcon_verify_test.cc b/cc/experimental/pqcrypto/signature/subtle/falcon_verify_test.cc
index c1cad8d..0372e2d 100644
--- a/cc/experimental/pqcrypto/signature/subtle/falcon_verify_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/falcon_verify_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/falcon_verify.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/sphincs_helper_pqclean.h b/cc/experimental/pqcrypto/signature/subtle/sphincs_helper_pqclean.h
index 4e8c99e..12d71ea 100644
--- a/cc/experimental/pqcrypto/signature/subtle/sphincs_helper_pqclean.h
+++ b/cc/experimental/pqcrypto/signature/subtle/sphincs_helper_pqclean.h
@@ -34,7 +34,7 @@
 
   SphincsHelperPqclean(const SphincsHelperPqclean &other) = delete;
   SphincsHelperPqclean &operator=(const SphincsHelperPqclean &other) = delete;
-  virtual ~SphincsHelperPqclean() {}
+  virtual ~SphincsHelperPqclean() = default;
 
   // Arguments:
   //   sig - output signature (allocated buffer of size at least
diff --git a/cc/experimental/pqcrypto/signature/subtle/sphincs_sign.cc b/cc/experimental/pqcrypto/signature/subtle/sphincs_sign.cc
index ee8257d..48d96f1 100644
--- a/cc/experimental/pqcrypto/signature/subtle/sphincs_sign.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/sphincs_sign.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/sphincs_sign.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -51,7 +52,7 @@
 }
 
 util::StatusOr<std::string> SphincsSign::Sign(absl::string_view data) const {
-  util::StatusOr<int32> key_size_index =
+  util::StatusOr<int32_t> key_size_index =
       SphincsKeySizeToIndex(key_.GetKey().size());
   if (!key_size_index.ok()) {
     return key_size_index.status();
diff --git a/cc/experimental/pqcrypto/signature/subtle/sphincs_sign_test.cc b/cc/experimental/pqcrypto/signature/subtle/sphincs_sign_test.cc
index 58df17c..070190b 100644
--- a/cc/experimental/pqcrypto/signature/subtle/sphincs_sign_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/sphincs_sign_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/sphincs_sign.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/experimental/pqcrypto/signature/subtle/sphincs_subtle_utils.h b/cc/experimental/pqcrypto/signature/subtle/sphincs_subtle_utils.h
index 95fa9e9..964638c 100644
--- a/cc/experimental/pqcrypto/signature/subtle/sphincs_subtle_utils.h
+++ b/cc/experimental/pqcrypto/signature/subtle/sphincs_subtle_utils.h
@@ -62,7 +62,7 @@
   SphincsHashType hash_type;
   SphincsVariant variant;
   SphincsSignatureType sig_length_type;
-  int32 private_key_size;
+  int32_t private_key_size;
 };
 
 // Representation of the Sphincs private key.
@@ -127,10 +127,10 @@
     SphincsParamsPqclean params);
 
 // Validates whether the private key size is safe to use for sphincs signature.
-crypto::tink::util::Status ValidatePrivateKeySize(int32 key_size);
+crypto::tink::util::Status ValidatePrivateKeySize(int32_t key_size);
 
 // Validates whether the public key size is safe to use for sphincs signature.
-crypto::tink::util::Status ValidatePublicKeySize(int32 key_size);
+crypto::tink::util::Status ValidatePublicKeySize(int32_t key_size);
 
 // Validates whether the parameters are safe to use for sphincs signature.
 crypto::tink::util::Status ValidateParams(SphincsParamsPqclean params);
@@ -138,7 +138,7 @@
 
 // Convert the sphincs private key size to the appropiate index in the
 // pqclean functions array.
-crypto::tink::util::StatusOr<int32> SphincsKeySizeToIndex(int32 key_size);
+crypto::tink::util::StatusOr<int32_t> SphincsKeySizeToIndex(int32_t key_size);
 
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/experimental/pqcrypto/signature/subtle/sphincs_verify.cc b/cc/experimental/pqcrypto/signature/subtle/sphincs_verify.cc
index 88379fa..6582b86 100644
--- a/cc/experimental/pqcrypto/signature/subtle/sphincs_verify.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/sphincs_verify.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/sphincs_verify.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -54,7 +55,7 @@
 util::Status SphincsVerify::Verify(absl::string_view signature,
                                    absl::string_view data) const {
   SphincsParamsPqclean params = key_.GetParams();
-  util::StatusOr<int32> key_size_index =
+  util::StatusOr<int32_t> key_size_index =
       SphincsKeySizeToIndex(params.private_key_size);
   if (!key_size_index.ok()) {
     return key_size_index.status();
diff --git a/cc/experimental/pqcrypto/signature/subtle/sphincs_verify_test.cc b/cc/experimental/pqcrypto/signature/subtle/sphincs_verify_test.cc
index 544db98..f0a48c5 100644
--- a/cc/experimental/pqcrypto/signature/subtle/sphincs_verify_test.cc
+++ b/cc/experimental/pqcrypto/signature/subtle/sphincs_verify_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/experimental/pqcrypto/signature/subtle/sphincs_verify.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/extensions.bzl b/cc/extensions.bzl
new file mode 100644
index 0000000..6ab6fc7
--- /dev/null
+++ b/cc/extensions.bzl
@@ -0,0 +1,30 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tink C++ Bazel Module extensions."""
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+def _wycheproof_impl(_ctx):
+    # Commit from 2019-12-17.
+    http_archive(
+        name = "wycheproof",
+        strip_prefix = "wycheproof-d8ed1ba95ac4c551db67f410c06131c3bc00a97c",
+        url = "https://github.com/google/wycheproof/archive/d8ed1ba95ac4c551db67f410c06131c3bc00a97c.zip",
+        sha256 = "eb1d558071acf1aa6d677d7f1cabec2328d1cf8381496c17185bd92b52ce7545",
+    )
+
+wycheproof_extension = module_extension(
+    implementation = _wycheproof_impl,
+)
diff --git a/cc/hybrid/BUILD.bazel b/cc/hybrid/BUILD.bazel
index 27c119e..209de7b 100644
--- a/cc/hybrid/BUILD.bazel
+++ b/cc/hybrid/BUILD.bazel
@@ -149,22 +149,27 @@
     deps = [
         "//:aead",
         "//:deterministic_aead",
-        "//:key_manager",
-        "//:registry",
+        "//aead:aes_ctr_hmac_aead_key_manager",
         "//daead/subtle:aead_or_daead",
         "//proto:aes_ctr_cc_proto",
         "//proto:aes_ctr_hmac_aead_cc_proto",
         "//proto:aes_gcm_cc_proto",
         "//proto:aes_siv_cc_proto",
+        "//proto:common_cc_proto",
         "//proto:hmac_cc_proto",
         "//proto:tink_cc_proto",
         "//proto:xchacha20_poly1305_cc_proto",
+        "//subtle:aes_gcm_boringssl",
+        "//subtle:aes_siv_boringssl",
+        "//subtle:xchacha20_poly1305_boringssl",
         "//util:errors",
         "//util:protobuf_helper",
         "//util:secret_data",
         "//util:statusor",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
+        "@com_google_absl//absl/strings",
     ],
 )
 
@@ -284,15 +289,15 @@
         ":ecies_aead_hkdf_public_key_manager",
         ":hybrid_config",
         ":hybrid_key_templates",
-        "//:config",
         "//:hybrid_decrypt",
         "//:hybrid_encrypt",
         "//:keyset_handle",
         "//:registry",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:status",
         "//util:test_matchers",
         "//util:test_util",
+        "@boringssl//:crypto",
         "@com_google_absl//absl/status",
         "@com_google_googletest//:gtest_main",
     ],
@@ -372,7 +377,6 @@
         ":ecies_aead_hkdf_public_key_manager",
         ":hybrid_config",
         ":hybrid_decrypt_factory",
-        "//:config",
         "//:crypto_format",
         "//:hybrid_decrypt",
         "//:hybrid_encrypt",
@@ -394,7 +398,6 @@
     deps = [
         ":hybrid_config",
         ":hybrid_encrypt_factory",
-        "//:config",
         "//:crypto_format",
         "//:hybrid_encrypt",
         "//:keyset_handle",
@@ -433,7 +436,6 @@
     srcs = ["ecies_aead_hkdf_dem_helper_test.cc"],
     deps = [
         ":ecies_aead_hkdf_dem_helper",
-        "//:registry",
         "//aead:aes_gcm_key_manager",
         "//daead:aes_siv_key_manager",
         "//util:secret_data",
@@ -452,7 +454,6 @@
         ":ecies_aead_hkdf_hybrid_decrypt",
         ":ecies_aead_hkdf_hybrid_encrypt",
         "//:hybrid_decrypt",
-        "//:registry",
         "//aead:aes_ctr_hmac_aead_key_manager",
         "//aead:aes_gcm_key_manager",
         "//aead:xchacha20_poly1305_key_manager",
@@ -479,7 +480,6 @@
     deps = [
         ":ecies_aead_hkdf_hybrid_encrypt",
         "//:hybrid_encrypt",
-        "//:registry",
         "//aead:aes_gcm_key_manager",
         "//internal:ec_util",
         "//proto:common_cc_proto",
diff --git a/cc/hybrid/BUILD.gn b/cc/hybrid/BUILD.gn
index f796359..9a507bc 100644
--- a/cc/hybrid/BUILD.gn
+++ b/cc/hybrid/BUILD.gn
@@ -176,18 +176,23 @@
   public_deps = [
     "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/status:statusor",
+    "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/tink/cc:aead",
     "//third_party/tink/cc:deterministic_aead",
-    "//third_party/tink/cc:key_manager",
-    "//third_party/tink/cc:registry",
+    "//third_party/tink/cc/aead:aes_ctr_hmac_aead_key_manager",
     "//third_party/tink/cc/daead/subtle:aead_or_daead",
     "//third_party/tink/cc/proto:aes_ctr_hmac_aead_proto",
     "//third_party/tink/cc/proto:aes_ctr_proto",
     "//third_party/tink/cc/proto:aes_gcm_proto",
     "//third_party/tink/cc/proto:aes_siv_proto",
+    "//third_party/tink/cc/proto:common_proto",
     "//third_party/tink/cc/proto:hmac_proto",
     "//third_party/tink/cc/proto:tink_proto",
     "//third_party/tink/cc/proto:xchacha20_poly1305_proto",
+    "//third_party/tink/cc/subtle:aes_gcm_boringssl",
+    "//third_party/tink/cc/subtle:aes_siv_boringssl",
+    "//third_party/tink/cc/subtle:xchacha20_poly1305_boringssl",
     "//third_party/tink/cc/util:errors",
     "//third_party/tink/cc/util:protobuf_helper",
     "//third_party/tink/cc/util:secret_data",
diff --git a/cc/hybrid/CMakeLists.txt b/cc/hybrid/CMakeLists.txt
index 67e17b7..cd213c8 100644
--- a/cc/hybrid/CMakeLists.txt
+++ b/cc/hybrid/CMakeLists.txt
@@ -137,11 +137,15 @@
   DEPS
     absl::memory
     absl::status
+    absl::statusor
+    absl::strings
     tink::core::aead
     tink::core::deterministic_aead
-    tink::core::key_manager
-    tink::core::registry
+    tink::aead::aes_ctr_hmac_aead_key_manager
     tink::daead::subtle::aead_or_daead
+    tink::subtle::aes_gcm_boringssl
+    tink::subtle::aes_siv_boringssl
+    tink::subtle::xchacha20_poly1305_boringssl
     tink::util::errors
     tink::util::protobuf_helper
     tink::util::secret_data
@@ -150,6 +154,7 @@
     tink::proto::aes_ctr_hmac_aead_cc_proto
     tink::proto::aes_gcm_cc_proto
     tink::proto::aes_siv_cc_proto
+    tink::proto::common_cc_proto
     tink::proto::hmac_cc_proto
     tink::proto::tink_cc_proto
     tink::proto::xchacha20_poly1305_cc_proto
@@ -258,12 +263,12 @@
     tink::hybrid::hybrid_key_templates
     gmock
     absl::status
-    tink::core::config
+    crypto
     tink::core::hybrid_decrypt
     tink::core::hybrid_encrypt
     tink::core::keyset_handle
     tink::core::registry
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::status
     tink::util::test_matchers
     tink::util::test_util
@@ -343,7 +348,6 @@
     tink::hybrid::hybrid_decrypt_factory
     gmock
     absl::memory
-    tink::core::config
     tink::core::crypto_format
     tink::core::hybrid_decrypt
     tink::core::hybrid_encrypt
@@ -363,7 +367,6 @@
     tink::hybrid::hybrid_config
     tink::hybrid::hybrid_encrypt_factory
     gmock
-    tink::core::config
     tink::core::crypto_format
     tink::core::hybrid_encrypt
     tink::core::keyset_handle
@@ -403,7 +406,6 @@
     tink::hybrid::ecies_aead_hkdf_dem_helper
     gmock
     absl::status
-    tink::core::registry
     tink::aead::aes_gcm_key_manager
     tink::daead::aes_siv_key_manager
     tink::util::secret_data
@@ -421,7 +423,6 @@
     gmock
     absl::memory
     tink::core::hybrid_decrypt
-    tink::core::registry
     tink::aead::aes_ctr_hmac_aead_key_manager
     tink::aead::aes_gcm_key_manager
     tink::aead::xchacha20_poly1305_key_manager
@@ -447,7 +448,6 @@
     gmock
     absl::memory
     tink::core::hybrid_encrypt
-    tink::core::registry
     tink::aead::aes_gcm_key_manager
     tink::internal::ec_util
     tink::util::enums
diff --git a/cc/hybrid/ecies_aead_hkdf_dem_helper.cc b/cc/hybrid/ecies_aead_hkdf_dem_helper.cc
index 9c169e1..6e46f4a 100644
--- a/cc/hybrid/ecies_aead_hkdf_dem_helper.cc
+++ b/cc/hybrid/ecies_aead_hkdf_dem_helper.cc
@@ -16,15 +16,22 @@
 
 #include "tink/hybrid/ecies_aead_hkdf_dem_helper.h"
 
+#include <stdint.h>
+
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
 #include "tink/aead.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
 #include "tink/deterministic_aead.h"
-#include "tink/key_manager.h"
-#include "tink/registry.h"
+#include "tink/subtle/aes_gcm_boringssl.h"
+#include "tink/subtle/aes_siv_boringssl.h"
+#include "tink/subtle/xchacha20_poly1305_boringssl.h"
 #include "tink/util/errors.h"
 #include "tink/util/protobuf_helper.h"
 #include "tink/util/statusor.h"
@@ -43,69 +50,28 @@
 using ::crypto::tink::subtle::AeadOrDaead;
 using ::google::crypto::tink::AesCtrHmacAeadKey;
 using ::google::crypto::tink::AesCtrHmacAeadKeyFormat;
-using ::google::crypto::tink::AesGcmKey;
 using ::google::crypto::tink::AesGcmKeyFormat;
-using ::google::crypto::tink::AesSivKey;
 using ::google::crypto::tink::AesSivKeyFormat;
 using ::google::crypto::tink::KeyTemplate;
-using ::google::crypto::tink::XChaCha20Poly1305Key;
 using ::google::crypto::tink::XChaCha20Poly1305KeyFormat;
 
-// Internal implementaton of the EciesAeadHkdfDemHelper class, paremetrized by
-// the Primitive used for data encapsulation (i.e Aead or DeterministicAead).
-template <class EncryptionPrimitive>
-class EciesAeadHkdfDemHelperImpl : public EciesAeadHkdfDemHelper {
- public:
-  static util::StatusOr<std::unique_ptr<const EciesAeadHkdfDemHelper>> New(
-      const google::crypto::tink::KeyTemplate& dem_key_template,
-      const DemKeyParams& key_params, const std::string& dem_type_url) {
-    auto key_manager_or =
-        Registry::get_key_manager<EncryptionPrimitive>(dem_type_url);
-    if (!key_manager_or.ok()) {
-      return ToStatusF(
-          absl::StatusCode::kFailedPrecondition,
-          "No manager for DEM key type '%s' found in the registry.",
-          dem_type_url);
-    }
-    const KeyManager<EncryptionPrimitive>* key_manager = key_manager_or.value();
-    return {absl::make_unique<EciesAeadHkdfDemHelperImpl<EncryptionPrimitive>>(
-        key_manager, dem_key_template, key_params)};
+crypto::tink::util::StatusOr<std::unique_ptr<AeadOrDaead>> Wrap(
+    crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::Aead>> aead_or) {
+  if (!aead_or.ok()) {
+    return aead_or.status();
   }
+  return std::make_unique<AeadOrDaead>(std::move(aead_or.value()));
+}
 
-  EciesAeadHkdfDemHelperImpl(
-      const KeyManager<EncryptionPrimitive>* key_manager,
-      const google::crypto::tink::KeyTemplate& key_template,
-      DemKeyParams key_params)
-      : EciesAeadHkdfDemHelper(key_template, key_params),
-        key_manager_(key_manager) {}
-
- protected:
-  crypto::tink::util::StatusOr<
-      std::unique_ptr<crypto::tink::subtle::AeadOrDaead>>
-  GetAeadOrDaead(const util::SecretData& symmetric_key_value) const override {
-    if (symmetric_key_value.size() != key_params_.key_size_in_bytes) {
-      return util::Status(absl::StatusCode::kInternal,
-                          "Wrong length of symmetric key.");
-    }
-    auto key_or = key_manager_->get_key_factory().NewKey(key_template_.value());
-    if (!key_or.ok()) return key_or.status();
-    auto key = std::move(key_or).value();
-    if (!ReplaceKeyBytes(symmetric_key_value, key.get())) {
-      return util::Status(absl::StatusCode::kInternal,
-                          "Generation of DEM-key failed.");
-    }
-
-    util::StatusOr<std::unique_ptr<EncryptionPrimitive>> primitive_or =
-        key_manager_->GetPrimitive(*key);
-    ZeroKeyBytes(key.get());
-
-    if (!primitive_or.ok()) return primitive_or.status();
-    return absl::make_unique<AeadOrDaead>(std::move(primitive_or.value()));
+crypto::tink::util::StatusOr<std::unique_ptr<AeadOrDaead>> Wrap(
+    crypto::tink::util::StatusOr<
+        std::unique_ptr<crypto::tink::DeterministicAead>>
+        daead_or) {
+  if (!daead_or.ok()) {
+    return daead_or.status();
   }
-
- private:
-  const KeyManager<EncryptionPrimitive>* key_manager_;  // not owned
-};
+  return std::make_unique<AeadOrDaead>(std::move(daead_or.value()));
+}
 
 }  // namespace
 
@@ -129,7 +95,10 @@
     uint32_t dem_key_size = key_format.aes_ctr_key_format().key_size() +
                             key_format.hmac_key_format().key_size();
     return {{AES_CTR_HMAC_AEAD_KEY, dem_key_size,
-             key_format.aes_ctr_key_format().key_size()}};
+             key_format.aes_ctr_key_format().key_size(),
+             key_format.aes_ctr_key_format().params().iv_size(),
+             key_format.hmac_key_format().params().hash(),
+             key_format.hmac_key_format().params().tag_size()}};
   }
   if (type_url ==
       "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key") {
@@ -158,86 +127,41 @@
   auto key_params_or = GetKeyParams(dem_key_template);
   if (!key_params_or.ok()) return key_params_or.status();
   DemKeyParams key_params = key_params_or.value();
-  const std::string& dem_type_url = dem_key_template.type_url();
-
-  if (key_params.key_type == AES_SIV_KEY) {
-    return EciesAeadHkdfDemHelperImpl<DeterministicAead>::New(
-        dem_key_template, key_params, dem_type_url);
-  } else {
-    return EciesAeadHkdfDemHelperImpl<Aead>::New(dem_key_template, key_params,
-                                                 dem_type_url);
-  }
+  return absl::WrapUnique<const EciesAeadHkdfDemHelper>(
+      new EciesAeadHkdfDemHelper(dem_key_template, key_params));
 }
 
-bool EciesAeadHkdfDemHelper::ReplaceKeyBytes(
-    const util::SecretData& key_bytes,
-    portable_proto::MessageLite* proto) const {
+crypto::tink::util::StatusOr<std::unique_ptr<AeadOrDaead>>
+EciesAeadHkdfDemHelper::GetAeadOrDaead(
+    const util::SecretData& symmetric_key_value) const {
+  if (symmetric_key_value.size() != key_params_.key_size_in_bytes) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Wrong length of symmetric key.");
+  }
   switch (key_params_.key_type) {
-    case AES_GCM_KEY: {
-      AesGcmKey* key = static_cast<AesGcmKey*>(proto);
-      key->set_key_value(std::string(util::SecretDataAsStringView(key_bytes)));
-      return true;
-    }
+    case AES_GCM_KEY:
+      return Wrap(subtle::AesGcmBoringSsl::New(symmetric_key_value));
     case AES_CTR_HMAC_AEAD_KEY: {
-      AesCtrHmacAeadKey* key = static_cast<AesCtrHmacAeadKey*>(proto);
-      auto aes_ctr_key = key->mutable_aes_ctr_key();
+      AesCtrHmacAeadKey key;
+      auto aes_ctr_key = key.mutable_aes_ctr_key();
+      aes_ctr_key->mutable_params()->set_iv_size(
+          key_params_.aes_ctr_key_iv_size_in_bytes);
       aes_ctr_key->set_key_value(
-          std::string(util::SecretDataAsStringView(key_bytes).substr(
-              0, key_params_.aes_ctr_key_size_in_bytes)));
-      auto hmac_key = key->mutable_hmac_key();
+          std::string(util::SecretDataAsStringView(symmetric_key_value)
+                          .substr(0, key_params_.aes_ctr_key_size_in_bytes)));
+      auto hmac_key = key.mutable_hmac_key();
+      hmac_key->mutable_params()->set_tag_size(
+          key_params_.hmac_key_tag_size_in_bytes);
+      hmac_key->mutable_params()->set_hash(key_params_.hmac_key_hash);
       hmac_key->set_key_value(
-          std::string(util::SecretDataAsStringView(key_bytes).substr(
-              key_params_.aes_ctr_key_size_in_bytes)));
-      return true;
+          std::string(util::SecretDataAsStringView(symmetric_key_value)
+                          .substr(key_params_.aes_ctr_key_size_in_bytes)));
+      return Wrap(AesCtrHmacAeadKeyManager().GetPrimitive<Aead>(key));
     }
-    case XCHACHA20_POLY1305_KEY: {
-      XChaCha20Poly1305Key* key = static_cast<XChaCha20Poly1305Key*>(proto);
-      key->set_key_value(std::string(util::SecretDataAsStringView(key_bytes)));
-      return true;
-    }
-    case AES_SIV_KEY: {
-      AesSivKey* key = static_cast<AesSivKey*>(proto);
-      key->set_key_value(std::string(util::SecretDataAsStringView(key_bytes)));
-      return true;
-    }
-  }
-  return false;
-}
-
-void EciesAeadHkdfDemHelper::ZeroKeyBytes(
-    portable_proto::MessageLite* proto) const {
-  switch (key_params_.key_type) {
-    case AES_GCM_KEY: {
-      AesGcmKey* key = static_cast<AesGcmKey*>(proto);
-      std::unique_ptr<std::string> key_value =
-          absl::WrapUnique(key->release_key_value());
-      util::SafeZeroString(key_value.get());
-      break;
-    }
-    case AES_CTR_HMAC_AEAD_KEY: {
-      AesCtrHmacAeadKey* key = static_cast<AesCtrHmacAeadKey*>(proto);
-      std::unique_ptr<std::string> aes_ctr_key_value =
-          absl::WrapUnique(key->mutable_aes_ctr_key()->release_key_value());
-      util::SafeZeroString(aes_ctr_key_value.get());
-      std::unique_ptr<std::string> hmac_key_value =
-          absl::WrapUnique(key->mutable_hmac_key()->release_key_value());
-      util::SafeZeroString(hmac_key_value.get());
-      break;
-    }
-    case XCHACHA20_POLY1305_KEY: {
-      XChaCha20Poly1305Key* key = static_cast<XChaCha20Poly1305Key*>(proto);
-      std::unique_ptr<std::string> key_value =
-          absl::WrapUnique(key->release_key_value());
-      util::SafeZeroString(key_value.get());
-      break;
-    }
-    case AES_SIV_KEY: {
-      AesSivKey* key = static_cast<AesSivKey*>(proto);
-      std::unique_ptr<std::string> key_value =
-          absl::WrapUnique(key->release_key_value());
-      util::SafeZeroString(key_value.get());
-      break;
-    }
+    case XCHACHA20_POLY1305_KEY:
+      return Wrap(subtle::XChacha20Poly1305BoringSsl::New(symmetric_key_value));
+    case AES_SIV_KEY:
+      return Wrap(subtle::AesSivBoringSsl::New(symmetric_key_value));
   }
 }
 
diff --git a/cc/hybrid/ecies_aead_hkdf_dem_helper.h b/cc/hybrid/ecies_aead_hkdf_dem_helper.h
index 0f6d160..c00970b 100644
--- a/cc/hybrid/ecies_aead_hkdf_dem_helper.h
+++ b/cc/hybrid/ecies_aead_hkdf_dem_helper.h
@@ -17,14 +17,16 @@
 #ifndef TINK_HYBRID_ECIES_AEAD_HKDF_DEM_HELPER_H_
 #define TINK_HYBRID_ECIES_AEAD_HKDF_DEM_HELPER_H_
 
+#include <stdint.h>
+
 #include <memory>
 
 #include "tink/aead.h"
 #include "tink/daead/subtle/aead_or_daead.h"
-#include "tink/key_manager.h"
 #include "tink/util/protobuf_helper.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/statusor.h"
+#include "proto/common.pb.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
@@ -38,7 +40,7 @@
   crypto::tink::util::StatusOr<std::unique_ptr<const EciesAeadHkdfDemHelper>>
       New(const google::crypto::tink::KeyTemplate& dem_key_template);
 
-  virtual ~EciesAeadHkdfDemHelper() {}
+  virtual ~EciesAeadHkdfDemHelper() = default;
 
   // Returns the size of the DEM-key in bytes.
   uint32_t dem_key_size_in_bytes() const {
@@ -50,7 +52,7 @@
   // be of length dem_key_size_in_bytes().
   virtual crypto::tink::util::StatusOr<
       std::unique_ptr<crypto::tink::subtle::AeadOrDaead>>
-  GetAeadOrDaead(const util::SecretData& symmetric_key_value) const = 0;
+  GetAeadOrDaead(const util::SecretData& symmetric_key_value) const;
 
  protected:
   enum DemKeyType {
@@ -64,6 +66,9 @@
     DemKeyType key_type;
     uint32_t key_size_in_bytes;
     uint32_t aes_ctr_key_size_in_bytes;
+    uint32_t aes_ctr_key_iv_size_in_bytes;
+    google::crypto::tink::HashType hmac_key_hash;
+    uint32_t hmac_key_tag_size_in_bytes;
   };
 
   EciesAeadHkdfDemHelper(const google::crypto::tink::KeyTemplate& key_template,
@@ -73,11 +78,6 @@
   static util::StatusOr<DemKeyParams> GetKeyParams(
       const ::google::crypto::tink::KeyTemplate& key_template);
 
-  bool ReplaceKeyBytes(const util::SecretData& key_bytes,
-                       portable_proto::MessageLite* proto) const;
-
-  void ZeroKeyBytes(portable_proto::MessageLite* proto) const;
-
   const google::crypto::tink::KeyTemplate key_template_;
   const DemKeyParams key_params_;
 };
diff --git a/cc/hybrid/ecies_aead_hkdf_dem_helper_test.cc b/cc/hybrid/ecies_aead_hkdf_dem_helper_test.cc
index 64393ad..a2bbd3a 100644
--- a/cc/hybrid/ecies_aead_hkdf_dem_helper_test.cc
+++ b/cc/hybrid/ecies_aead_hkdf_dem_helper_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/ecies_aead_hkdf_dem_helper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -24,7 +25,6 @@
 #include "absl/status/status.h"
 #include "tink/aead/aes_gcm_key_manager.h"
 #include "tink/daead/aes_siv_key_manager.h"
-#include "tink/registry.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
@@ -69,8 +69,6 @@
   key_format.set_key_size(16);
   std::unique_ptr<AesGcmKeyManager> key_manager(new AesGcmKeyManager());
   std::string dem_key_type = key_manager->get_key_type();
-  ASSERT_THAT(Registry::RegisterKeyTypeManager(std::move(key_manager), true),
-              IsOk());
 
   google::crypto::tink::KeyTemplate dem_key_template;
   dem_key_template.set_type_url(dem_key_type);
@@ -96,8 +94,6 @@
   key_format.set_key_size(64);
   std::unique_ptr<AesSivKeyManager> key_manager(new AesSivKeyManager());
   std::string dem_key_type = key_manager->get_key_type();
-  ASSERT_THAT(Registry::RegisterKeyTypeManager(std::move(key_manager), true),
-              IsOk());
 
   google::crypto::tink::KeyTemplate dem_key_template;
   dem_key_template.set_type_url(dem_key_type);
diff --git a/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt.cc b/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt.cc
index 497b95f..d0c1377 100644
--- a/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt.cc
+++ b/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/ecies_aead_hkdf_hybrid_decrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt_test.cc b/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt_test.cc
index 44b79c2..6bfc624 100644
--- a/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt_test.cc
+++ b/cc/hybrid/ecies_aead_hkdf_hybrid_decrypt_test.cc
@@ -16,8 +16,10 @@
 
 #include "tink/hybrid/ecies_aead_hkdf_hybrid_decrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
@@ -29,7 +31,6 @@
 #include "tink/hybrid_decrypt.h"
 #include "tink/internal/ec_util.h"
 #include "tink/internal/ssl_util.h"
-#include "tink/registry.h"
 #include "tink/subtle/random.h"
 #include "tink/util/enums.h"
 #include "tink/util/statusor.h"
@@ -215,21 +216,6 @@
   }
 }
 
-TEST_F(EciesAeadHkdfHybridDecryptTest, testGettingHybridEncryptWithoutManager) {
-  // Prepare an ECIES key.
-  Registry::Reset();
-  auto ecies_key = test::GetEciesAesGcmHkdfTestKey(EllipticCurveType::NIST_P256,
-                                                   EcPointFormat::UNCOMPRESSED,
-                                                   HashType::SHA256, 32);
-
-  // Try to get a HybridEncrypt primitive without DEM key manager.
-  auto bad_result(EciesAeadHkdfHybridDecrypt::New(ecies_key));
-  EXPECT_FALSE(bad_result.ok());
-  EXPECT_EQ(absl::StatusCode::kFailedPrecondition, bad_result.status().code());
-  EXPECT_PRED_FORMAT2(testing::IsSubstring, "No manager for DEM",
-                      std::string(bad_result.status().message()));
-}
-
 TEST_F(EciesAeadHkdfHybridDecryptTest, testAesGcmHybridDecryption) {
   // Register DEM key manager.
   std::string dem_key_type = AesGcmKeyManager().get_key_type();
diff --git a/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt.cc b/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt.cc
index 2f70a7c..a8af6c6 100644
--- a/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt.cc
+++ b/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/ecies_aead_hkdf_hybrid_encrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt_test.cc b/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt_test.cc
index f2c4277..610fa5a 100644
--- a/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt_test.cc
+++ b/cc/hybrid/ecies_aead_hkdf_hybrid_encrypt_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/ecies_aead_hkdf_hybrid_encrypt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -24,7 +25,6 @@
 #include "tink/aead/aes_gcm_key_manager.h"
 #include "tink/hybrid_encrypt.h"
 #include "tink/internal/ec_util.h"
-#include "tink/registry.h"
 #include "tink/util/enums.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_util.h"
@@ -113,19 +113,6 @@
       HashType::SHA256,
       32);
 
-  // Try to get a HybridEncrypt primitive without DEM key manager.
-  auto bad_result(EciesAeadHkdfHybridEncrypt::New(ecies_key.public_key()));
-  EXPECT_FALSE(bad_result.ok());
-  EXPECT_EQ(absl::StatusCode::kFailedPrecondition, bad_result.status().code());
-  EXPECT_PRED_FORMAT2(testing::IsSubstring, "No manager for DEM",
-                      std::string(bad_result.status().message()));
-
-  // Register DEM key manager.
-  ASSERT_TRUE(Registry::RegisterKeyTypeManager(
-                  absl::make_unique<AesGcmKeyManager>(), true)
-                  .ok());
-  std::string dem_key_type = AesGcmKeyManager().get_key_type();
-
   // Generate and test many keys with various parameters.
   std::string plaintext = "some plaintext";
   std::string context_info = "some context info";
diff --git a/cc/hybrid/ecies_aead_hkdf_private_key_manager.h b/cc/hybrid/ecies_aead_hkdf_private_key_manager.h
index 810f37c..ec82996 100644
--- a/cc/hybrid/ecies_aead_hkdf_private_key_manager.h
+++ b/cc/hybrid/ecies_aead_hkdf_private_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_HYBRID_ECIES_AEAD_HKDF_PRIVATE_KEY_MANAGER_H_
 #define TINK_HYBRID_ECIES_AEAD_HKDF_PRIVATE_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/hybrid/ecies_aead_hkdf_public_key_manager.h b/cc/hybrid/ecies_aead_hkdf_public_key_manager.h
index 10ecedf..9c6d671 100644
--- a/cc/hybrid/ecies_aead_hkdf_public_key_manager.h
+++ b/cc/hybrid/ecies_aead_hkdf_public_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_HYBRID_ECIES_AEAD_HKDF_PUBLIC_KEY_MANAGER_H_
 #define TINK_HYBRID_ECIES_AEAD_HKDF_PUBLIC_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/hybrid/failing_hybrid.cc b/cc/hybrid/failing_hybrid.cc
index 0496d17..6cda606 100644
--- a/cc/hybrid/failing_hybrid.cc
+++ b/cc/hybrid/failing_hybrid.cc
@@ -15,11 +15,12 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/hybrid/failing_hybrid.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "tink/hybrid_encrypt.h"
 #include "absl/strings/string_view.h"
+#include "tink/hybrid_encrypt.h"
 
 namespace crypto {
 namespace tink {
diff --git a/cc/hybrid/failing_hybrid.h b/cc/hybrid/failing_hybrid.h
index 89e35ff..9009d2c 100644
--- a/cc/hybrid/failing_hybrid.h
+++ b/cc/hybrid/failing_hybrid.h
@@ -16,6 +16,7 @@
 #ifndef TINK_HYBRID_FAILING_HYBRID_H_
 #define TINK_HYBRID_FAILING_HYBRID_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/hybrid/hybrid_config.cc b/cc/hybrid/hybrid_config.cc
index 8a3bb0e..bb7dcac 100644
--- a/cc/hybrid/hybrid_config.cc
+++ b/cc/hybrid/hybrid_config.cc
@@ -28,20 +28,13 @@
 #include "tink/util/status.h"
 #include "proto/config.pb.h"
 
-using google::crypto::tink::RegistryConfig;
-
 namespace crypto {
 namespace tink {
 
 // static
-const RegistryConfig& HybridConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
-// static
 util::Status HybridConfig::Register() {
   auto status = AeadConfig::Register();
+  if (!status.ok()) return status;
 
   // Register primitive wrappers.
   status = Registry::RegisterPrimitiveWrapper(
diff --git a/cc/hybrid/hybrid_config.h b/cc/hybrid/hybrid_config.h
index fdfbc37..adea356 100644
--- a/cc/hybrid/hybrid_config.h
+++ b/cc/hybrid/hybrid_config.h
@@ -37,16 +37,6 @@
 //
 class HybridConfig {
  public:
-  static constexpr char kHybridDecryptCatalogueName[] = "TinkHybridDecrypt";
-  static constexpr char kHybridDecryptPrimitiveName[] = "HybridDecrypt";
-  static constexpr char kHybridEncryptCatalogueName[] = "TinkHybridEncrypt";
-  static constexpr char kHybridEncryptPrimitiveName[] = "HybridEncrypt";
-
-  // Returns config with implementations of HybridEncrypt and HybridDecrypt
-  // supported in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers HybridEncrypt and HybridDecrypt primitive wrappers, and key
   // managers for all implementations of HybridEncrypt and HybridDecrypt from
   // the current Tink release.
diff --git a/cc/hybrid/hybrid_config_test.cc b/cc/hybrid/hybrid_config_test.cc
index d09a281..bb5467c 100644
--- a/cc/hybrid/hybrid_config_test.cc
+++ b/cc/hybrid/hybrid_config_test.cc
@@ -23,13 +23,13 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/config.h"
-#include "tink/config/tink_fips.h"
+#include "openssl/crypto.h"
 #include "tink/hybrid/ecies_aead_hkdf_private_key_manager.h"
 #include "tink/hybrid/ecies_aead_hkdf_public_key_manager.h"
 #include "tink/hybrid/hybrid_key_templates.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/hybrid_encrypt.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/keyset_handle.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
@@ -51,7 +51,7 @@
 };
 
 TEST_F(HybridConfigTest, Basic) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -77,7 +77,7 @@
 // Tests that the HybridEncryptWrapper has been properly registered and we
 // can wrap primitives.
 TEST_F(HybridConfigTest, EncryptWrapperRegistered) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -112,7 +112,7 @@
 // Tests that the HybridDecryptWrapper has been properly registered and we
 // can wrap primitives.
 TEST_F(HybridConfigTest, DecryptWrapperRegistered) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -146,8 +146,8 @@
 
 // FIPS-only mode tests
 TEST_F(HybridConfigTest, RegisterNonFipsTemplates) {
-  if (!IsFipsModeEnabled()) {
-    GTEST_SKIP() << "Only supported in FIPS-only mode";
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto";
   }
 
   EXPECT_THAT(HybridConfig::Register(), IsOk());
diff --git a/cc/hybrid/hybrid_decrypt_factory.cc b/cc/hybrid/hybrid_decrypt_factory.cc
index 401d257..0cb9d26 100644
--- a/cc/hybrid/hybrid_decrypt_factory.cc
+++ b/cc/hybrid/hybrid_decrypt_factory.cc
@@ -16,15 +16,16 @@
 
 #include "tink/hybrid/hybrid_decrypt_factory.h"
 
+#include <memory>
+
+#include "tink/hybrid/hybrid_decrypt_wrapper.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
 #include "tink/registry.h"
-#include "tink/hybrid/hybrid_decrypt_wrapper.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
-
 namespace crypto {
 namespace tink {
 
diff --git a/cc/hybrid/hybrid_decrypt_factory.h b/cc/hybrid/hybrid_decrypt_factory.h
index 5218697..d2e3e43 100644
--- a/cc/hybrid/hybrid_decrypt_factory.h
+++ b/cc/hybrid/hybrid_decrypt_factory.h
@@ -17,6 +17,8 @@
 #ifndef TINK_HYBRID_HYBRID_DECRYPT_FACTORY_H_
 #define TINK_HYBRID_HYBRID_DECRYPT_FACTORY_H_
 
+#include <memory>
+
 #include "absl/base/macros.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/key_manager.h"
diff --git a/cc/hybrid/hybrid_decrypt_factory_test.cc b/cc/hybrid/hybrid_decrypt_factory_test.cc
index 2dce36a..c2b405a 100644
--- a/cc/hybrid/hybrid_decrypt_factory_test.cc
+++ b/cc/hybrid/hybrid_decrypt_factory_test.cc
@@ -16,12 +16,12 @@
 
 #include "tink/hybrid/hybrid_decrypt_factory.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
-#include "tink/config.h"
 #include "tink/crypto_format.h"
 #include "tink/hybrid/ecies_aead_hkdf_public_key_manager.h"
 #include "tink/hybrid/hybrid_config.h"
@@ -34,7 +34,6 @@
 #include "proto/ecies_aead_hkdf.pb.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddRawKey;
 using crypto::tink::test::AddTinkKey;
 using google::crypto::tink::EciesAeadHkdfPrivateKey;
diff --git a/cc/hybrid/hybrid_decrypt_wrapper.cc b/cc/hybrid/hybrid_decrypt_wrapper.cc
index 295f9e6..f07440b 100644
--- a/cc/hybrid/hybrid_decrypt_wrapper.cc
+++ b/cc/hybrid/hybrid_decrypt_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/hybrid_decrypt_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -51,7 +52,7 @@
       absl::string_view ciphertext,
       absl::string_view context_info) const override;
 
-  ~HybridDecryptSetWrapper() override {}
+  ~HybridDecryptSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<HybridDecrypt>> hybrid_decrypt_set_;
diff --git a/cc/hybrid/hybrid_decrypt_wrapper.h b/cc/hybrid/hybrid_decrypt_wrapper.h
index 0d2ea55..14ea1d7 100644
--- a/cc/hybrid/hybrid_decrypt_wrapper.h
+++ b/cc/hybrid/hybrid_decrypt_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_HYBRID_HYBRID_DECRYPT_WRAPPER_H_
 #define TINK_HYBRID_HYBRID_DECRYPT_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/primitive_set.h"
diff --git a/cc/hybrid/hybrid_encrypt_factory.cc b/cc/hybrid/hybrid_encrypt_factory.cc
index 9e2d92f..57e8c06 100644
--- a/cc/hybrid/hybrid_encrypt_factory.cc
+++ b/cc/hybrid/hybrid_encrypt_factory.cc
@@ -16,15 +16,16 @@
 
 #include "tink/hybrid/hybrid_encrypt_factory.h"
 
+#include <memory>
+
+#include "tink/hybrid/hybrid_encrypt_wrapper.h"
 #include "tink/hybrid_encrypt.h"
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
 #include "tink/registry.h"
-#include "tink/hybrid/hybrid_encrypt_wrapper.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
-
 namespace crypto {
 namespace tink {
 
diff --git a/cc/hybrid/hybrid_encrypt_factory.h b/cc/hybrid/hybrid_encrypt_factory.h
index 05f776c..23b6f9d 100644
--- a/cc/hybrid/hybrid_encrypt_factory.h
+++ b/cc/hybrid/hybrid_encrypt_factory.h
@@ -17,6 +17,8 @@
 #ifndef TINK_HYBRID_HYBRID_ENCRYPT_FACTORY_H_
 #define TINK_HYBRID_HYBRID_ENCRYPT_FACTORY_H_
 
+#include <memory>
+
 #include "absl/base/macros.h"
 #include "tink/hybrid_encrypt.h"
 #include "tink/key_manager.h"
diff --git a/cc/hybrid/hybrid_encrypt_factory_test.cc b/cc/hybrid/hybrid_encrypt_factory_test.cc
index 415c786..14bd2d4 100644
--- a/cc/hybrid/hybrid_encrypt_factory_test.cc
+++ b/cc/hybrid/hybrid_encrypt_factory_test.cc
@@ -20,7 +20,6 @@
 #include <utility>
 
 #include "gtest/gtest.h"
-#include "tink/config.h"
 #include "tink/crypto_format.h"
 #include "tink/hybrid/hybrid_config.h"
 #include "tink/hybrid_encrypt.h"
@@ -31,7 +30,6 @@
 #include "proto/ecies_aead_hkdf.pb.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddRawKey;
 using crypto::tink::test::AddTinkKey;
 using google::crypto::tink::EciesAeadHkdfPublicKey;
diff --git a/cc/hybrid/hybrid_encrypt_wrapper.cc b/cc/hybrid/hybrid_encrypt_wrapper.cc
index d941084..72f3b78 100644
--- a/cc/hybrid/hybrid_encrypt_wrapper.cc
+++ b/cc/hybrid/hybrid_encrypt_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/hybrid_encrypt_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -25,8 +26,8 @@
 #include "tink/internal/monitoring_util.h"
 #include "tink/internal/registry_impl.h"
 #include "tink/internal/util.h"
-#include "tink/primitive_set.h"
 #include "tink/monitoring/monitoring.h"
+#include "tink/primitive_set.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
@@ -66,7 +67,7 @@
       absl::string_view plaintext,
       absl::string_view context_info) const override;
 
-  ~HybridEncryptSetWrapper() override {}
+  ~HybridEncryptSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<HybridEncrypt>> hybrid_encrypt_set_;
diff --git a/cc/hybrid/hybrid_encrypt_wrapper.h b/cc/hybrid/hybrid_encrypt_wrapper.h
index ad78604..4d100da 100644
--- a/cc/hybrid/hybrid_encrypt_wrapper.h
+++ b/cc/hybrid/hybrid_encrypt_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_HYBRID_HYBRID_ENCRYPT_WRAPPER_H_
 #define TINK_HYBRID_HYBRID_ENCRYPT_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/hybrid_encrypt.h"
 #include "tink/primitive_set.h"
diff --git a/cc/hybrid/internal/CMakeLists.txt b/cc/hybrid/internal/CMakeLists.txt
index 6dc37fa..182b12c 100644
--- a/cc/hybrid/internal/CMakeLists.txt
+++ b/cc/hybrid/internal/CMakeLists.txt
@@ -201,6 +201,7 @@
     tink::util::status
     tink::util::statusor
     tink::proto::hpke_cc_proto
+  TESTONLY
   TAGS
     exclude_if_openssl
 )
@@ -253,6 +254,7 @@
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
+  TESTONLY
   TAGS
     exclude_if_openssl
 )
diff --git a/cc/hybrid/internal/hpke_context_boringssl.cc b/cc/hybrid/internal/hpke_context_boringssl.cc
index 47a51ed..42374a1 100644
--- a/cc/hybrid/internal/hpke_context_boringssl.cc
+++ b/cc/hybrid/internal/hpke_context_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_context_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/internal/hpke_context_boringssl_test.cc b/cc/hybrid/internal/hpke_context_boringssl_test.cc
index 54f5f20..cded6bc 100644
--- a/cc/hybrid/internal/hpke_context_boringssl_test.cc
+++ b/cc/hybrid/internal/hpke_context_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_context_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/internal/hpke_context_test.cc b/cc/hybrid/internal/hpke_context_test.cc
index 1338712..8592558 100644
--- a/cc/hybrid/internal/hpke_context_test.cc
+++ b/cc/hybrid/internal/hpke_context_test.cc
@@ -16,7 +16,9 @@
 
 #include "tink/hybrid/internal/hpke_context.h"
 
+#include <memory>
 #include <string>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/hybrid/internal/hpke_decrypt_boringssl.cc b/cc/hybrid/internal/hpke_decrypt_boringssl.cc
index ec529d9..46f0796 100644
--- a/cc/hybrid/internal/hpke_decrypt_boringssl.cc
+++ b/cc/hybrid/internal/hpke_decrypt_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_decrypt_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/internal/hpke_decrypt_boringssl_test.cc b/cc/hybrid/internal/hpke_decrypt_boringssl_test.cc
index afdf7e3..05106fa 100644
--- a/cc/hybrid/internal/hpke_decrypt_boringssl_test.cc
+++ b/cc/hybrid/internal/hpke_decrypt_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_decrypt_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/internal/hpke_encrypt_boringssl.cc b/cc/hybrid/internal/hpke_encrypt_boringssl.cc
index ec7a010..198ce16 100644
--- a/cc/hybrid/internal/hpke_encrypt_boringssl.cc
+++ b/cc/hybrid/internal/hpke_encrypt_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_encrypt_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/internal/hpke_encrypt_boringssl_test.cc b/cc/hybrid/internal/hpke_encrypt_boringssl_test.cc
index 9f6ad14..7f668a3 100644
--- a/cc/hybrid/internal/hpke_encrypt_boringssl_test.cc
+++ b/cc/hybrid/internal/hpke_encrypt_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_encrypt_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/hybrid/internal/hpke_key_boringssl.cc b/cc/hybrid/internal/hpke_key_boringssl.cc
index 0448bee..5c8b198 100644
--- a/cc/hybrid/internal/hpke_key_boringssl.cc
+++ b/cc/hybrid/internal/hpke_key_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_key_boringssl.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
diff --git a/cc/hybrid/internal/hpke_key_boringssl_test.cc b/cc/hybrid/internal/hpke_key_boringssl_test.cc
index 802058d..83840d6 100644
--- a/cc/hybrid/internal/hpke_key_boringssl_test.cc
+++ b/cc/hybrid/internal/hpke_key_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/hybrid/internal/hpke_key_boringssl.h"
 
+#include <memory>
 #include <string>
 
 #include "gtest/gtest.h"
diff --git a/cc/hybrid/internal/hpke_private_key_manager.cc b/cc/hybrid/internal/hpke_private_key_manager.cc
index da0d663..5fc4047 100644
--- a/cc/hybrid/internal/hpke_private_key_manager.cc
+++ b/cc/hybrid/internal/hpke_private_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/hybrid/internal/hpke_private_key_manager.h"
 
+#include <memory>
+
 #include "absl/status/status.h"
 #include "tink/hybrid/internal/hpke_key_manager_util.h"
 #include "tink/internal/ec_util.h"
diff --git a/cc/hybrid/internal/hpke_private_key_manager.h b/cc/hybrid/internal/hpke_private_key_manager.h
index 745f6f3..ef2e6b9 100644
--- a/cc/hybrid/internal/hpke_private_key_manager.h
+++ b/cc/hybrid/internal/hpke_private_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_HYBRID_INTERNAL_HPKE_PRIVATE_KEY_MANAGER_H_
 #define TINK_HYBRID_INTERNAL_HPKE_PRIVATE_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "tink/core/key_type_manager.h"
diff --git a/cc/hybrid/internal/hpke_private_key_manager_test.cc b/cc/hybrid/internal/hpke_private_key_manager_test.cc
index 9076409..116cf04 100644
--- a/cc/hybrid/internal/hpke_private_key_manager_test.cc
+++ b/cc/hybrid/internal/hpke_private_key_manager_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/hybrid/internal/hpke_private_key_manager.h"
 
+#include <memory>
+
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "tink/hybrid/internal/hpke_encrypt.h"
diff --git a/cc/hybrid/internal/hpke_public_key_manager.h b/cc/hybrid/internal/hpke_public_key_manager.h
index 1d43df8..ebb7cd5 100644
--- a/cc/hybrid/internal/hpke_public_key_manager.h
+++ b/cc/hybrid/internal/hpke_public_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_HYBRID_INTERNAL_HPKE_PUBLIC_KEY_MANAGER_H_
 #define TINK_HYBRID_INTERNAL_HPKE_PUBLIC_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/hybrid/internal/hpke_test_util.h b/cc/hybrid/internal/hpke_test_util.h
index 5a7ea1a..10532c2 100644
--- a/cc/hybrid/internal/hpke_test_util.h
+++ b/cc/hybrid/internal/hpke_test_util.h
@@ -18,6 +18,7 @@
 #define TINK_HYBRID_INTERNAL_HPKE_TEST_UTIL_H_
 
 #include <string>
+#include <vector>
 
 #include "absl/strings/escaping.h"
 #include "tink/hybrid/internal/hpke_util.h"
diff --git a/cc/hybrid_decrypt.h b/cc/hybrid_decrypt.h
index f685d64..6d553c3 100644
--- a/cc/hybrid_decrypt.h
+++ b/cc/hybrid_decrypt.h
@@ -61,7 +61,7 @@
   virtual crypto::tink::util::StatusOr<std::string> Decrypt(
       absl::string_view ciphertext, absl::string_view context_info) const = 0;
 
-  virtual ~HybridDecrypt() {}
+  virtual ~HybridDecrypt() = default;
 };
 
 }  // namespace tink
diff --git a/cc/hybrid_encrypt.h b/cc/hybrid_encrypt.h
index 67b18dd..20f54dd 100644
--- a/cc/hybrid_encrypt.h
+++ b/cc/hybrid_encrypt.h
@@ -61,7 +61,7 @@
   virtual crypto::tink::util::StatusOr<std::string> Encrypt(
       absl::string_view plaintext, absl::string_view context_info) const = 0;
 
-  virtual ~HybridEncrypt() {}
+  virtual ~HybridEncrypt() = default;
 };
 
 }  // namespace tink
diff --git a/cc/input_stream.h b/cc/input_stream.h
index f1ac7f1..cd832b0 100644
--- a/cc/input_stream.h
+++ b/cc/input_stream.h
@@ -27,8 +27,8 @@
 // Protocol Buffers' google::protobuf::io::ZeroCopyInputStream.
 class InputStream {
  public:
-  InputStream() {}
-  virtual ~InputStream() {}
+  InputStream() = default;
+  virtual ~InputStream() = default;
 
   // Obtains a chunk of data from the stream.
   //
diff --git a/cc/insecure_secret_key_access.h b/cc/insecure_secret_key_access.h
index 90067f2..915b4f3 100644
--- a/cc/insecure_secret_key_access.h
+++ b/cc/insecure_secret_key_access.h
@@ -14,8 +14,8 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_INSECURE_KEY_ACCESS_H_
-#define TINK_INSECURE_KEY_ACCESS_H_
+#ifndef TINK_INSECURE_SECRET_KEY_ACCESS_H_
+#define TINK_INSECURE_SECRET_KEY_ACCESS_H_
 
 #include "tink/secret_key_access_token.h"
 
@@ -39,4 +39,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_INSECURE_KEY_ACCESS_H_
+#endif  // TINK_INSECURE_SECRET_KEY_ACCESS_H_
diff --git a/cc/integration/awskms/.bazelrc b/cc/integration/awskms/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/cc/integration/awskms/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/cc/integration/awskms/.bazelversion b/cc/integration/awskms/.bazelversion
new file mode 100644
index 0000000..09b254e
--- /dev/null
+++ b/cc/integration/awskms/.bazelversion
@@ -0,0 +1 @@
+6.0.0
diff --git a/cc/integration/awskms/BUILD.bazel b/cc/integration/awskms/BUILD.bazel
index 0367300..1c81f7e 100644
--- a/cc/integration/awskms/BUILD.bazel
+++ b/cc/integration/awskms/BUILD.bazel
@@ -3,24 +3,6 @@
 licenses(["notice"])
 
 cc_library(
-    name = "aws_crypto",
-    srcs = [
-        "aws_crypto.cc",
-    ],
-    hdrs = [
-        "aws_crypto.h",
-    ],
-    include_prefix = "tink/integration/awskms",
-    visibility = ["//visibility:public"],
-    deps = [
-        "@aws_cpp_sdk//:aws_sdk_core",
-        "@boringssl//:crypto",
-        "@com_google_absl//absl/base",
-    ],
-    alwayslink = 1,
-)
-
-cc_library(
     name = "aws_kms_aead",
     srcs = ["aws_kms_aead.cc"],
     hdrs = ["aws_kms_aead.h"],
@@ -44,9 +26,9 @@
     include_prefix = "tink/integration/awskms",
     visibility = ["//visibility:public"],
     deps = [
-        ":aws_crypto",
         ":aws_kms_aead",
         "@aws_cpp_sdk//:aws_sdk_core",
+        "@com_google_absl//absl/base",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_absl//absl/synchronization",
@@ -58,34 +40,34 @@
     alwayslink = 1,
 )
 
-# tests
-
 cc_test(
-    name = "aws_kms_aead_test",
-    size = "medium",
-    srcs = ["aws_kms_aead_test.cc"],
-    copts = ["-Iexternal/gtest/include"],
+    name = "aws_kms_aead_integration_test",
+    size = "small",
+    srcs = ["aws_kms_aead_integration_test.cc"],
+    data = ["//testdata/aws:credentials"],
+    # This target requires valid credentials to interact with the AWS KMS.
+    tags = ["manual"],
     deps = [
         ":aws_kms_aead",
-        "@aws_cpp_sdk//:aws_sdk_core",
+        ":aws_kms_client",
+        "//tink/integration/awskms/internal:test_file_util",
+        "@bazel_tools//tools/cpp/runfiles",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
-        "@tink_cc//util:status",
         "@tink_cc//util:statusor",
+        "@tink_cc//util:test_matchers",
     ],
 )
 
 cc_test(
     name = "aws_kms_client_test",
-    size = "medium",
+    size = "small",
     srcs = ["aws_kms_client_test.cc"],
-    copts = ["-Iexternal/gtest/include"],
-    data = [
-        "//testdata/aws:credentials",
-        "//testdata/gcp:credentials",
-    ],
+    data = ["//testdata/aws:credentials"],
     deps = [
         ":aws_kms_client",
+        "//tink/integration/awskms/internal:test_file_util",
         "@aws_cpp_sdk//:aws_sdk_core",
         "@com_google_absl//absl/status",
         "@com_google_googletest//:gtest_main",
diff --git a/cc/integration/awskms/aws_crypto.cc b/cc/integration/awskms/aws_crypto.cc
deleted file mode 100644
index c73eac4..0000000
--- a/cc/integration/awskms/aws_crypto.cc
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/integration/awskms/aws_crypto.h"
-
-#include "aws/core/Aws.h"
-#include "aws/core/utils/Outcome.h"
-#include "aws/core/utils/crypto/Factories.h"
-#include "aws/core/utils/crypto/HashResult.h"
-#include "aws/core/utils/crypto/HMAC.h"
-#include "aws/core/utils/crypto/Hash.h"
-
-#include "absl/base/attributes.h"
-#include "openssl/hmac.h"
-#include "openssl/sha.h"
-
-namespace crypto {
-namespace tink {
-namespace integration {
-namespace awskms {
-
-ABSL_CONST_INIT const char* kAwsCryptoAllocationTag = "AwsCryptoAllocation";
-
-class AwsSha256HmacOpenSslImpl : public Aws::Utils::Crypto::HMAC {
- public:
-  AwsSha256HmacOpenSslImpl() {}
-
-  virtual ~AwsSha256HmacOpenSslImpl() = default;
-
-  Aws::Utils::Crypto::HashResult Calculate(
-      const Aws::Utils::ByteBuffer& toSign,
-      const Aws::Utils::ByteBuffer& secret) override {
-    unsigned int length = SHA256_DIGEST_LENGTH;
-    Aws::Utils::ByteBuffer digest(length);
-    memset(digest.GetUnderlyingData(), 0, length);
-
-    HMAC_CTX ctx;
-    HMAC_CTX_init(&ctx);
-
-    HMAC_Init_ex(&ctx, secret.GetUnderlyingData(),
-                 static_cast<int>(secret.GetLength()), EVP_sha256(), NULL);
-    HMAC_Update(&ctx, toSign.GetUnderlyingData(), toSign.GetLength());
-    HMAC_Final(&ctx, digest.GetUnderlyingData(), &length);
-    HMAC_CTX_cleanup(&ctx);
-
-    return Aws::Utils::Crypto::HashResult(std::move(digest));
-  }
-};
-
-class AwsSha256OpenSslImpl : public Aws::Utils::Crypto::Hash {
- public:
-  AwsSha256OpenSslImpl() {}
-
-  virtual ~AwsSha256OpenSslImpl() = default;
-
-  Aws::Utils::Crypto::HashResult Calculate(const Aws::String& str) override {
-    SHA256_CTX sha256;
-    SHA256_Init(&sha256);
-    SHA256_Update(&sha256, str.data(), str.size());
-
-    Aws::Utils::ByteBuffer hash(SHA256_DIGEST_LENGTH);
-    SHA256_Final(hash.GetUnderlyingData(), &sha256);
-
-    return Aws::Utils::Crypto::HashResult(std::move(hash));
-  }
-
-  Aws::Utils::Crypto::HashResult Calculate(Aws::IStream& stream) override {
-    SHA256_CTX sha256;
-    SHA256_Init(&sha256);
-
-    auto currentPos = stream.tellg();
-    if (currentPos == std::streampos(std::streamoff(-1))) {
-      currentPos = 0;
-      stream.clear();
-    }
-
-    stream.seekg(0, stream.beg);
-
-    char streamBuffer
-        [Aws::Utils::Crypto::Hash::INTERNAL_HASH_STREAM_BUFFER_SIZE];
-    while (stream.good()) {
-      stream.read(streamBuffer,
-                  Aws::Utils::Crypto::Hash::INTERNAL_HASH_STREAM_BUFFER_SIZE);
-      auto bytesRead = stream.gcount();
-
-      if (bytesRead > 0) {
-        SHA256_Update(&sha256, streamBuffer, static_cast<size_t>(bytesRead));
-      }
-    }
-
-    stream.clear();
-    stream.seekg(currentPos, stream.beg);
-
-    Aws::Utils::ByteBuffer hash(SHA256_DIGEST_LENGTH);
-    SHA256_Final(hash.GetUnderlyingData(), &sha256);
-
-    return Aws::Utils::Crypto::HashResult(std::move(hash));
-  }
-};
-
-std::shared_ptr<Aws::Utils::Crypto::Hash>
-AwsSha256Factory::CreateImplementation() const {
-  return Aws::MakeShared<AwsSha256OpenSslImpl>(kAwsCryptoAllocationTag);
-}
-
-std::shared_ptr<Aws::Utils::Crypto::HMAC>
-AwsSha256HmacFactory::CreateImplementation() const {
-  return Aws::MakeShared<AwsSha256HmacOpenSslImpl>(kAwsCryptoAllocationTag);
-}
-
-
-}  // namespace awskms
-}  // namespace integration
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/integration/awskms/aws_crypto.h b/cc/integration/awskms/aws_crypto.h
deleted file mode 100644
index 01f8c92..0000000
--- a/cc/integration/awskms/aws_crypto.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_INTEGRATION_AWSKMS_AWS_CRYPTO_H_
-#define TINK_INTEGRATION_AWSKMS_AWS_CRYPTO_H_
-
-#include "aws/core/Aws.h"
-#include "aws/core/utils/crypto/Factories.h"
-#include "aws/core/utils/crypto/HMAC.h"
-#include "aws/core/utils/crypto/Hash.h"
-
-namespace crypto {
-namespace tink {
-namespace integration {
-namespace awskms {
-
-// Helpers for building AWS C++ Client without OpenSSL, to avoid
-// collisions with BoringSSL used by Tink.
-// These were "borrowed" from TensorFlow (https://github.com/tensorflow/).
-//
-// With these helpers the initialization of AWS API looks as follows:
-//
-//   Aws::SDKOptions options;
-//   options.cryptoOptions.sha256Factory_create_fn = []() {
-//       return Aws::MakeShared<AwsSha256Factory>(kAwsCryptoAllocationTag);
-//   };
-//   options.cryptoOptions.sha256HMACFactory_create_fn = []() {
-//       return Aws::MakeShared<AwsSha256HmacFactory>(kAwsCryptoAllocationTag);
-//   };
-//   Aws::InitAPI(options);
-//
-///////////////////////////////////////////////////////////////////////////////
-
-extern const char* kAwsCryptoAllocationTag;
-
-class AwsSha256Factory : public Aws::Utils::Crypto::HashFactory {
- public:
-  std::shared_ptr<Aws::Utils::Crypto::Hash> CreateImplementation()
-      const override;
-};
-
-class AwsSha256HmacFactory : public Aws::Utils::Crypto::HMACFactory {
- public:
-  std::shared_ptr<Aws::Utils::Crypto::HMAC> CreateImplementation()
-      const override;
-};
-
-}  // namespace awskms
-}  // namespace integration
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_INTEGRATION_AWSKMS_AWS_CRYPTO_H_
diff --git a/cc/integration/awskms/aws_kms_aead.cc b/cc/integration/awskms/aws_kms_aead.cc
index e231b9a..a34b2e2 100644
--- a/cc/integration/awskms/aws_kms_aead.cc
+++ b/cc/integration/awskms/aws_kms_aead.cc
@@ -16,11 +16,6 @@
 
 #include "tink/integration/awskms/aws_kms_aead.h"
 
-#include "absl/status/status.h"
-#include "absl/strings/escaping.h"
-#include "absl/strings/match.h"
-#include "absl/strings/str_cat.h"
-#include "absl/strings/string_view.h"
 #include "aws/core/auth/AWSCredentialsProvider.h"
 #include "aws/core/client/AWSClient.h"
 #include "aws/core/utils/Outcome.h"
@@ -31,6 +26,11 @@
 #include "aws/kms/model/DecryptResult.h"
 #include "aws/kms/model/EncryptRequest.h"
 #include "aws/kms/model/EncryptResult.h"
+#include "absl/status/status.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/match.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
 #include "tink/aead.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -39,24 +39,8 @@
 namespace tink {
 namespace integration {
 namespace awskms {
-
-using crypto::tink::util::Status;
-using crypto::tink::util::StatusOr;
-
 namespace {
 
-// TODO: pull out hex-helpers from test_util and remove the copy below.
-std::string HexEncode(absl::string_view bytes) {
-  std::string hexchars = "0123456789abcdef";
-  std::string res(bytes.size() * 2, static_cast<char>(255));
-  for (size_t i = 0; i < bytes.size(); ++i) {
-    uint8_t c = static_cast<uint8_t>(bytes[i]);
-    res[2 * i] = hexchars[c / 16];
-    res[2 * i + 1] = hexchars[c % 16];
-  }
-  return res;
-}
-
 std::string AwsErrorToString(Aws::Client::AWSError<Aws::KMS::KMSErrors> err) {
   return absl::StrCat("AWS error code: ", err.GetErrorType(), ", ",
                       err.GetExceptionName(), ": ", err.GetMessage());
@@ -64,28 +48,21 @@
 
 }  // namespace
 
-AwsKmsAead::AwsKmsAead(absl::string_view key_arn,
-                       std::shared_ptr<Aws::KMS::KMSClient> aws_client) :
-    key_arn_(key_arn), aws_client_(aws_client) {
-}
-
-// static
-StatusOr<std::unique_ptr<Aead>>
-AwsKmsAead::New(absl::string_view key_arn,
-                std::shared_ptr<Aws::KMS::KMSClient> aws_client) {
+util::StatusOr<std::unique_ptr<Aead>> AwsKmsAead::New(
+    absl::string_view key_arn,
+    std::shared_ptr<Aws::KMS::KMSClient> aws_client) {
   if (key_arn.empty()) {
-    return Status(absl::StatusCode::kInvalidArgument,
-                  "Key ARN cannot be empty.");
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Key ARN cannot be empty.");
   }
   if (aws_client == nullptr) {
-    return Status(absl::StatusCode::kInvalidArgument,
-                  "AWS KMS client cannot be null.");
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "AWS KMS client cannot be null.");
   }
-  std::unique_ptr<Aead> aead(new AwsKmsAead(key_arn, aws_client));
-  return std::move(aead);
+  return {absl::WrapUnique(new AwsKmsAead(key_arn, aws_client))};
 }
 
-StatusOr<std::string> AwsKmsAead::Encrypt(
+util::StatusOr<std::string> AwsKmsAead::Encrypt(
     absl::string_view plaintext, absl::string_view associated_data) const {
   Aws::KMS::Model::EncryptRequest req;
   req.SetKeyId(key_arn_.c_str());
@@ -95,23 +72,23 @@
   req.SetPlaintext(plaintext_buffer);
   if (!associated_data.empty()) {
     req.AddEncryptionContext("associatedData",
-                             HexEncode(associated_data).c_str());
+                             absl::BytesToHexString(associated_data).c_str());
   }
   auto outcome = aws_client_->Encrypt(req);
-  if (outcome.IsSuccess()) {
-    auto& blob = outcome.GetResult().GetCiphertextBlob();
-    std::string ciphertext(
-        reinterpret_cast<const char*>(blob.GetUnderlyingData()),
-        blob.GetLength());
-    return ciphertext;
+  if (!outcome.IsSuccess()) {
+    auto& err = outcome.GetError();
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("AWS KMS encryption failed with error: ",
+                                     AwsErrorToString(err)));
   }
-  auto& err = outcome.GetError();
-  return util::Status(absl::StatusCode::kInvalidArgument,
-                      absl::StrCat("AWS KMS encryption failed with error: ",
-                                   AwsErrorToString(err)));
+  auto& blob = outcome.GetResult().GetCiphertextBlob();
+  std::string ciphertext(
+      reinterpret_cast<const char*>(blob.GetUnderlyingData()),
+      blob.GetLength());
+  return ciphertext;
 }
 
-StatusOr<std::string> AwsKmsAead::Decrypt(
+util::StatusOr<std::string> AwsKmsAead::Decrypt(
     absl::string_view ciphertext, absl::string_view associated_data) const {
   Aws::KMS::Model::DecryptRequest req;
   req.SetKeyId(key_arn_.c_str());
@@ -121,24 +98,20 @@
   req.SetCiphertextBlob(ciphertext_buffer);
   if (!associated_data.empty()) {
     req.AddEncryptionContext("associatedData",
-                             HexEncode(associated_data).c_str());
+                             absl::BytesToHexString(associated_data).c_str());
   }
   auto outcome = aws_client_->Decrypt(req);
-  if (outcome.IsSuccess()) {
-    if (outcome.GetResult().GetKeyId() != Aws::String(key_arn_.c_str())) {
-      return util::Status(absl::StatusCode::kInvalidArgument,
-                          "AWS KMS decryption failed: wrong key ARN.");
-    }
-    auto& buffer = outcome.GetResult().GetPlaintext();
-    std::string plaintext(
-        reinterpret_cast<const char*>(buffer.GetUnderlyingData()),
-        buffer.GetLength());
-    return plaintext;
+  if (!outcome.IsSuccess()) {
+    auto& err = outcome.GetError();
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("AWS KMS decryption failed with error: ",
+                                     AwsErrorToString(err)));
   }
-  auto& err = outcome.GetError();
-  return util::Status(absl::StatusCode::kInvalidArgument,
-                      absl::StrCat("AWS KMS decryption failed with error: ",
-                                   AwsErrorToString(err)));
+  auto& buffer = outcome.GetResult().GetPlaintext();
+  std::string plaintext(
+      reinterpret_cast<const char*>(buffer.GetUnderlyingData()),
+      buffer.GetLength());
+  return plaintext;
 }
 
 }  // namespace awskms
diff --git a/cc/integration/awskms/aws_kms_aead.h b/cc/integration/awskms/aws_kms_aead.h
index be400ab..31c9157 100644
--- a/cc/integration/awskms/aws_kms_aead.h
+++ b/cc/integration/awskms/aws_kms_aead.h
@@ -17,8 +17,8 @@
 #ifndef TINK_INTEGRATION_AWSKMS_AWS_KMS_AEAD_H_
 #define TINK_INTEGRATION_AWSKMS_AWS_KMS_AEAD_H_
 
-#include "absl/strings/string_view.h"
 #include "aws/kms/KMSClient.h"
+#include "absl/strings/string_view.h"
 #include "tink/aead.h"
 #include "tink/util/statusor.h"
 
@@ -27,15 +27,20 @@
 namespace integration {
 namespace awskms {
 
-// AwsKmsAead is an implementation of AEAD that forwards
-// encryption/decryption requests to a key managed by
-// <a href="https://aws.amazon.com/kms/">AWS KMS</a>.
+// AwsKmsAead is an implementation of AEAD that forwards encryption/decryption
+// requests to a key managed by the AWS KMS (https://aws.amazon.com/kms).
 class AwsKmsAead : public Aead {
  public:
-  // Creates a new AwsKmsAead that is bound to the key specified in 'key_arn',
+  // Move only.
+  AwsKmsAead(AwsKmsAead&& other) = default;
+  AwsKmsAead& operator=(AwsKmsAead&& other) = default;
+  AwsKmsAead(const AwsKmsAead&) = delete;
+  AwsKmsAead& operator=(const AwsKmsAead&) = delete;
+
+  // Creates a new AwsKmsAead that is bound to the key specified in `key_arn`,
   // and that uses the given client when communicating with the KMS.
-  static crypto::tink::util::StatusOr<std::unique_ptr<Aead>>
-  New(absl::string_view key_arn,
+  static crypto::tink::util::StatusOr<std::unique_ptr<Aead>> New(
+      absl::string_view key_arn,
       std::shared_ptr<Aws::KMS::KMSClient> aws_client);
 
   crypto::tink::util::StatusOr<std::string> Encrypt(
@@ -46,16 +51,15 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  virtual ~AwsKmsAead() {}
-
  private:
   AwsKmsAead(absl::string_view key_arn,
-             std::shared_ptr<Aws::KMS::KMSClient> aws_client);
+             std::shared_ptr<Aws::KMS::KMSClient> aws_client)
+      : key_arn_(key_arn), aws_client_(aws_client) {}
+
   std::string key_arn_;  // The location of a crypto key in AWS KMS.
   std::shared_ptr<Aws::KMS::KMSClient> aws_client_;
 };
 
-
 }  // namespace awskms
 }  // namespace integration
 }  // namespace tink
diff --git a/cc/integration/awskms/aws_kms_aead_integration_test.cc b/cc/integration/awskms/aws_kms_aead_integration_test.cc
new file mode 100644
index 0000000..a549cff
--- /dev/null
+++ b/cc/integration/awskms/aws_kms_aead_integration_test.cc
@@ -0,0 +1,91 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "tink/integration/awskms/aws_kms_aead.h"
+#include "tink/integration/awskms/aws_kms_client.h"
+#include "tink/integration/awskms/internal/test_file_util.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace integration {
+namespace awskms {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+
+constexpr absl::string_view kAwsKmsKeyUri =
+    "aws-kms://arn:aws:kms:us-east-2:235739564943:key/"
+    "3ee50705-5a82-4f5b-9753-05c4f473922f";
+
+constexpr absl::string_view kAwsKmsKeyAliasUri =
+    "aws-kms://arn:aws:kms:us-east-2:235739564943:alias/"
+    "unit-and-integration-testing";
+
+
+TEST(AwsKmsAeadTest, EncryptDecrypt) {
+  std::string credentials =
+      internal::RunfilesPath("testdata/aws/credentials.ini");
+  util::StatusOr<std::unique_ptr<AwsKmsClient>> client =
+      AwsKmsClient::New(/*key_uri=*/"", credentials);
+  ASSERT_THAT(client, IsOk());
+
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      (*client)->GetAead(kAwsKmsKeyUri);
+  ASSERT_THAT(aead, IsOk());
+
+  constexpr absl::string_view kPlaintext = "plaintext";
+  constexpr absl::string_view kAssociatedData = "aad";
+
+  util::StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(kPlaintext, kAssociatedData);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT((*aead)->Decrypt(*ciphertext, kAssociatedData),
+              IsOkAndHolds(kPlaintext));
+}
+
+TEST(AwsKmsAeadTest, EncryptDecryptWithKeyAlias) {
+  std::string credentials =
+      internal::RunfilesPath("testdata/aws/credentials.ini");
+  util::StatusOr<std::unique_ptr<AwsKmsClient>> client =
+      AwsKmsClient::New(/*key_uri=*/"", credentials);
+  ASSERT_THAT(client, IsOk());
+
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      (*client)->GetAead(kAwsKmsKeyAliasUri);
+  ASSERT_THAT(aead, IsOk());
+
+  constexpr absl::string_view kPlaintext = "plaintext";
+  constexpr absl::string_view kAssociatedData = "aad";
+
+  util::StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(kPlaintext, kAssociatedData);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT((*aead)->Decrypt(*ciphertext, kAssociatedData),
+              IsOkAndHolds(kPlaintext));
+}
+
+}  // namespace
+}  // namespace awskms
+}  // namespace integration
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/integration/awskms/aws_kms_aead_test.cc b/cc/integration/awskms/aws_kms_aead_test.cc
deleted file mode 100644
index 99d42e3..0000000
--- a/cc/integration/awskms/aws_kms_aead_test.cc
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-#include "tink/integration/awskms/aws_kms_aead.h"
-
-#include <string>
-#include <vector>
-
-#include "absl/strings/str_cat.h"
-#include "aws/core/Aws.h"
-#include "aws/kms/KMSClient.h"
-#include "tink/util/status.h"
-#include "tink/util/statusor.h"
-#include "gtest/gtest.h"
-
-namespace crypto {
-namespace tink {
-namespace subtle {
-namespace {
-
-using crypto::tink::integration::awskms::AwsKmsAead;
-
-class AwsKmsAeadTest : public ::testing::Test {
-  // TODO(przydatek): add a test with a mock KMSClient.
-};
-
-
-}  // namespace
-}  // namespace subtle
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/integration/awskms/aws_kms_client.cc b/cc/integration/awskms/aws_kms_client.cc
index 9b57dc5..158bced 100644
--- a/cc/integration/awskms/aws_kms_client.cc
+++ b/cc/integration/awskms/aws_kms_client.cc
@@ -19,13 +19,6 @@
 #include <iostream>
 #include <sstream>
 
-#include "absl/status/status.h"
-#include "absl/strings/match.h"
-#include "absl/strings/ascii.h"
-#include "absl/strings/escaping.h"
-#include "absl/strings/str_split.h"
-#include "absl/strings/string_view.h"
-#include "absl/synchronization/mutex.h"
 #include "aws/core/Aws.h"
 #include "aws/core/auth/AWSCredentialsProvider.h"
 #include "aws/core/auth/AWSCredentialsProviderChain.h"
@@ -33,7 +26,14 @@
 #include "aws/core/utils/crypto/Factories.h"
 #include "aws/core/utils/memory/AWSMemory.h"
 #include "aws/kms/KMSClient.h"
-#include "tink/integration/awskms/aws_crypto.h"
+#include "absl/base/call_once.h"
+#include "absl/status/status.h"
+#include "absl/strings/ascii.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/match.h"
+#include "absl/strings/str_split.h"
+#include "absl/strings/string_view.h"
+#include "absl/synchronization/mutex.h"
 #include "tink/integration/awskms/aws_kms_aead.h"
 #include "tink/kms_client.h"
 #include "tink/util/status.h"
@@ -46,9 +46,10 @@
 namespace {
 
 constexpr absl::string_view kKeyUriPrefix = "aws-kms://";
+constexpr char kTinkAwsKmsAllocationTag[] = "tink::integration::awskms";
 
 // Returns AWS key ARN contained in `key_uri`. If `key_uri` does not refer to an
-// AWS key, returns an empty string.
+// AWS key, returns an error.
 util::StatusOr<std::string> GetKeyArn(absl::string_view key_uri) {
   if (!absl::StartsWithIgnoreCase(key_uri, kKeyUriPrefix)) {
     return util::Status(absl::StatusCode::kInvalidArgument,
@@ -61,8 +62,8 @@
 // `key_arn`.
 // An AWS key ARN is of the form
 // arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab.
-util::StatusOr<Aws::Client::ClientConfiguration>
-    GetAwsClientConfig(absl::string_view key_arn) {
+util::StatusOr<Aws::Client::ClientConfiguration> GetAwsClientConfig(
+    absl::string_view key_arn) {
   std::vector<std::string> key_arn_parts = absl::StrSplit(key_arn, ':');
   if (key_arn_parts.size() < 6) {
     return util::Status(absl::StatusCode::kInvalidArgument,
@@ -97,9 +98,9 @@
                                      absl::string_view line) {
   std::vector<std::string> parts = absl::StrSplit(line, '=');
   if (parts.size() != 2 || absl::StripAsciiWhitespace(parts[0]) != name) {
-    return util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("Expected line in format ", name, " = value"));
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("Expected line to have the format: ", name,
+                                     " = value. Found: ", line));
   }
   return std::string(absl::StripAsciiWhitespace(parts[1]));
 }
@@ -131,68 +132,56 @@
 // Aws::Auth::ProfileConfigFileAWSCredentialsProvider.
 util::StatusOr<Aws::Auth::AWSCredentials> GetAwsCredentials(
     absl::string_view credentials_path) {
-  if (!credentials_path.empty()) {  // Read credentials from given file.
-    auto creds_result = ReadFile(std::string(credentials_path));
-    if (!creds_result.ok()) {
-      return creds_result.status();
-    }
-    std::vector<std::string> creds_lines =
-        absl::StrSplit(creds_result.value(), '\n');
-    if (creds_lines.size() < 3) {
-      return util::Status(absl::StatusCode::kInvalidArgument,
-                          absl::StrCat("Invalid format of credentials in file ",
-                                       credentials_path));
-    }
-    auto key_id_result = GetValue("aws_access_key_id", creds_lines[1]);
-    if (!key_id_result.ok()) {
-      return util::Status(absl::StatusCode::kInvalidArgument,
-                          absl::StrCat("Invalid format of credentials in file ",
-                                       credentials_path, " : ",
-                                       key_id_result.status().message()));
-    }
-    auto secret_key_result = GetValue("aws_secret_access_key", creds_lines[2]);
-    if (!secret_key_result.ok()) {
-      return util::Status(
-          absl::StatusCode::kInvalidArgument,
-          absl::StrCat("Invalid format of credentials in file ",
-                       credentials_path, " : ",
-                       secret_key_result.status().message()));
-    }
-    return Aws::Auth::AWSCredentials(key_id_result.value().c_str(),
-                                     secret_key_result.value().c_str());
+  if (credentials_path.empty()) {
+    // Get default credentials.
+    Aws::Auth::DefaultAWSCredentialsProviderChain provider_chain;
+    return provider_chain.GetAWSCredentials();
   }
+  // Read credentials from the given file.
+  util::StatusOr<std::string> creds_result =
+      ReadFile(std::string(credentials_path));
+  if (!creds_result.ok()) {
+    return creds_result.status();
+  }
+  std::vector<std::string> creds_lines =
+      absl::StrSplit(creds_result.value(), '\n');
+  if (creds_lines.size() < 3) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("Invalid format of credentials in file ",
+                                     credentials_path));
+  }
+  util::StatusOr<std::string> key_id_result =
+      GetValue("aws_access_key_id", creds_lines[1]);
+  if (!key_id_result.ok()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Invalid format of credentials in file ", credentials_path,
+                     " : ", key_id_result.status().message()));
+  }
+  util::StatusOr<std::string> secret_key_result =
+      GetValue("aws_secret_access_key", creds_lines[2]);
+  if (!secret_key_result.ok()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Invalid format of credentials in file ", credentials_path,
+                     " : ", secret_key_result.status().message()));
+  }
+  return Aws::Auth::AWSCredentials(key_id_result.value().c_str(),
+                                   secret_key_result.value().c_str());
+}
 
-  // Get default credentials.
-  Aws::Auth::DefaultAWSCredentialsProviderChain provider_chain;
-  return provider_chain.GetAWSCredentials();
+void InitAwsApi() {
+  Aws::SDKOptions options;
+  Aws::InitAPI(options);
 }
 
 }  // namespace
 
-bool AwsKmsClient::aws_api_is_initialized_;
-absl::Mutex AwsKmsClient::aws_api_init_mutex_;
-
-void AwsKmsClient::InitAwsApi() {
-  absl::MutexLock lock(&aws_api_init_mutex_);
-  if (aws_api_is_initialized_) {
-    return;
-  }
-  Aws::SDKOptions options;
-  options.cryptoOptions.sha256Factory_create_fn = []() {
-    return Aws::MakeShared<AwsSha256Factory>(kAwsCryptoAllocationTag);
-  };
-  options.cryptoOptions.sha256HMACFactory_create_fn = []() {
-    return Aws::MakeShared<AwsSha256HmacFactory>(kAwsCryptoAllocationTag);
-  };
-  Aws::InitAPI(options);
-  aws_api_is_initialized_ = true;
-}
+static absl::once_flag aws_initialization_once;
 
 util::StatusOr<std::unique_ptr<AwsKmsClient>> AwsKmsClient::New(
     absl::string_view key_uri, absl::string_view credentials_path) {
-  if (!aws_api_is_initialized_) {
-    InitAwsApi();
-  }
+  absl::call_once(aws_initialization_once, []() { InitAwsApi(); });
   // Read credentials.
   util::StatusOr<Aws::Auth::AWSCredentials> credentials =
       GetAwsCredentials(credentials_path);
@@ -217,7 +206,7 @@
   auto client = absl::WrapUnique(new AwsKmsClient(*key_arn, *credentials));
   // Create AWS KMSClient.
   client->aws_client_ = Aws::MakeShared<Aws::KMS::KMSClient>(
-      kAwsCryptoAllocationTag, client->credentials_, *client_config);
+      kTinkAwsKmsAllocationTag, client->credentials_, *client_config);
   return std::move(client);
 }
 
@@ -230,33 +219,29 @@
   return key_arn_.empty() ? true : key_arn_ == *key_arn;
 }
 
-util::StatusOr<std::unique_ptr<Aead>>
-AwsKmsClient::GetAead(absl::string_view key_uri) const {
-  if (!DoesSupport(key_uri)) {
-    if (!key_arn_.empty()) {
-      return util::Status(absl::StatusCode::kInvalidArgument,
-          absl::StrCat("This client is bound to ", key_arn_,
-                       " and cannot use key ", key_uri));
-    }
-    return util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("This client does not support key ", key_uri));
+util::StatusOr<std::unique_ptr<Aead>> AwsKmsClient::GetAead(
+    absl::string_view key_uri) const {
+  util::StatusOr<std::string> key_arn = GetKeyArn(key_uri);
+  if (!key_arn.ok()) {
+    return key_arn.status();
   }
-
   // This client is bound to a specific key.
   if (!key_arn_.empty()) {
+    if (key_arn_ != *key_arn) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          absl::StrCat("This client is bound to ", key_arn_,
+                                       " and cannot use key ", key_uri));
+    }
     return AwsKmsAead::New(key_arn_, aws_client_);
   }
 
-  // Create an Aws::KMS::KMSClient for the given key.
-  util::StatusOr<std::string> key_arn = GetKeyArn(key_uri);
   util::StatusOr<Aws::Client::ClientConfiguration> client_config =
       GetAwsClientConfig(*key_arn);
   if (!client_config.ok()) {
     return client_config.status();
   }
   auto aws_client = Aws::MakeShared<Aws::KMS::KMSClient>(
-      kAwsCryptoAllocationTag, credentials_, *client_config);
+      kTinkAwsKmsAllocationTag, credentials_, *client_config);
   return AwsKmsAead::New(*key_arn, aws_client);
 }
 
diff --git a/cc/integration/awskms/aws_kms_client.h b/cc/integration/awskms/aws_kms_client.h
index b97e627..2dac113 100644
--- a/cc/integration/awskms/aws_kms_client.h
+++ b/cc/integration/awskms/aws_kms_client.h
@@ -19,10 +19,10 @@
 
 #include <memory>
 
-#include "absl/strings/string_view.h"
-#include "absl/synchronization/mutex.h"
 #include "aws/core/auth/AWSCredentialsProvider.h"
 #include "aws/kms/KMSClient.h"
+#include "absl/strings/string_view.h"
+#include "absl/synchronization/mutex.h"
 #include "tink/aead.h"
 #include "tink/kms_client.h"
 #include "tink/kms_clients.h"
@@ -34,17 +34,23 @@
 namespace integration {
 namespace awskms {
 
-// AwsKmsClient is an implementation of KmsClient for
-// <a href="https://aws.amazon.com/kms/">AWS KMS</a>
+// AwsKmsClient is an implementation of KmsClient for AWS KMS
+// (https://aws.amazon.com/kms/).
 class AwsKmsClient : public crypto::tink::KmsClient {
  public:
+  // Move only.
+  AwsKmsClient(AwsKmsClient&& other) = default;
+  AwsKmsClient& operator=(AwsKmsClient&& other) = default;
+  AwsKmsClient(const AwsKmsClient&) = delete;
+  AwsKmsClient& operator=(const AwsKmsClient&) = delete;
+
   // Creates a new AwsKmsClient that is bound to the key specified in `key_uri`,
   // if not empty, and that uses the credentials in `credentials_path`, if not
   // empty, or the default ones to authenticate to the KMS.
   //
   // If `key_uri` is empty, then the client is not bound to any particular key.
-  static crypto::tink::util::StatusOr<std::unique_ptr<AwsKmsClient>>
-  New(absl::string_view key_uri, absl::string_view credentials_path);
+  static crypto::tink::util::StatusOr<std::unique_ptr<AwsKmsClient>> New(
+      absl::string_view key_uri, absl::string_view credentials_path);
 
   // Creates a new client and registers it in KMSClients.
   static crypto::tink::util::Status RegisterNewClient(
@@ -55,10 +61,8 @@
   // to a specific key.
   bool DoesSupport(absl::string_view key_uri) const override;
 
-  // Returns an Aead-primitive backed by KMS key specified by `key_uri`,
-  // provided that this KmsClient does support `key_uri`.
-  crypto::tink::util::StatusOr<std::unique_ptr<Aead>>
-  GetAead(absl::string_view key_uri) const override;
+  crypto::tink::util::StatusOr<std::unique_ptr<Aead>> GetAead(
+      absl::string_view key_uri) const override;
 
  private:
   AwsKmsClient(absl::string_view key_arn, Aws::Auth::AWSCredentials credentials)
@@ -66,11 +70,6 @@
   AwsKmsClient(Aws::Auth::AWSCredentials credentials)
       : credentials_(credentials) {}
 
-  // Initializes AWS API.
-  static void InitAwsApi();
-  static bool aws_api_is_initialized_;
-  static absl::Mutex aws_api_init_mutex_;
-
   std::string key_arn_;
   Aws::Auth::AWSCredentials credentials_;
   std::shared_ptr<Aws::KMS::KMSClient> aws_client_;
diff --git a/cc/integration/awskms/aws_kms_client_test.cc b/cc/integration/awskms/aws_kms_client_test.cc
index 490cfc1..042d76b 100644
--- a/cc/integration/awskms/aws_kms_client_test.cc
+++ b/cc/integration/awskms/aws_kms_client_test.cc
@@ -17,6 +17,8 @@
 #include "tink/integration/awskms/aws_kms_client.h"
 
 #include <cstdlib>
+#include <fstream>
+#include <ios>
 #include <string>
 #include <vector>
 
@@ -25,6 +27,7 @@
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
+#include "tink/integration/awskms/internal/test_file_util.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
@@ -41,6 +44,7 @@
 using ::crypto::tink::test::IsOkAndHolds;
 using ::crypto::tink::test::StatusIs;
 using ::testing::IsNull;
+using ::testing::IsSubstring;
 using ::testing::Not;
 
 constexpr absl::string_view kAwsKey1 =
@@ -51,7 +55,7 @@
 
 TEST(AwsKmsClientTest, CreateClientNotBoundToSpecificKeySupportsAllValidKeys) {
   std::string creds_file =
-      absl::StrCat(getenv("TEST_SRCDIR"), "/tink_cc_awskms/testdata/aws/credentials.ini");
+      internal::RunfilesPath("testdata/aws/credentials.ini");
   util::StatusOr<std::unique_ptr<AwsKmsClient>> client =
       AwsKmsClient::New(/*key_uri=*/"", creds_file);
   ASSERT_THAT(client, IsOk());
@@ -64,7 +68,7 @@
 // different key URI.
 TEST(AwsKmsClientTest, CreateClientBoundToSpecificKeySupportOnlyOneKey) {
   std::string creds_file =
-      absl::StrCat(getenv("TEST_SRCDIR"), "/tink_cc_awskms/testdata/aws/credentials.ini");
+      internal::RunfilesPath("testdata/aws/credentials.ini");
   util::StatusOr<std::unique_ptr<AwsKmsClient>> client =
       AwsKmsClient::New(kAwsKey1, creds_file);
   ASSERT_THAT(client, IsOk());
@@ -75,18 +79,43 @@
 
 TEST(AwsKmsClientTest, RegisterKmsClient) {
   std::string creds_file =
-      absl::StrCat(getenv("TEST_SRCDIR"), "/tink_cc_awskms/testdata/aws/credentials.ini");
+      internal::RunfilesPath("testdata/aws/credentials.ini");
   ASSERT_THAT(AwsKmsClient::RegisterNewClient(kAwsKey1, creds_file), IsOk());
   util::StatusOr<const KmsClient*> kms_client = KmsClients::Get(kAwsKey1);
   EXPECT_THAT(kms_client, IsOkAndHolds(Not(IsNull())));
 }
 
 TEST(AwsKmsClientTest, RegisterKmsClientFailsWhenKeyIsInvalid) {
-  std::string creds_file = absl::StrCat(getenv("TEST_SRCDIR"),
-                                        "/tink_cc_awskms/testdata/gcp/credentials.json");
-  auto client = AwsKmsClient::RegisterNewClient(
-      "gcp-kms://projects/someProject/.../cryptoKeys/key1", creds_file);
+  util::Status client = AwsKmsClient::RegisterNewClient(
+      "gcp-kms://projects/someProject/.../cryptoKeys/key1",
+      internal::RunfilesPath("testdata/aws/credentials.ini"));
   EXPECT_THAT(client, StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_PRED_FORMAT2(IsSubstring, "Invalid key URI",
+                      std::string(client.message()));
+}
+
+TEST(AwsKmsClientTest, RegisterKmsClientFailsWhenCredentialsDoNotExist) {
+  util::Status client =
+      AwsKmsClient::RegisterNewClient(kAwsKey1, "this/file/does/not/exist.ini");
+  EXPECT_THAT(client, StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_PRED_FORMAT2(IsSubstring, "Error opening file",
+                      std::string(client.message()));
+}
+
+TEST(AwsKmsClientTest, RegisterKmsClientFailsWhenMalformedCredentials) {
+  // Create an invalid credentials file.
+  std::string malformed_content = "These are malformed credentials.";
+  std::string invalid_credentials_file =
+      internal::RunfilesPath("testdata/aws/invalid.ini");
+  std::ofstream out_stream(invalid_credentials_file, std::ios::binary);
+  out_stream.write(malformed_content.data(), malformed_content.size());
+  out_stream.close();
+
+  util::Status client =
+      AwsKmsClient::RegisterNewClient(kAwsKey1, invalid_credentials_file);
+  EXPECT_THAT(client, StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_PRED_FORMAT2(IsSubstring, "Invalid format",
+                      std::string(client.message()));
 }
 
 }  // namespace
diff --git a/cc/integration/awskms/internal/BUILD.bazel b/cc/integration/awskms/internal/BUILD.bazel
new file mode 100644
index 0000000..2f41114
--- /dev/null
+++ b/cc/integration/awskms/internal/BUILD.bazel
@@ -0,0 +1,17 @@
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "test_file_util",
+    srcs = ["test_file_util_bazel.cc"],
+    hdrs = ["test_file_util.h"],
+    include_prefix = "tink/integration/awskms/internal",
+    testonly = 1,
+    deps = [
+        "@bazel_tools//tools/cpp/runfiles",
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/strings",
+    ],
+    alwayslink = 1,
+)
diff --git a/cc/integration/awskms/internal/test_file_util.h b/cc/integration/awskms/internal/test_file_util.h
new file mode 100644
index 0000000..80aef6d
--- /dev/null
+++ b/cc/integration/awskms/internal/test_file_util.h
@@ -0,0 +1,36 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_INTEGRATION_AWSKMS_INTERNAL_TEST_FILE_UTIL_H_
+#define TINK_INTEGRATION_AWSKMS_INTERNAL_TEST_FILE_UTIL_H_
+
+#include "absl/strings/string_view.h"
+
+namespace crypto {
+namespace tink {
+namespace integration {
+namespace awskms {
+namespace internal {
+
+// Returns the path of the specified file in the runfiles directory.
+std::string RunfilesPath(absl::string_view path);
+
+}  // namespace internal
+}  // namespace awskms
+}  // namespace integration
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTEGRATION_AWSKMS_INTERNAL_TEST_FILE_UTIL_H_
diff --git a/cc/integration/awskms/internal/test_file_util_bazel.cc b/cc/integration/awskms/internal/test_file_util_bazel.cc
new file mode 100644
index 0000000..e59bf04
--- /dev/null
+++ b/cc/integration/awskms/internal/test_file_util_bazel.cc
@@ -0,0 +1,45 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#include "tink/integration/awskms/internal/test_file_util.h"
+
+#include "absl/log/check.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "tools/cpp/runfiles/runfiles.h"
+
+namespace crypto {
+namespace tink {
+namespace integration {
+namespace awskms {
+namespace internal {
+
+using ::bazel::tools::cpp::runfiles::Runfiles;
+
+std::string RunfilesPath(absl::string_view path) {
+  std::string error;
+  std::unique_ptr<Runfiles> runfiles(Runfiles::CreateForTest(&error));
+  CHECK(runfiles != nullptr) << "Unable to determine runfile path: ";
+  const char* workspace_dir = getenv("TEST_WORKSPACE");
+  CHECK(workspace_dir != nullptr && workspace_dir[0] != '\0')
+      << "Unable to determine workspace name.";
+  return runfiles->Rlocation(absl::StrCat(workspace_dir, "/", path));
+}
+
+}  // namespace internal
+}  // namespace awskms
+}  // namespace integration
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/integration/awskms/testdata/README.md b/cc/integration/awskms/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/cc/integration/awskms/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/cc/integration/awskms/testdata/aws/README.md b/cc/integration/awskms/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/cc/integration/awskms/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/cc/integration/awskms/testdata/gcp/README.md b/cc/integration/awskms/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/cc/integration/awskms/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/cc/integration/awskms/third_party/aws_checksums.BUILD.bazel b/cc/integration/awskms/third_party/aws_checksums.BUILD.bazel
index 25fc636..916a28b 100644
--- a/cc/integration/awskms/third_party/aws_checksums.BUILD.bazel
+++ b/cc/integration/awskms/third_party/aws_checksums.BUILD.bazel
@@ -3,9 +3,12 @@
 cc_library(
     name = "aws_checksums",
     srcs = glob([
-        "source/intel/*.c",
         "source/*.c",
-    ]),
+    ]) + select({
+        "@platforms//cpu:x86_64": glob(["source/intel/*.c"]),
+        "@platforms//cpu:aarch64": glob(["source/arm/*.c"]),
+        "//conditions:default": ["@platforms//:incompatible"],
+    }),
     hdrs = glob([
         "include/**/*.h"
     ]),
diff --git a/cc/integration/awskms/third_party/curl.BUILD.bazel b/cc/integration/awskms/third_party/curl.BUILD.bazel
index 210b632..5a6530d 100644
--- a/cc/integration/awskms/third_party/curl.BUILD.bazel
+++ b/cc/integration/awskms/third_party/curl.BUILD.bazel
@@ -3,24 +3,6 @@
 
 licenses(["notice"])  # MIT/X derivative license
 
-# Settings for building in various environments.
-config_setting(
-    name = "linux_x86_64",
-    values = {"cpu": "k8"},
-)
-
-config_setting(
-    name = "mac_x86_64",
-    values = {"cpu": "darwin"},
-)
-
-config_setting(
-    name = "darwin_arm64",
-    values = {
-        "cpu": "darwin_arm64",
-    },
-)
-
 cc_library(
     name = "curl",
     srcs = [
@@ -220,15 +202,9 @@
         "lib/wildcard.h",
         "lib/x509asn1.h",
     ] + select({
-        ":mac_x86_64": [
-            "lib/vtls/darwinssl.c",
-        ],
-        ":darwin_arm64": [
-            "lib/vtls/darwinssl.c",
-        ],
-        ":linux_x86_64": [
-            "lib/vtls/openssl.c",
-        ],
+        "@platforms//os:macos": ["lib/vtls/darwinssl.c"],
+        "@platforms//os:linux": ["lib/vtls/openssl.c"],
+        "//conditions:default": ["@platforms//:incompatible"],
     }),
     hdrs = [
         ":configure",
@@ -252,34 +228,21 @@
         "-DHAVE_ZLIB_H",
         "-Wno-string-plus-int",
     ] + select({
-        ":mac_x86_64": [
-            "-fno-constant-cfstrings",
-        ],
-        ":darwin_arm64": [
-            "-fno-constant-cfstrings",
-        ],
-        ":linux_x86_64": [
-            "-DCURL_MAX_WRITE_SIZE=65536",
-        ],
+        "@platforms//os:macos": ["-fno-constant-cfstrings"],
+        "@platforms//os:linux": ["-DCURL_MAX_WRITE_SIZE=65536"],
+        "//conditions:default": ["@platforms//:incompatible"],
     }),
     defines = ["CURL_STATICLIB"],
     includes = ["include"],
     linkopts = select({
-        ":mac_x86_64": [
+        "@platforms//os:macos": [
             "-Wl,-framework",
             "-Wl,CoreFoundation",
             "-Wl,-framework",
             "-Wl,Security",
         ],
-        ":darwin_arm64": [
-            "-Wl,-framework",
-            "-Wl,CoreFoundation",
-            "-Wl,-framework",
-            "-Wl,Security",
-        ],
-        ":linux_x86_64": [
-            "-lrt",
-        ],
+        "@platforms//os:linux": ["-lrt"],
+        "//conditions:default": ["@platforms//:incompatible"],
     }),
     visibility = ["//visibility:public"],
     deps = [
diff --git a/cc/integration/gcpkms/.bazelrc b/cc/integration/gcpkms/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/cc/integration/gcpkms/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/cc/integration/gcpkms/.bazelversion b/cc/integration/gcpkms/.bazelversion
new file mode 100644
index 0000000..09b254e
--- /dev/null
+++ b/cc/integration/gcpkms/.bazelversion
@@ -0,0 +1 @@
+6.0.0
diff --git a/cc/integration/gcpkms/BUILD.bazel b/cc/integration/gcpkms/BUILD.bazel
index 86d878f..4e9139d 100644
--- a/cc/integration/gcpkms/BUILD.bazel
+++ b/cc/integration/gcpkms/BUILD.bazel
@@ -39,17 +39,25 @@
     ],
 )
 
-# tests
-
 cc_test(
-    name = "gcp_kms_aead_test",
+    name = "gcp_kms_aead_integration_test",
     size = "medium",
-    srcs = ["gcp_kms_aead_test.cc"],
+    srcs = ["gcp_kms_aead_integration_test.cc"],
+    data = [
+      "//testdata/gcp:credentials",
+      "@google_root_pem//file"
+    ],
+    # This target requires valid credentials to interact with the Google Cloud
+    # KMS.
+    tags = ["manual"],
     deps = [
         ":gcp_kms_aead",
+        ":gcp_kms_client",
+        "@bazel_tools//tools/cpp/runfiles",
+        "@com_google_absl//absl/log:check",
         "@com_google_googletest//:gtest_main",
-        "@tink_cc//util:status",
         "@tink_cc//util:statusor",
+        "@tink_cc//util:test_matchers",
     ],
 )
 
diff --git a/cc/integration/gcpkms/gcp_kms_aead.cc b/cc/integration/gcpkms/gcp_kms_aead.cc
index ae57ff1..1d879a4 100644
--- a/cc/integration/gcpkms/gcp_kms_aead.cc
+++ b/cc/integration/gcpkms/gcp_kms_aead.cc
@@ -16,13 +16,12 @@
 
 #include "tink/integration/gcpkms/gcp_kms_aead.h"
 
+#include <memory>
 #include <string>
-#include <utility>
 
 #include "google/cloud/kms/v1/service.grpc.pb.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "absl/strings/match.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "tink/aead.h"
@@ -34,37 +33,27 @@
 namespace integration {
 namespace gcpkms {
 
-using crypto::tink::util::Status;
-using crypto::tink::util::StatusOr;
-using google::cloud::kms::v1::DecryptRequest;
-using google::cloud::kms::v1::DecryptResponse;
-using google::cloud::kms::v1::EncryptRequest;
-using google::cloud::kms::v1::EncryptResponse;
-using google::cloud::kms::v1::KeyManagementService;
-using grpc::ClientContext;
+using ::google::cloud::kms::v1::DecryptRequest;
+using ::google::cloud::kms::v1::DecryptResponse;
+using ::google::cloud::kms::v1::EncryptRequest;
+using ::google::cloud::kms::v1::EncryptResponse;
+using ::google::cloud::kms::v1::KeyManagementService;
 
-GcpKmsAead::GcpKmsAead(
+util::StatusOr<std::unique_ptr<Aead>> GcpKmsAead::New(
     absl::string_view key_name,
-    std::shared_ptr<KeyManagementService::Stub> kms_stub)
-    : key_name_(key_name), kms_stub_(kms_stub) {}
-
-// static
-StatusOr<std::unique_ptr<Aead>>
-GcpKmsAead::New(absl::string_view key_name,
-                std::shared_ptr<KeyManagementService::Stub> kms_stub) {
+    std::shared_ptr<KeyManagementService::Stub> kms_stub) {
   if (key_name.empty()) {
-    return Status(absl::StatusCode::kInvalidArgument,
+    return util::Status(absl::StatusCode::kInvalidArgument,
                         "Key URI cannot be empty.");
   }
   if (kms_stub == nullptr) {
-    return Status(absl::StatusCode::kInvalidArgument,
+    return util::Status(absl::StatusCode::kInvalidArgument,
                         "KMS stub cannot be null.");
   }
-  std::unique_ptr<Aead> aead(new GcpKmsAead(key_name, kms_stub));
-  return std::move(aead);
+  return absl::WrapUnique(new GcpKmsAead(key_name, kms_stub));
 }
 
-StatusOr<std::string> GcpKmsAead::Encrypt(
+util::StatusOr<std::string> GcpKmsAead::Encrypt(
     absl::string_view plaintext, absl::string_view associated_data) const {
   EncryptRequest req;
   req.set_name(key_name_);
@@ -72,19 +61,21 @@
   req.set_additional_authenticated_data(std::string(associated_data));
 
   EncryptResponse resp;
-  ClientContext context;
+  grpc::ClientContext context;
   context.AddMetadata("x-goog-request-params",
                       absl::StrCat("name=", key_name_));
 
-  auto status =  kms_stub_->Encrypt(&context, req, &resp);
+  grpc::Status status = kms_stub_->Encrypt(&context, req, &resp);
 
-  if (status.ok()) return resp.ciphertext();
-  return Status(
-      absl::StatusCode::kInvalidArgument,
-      absl::StrCat("GCP KMS encryption failed: ", status.error_message()));
+  if (!status.ok()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("GCP KMS encryption failed: ", status.error_message()));
+  }
+  return resp.ciphertext();
 }
 
-StatusOr<std::string> GcpKmsAead::Decrypt(
+util::StatusOr<std::string> GcpKmsAead::Decrypt(
     absl::string_view ciphertext, absl::string_view associated_data) const {
   DecryptRequest req;
   req.set_name(key_name_);
@@ -92,16 +83,18 @@
   req.set_additional_authenticated_data(std::string(associated_data));
 
   DecryptResponse resp;
-  ClientContext context;
+  grpc::ClientContext context;
   context.AddMetadata("x-goog-request-params",
                       absl::StrCat("name=", key_name_));
 
-  auto status =  kms_stub_->Decrypt(&context, req, &resp);
+  grpc::Status status = kms_stub_->Decrypt(&context, req, &resp);
 
-  if (status.ok()) return resp.plaintext();
-  return Status(
-      absl::StatusCode::kInvalidArgument,
-      absl::StrCat("GCP KMS encryption failed: ", status.error_message()));
+  if (!status.ok()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("GCP KMS encryption failed: ", status.error_message()));
+  }
+  return resp.plaintext();
 }
 
 }  // namespace gcpkms
diff --git a/cc/integration/gcpkms/gcp_kms_aead.h b/cc/integration/gcpkms/gcp_kms_aead.h
index 8e403ff..ff7833c 100644
--- a/cc/integration/gcpkms/gcp_kms_aead.h
+++ b/cc/integration/gcpkms/gcp_kms_aead.h
@@ -17,6 +17,7 @@
 #ifndef TINK_INTEGRATION_GCPKMS_GCP_KMS_AEAD_H_
 #define TINK_INTEGRATION_GCPKMS_GCP_KMS_AEAD_H_
 
+#include <memory>
 #include <string>
 
 #include "google/cloud/kms/v1/service.grpc.pb.h"
@@ -29,18 +30,25 @@
 namespace integration {
 namespace gcpkms {
 
-// GcpKmsAead is an implementation of AEAD that forwards
-// encryption/decryption requests to a key managed by
-// <a href="https://cloud.google.com/kms/">Google Cloud KMS</a>.
+// GcpKmsAead is an implementation of AEAD that forwards encryption/decryption
+// requests to a key managed by the Google Cloud KMS
+// (https://cloud.google.com/kms/).
 class GcpKmsAead : public Aead {
  public:
-  // Creates a new GcpKmsAead that is bound to the key specified in 'key_name',
+  // Move only.
+  GcpKmsAead(GcpKmsAead&& other) = default;
+  GcpKmsAead& operator=(GcpKmsAead&& other) = default;
+  GcpKmsAead(const GcpKmsAead&) = delete;
+  GcpKmsAead& operator=(const GcpKmsAead&) = delete;
+
+  // Creates a new GcpKmsAead that is bound to the key specified in `key_name`,
   // and that uses the channel when communicating with the KMS.
-  // Valid values for 'key_name' have the following format:
+  //
+  // Valid values for `key_name` have the following format:
   //    projects/*/locations/*/keyRings/*/cryptoKeys/*.
   // See https://cloud.google.com/kms/docs/object-hierarchy for more info.
-  static crypto::tink::util::StatusOr<std::unique_ptr<Aead>>
-  New(absl::string_view key_name,
+  static crypto::tink::util::StatusOr<std::unique_ptr<Aead>> New(
+      absl::string_view key_name,
       std::shared_ptr<google::cloud::kms::v1::KeyManagementService::Stub>
           kms_stub);
 
@@ -52,18 +60,17 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  virtual ~GcpKmsAead() {}
-
  private:
-  GcpKmsAead(
+  explicit GcpKmsAead(
       absl::string_view key_name,
       std::shared_ptr<google::cloud::kms::v1::KeyManagementService::Stub>
-          kms_stub);
-  std::string key_name_;  // The location of a crypto key in GCP KMS.
-  std::shared_ptr<google::cloud::kms::v1::KeyManagementService::Stub>
-      kms_stub_;
-};
+          kms_stub)
+      : key_name_(key_name), kms_stub_(kms_stub) {}
 
+  // The location of a crypto key in GCP KMS.
+  std::string key_name_;
+  std::shared_ptr<google::cloud::kms::v1::KeyManagementService::Stub> kms_stub_;
+};
 
 }  // namespace gcpkms
 }  // namespace integration
diff --git a/cc/integration/gcpkms/gcp_kms_aead_integration_test.cc b/cc/integration/gcpkms/gcp_kms_aead_integration_test.cc
new file mode 100644
index 0000000..4b7b1e2
--- /dev/null
+++ b/cc/integration/gcpkms/gcp_kms_aead_integration_test.cc
@@ -0,0 +1,92 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "gtest/gtest.h"
+#include "absl/log/check.h"
+#include "tink/integration/gcpkms/gcp_kms_aead.h"
+#include "tink/integration/gcpkms/gcp_kms_client.h"
+#include "tink/util/test_matchers.h"
+#include "tools/cpp/runfiles/runfiles.h"
+
+namespace crypto {
+namespace tink {
+namespace integration {
+namespace gcpkms {
+namespace {
+
+using ::bazel::tools::cpp::runfiles::Runfiles;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::testing::Environment;
+
+constexpr absl::string_view kGcpKmsKeyUri =
+    "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/"
+    "unit-and-integration-testing/cryptoKeys/aead-key";
+
+std::string RunfilesPath(absl::string_view path) {
+  std::string error;
+  std::unique_ptr<Runfiles> runfiles(Runfiles::CreateForTest(&error));
+  CHECK(runfiles != nullptr) << "Unable to determine runfile path: ";
+  const char* workspace_dir = getenv("TEST_WORKSPACE");
+  CHECK(workspace_dir != nullptr && workspace_dir[0] != '\0')
+      << "Unable to determine workspace name.";
+  return runfiles->Rlocation(absl::StrCat(workspace_dir, "/", path));
+}
+
+class GcpKmsAeadIntegrationTestEnvironment : public Environment {
+ public:
+  ~GcpKmsAeadIntegrationTestEnvironment() override = default;
+
+  void SetUp() override {
+    // Set root certificates for gRPC in Bazel Test which are needed on macOS.
+    const char* test_srcdir = getenv("TEST_SRCDIR");
+    if (test_srcdir != nullptr) {
+      setenv(
+          "GRPC_DEFAULT_SSL_ROOTS_FILE_PATH",
+          absl::StrCat(test_srcdir, "/google_root_pem/file/downloaded").c_str(),
+          /*overwrite=*/false);
+    }
+  }
+};
+
+Environment* const foo_env = testing::AddGlobalTestEnvironment(
+    new GcpKmsAeadIntegrationTestEnvironment());
+
+TEST(GcpKmsAeadIntegrationTest, EncryptDecrypt) {
+  std::string credentials = RunfilesPath("testdata/gcp/credential.json");
+  util::StatusOr<std::unique_ptr<GcpKmsClient>> client =
+      GcpKmsClient::New(/*key_uri=*/"", credentials);
+  ASSERT_THAT(client, IsOk());
+
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      (*client)->GetAead(kGcpKmsKeyUri);
+  ASSERT_THAT(aead, IsOk());
+
+  constexpr absl::string_view kPlaintext = "plaintext";
+  constexpr absl::string_view kAssociatedData = "aad";
+
+  util::StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(kPlaintext, kAssociatedData);
+  ASSERT_THAT(ciphertext, IsOk());
+  EXPECT_THAT((*aead)->Decrypt(*ciphertext, kAssociatedData),
+              IsOkAndHolds(kPlaintext));
+}
+
+}  // namespace
+}  // namespace gcpkms
+}  // namespace integration
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/integration/gcpkms/gcp_kms_aead_test.cc b/cc/integration/gcpkms/gcp_kms_aead_test.cc
deleted file mode 100644
index a94433e..0000000
--- a/cc/integration/gcpkms/gcp_kms_aead_test.cc
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-#include "gtest/gtest.h"
-#include "tink/integration/gcpkms/gcp_kms_aead.h"
-
-namespace crypto {
-namespace tink {
-namespace subtle {
-namespace {
-
-using crypto::tink::integration::gcpkms::GcpKmsAead;
-
-class GcpKmsAeadTest : public ::testing::Test {
-  // TODO(kste): Add tests when mock for
-  // google::cloud::kms::v1::KeyManagementService::StubInterface is available.
-};
-
-
-}  // namespace
-}  // namespace subtle
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/integration/gcpkms/gcp_kms_client.cc b/cc/integration/gcpkms/gcp_kms_client.cc
index c90572c..f38d133 100644
--- a/cc/integration/gcpkms/gcp_kms_client.cc
+++ b/cc/integration/gcpkms/gcp_kms_client.cc
@@ -17,6 +17,7 @@
 
 #include <fstream>
 #include <iostream>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
@@ -26,9 +27,8 @@
 #include "grpcpp/security/credentials.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "absl/strings/ascii.h"
+#include "absl/strings/match.h"
 #include "absl/strings/str_cat.h"
-#include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
 #include "tink/integration/gcpkms/gcp_kms_aead.h"
 #include "tink/kms_clients.h"
@@ -43,23 +43,18 @@
 
 namespace {
 
-using crypto::tink::Version;
-using crypto::tink::util::Status;
-using crypto::tink::util::StatusOr;
-using google::cloud::kms::v1::KeyManagementService;
-using grpc::ChannelArguments;
-using grpc::ChannelCredentials;
+using ::google::cloud::kms::v1::KeyManagementService;
 
-static constexpr char kKeyUriPrefix[] = "gcp-kms://";
-static constexpr char kGcpKmsServer[] = "cloudkms.googleapis.com";
-static constexpr char kTinkUserAgentPrefix[] = "Tink/";
+static constexpr absl::string_view kKeyUriPrefix = "gcp-kms://";
+static constexpr absl::string_view kGcpKmsServer = "cloudkms.googleapis.com";
+static constexpr absl::string_view kTinkUserAgentPrefix = "Tink/";
 
-StatusOr<std::string> ReadFile(absl::string_view filename) {
+util::StatusOr<std::string> ReadFile(absl::string_view filename) {
   std::ifstream input_stream;
   input_stream.open(std::string(filename), std::ifstream::in);
   if (!input_stream.is_open()) {
-    return Status(absl::StatusCode::kInvalidArgument,
-                  absl::StrCat("Error reading file ", filename));
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("Error reading file ", filename));
   }
   std::stringstream input;
   input << input_stream.rdbuf();
@@ -67,104 +62,110 @@
   return input.str();
 }
 
-StatusOr<std::shared_ptr<ChannelCredentials>> GetCredentials(
+util::StatusOr<std::shared_ptr<grpc::ChannelCredentials>> GetCredentials(
     absl::string_view credentials_path) {
   if (credentials_path.empty()) {
-    auto creds = grpc::GoogleDefaultCredentials();
+    std::shared_ptr<grpc::ChannelCredentials> creds =
+        grpc::GoogleDefaultCredentials();
     if (creds == nullptr) {
-      return Status(absl::StatusCode::kInternal,
-                    "Could not read default credentials");
+      return util::Status(absl::StatusCode::kInternal,
+                          "Could not read default credentials");
     }
     return creds;
   }
 
   // Try reading credentials from a file.
-  auto json_creds_result = ReadFile(credentials_path);
-  if (!json_creds_result.ok()) return json_creds_result.status();
-  auto creds =
-      grpc::ServiceAccountJWTAccessCredentials(json_creds_result.value());
-  if (creds != nullptr) {
-    // Creating "empty" 'channel_creds', to convert 'creds'
-    // to ChannelCredentials via CompositeChannelCredentials().
-    auto channel_creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
-    return grpc::CompositeChannelCredentials(channel_creds, creds);
+  util::StatusOr<std::string> json_creds_result = ReadFile(credentials_path);
+  if (!json_creds_result.ok()) {
+    return json_creds_result.status();
   }
-  return Status(
-      absl::StatusCode::kInvalidArgument,
-      absl::StrCat("Could not load credentials from file ", credentials_path));
+  std::shared_ptr<grpc::CallCredentials> creds =
+      grpc::ServiceAccountJWTAccessCredentials(json_creds_result.value());
+  if (creds == nullptr) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("Could not load credentials from file ",
+                                     credentials_path));
+  }
+  // Creating "empty" 'channel_creds', to convert 'creds' to ChannelCredentials
+  // via CompositeChannelCredentials().
+  std::shared_ptr<grpc::ChannelCredentials> channel_creds =
+      grpc::SslCredentials(grpc::SslCredentialsOptions());
+  return grpc::CompositeChannelCredentials(channel_creds, creds);
 }
 
-// Returns GCP KMS key name contained in 'key_uri'.
-// If 'key_uri' does not refer to an GCP key, returns an empty string.
-std::string GetKeyName(absl::string_view key_uri) {
-  if (!absl::StartsWithIgnoreCase(key_uri, kKeyUriPrefix)) return "";
-  return std::string(key_uri.substr(std::string(kKeyUriPrefix).length()));
+// Returns GCP KMS key name contained in `key_uri`. If `key_uri` does not refer
+// to a GCP key, returns an error status.
+util::StatusOr<std::string> GetKeyName(absl::string_view key_uri) {
+  if (!absl::StartsWithIgnoreCase(key_uri, kKeyUriPrefix)) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("The key URI ", key_uri,
+                                     " does not start with ", kKeyUriPrefix));
+  }
+  return std::string(key_uri.substr(kKeyUriPrefix.length()));
 }
 
 }  // namespace
 
-// static
-StatusOr<std::unique_ptr<GcpKmsClient>> GcpKmsClient::New(
+util::StatusOr<std::unique_ptr<GcpKmsClient>> GcpKmsClient::New(
     absl::string_view key_uri, absl::string_view credentials_path) {
-  std::unique_ptr<GcpKmsClient> client(new GcpKmsClient());
-
-  // If a specific key is given, create a GCP KMSClient.
+  // Empty key name by default.
+  std::string key_name = "";
   if (!key_uri.empty()) {
-    client->key_name_ = GetKeyName(key_uri);
-    if (client->key_name_.empty()) {
-      return Status(absl::StatusCode::kInvalidArgument,
-                    absl::StrCat("Key '", key_uri, "' not supported"));
+    util::StatusOr<std::string> key_name_from_uri = GetKeyName(key_uri);
+    if (!key_name_from_uri.ok()) {
+      return key_name_from_uri.status();
     }
+    key_name = key_name_from_uri.value();
   }
+
   // Read credentials.
-  auto creds_result = GetCredentials(credentials_path);
+  util::StatusOr<std::shared_ptr<grpc::ChannelCredentials>> creds_result =
+      GetCredentials(credentials_path);
   if (!creds_result.ok()) {
     return creds_result.status();
   }
 
   // Create a KMS stub.
-  ChannelArguments args;
+  grpc::ChannelArguments args;
   args.SetUserAgentPrefix(
-      absl::StrCat(kTinkUserAgentPrefix, Version::kTinkVersion, " CPP-Python"));
-  client->kms_stub_ = KeyManagementService::NewStub(
-      grpc::CreateCustomChannel(kGcpKmsServer, creds_result.value(), args));
-  return std::move(client);
+      absl::StrCat(kTinkUserAgentPrefix, Version::kTinkVersion, " CPP"));
+  std::shared_ptr<KeyManagementService::Stub> kms_stub =
+      KeyManagementService::NewStub(grpc::CreateCustomChannel(
+          std::string(kGcpKmsServer), creds_result.value(), args));
+  return absl::WrapUnique(new GcpKmsClient(key_name, std::move(kms_stub)));
 }
 
 bool GcpKmsClient::DoesSupport(absl::string_view key_uri) const {
-  if (!key_name_.empty()) {
-    return key_name_ == GetKeyName(key_uri);
+  util::StatusOr<std::string> key_name = GetKeyName(key_uri);
+  if (!key_name.ok()) {
+    return false;
   }
-  return !GetKeyName(key_uri).empty();
+  return key_name_.empty() ? true : key_name_ == *key_name;
 }
 
-StatusOr<std::unique_ptr<Aead>> GcpKmsClient::GetAead(
+util::StatusOr<std::unique_ptr<Aead>> GcpKmsClient::GetAead(
     absl::string_view key_uri) const {
-  if (!DoesSupport(key_uri)) {
-    if (!key_name_.empty()) {
-      return Status(absl::StatusCode::kInvalidArgument,
-                    absl::StrCat("This client is bound to ", key_name_,
-                                 " and cannot use key ", key_uri));
-    } else {
-      return Status(absl::StatusCode::kInvalidArgument,
-                    absl::StrCat("This client does not support key ", key_uri));
-    }
+  util::StatusOr<std::string> key_name_from_key_uri = GetKeyName(key_uri);
+  // key_uri is invalid.
+  if (!key_name_from_key_uri.ok()) {
+    return key_name_from_key_uri.status();
   }
-  if (!key_name_.empty()) {  // This client is bound to a specific key.
-    return GcpKmsAead::New(key_name_, kms_stub_);
-  } else {  // Create an GCP KMSClient for the given key.
-    auto key_name = GetKeyName(key_uri);
-    return GcpKmsAead::New(key_name, kms_stub_);
+  // key_uri is valid, but if key_name_ is not empty key_name_from_key_uri must
+  // be equal to key_name_.
+  if (!key_name_.empty() && key_name_ != *key_name_from_key_uri) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("This client is bound to ", key_name_,
+                                     " and cannot use key ", key_uri));
   }
+  return GcpKmsAead::New(*key_name_from_key_uri, kms_stub_);
 }
 
-Status GcpKmsClient::RegisterNewClient(absl::string_view key_uri,
-                                       absl::string_view credentials_path) {
+util::Status GcpKmsClient::RegisterNewClient(
+    absl::string_view key_uri, absl::string_view credentials_path) {
   auto client_result = GcpKmsClient::New(key_uri, credentials_path);
   if (!client_result.ok()) {
     return client_result.status();
   }
-
   return KmsClients::Add(std::move(client_result.value()));
 }
 
diff --git a/cc/integration/gcpkms/gcp_kms_client.h b/cc/integration/gcpkms/gcp_kms_client.h
index 2040eaf..0523dd7 100644
--- a/cc/integration/gcpkms/gcp_kms_client.h
+++ b/cc/integration/gcpkms/gcp_kms_client.h
@@ -19,6 +19,7 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "google/cloud/kms/v1/service.grpc.pb.h"
 #include "grpcpp/channel.h"
@@ -33,40 +34,48 @@
 namespace integration {
 namespace gcpkms {
 
-
-// GcpKmsClient is an implementation of KmsClient for
-// <a href="https://cloud.google.com/kms/">Google Cloud KMS</a>.
-class GcpKmsClient : public crypto::tink::KmsClient  {
+// GcpKmsClient is an implementation of KmsClient for Google Cloud KMS
+// (https://cloud.google.com/kms/).
+class GcpKmsClient : public crypto::tink::KmsClient {
  public:
-  // Creates a new GcpKmsClient that is bound to the key specified in 'key_uri',
-  // and that uses the specifed credentials when communicating with the KMS.
+  // Move only.
+  GcpKmsClient(GcpKmsClient&& other) = default;
+  GcpKmsClient& operator=(GcpKmsClient&& other) = default;
+  GcpKmsClient(const GcpKmsClient&) = delete;
+  GcpKmsClient& operator=(const GcpKmsClient&) = delete;
+
+  // Creates a new GcpKmsClient that is bound to the key specified in `key_uri`,
+  // and that uses the specified credentials when communicating with the KMS.
   //
-  // Either of arguments can be empty.
-  // If 'key_uri' is empty, then the client is not bound to any particular key.
-  // If 'credential_path' is empty, then default credentials will be used.
-  static crypto::tink::util::StatusOr<std::unique_ptr<GcpKmsClient>>
-  New(absl::string_view key_uri, absl::string_view credentials_path);
+  // Either argument can be empty.
+  // If `key_uri` is empty, then the client is not bound to any particular key.
+  // If `credential_path` is empty, then default credentials will be used.
+  static crypto::tink::util::StatusOr<std::unique_ptr<GcpKmsClient>> New(
+      absl::string_view key_uri, absl::string_view credentials_path);
 
   // Creates a new client and registers it in KMSClients.
   static crypto::tink::util::Status RegisterNewClient(
       absl::string_view key_uri, absl::string_view credentials_path);
 
-  // Returns true iff this client does support KMS key specified by 'key_uri'.
+  // Returns true iff this client does support KMS key specified by `key_uri`.
   bool DoesSupport(absl::string_view key_uri) const override;
 
-  // Returns an Aead-primitive backed by KMS key specified by 'key_uri',
-  // provided that this KmsClient does support 'key_uri'.
-  crypto::tink::util::StatusOr<std::unique_ptr<Aead>>
-  GetAead(absl::string_view key_uri) const override;
+  // Returns an Aead-primitive backed by KMS key specified by `key_uri`,
+  // provided that this KmsClient does support `key_uri`.
+  crypto::tink::util::StatusOr<std::unique_ptr<Aead>> GetAead(
+      absl::string_view key_uri) const override;
 
  private:
-  GcpKmsClient() {}
+  explicit GcpKmsClient(
+      std::string key_name,
+      std::shared_ptr<google::cloud::kms::v1::KeyManagementService::Stub>
+          kms_stub)
+      : key_name_(key_name), kms_stub_(std::move(kms_stub)) {}
 
   std::string key_name_;
   std::shared_ptr<google::cloud::kms::v1::KeyManagementService::Stub> kms_stub_;
 };
 
-
 }  // namespace gcpkms
 }  // namespace integration
 }  // namespace tink
diff --git a/cc/integration/gcpkms/gcp_kms_client_test.cc b/cc/integration/gcpkms/gcp_kms_client_test.cc
index c8400f4..a53eed7 100644
--- a/cc/integration/gcpkms/gcp_kms_client_test.cc
+++ b/cc/integration/gcpkms/gcp_kms_client_test.cc
@@ -29,16 +29,14 @@
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
-using ::crypto::tink::test::IsOk;
-using ::crypto::tink::test::StatusIs;
-
 namespace crypto {
 namespace tink {
 namespace integration {
 namespace gcpkms {
 namespace {
 
-using crypto::tink::integration::gcpkms::GcpKmsClient;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
 
 TEST(GcpKmsClientTest, ClientNotBoundToAKey) {
   std::string gcp_key1 = "gcp-kms://projects/someProject/.../cryptoKeys/key1";
@@ -47,12 +45,12 @@
   std::string creds_file =
       std::string(getenv("TEST_SRCDIR")) + "/tink_cc_gcpkms/testdata/gcp/credential.json";
 
-  auto client_result = GcpKmsClient::New("", creds_file);
-  EXPECT_TRUE(client_result.ok()) << client_result.status();
-  auto client = std::move(client_result.value());
-  EXPECT_TRUE(client->DoesSupport(gcp_key1));
-  EXPECT_TRUE(client->DoesSupport(gcp_key2));
-  EXPECT_FALSE(client->DoesSupport(non_gcp_key));
+  util::StatusOr<std::unique_ptr<GcpKmsClient>> client =
+      GcpKmsClient::New("", creds_file);
+  ASSERT_THAT(client, IsOk());
+  EXPECT_TRUE((*client)->DoesSupport(gcp_key1));
+  EXPECT_TRUE((*client)->DoesSupport(gcp_key2));
+  EXPECT_FALSE((*client)->DoesSupport(non_gcp_key));
 }
 
 TEST(GcpKmsClientTest, ClientBoundToASpecificKey) {
@@ -62,12 +60,12 @@
   std::string creds_file =
       std::string(getenv("TEST_SRCDIR")) + "/tink_cc_gcpkms/testdata/gcp/credential.json";
 
-  auto client_result = GcpKmsClient::New(gcp_key1, creds_file);
-  EXPECT_TRUE(client_result.ok()) << client_result.status();
-  auto client = std::move(client_result.value());
-  EXPECT_TRUE(client->DoesSupport(gcp_key1));
-  EXPECT_FALSE(client->DoesSupport(gcp_key2));
-  EXPECT_FALSE(client->DoesSupport(non_gcp_key));
+  util::StatusOr<std::unique_ptr<GcpKmsClient>> client =
+      GcpKmsClient::New(gcp_key1, creds_file);
+  ASSERT_THAT(client, IsOk());
+  EXPECT_TRUE((*client)->DoesSupport(gcp_key1));
+  EXPECT_FALSE((*client)->DoesSupport(gcp_key2));
+  EXPECT_FALSE((*client)->DoesSupport(non_gcp_key));
 }
 
 TEST(GcpKmsClientTest, ClientCreationAndRegistry) {
@@ -75,10 +73,11 @@
   std::string creds_file =
       absl::StrCat(getenv("TEST_SRCDIR"), "/tink_cc_gcpkms/testdata/gcp/credential.json");
 
-  auto client_result = GcpKmsClient::RegisterNewClient(gcp_key1, creds_file);
-  EXPECT_THAT(client_result, IsOk());
+  util::Status client_result =
+      GcpKmsClient::RegisterNewClient(gcp_key1, creds_file);
+  ASSERT_THAT(client_result, IsOk());
 
-  auto registry_result = KmsClients::Get(gcp_key1);
+  util::StatusOr<const KmsClient*> registry_result = KmsClients::Get(gcp_key1);
   EXPECT_THAT(registry_result, IsOk());
 }
 
@@ -87,7 +86,8 @@
   std::string creds_file =
       std::string(getenv("TEST_SRCDIR")) + "/tink_cc_gcpkms/testdata/gcp/credential.json";
 
-  auto client_result = GcpKmsClient::RegisterNewClient(non_gcp_key, creds_file);
+  util::Status client_result =
+      GcpKmsClient::RegisterNewClient(non_gcp_key, creds_file);
   EXPECT_THAT(client_result, StatusIs(absl::StatusCode::kInvalidArgument));
 }
 
diff --git a/cc/integration/gcpkms/testdata/README.md b/cc/integration/gcpkms/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/cc/integration/gcpkms/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/cc/integration/gcpkms/testdata/aws/README.md b/cc/integration/gcpkms/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/cc/integration/gcpkms/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/cc/integration/gcpkms/testdata/gcp/README.md b/cc/integration/gcpkms/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/cc/integration/gcpkms/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/cc/integration/gcpkms/tink_cc_gcpkms_deps.bzl b/cc/integration/gcpkms/tink_cc_gcpkms_deps.bzl
index c6ab778..8cfb30d 100644
--- a/cc/integration/gcpkms/tink_cc_gcpkms_deps.bzl
+++ b/cc/integration/gcpkms/tink_cc_gcpkms_deps.bzl
@@ -27,25 +27,25 @@
 
     # gRPC needs rules_apple, which in turn needs rules_swift and apple_support.
     if not native.existing_rule("build_bazel_rules_apple"):
-        # Release from 2022-05-02.
+        # Release from 2022-09-16.
         http_archive(
             name = "build_bazel_rules_apple",
-            sha256 = "12865e5944f09d16364aa78050366aca9dc35a32a018fa35f5950238b08bf744",
-            url = "https://github.com/bazelbuild/rules_apple/releases/download/0.34.2/rules_apple.0.34.2.tar.gz",
+            sha256 = "90e3b5e8ff942be134e64a83499974203ea64797fd620eddeb71b3a8e1bff681",
+            url = "https://github.com/bazelbuild/rules_apple/releases/download/1.1.2/rules_apple.1.1.2.tar.gz",
         )
     if not native.existing_rule("build_bazel_rules_swift"):
-        # Release from 2022-03-23.
+        # Release from 2022-09-16.
         http_archive(
             name = "build_bazel_rules_swift",
-            sha256 = "a2fd565e527f83fb3f9eb07eb9737240e668c9242d3bc318712efa54a7deda97",
-            url = "https://github.com/bazelbuild/rules_swift/releases/download/0.27.0/rules_swift.0.27.0.tar.gz",
+            sha256 = "51efdaf85e04e51174de76ef563f255451d5a5cd24c61ad902feeadafc7046d9",
+            url = "https://github.com/bazelbuild/rules_swift/releases/download/1.2.0/rules_swift.1.2.0.tar.gz",
         )
     if not native.existing_rule("build_bazel_apple_support"):
-        # Release from 2022-02-03.
+        # Release from 2022-10-31.
         http_archive(
             name = "build_bazel_apple_support",
-            sha256 = "5bbce1b2b9a3d4b03c0697687023ef5471578e76f994363c641c5f50ff0c7268",
-            url = "https://github.com/bazelbuild/apple_support/releases/download/0.13.0/apple_support.0.13.0.tar.gz",
+            sha256 = "2e3dc4d0000e8c2f5782ea7bb53162f37c485b5d8dc62bb3d7d7fc7c276f0d00",
+            url = "https://github.com/bazelbuild/apple_support/releases/download/1.3.2/apple_support.1.3.2.tar.gz",
         )
 
     if not native.existing_rule("com_google_googleapis"):
@@ -57,7 +57,7 @@
             url = "https://github.com/googleapis/googleapis/archive/2f9af297c84c55c8b871ba4495e01ade42476c92.tar.gz",
         )
 
-    if "upb" not in native.existing_rules():
+    if not native.existing_rule("upb"):
         # Matches version embedded in com_github_grpc_grpc from 2022-05-11.
         http_archive(
             name = "upb",
@@ -66,7 +66,7 @@
             url = "https://github.com/protocolbuffers/upb/archive/bef53686ec702607971bd3ea4d4fefd80c6cc6e8.tar.gz",
         )
 
-    if "envoy_api" not in native.existing_rules():
+    if not native.existing_rule("envoy_api"):
         # Matches version embedded in com_github_grpc_grpc from 2022-05-11.
         http_archive(
             name = "envoy_api",
@@ -75,7 +75,7 @@
             url = "https://github.com/envoyproxy/data-plane-api/archive/9c42588c956220b48eb3099d186487c2f04d32ec.tar.gz",
         )
 
-    if "com_envoyproxy_protoc_gen_validate" not in native.existing_rules():
+    if not native.existing_rule("com_envoyproxy_protoc_gen_validate"):
         # Matches version embedded in com_github_grpc_grpc from 2022-05-11.
         http_archive(
             name = "com_envoyproxy_protoc_gen_validate",
@@ -88,7 +88,7 @@
             patch_args = ["-p1"],
         )
 
-    if "bazel_gazelle" not in native.existing_rules():
+    if not native.existing_rule("bazel_gazelle"):
         # Matches version embedded in com_github_grpc_grpc from 2022-05-11.
         http_archive(
             name = "bazel_gazelle",
@@ -117,23 +117,3 @@
             strip_prefix = "grpc-java-1.46.0",
             url = "https://github.com/grpc/grpc-java/archive/v1.46.0.tar.gz",
         )
-
-    if not native.existing_rule("curl"):
-        # Release from 2016-05-30.
-        http_archive(
-            name = "curl",
-            url = "https://mirror.bazel.build/curl.haxx.se/download/curl-7.49.1.tar.gz",
-            sha256 = "ff3e80c1ca6a068428726cd7dd19037a47cc538ce58ef61c59587191039b2ca6",
-            strip_prefix = "curl-7.49.1",
-            build_file = "@tink_cc_awskms//:third_party/curl.BUILD.bazel",
-        )
-
-    if not native.existing_rule("zlib"):
-        # Releaes from 2022-03-27.
-        http_archive(
-            name = "zlib",
-            url = "https://mirror.bazel.build/zlib.net/zlib-1.2.12.tar.gz",
-            sha256 = "91844808532e5ce316b3c010929493c0244f3d37593afd6de04f71821d5136d9",
-            strip_prefix = "zlib-1.2.12",
-            build_file = "@tink_cc_awskms//:third_party/zlib.BUILD.bazel",
-        )
diff --git a/cc/internal/BUILD.bazel b/cc/internal/BUILD.bazel
index 84932a9..d573b1d 100644
--- a/cc/internal/BUILD.bazel
+++ b/cc/internal/BUILD.bazel
@@ -25,18 +25,30 @@
     srcs = ["util.cc"],
     hdrs = ["util.h"],
     include_prefix = "tink/internal",
-    deps = ["@com_google_absl//absl/strings"],
+    deps = [
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/strings",
+    ],
 )
 
 cc_library(
     name = "test_file_util",
     testonly = 1,
-    srcs = ["test_file_util_bazel.cc"],
+    srcs = [
+        "test_file_util.cc",
+        "test_file_util_bazel.cc",
+    ],
     hdrs = ["test_file_util.h"],
     include_prefix = "tink/internal",
     deps = [
+        "//subtle:random",
+        "//util:status",
+        "//util:test_util",
         "@bazel_tools//tools/cpp/runfiles",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest",
     ],
 )
 
@@ -50,6 +62,7 @@
         "//:primitive_set",
         "//:primitive_wrapper",
         "//proto:tink_cc_proto",
+        "//util:status",
         "//util:statusor",
         "//util:validation",
         "@com_google_absl//absl/container:flat_hash_map",
@@ -78,30 +91,26 @@
     include_prefix = "tink/internal",
     deps = [
         ":fips_utils",
+        ":key_type_info_store",
         ":keyset_wrapper",
-        ":keyset_wrapper_impl",
-        "//:catalogue",
-        "//:core/key_manager_impl",
+        ":keyset_wrapper_store",
         "//:core/key_type_manager",
-        "//:core/private_key_manager_impl",
         "//:core/private_key_type_manager",
+        "//:input_stream",
         "//:key_manager",
         "//:primitive_set",
         "//:primitive_wrapper",
         "//monitoring",
         "//proto:tink_cc_proto",
         "//util:errors",
-        "//util:protobuf_helper",
         "//util:status",
         "//util:statusor",
-        "//util:validation",
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_absl//absl/synchronization",
-        "@com_google_absl//absl/types:optional",
     ],
 )
 
@@ -148,6 +157,8 @@
         "//util:status",
         "//util:statusor",
         "@boringssl//:crypto",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
         "@com_google_absl//absl/types:span",
     ],
 )
@@ -160,6 +171,7 @@
     deps = [
         ":bn_util",
         ":err_util",
+        ":fips_utils",
         ":ssl_unique_ptr",
         ":ssl_util",
         "//config:tink_fips",
@@ -170,6 +182,7 @@
         "//util:statusor",
         "@boringssl//:crypto",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
         "@com_google_absl//absl/strings",
     ],
 )
@@ -234,6 +247,7 @@
         "//:primitive_set",
         "//:primitive_wrapper",
         "//proto:tink_cc_proto",
+        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "//util:test_util",
@@ -252,6 +266,7 @@
         ":key_info",
         "//proto:tink_cc_proto",
         "@com_google_googletest//:gtest_main",
+        "@com_google_protobuf//:protobuf",
     ],
 )
 
@@ -261,38 +276,46 @@
     srcs = ["registry_impl_test.cc"],
     tags = ["fips"],
     deps = [
+        ":fips_utils",
+        ":registry_impl",
         "//:aead",
-        "//:catalogue",
         "//:core/key_manager_impl",
         "//:core/key_type_manager",
-        "//:crypto_format",
-        "//:keyset_manager",
+        "//:core/private_key_manager_impl",
+        "//:core/private_key_type_manager",
+        "//:core/template_util",
+        "//:hybrid_decrypt",
+        "//:input_stream",
+        "//:key_manager",
+        "//:mac",
+        "//:primitive_set",
+        "//:primitive_wrapper",
         "//:registry",
         "//aead:aead_wrapper",
         "//aead:aes_gcm_key_manager",
-        "//config:tink_fips",
         "//hybrid:ecies_aead_hkdf_private_key_manager",
         "//hybrid:ecies_aead_hkdf_public_key_manager",
-        "//monitoring",
         "//monitoring:monitoring_client_mocks",
         "//proto:aes_ctr_hmac_aead_cc_proto",
         "//proto:aes_gcm_cc_proto",
         "//proto:common_cc_proto",
         "//proto:ecdsa_cc_proto",
+        "//proto:ecies_aead_hkdf_cc_proto",
         "//proto:tink_cc_proto",
         "//subtle:aes_gcm_boringssl",
         "//subtle:random",
+        "//util:input_stream_util",
         "//util:istream_input_stream",
         "//util:protobuf_helper",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
-        "//util:test_keyset_handle",
         "//util:test_matchers",
         "//util:test_util",
         "@boringssl//:crypto",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -333,27 +356,30 @@
         ":bn_util",
         ":ssl_unique_ptr",
         "//util:secret_data",
+        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:span",
         "@com_google_googletest//:gtest_main",
     ],
 )
 
 cc_test(
     name = "rsa_util_test",
-    size = "large",
     srcs = ["rsa_util_test.cc"],
     deps = [
         ":bn_util",
         ":rsa_util",
         ":ssl_unique_ptr",
         "//subtle:random",
+        "//util:secret_data",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@boringssl//:crypto",
+        "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -361,12 +387,11 @@
 
 cc_test(
     name = "ec_util_test",
-    size = "large",
     srcs = ["ec_util_test.cc"],
     data = [
-        "@wycheproof//testvectors:ecdh",
-        "@wycheproof//testvectors:ecdsa_webcrypto",
-        "@wycheproof//testvectors:eddsa",
+        "//testvectors:ecdh",
+        "//testvectors:ecdsa_webcrypto",
+        "//testvectors:eddsa",
     ],
     deps = [
         ":bn_util",
@@ -378,12 +403,15 @@
         "//subtle:subtle_util",
         "//subtle:wycheproof_util",
         "//util:secret_data",
+        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@boringssl//:crypto",
+        "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_absl//absl/types:span",
         "@com_google_googletest//:gtest_main",
+        "@rapidjson",
     ],
 )
 
@@ -394,11 +422,11 @@
     deps = [
         ":md_util",
         "//subtle:common_enums",
+        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
-        "@com_google_absl//absl/types:span",
         "@com_google_googletest//:gtest_main",
     ],
 )
@@ -428,6 +456,7 @@
         ":aes_util",
         "//subtle:subtle_util",
         "//util:secret_data",
+        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@boringssl//:crypto",
@@ -443,12 +472,16 @@
     hdrs = ["monitoring_util.h"],
     include_prefix = "tink/internal",
     deps = [
+        ":key_status_util",
+        "//:key_status",
         "//:primitive_set",
         "//monitoring",
         "//proto:tink_cc_proto",
+        "//util:status",
         "//util:statusor",
         "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
     ],
 )
 
@@ -457,12 +490,718 @@
     srcs = ["monitoring_util_test.cc"],
     deps = [
         ":monitoring_util",
+        "//:key_status",
         "//:primitive_set",
         "//monitoring",
         "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
         "//util:test_matchers",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_library(
+    name = "serialization",
+    hdrs = ["serialization.h"],
+    include_prefix = "tink/internal",
+    deps = ["@com_google_absl//absl/strings"],
+)
+
+cc_library(
+    name = "proto_parameters_serialization",
+    srcs = ["proto_parameters_serialization.cc"],
+    hdrs = ["proto_parameters_serialization.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        ":util",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "proto_parameters_serialization_test",
+    srcs = ["proto_parameters_serialization_test.cc"],
+    deps = [
+        ":proto_parameters_serialization",
+        "//proto:test_proto_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "proto_key_serialization",
+    srcs = ["proto_key_serialization.cc"],
+    hdrs = ["proto_key_serialization.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        ":util",
+        "//:restricted_data",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "proto_key_serialization_test",
+    srcs = ["proto_key_serialization_test.cc"],
+    deps = [
+        ":proto_key_serialization",
+        "//:insecure_secret_key_access",
+        "//:restricted_data",
+        "//proto:tink_cc_proto",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "legacy_proto_parameters",
+    srcs = ["legacy_proto_parameters.cc"],
+    hdrs = ["legacy_proto_parameters.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":proto_parameters_serialization",
+        "//:parameters",
+        "//proto:tink_cc_proto",
+    ],
+)
+
+cc_test(
+    name = "legacy_proto_parameters_test",
+    srcs = ["legacy_proto_parameters_test.cc"],
+    deps = [
+        ":legacy_proto_parameters",
+        ":proto_parameters_serialization",
+        "//:parameters",
+        "//proto:test_proto_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "legacy_proto_key",
+    srcs = ["legacy_proto_key.cc"],
+    hdrs = ["legacy_proto_key.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":proto_key_serialization",
+        "//:key",
+        "//:parameters",
+        "//:secret_key_access_token",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "legacy_proto_key_test",
+    srcs = ["legacy_proto_key_test.cc"],
+    deps = [
+        ":legacy_proto_key",
+        ":proto_key_serialization",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:parameters",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//proto:tink_cc_proto",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "parser_index",
+    hdrs = ["parser_index.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "parser_index_test",
+    srcs = ["parser_index_test.cc"],
+    deps = [
+        ":parser_index",
+        ":serialization",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "serializer_index",
+    hdrs = ["serializer_index.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        "//:key",
+        "//:parameters",
+    ],
+)
+
+cc_test(
+    name = "serializer_index_test",
+    srcs = ["serializer_index_test.cc"],
+    deps = [
+        ":serialization_test_util",
+        ":serializer_index",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "parameters_parser",
+    hdrs = ["parameters_parser.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":parser_index",
+        ":serialization",
+        "//:parameters",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "parameters_parser_test",
+    srcs = ["parameters_parser_test.cc"],
+    deps = [
+        ":parameters_parser",
+        ":parser_index",
+        ":serialization",
+        ":serialization_test_util",
+        "//:parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "parameters_serializer",
+    hdrs = ["parameters_serializer.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        ":serializer_index",
+        "//:parameters",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "parameters_serializer_test",
+    srcs = ["parameters_serializer_test.cc"],
+    deps = [
+        ":parameters_serializer",
+        ":serialization",
+        ":serialization_test_util",
+        ":serializer_index",
+        "//:parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "key_parser",
+    hdrs = ["key_parser.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":parser_index",
+        ":serialization",
+        "//:key",
+        "//:secret_key_access_token",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/functional:function_ref",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "key_parser_test",
+    srcs = ["key_parser_test.cc"],
+    deps = [
+        ":key_parser",
+        ":parser_index",
+        ":serialization",
+        ":serialization_test_util",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "key_serializer",
+    hdrs = ["key_serializer.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        ":serializer_index",
+        "//:key",
+        "//:secret_key_access_token",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/functional:function_ref",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "key_serializer_test",
+    srcs = ["key_serializer_test.cc"],
+    deps = [
+        ":key_serializer",
+        ":serialization",
+        ":serialization_test_util",
+        ":serializer_index",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:secret_key_access_token",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "key_status_util",
+    srcs = ["key_status_util.cc"],
+    hdrs = ["key_status_util.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        "//:key_status",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+    ],
+)
+
+cc_test(
+    name = "key_status_util_test",
+    srcs = ["key_status_util_test.cc"],
+    deps = [
+        ":key_status_util",
+        "//:key_status",
+        "//proto:tink_cc_proto",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "keyset_handle_builder_entry",
+    srcs = ["keyset_handle_builder_entry.cc"],
+    hdrs = ["keyset_handle_builder_entry.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":key_status_util",
+        ":legacy_proto_key",
+        ":legacy_proto_parameters",
+        ":mutable_serialization_registry",
+        ":proto_key_serialization",
+        ":proto_parameters_serialization",
+        ":serialization",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:key_status",
+        "//:parameters",
+        "//:registry",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "keyset_handle_builder_entry_test",
+    srcs = ["keyset_handle_builder_entry_test.cc"],
+    deps = [
+        ":keyset_handle_builder_entry",
+        ":legacy_proto_key",
+        ":legacy_proto_parameters",
+        ":proto_key_serialization",
+        ":proto_parameters_serialization",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:key_status",
+        "//:keyset_handle",
+        "//:keyset_handle_builder",
+        "//:parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//config:tink_config",
+        "//mac:aes_cmac_key",
+        "//mac:aes_cmac_parameters",
+        "//mac:mac_key_templates",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "serialization_registry",
+    srcs = ["serialization_registry.cc"],
+    hdrs = ["serialization_registry.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":key_parser",
+        ":key_serializer",
+        ":parameters_parser",
+        ":parameters_serializer",
+        ":parser_index",
+        ":serialization",
+        ":serializer_index",
+        "//:key",
+        "//:parameters",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings:str_format",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "serialization_registry_test",
+    srcs = ["serialization_registry_test.cc"],
+    deps = [
+        ":key_parser",
+        ":key_serializer",
+        ":parameters_parser",
+        ":parameters_serializer",
+        ":serialization",
+        ":serialization_registry",
+        ":serialization_test_util",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:parameters",
+        "//:secret_key_access_token",
+        "//util:status",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "mutable_serialization_registry",
+    srcs = ["mutable_serialization_registry.cc"],
+    hdrs = ["mutable_serialization_registry.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":key_parser",
+        ":key_serializer",
+        ":legacy_proto_key",
+        ":parameters_parser",
+        ":parameters_serializer",
+        ":proto_key_serialization",
+        ":serialization",
+        ":serialization_registry",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:parameters",
+        "//:secret_key_access_token",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/synchronization",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "mutable_serialization_registry_test",
+    srcs = ["mutable_serialization_registry_test.cc"],
+    deps = [
+        ":key_parser",
+        ":key_serializer",
+        ":mutable_serialization_registry",
+        ":parameters_parser",
+        ":parameters_serializer",
+        ":proto_key_serialization",
+        ":serialization",
+        ":serialization_test_util",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:parameters",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "serialization_test_util",
+    testonly = 1,
+    hdrs = ["serialization_test_util.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":serialization",
+        "//:key",
+        "//:parameters",
+        "//:secret_key_access_token",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_test(
+    name = "serialization_test_util_test",
+    srcs = ["serialization_test_util_test.cc"],
+    deps = [
+        ":serialization_test_util",
+        "//:insecure_secret_key_access",
+        "//:parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "test_random_access_stream",
+    testonly = 1,
+    srcs = ["test_random_access_stream.cc"],
+    hdrs = ["test_random_access_stream.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        "//:random_access_stream",
+        "//util:buffer",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "test_random_access_stream_test",
+    srcs = ["test_random_access_stream_test.cc"],
+    deps = [
+        ":test_random_access_stream",
+        "//subtle:random",
+        "//util:buffer",
+        "//util:status",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "configuration_impl",
+    hdrs = ["configuration_impl.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":key_type_info_store",
+        ":keyset_wrapper_store",
+        "//:configuration",
+    ],
+)
+
+cc_test(
+    name = "configuration_impl_test",
+    srcs = ["configuration_impl_test.cc"],
+    deps = [
+        ":configuration_impl",
+        ":keyset_wrapper_store",
+        "//:cleartext_keyset_handle",
+        "//:configuration",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:rsa_ssa_pss_cc_proto",
+        "//subtle:random",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "key_type_info_store",
+    srcs = ["key_type_info_store.cc"],
+    hdrs = ["key_type_info_store.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":fips_utils",
+        "//:core/key_manager_impl",
+        "//:core/key_type_manager",
+        "//:core/private_key_manager_impl",
+        "//:core/private_key_type_manager",
+        "//:key_manager",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "key_type_info_store_test",
+    srcs = ["key_type_info_store_test.cc"],
+    deps = [
+        ":fips_utils",
+        ":key_type_info_store",
+        "//:aead",
+        "//:core/key_manager_impl",
+        "//:key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//aead:cord_aead",
+        "//aead:kms_envelope_aead_key_manager",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:common_cc_proto",
+        "//proto:ecdsa_cc_proto",
+        "//signature:ecdsa_sign_key_manager",
+        "//signature:ecdsa_verify_key_manager",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "keyset_wrapper_store",
+    hdrs = ["keyset_wrapper_store.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":keyset_wrapper",
+        ":keyset_wrapper_impl",
+        "//:primitive_wrapper",
+        "//util:status",
+        "//util:statusor",
+    ],
+)
+
+cc_test(
+    name = "keyset_wrapper_store_test",
+    srcs = ["keyset_wrapper_store_test.cc"],
+    deps = [
+        ":keyset_wrapper_store",
+        ":registry_impl",
+        "//:primitive_set",
+        "//:primitive_wrapper",
+        "//mac:mac_wrapper",
+        "//proto:aes_gcm_cc_proto",
+        "//subtle:random",
+        "//util:status",
+        "//util:statusor",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "key_gen_configuration_impl",
+    hdrs = ["key_gen_configuration_impl.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":key_type_info_store",
+        "//:key_gen_configuration",
+    ],
+)
+
+cc_test(
+    name = "key_gen_configuration_impl_test",
+    srcs = ["key_gen_configuration_impl_test.cc"],
+    deps = [
+        ":key_gen_configuration_impl",
+        "//:key_gen_configuration",
+        "//aead:aead_key_templates",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:rsa_ssa_pss_cc_proto",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/internal/BUILD.gn b/cc/internal/BUILD.gn
index 7e51e25..affe66f 100644
--- a/cc/internal/BUILD.gn
+++ b/cc/internal/BUILD.gn
@@ -26,7 +26,11 @@
     "util.cc",
     "util.h",
   ]
-  public_deps = [ "//third_party/abseil-cpp/absl/strings:strings" ]
+  public_deps = [
+    "//third_party/abseil-cpp/absl/base:core_headers",
+    "//third_party/abseil-cpp/absl/log:log",
+    "//third_party/abseil-cpp/absl/strings:strings",
+  ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
 
@@ -42,6 +46,7 @@
     "//third_party/tink/cc:primitive_set",
     "//third_party/tink/cc:primitive_wrapper",
     "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
     "//third_party/tink/cc/util:validation",
   ]
@@ -79,30 +84,26 @@
   ]
   public_deps = [
     ":fips_utils",
+    ":key_type_info_store",
     ":keyset_wrapper",
-    ":keyset_wrapper_impl",
+    ":keyset_wrapper_store",
     "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
     "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/abseil-cpp/absl/synchronization:synchronization",
-    "//third_party/abseil-cpp/absl/types:optional",
-    "//third_party/tink/cc:catalogue",
-    "//third_party/tink/cc:core/key_manager_impl",
     "//third_party/tink/cc:core/key_type_manager",
-    "//third_party/tink/cc:core/private_key_manager_impl",
     "//third_party/tink/cc:core/private_key_type_manager",
+    "//third_party/tink/cc:input_stream",
     "//third_party/tink/cc:key_manager",
     "//third_party/tink/cc:primitive_set",
     "//third_party/tink/cc:primitive_wrapper",
     "//third_party/tink/cc/monitoring:monitoring",
     "//third_party/tink/cc/proto:tink_proto",
     "//third_party/tink/cc/util:errors",
-    "//third_party/tink/cc/util:protobuf_helper",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
-    "//third_party/tink/cc/util:validation",
   ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
@@ -146,6 +147,8 @@
   ]
   public_deps = [
     ":ssl_unique_ptr",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/abseil-cpp/absl/types:span",
     "//third_party/boringssl:crypto",
     "//third_party/tink/cc/subtle:subtle_util",
@@ -233,12 +236,332 @@
   configs -= [ "//build/config:no_rtti" ]
   sources = [ "monitoring_util.h" ]
   public_deps = [
+    ":key_status_util",
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
     "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/tink/cc:key_status",
     "//third_party/tink/cc:primitive_set",
     "//third_party/tink/cc/monitoring:monitoring",
     "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
   ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
+
+# CC Library : serialization
+source_set("serialization") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "serialization.h" ]
+  public_deps = [ "//third_party/abseil-cpp/absl/strings:strings" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : proto_parameters_serialization
+source_set("proto_parameters_serialization") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "proto_parameters_serialization.cc",
+    "proto_parameters_serialization.h",
+  ]
+  public_deps = [
+    ":serialization",
+    ":util",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : proto_key_serialization
+source_set("proto_key_serialization") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "proto_key_serialization.cc",
+    "proto_key_serialization.h",
+  ]
+  public_deps = [
+    ":serialization",
+    ":util",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:restricted_data",
+    "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : legacy_proto_key
+source_set("legacy_proto_key") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "legacy_proto_key.cc",
+    "legacy_proto_key.h",
+  ]
+  public_deps = [
+    ":proto_key_serialization",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc:parameters",
+    "//third_party/tink/cc:secret_key_access_token",
+    "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : parser_index
+source_set("parser_index") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "parser_index.h" ]
+  public_deps = [
+    ":serialization",
+    "//third_party/abseil-cpp/absl/strings:strings",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : serializer_index
+source_set("serializer_index") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "serializer_index.h" ]
+  public_deps = [
+    ":serialization",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc:parameters",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : parameters_parser
+source_set("parameters_parser") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "parameters_parser.h" ]
+  public_deps = [
+    ":parser_index",
+    ":serialization",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/tink/cc:parameters",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : parameters_serializer
+source_set("parameters_serializer") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "parameters_serializer.h" ]
+  public_deps = [
+    ":serialization",
+    ":serializer_index",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/tink/cc:parameters",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_parser
+source_set("key_parser") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_parser.h" ]
+  public_deps = [
+    ":parser_index",
+    ":serialization",
+    "//third_party/abseil-cpp/absl/functional:function_ref",
+    "//third_party/abseil-cpp/absl/log:log",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc:secret_key_access_token",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_serializer
+source_set("key_serializer") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_serializer.h" ]
+  public_deps = [
+    ":serialization",
+    ":serializer_index",
+    "//third_party/abseil-cpp/absl/functional:function_ref",
+    "//third_party/abseil-cpp/absl/log:log",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc:secret_key_access_token",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_status_util
+source_set("key_status_util") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "key_status_util.cc",
+    "key_status_util.h",
+  ]
+  public_deps = [
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/tink/cc:key_status",
+    "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : serialization_registry
+source_set("serialization_registry") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "serialization_registry.cc",
+    "serialization_registry.h",
+  ]
+  public_deps = [
+    ":key_parser",
+    ":key_serializer",
+    ":parameters_parser",
+    ":parameters_serializer",
+    ":parser_index",
+    ":serialization",
+    ":serializer_index",
+    "//third_party/abseil-cpp/absl/container:flat_hash_map",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:str_format",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc:parameters",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : mutable_serialization_registry
+source_set("mutable_serialization_registry") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "mutable_serialization_registry.cc",
+    "mutable_serialization_registry.h",
+  ]
+  public_deps = [
+    ":key_parser",
+    ":key_serializer",
+    ":legacy_proto_key",
+    ":parameters_parser",
+    ":parameters_serializer",
+    ":proto_key_serialization",
+    ":serialization",
+    ":serialization_registry",
+    "//third_party/abseil-cpp/absl/base:core_headers",
+    "//third_party/abseil-cpp/absl/memory:memory",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/synchronization:synchronization",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:insecure_secret_key_access",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc:parameters",
+    "//third_party/tink/cc:secret_key_access_token",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : configuration_impl
+source_set("configuration_impl") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "configuration_impl.h" ]
+  public_deps = [
+    ":key_type_info_store",
+    ":keyset_wrapper_store",
+    "//third_party/tink/cc:configuration",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_type_info_store
+source_set("key_type_info_store") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "key_type_info_store.cc",
+    "key_type_info_store.h",
+  ]
+  public_deps = [
+    ":fips_utils",
+    "//third_party/abseil-cpp/absl/container:flat_hash_map",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/tink/cc:core/key_manager_impl",
+    "//third_party/tink/cc:core/key_type_manager",
+    "//third_party/tink/cc:core/private_key_manager_impl",
+    "//third_party/tink/cc:core/private_key_type_manager",
+    "//third_party/tink/cc:key_manager",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : keyset_wrapper_store
+source_set("keyset_wrapper_store") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "keyset_wrapper_store.h" ]
+  public_deps = [
+    ":keyset_wrapper",
+    ":keyset_wrapper_impl",
+    "//third_party/tink/cc:primitive_wrapper",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_gen_configuration_impl
+source_set("key_gen_configuration_impl") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_gen_configuration_impl.h" ]
+  public_deps = [
+    ":key_type_info_store",
+    "//third_party/tink/cc:key_gen_configuration",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
diff --git a/cc/internal/CMakeLists.txt b/cc/internal/CMakeLists.txt
index 20e4305..82d2499 100644
--- a/cc/internal/CMakeLists.txt
+++ b/cc/internal/CMakeLists.txt
@@ -24,16 +24,25 @@
     util.cc
     util.h
   DEPS
+    absl::core_headers
+    absl::log
     absl::strings
 )
 
 tink_cc_library(
   NAME test_file_util
   SRCS
+    test_file_util.cc
     test_file_util_cmake.cc
     test_file_util.h
   DEPS
+    absl::check
     absl::strings
+    gmock
+    tink::subtle::random
+    tink::util::status
+    tink::util::test_util
+  TESTONLY
 )
 
 tink_cc_library(
@@ -46,6 +55,7 @@
     absl::flat_hash_map
     tink::core::primitive_set
     tink::core::primitive_wrapper
+    tink::util::status
     tink::util::statusor
     tink::util::validation
     tink::proto::tink_cc_proto
@@ -84,29 +94,25 @@
     registry_impl.h
   DEPS
     tink::internal::fips_utils
+    tink::internal::key_type_info_store
     tink::internal::keyset_wrapper
-    tink::internal::keyset_wrapper_impl
+    tink::internal::keyset_wrapper_store
     absl::core_headers
     absl::flat_hash_map
     absl::memory
     absl::status
     absl::strings
     absl::synchronization
-    absl::optional
-    tink::core::catalogue
-    tink::core::key_manager_impl
     tink::core::key_type_manager
-    tink::core::private_key_manager_impl
     tink::core::private_key_type_manager
+    tink::core::input_stream
     tink::core::key_manager
     tink::core::primitive_set
     tink::core::primitive_wrapper
     tink::monitoring::monitoring
     tink::util::errors
-    tink::util::protobuf_helper
     tink::util::status
     tink::util::statusor
-    tink::util::validation
     tink::proto::tink_cc_proto
 )
 
@@ -152,6 +158,8 @@
     bn_util.h
   DEPS
     tink::internal::ssl_unique_ptr
+    absl::status
+    absl::strings
     absl::span
     crypto
     tink::subtle::subtle_util
@@ -168,9 +176,11 @@
   DEPS
     tink::internal::bn_util
     tink::internal::err_util
+    tink::internal::fips_utils
     tink::internal::ssl_unique_ptr
     tink::internal::ssl_util
     absl::status
+    absl::statusor
     absl::strings
     crypto
     tink::config::tink_fips
@@ -204,6 +214,7 @@
     absl::strings
     tink::core::primitive_set
     tink::core::primitive_wrapper
+    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
     tink::util::test_util
@@ -217,6 +228,7 @@
   DEPS
     tink::internal::key_info
     gmock
+    protobuf::libprotobuf-lite
     tink::proto::tink_cc_proto
 )
 
@@ -225,39 +237,47 @@
   SRCS
     registry_impl_test.cc
   DEPS
+    tink::internal::fips_utils
+    tink::internal::registry_impl
     gmock
     absl::memory
     absl::status
+    absl::statusor
     absl::strings
     crypto
     tink::core::aead
-    tink::core::catalogue
     tink::core::key_manager_impl
     tink::core::key_type_manager
-    tink::core::crypto_format
-    tink::core::keyset_manager
+    tink::core::private_key_manager_impl
+    tink::core::private_key_type_manager
+    tink::core::template_util
+    tink::core::hybrid_decrypt
+    tink::core::input_stream
+    tink::core::key_manager
+    tink::core::mac
+    tink::core::primitive_set
+    tink::core::primitive_wrapper
     tink::core::registry
     tink::aead::aead_wrapper
     tink::aead::aes_gcm_key_manager
-    tink::config::tink_fips
     tink::hybrid::ecies_aead_hkdf_private_key_manager
     tink::hybrid::ecies_aead_hkdf_public_key_manager
-    tink::monitoring::monitoring
     tink::monitoring::monitoring_client_mocks
     tink::subtle::aes_gcm_boringssl
     tink::subtle::random
+    tink::util::input_stream_util
     tink::util::istream_input_stream
     tink::util::protobuf_helper
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
-    tink::util::test_keyset_handle
     tink::util::test_matchers
     tink::util::test_util
     tink::proto::aes_ctr_hmac_aead_cc_proto
     tink::proto::aes_gcm_cc_proto
     tink::proto::common_cc_proto
     tink::proto::ecdsa_cc_proto
+    tink::proto::ecies_aead_hkdf_cc_proto
     tink::proto::tink_cc_proto
 )
 
@@ -281,8 +301,10 @@
     tink::internal::ssl_unique_ptr
     gmock
     absl::strings
+    absl::span
     crypto
     tink::util::secret_data
+    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
 )
@@ -296,9 +318,11 @@
     tink::internal::rsa_util
     tink::internal::ssl_unique_ptr
     gmock
+    absl::status
     absl::strings
     crypto
     tink::subtle::random
+    tink::util::secret_data
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
@@ -330,13 +354,16 @@
     tink::internal::ssl_unique_ptr
     tink::internal::ssl_util
     gmock
+    absl::status
     absl::strings
     absl::span
     crypto
+    rapidjson
     tink::subtle::common_enums
     tink::subtle::subtle_util
     tink::subtle::wycheproof_util
     tink::util::secret_data
+    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
 )
@@ -366,9 +393,9 @@
     tink::internal::md_util
     gmock
     absl::strings
-    absl::span
     crypto
     tink::subtle::common_enums
+    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
 )
@@ -402,6 +429,7 @@
     crypto
     tink::subtle::subtle_util
     tink::util::secret_data
+    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
 )
@@ -411,10 +439,14 @@
   SRCS
     monitoring_util.h
   DEPS
+    tink::internal::key_status_util
     absl::flat_hash_map
     absl::status
+    absl::strings
+    tink::core::key_status
     tink::core::primitive_set
     tink::monitoring::monitoring
+    tink::util::status
     tink::util::statusor
     tink::proto::tink_cc_proto
 )
@@ -426,10 +458,697 @@
   DEPS
     tink::internal::monitoring_util
     gmock
+    absl::flat_hash_map
+    absl::memory
     absl::status
     absl::strings
+    tink::core::key_status
     tink::core::primitive_set
     tink::monitoring::monitoring
+    tink::util::status
+    tink::util::statusor
     tink::util::test_matchers
     tink::proto::tink_cc_proto
 )
+
+tink_cc_library(
+  NAME serialization
+  SRCS
+    serialization.h
+  DEPS
+    absl::strings
+)
+
+tink_cc_library(
+  NAME proto_parameters_serialization
+  SRCS
+    proto_parameters_serialization.cc
+    proto_parameters_serialization.h
+  DEPS
+    tink::internal::serialization
+    tink::internal::util
+    absl::status
+    absl::strings
+    tink::util::status
+    tink::util::statusor
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME proto_parameters_serialization_test
+  SRCS
+    proto_parameters_serialization_test.cc
+  DEPS
+    tink::internal::proto_parameters_serialization
+    gmock
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::test_proto_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME proto_key_serialization
+  SRCS
+    proto_key_serialization.cc
+    proto_key_serialization.h
+  DEPS
+    tink::internal::serialization
+    tink::internal::util
+    absl::status
+    absl::strings
+    absl::optional
+    tink::core::restricted_data
+    tink::util::status
+    tink::util::statusor
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME proto_key_serialization_test
+  SRCS
+    proto_key_serialization_test.cc
+  DEPS
+    tink::internal::proto_key_serialization
+    gmock
+    absl::status
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME legacy_proto_parameters
+  SRCS
+    legacy_proto_parameters.cc
+    legacy_proto_parameters.h
+  DEPS
+    tink::internal::proto_parameters_serialization
+    tink::core::parameters
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME legacy_proto_parameters_test
+  SRCS
+    legacy_proto_parameters_test.cc
+  DEPS
+    tink::internal::legacy_proto_parameters
+    tink::internal::proto_parameters_serialization
+    gmock
+    tink::core::parameters
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::test_proto_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME legacy_proto_key
+  SRCS
+    legacy_proto_key.cc
+    legacy_proto_key.h
+  DEPS
+    tink::internal::proto_key_serialization
+    absl::status
+    absl::strings
+    absl::optional
+    tink::core::key
+    tink::core::parameters
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME legacy_proto_key_test
+  SRCS
+    legacy_proto_key_test.cc
+  DEPS
+    tink::internal::legacy_proto_key
+    tink::internal::proto_key_serialization
+    gmock
+    absl::status
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::parameters
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME parser_index
+  SRCS
+    parser_index.h
+  DEPS
+    tink::internal::serialization
+    absl::strings
+)
+
+tink_cc_test(
+  NAME parser_index_test
+  SRCS
+    parser_index_test.cc
+  DEPS
+    tink::internal::parser_index
+    tink::internal::serialization
+    gmock
+    absl::strings
+)
+
+tink_cc_library(
+  NAME serializer_index
+  SRCS
+    serializer_index.h
+  DEPS
+    tink::internal::serialization
+    tink::core::key
+    tink::core::parameters
+)
+
+tink_cc_test(
+  NAME serializer_index_test
+  SRCS
+    serializer_index_test.cc
+  DEPS
+    tink::internal::serialization_test_util
+    tink::internal::serializer_index
+    gmock
+)
+
+tink_cc_library(
+  NAME parameters_parser
+  SRCS
+    parameters_parser.h
+  DEPS
+    tink::internal::parser_index
+    tink::internal::serialization
+    absl::status
+    absl::strings
+    tink::core::parameters
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME parameters_parser_test
+  SRCS
+    parameters_parser_test.cc
+  DEPS
+    tink::internal::parameters_parser
+    tink::internal::parser_index
+    tink::internal::serialization
+    tink::internal::serialization_test_util
+    gmock
+    absl::memory
+    absl::status
+    tink::core::parameters
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME parameters_serializer
+  SRCS
+    parameters_serializer.h
+  DEPS
+    tink::internal::serialization
+    tink::internal::serializer_index
+    absl::status
+    absl::strings
+    tink::core::parameters
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME parameters_serializer_test
+  SRCS
+    parameters_serializer_test.cc
+  DEPS
+    tink::internal::parameters_serializer
+    tink::internal::serialization
+    tink::internal::serialization_test_util
+    tink::internal::serializer_index
+    gmock
+    absl::memory
+    absl::status
+    tink::core::parameters
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME key_parser
+  SRCS
+    key_parser.h
+  DEPS
+    tink::internal::parser_index
+    tink::internal::serialization
+    absl::function_ref
+    absl::log
+    absl::status
+    absl::strings
+    absl::optional
+    tink::core::key
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME key_parser_test
+  SRCS
+    key_parser_test.cc
+  DEPS
+    tink::internal::key_parser
+    tink::internal::parser_index
+    tink::internal::serialization
+    tink::internal::serialization_test_util
+    gmock
+    absl::memory
+    absl::status
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::parameters
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME key_serializer
+  SRCS
+    key_serializer.h
+  DEPS
+    tink::internal::serialization
+    tink::internal::serializer_index
+    absl::function_ref
+    absl::log
+    absl::status
+    absl::optional
+    tink::core::key
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME key_serializer_test
+  SRCS
+    key_serializer_test.cc
+  DEPS
+    tink::internal::key_serializer
+    tink::internal::serialization
+    tink::internal::serialization_test_util
+    tink::internal::serializer_index
+    gmock
+    absl::memory
+    absl::status
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::secret_key_access_token
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME key_status_util
+  SRCS
+    key_status_util.cc
+    key_status_util.h
+  DEPS
+    absl::status
+    tink::core::key_status
+    tink::util::status
+    tink::util::statusor
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME key_status_util_test
+  SRCS
+    key_status_util_test.cc
+  DEPS
+    tink::internal::key_status_util
+    gmock
+    absl::status
+    tink::core::key_status
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME keyset_handle_builder_entry
+  SRCS
+    keyset_handle_builder_entry.cc
+    keyset_handle_builder_entry.h
+  DEPS
+    tink::internal::key_status_util
+    tink::internal::legacy_proto_key
+    tink::internal::legacy_proto_parameters
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::internal::serialization
+    absl::status
+    absl::strings
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::key_status
+    tink::core::parameters
+    tink::core::registry
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME keyset_handle_builder_entry_test
+  SRCS
+    keyset_handle_builder_entry_test.cc
+  DEPS
+    tink::internal::keyset_handle_builder_entry
+    tink::internal::legacy_proto_key
+    tink::internal::legacy_proto_parameters
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    gmock
+    absl::memory
+    absl::status
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::key_status
+    tink::core::keyset_handle
+    tink::core::keyset_handle_builder
+    tink::core::parameters
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::config::tink_config
+    tink::mac::aes_cmac_key
+    tink::mac::aes_cmac_parameters
+    tink::mac::mac_key_templates
+    tink::util::status
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME serialization_registry
+  SRCS
+    serialization_registry.cc
+    serialization_registry.h
+  DEPS
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::parser_index
+    tink::internal::serialization
+    tink::internal::serializer_index
+    absl::flat_hash_map
+    absl::status
+    absl::str_format
+    absl::optional
+    tink::core::key
+    tink::core::parameters
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME serialization_registry_test
+  SRCS
+    serialization_registry_test.cc
+  DEPS
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::serialization
+    tink::internal::serialization_registry
+    tink::internal::serialization_test_util
+    gmock
+    absl::status
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::parameters
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME mutable_serialization_registry
+  SRCS
+    mutable_serialization_registry.cc
+    mutable_serialization_registry.h
+  DEPS
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::legacy_proto_key
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::serialization
+    tink::internal::serialization_registry
+    absl::core_headers
+    absl::memory
+    absl::status
+    absl::synchronization
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::parameters
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME mutable_serialization_registry_test
+  SRCS
+    mutable_serialization_registry_test.cc
+  DEPS
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::mutable_serialization_registry
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::serialization
+    tink::internal::serialization_test_util
+    gmock
+    absl::status
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::parameters
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::util::status
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME serialization_test_util
+  SRCS
+    serialization_test_util.h
+  DEPS
+    tink::internal::serialization
+    absl::strings
+    absl::optional
+    tink::core::key
+    tink::core::parameters
+    tink::core::secret_key_access_token
+    tink::util::statusor
+  TESTONLY
+)
+
+tink_cc_test(
+  NAME serialization_test_util_test
+  SRCS
+    serialization_test_util_test.cc
+  DEPS
+    tink::internal::serialization_test_util
+    gmock
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::parameters
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME test_random_access_stream
+  SRCS
+    test_random_access_stream.cc
+    test_random_access_stream.h
+  DEPS
+    absl::status
+    absl::strings
+    tink::core::random_access_stream
+    tink::util::buffer
+    tink::util::status
+    tink::util::statusor
+  TESTONLY
+)
+
+tink_cc_test(
+  NAME test_random_access_stream_test
+  SRCS
+    test_random_access_stream_test.cc
+  DEPS
+    tink::internal::test_random_access_stream
+    gmock
+    absl::status
+    tink::subtle::random
+    tink::util::buffer
+    tink::util::status
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME configuration_impl
+  SRCS
+    configuration_impl.h
+  DEPS
+    tink::internal::key_type_info_store
+    tink::internal::keyset_wrapper_store
+    tink::core::configuration
+)
+
+tink_cc_test(
+  NAME configuration_impl_test
+  SRCS
+    configuration_impl_test.cc
+  DEPS
+    tink::internal::configuration_impl
+    tink::internal::keyset_wrapper_store
+    gmock
+    absl::status
+    tink::core::cleartext_keyset_handle
+    tink::core::configuration
+    tink::subtle::random
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::rsa_ssa_pss_cc_proto
+)
+
+tink_cc_library(
+  NAME key_type_info_store
+  SRCS
+    key_type_info_store.cc
+    key_type_info_store.h
+  DEPS
+    tink::internal::fips_utils
+    absl::flat_hash_map
+    absl::status
+    absl::strings
+    tink::core::key_manager_impl
+    tink::core::key_type_manager
+    tink::core::private_key_manager_impl
+    tink::core::private_key_type_manager
+    tink::core::key_manager
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME key_type_info_store_test
+  SRCS
+    key_type_info_store_test.cc
+  DEPS
+    tink::internal::fips_utils
+    tink::internal::key_type_info_store
+    gmock
+    absl::status
+    absl::optional
+    tink::core::aead
+    tink::core::key_manager_impl
+    tink::core::key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::aead::cord_aead
+    tink::aead::kms_envelope_aead_key_manager
+    tink::signature::ecdsa_sign_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::util::test_matchers
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::common_cc_proto
+    tink::proto::ecdsa_cc_proto
+)
+
+tink_cc_library(
+  NAME keyset_wrapper_store
+  SRCS
+    keyset_wrapper_store.h
+  DEPS
+    tink::internal::keyset_wrapper
+    tink::internal::keyset_wrapper_impl
+    tink::core::primitive_wrapper
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME keyset_wrapper_store_test
+  SRCS
+    keyset_wrapper_store_test.cc
+  DEPS
+    tink::internal::keyset_wrapper_store
+    tink::internal::registry_impl
+    gmock
+    absl::status
+    tink::core::primitive_set
+    tink::core::primitive_wrapper
+    tink::mac::mac_wrapper
+    tink::subtle::random
+    tink::util::status
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_gcm_cc_proto
+)
+
+tink_cc_library(
+  NAME key_gen_configuration_impl
+  SRCS
+    key_gen_configuration_impl.h
+  DEPS
+    tink::internal::key_type_info_store
+    tink::core::key_gen_configuration
+)
+
+tink_cc_test(
+  NAME key_gen_configuration_impl_test
+  SRCS
+    key_gen_configuration_impl_test.cc
+  DEPS
+    tink::internal::key_gen_configuration_impl
+    gmock
+    tink::core::key_gen_configuration
+    tink::aead::aead_key_templates
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::rsa_ssa_pss_cc_proto
+)
diff --git a/cc/internal/aes_util.cc b/cc/internal/aes_util.cc
index d7d8c29..55af17b 100644
--- a/cc/internal/aes_util.cc
+++ b/cc/internal/aes_util.cc
@@ -16,6 +16,7 @@
 #include "tink/internal/aes_util.h"
 
 #include <cstdint>
+#include <string>
 #include <vector>
 
 #include "absl/status/status.h"
@@ -24,6 +25,7 @@
 #include "absl/types/span.h"
 #include "openssl/aes.h"
 #include "openssl/evp.h"
+#include "tink/util/statusor.h"
 #ifndef OPENSSL_IS_BORINGSSL
 // This is needed to use block128_f, which is necessary when OpenSSL is used.
 #include "openssl/modes.h"
diff --git a/cc/internal/aes_util_test.cc b/cc/internal/aes_util_test.cc
index d28a813..806a49b 100644
--- a/cc/internal/aes_util_test.cc
+++ b/cc/internal/aes_util_test.cc
@@ -18,7 +18,6 @@
 #include <algorithm>
 #include <cstdint>
 #include <string>
-#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -30,6 +29,7 @@
 #include "openssl/evp.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/secret_data.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 
diff --git a/cc/internal/bn_util.cc b/cc/internal/bn_util.cc
index 22a2513..0d73ba8 100644
--- a/cc/internal/bn_util.cc
+++ b/cc/internal/bn_util.cc
@@ -15,15 +15,21 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/bn_util.h"
 
+#include <stddef.h>
+
+#include <memory>
 #include <string>
 #include <utility>
 
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "openssl/bn.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
+#include "tink/util/statusor.h"
 
 namespace crypto {
 namespace tink {
@@ -34,6 +40,10 @@
   if (bignum == nullptr) {
     return util::Status(absl::StatusCode::kInvalidArgument, "BIGNUM is NULL");
   }
+  if (BN_is_negative(bignum)) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Value must not be negative");
+  }
 
   // BN_bn2binpad returns the length of the buffer on success and -1 on failure.
   int len = BN_bn2binpad(
diff --git a/cc/internal/bn_util.h b/cc/internal/bn_util.h
index 8eea532..fce7eda 100644
--- a/cc/internal/bn_util.h
+++ b/cc/internal/bn_util.h
@@ -16,11 +16,16 @@
 #ifndef TINK_INTERNAL_BN_UTIL_H_
 #define TINK_INTERNAL_BN_UTIL_H_
 
+#include <stddef.h>
+
 #include <string>
 
+#include "absl/strings/string_view.h"
+#include "absl/types/span.h"
 #include "openssl/bn.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/util/secret_data.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
 namespace crypto {
diff --git a/cc/internal/bn_util_test.cc b/cc/internal/bn_util_test.cc
index 161cbe6..7b6fd5c 100644
--- a/cc/internal/bn_util_test.cc
+++ b/cc/internal/bn_util_test.cc
@@ -15,6 +15,9 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/bn_util.h"
 
+#include <stddef.h>
+
+#include <memory>
 #include <string>
 #include <vector>
 
@@ -22,9 +25,12 @@
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/span.h"
 #include "openssl/bn.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/util/secret_data.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 
@@ -59,6 +65,21 @@
   }
 }
 
+TEST(StringToBignum, IgnoresLeadingZeros) {
+  std::string encoded = absl::HexStringToBytes("0102");
+  std::string encoded_with_leading_zeros = absl::HexStringToBytes("0000000102");
+
+  util::StatusOr<internal::SslUniquePtr<BIGNUM>> num =
+      StringToBignum(encoded);
+  ASSERT_THAT(num, IsOk());
+
+  util::StatusOr<internal::SslUniquePtr<BIGNUM>> num2 =
+      StringToBignum(encoded_with_leading_zeros);
+  ASSERT_THAT(num2, IsOk());
+
+  EXPECT_EQ(BN_cmp(num2->get(), num->get()), 0);
+}
+
 TEST(BnUtil, BignumToString) {
   std::vector<std::string> bn_strs = {"0000000000000000", "0000000000000001",
                                       "1000000000000000", "ffffffffffffffff",
@@ -75,6 +96,94 @@
   }
 }
 
+TEST(BignumToStringWithBNNumBytes, NoLeadingZeros) {
+  {
+    util::StatusOr<internal::SslUniquePtr<BIGNUM>> bn0 =
+      StringToBignum(absl::HexStringToBytes("000000"));
+    ASSERT_THAT(bn0, IsOk());
+
+    util::StatusOr<std::string> encoded0 =
+        internal::BignumToString(bn0->get(), BN_num_bytes(bn0->get()));
+    ASSERT_THAT(encoded0, IsOk());
+    EXPECT_EQ(*encoded0, absl::HexStringToBytes(""));
+  }
+
+  {
+    util::StatusOr<internal::SslUniquePtr<BIGNUM>> bn127 =
+      StringToBignum(absl::HexStringToBytes("00007F"));
+    ASSERT_THAT(bn127, IsOk());
+
+    util::StatusOr<std::string> encoded127 =
+        internal::BignumToString(bn127->get(), BN_num_bytes(bn127->get()));
+    ASSERT_THAT(encoded127, IsOk());
+    EXPECT_EQ(*encoded127, absl::HexStringToBytes("7F"));
+  }
+
+  {
+    util::StatusOr<internal::SslUniquePtr<BIGNUM>> bn128 =
+        StringToBignum(absl::HexStringToBytes("000080"));
+    ASSERT_THAT(bn128, IsOk());
+
+    util::StatusOr<std::string> encoded128 =
+        internal::BignumToString(bn128->get(), BN_num_bytes(bn128->get()));
+    ASSERT_THAT(encoded128, IsOk());
+    EXPECT_EQ(*encoded128, absl::HexStringToBytes("80"));
+  }
+
+  {
+    util::StatusOr<internal::SslUniquePtr<BIGNUM>> bn255 =
+      StringToBignum(absl::HexStringToBytes("0000FF"));
+    ASSERT_THAT(bn255, IsOk());
+
+      util::StatusOr<std::string> encoded255 =
+        internal::BignumToString(bn255->get(), BN_num_bytes(bn255->get()));
+    ASSERT_THAT(encoded255, IsOk());
+    EXPECT_EQ(*encoded255, absl::HexStringToBytes("FF"));
+  }
+
+  {
+    util::StatusOr<internal::SslUniquePtr<BIGNUM>> bn256 =
+      StringToBignum(absl::HexStringToBytes("000100"));
+    ASSERT_THAT(bn256, IsOk());
+
+    util::StatusOr<std::string> encoded256 =
+        internal::BignumToString(bn256->get(), BN_num_bytes(bn256->get()));
+    ASSERT_THAT(encoded256, IsOk());
+    EXPECT_EQ(*encoded256, absl::HexStringToBytes("0100"));
+  }
+}
+
+
+TEST(BignumToString, PadsWithLeadingZeros) {
+  util::StatusOr<internal::SslUniquePtr<BIGNUM>> num =
+      StringToBignum(absl::HexStringToBytes("0102"));
+  ASSERT_THAT(num, IsOk());
+
+  util::StatusOr<std::string> encoded =
+      BignumToString(num->get(), /*len=*/ 2);
+  ASSERT_THAT(encoded, IsOk());
+  EXPECT_EQ(*encoded, absl::HexStringToBytes("0102"));
+
+  util::StatusOr<std::string> encodedWithPadding =
+      BignumToString(num->get(), /*len=*/ 5);
+  ASSERT_THAT(encodedWithPadding, IsOk());
+  EXPECT_EQ(*encodedWithPadding, absl::HexStringToBytes("0000000102"));
+
+  // try to encode with a value for len that is too short.
+  ASSERT_THAT(BignumToString(num->get(), /*len=*/1), Not(IsOk()));
+}
+
+TEST(BignumToString, RejectsNegativeNumbers) {
+  // create a negative BIGNUM
+  util::StatusOr<internal::SslUniquePtr<BIGNUM>> number = HexToBignum("01");
+  ASSERT_THAT(number, IsOk());
+  BN_set_negative(number->get(), 1);
+  // Check that number is negative
+  ASSERT_EQ(CompareBignumWithWord(number->get(), /*word=*/0), -1);
+
+  ASSERT_THAT(BignumToString(number->get(), /*len=*/2), Not(IsOk()));
+}
+
 TEST(BnUtil, BignumToSecretData) {
   std::vector<std::string> bn_strs = {"0000000000000000", "0000000000000001",
                                       "1000000000000000", "ffffffffffffffff",
diff --git a/cc/internal/configuration_impl.h b/cc/internal/configuration_impl.h
new file mode 100644
index 0000000..cf59e10
--- /dev/null
+++ b/cc/internal/configuration_impl.h
@@ -0,0 +1,141 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_CONFIGURATION_IMPL_H_
+#define TINK_INTERNAL_CONFIGURATION_IMPL_H_
+
+#include "tink/configuration.h"
+#include "tink/internal/key_type_info_store.h"
+#include "tink/internal/keyset_wrapper_store.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+constexpr absl::string_view kConfigurationImplErr =
+    "Use crypto::tink::Registry instead when in global registry mode.";
+
+class ConfigurationImpl {
+ public:
+  template <class PW>
+  static crypto::tink::util::Status AddPrimitiveWrapper(
+      std::unique_ptr<PW> wrapper, crypto::tink::Configuration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kConfigurationImplErr);
+    }
+
+    // Function `primitive_getter` must be defined here, since
+    // PW::InputPrimitive is not accessible later.
+    // TODO(b/284084337): Move primitive getter out of key manager.
+    std::function<crypto::tink::util::StatusOr<
+        std::unique_ptr<typename PW::InputPrimitive>>(
+        const google::crypto::tink::KeyData& key_data)>
+        primitive_getter =
+            [&config](const google::crypto::tink::KeyData& key_data)
+        -> crypto::tink::util::StatusOr<
+            std::unique_ptr<typename PW::InputPrimitive>> {
+      crypto::tink::util::StatusOr<
+          const crypto::tink::internal::KeyTypeInfoStore::Info*>
+          info = config.key_type_info_store_.Get(key_data.type_url());
+      if (!info.ok()) {
+        return info.status();
+      }
+
+      crypto::tink::util::StatusOr<
+          const crypto::tink::KeyManager<typename PW::InputPrimitive>*>
+          key_manager = (*info)->get_key_manager<typename PW::InputPrimitive>(
+              key_data.type_url());
+      if (!key_manager.ok()) {
+        return key_manager.status();
+      }
+
+      return (*key_manager)->GetPrimitive(key_data);
+    };
+
+    return config.keyset_wrapper_store_
+        .Add<typename PW::InputPrimitive, typename PW::Primitive>(
+            std::move(wrapper), primitive_getter);
+  }
+
+  template <class KM>
+  static crypto::tink::util::Status AddKeyTypeManager(
+      std::unique_ptr<KM> key_manager, crypto::tink::Configuration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kConfigurationImplErr);
+    }
+    return config.key_type_info_store_.AddKeyTypeManager(
+        std::move(key_manager), /*new_key_allowed=*/true);
+  }
+
+  template <class PrivateKM, class PublicKM>
+  static crypto::tink::util::Status AddAsymmetricKeyManagers(
+      std::unique_ptr<PrivateKM> private_key_manager,
+      std::unique_ptr<PublicKM> public_key_manager,
+      crypto::tink::Configuration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kConfigurationImplErr);
+    }
+    return config.key_type_info_store_.AddAsymmetricKeyTypeManagers(
+        std::move(private_key_manager), std::move(public_key_manager),
+        /*new_key_allowed=*/true);
+  }
+
+  static crypto::tink::util::StatusOr<
+      const crypto::tink::internal::KeyTypeInfoStore*>
+  GetKeyTypeInfoStore(const crypto::tink::Configuration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kConfigurationImplErr);
+    }
+    return &config.key_type_info_store_;
+  }
+
+  static crypto::tink::util::StatusOr<
+      const crypto::tink::internal::KeysetWrapperStore*>
+  GetKeysetWrapperStore(const crypto::tink::Configuration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kConfigurationImplErr);
+    }
+    return &config.keyset_wrapper_store_;
+  }
+
+  // `config` can be set to global registry mode only if empty.
+  static crypto::tink::util::Status SetGlobalRegistryMode(
+      crypto::tink::Configuration& config) {
+    if (!config.key_type_info_store_.IsEmpty() ||
+        !config.keyset_wrapper_store_.IsEmpty()) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        "Using the global registry is only "
+                                        "allowed when Configuration is empty.");
+    }
+    config.global_registry_mode_ = true;
+    return crypto::tink::util::OkStatus();
+  }
+
+  static bool GetGlobalRegistryMode(const crypto::tink::Configuration& config) {
+    return config.global_registry_mode_;
+  }
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_CONFIGURATION_IMPL_H_
diff --git a/cc/internal/configuration_impl_test.cc b/cc/internal/configuration_impl_test.cc
new file mode 100644
index 0000000..0b33108
--- /dev/null
+++ b/cc/internal/configuration_impl_test.cc
@@ -0,0 +1,466 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/configuration_impl.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/configuration.h"
+#include "tink/internal/keyset_wrapper_store.h"
+#include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/rsa_ssa_pss.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::OutputPrefixType;
+using ::google::crypto::tink::RsaSsaPssKeyFormat;
+using ::google::crypto::tink::RsaSsaPssParams;
+using ::google::crypto::tink::RsaSsaPssPrivateKey;
+using ::google::crypto::tink::RsaSsaPssPublicKey;
+
+class FakePrimitive {
+ public:
+  explicit FakePrimitive(std::string s) : s_(s) {}
+  std::string get() { return s_; }
+
+ private:
+  std::string s_;
+};
+
+class FakePrimitive2 {
+ public:
+  explicit FakePrimitive2(std::string s) : s_(s) {}
+  std::string get() { return s_ + "2"; }
+
+ private:
+  std::string s_;
+};
+
+// Transforms AesGcmKey into FakePrimitive.
+class FakeKeyTypeManager
+    : public KeyTypeManager<AesGcmKey, AesGcmKeyFormat, List<FakePrimitive>> {
+ public:
+  class FakePrimitiveFactory : public PrimitiveFactory<FakePrimitive> {
+   public:
+    util::StatusOr<std::unique_ptr<FakePrimitive>> Create(
+        const AesGcmKey& key) const override {
+      return absl::make_unique<FakePrimitive>(key.key_value());
+    }
+  };
+
+  FakeKeyTypeManager()
+      : KeyTypeManager(absl::make_unique<FakePrimitiveFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::SYMMETRIC;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const AesGcmKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateKeyFormat(
+      const AesGcmKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  util::StatusOr<AesGcmKey> CreateKey(
+      const AesGcmKeyFormat& key_format) const override {
+    return AesGcmKey();
+  }
+
+  util::StatusOr<AesGcmKey> DeriveKey(
+      const AesGcmKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return AesGcmKey();
+  }
+
+ private:
+  const std::string key_type_ =
+      "type.googleapis.com/google.crypto.tink.AesGcmKey";
+};
+
+// Transforms FakePrimitive into FakePrimitive.
+class FakePrimitiveWrapper
+    : public PrimitiveWrapper<FakePrimitive, FakePrimitive> {
+ public:
+  util::StatusOr<std::unique_ptr<FakePrimitive>> Wrap(
+      std::unique_ptr<PrimitiveSet<FakePrimitive>> primitive_set)
+      const override {
+    return absl::make_unique<FakePrimitive>(
+        primitive_set->get_primary()->get_primitive().get());
+  }
+};
+
+// Transforms FakePrimitive2 into FakePrimitive.
+class FakePrimitiveWrapper2
+    : public PrimitiveWrapper<FakePrimitive2, FakePrimitive> {
+ public:
+  util::StatusOr<std::unique_ptr<FakePrimitive>> Wrap(
+      std::unique_ptr<PrimitiveSet<FakePrimitive2>> primitive_set)
+      const override {
+    return absl::make_unique<FakePrimitive>(
+        primitive_set->get_primary()->get_primitive().get());
+  }
+};
+
+std::string AddAesGcmKeyToKeyset(Keyset& keyset, uint32_t key_id,
+                                 OutputPrefixType output_prefix_type,
+                                 KeyStatusType key_status_type) {
+  AesGcmKey key;
+  key.set_version(0);
+  key.set_key_value(subtle::Random::GetRandomBytes(16));
+  KeyData key_data;
+  key_data.set_value(key.SerializeAsString());
+  key_data.set_type_url("type.googleapis.com/google.crypto.tink.AesGcmKey");
+  test::AddKeyData(key_data, key_id, output_prefix_type, key_status_type,
+                   &keyset);
+  return key.key_value();
+}
+
+TEST(ConfigurationImplTest, AddPrimitiveWrapper) {
+  Configuration config;
+  EXPECT_THAT((ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper>(), config)),
+              IsOk());
+}
+
+TEST(ConfigurationImplTest, AddKeyTypeManager) {
+  Configuration config;
+  EXPECT_THAT(ConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+}
+
+TEST(ConfigurationImplTest, GetKeyTypeInfoStore) {
+  Configuration config;
+  ASSERT_THAT(ConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+
+  std::string type_url = FakeKeyTypeManager().get_key_type();
+  util::StatusOr<const KeyTypeInfoStore*> store =
+      ConfigurationImpl::GetKeyTypeInfoStore(config);
+  ASSERT_THAT(store, IsOk());
+  util::StatusOr<const KeyTypeInfoStore::Info*> info = (*store)->Get(type_url);
+  ASSERT_THAT(info, IsOk());
+
+  util::StatusOr<const KeyManager<FakePrimitive>*> key_manager =
+      (*info)->get_key_manager<FakePrimitive>(type_url);
+  ASSERT_THAT(key_manager, IsOk());
+  EXPECT_EQ((*key_manager)->get_key_type(), type_url);
+}
+
+TEST(ConfigurationImplTest, GetKeyTypeInfoStoreMissingInfoFails) {
+  Configuration config;
+  util::StatusOr<const KeyTypeInfoStore*> store =
+      ConfigurationImpl::GetKeyTypeInfoStore(config);
+  ASSERT_THAT(store, IsOk());
+  EXPECT_THAT((*store)->Get("i.do.not.exist").status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(ConfigurationImplTest, GetKeysetWrapperStoreAndWrap) {
+  Configuration config;
+  ASSERT_THAT((ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper>(), config)),
+              IsOk());
+  ASSERT_THAT(ConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+
+  util::StatusOr<const KeysetWrapperStore*> store =
+      ConfigurationImpl::GetKeysetWrapperStore(config);
+  ASSERT_THAT(store, IsOk());
+  util::StatusOr<const KeysetWrapper<FakePrimitive>*> wrapper =
+      (*store)->Get<FakePrimitive>();
+  ASSERT_THAT(wrapper, IsOk());
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(
+      keyset, /*key_id=*/13, OutputPrefixType::TINK, KeyStatusType::ENABLED);
+  keyset.set_primary_key_id(13);
+
+  util::StatusOr<std::unique_ptr<FakePrimitive>> aead =
+      (*wrapper)->Wrap(keyset, /*annotations=*/{});
+  ASSERT_THAT(aead, IsOk());
+  EXPECT_EQ((*aead)->get(), raw_key);
+}
+
+TEST(ConfigurationImplTest, KeysetWrapperWrapMissingKeyTypeInfoFails) {
+  Configuration config;
+  ASSERT_THAT(ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper>(), config),
+              IsOk());
+
+  util::StatusOr<const KeysetWrapperStore*> store =
+      ConfigurationImpl::GetKeysetWrapperStore(config);
+  ASSERT_THAT(store, IsOk());
+  util::StatusOr<const KeysetWrapper<FakePrimitive>*> wrapper =
+      (*store)->Get<FakePrimitive>();
+  ASSERT_THAT(wrapper, IsOk());
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(
+      keyset, /*key_id=*/13, OutputPrefixType::TINK, KeyStatusType::ENABLED);
+  keyset.set_primary_key_id(13);
+
+  EXPECT_THAT((*wrapper)->Wrap(keyset, /*annotations=*/{}).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(ConfigurationImplTest, KeysetWrapperWrapMissingKeyManagerFails) {
+  Configuration config;
+  // Transforms FakePrimitive2 to FakePrimitive.
+  ASSERT_THAT((ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper2>(), config)),
+              IsOk());
+  // Transforms KeyData to FakePrimitive.
+  ASSERT_THAT(ConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+
+  // AesGcmKey KeyData -> FakePrimitive2 -> FakePrimitive is the success path,
+  // but the AesGcmKey KeyData -> FakePrimitive2 transformation is not
+  // registered.
+  util::StatusOr<const KeysetWrapperStore*> store =
+      ConfigurationImpl::GetKeysetWrapperStore(config);
+  ASSERT_THAT(store, IsOk());
+  util::StatusOr<const KeysetWrapper<FakePrimitive>*> wrapper =
+      (*store)->Get<FakePrimitive>();
+  ASSERT_THAT(wrapper, IsOk());
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(
+      keyset, /*key_id=*/13, OutputPrefixType::TINK, KeyStatusType::ENABLED);
+  keyset.set_primary_key_id(13);
+
+  // FakeKeyTypeManager cannot transform AesGcmKey KeyData -> FakePrimitive2.
+  EXPECT_THAT((*wrapper)->Wrap(keyset, /*annotations=*/{}).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+class FakeSignKeyManager
+    : public PrivateKeyTypeManager<RsaSsaPssPrivateKey, RsaSsaPssKeyFormat,
+                                   RsaSsaPssPublicKey, List<PublicKeySign>> {
+ public:
+  class PublicKeySignFactory : public PrimitiveFactory<PublicKeySign> {
+   public:
+    util::StatusOr<std::unique_ptr<PublicKeySign>> Create(
+        const RsaSsaPssPrivateKey& key) const override {
+      return {absl::make_unique<test::DummyPublicKeySign>("a public key sign")};
+    }
+  };
+
+  explicit FakeSignKeyManager()
+      : PrivateKeyTypeManager(absl::make_unique<PublicKeySignFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::ASYMMETRIC_PRIVATE;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const RsaSsaPssPrivateKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateKeyFormat(
+      const RsaSsaPssKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  util::StatusOr<RsaSsaPssPrivateKey> CreateKey(
+      const RsaSsaPssKeyFormat& key_format) const override {
+    return RsaSsaPssPrivateKey();
+  }
+
+  util::StatusOr<RsaSsaPssPrivateKey> DeriveKey(
+      const RsaSsaPssKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return RsaSsaPssPrivateKey();
+  }
+
+  util::StatusOr<RsaSsaPssPublicKey> GetPublicKey(
+      const RsaSsaPssPrivateKey& private_key) const override {
+    return private_key.public_key();
+  }
+
+ private:
+  const std::string key_type_ = "some.sign.key.type";
+};
+
+class FakeVerifyKeyManager
+    : public KeyTypeManager<RsaSsaPssPublicKey, void, List<PublicKeyVerify>> {
+ public:
+  class PublicKeyVerifyFactory : public PrimitiveFactory<PublicKeyVerify> {
+   public:
+    util::StatusOr<std::unique_ptr<PublicKeyVerify>> Create(
+        const RsaSsaPssPublicKey& key) const override {
+      return {
+          absl::make_unique<test::DummyPublicKeyVerify>("a public key verify")};
+    }
+  };
+
+  explicit FakeVerifyKeyManager()
+      : KeyTypeManager(absl::make_unique<PublicKeyVerifyFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::ASYMMETRIC_PUBLIC;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const RsaSsaPssPublicKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateParams(const RsaSsaPssParams& params) const {
+    return util::OkStatus();
+  }
+
+ private:
+  const std::string key_type_ = "some.verify.key.type";
+};
+
+TEST(ConfigurationImplTest, AddAsymmetricKeyManagers) {
+  Configuration config;
+  EXPECT_THAT(ConfigurationImpl::AddAsymmetricKeyManagers(
+                  absl::make_unique<FakeSignKeyManager>(),
+                  absl::make_unique<FakeVerifyKeyManager>(), config),
+              IsOk());
+}
+
+TEST(ConfigurationImplTest, GetKeyTypeInfoStoreAsymmetric) {
+  Configuration config;
+  ASSERT_THAT(ConfigurationImpl::AddAsymmetricKeyManagers(
+                  absl::make_unique<FakeSignKeyManager>(),
+                  absl::make_unique<FakeVerifyKeyManager>(), config),
+              IsOk());
+
+  {
+    std::string type_url = FakeSignKeyManager().get_key_type();
+    util::StatusOr<const KeyTypeInfoStore*> store =
+        ConfigurationImpl::GetKeyTypeInfoStore(config);
+    ASSERT_THAT(store, IsOk());
+    util::StatusOr<const KeyTypeInfoStore::Info*> info =
+        (*store)->Get(type_url);
+    ASSERT_THAT(info, IsOk());
+
+    util::StatusOr<const KeyManager<PublicKeySign>*> key_manager =
+        (*info)->get_key_manager<PublicKeySign>(type_url);
+    ASSERT_THAT(key_manager, IsOk());
+    EXPECT_EQ((*key_manager)->get_key_type(), type_url);
+  }
+  {
+    std::string type_url = FakeVerifyKeyManager().get_key_type();
+    util::StatusOr<const KeyTypeInfoStore*> store =
+        ConfigurationImpl::GetKeyTypeInfoStore(config);
+    ASSERT_THAT(store, IsOk());
+    util::StatusOr<const KeyTypeInfoStore::Info*> info =
+        (*store)->Get(type_url);
+    ASSERT_THAT(info, IsOk());
+
+    util::StatusOr<const KeyManager<PublicKeyVerify>*> key_manager =
+        (*info)->get_key_manager<PublicKeyVerify>(type_url);
+    ASSERT_THAT(key_manager, IsOk());
+    EXPECT_EQ((*key_manager)->get_key_type(), type_url);
+  }
+}
+
+TEST(ConfigurationImplTest, GlobalRegistryMode) {
+  Registry::Reset();
+  Configuration config;
+  ASSERT_THAT(ConfigurationImpl::SetGlobalRegistryMode(config), IsOk());
+  EXPECT_TRUE(ConfigurationImpl::GetGlobalRegistryMode(config));
+
+  // Check that ConfigurationImpl functions return kFailedPrecondition.
+  EXPECT_THAT(ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper>(), config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(ConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(ConfigurationImpl::AddAsymmetricKeyManagers(
+                  absl::make_unique<FakeSignKeyManager>(),
+                  absl::make_unique<FakeVerifyKeyManager>(), config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(ConfigurationImpl::GetKeyTypeInfoStore(config).status(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(ConfigurationImpl::GetKeysetWrapperStore(config).status(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(
+      keyset, /*key_id=*/13, OutputPrefixType::TINK, KeyStatusType::ENABLED);
+  keyset.set_primary_key_id(13);
+  std::unique_ptr<KeysetHandle> handle =
+      CleartextKeysetHandle::GetKeysetHandle(keyset);
+  // TODO(b/265705174): Replace with GetPrimitive(config) once implemented.
+  EXPECT_THAT(handle->GetPrimitive<FakePrimitive>().status(),
+              StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper>()),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<FakeKeyTypeManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  // TODO(b/265705174): Replace with GetPrimitive(config) once implemented.
+  EXPECT_THAT(handle->GetPrimitive<FakePrimitive>(), IsOk());
+}
+
+TEST(ConfigurationImplTest, GlobalRegistryModeWithNonEmptyConfigFails) {
+  Configuration config;
+  ASSERT_THAT(ConfigurationImpl::AddPrimitiveWrapper(
+                  absl::make_unique<FakePrimitiveWrapper>(), config),
+              IsOk());
+  EXPECT_THAT(ConfigurationImpl::SetGlobalRegistryMode(config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_FALSE(ConfigurationImpl::GetGlobalRegistryMode(config));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/ec_util.cc b/cc/internal/ec_util.cc
index 1dac7b0..f832227 100644
--- a/cc/internal/ec_util.cc
+++ b/cc/internal/ec_util.cc
@@ -21,14 +21,16 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "openssl/bn.h"
 #include "openssl/ec.h"
+#include "openssl/crypto.h"
 #include "openssl/ecdsa.h"
 #include "openssl/evp.h"
 #include "tink/internal/bn_util.h"
diff --git a/cc/internal/ec_util.h b/cc/internal/ec_util.h
index 155cd7b..3a47b36 100644
--- a/cc/internal/ec_util.h
+++ b/cc/internal/ec_util.h
@@ -16,8 +16,12 @@
 #ifndef TINK_INTERNAL_EC_UTIL_H_
 #define TINK_INTERNAL_EC_UTIL_H_
 
+#include <stdint.h>
+
+#include <memory>
 #include <string>
 
+#include "absl/strings/string_view.h"
 #include "openssl/ec.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/common_enums.h"
diff --git a/cc/internal/ec_util_test.cc b/cc/internal/ec_util_test.cc
index 4f4b1ba..5354e9a 100644
--- a/cc/internal/ec_util_test.cc
+++ b/cc/internal/ec_util_test.cc
@@ -15,20 +15,25 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/ec_util.h"
 
+#include <stdint.h>
+
 #include <memory>
 #include <string>
 #include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "absl/status/status.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
+#include "openssl/bn.h"
 #include "openssl/ec.h"
 #include "openssl/ecdsa.h"
 #include "openssl/evp.h"
+#include "include/rapidjson/document.h"
 #include "tink/internal/bn_util.h"
 #include "tink/internal/fips_utils.h"
 #include "tink/internal/ssl_unique_ptr.h"
@@ -37,6 +42,7 @@
 #include "tink/subtle/subtle_util.h"
 #include "tink/subtle/wycheproof_util.h"
 #include "tink/util/secret_data.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 
@@ -425,7 +431,7 @@
   EllipticCurveType curve;
 };
 
-const std::vector<EncodingTestVector> GetEncodingTestVectors() {
+std::vector<EncodingTestVector> GetEncodingTestVectors() {
   return {
       {EcPointFormat::UNCOMPRESSED,
        "00093057fb862f2ad2e82e581baeb3324e7b32946f2ba845a9beeed87d6995f54918ec6"
@@ -826,7 +832,6 @@
   others = ReadEcdhWycheproofTestVectors(
       /*file_name=*/"ecdh_secp521r1_test.json");
   test_vectors.insert(test_vectors.end(), others.begin(), others.end());
-// placeholder_disabled_subtle_test, please ignore
   others = ReadEcdhWycheproofTestVectors(
       /*file_name=*/"ecdh_test.json");
   test_vectors.insert(test_vectors.end(), others.begin(), others.end());
diff --git a/cc/internal/err_util.cc b/cc/internal/err_util.cc
index 3f5c0ad..06dc10d 100644
--- a/cc/internal/err_util.cc
+++ b/cc/internal/err_util.cc
@@ -15,6 +15,8 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/err_util.h"
 
+#include <stddef.h>
+
 #include <string>
 
 #include "openssl/err.h"
diff --git a/cc/internal/err_util_test.cc b/cc/internal/err_util_test.cc
index 6da867f..7343fd4 100644
--- a/cc/internal/err_util_test.cc
+++ b/cc/internal/err_util_test.cc
@@ -16,6 +16,7 @@
 #include "tink/internal/err_util.h"
 
 #include <string>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/internal/fips_utils.cc b/cc/internal/fips_utils.cc
index 2384a26..b08e7c7 100644
--- a/cc/internal/fips_utils.cc
+++ b/cc/internal/fips_utils.cc
@@ -18,8 +18,10 @@
 
 #include <atomic>
 
+#include "absl/base/attributes.h"
 #include "absl/status/status.h"
 #include "openssl/crypto.h"
+#include "tink/util/status.h"
 
 namespace crypto {
 namespace tink {
@@ -37,8 +39,17 @@
 
 void UnSetFipsRestricted() { is_fips_restricted = false; }
 
-crypto::tink::util::Status ChecksFipsCompatibility(
-    FipsCompatibility fips_status) {
+bool IsFipsModeEnabled() { return kUseOnlyFips || is_fips_restricted; }
+
+bool IsFipsEnabledInSsl() {
+#ifdef OPENSSL_IS_BORINGSSL
+  return FIPS_mode();
+#else
+  return false;
+#endif
+}
+
+util::Status ChecksFipsCompatibility(FipsCompatibility fips_status) {
   switch (fips_status) {
     case FipsCompatibility::kNotFips:
       if (IsFipsModeEnabled()) {
@@ -48,7 +59,7 @@
         return util::OkStatus();
       }
     case FipsCompatibility::kRequiresBoringCrypto:
-      if ((IsFipsModeEnabled()) && !FIPS_mode()) {
+      if ((IsFipsModeEnabled()) && !IsFipsEnabledInSsl()) {
         return util::Status(
             absl::StatusCode::kInternal,
             "BoringSSL not built with the BoringCrypto module. If you want to "
@@ -63,8 +74,6 @@
   }
 }
 
-bool IsFipsModeEnabled() { return kUseOnlyFips || is_fips_restricted; }
-
 }  // namespace internal
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/internal/fips_utils.h b/cc/internal/fips_utils.h
index a1867cd..165aa33 100644
--- a/cc/internal/fips_utils.h
+++ b/cc/internal/fips_utils.h
@@ -34,6 +34,9 @@
 // the FIPS restrictions have been enabled at runtime.
 bool IsFipsModeEnabled();
 
+// Returns true if the Ssl layer (BoringSSL or OpenSSL) has FIPS mode enabled.
+bool IsFipsEnabledInSsl();
+
 // Enable FIPS restrictions. If Tink has been built in FIPS mode this is
 // redundant.
 void SetFipsRestricted();
diff --git a/cc/internal/fips_utils_test.cc b/cc/internal/fips_utils_test.cc
index e16f635..c852c30 100644
--- a/cc/internal/fips_utils_test.cc
+++ b/cc/internal/fips_utils_test.cc
@@ -24,7 +24,7 @@
 
 namespace crypto {
 namespace tink {
-
+namespace internal {
 namespace {
 
 using ::crypto::tink::test::IsOk;
@@ -32,58 +32,52 @@
 
 class FipsIncompatible {
  public:
-  static constexpr crypto::tink::internal::FipsCompatibility kFipsStatus =
-      crypto::tink::internal::FipsCompatibility::kNotFips;
+  static constexpr FipsCompatibility kFipsStatus = FipsCompatibility::kNotFips;
 };
 
 class FipsCompatibleWithBoringCrypto {
  public:
-  static constexpr crypto::tink::internal::FipsCompatibility kFipsStatus =
-      crypto::tink::internal::FipsCompatibility::kRequiresBoringCrypto;
+  static constexpr FipsCompatibility kFipsStatus =
+      FipsCompatibility::kRequiresBoringCrypto;
 };
 
 TEST(FipsUtilsTest, CompatibilityInNonFipsMode) {
-  if (internal::kUseOnlyFips) {
+  if (kUseOnlyFips) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
-  EXPECT_THAT(internal::CheckFipsCompatibility<FipsIncompatible>(), IsOk());
-  EXPECT_THAT(
-      internal::CheckFipsCompatibility<FipsCompatibleWithBoringCrypto>(),
-      IsOk());
+  EXPECT_THAT(CheckFipsCompatibility<FipsIncompatible>(), IsOk());
+  EXPECT_THAT(CheckFipsCompatibility<FipsCompatibleWithBoringCrypto>(), IsOk());
 }
 
 TEST(FipsUtilsTest, CompatibilityInFipsMode) {
-  if (!internal::kUseOnlyFips || !FIPS_mode()) {
+  if (!kUseOnlyFips || !IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should only run in FIPS mode with Boringcrypto available.";
   }
 
-  EXPECT_THAT(internal::CheckFipsCompatibility<FipsIncompatible>(),
+  EXPECT_THAT(CheckFipsCompatibility<FipsIncompatible>(),
               StatusIs(absl::StatusCode::kInternal));
-  EXPECT_THAT(
-      internal::CheckFipsCompatibility<FipsCompatibleWithBoringCrypto>(),
-      IsOk());
+  EXPECT_THAT(CheckFipsCompatibility<FipsCompatibleWithBoringCrypto>(), IsOk());
 }
 
 TEST(TinkFipsTest, CompatibilityInFipsModeWithoutBoringCrypto) {
-  if (!internal::kUseOnlyFips || FIPS_mode()) {
+  if (!kUseOnlyFips || IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test only run if BoringCrypto module is not available.";
   }
 
   // In FIPS only mode compatibility checks should disallow algorithms
   // with the FipsCompatibility::kNone flag.
-  EXPECT_THAT(internal::CheckFipsCompatibility<FipsIncompatible>(),
+  EXPECT_THAT(CheckFipsCompatibility<FipsIncompatible>(),
               StatusIs(absl::StatusCode::kInternal));
 
   // FIPS validated implementations are not allowed if BoringCrypto is not
   // available.
-  EXPECT_THAT(
-      internal::CheckFipsCompatibility<FipsCompatibleWithBoringCrypto>(),
-      StatusIs(absl::StatusCode::kInternal));
+  EXPECT_THAT(CheckFipsCompatibility<FipsCompatibleWithBoringCrypto>(),
+              StatusIs(absl::StatusCode::kInternal));
 }
 
 }  // namespace
-
+}  // namespace internal
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/internal/key_gen_configuration_impl.h b/cc/internal/key_gen_configuration_impl.h
new file mode 100644
index 0000000..2d3d602
--- /dev/null
+++ b/cc/internal/key_gen_configuration_impl.h
@@ -0,0 +1,91 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEY_GEN_CONFIGURATION_IMPL_H_
+#define TINK_INTERNAL_KEY_GEN_CONFIGURATION_IMPL_H_
+
+#include "tink/internal/key_type_info_store.h"
+#include "tink/key_gen_configuration.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+constexpr absl::string_view kKeyGenConfigurationImplErr =
+    "Use crypto::tink::Registry instead when in global registry mode.";
+
+class KeyGenConfigurationImpl {
+ public:
+  template <class KM>
+  static crypto::tink::util::Status AddKeyTypeManager(
+      std::unique_ptr<KM> key_manager,
+      crypto::tink::KeyGenConfiguration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kKeyGenConfigurationImplErr);
+    }
+    return config.key_type_info_store_.AddKeyTypeManager(
+        std::move(key_manager), /*new_key_allowed=*/true);
+  }
+
+  template <class PrivateKM, class PublicKM>
+  static crypto::tink::util::Status AddAsymmetricKeyManagers(
+      std::unique_ptr<PrivateKM> private_key_manager,
+      std::unique_ptr<PublicKM> public_key_manager,
+      crypto::tink::KeyGenConfiguration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kKeyGenConfigurationImplErr);
+    }
+    return config.key_type_info_store_.AddAsymmetricKeyTypeManagers(
+        std::move(private_key_manager), std::move(public_key_manager),
+        /*new_key_allowed=*/true);
+  }
+
+  static crypto::tink::util::StatusOr<
+      const crypto::tink::internal::KeyTypeInfoStore*>
+  GetKeyTypeInfoStore(const crypto::tink::KeyGenConfiguration& config) {
+    if (config.global_registry_mode_) {
+      return crypto::tink::util::Status(absl::StatusCode::kFailedPrecondition,
+                                        kKeyGenConfigurationImplErr);
+    }
+    return &config.key_type_info_store_;
+  }
+
+  // `config` can be set to global registry mode only if empty.
+  static crypto::tink::util::Status SetGlobalRegistryMode(
+      crypto::tink::KeyGenConfiguration& config) {
+    if (!config.key_type_info_store_.IsEmpty()) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kFailedPrecondition,
+          "Using the global registry is only allowed when KeyGenConfiguration "
+          "is empty.");
+    }
+    config.global_registry_mode_ = true;
+    return crypto::tink::util::OkStatus();
+  }
+
+  static bool GetGlobalRegistryMode(
+      const crypto::tink::KeyGenConfiguration& config) {
+    return config.global_registry_mode_;
+  }
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEY_GEN_CONFIGURATION_IMPL_H_
diff --git a/cc/internal/key_gen_configuration_impl_test.cc b/cc/internal/key_gen_configuration_impl_test.cc
new file mode 100644
index 0000000..d9a9126
--- /dev/null
+++ b/cc/internal/key_gen_configuration_impl_test.cc
@@ -0,0 +1,312 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_gen_configuration_impl.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/key_gen_configuration.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/rsa_ssa_pss.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::RsaSsaPssKeyFormat;
+using ::google::crypto::tink::RsaSsaPssParams;
+using ::google::crypto::tink::RsaSsaPssPrivateKey;
+using ::google::crypto::tink::RsaSsaPssPublicKey;
+
+class FakePrimitive {
+ public:
+  explicit FakePrimitive(std::string s) : s_(s) {}
+  std::string get() { return s_; }
+
+ private:
+  std::string s_;
+};
+
+class FakeKeyTypeManager
+    : public KeyTypeManager<AesGcmKey, AesGcmKeyFormat, List<FakePrimitive>> {
+ public:
+  class FakePrimitiveFactory : public PrimitiveFactory<FakePrimitive> {
+   public:
+    util::StatusOr<std::unique_ptr<FakePrimitive>> Create(
+        const AesGcmKey& key) const override {
+      return absl::make_unique<FakePrimitive>(key.key_value());
+    }
+  };
+
+  FakeKeyTypeManager()
+      : KeyTypeManager(absl::make_unique<FakePrimitiveFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::SYMMETRIC;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const AesGcmKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateKeyFormat(
+      const AesGcmKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  util::StatusOr<AesGcmKey> CreateKey(
+      const AesGcmKeyFormat& key_format) const override {
+    return AesGcmKey();
+  }
+
+  util::StatusOr<AesGcmKey> DeriveKey(
+      const AesGcmKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return AesGcmKey();
+  }
+
+ private:
+  const std::string key_type_ =
+      "type.googleapis.com/google.crypto.tink.AesGcmKey";
+};
+
+TEST(KeyGenConfigurationImplTest, AddKeyTypeManager) {
+  KeyGenConfiguration config;
+  EXPECT_THAT(KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+}
+
+TEST(KeyGenConfigurationImplTest, GetKeyTypeInfoStore) {
+  KeyGenConfiguration config;
+  ASSERT_THAT(KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+
+  std::string type_url = FakeKeyTypeManager().get_key_type();
+  util::StatusOr<const KeyTypeInfoStore*> store =
+      KeyGenConfigurationImpl::GetKeyTypeInfoStore(config);
+  ASSERT_THAT(store, IsOk());
+  util::StatusOr<const KeyTypeInfoStore::Info*> info = (*store)->Get(type_url);
+  ASSERT_THAT(info, IsOk());
+
+  util::StatusOr<const KeyManager<FakePrimitive>*> key_manager =
+      (*info)->get_key_manager<FakePrimitive>(type_url);
+  ASSERT_THAT(key_manager, IsOk());
+  EXPECT_EQ((*key_manager)->get_key_type(), type_url);
+}
+
+TEST(KeyGenConfigurationImplTest, GetKeyTypeInfoStoreMissingInfoFails) {
+  KeyGenConfiguration config;
+  util::StatusOr<const KeyTypeInfoStore*> store =
+      KeyGenConfigurationImpl::GetKeyTypeInfoStore(config);
+  ASSERT_THAT(store, IsOk());
+  EXPECT_THAT((*store)->Get("i.do.not.exist").status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+class FakeSignKeyManager
+    : public PrivateKeyTypeManager<RsaSsaPssPrivateKey, RsaSsaPssKeyFormat,
+                                   RsaSsaPssPublicKey, List<PublicKeySign>> {
+ public:
+  class PublicKeySignFactory : public PrimitiveFactory<PublicKeySign> {
+   public:
+    util::StatusOr<std::unique_ptr<PublicKeySign>> Create(
+        const RsaSsaPssPrivateKey& key) const override {
+      return {absl::make_unique<test::DummyPublicKeySign>("a public key sign")};
+    }
+  };
+
+  explicit FakeSignKeyManager()
+      : PrivateKeyTypeManager(absl::make_unique<PublicKeySignFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::ASYMMETRIC_PRIVATE;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const RsaSsaPssPrivateKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateKeyFormat(
+      const RsaSsaPssKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  util::StatusOr<RsaSsaPssPrivateKey> CreateKey(
+      const RsaSsaPssKeyFormat& key_format) const override {
+    return RsaSsaPssPrivateKey();
+  }
+
+  util::StatusOr<RsaSsaPssPrivateKey> DeriveKey(
+      const RsaSsaPssKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return RsaSsaPssPrivateKey();
+  }
+
+  util::StatusOr<RsaSsaPssPublicKey> GetPublicKey(
+      const RsaSsaPssPrivateKey& private_key) const override {
+    return private_key.public_key();
+  }
+
+ private:
+  const std::string key_type_ = "some.sign.key.type";
+};
+
+class FakeVerifyKeyManager
+    : public KeyTypeManager<RsaSsaPssPublicKey, void, List<PublicKeyVerify>> {
+ public:
+  class PublicKeyVerifyFactory : public PrimitiveFactory<PublicKeyVerify> {
+   public:
+    util::StatusOr<std::unique_ptr<PublicKeyVerify>> Create(
+        const RsaSsaPssPublicKey& key) const override {
+      return {
+          absl::make_unique<test::DummyPublicKeyVerify>("a public key verify")};
+    }
+  };
+
+  explicit FakeVerifyKeyManager()
+      : KeyTypeManager(absl::make_unique<PublicKeyVerifyFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::ASYMMETRIC_PUBLIC;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const RsaSsaPssPublicKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateParams(const RsaSsaPssParams& params) const {
+    return util::OkStatus();
+  }
+
+ private:
+  const std::string key_type_ = "some.verify.key.type";
+};
+
+TEST(KeyGenConfigurationImplTest, AddAsymmetricKeyManagers) {
+  KeyGenConfiguration config;
+  EXPECT_THAT(KeyGenConfigurationImpl::AddAsymmetricKeyManagers(
+                  absl::make_unique<FakeSignKeyManager>(),
+                  absl::make_unique<FakeVerifyKeyManager>(), config),
+              IsOk());
+}
+
+TEST(KeyGenConfigurationImplTest, GetKeyTypeInfoStoreAsymmetric) {
+  KeyGenConfiguration config;
+  ASSERT_THAT(KeyGenConfigurationImpl::AddAsymmetricKeyManagers(
+                  absl::make_unique<FakeSignKeyManager>(),
+                  absl::make_unique<FakeVerifyKeyManager>(), config),
+              IsOk());
+
+  {
+    std::string type_url = FakeSignKeyManager().get_key_type();
+    util::StatusOr<const KeyTypeInfoStore*> store =
+        KeyGenConfigurationImpl::GetKeyTypeInfoStore(config);
+    ASSERT_THAT(store, IsOk());
+    util::StatusOr<const KeyTypeInfoStore::Info*> info =
+        (*store)->Get(type_url);
+    ASSERT_THAT(info, IsOk());
+
+    util::StatusOr<const KeyManager<PublicKeySign>*> key_manager =
+        (*info)->get_key_manager<PublicKeySign>(type_url);
+    ASSERT_THAT(key_manager, IsOk());
+    EXPECT_EQ((*key_manager)->get_key_type(), type_url);
+  }
+  {
+    std::string type_url = FakeVerifyKeyManager().get_key_type();
+    util::StatusOr<const KeyTypeInfoStore*> store =
+        KeyGenConfigurationImpl::GetKeyTypeInfoStore(config);
+    ASSERT_THAT(store, IsOk());
+    util::StatusOr<const KeyTypeInfoStore::Info*> info =
+        (*store)->Get(type_url);
+    ASSERT_THAT(info, IsOk());
+
+    util::StatusOr<const KeyManager<PublicKeyVerify>*> key_manager =
+        (*info)->get_key_manager<PublicKeyVerify>(type_url);
+    ASSERT_THAT(key_manager, IsOk());
+    EXPECT_EQ((*key_manager)->get_key_type(), type_url);
+  }
+}
+
+TEST(KeyGenConfigurationImplTest, GlobalRegistryMode) {
+  Registry::Reset();
+  KeyGenConfiguration config;
+  ASSERT_THAT(KeyGenConfigurationImpl::SetGlobalRegistryMode(config), IsOk());
+  EXPECT_TRUE(KeyGenConfigurationImpl::GetGlobalRegistryMode(config));
+
+  // Check that KeyGenConfigurationImpl functions return kFailedPrecondition.
+  EXPECT_THAT(KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(KeyGenConfigurationImpl::AddAsymmetricKeyManagers(
+                  absl::make_unique<FakeSignKeyManager>(),
+                  absl::make_unique<FakeVerifyKeyManager>(), config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_THAT(KeyGenConfigurationImpl::GetKeyTypeInfoStore(config).status(),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+
+  // TODO(b/265705174): Replace with KeysetHandle::GenerateNew(config).
+  EXPECT_THAT(Registry::NewKeyData(AeadKeyTemplates::Aes256Gcm()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<FakeKeyTypeManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  // TODO(b/265705174): Replace with KeysetHandle::GenerateNew(config) once
+  // implemented.
+  EXPECT_THAT(Registry::NewKeyData(AeadKeyTemplates::Aes256Gcm()), IsOk());
+}
+
+TEST(KeyGenConfigurationImplTest, GlobalRegistryModeWithNonEmptyConfigFails) {
+  KeyGenConfiguration config;
+  ASSERT_THAT(KeyGenConfigurationImpl::AddKeyTypeManager(
+                  absl::make_unique<FakeKeyTypeManager>(), config),
+              IsOk());
+  EXPECT_THAT(KeyGenConfigurationImpl::SetGlobalRegistryMode(config),
+              StatusIs(absl::StatusCode::kFailedPrecondition));
+  EXPECT_FALSE(KeyGenConfigurationImpl::GetGlobalRegistryMode(config));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/key_info.cc b/cc/internal/key_info.cc
index c864741..04d0c13 100644
--- a/cc/internal/key_info.cc
+++ b/cc/internal/key_info.cc
@@ -16,6 +16,8 @@
 
 #include "tink/internal/key_info.h"
 
+#include "proto/tink.pb.h"
+
 namespace crypto {
 namespace tink {
 
diff --git a/cc/internal/key_parser.h b/cc/internal/key_parser.h
new file mode 100644
index 0000000..c734814
--- /dev/null
+++ b/cc/internal/key_parser.h
@@ -0,0 +1,123 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEY_PARSER_H_
+#define TINK_INTERNAL_KEY_PARSER_H_
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <typeindex>
+#include <utility>
+
+#include "absl/functional/function_ref.h"
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/internal/parser_index.h"
+#include "tink/internal/serialization.h"
+#include "tink/key.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Non-template base class that can be used with internal registry map.
+class KeyParser {
+ public:
+  // Parses a `serialization` into a key.
+  //
+  // This function is usually called on a `Serialization` subclass matching the
+  // value returned by `ObjectIdentifier()`. However, implementations should
+  // check that this is the case.
+  virtual util::StatusOr<std::unique_ptr<Key>> ParseKey(
+      const Serialization& serialization,
+      absl::optional<SecretKeyAccessToken> token) const = 0;
+
+  // Returns the object identifier for `SerializationT`, which is only valid
+  // for the lifetime of this object.
+  //
+  // The object identifier is a unique identifier per registry for this object
+  // (in the standard proto serialization, it is the type URL). In other words,
+  // when registering a `KeyParser`, the registry will invoke this to get
+  // the handled object identifier. In order to parse an object of
+  // `SerializationT`, the registry will then obtain the object identifier of
+  // this serialization object, and call the parser corresponding to this
+  // object.
+  virtual absl::string_view ObjectIdentifier() const = 0;
+
+  // Returns an index that can be used to look up the `KeyParser`
+  // object registered for the `KeyT` type in a registry.
+  virtual ParserIndex Index() const = 0;
+
+  virtual ~KeyParser() = default;
+};
+
+// Parses `SerializationT` objects into `KeyT` objects.
+template <typename SerializationT, typename KeyT>
+class KeyParserImpl : public KeyParser {
+ public:
+  // Creates a key parser with `object_identifier` and parsing `function`. The
+  // referenced `function` should outlive the created key parser object.
+  explicit KeyParserImpl(
+      absl::string_view object_identifier,
+      absl::FunctionRef<util::StatusOr<KeyT>(
+          SerializationT, absl::optional<SecretKeyAccessToken>)>
+          function)
+      : object_identifier_(object_identifier), function_(function) {}
+
+  util::StatusOr<std::unique_ptr<Key>> ParseKey(
+      const Serialization& serialization,
+      absl::optional<SecretKeyAccessToken> token) const override {
+    if (serialization.ObjectIdentifier() != object_identifier_) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Invalid object identifier for this key parser.");
+    }
+    const SerializationT* st =
+        dynamic_cast<const SerializationT*>(&serialization);
+    if (st == nullptr) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Invalid serialization type for this key parser.");
+    }
+    util::StatusOr<KeyT> key = function_(*st, token);
+    if (!key.ok()) return key.status();
+    return {absl::make_unique<KeyT>(std::move(*key))};
+  }
+
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+  ParserIndex Index() const override {
+    return ParserIndex::Create<SerializationT>(object_identifier_);
+  }
+
+ private:
+  std::string object_identifier_;
+  std::function<util::StatusOr<KeyT>(SerializationT,
+                                     absl::optional<SecretKeyAccessToken>)>
+      function_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEY_PARSER_H_
diff --git a/cc/internal/key_parser_test.cc b/cc/internal/key_parser_test.cc
new file mode 100644
index 0000000..c9dac8e
--- /dev/null
+++ b/cc/internal/key_parser_test.cc
@@ -0,0 +1,110 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_parser.h"
+
+#include <memory>
+#include <optional>
+#include <string_view>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/parser_index.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_test_util.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Eq;
+
+TEST(KeyParserTest, Create) {
+  std::unique_ptr<KeyParser> parser =
+      absl::make_unique<KeyParserImpl<NoIdSerialization, NoIdKey>>(
+          kNoIdTypeUrl, ParseNoIdKey);
+
+  EXPECT_THAT(parser->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+  EXPECT_THAT(
+      parser->Index(),
+      Eq(ParserIndex::Create<NoIdSerialization>(kNoIdTypeUrl)));
+}
+
+TEST(KeyParserTest, ParseKey) {
+  std::unique_ptr<KeyParser> parser =
+      absl::make_unique<KeyParserImpl<NoIdSerialization, NoIdKey>>(
+          kNoIdTypeUrl, ParseNoIdKey);
+
+  NoIdSerialization serialization;
+  util::StatusOr<std::unique_ptr<Key>> key =
+      parser->ParseKey(serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key)->GetIdRequirement(), Eq(absl::nullopt));
+  EXPECT_THAT((*key)->GetParameters(), Eq(NoIdParams()));
+  EXPECT_THAT(**key, Eq(NoIdKey()));
+}
+
+TEST(KeyParserTest, ParsePublicKeyNoAccessToken) {
+  std::unique_ptr<KeyParser> parser =
+      absl::make_unique<KeyParserImpl<NoIdSerialization, NoIdKey>>(
+          kNoIdTypeUrl, ParseNoIdKey);
+
+  NoIdSerialization serialization;
+  util::StatusOr<std::unique_ptr<Key>> public_key =
+      parser->ParseKey(serialization, absl::nullopt);
+  ASSERT_THAT(public_key, IsOk());
+  EXPECT_THAT((*public_key)->GetIdRequirement(), Eq(absl::nullopt));
+  EXPECT_THAT((*public_key)->GetParameters(), Eq(NoIdParams()));
+  EXPECT_THAT(**public_key, Eq(NoIdKey()));
+}
+
+TEST(KeyParserTest, ParseKeyWithInvalidSerializationType) {
+  std::unique_ptr<KeyParser> parser =
+      absl::make_unique<KeyParserImpl<NoIdSerialization, NoIdKey>>(
+          "example_type_url", ParseNoIdKey);
+
+  IdKeySerialization serialization(/*id=*/123);
+  util::StatusOr<std::unique_ptr<Key>> key =
+      parser->ParseKey(serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(KeyParserTest, ParseKeyWithInvalidObjectIdentifier) {
+  std::unique_ptr<KeyParser> parser =
+      absl::make_unique<KeyParserImpl<NoIdSerialization, NoIdKey>>(
+          "mismatched_type_url", ParseNoIdKey);
+
+  NoIdSerialization serialization;
+  util::StatusOr<std::unique_ptr<Key>> key =
+      parser->ParseKey(serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/key_serializer.h b/cc/internal/key_serializer.h
new file mode 100644
index 0000000..1d879a6
--- /dev/null
+++ b/cc/internal/key_serializer.h
@@ -0,0 +1,92 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEY_SERIALIZER_H_
+#define TINK_INTERNAL_KEY_SERIALIZER_H_
+
+#include <functional>
+#include <memory>
+#include <typeindex>
+#include <utility>
+
+#include "absl/functional/function_ref.h"
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serializer_index.h"
+#include "tink/key.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Non-template base class that can be used with internal registry map.
+class KeySerializer {
+ public:
+  // Returns the serialization of `key`.
+  virtual util::StatusOr<std::unique_ptr<Serialization>> SerializeKey(
+      const Key& key, absl::optional<SecretKeyAccessToken> token) const = 0;
+
+  // Returns an index that can be used to look up the `KeySerializer`
+  // object registered for the `KeyT` type in a registry.
+  virtual SerializerIndex Index() const = 0;
+
+  virtual ~KeySerializer() = default;
+};
+
+// Serializes `KeyT` objects into `SerializationT` objects.
+template <typename KeyT, typename SerializationT>
+class KeySerializerImpl : public KeySerializer {
+ public:
+  // Creates a key serializer with serialization `function`. The referenced
+  // `function` should outlive the created key serializer object.
+  explicit KeySerializerImpl(absl::FunctionRef<util::StatusOr<SerializationT>(
+                                 KeyT, absl::optional<SecretKeyAccessToken>)>
+                                 function)
+      : function_(function) {}
+
+  util::StatusOr<std::unique_ptr<Serialization>> SerializeKey(
+      const Key& key,
+      absl::optional<SecretKeyAccessToken> token) const override {
+    const KeyT* kt = dynamic_cast<const KeyT*>(&key);
+    if (kt == nullptr) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Invalid key type for this key serializer.");
+    }
+    util::StatusOr<SerializationT> serialization = function_(*kt, token);
+    if (!serialization.ok()) return serialization.status();
+    return {absl::make_unique<SerializationT>(std::move(*serialization))};
+  }
+
+  SerializerIndex Index() const override {
+    return SerializerIndex::Create<KeyT, SerializationT>();
+  }
+
+ private:
+  std::function<util::StatusOr<SerializationT>(
+      KeyT, absl::optional<SecretKeyAccessToken>)>
+      function_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEY_SERIALIZER_H_
diff --git a/cc/internal/key_serializer_test.cc b/cc/internal/key_serializer_test.cc
new file mode 100644
index 0000000..e67257e
--- /dev/null
+++ b/cc/internal/key_serializer_test.cc
@@ -0,0 +1,92 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_serializer.h"
+
+#include <memory>
+#include <string_view>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_test_util.h"
+#include "tink/internal/serializer_index.h"
+#include "tink/key.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Eq;
+
+TEST(KeySerializerTest, Create) {
+  std::unique_ptr<KeySerializer> serializer =
+      absl::make_unique<KeySerializerImpl<NoIdKey, NoIdSerialization>>(
+          SerializeNoIdKey);
+
+  EXPECT_THAT(serializer->Index(),
+              Eq(SerializerIndex::Create<NoIdKey, NoIdSerialization>()));
+}
+
+TEST(KeySerializerTest, SerializeKey) {
+  std::unique_ptr<KeySerializer> serializer =
+      absl::make_unique<KeySerializerImpl<NoIdKey, NoIdSerialization>>(
+          SerializeNoIdKey);
+
+  NoIdKey key;
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      serializer->SerializeKey(key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(KeySerializerTest, SerializePublicKeyNoAccessToken) {
+  std::unique_ptr<KeySerializer> serializer =
+      absl::make_unique<KeySerializerImpl<NoIdKey, NoIdSerialization>>(
+          SerializeNoIdKey);
+
+  NoIdKey public_key;
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      serializer->SerializeKey(public_key, absl::nullopt);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(KeySerializerTest, SerializeKeyWithInvalidKeyType) {
+  std::unique_ptr<KeySerializer> serializer =
+      absl::make_unique<KeySerializerImpl<NoIdKey, NoIdSerialization>>(
+          SerializeNoIdKey);
+
+  IdKey key(/*id=*/123);
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      serializer->SerializeKey(key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/key_status_util.cc b/cc/internal/key_status_util.cc
new file mode 100644
index 0000000..a28f418
--- /dev/null
+++ b/cc/internal/key_status_util.cc
@@ -0,0 +1,76 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_status_util.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "tink/key_status.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::google::crypto::tink::KeyStatusType;
+
+util::StatusOr<KeyStatus> FromKeyStatusType(KeyStatusType status_type) {
+  switch (status_type) {
+    case KeyStatusType::ENABLED:
+      return KeyStatus::kEnabled;
+    case KeyStatusType::DISABLED:
+      return KeyStatus::kDisabled;
+    case KeyStatusType::DESTROYED:
+      return KeyStatus::kDestroyed;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Invalid key status type.");
+  }
+}
+
+util::StatusOr<KeyStatusType> ToKeyStatusType(KeyStatus status) {
+  switch (status) {
+    case KeyStatus::kEnabled:
+      return KeyStatusType::ENABLED;
+    case KeyStatus::kDisabled:
+      return KeyStatusType::DISABLED;
+    case KeyStatus::kDestroyed:
+      return KeyStatusType::DESTROYED;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Invalid key status.");
+  }
+}
+
+std::string ToKeyStatusName(KeyStatus status) {
+  switch (status) {
+    case KeyStatus::kEnabled:
+    return KeyStatusType_Name(KeyStatusType::ENABLED);
+    case KeyStatus::kDisabled:
+    return KeyStatusType_Name(KeyStatusType::DISABLED);
+    case KeyStatus::kDestroyed:
+      return KeyStatusType_Name(KeyStatusType::DESTROYED);
+    default:
+      return KeyStatusType_Name(KeyStatusType::UNKNOWN_STATUS);
+  }
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/key_status_util.h b/cc/internal/key_status_util.h
new file mode 100644
index 0000000..f9736cf
--- /dev/null
+++ b/cc/internal/key_status_util.h
@@ -0,0 +1,48 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEY_STATUS_UTIL_H_
+#define TINK_INTERNAL_KEY_STATUS_UTIL_H_
+
+#include <string>
+
+#include "tink/key_status.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Returns `KeyStatus` C++ enum for a given `KeyStatusType` proto enum. If
+// `status_type` is unrecognized (i.e., not handled), then an error is returned.
+util::StatusOr<KeyStatus> FromKeyStatusType(
+    google::crypto::tink::KeyStatusType status_type);
+
+// Returns `KeyStatusType` proto enum for a given `KeyStatus` C++ enum. If
+// `status` is unrecognized (i.e., not handled), then an error is returned.
+util::StatusOr<google::crypto::tink::KeyStatusType> ToKeyStatusType(
+    KeyStatus status);
+
+// Returns a canonical name for a `KeyStatus` based on the corresponding
+// `KeyStatusType` proto enum.
+std::string ToKeyStatusName(KeyStatus status);
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEY_STATUS_UTIL_H_
diff --git a/cc/internal/key_status_util_test.cc b/cc/internal/key_status_util_test.cc
new file mode 100644
index 0000000..6ac0933
--- /dev/null
+++ b/cc/internal/key_status_util_test.cc
@@ -0,0 +1,79 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_status_util.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/key_status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyStatusType;
+
+TEST(KeyStatusUtilTest, FromKeyStatusType) {
+  util::StatusOr<KeyStatus> enabled = FromKeyStatusType(KeyStatusType::ENABLED);
+  EXPECT_THAT(enabled, IsOkAndHolds(KeyStatus::kEnabled));
+
+  util::StatusOr<KeyStatus> disabled =
+      FromKeyStatusType(KeyStatusType::DISABLED);
+  EXPECT_THAT(disabled, IsOkAndHolds(KeyStatus::kDisabled));
+
+  util::StatusOr<KeyStatus> destroyed =
+      FromKeyStatusType(KeyStatusType::DESTROYED);
+  EXPECT_THAT(destroyed, IsOkAndHolds(KeyStatus::kDestroyed));
+
+  util::StatusOr<KeyStatus> unknown =
+      FromKeyStatusType(KeyStatusType::UNKNOWN_STATUS);
+  EXPECT_THAT(unknown.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(KeyStatusUtilTest, ToKeyStatusType) {
+  util::StatusOr<KeyStatusType> enabled = ToKeyStatusType(KeyStatus::kEnabled);
+  EXPECT_THAT(enabled, IsOkAndHolds(KeyStatusType::ENABLED));
+
+  util::StatusOr<KeyStatusType> disabled =
+      ToKeyStatusType(KeyStatus::kDisabled);
+  EXPECT_THAT(disabled, IsOkAndHolds(KeyStatusType::DISABLED));
+
+  util::StatusOr<KeyStatusType> destroyed =
+      ToKeyStatusType(KeyStatus::kDestroyed);
+  EXPECT_THAT(destroyed, IsOkAndHolds(KeyStatusType::DESTROYED));
+
+  util::StatusOr<KeyStatusType> unknown = ToKeyStatusType(
+      KeyStatus::kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements);
+  EXPECT_THAT(unknown.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(KeyStatusUtilTest, ToKeyStatusName) {
+  EXPECT_EQ(ToKeyStatusName(KeyStatus::kEnabled), "ENABLED");
+  EXPECT_EQ(ToKeyStatusName(KeyStatus::kDisabled), "DISABLED");
+  EXPECT_EQ(ToKeyStatusName(KeyStatus::kDestroyed), "DESTROYED");
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/key_type_info_store.cc b/cc/internal/key_type_info_store.cc
new file mode 100644
index 0000000..5d041ff
--- /dev/null
+++ b/cc/internal/key_type_info_store.cc
@@ -0,0 +1,64 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_type_info_store.h"
+
+#include <memory>
+#include <typeindex>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+util::StatusOr<KeyTypeInfoStore::Info*> KeyTypeInfoStore::Get(
+    absl::string_view type_url) const {
+  auto it = type_url_to_info_.find(type_url);
+  if (it == type_url_to_info_.end()) {
+    return ToStatusF(absl::StatusCode::kNotFound,
+                     "No manager for type '%s' has been registered.", type_url);
+  }
+  return it->second.get();
+}
+
+util::Status KeyTypeInfoStore::IsInsertable(
+    absl::string_view type_url, const std::type_index& key_manager_type_index,
+    bool new_key_allowed) const {
+  auto it = type_url_to_info_.find(type_url);
+  if (it == type_url_to_info_.end()) {
+    return crypto::tink::util::OkStatus();
+  }
+  if (it->second->key_manager_type_index() != key_manager_type_index) {
+    return ToStatusF(absl::StatusCode::kAlreadyExists,
+                     "A manager for type '%s' has been already registered.",
+                     type_url);
+  }
+  if (!it->second->new_key_allowed() && new_key_allowed) {
+    return ToStatusF(absl::StatusCode::kAlreadyExists,
+                     "A manager for type '%s' has been already registered "
+                     "with forbidden new key operation.",
+                     type_url);
+  }
+  return util::OkStatus();
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/key_type_info_store.h b/cc/internal/key_type_info_store.h
new file mode 100644
index 0000000..603fb00
--- /dev/null
+++ b/cc/internal/key_type_info_store.h
@@ -0,0 +1,425 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEY_TYPE_INFO_STORE_H_
+#define TINK_INTERNAL_KEY_TYPE_INFO_STORE_H_
+
+#include <atomic>
+#include <functional>
+#include <initializer_list>
+#include <memory>
+#include <string>
+#include <typeindex>
+#include <utility>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/strings/str_join.h"
+#include "tink/core/key_manager_impl.h"
+#include "tink/core/key_type_manager.h"
+#include "tink/core/private_key_manager_impl.h"
+#include "tink/core/private_key_type_manager.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/key_manager.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Stores information about key types constructed from their KeyTypeManager or
+// KeyManager. This is used by the Configuration and Registry classes.
+//
+// Once inserted, Info objects must remain valid for the lifetime of the
+// KeyTypeInfoStore object, and the Info object's pointer stability is required.
+// Elements in Info, which include the KeyTypeManager or KeyManager, must not
+// be replaced.
+//
+// Example:
+//  KeyTypeInfoStore store;
+//  crypto::tink::util::Status status =
+//      store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(), true);
+//  crypto::tink::util::StatusOr<KeyTypeInfoStore::Info*> info =
+//      store.Get(AesGcmKeyManager().get_key_type());
+class KeyTypeInfoStore {
+ public:
+  KeyTypeInfoStore() = default;
+
+  // Movable, but not copyable.
+  KeyTypeInfoStore(KeyTypeInfoStore&& other) = default;
+  KeyTypeInfoStore& operator=(KeyTypeInfoStore&& other) = default;
+
+  // Information about a key type constructed from its KeyTypeManager or
+  // KeyManager.
+  class Info {
+   public:
+    // Takes ownership of `manager`.
+    template <typename KeyProto, typename KeyFormatProto,
+              typename... Primitives>
+    Info(KeyTypeManager<KeyProto, KeyFormatProto, List<Primitives...>>* manager,
+         bool new_key_allowed)
+        : key_manager_type_index_(std::type_index(typeid(*manager))),
+          public_key_type_manager_type_index_(absl::nullopt),
+          new_key_allowed_(new_key_allowed),
+          key_type_manager_(absl::WrapUnique(manager)),
+          internal_key_factory_(
+              absl::make_unique<internal::KeyFactoryImpl<KeyTypeManager<
+                  KeyProto, KeyFormatProto, List<Primitives...>>>>(manager)),
+          key_factory_(internal_key_factory_.get()),
+          key_deriver_(CreateDeriverFunctionFor(manager)) {
+      // TODO(C++17): Replace with a fold expression.
+      (void)std::initializer_list<int>{
+          0, (primitive_to_manager_.emplace(
+                  std::type_index(typeid(Primitives)),
+                  internal::MakeKeyManager<Primitives>(manager)),
+              0)...};
+    }
+
+    // Takes ownership of `private_manager`, but not of `public_manager`, which
+    // must only be alive for the duration of the constructor.
+    template <typename PrivateKeyProto, typename KeyFormatProto,
+              typename PublicKeyProto, typename PublicPrimitivesList,
+              typename... PrivatePrimitives>
+    Info(PrivateKeyTypeManager<PrivateKeyProto, KeyFormatProto, PublicKeyProto,
+                               List<PrivatePrimitives...>>* private_manager,
+         KeyTypeManager<PublicKeyProto, void, PublicPrimitivesList>*
+             public_manager,
+         bool new_key_allowed)
+        : key_manager_type_index_(std::type_index(typeid(*private_manager))),
+          public_key_type_manager_type_index_(
+              std::type_index(typeid(*public_manager))),
+          new_key_allowed_(new_key_allowed),
+          key_type_manager_(absl::WrapUnique(private_manager)),
+          internal_key_factory_(
+              absl::make_unique<internal::PrivateKeyFactoryImpl<
+                  PrivateKeyProto, KeyFormatProto, PublicKeyProto,
+                  List<PrivatePrimitives...>, PublicPrimitivesList>>(
+                  private_manager, public_manager)),
+          key_factory_(internal_key_factory_.get()),
+          key_deriver_(CreateDeriverFunctionFor(private_manager)) {
+      // TODO(C++17): Replace with a fold expression.
+      (void)std::initializer_list<int>{
+          0, (primitive_to_manager_.emplace(
+                  std::type_index(typeid(PrivatePrimitives)),
+                  internal::MakePrivateKeyManager<PrivatePrimitives>(
+                      private_manager, public_manager)),
+              0)...};
+    }
+
+    // Takes ownership of `manager`. KeyManager is the legacy/internal version
+    // of KeyTypeManager.
+    template <typename P>
+    Info(KeyManager<P>* manager, bool new_key_allowed)
+        : key_manager_type_index_(std::type_index(typeid(*manager))),
+          public_key_type_manager_type_index_(absl::nullopt),
+          new_key_allowed_(new_key_allowed),
+          key_type_manager_(nullptr),
+          internal_key_factory_(nullptr),
+          key_factory_(&manager->get_key_factory()) {
+      primitive_to_manager_.emplace(std::type_index(typeid(P)),
+                                    absl::WrapUnique(manager));
+    }
+
+    template <typename P>
+    crypto::tink::util::StatusOr<const KeyManager<P>*> get_key_manager(
+        absl::string_view requested_type_url) const {
+      auto it = primitive_to_manager_.find(std::type_index(typeid(P)));
+      if (it == primitive_to_manager_.end()) {
+        return crypto::tink::util::Status(
+            absl::StatusCode::kInvalidArgument,
+            absl::StrCat(
+                "Primitive type ", typeid(P).name(),
+                " not among supported primitives ",
+                absl::StrJoin(
+                    primitive_to_manager_.begin(), primitive_to_manager_.end(),
+                    ", ",
+                    [](std::string* out,
+                       const std::pair<const std::type_index,
+                                       std::unique_ptr<KeyManagerBase>>& kv) {
+                      absl::StrAppend(out, kv.first.name());
+                    }),
+                " for type URL ", requested_type_url));
+      }
+      return static_cast<const KeyManager<P>*>(it->second.get());
+    }
+
+    const std::type_index& key_manager_type_index() const {
+      return key_manager_type_index_;
+    }
+
+    const absl::optional<std::type_index>& public_key_type_manager_type_index()
+        const {
+      return public_key_type_manager_type_index_;
+    }
+
+    bool new_key_allowed() const { return new_key_allowed_.load(); }
+
+    void set_new_key_allowed(bool b) { new_key_allowed_.store(b); }
+
+    const KeyFactory& key_factory() const { return *key_factory_; }
+
+    const std::function<crypto::tink::util::StatusOr<
+        google::crypto::tink::KeyData>(absl::string_view, InputStream*)>&
+    key_deriver() const {
+      return key_deriver_;
+    }
+
+   private:
+    // Dynamic type_index of the KeyManager or KeyTypeManager for this key type.
+    std::type_index key_manager_type_index_;
+    // Dynamic type_index of the public KeyTypeManager for this key type when
+    // inserted into the registry via RegisterAsymmetricKeyManagers. Otherwise,
+    // nullopt.
+    absl::optional<std::type_index> public_key_type_manager_type_index_;
+    // Whether the key manager allows the creation of new keys.
+    std::atomic<bool> new_key_allowed_;
+
+    // Map from primitive type_index to KeyManager.
+    absl::flat_hash_map<std::type_index, std::unique_ptr<KeyManagerBase>>
+        primitive_to_manager_;
+    // Key type manager. Equals nullptr if Info was constructed from a
+    // KeyManager.
+    const std::shared_ptr<void> key_type_manager_;
+
+    // Key factory. Equals nullptr if Info was constructed from a KeyManager.
+    std::unique_ptr<const KeyFactory> internal_key_factory_;
+    // Unowned version of `internal_key_factory_` if Info was constructed from a
+    // KeyTypeManager. Key factory belonging to the KeyManager if Info was
+    // constructed from a KeyManager.
+    const KeyFactory* key_factory_;
+
+    // Derives a key if Info was constructed from a KeyTypeManager with a
+    // non-void KeyFormat type. Else, this function is empty and casting to a
+    // bool returns false.
+    std::function<crypto::tink::util::StatusOr<google::crypto::tink::KeyData>(
+        absl::string_view, InputStream*)>
+        key_deriver_;
+  };
+
+  // Adds a crypto::tink::KeyTypeManager to KeyTypeInfoStore. `new_key_allowed`
+  // indicates whether `manager` may create new keys.
+  template <class KeyTypeManager>
+  crypto::tink::util::Status AddKeyTypeManager(
+      std::unique_ptr<KeyTypeManager> manager, bool new_key_allowed);
+
+  // Adds a pair of crypto::tink::PrivateKeyTypeManager and
+  // crypto::tink::KeyTypeManager to KeyTypeInfoStore. `new_key_allowed`
+  // indicates whether `private_manager` may create new keys.
+  template <class PrivateKeyTypeManager, class PublicKeyTypeManager>
+  crypto::tink::util::Status AddAsymmetricKeyTypeManagers(
+      std::unique_ptr<PrivateKeyTypeManager> private_manager,
+      std::unique_ptr<PublicKeyTypeManager> public_manager,
+      bool new_key_allowed);
+
+  // Adds a crypto::tink::KeyManager to KeyTypeInfoStore. `new_key_allowed`
+  // indicates whether `manager` may create new keys. KeyManager is the
+  // legacy/internal version of KeyTypeManager.
+  template <class P>
+  crypto::tink::util::Status AddKeyManager(
+      std::unique_ptr<KeyManager<P>> manager, bool new_key_allowed);
+
+  // Gets Info associated with `type_url`, returning either a valid, non-null
+  // Info or an error.
+  crypto::tink::util::StatusOr<Info*> Get(absl::string_view type_url) const;
+
+  bool IsEmpty() const { return type_url_to_info_.empty(); }
+
+ private:
+  // Whether a key manager with `type_url` and `key_manager_type_index` can be
+  // inserted.
+  crypto::tink::util::Status IsInsertable(
+      absl::string_view type_url, const std::type_index& key_manager_type_index,
+      bool new_key_allowed) const;
+
+  void Add(std::string type_url, std::unique_ptr<Info> info,
+           bool new_key_allowed) {
+    auto it = type_url_to_info_.find(type_url);
+    if (it != type_url_to_info_.end()) {
+      it->second->set_new_key_allowed(new_key_allowed);
+    } else {
+      type_url_to_info_.insert({type_url, std::move(info)});
+    }
+  }
+
+  // Map from the type_url to Info.
+  // Elements in Info must not be replaced, and pointer stability is required
+  // for `Get()`.
+  absl::flat_hash_map<std::string, std::unique_ptr<Info>> type_url_to_info_;
+};
+
+template <class P>
+crypto::tink::util::Status KeyTypeInfoStore::AddKeyManager(
+    std::unique_ptr<KeyManager<P>> manager, bool new_key_allowed) {
+  std::string type_url = manager->get_key_type();
+  if (!manager->DoesSupport(type_url)) {
+    return ToStatusF(absl::StatusCode::kInvalidArgument,
+                     "The manager does not support type '%s'.", type_url);
+  }
+
+  crypto::tink::util::Status status = IsInsertable(
+      type_url, std::type_index(typeid(*manager)), new_key_allowed);
+  if (!status.ok()) {
+    return status;
+  }
+
+  auto info = absl::make_unique<Info>(manager.release(), new_key_allowed);
+  Add(type_url, std::move(info), new_key_allowed);
+  return crypto::tink::util::OkStatus();
+}
+
+template <class KeyTypeManager>
+crypto::tink::util::Status KeyTypeInfoStore::AddKeyTypeManager(
+    std::unique_ptr<KeyTypeManager> manager, bool new_key_allowed) {
+  // Check FIPS status.
+  internal::FipsCompatibility fips_compatible = manager->FipsStatus();
+  auto fips_status = internal::ChecksFipsCompatibility(fips_compatible);
+  if (!fips_status.ok()) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInternal,
+        absl::StrCat("Failed registering the key manager for ",
+                     typeid(*manager).name(),
+                     " as it is not FIPS compatible: ", fips_status.message()));
+  }
+
+  std::string type_url = manager->get_key_type();
+  crypto::tink::util::Status status = IsInsertable(
+      type_url, std::type_index(typeid(*manager)), new_key_allowed);
+  if (!status.ok()) {
+    return status;
+  }
+
+  auto info = absl::make_unique<Info>(manager.release(), new_key_allowed);
+  Add(type_url, std::move(info), new_key_allowed);
+  return crypto::tink::util::OkStatus();
+}
+
+template <class PrivateKeyTypeManager, class PublicKeyTypeManager>
+crypto::tink::util::Status KeyTypeInfoStore::AddAsymmetricKeyTypeManagers(
+    std::unique_ptr<PrivateKeyTypeManager> private_manager,
+    std::unique_ptr<PublicKeyTypeManager> public_manager,
+    bool new_key_allowed) {
+  std::string private_type_url = private_manager->get_key_type();
+  std::string public_type_url = public_manager->get_key_type();
+  if (private_type_url == public_type_url) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Passed in key managers must have different get_key_type() results.");
+  }
+
+  // Check FIPS status.
+  auto private_fips_status =
+      internal::ChecksFipsCompatibility(private_manager->FipsStatus());
+  if (!private_fips_status.ok()) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInternal,
+        absl::StrCat(
+            "Failed registering the key manager for ",
+            typeid(*private_manager).name(),
+            " as it is not FIPS compatible: ", private_fips_status.message()));
+  }
+  auto public_fips_status =
+      internal::ChecksFipsCompatibility(public_manager->FipsStatus());
+  if (!public_fips_status.ok()) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInternal,
+        absl::StrCat(
+            "Failed registering the key manager for ",
+            typeid(*public_manager).name(),
+            " as it is not FIPS compatible: ", public_fips_status.message()));
+  }
+
+  crypto::tink::util::Status private_status =
+      IsInsertable(private_type_url, std::type_index(typeid(*private_manager)),
+                   new_key_allowed);
+  if (!private_status.ok()) {
+    return private_status;
+  }
+  crypto::tink::util::Status public_status =
+      IsInsertable(public_type_url, std::type_index(typeid(*public_manager)),
+                   new_key_allowed);
+  if (!public_status.ok()) {
+    return public_status;
+  }
+
+  util::StatusOr<KeyTypeInfoStore::Info*> private_found = Get(private_type_url);
+  util::StatusOr<const KeyTypeInfoStore::Info*> public_found =
+      Get(public_type_url);
+
+  // Only one of the private and public key type managers is found.
+  if (private_found.ok() && !public_found.ok()) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat(
+            "Private key manager corresponding to ",
+            typeid(*private_manager).name(),
+            " was previously registered, but key manager corresponding to ",
+            typeid(*public_manager).name(),
+            " was not, so it's impossible to register them jointly"));
+  }
+  if (!private_found.ok() && public_found.ok()) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Key manager corresponding to ",
+                     typeid(*public_manager).name(),
+                     " was previously registered, but private key manager "
+                     "corresponding to ",
+                     typeid(*private_manager).name(),
+                     " was not, so it's impossible to register them jointly"));
+  }
+
+  // Both private and public key type managers are found.
+  if (private_found.ok() && public_found.ok()) {
+    if (!(*private_found)->public_key_type_manager_type_index().has_value()) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat("private key manager corresponding to ",
+                       typeid(*private_manager).name(),
+                       " is already registered without public key manager, "
+                       "cannot be re-registered with public key manager. "));
+    }
+    if ((*private_found)->public_key_type_manager_type_index() !=
+        std::type_index(typeid(*public_manager))) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat(
+              "private key manager corresponding to ",
+              typeid(*private_manager).name(), " is already registered with ",
+              (*private_found)->public_key_type_manager_type_index()->name(),
+              ", cannot be re-registered with ",
+              typeid(*public_manager).name()));
+    }
+    // Since `private_manager` passed the `IsInsertable` check above, the
+    // `set_new_key_allowed` operation is permissible.
+    (*private_found)->set_new_key_allowed(new_key_allowed);
+    return crypto::tink::util::OkStatus();
+  }
+
+  // Both private and public key type managers were not found.
+  auto private_info = absl::make_unique<Info>(
+      private_manager.release(), public_manager.get(), new_key_allowed);
+  Add(private_type_url, std::move(private_info), new_key_allowed);
+  // TODO(b/265705174): Store public key type managers in an asymmetric pair
+  // with new_key_allowed = false.
+  auto public_info =
+      absl::make_unique<Info>(public_manager.release(), new_key_allowed);
+  Add(public_type_url, std::move(public_info), new_key_allowed);
+
+  return crypto::tink::util::OkStatus();
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEY_TYPE_INFO_STORE_H_
diff --git a/cc/internal/key_type_info_store_test.cc b/cc/internal/key_type_info_store_test.cc
new file mode 100644
index 0000000..3768ac9
--- /dev/null
+++ b/cc/internal/key_type_info_store_test.cc
@@ -0,0 +1,443 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/key_type_info_store.h"
+
+#include <memory>
+#include <string>
+#include <typeindex>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/aead.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/aead/cord_aead.h"
+#include "tink/aead/kms_envelope_aead_key_manager.h"
+#include "tink/core/key_manager_impl.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/key_manager.h"
+#include "tink/signature/ecdsa_sign_key_manager.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/util/test_matchers.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/common.pb.h"
+#include "proto/ecdsa.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::EcdsaKeyFormat;
+using ::google::crypto::tink::EcdsaParams;
+using ::google::crypto::tink::EcdsaSignatureEncoding;
+using ::google::crypto::tink::EllipticCurveType;
+using ::google::crypto::tink::HashType;
+
+// TODO(b/265705174): Use fake key managers to avoid relying on key manager
+// implementations.
+TEST(KeyTypeInfoStoreTest, AddKeyTypeManager) {
+  KeyTypeInfoStore store;
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/true),
+              IsOk());
+
+  std::string type_url = AesGcmKeyManager().get_key_type();
+  util::StatusOr<KeyTypeInfoStore::Info*> info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), true);
+
+  util::StatusOr<const KeyManager<Aead>*> manager =
+      (*info)->get_key_manager<Aead>(type_url);
+  ASSERT_THAT(manager, IsOk());
+  EXPECT_EQ((*manager)->get_key_type(), type_url);
+}
+
+TEST(KeyTypeInfoStoreTest, AddKeyTypeManagerNoBoringCrypto) {
+  if (!kUseOnlyFips || IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Only supported in FIPS-mode with BoringCrypto not available.";
+  }
+  KeyTypeInfoStore store;
+  EXPECT_THAT(
+      store.AddKeyTypeManager(absl::make_unique<KmsEnvelopeAeadKeyManager>(),
+                              /*new_key_allowed=*/true),
+      StatusIs(absl::StatusCode::kInternal));
+}
+
+TEST(KeyTypeInfoStoreTest, AddKeyTypeManagerAndChangeNewKeyAllowed) {
+  KeyTypeInfoStore store;
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/true),
+              IsOk());
+
+  std::string type_url = AesGcmKeyManager().get_key_type();
+  util::StatusOr<KeyTypeInfoStore::Info*> info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), true);
+
+  // new_key_allowed true -> true is allowed.
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/true),
+              IsOk());
+  info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), true);
+
+  // new_key_allowed true -> false is allowed.
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/false),
+              IsOk());
+  info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), false);
+
+  // new_key_allowed false -> false is allowed.
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/false),
+              IsOk());
+  info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), false);
+
+  // new_key_allowed false -> true is not allowed.
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/true),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(KeyTypeInfoStoreTest, AddAsymmetricKeyTypeManagers) {
+  KeyTypeInfoStore store;
+  ASSERT_THAT(store.AddAsymmetricKeyTypeManagers(
+                  absl::make_unique<EcdsaSignKeyManager>(),
+                  absl::make_unique<EcdsaVerifyKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+
+  {
+    std::string private_type_url = EcdsaSignKeyManager().get_key_type();
+    util::StatusOr<KeyTypeInfoStore::Info*> info = store.Get(private_type_url);
+    ASSERT_THAT(info, IsOk());
+
+    util::StatusOr<const KeyManager<PublicKeySign>*> manager =
+        (*info)->get_key_manager<PublicKeySign>(private_type_url);
+    ASSERT_THAT(manager, IsOk());
+    EXPECT_EQ((*manager)->get_key_type(), private_type_url);
+  }
+  {
+    std::string public_type_url = EcdsaVerifyKeyManager().get_key_type();
+    util::StatusOr<KeyTypeInfoStore::Info*> info = store.Get(public_type_url);
+    ASSERT_THAT(info, IsOk());
+
+    util::StatusOr<const KeyManager<PublicKeyVerify>*> manager =
+        (*info)->get_key_manager<PublicKeyVerify>(public_type_url);
+    ASSERT_THAT(manager, IsOk());
+    EXPECT_EQ((*manager)->get_key_type(), public_type_url);
+  }
+}
+
+TEST(KeyTypeInfoStoreTest, AddAsymmetricKeyTypeManagersAlreadyExists) {
+  {
+    KeyTypeInfoStore store;
+    ASSERT_THAT(
+        store.AddKeyTypeManager(absl::make_unique<EcdsaSignKeyManager>(),
+                                /*new_key_allowed=*/true),
+        IsOk());
+    EXPECT_THAT(store.AddAsymmetricKeyTypeManagers(
+                    absl::make_unique<EcdsaSignKeyManager>(),
+                    absl::make_unique<EcdsaVerifyKeyManager>(),
+                    /*new_key_allowed=*/true),
+                StatusIs(absl::StatusCode::kInvalidArgument));
+  }
+  {
+    KeyTypeInfoStore store;
+    ASSERT_THAT(
+        store.AddKeyTypeManager(absl::make_unique<EcdsaVerifyKeyManager>(),
+                                /*new_key_allowed=*/true),
+        IsOk());
+    EXPECT_THAT(store.AddAsymmetricKeyTypeManagers(
+                    absl::make_unique<EcdsaSignKeyManager>(),
+                    absl::make_unique<EcdsaVerifyKeyManager>(),
+                    /*new_key_allowed=*/true),
+                StatusIs(absl::StatusCode::kInvalidArgument));
+  }
+  {
+    KeyTypeInfoStore store;
+    EXPECT_THAT(store.AddAsymmetricKeyTypeManagers(
+                    absl::make_unique<EcdsaSignKeyManager>(),
+                    absl::make_unique<EcdsaVerifyKeyManager>(),
+                    /*new_key_allowed=*/true),
+                IsOk());
+    EXPECT_THAT(store.AddAsymmetricKeyTypeManagers(
+                    absl::make_unique<EcdsaSignKeyManager>(),
+                    absl::make_unique<EcdsaVerifyKeyManager>(),
+                    /*new_key_allowed=*/true),
+                IsOk());
+  }
+}
+
+TEST(KeyTypeInfoStoreTest, AddAsymmetricKeyTypeManagersAndChangeNewKeyAllowed) {
+  KeyTypeInfoStore store;
+  ASSERT_THAT(store.AddAsymmetricKeyTypeManagers(
+                  absl::make_unique<EcdsaSignKeyManager>(),
+                  absl::make_unique<EcdsaVerifyKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+
+  std::string private_type_url = EcdsaSignKeyManager().get_key_type();
+  std::string public_type_url = EcdsaVerifyKeyManager().get_key_type();
+
+  util::StatusOr<KeyTypeInfoStore::Info*> private_info =
+      store.Get(private_type_url);
+  ASSERT_THAT(private_info, IsOk());
+  EXPECT_EQ((*private_info)->new_key_allowed(), true);
+  util::StatusOr<KeyTypeInfoStore::Info*> public_info =
+      store.Get(public_type_url);
+  ASSERT_THAT(public_info, IsOk());
+  EXPECT_EQ((*public_info)->new_key_allowed(), true);
+
+  // new_key_allowed true -> true is allowed.
+  ASSERT_THAT(store.AddAsymmetricKeyTypeManagers(
+                  absl::make_unique<EcdsaSignKeyManager>(),
+                  absl::make_unique<EcdsaVerifyKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+  private_info = store.Get(private_type_url);
+  ASSERT_THAT(private_info, IsOk());
+  EXPECT_EQ((*private_info)->new_key_allowed(), true);
+  public_info = store.Get(public_type_url);
+  ASSERT_THAT(public_info, IsOk());
+  EXPECT_EQ((*public_info)->new_key_allowed(), true);
+
+  // new_key_allowed true -> false is allowed.
+  ASSERT_THAT(store.AddAsymmetricKeyTypeManagers(
+                  absl::make_unique<EcdsaSignKeyManager>(),
+                  absl::make_unique<EcdsaVerifyKeyManager>(),
+                  /*new_key_allowed=*/false),
+              IsOk());
+  private_info = store.Get(private_type_url);
+  ASSERT_THAT(private_info, IsOk());
+  EXPECT_EQ((*private_info)->new_key_allowed(), false);
+  public_info = store.Get(public_type_url);
+  ASSERT_THAT(public_info, IsOk());
+  EXPECT_EQ((*public_info)->new_key_allowed(), true);
+
+  // new_key_allowed false -> false is allowed.
+  ASSERT_THAT(store.AddAsymmetricKeyTypeManagers(
+                  absl::make_unique<EcdsaSignKeyManager>(),
+                  absl::make_unique<EcdsaVerifyKeyManager>(),
+                  /*new_key_allowed=*/false),
+              IsOk());
+  private_info = store.Get(private_type_url);
+  ASSERT_THAT(private_info, IsOk());
+  EXPECT_EQ((*private_info)->new_key_allowed(), false);
+  public_info = store.Get(public_type_url);
+  ASSERT_THAT(public_info, IsOk());
+  EXPECT_EQ((*public_info)->new_key_allowed(), true);
+
+  // new_key_allowed false -> true is not allowed.
+  ASSERT_THAT(store.AddAsymmetricKeyTypeManagers(
+                  absl::make_unique<EcdsaSignKeyManager>(),
+                  absl::make_unique<EcdsaVerifyKeyManager>(),
+                  /*new_key_allowed=*/true),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(KeyTypeInfoStoreTest, AddKeyManager) {
+  KeyTypeInfoStore store;
+  AesGcmKeyManager manager;
+  ASSERT_THAT(store.AddKeyManager(MakeKeyManager<Aead>(&manager),
+                                  /*new_key_allowed=*/true),
+              IsOk());
+
+  std::string type_url = manager.get_key_type();
+  util::StatusOr<KeyTypeInfoStore::Info*> info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+
+  util::StatusOr<const KeyManager<Aead>*> got_manager =
+      (*info)->get_key_manager<Aead>(type_url);
+  ASSERT_THAT(got_manager, IsOk());
+  EXPECT_EQ((*got_manager)->get_key_type(), type_url);
+}
+
+TEST(KeyTypeInfoStoreTest, AddKeyManagerAndChangeNewKeyAllowed) {
+  KeyTypeInfoStore store;
+  AesGcmKeyManager manager;
+  ASSERT_THAT(store.AddKeyManager(MakeKeyManager<Aead>(&manager),
+                                  /*new_key_allowed=*/true),
+              IsOk());
+
+  std::string type_url = manager.get_key_type();
+  util::StatusOr<KeyTypeInfoStore::Info*> info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), true);
+
+  // new_key_allowed true -> true is allowed.
+  ASSERT_THAT(store.AddKeyManager(MakeKeyManager<Aead>(&manager),
+                                  /*new_key_allowed=*/true),
+              IsOk());
+  info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), true);
+
+  // new_key_allowed true -> false is allowed.
+  ASSERT_THAT(store.AddKeyManager(MakeKeyManager<Aead>(&manager),
+                                  /*new_key_allowed=*/false),
+              IsOk());
+  info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), false);
+
+  // new_key_allowed false -> false is allowed.
+  ASSERT_THAT(store.AddKeyManager(MakeKeyManager<Aead>(&manager),
+                                  /*new_key_allowed=*/false),
+              IsOk());
+  info = store.Get(type_url);
+  ASSERT_THAT(info, IsOk());
+  EXPECT_EQ((*info)->new_key_allowed(), false);
+
+  // new_key_allowed false -> true is not allowed.
+  ASSERT_THAT(store.AddKeyManager(MakeKeyManager<Aead>(&manager),
+                                  /*new_key_allowed=*/true),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(KeyTypeInfoStoreTest, Get) {
+  KeyTypeInfoStore store;
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/true),
+              IsOk());
+  util::StatusOr<KeyTypeInfoStore::Info*> info =
+      store.Get(AesGcmKeyManager().get_key_type());
+  EXPECT_THAT(info, IsOk());
+
+  EXPECT_THAT(store.Get("nonexistent.type.url").status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(KeyTypeInfoStoreTest, IsEmpty) {
+  KeyTypeInfoStore store;
+  EXPECT_EQ(store.IsEmpty(), true);
+
+  ASSERT_THAT(store.AddKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                      /*new_key_allowed=*/true),
+              IsOk());
+  EXPECT_THAT(store.IsEmpty(), false);
+}
+
+TEST(KeyTypeInfoStoreInfoTest, ConstructWithKeyTypeManager) {
+  KeyTypeInfoStore::Info info(absl::make_unique<AesGcmKeyManager>().release(),
+                              /*new_key_allowed=*/false);
+
+  EXPECT_EQ(info.key_manager_type_index(),
+            std::type_index(typeid(AesGcmKeyManager)));
+  EXPECT_EQ(info.public_key_type_manager_type_index(), absl::nullopt);
+
+  EXPECT_EQ(info.new_key_allowed(), false);
+  info.set_new_key_allowed(true);
+  EXPECT_EQ(info.new_key_allowed(), true);
+
+  std::string type_url = AesGcmKeyManager().get_key_type();
+  util::StatusOr<const KeyManager<Aead>*> aead_manager =
+      info.get_key_manager<Aead>(type_url);
+  ASSERT_THAT(aead_manager, IsOk());
+  EXPECT_EQ((*aead_manager)->DoesSupport(type_url), true);
+  util::StatusOr<const KeyManager<CordAead>*> cord_aead_manager =
+      info.get_key_manager<CordAead>(type_url);
+  ASSERT_THAT(aead_manager, IsOk());
+  EXPECT_EQ((*aead_manager)->DoesSupport(type_url), true);
+
+  AesGcmKeyFormat format;
+  format.set_key_size(32);
+  EXPECT_THAT(info.key_factory().NewKeyData(format.SerializeAsString()),
+              IsOk());
+
+  EXPECT_EQ((bool)info.key_deriver(), true);
+}
+
+TEST(KeyTypeInfoStoreInfoTest, ConstructWithAsymmetricKeyTypeManagers) {
+  KeyTypeInfoStore::Info info(
+      absl::make_unique<EcdsaSignKeyManager>().release(),
+      absl::make_unique<EcdsaVerifyKeyManager>().get(),
+      /*new_key_allowed=*/false);
+
+  EXPECT_EQ(info.key_manager_type_index(),
+            std::type_index(typeid(EcdsaSignKeyManager)));
+  EXPECT_EQ(info.public_key_type_manager_type_index(),
+            std::type_index(typeid(EcdsaVerifyKeyManager)));
+
+  EXPECT_EQ(info.new_key_allowed(), false);
+  info.set_new_key_allowed(true);
+  EXPECT_EQ(info.new_key_allowed(), true);
+
+  std::string type_url = EcdsaSignKeyManager().get_key_type();
+  util::StatusOr<const KeyManager<PublicKeySign>*> manager =
+      info.get_key_manager<PublicKeySign>(type_url);
+  ASSERT_THAT(manager, IsOk());
+  EXPECT_EQ((*manager)->DoesSupport(type_url), true);
+
+  EcdsaKeyFormat format;
+  EcdsaParams* params = format.mutable_params();
+  params->set_hash_type(HashType::SHA256);
+  params->set_curve(EllipticCurveType::NIST_P256);
+  params->set_encoding(EcdsaSignatureEncoding::DER);
+  EXPECT_THAT(info.key_factory().NewKeyData(format.SerializeAsString()),
+              IsOk());
+
+  EXPECT_EQ((bool)info.key_deriver(), true);
+}
+
+TEST(KeyTypeInfoStoreInfoTest, ConstructWithKeyManager) {
+  AesGcmKeyManager key_type_manager;
+  std::unique_ptr<KeyManager<Aead>> manager =
+      MakeKeyManager<Aead>(&key_type_manager);
+  std::type_index type_index = std::type_index(typeid(*manager));
+  KeyTypeInfoStore::Info info(manager.release(),
+                              /*new_key_allowed=*/false);
+
+  EXPECT_EQ(info.key_manager_type_index(), type_index);
+  EXPECT_EQ(info.public_key_type_manager_type_index(), absl::nullopt);
+
+  EXPECT_EQ(info.new_key_allowed(), false);
+  info.set_new_key_allowed(true);
+  EXPECT_EQ(info.new_key_allowed(), true);
+
+  std::string type_url = AesGcmKeyManager().get_key_type();
+  util::StatusOr<const KeyManager<Aead>*> got_manager =
+      info.get_key_manager<Aead>(type_url);
+  ASSERT_THAT(got_manager, IsOk());
+  EXPECT_EQ((*got_manager)->DoesSupport(type_url), true);
+  // Inserted KeyManager only supports Aead, not CordAead.
+  EXPECT_THAT(info.get_key_manager<CordAead>(type_url).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+
+  AesGcmKeyFormat format;
+  format.set_key_size(32);
+  EXPECT_THAT(info.key_factory().NewKeyData(format.SerializeAsString()),
+              IsOk());
+
+  EXPECT_EQ((bool)info.key_deriver(), false);
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/keyset_handle_builder_entry.cc b/cc/internal/keyset_handle_builder_entry.cc
new file mode 100644
index 0000000..d407cc9
--- /dev/null
+++ b/cc/internal/keyset_handle_builder_entry.cc
@@ -0,0 +1,216 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/keyset_handle_builder_entry.h"
+
+#include <memory>
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/key_status_util.h"
+#include "tink/internal/legacy_proto_key.h"
+#include "tink/internal/legacy_proto_parameters.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/internal/serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/registry.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+
+Keyset::Key ToKeysetKey(int id, KeyStatusType status,
+                        const ProtoKeySerialization& serialization) {
+  KeyData key_data;
+  key_data.set_type_url(std::string(serialization.TypeUrl()));
+  // OSS proto library complains if serialized key is not converted to string.
+  key_data.set_value(std::string(serialization.SerializedKeyProto().GetSecret(
+      InsecureSecretKeyAccess::Get())));
+  key_data.set_key_material_type(serialization.KeyMaterialType());
+  Keyset::Key key;
+  key.set_status(status);
+  key.set_key_id(id);
+  key.set_output_prefix_type(serialization.GetOutputPrefixType());
+  *key.mutable_key_data() = key_data;
+  return key;
+}
+
+util::StatusOr<ProtoParametersSerialization> SerializeParameters(
+    const Parameters& params) {
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<ProtoParametersSerialization>(params);
+  if (!serialization.ok()) return serialization.status();
+
+  const ProtoParametersSerialization* proto_serialization =
+      dynamic_cast<const ProtoParametersSerialization*>(serialization->get());
+  if (proto_serialization == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Failed to serialize proto parameters.");
+  }
+
+  return *proto_serialization;
+}
+
+util::StatusOr<ProtoParametersSerialization> SerializeLegacyParameters(
+    const Parameters* params) {
+  const LegacyProtoParameters* proto_params =
+      dynamic_cast<const LegacyProtoParameters*>(params);
+  if (proto_params == nullptr) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to serialize legacy proto parameters.");
+  }
+  return proto_params->Serialization();
+}
+
+util::StatusOr<ProtoKeySerialization> SerializeKey(const Key& key) {
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<ProtoKeySerialization>(key,
+                                               InsecureSecretKeyAccess::Get());
+  if (!serialization.ok()) return serialization.status();
+
+  const ProtoKeySerialization* serialized_proto_key =
+      dynamic_cast<const ProtoKeySerialization*>(serialization->get());
+  if (serialized_proto_key == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Failed to serialize proto key.");
+  }
+
+  return *serialized_proto_key;
+}
+
+util::StatusOr<ProtoKeySerialization> SerializeLegacyKey(const Key* key) {
+  const LegacyProtoKey* proto_key = dynamic_cast<const LegacyProtoKey*>(key);
+  if (proto_key == nullptr) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to serialize legacy proto key.");
+  }
+  util::StatusOr<const ProtoKeySerialization*> serialized_key =
+      proto_key->Serialization(InsecureSecretKeyAccess::Get());
+  if (!serialized_key.ok()) return serialized_key.status();
+
+  return **serialized_key;
+}
+
+util::StatusOr<Keyset::Key> CreateKeysetKeyFromProtoParametersSerialization(
+    const ProtoParametersSerialization& serialization, int id,
+    KeyStatusType status) {
+  util::StatusOr<std::unique_ptr<KeyData>> key_data =
+      Registry::NewKeyData(serialization.GetKeyTemplate());
+  if (!key_data.ok()) return key_data.status();
+
+  Keyset::Key key;
+  key.set_status(status);
+  key.set_key_id(id);
+  key.set_output_prefix_type(
+      serialization.GetKeyTemplate().output_prefix_type());
+  *key.mutable_key_data() = **key_data;
+  return key;
+}
+
+util::StatusOr<Keyset::Key> CreateKeysetKeyFromProtoKeySerialization(
+    const ProtoKeySerialization& key, int id, KeyStatusType status) {
+  absl::optional<int> id_requirement = key.IdRequirement();
+  if (id_requirement.has_value() && *id_requirement != id) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong ID set for key with ID requirement.");
+  }
+  return ToKeysetKey(id, status, key);
+}
+
+}  // namespace
+
+void KeysetHandleBuilderEntry::SetFixedId(int id) {
+  strategy_.strategy = KeyIdStrategyEnum::kFixedId;
+  strategy_.id_requirement = id;
+}
+
+void KeysetHandleBuilderEntry::SetRandomId() {
+  strategy_.strategy = KeyIdStrategyEnum::kRandomId;
+  strategy_.id_requirement = absl::nullopt;
+}
+
+util::StatusOr<Keyset::Key> KeyEntry::CreateKeysetKey(int id) {
+  util::StatusOr<KeyStatusType> key_status = ToKeyStatusType(key_status_);
+  if (!key_status.ok()) return key_status.status();
+
+  if (GetKeyIdRequirement().has_value() && GetKeyIdRequirement() != id) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Requested id does not match id requirement.");
+  }
+
+  util::StatusOr<ProtoKeySerialization> serialization = SerializeKey(*key_);
+  if (!serialization.ok() &&
+      serialization.status().code() != absl::StatusCode::kNotFound) {
+    return serialization.status();
+  }
+
+  if (serialization.status().code() == absl::StatusCode::kNotFound) {
+    // Fallback to legacy proto key.
+    serialization = SerializeLegacyKey(key_.get());
+    if (!serialization.ok()) return serialization.status();
+  }
+
+  return CreateKeysetKeyFromProtoKeySerialization(*serialization, id,
+                                                  *key_status);
+}
+
+util::StatusOr<Keyset::Key> ParametersEntry::CreateKeysetKey(int id) {
+  util::StatusOr<KeyStatusType> key_status = ToKeyStatusType(key_status_);
+  if (!key_status.ok()) return key_status.status();
+
+  if (GetKeyIdRequirement().has_value() && GetKeyIdRequirement() != id) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Requested id does not match id requirement.");
+  }
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      SerializeParameters(*parameters_);
+  if (!serialization.ok() &&
+      serialization.status().code() != absl::StatusCode::kNotFound) {
+    return serialization.status();
+  }
+
+  if (serialization.status().code() == absl::StatusCode::kNotFound) {
+    // Fallback to legacy proto parameters.
+    serialization = SerializeLegacyParameters(parameters_.get());
+    if (!serialization.ok()) return serialization.status();
+  }
+
+  return CreateKeysetKeyFromProtoParametersSerialization(*serialization, id,
+                                                         *key_status);
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/keyset_handle_builder_entry.h b/cc/internal/keyset_handle_builder_entry.h
new file mode 100644
index 0000000..a1d0a6c
--- /dev/null
+++ b/cc/internal/keyset_handle_builder_entry.h
@@ -0,0 +1,132 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEYSET_HANDLE_BUILDER_ENTRY_H_
+#define TINK_INTERNAL_KEYSET_HANDLE_BUILDER_ENTRY_H_
+
+#include <memory>
+#include <utility>
+
+#include "absl/types/optional.h"
+#include "tink/key.h"
+#include "tink/key_status.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+enum class KeyIdStrategyEnum : int {
+  kFixedId = 1,
+  kRandomId = 2,
+  // Added to guard from failures that may be caused by future expansions.
+  kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+};
+
+struct KeyIdStrategy {
+  KeyIdStrategyEnum strategy;
+  absl::optional<int> id_requirement;
+};
+
+// Internal keyset handle builder entry. The public keyset handle builder
+// entry will delegate its method calls to an instance of this class.
+class KeysetHandleBuilderEntry {
+ public:
+  KeysetHandleBuilderEntry() = default;
+  virtual ~KeysetHandleBuilderEntry() = default;
+
+  // Sets the key `status` of this entry.
+  void SetStatus(KeyStatus status) { key_status_ = status; }
+  // Returns key status of this entry.
+  KeyStatus GetStatus() const { return key_status_; }
+
+  // Assigns a fixed `id` when this keyset is built.
+  void SetFixedId(int id);
+  // Assigns an unused random id when this keyset is built.
+  void SetRandomId();
+
+  // Sets this entry as the primary key.
+  void SetPrimary() { is_primary_ = true; }
+  // Unsets this entry as the primary key.
+  void UnsetPrimary() { is_primary_ = false; }
+  // Returns whether or not this entry has been marked as a primary.
+  bool IsPrimary() const { return is_primary_; }
+
+  // Returns key id strategy.
+  KeyIdStrategy GetKeyIdStrategy() { return strategy_; }
+  // Returns key id strategy enum.
+  KeyIdStrategyEnum GetKeyIdStrategyEnum() { return strategy_.strategy; }
+  // Returns key id requirement.
+  absl::optional<int> GetKeyIdRequirement() { return strategy_.id_requirement; }
+
+  // Creates a Keyset::Key proto with the specified key `id` from either a
+  // `Key` object or a `Parameters` object.
+  virtual crypto::tink::util::StatusOr<google::crypto::tink::Keyset::Key>
+  CreateKeysetKey(int id) = 0;
+
+ protected:
+  KeyStatus key_status_ = KeyStatus::kDisabled;
+
+ private:
+  bool is_primary_ = false;
+  KeyIdStrategy strategy_ =
+      KeyIdStrategy{KeyIdStrategyEnum::kRandomId, absl::nullopt};
+};
+
+// Internal keyset handle builder entry constructed from a `Key` object.
+class KeyEntry : public KeysetHandleBuilderEntry {
+ public:
+  // Movable, but not copyable.
+  KeyEntry(KeyEntry&& other) = default;
+  KeyEntry& operator=(KeyEntry&& other) = default;
+  KeyEntry(const KeyEntry& other) = delete;
+  KeyEntry& operator=(const KeyEntry& other) = delete;
+
+  explicit KeyEntry(std::shared_ptr<const Key> key) : key_(std::move(key)) {}
+
+  crypto::tink::util::StatusOr<google::crypto::tink::Keyset::Key>
+  CreateKeysetKey(int id) override;
+
+ private:
+  std::shared_ptr<const Key> key_;
+};
+
+// Internal keyset handle builder entry constructed from a `Parameters` object.
+class ParametersEntry : public KeysetHandleBuilderEntry {
+ public:
+  // Movable, but not copyable.
+  ParametersEntry(ParametersEntry&& other) = default;
+  ParametersEntry& operator=(ParametersEntry&& other) = default;
+  ParametersEntry(const ParametersEntry& other) = delete;
+  ParametersEntry& operator=(const ParametersEntry& other) = delete;
+
+  explicit ParametersEntry(std::shared_ptr<const Parameters> parameters)
+      : parameters_(std::move(parameters)) {}
+
+  crypto::tink::util::StatusOr<google::crypto::tink::Keyset::Key>
+  CreateKeysetKey(int id) override;
+
+ private:
+  std::shared_ptr<const Parameters> parameters_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEYSET_HANDLE_BUILDER_ENTRY_H_
diff --git a/cc/internal/keyset_handle_builder_entry_test.cc b/cc/internal/keyset_handle_builder_entry_test.cc
new file mode 100644
index 0000000..f26b6a1
--- /dev/null
+++ b/cc/internal/keyset_handle_builder_entry_test.cc
@@ -0,0 +1,290 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/keyset_handle_builder_entry.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/config/tink_config.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/legacy_proto_key.h"
+#include "tink/internal/legacy_proto_parameters.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/key.h"
+#include "tink/key_status.h"
+#include "tink/keyset_handle.h"
+#include "tink/keyset_handle_builder.h"
+#include "tink/mac/aes_cmac_key.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/mac/mac_key_templates.h"
+#include "tink/parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::Test;
+
+util::StatusOr<LegacyProtoParameters> CreateLegacyProtoParameters() {
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create(MacKeyTemplates::AesCmac());
+  if (!serialization.ok()) return serialization.status();
+
+  return LegacyProtoParameters(*serialization);
+}
+
+TEST(KeysetHandleBuilderEntryTest, Status) {
+  util::StatusOr<LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  ParametersEntry entry =
+      ParametersEntry(absl::make_unique<LegacyProtoParameters>(*parameters));
+
+  entry.SetStatus(KeyStatus::kEnabled);
+  EXPECT_THAT(entry.GetStatus(), KeyStatus::kEnabled);
+
+  entry.SetStatus(KeyStatus::kDisabled);
+  EXPECT_THAT(entry.GetStatus(), KeyStatus::kDisabled);
+
+  entry.SetStatus(KeyStatus::kDestroyed);
+  EXPECT_THAT(entry.GetStatus(), KeyStatus::kDestroyed);
+}
+
+TEST(KeysetHandleBuilderEntryTest, IdStrategy) {
+  util::StatusOr<LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  ParametersEntry entry =
+      ParametersEntry(absl::make_unique<LegacyProtoParameters>(*parameters));
+
+  entry.SetFixedId(123);
+  EXPECT_THAT(entry.GetKeyIdStrategyEnum(), KeyIdStrategyEnum::kFixedId);
+  EXPECT_THAT(entry.GetKeyIdStrategy().strategy, KeyIdStrategyEnum::kFixedId);
+  EXPECT_THAT(entry.GetKeyIdStrategy().id_requirement, 123);
+  EXPECT_THAT(entry.GetKeyIdRequirement(), 123);
+
+  entry.SetRandomId();
+  EXPECT_THAT(entry.GetKeyIdStrategyEnum(), KeyIdStrategyEnum::kRandomId);
+  EXPECT_THAT(entry.GetKeyIdStrategy().strategy, KeyIdStrategyEnum::kRandomId);
+  EXPECT_THAT(entry.GetKeyIdStrategy().id_requirement, absl::nullopt);
+  EXPECT_THAT(entry.GetKeyIdRequirement(), absl::nullopt);
+}
+
+TEST(KeysetHandleBuilderEntryTest, Primary) {
+  util::StatusOr<LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  ParametersEntry entry =
+      ParametersEntry(absl::make_unique<LegacyProtoParameters>(*parameters));
+
+  entry.SetPrimary();
+  EXPECT_THAT(entry.IsPrimary(), IsTrue());
+
+  entry.UnsetPrimary();
+  EXPECT_THAT(entry.IsPrimary(), IsFalse());
+}
+
+class CreateKeysetKeyTest : public Test {
+ protected:
+  void SetUp() override { ASSERT_THAT(TinkConfig::Register(), IsOk()); }
+};
+
+TEST_F(CreateKeysetKeyTest, CreateKeysetKeyFromParameters) {
+  util::StatusOr<LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  ParametersEntry entry =
+      ParametersEntry(absl::make_unique<LegacyProtoParameters>(*parameters));
+  entry.SetStatus(KeyStatus::kEnabled);
+  entry.SetFixedId(123);
+  util::StatusOr<Keyset::Key> keyset_key = entry.CreateKeysetKey(/*id=*/123);
+  ASSERT_THAT(keyset_key, IsOk());
+
+  EXPECT_THAT(keyset_key->status(), Eq(KeyStatusType::ENABLED));
+  EXPECT_THAT(keyset_key->key_id(), Eq(123));
+  EXPECT_THAT(
+      keyset_key->output_prefix_type(),
+      Eq(parameters->Serialization().GetKeyTemplate().output_prefix_type()));
+  EXPECT_THAT(keyset_key->key_data().type_url(),
+              Eq(parameters->Serialization().GetKeyTemplate().type_url()));
+}
+
+TEST_F(CreateKeysetKeyTest, CreateKeysetKeyFromParametersWithDifferentKeyId) {
+  util::StatusOr<LegacyProtoParameters> parameters =
+      CreateLegacyProtoParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  ParametersEntry entry =
+      ParametersEntry(absl::make_unique<LegacyProtoParameters>(*parameters));
+  entry.SetStatus(KeyStatus::kEnabled);
+  entry.SetFixedId(123);
+  util::StatusOr<Keyset::Key> keyset_key = entry.CreateKeysetKey(/*id=*/456);
+  EXPECT_THAT(keyset_key.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(CreateKeysetKeyTest, CreateKeysetKeyFromKey) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/123);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  KeyEntry entry = KeyEntry(absl::make_unique<LegacyProtoKey>(*key));
+  entry.SetStatus(KeyStatus::kEnabled);
+  entry.SetFixedId(123);
+  util::StatusOr<Keyset::Key> keyset_key = entry.CreateKeysetKey(/*id=*/123);
+  ASSERT_THAT(keyset_key, IsOk());
+
+  EXPECT_THAT(keyset_key->status(), Eq(KeyStatusType::ENABLED));
+  EXPECT_THAT(keyset_key->key_id(), Eq(123));
+  EXPECT_THAT(keyset_key->output_prefix_type(), OutputPrefixType::TINK);
+  EXPECT_THAT(keyset_key->key_data().type_url(), Eq("type_url"));
+  EXPECT_THAT(keyset_key->key_data().key_material_type(),
+              Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(keyset_key->key_data().value(), Eq("serialized_key"));
+}
+
+TEST_F(CreateKeysetKeyTest, CreateKeysetKeyFromKeyWithDifferentEntryKeyId) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/123);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  KeyEntry entry = KeyEntry(absl::make_unique<LegacyProtoKey>(*key));
+  entry.SetStatus(KeyStatus::kEnabled);
+  entry.SetFixedId(123);
+  util::StatusOr<Keyset::Key> keyset_key = entry.CreateKeysetKey(/*id=*/456);
+  EXPECT_THAT(keyset_key.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(CreateKeysetKeyTest,
+       CreateKeysetKeyFromKeyWithDifferentSerializationKeyId) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/123);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  KeyEntry entry = KeyEntry(absl::make_unique<LegacyProtoKey>(*key));
+  entry.SetStatus(KeyStatus::kEnabled);
+  util::StatusOr<Keyset::Key> keyset_key = entry.CreateKeysetKey(/*id=*/456);
+  EXPECT_THAT(keyset_key.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(CreateKeysetKeyTest, CreateKeysetFromNonLegacyParameters) {
+  util::StatusOr<AesCmacParameters> aes_cmac_parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(aes_cmac_parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *aes_cmac_parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+}
+
+TEST_F(CreateKeysetKeyTest,
+       CreateKeysetWithAllowedParametersProhibitedByKeyManager) {
+  util::StatusOr<AesCmacParameters> aes_cmac_parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/16,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(aes_cmac_parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *aes_cmac_parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(CreateKeysetKeyTest, CreateKeysetFromNonLegacyKey) {
+  util::StatusOr<AesCmacParameters> aes_cmac_parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(aes_cmac_parameters, IsOk());
+  util::StatusOr<AesCmacKey> aes_cmac_key = AesCmacKey::Create(
+      *aes_cmac_parameters, RestrictedData(32), 123, GetPartialKeyAccess());
+  ASSERT_THAT(aes_cmac_key.status(), IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableKey(
+              *aes_cmac_key, KeyStatus::kEnabled, /*is_primary=*/true))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/keyset_wrapper.h b/cc/internal/keyset_wrapper.h
index ab10b9e..3258b57 100644
--- a/cc/internal/keyset_wrapper.h
+++ b/cc/internal/keyset_wrapper.h
@@ -16,6 +16,7 @@
 #ifndef TINK_INTERNAL_KEYSET_WRAPPER_H_
 #define TINK_INTERNAL_KEYSET_WRAPPER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/container/flat_hash_map.h"
@@ -41,7 +42,7 @@
 template <typename Primitive>
 class KeysetWrapper {
  public:
-  virtual ~KeysetWrapper() {}
+  virtual ~KeysetWrapper() = default;
 
   // Wraps a given `keyset` with annotations `annotations`.
   virtual crypto::tink::util::StatusOr<std::unique_ptr<Primitive>> Wrap(
diff --git a/cc/internal/keyset_wrapper_impl.h b/cc/internal/keyset_wrapper_impl.h
index 4dc092f..9273e4e 100644
--- a/cc/internal/keyset_wrapper_impl.h
+++ b/cc/internal/keyset_wrapper_impl.h
@@ -16,13 +16,17 @@
 #ifndef TINK_INTERNAL_KEYSET_WRAPPER_IMPL_H_
 #define TINK_INTERNAL_KEYSET_WRAPPER_IMPL_H_
 
+#include <functional>
+#include <memory>
 #include <string>
+#include <utility>
 
 #include "absl/container/flat_hash_map.h"
 #include "tink/internal/key_info.h"
 #include "tink/internal/keyset_wrapper.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/validation.h"
 #include "proto/tink.pb.h"
@@ -50,22 +54,27 @@
       const override {
     crypto::tink::util::Status status = ValidateKeyset(keyset);
     if (!status.ok()) return status;
-    auto primitives = absl::make_unique<PrimitiveSet<P>>(annotations);
+    typename PrimitiveSet<P>::Builder primitives_builder;
+    primitives_builder.AddAnnotations(annotations);
     for (const google::crypto::tink::Keyset::Key& key : keyset.key()) {
       if (key.status() != google::crypto::tink::KeyStatusType::ENABLED) {
         continue;
       }
       auto primitive = primitive_getter_(key.key_data());
       if (!primitive.ok()) return primitive.status();
-      auto entry = primitives->AddPrimitive(std::move(primitive.value()),
-                                            KeyInfoFromKey(key));
-      if (!entry.ok()) return entry.status();
       if (key.key_id() == keyset.primary_key_id()) {
-        auto primary_result = primitives->set_primary(entry.value());
-        if (!primary_result.ok()) return primary_result;
+        primitives_builder.AddPrimaryPrimitive(std::move(primitive.value()),
+                                               KeyInfoFromKey(key));
+      } else {
+        primitives_builder.AddPrimitive(std::move(primitive.value()),
+                                        KeyInfoFromKey(key));
       }
     }
-    return transforming_wrapper_.Wrap(std::move(primitives));
+    crypto::tink::util::StatusOr<PrimitiveSet<P>> primitives =
+        std::move(primitives_builder).Build();
+    if (!primitives.ok()) return primitives.status();
+    return transforming_wrapper_.Wrap(
+        absl::make_unique<PrimitiveSet<P>>(*std::move(primitives)));
   }
 
  private:
diff --git a/cc/internal/keyset_wrapper_impl_test.cc b/cc/internal/keyset_wrapper_impl_test.cc
index 0606aa1..7c2955b 100644
--- a/cc/internal/keyset_wrapper_impl_test.cc
+++ b/cc/internal/keyset_wrapper_impl_test.cc
@@ -15,9 +15,13 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/keyset_wrapper_impl.h"
 
+#include <stdint.h>
+
 #include <memory>
 #include <string>
+#include <tuple>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -25,8 +29,10 @@
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/match.h"
+#include "absl/strings/string_view.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
diff --git a/cc/internal/keyset_wrapper_store.h b/cc/internal/keyset_wrapper_store.h
new file mode 100644
index 0000000..dab9f66
--- /dev/null
+++ b/cc/internal/keyset_wrapper_store.h
@@ -0,0 +1,208 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_KEYSET_WRAPPER_STORE_H_
+#define TINK_INTERNAL_KEYSET_WRAPPER_STORE_H_
+
+#include <memory>
+#include <typeindex>
+
+#include "tink/internal/keyset_wrapper.h"
+#include "tink/internal/keyset_wrapper_impl.h"
+#include "tink/primitive_wrapper.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Stores KeysetWrappers constructed from their PrimitiveWrapper. This is used
+// by the Configuration and Registry classes.
+//
+// Once inserted, elements in Info, which include the PrimitiveWrapper, must not
+// be replaced.
+//
+// Example:
+//  KeysetWrapperStore store;
+//  crypto::tink::util::Status status = store.Add<Aead, Aead>(
+//      absl::make_unique<AeadWrapper>(), primitive_getter);
+//  crypto::tink::util::StatusOr<const KeysetWrapper<Aead>*> wrapper =
+//      store.Get<Aead>();
+class KeysetWrapperStore {
+ public:
+  KeysetWrapperStore() = default;
+
+  // Movable, but not copyable.
+  KeysetWrapperStore(KeysetWrapperStore&& other) = default;
+  KeysetWrapperStore& operator=(KeysetWrapperStore&& other) = default;
+
+  // Adds a crypto::tink::PrimitiveWrapper and `primitive_getter` function to
+  // KeysetWrapperStore.
+  template <class P, class Q>
+  crypto::tink::util::Status Add(
+      std::unique_ptr<PrimitiveWrapper<P, Q>> wrapper,
+      std::function<crypto::tink::util::StatusOr<std::unique_ptr<P>>(
+          const google::crypto::tink::KeyData& key_data)>
+          primitive_getter);
+
+  // Gets the PrimitiveWrapper that produces primitive P. This is a legacy
+  // function.
+  template <class P>
+  crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*>
+  GetPrimitiveWrapper() const;
+
+  // Gets the KeysetWrapper that produces primitive Q.
+  template <class Q>
+  crypto::tink::util::StatusOr<const KeysetWrapper<Q>*> Get() const;
+
+  bool IsEmpty() const { return primitive_to_info_.empty(); }
+
+ private:
+  class Info {
+   public:
+    template <typename P, typename Q>
+    explicit Info(
+        std::unique_ptr<PrimitiveWrapper<P, Q>> wrapper,
+        std::function<crypto::tink::util::StatusOr<std::unique_ptr<P>>(
+            const google::crypto::tink::KeyData& key_data)>
+            primitive_getter)
+        : is_same_primitive_wrapping_(std::is_same<P, Q>::value),
+          wrapper_type_index_(std::type_index(typeid(*wrapper))),
+          q_type_index_(std::type_index(typeid(Q))) {
+      keyset_wrapper_ = absl::make_unique<KeysetWrapperImpl<P, Q>>(
+          wrapper.get(), primitive_getter);
+      original_wrapper_ = std::move(wrapper);
+    }
+
+    template <typename Q>
+    crypto::tink::util::StatusOr<const KeysetWrapper<Q>*> Get() const {
+      if (q_type_index_ != std::type_index(typeid(Q))) {
+        return crypto::tink::util::Status(
+            absl::StatusCode::kInternal,
+            "RegistryImpl::KeysetWrapper() called with wrong type");
+      }
+      return static_cast<KeysetWrapper<Q>*>(keyset_wrapper_.get());
+    }
+
+    // TODO(b/171021679): Deprecate this and upstream functions.
+    template <typename P>
+    crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*>
+    GetPrimitiveWrapper() const {
+      if (!is_same_primitive_wrapping_) {
+        // This happens if a user uses a legacy method (like Registry::Wrap)
+        // directly or has a custom key manager for a primitive which has a
+        // PrimitiveWrapper<P,Q> with P != Q.
+        return crypto::tink::util::Status(
+            absl::StatusCode::kFailedPrecondition,
+            absl::StrCat("Cannot use primitive type ", typeid(P).name(),
+                         " with a custom key manager."));
+      }
+      if (q_type_index_ != std::type_index(typeid(P))) {
+        return crypto::tink::util::Status(
+            absl::StatusCode::kInternal,
+            "RegistryImpl::LegacyWrapper() called with wrong type");
+      }
+      return static_cast<const PrimitiveWrapper<P, P>*>(
+          original_wrapper_.get());
+    }
+
+    // Returns true if the PrimitiveWrapper is the same class as the one used
+    // to construct this Info.
+    template <typename P, typename Q>
+    bool HasSameType(const PrimitiveWrapper<P, Q>& wrapper) {
+      return wrapper_type_index_ == std::type_index(typeid(wrapper));
+    }
+
+   private:
+    bool is_same_primitive_wrapping_;
+    // dynamic std::type_index of the actual PrimitiveWrapper<P,Q> class for
+    // which this key was inserted.
+    std::type_index wrapper_type_index_;
+    // dynamic std::type_index of Q, when PrimitiveWrapper<P,Q> was inserted.
+    std::type_index q_type_index_;
+    // The primitive_wrapper passed in. We use a shared_ptr because
+    // unique_ptr<void> is invalid.
+    std::shared_ptr<void> original_wrapper_;
+    // The keyset_wrapper_. We use a shared_ptr because unique_ptr<void> is
+    // invalid.
+    std::shared_ptr<void> keyset_wrapper_;
+  };
+
+  // Map from primitive type_index to Info.
+  absl::flat_hash_map<std::type_index, Info> primitive_to_info_;
+};
+
+template <class P, class Q>
+crypto::tink::util::Status KeysetWrapperStore::Add(
+    std::unique_ptr<PrimitiveWrapper<P, Q>> wrapper,
+    std::function<crypto::tink::util::StatusOr<std::unique_ptr<P>>(
+        const google::crypto::tink::KeyData& key_data)>
+        primitive_getter) {
+  if (wrapper == nullptr) {
+    return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
+                                      "Parameter 'wrapper' must be non-null.");
+  }
+  if (primitive_getter == nullptr) {
+    return crypto::tink::util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Parameter 'primitive_getter' must be non-null.");
+  }
+
+  auto it = primitive_to_info_.find(std::type_index(typeid(Q)));
+  if (it != primitive_to_info_.end()) {
+    if (!it->second.HasSameType(*wrapper)) {
+      return util::Status(absl::StatusCode::kAlreadyExists,
+                          "A wrapper named for this primitive already exists.");
+    }
+    return crypto::tink::util::OkStatus();
+  }
+
+  primitive_to_info_.insert(
+      {std::type_index(typeid(Q)), Info(std::move(wrapper), primitive_getter)});
+
+  return crypto::tink::util::OkStatus();
+}
+
+template <class P>
+crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*>
+KeysetWrapperStore::GetPrimitiveWrapper() const {
+  auto it = primitive_to_info_.find(std::type_index(typeid(P)));
+  if (it == primitive_to_info_.end()) {
+    return util::Status(
+        absl::StatusCode::kNotFound,
+        absl::StrCat("No wrapper registered for type ", typeid(P).name()));
+  }
+  return it->second.GetPrimitiveWrapper<P>();
+}
+
+template <class P>
+crypto::tink::util::StatusOr<const KeysetWrapper<P>*> KeysetWrapperStore::Get()
+    const {
+  auto it = primitive_to_info_.find(std::type_index(typeid(P)));
+  if (it == primitive_to_info_.end()) {
+    return util::Status(
+        absl::StatusCode::kNotFound,
+        absl::StrCat("No wrapper registered for type ", typeid(P).name()));
+  }
+  return it->second.Get<P>();
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_KEYSET_WRAPPER_STORE_H_
diff --git a/cc/internal/keyset_wrapper_store_test.cc b/cc/internal/keyset_wrapper_store_test.cc
new file mode 100644
index 0000000..9c7231b
--- /dev/null
+++ b/cc/internal/keyset_wrapper_store_test.cc
@@ -0,0 +1,408 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/keyset_wrapper_store.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/internal/registry_impl.h"
+#include "tink/mac/mac_wrapper.h"
+#include "tink/primitive_set.h"
+#include "tink/primitive_wrapper.h"
+#include "tink/subtle/random.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/aes_gcm.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeysetInfo;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+
+class FakePrimitive {
+ public:
+  explicit FakePrimitive(std::string s) : s_(s) {}
+  std::string get() { return s_; }
+
+ private:
+  std::string s_;
+};
+
+class FakeKeyTypeManager
+    : public KeyTypeManager<AesGcmKey, AesGcmKeyFormat, List<FakePrimitive>> {
+ public:
+  class FakePrimitiveFactory : public PrimitiveFactory<FakePrimitive> {
+   public:
+    util::StatusOr<std::unique_ptr<FakePrimitive>> Create(
+        const AesGcmKey& key) const override {
+      return absl::make_unique<FakePrimitive>(key.key_value());
+    }
+  };
+
+  FakeKeyTypeManager()
+      : KeyTypeManager(absl::make_unique<FakePrimitiveFactory>()) {}
+
+  KeyData::KeyMaterialType key_material_type() const override {
+    return KeyData::SYMMETRIC;
+  }
+
+  uint32_t get_version() const override { return 0; }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  util::Status ValidateKey(const AesGcmKey& key) const override {
+    return util::OkStatus();
+  }
+
+  util::Status ValidateKeyFormat(
+      const AesGcmKeyFormat& key_format) const override {
+    return util::OkStatus();
+  }
+
+  util::StatusOr<AesGcmKey> CreateKey(
+      const AesGcmKeyFormat& key_format) const override {
+    return AesGcmKey();
+  }
+
+  util::StatusOr<AesGcmKey> DeriveKey(
+      const AesGcmKeyFormat& key_format,
+      InputStream* input_stream) const override {
+    return AesGcmKey();
+  }
+
+ private:
+  const std::string key_type_ =
+      "type.googleapis.com/google.crypto.tink.AesGcmKey";
+};
+
+class FakePrimitiveWrapper
+    : public PrimitiveWrapper<FakePrimitive, FakePrimitive> {
+ public:
+  util::StatusOr<std::unique_ptr<FakePrimitive>> Wrap(
+      std::unique_ptr<PrimitiveSet<FakePrimitive>> primitive_set)
+      const override {
+    return absl::make_unique<FakePrimitive>(
+        primitive_set->get_primary()->get_primitive().get());
+  }
+};
+
+class FakePrimitiveWrapper2
+    : public PrimitiveWrapper<FakePrimitive, FakePrimitive> {
+ public:
+  util::StatusOr<std::unique_ptr<FakePrimitive>> Wrap(
+      std::unique_ptr<PrimitiveSet<FakePrimitive>> primitive_set)
+      const override {
+    return absl::make_unique<FakePrimitive>(
+        primitive_set->get_primary()->get_primitive().get());
+  }
+};
+
+std::string AddAesGcmKeyToKeyset(Keyset& keyset, uint32_t key_id,
+                                 OutputPrefixType output_prefix_type,
+                                 KeyStatusType key_status_type) {
+  AesGcmKey key;
+  key.set_version(0);
+  key.set_key_value(subtle::Random::GetRandomBytes(16));
+  KeyData key_data;
+  key_data.set_value(key.SerializeAsString());
+  key_data.set_type_url("type.googleapis.com/google.crypto.tink.AesGcmKey");
+  test::AddKeyData(key_data, key_id, output_prefix_type, key_status_type,
+                   &keyset);
+  return key.key_value();
+}
+
+// Returns the function that relies on `registry` to transform `key_data` into
+// FakePrimitive.
+util::StatusOr<std::function<
+    util::StatusOr<std::unique_ptr<FakePrimitive>>(const KeyData& key_data)>>
+PrimitiveGetter(RegistryImpl& registry) {
+  util::Status status =
+      registry.RegisterKeyTypeManager<AesGcmKey, AesGcmKeyFormat,
+                                      List<FakePrimitive>>(
+          absl::make_unique<FakeKeyTypeManager>(),
+          /*new_key_allowed=*/true);
+  if (!status.ok()) {
+    return status;
+  }
+  return [&registry](const KeyData& key_data) {
+    return registry.GetPrimitive<FakePrimitive>(key_data);
+  };
+}
+
+TEST(KeysetWrapperStoreTest, Add) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  EXPECT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+}
+
+TEST(KeysetWrapperStoreTest, AddNull) {
+  KeysetWrapperStore store;
+  EXPECT_THAT((store.Add<FakePrimitive, FakePrimitive>(nullptr, nullptr)),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+
+  EXPECT_THAT((store.Add<FakePrimitive, FakePrimitive>(
+                  absl::make_unique<FakePrimitiveWrapper>(), nullptr)),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  EXPECT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(nullptr, *primitive_getter)),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(KeysetWrapperStoreTest, AddWrappersForDifferentPrimitivesSucceeds) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+
+  std::function<util::StatusOr<std::unique_ptr<Mac>>(const KeyData& key_data)>
+      primitive_getter_mac = [&registry](const KeyData& key_data) {
+        return registry.GetPrimitive<Mac>(key_data);
+      };
+  EXPECT_THAT((store.Add<Mac, Mac>(absl::make_unique<MacWrapper>(),
+                                   primitive_getter_mac)),
+              IsOk());
+}
+
+TEST(KeysetWrapperStoreTest, AddSameWrapperTwiceSucceeds) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+  EXPECT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+}
+
+TEST(KeysetWrapperStoreTest, AddDifferentWrappersForSamePrimitiveFails) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+  EXPECT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper2>(), *primitive_getter)),
+      StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(KeysetWrapperStoreTest, GetPrimitiveWrapper) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+
+  util::StatusOr<const PrimitiveWrapper<FakePrimitive, FakePrimitive>*>
+      legacy_wrapper = store.GetPrimitiveWrapper<FakePrimitive>();
+  ASSERT_THAT(legacy_wrapper, IsOk());
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(keyset, 13, OutputPrefixType::TINK,
+                                             KeyStatusType::ENABLED);
+  KeysetInfo keyset_info;
+  keyset_info.add_key_info();
+  keyset_info.mutable_key_info(0)->set_output_prefix_type(
+      OutputPrefixType::TINK);
+  keyset_info.mutable_key_info(0)->set_key_id(1234543);
+  keyset_info.mutable_key_info(0)->set_status(KeyStatusType::ENABLED);
+  keyset_info.set_primary_key_id(1234543);
+  std::unique_ptr<PrimitiveSet<FakePrimitive>> primitive_set(
+      new PrimitiveSet<FakePrimitive>());
+  auto entry = primitive_set->AddPrimitive(
+      absl::make_unique<FakePrimitive>(raw_key), keyset_info.key_info(0));
+  ASSERT_THAT(entry, IsOk());
+  ASSERT_THAT(primitive_set->set_primary(*entry), IsOk());
+
+  util::StatusOr<std::unique_ptr<FakePrimitive>> legacy_aead =
+      (*legacy_wrapper)->Wrap(std::move(primitive_set));
+  ASSERT_THAT(legacy_aead, IsOk());
+  EXPECT_THAT((*legacy_aead)->get(), Eq(raw_key));
+}
+
+TEST(KeysetWrapperStoreTest, GetPrimitiveWrapperNonexistentWrapperFails) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+
+  EXPECT_THAT(store.GetPrimitiveWrapper<Mac>().status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(KeysetWrapperStoreTest, Get) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+
+  util::StatusOr<const KeysetWrapper<FakePrimitive>*> wrapper =
+      store.Get<FakePrimitive>();
+  ASSERT_THAT(wrapper, IsOk());
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(keyset, 13, OutputPrefixType::TINK,
+                                             KeyStatusType::ENABLED);
+  keyset.set_primary_key_id(13);
+
+  util::StatusOr<std::unique_ptr<FakePrimitive>> aead =
+      (*wrapper)->Wrap(keyset, /*annotations=*/{});
+  ASSERT_THAT(aead, IsOk());
+  EXPECT_THAT((*aead)->get(), Eq(raw_key));
+}
+
+TEST(KeysetWrapperStoreTest, GetNonexistentWrapperFails) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+
+  EXPECT_THAT(store.Get<Mac>().status(), StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(KeysetWrapperStoreTest, IsEmpty) {
+  KeysetWrapperStore store;
+  EXPECT_EQ(store.IsEmpty(), true);
+
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+  EXPECT_THAT(store.IsEmpty(), false);
+}
+
+TEST(KeysetWrapperStoreTest, Move) {
+  RegistryImpl registry;
+  util::StatusOr<std::function<util::StatusOr<std::unique_ptr<FakePrimitive>>(
+      const KeyData& key_data)>>
+      primitive_getter = PrimitiveGetter(registry);
+  ASSERT_THAT(primitive_getter, IsOk());
+
+  KeysetWrapperStore store;
+  ASSERT_THAT(
+      (store.Add<FakePrimitive, FakePrimitive>(
+          absl::make_unique<FakePrimitiveWrapper>(), *primitive_getter)),
+      IsOk());
+
+  util::StatusOr<const KeysetWrapper<FakePrimitive>*> wrapper =
+      store.Get<FakePrimitive>();
+  ASSERT_THAT(wrapper, IsOk());
+
+  KeysetWrapperStore new_store = std::move(store);
+  wrapper = new_store.Get<FakePrimitive>();
+  ASSERT_THAT(wrapper, IsOk());
+
+  Keyset keyset;
+  std::string raw_key = AddAesGcmKeyToKeyset(keyset, 13, OutputPrefixType::TINK,
+                                             KeyStatusType::ENABLED);
+  keyset.set_primary_key_id(13);
+
+  util::StatusOr<std::unique_ptr<FakePrimitive>> aead =
+      (*wrapper)->Wrap(keyset, /*annotations=*/{});
+  ASSERT_THAT(aead, IsOk());
+  EXPECT_THAT((*aead)->get(), Eq(raw_key));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/legacy_proto_key.cc b/cc/internal/legacy_proto_key.cc
new file mode 100644
index 0000000..6a95107
--- /dev/null
+++ b/cc/internal/legacy_proto_key.cc
@@ -0,0 +1,94 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/legacy_proto_key.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::google::crypto::tink::KeyData;
+
+util::Status CheckKeyAccess(KeyData::KeyMaterialType key_material_type,
+                            absl::optional<SecretKeyAccessToken> token) {
+  if (key_material_type == KeyData::SYMMETRIC ||
+      key_material_type == KeyData::ASYMMETRIC_PRIVATE) {
+    if (!token.has_value()) {
+      return util::Status(
+          absl::StatusCode::kPermissionDenied,
+          "Missing secret key access token for legacy proto key.");
+    }
+  }
+  return util::OkStatus();
+}
+
+}  // namespace
+
+bool UnusableLegacyProtoParameters::operator==(const Parameters& other) const {
+  const UnusableLegacyProtoParameters* that =
+      dynamic_cast<const UnusableLegacyProtoParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  return type_url_ == that->type_url_ &&
+         output_prefix_type_ == that->output_prefix_type_;
+}
+
+util::StatusOr<LegacyProtoKey> LegacyProtoKey::Create(
+    ProtoKeySerialization serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  util::Status access_check_status =
+      CheckKeyAccess(serialization.KeyMaterialType(), token);
+  if (!access_check_status.ok()) {
+    return access_check_status;
+  }
+  return LegacyProtoKey(serialization);
+}
+
+bool LegacyProtoKey::operator==(const Key& other) const {
+  const LegacyProtoKey* that = dynamic_cast<const LegacyProtoKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  return serialization_.EqualsWithPotentialFalseNegatives(that->serialization_);
+}
+
+util::StatusOr<const ProtoKeySerialization*> LegacyProtoKey::Serialization(
+    absl::optional<SecretKeyAccessToken> token) const {
+  util::Status access_check_status =
+      CheckKeyAccess(serialization_.KeyMaterialType(), token);
+  if (!access_check_status.ok()) {
+    return access_check_status;
+  }
+  return &serialization_;
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/legacy_proto_key.h b/cc/internal/legacy_proto_key.h
new file mode 100644
index 0000000..9372cdf
--- /dev/null
+++ b/cc/internal/legacy_proto_key.h
@@ -0,0 +1,109 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_LEGACY_PROTO_KEY_H_
+#define TINK_INTERNAL_LEGACY_PROTO_KEY_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Parameters returned by `LegacyProtoKey::GetParameters()` that cannot be used
+// to create other LegacyProtoKey instances.
+class UnusableLegacyProtoParameters : public Parameters {
+ public:
+  // Copyable and movable.
+  UnusableLegacyProtoParameters(const UnusableLegacyProtoParameters& other) =
+      default;
+  UnusableLegacyProtoParameters& operator=(
+      const UnusableLegacyProtoParameters& other) = default;
+  UnusableLegacyProtoParameters(UnusableLegacyProtoParameters&& other) =
+      default;
+  UnusableLegacyProtoParameters& operator=(
+      UnusableLegacyProtoParameters&& other) = default;
+
+  explicit UnusableLegacyProtoParameters(
+      absl::string_view type_url,
+      google::crypto::tink::OutputPrefixType output_prefix_type)
+      : type_url_(type_url), output_prefix_type_(output_prefix_type) {}
+
+  bool HasIdRequirement() const override {
+    return output_prefix_type_ != google::crypto::tink::OutputPrefixType::RAW;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  std::string type_url_;
+  google::crypto::tink::OutputPrefixType output_prefix_type_;
+};
+
+// Key type for legacy proto keys.
+class LegacyProtoKey : public Key {
+ public:
+  // Copyable and movable.
+  LegacyProtoKey(const LegacyProtoKey& other) = default;
+  LegacyProtoKey& operator=(const LegacyProtoKey& other) = default;
+  LegacyProtoKey(LegacyProtoKey&& other) = default;
+  LegacyProtoKey& operator=(LegacyProtoKey&& other) = default;
+
+  // Creates `LegacyProtoKey` object from `serialization`.  Requires `token` if
+  // the key material type is either SYMMETRIC or ASYMMETRIC_PRIVATE.
+  static util::StatusOr<LegacyProtoKey> Create(
+      ProtoKeySerialization serialization,
+      absl::optional<SecretKeyAccessToken> token);
+
+  const Parameters& GetParameters() const override {
+    return unusable_proto_parameters_;
+  }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return serialization_.IdRequirement();
+  }
+
+  bool operator==(const Key& other) const override;
+
+  // Returns `ProtoKeySerialization` pointer for this object.  Requires `token`
+  // if the key material type is either SYMMETRIC or ASYMMETRIC_PRIVATE.
+  util::StatusOr<const ProtoKeySerialization*> Serialization(
+      absl::optional<SecretKeyAccessToken> token) const;
+
+ private:
+  explicit LegacyProtoKey(ProtoKeySerialization serialization)
+      : serialization_(serialization),
+        unusable_proto_parameters_(serialization.TypeUrl(),
+                                   serialization.GetOutputPrefixType()) {}
+
+  ProtoKeySerialization serialization_;
+  UnusableLegacyProtoParameters unusable_proto_parameters_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_LEGACY_PROTO_KEY_H_
diff --git a/cc/internal/legacy_proto_key_test.cc b/cc/internal/legacy_proto_key_test.cc
new file mode 100644
index 0000000..800d630
--- /dev/null
+++ b/cc/internal/legacy_proto_key_test.cc
@@ -0,0 +1,417 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/legacy_proto_key.h"
+
+#include <string>
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+class LegacyProtoKeyTest : public ::testing::Test {
+ protected:
+  // Although this is a friend class, this utility function is necessary to
+  // access `ProtoKeySerialization::EqualsWithPotentialFalseNegatives()`
+  // since the test fixtures are subclasses that would not have direct access.
+  bool Equals(ProtoKeySerialization serialization,
+              ProtoKeySerialization other) {
+    return serialization.EqualsWithPotentialFalseNegatives(other);
+  }
+};
+
+TEST_F(LegacyProtoKeyTest, CreateAndSerialization) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  EXPECT_THAT(key->GetIdRequirement(), Eq(12345));
+  EXPECT_THAT(key->GetParameters().HasIdRequirement(), IsTrue());
+  EXPECT_THAT(key->Serialization(InsecureSecretKeyAccess::Get()), IsOk());
+
+  util::StatusOr<const ProtoKeySerialization*> key_serialization =
+      key->Serialization(InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key_serialization.status(), IsOk());
+  EXPECT_THAT(Equals(**key_serialization, *serialization), IsTrue());
+}
+
+TEST_F(LegacyProtoKeyTest, Equals) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> other_key = LegacyProtoKey::Create(
+      *other_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key == *other_key);
+  EXPECT_TRUE(*other_key == *key);
+  EXPECT_FALSE(*key != *other_key);
+  EXPECT_FALSE(*other_key != *key);
+}
+
+TEST_F(LegacyProtoKeyTest, TypeUrlNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("other_type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> other_key = LegacyProtoKey::Create(
+      *other_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST_F(LegacyProtoKeyTest, SerializedKeyNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  RestrictedData other_serialized_key =
+      RestrictedData("other_serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", other_serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> other_key = LegacyProtoKey::Create(
+      *other_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST_F(LegacyProtoKeyTest, KeyMaterialTypeNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key, KeyData::REMOTE,
+                                    OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> other_key = LegacyProtoKey::Create(
+      *other_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST_F(LegacyProtoKeyTest, OutputPrefixTypeNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC,
+                                    OutputPrefixType::CRUNCHY,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> other_key = LegacyProtoKey::Create(
+      *other_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST_F(LegacyProtoKeyTest, IdRequirementNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/6789);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> other_key = LegacyProtoKey::Create(
+      *other_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+using AllOutputPrefixTypesTest =
+    TestWithParam<std::tuple<OutputPrefixType, absl::optional<int>>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AllOutputPrefixTypesTestSuite, AllOutputPrefixTypesTest,
+    Values(std::make_tuple(OutputPrefixType::RAW, absl::nullopt),
+           std::make_tuple(OutputPrefixType::TINK, 123),
+           std::make_tuple(OutputPrefixType::CRUNCHY, 456),
+           std::make_tuple(OutputPrefixType::LEGACY, 789)));
+
+TEST_P(AllOutputPrefixTypesTest, GetIdRequirement) {
+  OutputPrefixType output_prefix_type;
+  absl::optional<int> id_requirement;
+  std::tie(output_prefix_type, id_requirement) = GetParam();
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, output_prefix_type,
+                                    id_requirement);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  EXPECT_THAT(key->GetIdRequirement(), Eq(id_requirement));
+}
+
+using AllKeyMaterialTypesTest = TestWithParam<KeyData::KeyMaterialType>;
+
+INSTANTIATE_TEST_SUITE_P(AllKeyMaterialTypesTestSuite, AllKeyMaterialTypesTest,
+                         Values(KeyData::SYMMETRIC, KeyData::ASYMMETRIC_PRIVATE,
+                                KeyData::ASYMMETRIC_PUBLIC, KeyData::REMOTE));
+
+TEST_P(AllKeyMaterialTypesTest, CreateAndSerializationWithSecretAccessToken) {
+  KeyData::KeyMaterialType key_material_type = GetParam();
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    key_material_type, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<const ProtoKeySerialization*> key_serialization =
+      key->Serialization(InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key_serialization.status(), IsOk());
+}
+
+using SecretKeyMaterialTypesTest = TestWithParam<KeyData::KeyMaterialType>;
+
+INSTANTIATE_TEST_SUITE_P(SecretKeyMaterialTypesTestSuite,
+                         SecretKeyMaterialTypesTest,
+                         Values(KeyData::SYMMETRIC,
+                                KeyData::ASYMMETRIC_PRIVATE));
+
+TEST_P(SecretKeyMaterialTypesTest, CreateWithoutSecretAccessToken) {
+  KeyData::KeyMaterialType key_material_type = GetParam();
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    key_material_type, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, /*token=*/absl::nullopt);
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kPermissionDenied));
+}
+
+TEST_P(SecretKeyMaterialTypesTest, SerializationWithoutSecretAccessToken) {
+  KeyData::KeyMaterialType key_material_type = GetParam();
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    key_material_type, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  // Must use token for key creation.
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<const ProtoKeySerialization*> key_serialization =
+      key->Serialization(/*token=*/absl::nullopt);
+  ASSERT_THAT(key_serialization.status(),
+              StatusIs(absl::StatusCode::kPermissionDenied));
+}
+
+using NonSecretKeyMaterialTypesTest = TestWithParam<KeyData::KeyMaterialType>;
+
+INSTANTIATE_TEST_SUITE_P(NonSecretKeyMaterialTypesTestSuite,
+                         NonSecretKeyMaterialTypesTest,
+                         Values(KeyData::ASYMMETRIC_PUBLIC, KeyData::REMOTE));
+
+TEST_P(NonSecretKeyMaterialTypesTest, CreateWithoutSecretAccessToken) {
+  KeyData::KeyMaterialType key_material_type = GetParam();
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    key_material_type, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, /*token=*/absl::nullopt);
+  ASSERT_THAT(key.status(), IsOk());
+}
+
+TEST_P(NonSecretKeyMaterialTypesTest, SerializationWithoutSecretAccessToken) {
+  KeyData::KeyMaterialType key_material_type = GetParam();
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    key_material_type, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  // Must use token for key creation.
+  util::StatusOr<LegacyProtoKey> key =
+      LegacyProtoKey::Create(*serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<const ProtoKeySerialization*> key_serialization =
+      key->Serialization(/*token=*/absl::nullopt);
+  ASSERT_THAT(key_serialization.status(), IsOk());
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/legacy_proto_parameters.cc b/cc/internal/legacy_proto_parameters.cc
new file mode 100644
index 0000000..bf512f7
--- /dev/null
+++ b/cc/internal/legacy_proto_parameters.cc
@@ -0,0 +1,37 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/legacy_proto_parameters.h"
+
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+bool LegacyProtoParameters::operator==(const Parameters& other) const {
+  const LegacyProtoParameters* that =
+      dynamic_cast<const LegacyProtoParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  return serialization_.EqualsWithPotentialFalseNegatives(that->serialization_);
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/legacy_proto_parameters.h b/cc/internal/legacy_proto_parameters.h
new file mode 100644
index 0000000..e204c95
--- /dev/null
+++ b/cc/internal/legacy_proto_parameters.h
@@ -0,0 +1,61 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_LEGACY_PROTO_PARAMETERS_H_
+#define TINK_INTERNAL_LEGACY_PROTO_PARAMETERS_H_
+
+#include <utility>
+
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/parameters.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+class LegacyProtoParameters : public Parameters {
+ public:
+  // Copyable and movable.
+  LegacyProtoParameters(const LegacyProtoParameters& other) = default;
+  LegacyProtoParameters& operator=(const LegacyProtoParameters& other) =
+      default;
+  LegacyProtoParameters(LegacyProtoParameters&& other) = default;
+  LegacyProtoParameters& operator=(LegacyProtoParameters&& other) = default;
+
+  explicit LegacyProtoParameters(ProtoParametersSerialization serialization)
+      : serialization_(std::move(serialization)) {}
+
+  bool HasIdRequirement() const override {
+    return serialization_.GetKeyTemplate().output_prefix_type() !=
+           google::crypto::tink::OutputPrefixType::RAW;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+  const ProtoParametersSerialization& Serialization() const {
+    return serialization_;
+  }
+
+ private:
+  ProtoParametersSerialization serialization_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_LEGACY_PROTO_PARAMETERS_H_
diff --git a/cc/internal/legacy_proto_parameters_test.cc b/cc/internal/legacy_proto_parameters_test.cc
new file mode 100644
index 0000000..ad720c1
--- /dev/null
+++ b/cc/internal/legacy_proto_parameters_test.cc
@@ -0,0 +1,176 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/legacy_proto_parameters.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/test_proto.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::crypto::tink::test::IsOk;
+using ::google::crypto::tink::OutputPrefixType;
+using ::google::crypto::tink::TestProto;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+class LegacyProtoParametersTest : public ::testing::Test {
+ protected:
+  // Although this is a friend class, this utility function is necessary to
+  // access `ProtoParametersSerialization::EqualsWithPotentialFalseNegatives()`
+  // since the test fixtures are subclasses that would not have direct access.
+  bool Equals(ProtoParametersSerialization serialization,
+              ProtoParametersSerialization other) {
+    return serialization.EqualsWithPotentialFalseNegatives(other);
+  }
+};
+
+TEST_F(LegacyProtoParametersTest, CreateWithIdRequirement) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::TINK,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  LegacyProtoParameters parameters(*serialization);
+
+  EXPECT_THAT(parameters.HasIdRequirement(), IsTrue());
+  EXPECT_THAT(Equals(*serialization, parameters.Serialization()), IsTrue());
+}
+
+TEST_F(LegacyProtoParametersTest, CreateWithoutIdRequirement) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  LegacyProtoParameters parameters(*serialization);
+
+  EXPECT_THAT(parameters.HasIdRequirement(), IsFalse());
+  EXPECT_THAT(Equals(*serialization, parameters.Serialization()), IsTrue());
+}
+
+TEST_F(LegacyProtoParametersTest, Equals) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  LegacyProtoParameters parameters(*serialization);
+  LegacyProtoParameters other_parameters(*other_serialization);
+
+  EXPECT_TRUE(parameters == other_parameters);
+  EXPECT_TRUE(other_parameters == parameters);
+  EXPECT_FALSE(parameters != other_parameters);
+  EXPECT_FALSE(other_parameters != parameters);
+}
+
+TEST_F(LegacyProtoParametersTest, TypeUrlNotEqual) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("other_type_url",
+                                           OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  LegacyProtoParameters parameters(*serialization);
+  LegacyProtoParameters other_parameters(*other_serialization);
+
+  EXPECT_TRUE(parameters != other_parameters);
+  EXPECT_TRUE(other_parameters != parameters);
+  EXPECT_FALSE(parameters == other_parameters);
+  EXPECT_FALSE(other_parameters == parameters);
+}
+
+TEST_F(LegacyProtoParametersTest, OutputPrefixTypeNotEqual) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::TINK,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  LegacyProtoParameters parameters(*serialization);
+  LegacyProtoParameters other_parameters(*other_serialization);
+
+  EXPECT_TRUE(parameters != other_parameters);
+  EXPECT_TRUE(other_parameters != parameters);
+  EXPECT_FALSE(parameters == other_parameters);
+  EXPECT_FALSE(other_parameters == parameters);
+}
+
+TEST_F(LegacyProtoParametersTest, DifferentValueNotEqual) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+  TestProto other_proto;
+  other_proto.set_num(67890);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           other_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  LegacyProtoParameters parameters(*serialization);
+  LegacyProtoParameters other_parameters(*other_serialization);
+
+  EXPECT_TRUE(parameters != other_parameters);
+  EXPECT_TRUE(other_parameters != parameters);
+  EXPECT_FALSE(parameters == other_parameters);
+  EXPECT_FALSE(other_parameters == parameters);
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/md_util.cc b/cc/internal/md_util.cc
index 36d36a0..acca9fe 100644
--- a/cc/internal/md_util.cc
+++ b/cc/internal/md_util.cc
@@ -15,6 +15,8 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/md_util.h"
 
+#include <stdint.h>
+
 #include <string>
 
 #include "absl/status/status.h"
@@ -26,6 +28,7 @@
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/status.h"
+#include "tink/util/statusor.h"
 
 namespace crypto {
 namespace tink {
diff --git a/cc/internal/md_util_test.cc b/cc/internal/md_util_test.cc
index e40f390..0802070 100644
--- a/cc/internal/md_util_test.cc
+++ b/cc/internal/md_util_test.cc
@@ -15,15 +15,16 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/md_util.h"
 
-#include <cstdint>
 #include <string>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
-#include "absl/types/span.h"
+#include "absl/strings/string_view.h"
 #include "openssl/evp.h"
 #include "tink/subtle/common_enums.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 
diff --git a/cc/internal/monitoring_util.h b/cc/internal/monitoring_util.h
index dbf5e81..a2b2c13 100644
--- a/cc/internal/monitoring_util.h
+++ b/cc/internal/monitoring_util.h
@@ -22,8 +22,12 @@
 #include "absl/container/flat_hash_map.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/strip.h"
+#include "tink/internal/key_status_util.h"
+#include "tink/key_status.h"
 #include "tink/monitoring/monitoring.h"
 #include "tink/primitive_set.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
 
@@ -31,6 +35,8 @@
 namespace tink {
 namespace internal {
 
+constexpr char kKeyTypePrefix[] = "type.googleapis.com/google.crypto.";
+
 // Constructs a MonitoringKeySetInfo object from a PrimitiveSet `primitive_set`
 // for a given primitive P.
 template <class P>
@@ -48,30 +54,14 @@
   }
   std::vector<MonitoringKeySetInfo::Entry> keyset_info_entries = {};
   for (const auto& entry : primitive_set_entries) {
-    MonitoringKeySetInfo::Entry::KeyStatus key_status;
-    switch (entry->get_status()) {
-      case google::crypto::tink::KeyStatusType::ENABLED: {
-        key_status = MonitoringKeySetInfo::Entry::KeyStatus::kEnabled;
-        break;
-      }
-      case google::crypto::tink::KeyStatusType::DISABLED: {
-        key_status = MonitoringKeySetInfo::Entry::KeyStatus::kDisabled;
-        break;
-      }
-      case google::crypto::tink::KeyStatusType::DESTROYED: {
-        key_status = MonitoringKeySetInfo::Entry::KeyStatus::kDestroyed;
-        break;
-      }
-      default:
-        return util::Status(
-            absl::StatusCode::kInvalidArgument,
-            absl::StrCat("Unknown key status ", entry->get_status()));
-    }
+    util::StatusOr<KeyStatus> key_status =
+        FromKeyStatusType(entry->get_status());
+    if (!key_status.ok()) return key_status.status();
 
-    // TODO(b/222245356): Populate key_format_as_string with the actual key
-    // format when available. For now, we use the key type URL.
     auto keyset_info_entry = MonitoringKeySetInfo::Entry(
-        key_status, entry->get_key_id(), entry->get_key_type_url());
+        *key_status, entry->get_key_id(),
+        absl::StripPrefix(entry->get_key_type_url(), kKeyTypePrefix),
+        OutputPrefixType_Name(entry->get_output_prefix_type()));
     keyset_info_entries.push_back(keyset_info_entry);
   }
   MonitoringKeySetInfo keyset_info(primitive_set.get_annotations(),
diff --git a/cc/internal/monitoring_util_test.cc b/cc/internal/monitoring_util_test.cc
index dd81870..c08b4fa 100644
--- a/cc/internal/monitoring_util_test.cc
+++ b/cc/internal/monitoring_util_test.cc
@@ -15,17 +15,24 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/monitoring_util.h"
 
+#include <stdint.h>
+
 #include <memory>
 #include <string>
-#include <tuple>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "absl/container/flat_hash_map.h"
+#include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
+#include "tink/key_status.h"
 #include "tink/monitoring/monitoring.h"
 #include "tink/primitive_set.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 #include "proto/tink.pb.h"
 
@@ -89,7 +96,8 @@
 MATCHER_P(MonitoringKeySetInfoEntryEq, other, "") {
   return arg.GetStatus() == other.GetStatus() &&
          arg.GetKeyId() == other.GetKeyId() &&
-         arg.GetParametersAsString() == other.GetParametersAsString();
+         arg.GetKeyPrefix() == other.GetKeyPrefix() &&
+         arg.GetKeyType() == other.GetKeyType();
 }
 
 TEST(MonitoringUtilTest, MonitoringKeySetInfoFromPrimitiveSetValid) {
@@ -134,14 +142,15 @@
               UnorderedElementsAreArray(kAnnotations));
   const std::vector<MonitoringKeySetInfo::Entry> &monitoring_entries =
       monitoring_keyset_info->GetEntries();
-  EXPECT_THAT(monitoring_entries,
-              UnorderedElementsAre(
-                  MonitoringKeySetInfoEntryEq(MonitoringKeySetInfo::Entry(
-                      MonitoringKeySetInfo::Entry::KeyStatus::kEnabled,
-                      /*key_id=*/1, kPrimitive1KeyTyepUrl)),
-                  MonitoringKeySetInfoEntryEq(MonitoringKeySetInfo::Entry(
-                      MonitoringKeySetInfo::Entry::KeyStatus::kEnabled,
-                      /*key_id=*/2, kPrimitive2KeyTypeUrl))));
+  EXPECT_THAT(
+      monitoring_entries,
+      UnorderedElementsAre(
+          MonitoringKeySetInfoEntryEq(MonitoringKeySetInfo::Entry(
+              KeyStatus::kEnabled,
+              /*key_id=*/1, "tink.SomePrimitiveInstance", "TINK")),
+          MonitoringKeySetInfoEntryEq(MonitoringKeySetInfo::Entry(
+              KeyStatus::kEnabled,
+              /*key_id=*/2, "tink.SomeOtherPrimitiveInstance", "TINK"))));
 }
 
 }  // namespace
diff --git a/cc/internal/mutable_serialization_registry.cc b/cc/internal/mutable_serialization_registry.cc
new file mode 100644
index 0000000..250d026
--- /dev/null
+++ b/cc/internal/mutable_serialization_registry.cc
@@ -0,0 +1,121 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/mutable_serialization_registry.h"
+
+#include <memory>
+
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/synchronization/mutex.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/legacy_proto_key.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_registry.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+MutableSerializationRegistry& MutableSerializationRegistry::GlobalInstance() {
+  static MutableSerializationRegistry* instance =
+      new MutableSerializationRegistry();
+  return *instance;
+}
+
+util::Status MutableSerializationRegistry::RegisterParametersParser(
+    ParametersParser* parser) {
+  absl::MutexLock lock(&registry_mutex_);
+  SerializationRegistry::Builder builder(registry_);
+  util::Status status = builder.RegisterParametersParser(parser);
+  if (!status.ok()) return status;
+  registry_ = builder.Build();
+  return util::OkStatus();
+}
+
+util::Status MutableSerializationRegistry::RegisterParametersSerializer(
+    ParametersSerializer* serializer) {
+  absl::MutexLock lock(&registry_mutex_);
+  SerializationRegistry::Builder builder(registry_);
+  util::Status status = builder.RegisterParametersSerializer(serializer);
+  if (!status.ok()) return status;
+  registry_ = builder.Build();
+  return util::OkStatus();
+}
+
+util::Status MutableSerializationRegistry::RegisterKeyParser(
+    KeyParser* parser) {
+  absl::MutexLock lock(&registry_mutex_);
+  SerializationRegistry::Builder builder(registry_);
+  util::Status status = builder.RegisterKeyParser(parser);
+  if (!status.ok()) return status;
+  registry_ = builder.Build();
+  return util::OkStatus();
+}
+
+util::Status MutableSerializationRegistry::RegisterKeySerializer(
+    KeySerializer* serializer) {
+  absl::MutexLock lock(&registry_mutex_);
+  SerializationRegistry::Builder builder(registry_);
+  util::Status status = builder.RegisterKeySerializer(serializer);
+  if (!status.ok()) return status;
+  registry_ = builder.Build();
+  return util::OkStatus();
+}
+
+util::StatusOr<std::unique_ptr<Parameters>>
+MutableSerializationRegistry::ParseParameters(
+    const Serialization& serialization) {
+  absl::MutexLock lock(&registry_mutex_);
+  return registry_.ParseParameters(serialization);
+}
+
+util::StatusOr<std::unique_ptr<Key>> MutableSerializationRegistry::ParseKey(
+    const Serialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  absl::MutexLock lock(&registry_mutex_);
+  return registry_.ParseKey(serialization, token);
+}
+
+util::StatusOr<std::unique_ptr<Key>>
+MutableSerializationRegistry::ParseKeyWithLegacyFallback(
+    const Serialization& serialization, SecretKeyAccessToken token) {
+  util::StatusOr<std::unique_ptr<Key>> key = ParseKey(serialization, token);
+  if (key.status().code() == absl::StatusCode::kNotFound) {
+    const ProtoKeySerialization* proto_serialization =
+        dynamic_cast<const ProtoKeySerialization*>(&serialization);
+    util::StatusOr<LegacyProtoKey> proto_key = internal::LegacyProtoKey::Create(
+        *proto_serialization, InsecureSecretKeyAccess::Get());
+    if (!proto_key.ok()) return proto_key.status();
+    return {absl::make_unique<LegacyProtoKey>(*proto_key)};
+  }
+  if (!key.ok()) return key.status();
+  return key;
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/mutable_serialization_registry.h b/cc/internal/mutable_serialization_registry.h
new file mode 100644
index 0000000..a9914dd
--- /dev/null
+++ b/cc/internal/mutable_serialization_registry.h
@@ -0,0 +1,117 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_MUTABLE_SERIALIZATION_REGISTRY_H_
+#define TINK_INTERNAL_MUTABLE_SERIALIZATION_REGISTRY_H_
+
+#include <memory>
+
+#include "absl/base/thread_annotations.h"
+#include "absl/synchronization/mutex.h"
+#include "absl/types/optional.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_registry.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// This class provides a global, mutable serialization registry by wrapping an
+// instance of an immutable `SerializationRegistry`.  This registry will enable
+// the Tink 2.0 C++ Keyset API in the near term.
+class MutableSerializationRegistry {
+ public:
+  // Returns the global serialization registry.
+  static MutableSerializationRegistry& GlobalInstance();
+
+  // Registers parameters `parser`. Returns an error if a different parameters
+  // parser with the same parser index has already been registered.
+  util::Status RegisterParametersParser(ParametersParser* parser)
+      ABSL_LOCKS_EXCLUDED(registry_mutex_);
+
+  // Registers parameters `serializer`. Returns an error if a different
+  // parameters serializer with the same serializer index has already been
+  // registered.
+  util::Status RegisterParametersSerializer(ParametersSerializer* serializer)
+      ABSL_LOCKS_EXCLUDED(registry_mutex_);
+
+  // Registers key `parser`. Returns an error if a different key parser with the
+  // same parser index has already been registered.
+  util::Status RegisterKeyParser(KeyParser* parser)
+      ABSL_LOCKS_EXCLUDED(registry_mutex_);
+
+  // Registers key `serializer`. Returns an error if a different key serializer
+  // with the same serializer index has already been registered.
+  util::Status RegisterKeySerializer(KeySerializer* serializer)
+      ABSL_LOCKS_EXCLUDED(registry_mutex_);
+
+  // Parses `serialization` into a `Parameters` instance.
+  util::StatusOr<std::unique_ptr<Parameters>> ParseParameters(
+      const Serialization& serialization) ABSL_LOCKS_EXCLUDED(registry_mutex_);
+
+  // Serializes `parameters` into a `Serialization` instance.
+  template <typename SerializationT>
+  util::StatusOr<std::unique_ptr<Serialization>> SerializeParameters(
+      const Parameters& parameters) ABSL_LOCKS_EXCLUDED(registry_mutex_) {
+    absl::MutexLock lock(&registry_mutex_);
+    return registry_.SerializeParameters<SerializationT>(parameters);
+  }
+
+  // Parses `serialization` into a `Key` instance.
+  util::StatusOr<std::unique_ptr<Key>> ParseKey(
+      const Serialization& serialization,
+      absl::optional<SecretKeyAccessToken> token)
+      ABSL_LOCKS_EXCLUDED(registry_mutex_);
+
+  // Similar to `ParseKey` but falls back to legacy proto key serialization if
+  // the corresponding key parser is not found.
+  util::StatusOr<std::unique_ptr<Key>> ParseKeyWithLegacyFallback(
+      const Serialization& serialization, SecretKeyAccessToken token);
+
+  // Serializes `parameters` into a `Serialization` instance.
+  template <typename SerializationT>
+  util::StatusOr<std::unique_ptr<Serialization>> SerializeKey(
+      const Key& key, absl::optional<SecretKeyAccessToken> token)
+      ABSL_LOCKS_EXCLUDED(registry_mutex_) {
+    absl::MutexLock lock(&registry_mutex_);
+    return registry_.SerializeKey<SerializationT>(key, token);
+  }
+
+  // Resets to a new empty registry.
+  void Reset() ABSL_LOCKS_EXCLUDED(registry_mutex_) {
+    absl::MutexLock lock(&registry_mutex_);
+    registry_ = SerializationRegistry();
+  }
+
+ private:
+  mutable absl::Mutex registry_mutex_;
+  SerializationRegistry registry_ ABSL_GUARDED_BY(registry_mutex_);
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_MUTABLE_SERIALIZATION_REGISTRY_H_
diff --git a/cc/internal/mutable_serialization_registry_test.cc b/cc/internal/mutable_serialization_registry_test.cc
new file mode 100644
index 0000000..7bea8b8
--- /dev/null
+++ b/cc/internal/mutable_serialization_registry_test.cc
@@ -0,0 +1,379 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/mutable_serialization_registry.h"
+
+#include <memory>
+#include <string_view>
+#include <typeindex>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_test_util.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+TEST(MutableSerializationRegistryTest, ParseParameters) {
+  MutableSerializationRegistry registry;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersParserImpl<IdParamsSerialization, IdParams> parser2(kIdTypeUrl,
+                                                                ParseIdParams);
+  ASSERT_THAT(registry.RegisterParametersParser(&parser1), IsOk());
+  ASSERT_THAT(registry.RegisterParametersParser(&parser2), IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> no_id_params =
+      registry.ParseParameters(NoIdSerialization());
+  ASSERT_THAT(no_id_params, IsOk());
+  EXPECT_THAT((*no_id_params)->HasIdRequirement(), IsFalse());
+  EXPECT_THAT(std::type_index(typeid(**no_id_params)),
+              std::type_index(typeid(NoIdParams)));
+
+  util::StatusOr<std::unique_ptr<Parameters>> id_params =
+      registry.ParseParameters(IdParamsSerialization());
+  ASSERT_THAT(id_params, IsOk());
+  EXPECT_THAT((*id_params)->HasIdRequirement(), IsTrue());
+  EXPECT_THAT(std::type_index(typeid(**id_params)),
+              std::type_index(typeid(IdParams)));
+}
+
+TEST(MutableSerializationRegistryTest, ParseParametersWithoutRegistration) {
+  MutableSerializationRegistry registry;
+
+  ASSERT_THAT(registry.ParseParameters(NoIdSerialization()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(MutableSerializationRegistryTest, RegisterSameParametersParser) {
+  MutableSerializationRegistry registry;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser(kNoIdTypeUrl,
+                                                             ParseNoIdParams);
+
+  EXPECT_THAT(registry.RegisterParametersParser(&parser), IsOk());
+  EXPECT_THAT(registry.RegisterParametersParser(&parser), IsOk());
+}
+
+TEST(MutableSerializationRegistryTest,
+     RegisterDifferentParametersParserWithSameIndex) {
+  MutableSerializationRegistry registry;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser2(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+
+  EXPECT_THAT(registry.RegisterParametersParser(&parser1), IsOk());
+  EXPECT_THAT(registry.RegisterParametersParser(&parser2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(MutableSerializationRegistryTest, SerializeParameters) {
+  MutableSerializationRegistry registry;
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  ParametersSerializerImpl<IdParams, IdParamsSerialization> serializer2(
+      kIdTypeUrl, SerializeIdParams);
+  ASSERT_THAT(registry.RegisterParametersSerializer(&serializer1), IsOk());
+  ASSERT_THAT(registry.RegisterParametersSerializer(&serializer2), IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization1 =
+      registry.SerializeParameters<NoIdSerialization>(NoIdParams());
+  ASSERT_THAT(serialization1, IsOk());
+  EXPECT_THAT((*serialization1)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization2 =
+      registry.SerializeParameters<IdParamsSerialization>(IdParams());
+  ASSERT_THAT(serialization2, IsOk());
+  EXPECT_THAT((*serialization2)->ObjectIdentifier(), Eq(kIdTypeUrl));
+}
+
+TEST(MutableSerializationRegistryTest, SerializeParametersWithoutRegistration) {
+  MutableSerializationRegistry registry;
+
+  ASSERT_THAT(
+      registry.SerializeParameters<NoIdSerialization>(NoIdParams()).status(),
+      StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(MutableSerializationRegistryTest, RegisterSameParametersSerializer) {
+  MutableSerializationRegistry registry;
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  EXPECT_THAT(registry.RegisterParametersSerializer(&serializer), IsOk());
+  EXPECT_THAT(registry.RegisterParametersSerializer(&serializer), IsOk());
+}
+
+TEST(MutableSerializationRegistryTest,
+     RegisterDifferentParametersSerializerWithSameIndex) {
+  MutableSerializationRegistry registry;
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer2(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  EXPECT_THAT(registry.RegisterParametersSerializer(&serializer1), IsOk());
+  EXPECT_THAT(registry.RegisterParametersSerializer(&serializer2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(MutableSerializationRegistryTest, ParseKey) {
+  MutableSerializationRegistry registry;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser1(kNoIdTypeUrl, ParseNoIdKey);
+  KeyParserImpl<IdKeySerialization, IdKey> parser2(kIdTypeUrl, ParseIdKey);
+  ASSERT_THAT(registry.RegisterKeyParser(&parser1), IsOk());
+  ASSERT_THAT(registry.RegisterKeyParser(&parser2), IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> no_id_key =
+      registry.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(no_id_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**no_id_key)),
+              std::type_index(typeid(NoIdKey)));
+
+  util::StatusOr<std::unique_ptr<Key>> id_key = registry.ParseKey(
+      IdKeySerialization(/*id=*/123), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(id_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**id_key)),
+              std::type_index(typeid(IdKey)));
+  EXPECT_THAT((*id_key)->GetIdRequirement(), Eq(123));
+}
+
+TEST(MutableSerializationRegistryTest, ParseKeyNoSecretAccess) {
+  MutableSerializationRegistry registry;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser(kNoIdTypeUrl, ParseNoIdKey);
+  ASSERT_THAT(registry.RegisterKeyParser(&parser), IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> no_id_public_key =
+      registry.ParseKey(NoIdSerialization(), absl::nullopt);
+  ASSERT_THAT(no_id_public_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**no_id_public_key)),
+              std::type_index(typeid(NoIdKey)));
+}
+
+TEST(MutableSerializationRegistryTest, ParseKeyWithLegacyFallback) {
+  MutableSerializationRegistry registry;
+  KeyParserImpl<IdKeySerialization, IdKey> parser(kIdTypeUrl, ParseIdKey);
+  ASSERT_THAT(registry.RegisterKeyParser(&parser), IsOk());
+
+  // Parse key with registered key parser.
+  util::StatusOr<std::unique_ptr<Key>> id_key =
+      registry.ParseKeyWithLegacyFallback(IdKeySerialization(/*id=*/123),
+                                          InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(id_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**id_key)),
+              std::type_index(typeid(IdKey)));
+  EXPECT_THAT((*id_key)->GetIdRequirement(), Eq(123));
+
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/456);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  // Fall back to legacy proto key.
+  util::StatusOr<std::unique_ptr<Key>> proto_key =
+      registry.ParseKeyWithLegacyFallback(*serialization,
+                                          InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(proto_key, IsOk());
+  EXPECT_THAT((*proto_key)->GetIdRequirement(), Eq(456));
+}
+
+TEST(MutableSerializationRegistryTest, ParseKeyWithoutRegistration) {
+  MutableSerializationRegistry registry;
+
+  ASSERT_THAT(
+      registry.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get())
+          .status(),
+      StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(MutableSerializationRegistryTest, RegisterSameKeyParser) {
+  MutableSerializationRegistry registry;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser(kNoIdTypeUrl, ParseNoIdKey);
+
+  EXPECT_THAT(registry.RegisterKeyParser(&parser), IsOk());
+  EXPECT_THAT(registry.RegisterKeyParser(&parser), IsOk());
+}
+
+TEST(MutableSerializationRegistryTest,
+     RegisterDifferentKeyParserWithSameIndex) {
+  MutableSerializationRegistry registry;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser1(kNoIdTypeUrl, ParseNoIdKey);
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser2(kNoIdTypeUrl, ParseNoIdKey);
+
+  EXPECT_THAT(registry.RegisterKeyParser(&parser1), IsOk());
+  EXPECT_THAT(registry.RegisterKeyParser(&parser2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(MutableSerializationRegistryTest, SerializeKey) {
+  MutableSerializationRegistry registry;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer1(SerializeNoIdKey);
+  KeySerializerImpl<IdKey, IdKeySerialization> serializer2(SerializeIdKey);
+  ASSERT_THAT(registry.RegisterKeySerializer(&serializer1), IsOk());
+  ASSERT_THAT(registry.RegisterKeySerializer(&serializer2), IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization1 =
+      registry.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                               InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization1, IsOk());
+  EXPECT_THAT((*serialization1)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization2 =
+      registry.SerializeKey<IdKeySerialization>(IdKey(123),
+                                                InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization2, IsOk());
+  EXPECT_THAT((*serialization2)->ObjectIdentifier(), Eq(kIdTypeUrl));
+}
+
+TEST(MutableSerializationRegistryTest, SerializeKeyNoSecretAccess) {
+  MutableSerializationRegistry registry;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer(SerializeNoIdKey);
+  ASSERT_THAT(registry.RegisterKeySerializer(&serializer), IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      registry.SerializeKey<NoIdSerialization>(NoIdKey(), absl::nullopt);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(MutableSerializationRegistryTest, SerializeKeyWithoutRegistration) {
+  MutableSerializationRegistry registry;
+
+  ASSERT_THAT(registry
+                  .SerializeKey<NoIdSerialization>(
+                      NoIdKey(), InsecureSecretKeyAccess::Get())
+                  .status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(MutableSerializationRegistryTest, RegisterSameKeySerializer) {
+  MutableSerializationRegistry registry;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer(SerializeNoIdKey);
+
+  EXPECT_THAT(registry.RegisterKeySerializer(&serializer), IsOk());
+  EXPECT_THAT(registry.RegisterKeySerializer(&serializer), IsOk());
+}
+
+TEST(MutableSerializationRegistryTest,
+     RegisterDifferentKeySerializerWithSameIndex) {
+  MutableSerializationRegistry registry;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer1(SerializeNoIdKey);
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer2(SerializeNoIdKey);
+
+  EXPECT_THAT(registry.RegisterKeySerializer(&serializer1), IsOk());
+  EXPECT_THAT(registry.RegisterKeySerializer(&serializer2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(MutableSerializationRegistryTest, Reset) {
+  MutableSerializationRegistry registry;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> params_parser(
+      kNoIdTypeUrl, ParseNoIdParams);
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> params_serializer(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  KeyParserImpl<NoIdSerialization, NoIdKey> key_parser(kNoIdTypeUrl,
+                                                       ParseNoIdKey);
+  KeySerializerImpl<NoIdKey, NoIdSerialization> key_serializer(
+      SerializeNoIdKey);
+
+  ASSERT_THAT(registry.RegisterParametersParser(&params_parser), IsOk());
+  ASSERT_THAT(registry.RegisterParametersSerializer(&params_serializer),
+              IsOk());
+  ASSERT_THAT(registry.RegisterKeyParser(&key_parser), IsOk());
+  ASSERT_THAT(registry.RegisterKeySerializer(&key_serializer), IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      registry.ParseParameters(NoIdSerialization());
+  ASSERT_THAT(params, IsOk());
+  util::StatusOr<std::unique_ptr<Serialization>> serialization1 =
+      registry.SerializeParameters<NoIdSerialization>(NoIdParams());
+  ASSERT_THAT(serialization1, IsOk());
+  util::StatusOr<std::unique_ptr<Key>> key =
+      registry.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  util::StatusOr<std::unique_ptr<Serialization>> serialization2 =
+      registry.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                               InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization2, IsOk());
+
+  registry.Reset();
+
+  ASSERT_THAT(registry.ParseParameters(NoIdSerialization()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+  ASSERT_THAT(
+      registry.SerializeParameters<NoIdSerialization>(NoIdParams()).status(),
+      StatusIs(absl::StatusCode::kNotFound));
+  ASSERT_THAT(
+      registry.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get())
+          .status(),
+      StatusIs(absl::StatusCode::kNotFound));
+  ASSERT_THAT(registry
+                  .SerializeKey<NoIdSerialization>(
+                      NoIdKey(), InsecureSecretKeyAccess::Get())
+                  .status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(MutableSerializationRegistryTest, GlobalInstance) {
+  MutableSerializationRegistry::GlobalInstance().Reset();
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser(kNoIdTypeUrl,
+                                                             ParseNoIdParams);
+  ASSERT_THAT(
+      MutableSerializationRegistry::GlobalInstance().RegisterParametersParser(
+          &parser),
+      IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          NoIdSerialization());
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), IsFalse());
+  EXPECT_THAT(std::type_index(typeid(**params)),
+              std::type_index(typeid(NoIdParams)));
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/parameters_parser.h b/cc/internal/parameters_parser.h
new file mode 100644
index 0000000..81c8eb0
--- /dev/null
+++ b/cc/internal/parameters_parser.h
@@ -0,0 +1,114 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_PARAMETERS_PARSER_H_
+#define TINK_INTERNAL_PARAMETERS_PARSER_H_
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <typeindex>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/internal/parser_index.h"
+#include "tink/internal/serialization.h"
+#include "tink/parameters.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Non-template base class that can be used with internal registry map.
+class ParametersParser {
+ public:
+  // Parses `serialization` into a parameters object.
+  //
+  // This function is usually called on a `Serialization` subclass matching the
+  // value returned by `ObjectIdentifier()`. However, implementations should
+  // verify that this is the case.
+  virtual util::StatusOr<std::unique_ptr<Parameters>> ParseParameters(
+      const Serialization& serialization) const = 0;
+
+  // Returns the object identifier for `SerializationT`, which is only valid
+  // for the lifetime of this object.
+  //
+  // The object identifier is a unique identifier per registry for this object
+  // (in the standard proto serialization, it is the type URL). In other words,
+  // when registering a `ParametersParser`, the registry will invoke this to get
+  // the handled object identifier. In order to parse an object of
+  // `SerializationT`, the registry will then obtain the object identifier of
+  // this serialization object, and call the parser corresponding to this
+  // object.
+  virtual absl::string_view ObjectIdentifier() const = 0;
+
+  // Returns an index that can be used to look up the `ParametersParser`
+  // object registered for the `ParametersT` type in a registry.
+  virtual ParserIndex Index() const = 0;
+
+  virtual ~ParametersParser() = default;
+};
+
+// Parses `SerializationT` objects into `ParametersT` objects.
+template <typename SerializationT, typename ParametersT>
+class ParametersParserImpl : public ParametersParser {
+ public:
+  explicit ParametersParserImpl(
+      absl::string_view object_identifier,
+      const std::function<util::StatusOr<ParametersT>(SerializationT)>&
+          function)
+      : object_identifier_(object_identifier), function_(function) {}
+
+  util::StatusOr<std::unique_ptr<Parameters>> ParseParameters(
+      const Serialization& serialization) const override {
+    if (serialization.ObjectIdentifier() != object_identifier_) {
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Invalid object identifier for this parameters parser.");
+    }
+    const SerializationT* st =
+        dynamic_cast<const SerializationT*>(&serialization);
+    if (st == nullptr) {
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Invalid serialization type for this parameters parser.");
+    }
+    util::StatusOr<ParametersT> parameters = function_(*st);
+    if (!parameters.ok()) return parameters.status();
+    return {absl::make_unique<ParametersT>(std::move(*parameters))};
+  }
+
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+  ParserIndex Index() const override {
+    return ParserIndex::Create<SerializationT>(object_identifier_);
+  }
+
+ private:
+  std::string object_identifier_;
+  std::function<util::StatusOr<ParametersT>(SerializationT)> function_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_PARAMETERS_PARSER_H_
diff --git a/cc/internal/parameters_parser_test.cc b/cc/internal/parameters_parser_test.cc
new file mode 100644
index 0000000..9480b91
--- /dev/null
+++ b/cc/internal/parameters_parser_test.cc
@@ -0,0 +1,90 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/parameters_parser.h"
+
+#include <memory>
+#include <string_view>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "tink/internal/parser_index.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_test_util.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Eq;
+using ::testing::IsFalse;
+
+TEST(ParametersParserTest, Create) {
+  std::unique_ptr<ParametersParser> parser =
+      absl::make_unique<ParametersParserImpl<NoIdSerialization, NoIdParams>>(
+          kNoIdTypeUrl, ParseNoIdParams);
+
+  EXPECT_THAT(parser->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+  EXPECT_THAT(parser->Index(),
+              Eq(ParserIndex::Create<NoIdSerialization>(kNoIdTypeUrl)));
+}
+
+TEST(ParametersParserTest, ParseParameters) {
+  std::unique_ptr<ParametersParser> parser =
+      absl::make_unique<ParametersParserImpl<NoIdSerialization, NoIdParams>>(
+          kNoIdTypeUrl, ParseNoIdParams);
+
+  NoIdSerialization serialization;
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      parser->ParseParameters(serialization);
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), IsFalse());
+}
+
+TEST(ParametersParserTest, ParseParametersWithInvalidSerializationType) {
+  std::unique_ptr<ParametersParser> parser =
+      absl::make_unique<ParametersParserImpl<NoIdSerialization, NoIdParams>>(
+          kNoIdTypeUrl, ParseNoIdParams);
+
+  IdParamsSerialization serialization;
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      parser->ParseParameters(serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(ParametersParserTest, ParseParametersWithInvalidObjectIdentifier) {
+  std::unique_ptr<ParametersParser> parser =
+      absl::make_unique<ParametersParserImpl<NoIdSerialization, NoIdParams>>(
+          "mismatched_type_url", ParseNoIdParams);
+
+  IdParamsSerialization serialization;
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      parser->ParseParameters(serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/parameters_serializer.h b/cc/internal/parameters_serializer.h
new file mode 100644
index 0000000..a26c803
--- /dev/null
+++ b/cc/internal/parameters_serializer.h
@@ -0,0 +1,104 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_PARAMETERS_SERIALIZER_H_
+#define TINK_INTERNAL_PARAMETERS_SERIALIZER_H_
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <typeindex>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serializer_index.h"
+#include "tink/parameters.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Non-template base class that can be used with internal registry map.
+class ParametersSerializer {
+ public:
+  // Returns the serialization of `parameters`.
+  virtual util::StatusOr<std::unique_ptr<Serialization>> SerializeParameters(
+      const Parameters& parameters) const = 0;
+
+  // Returns the object identifier for this serialization, which is only valid
+  // for the lifetime of this object.
+  //
+  // The object identifier is a unique identifier per registry for this object
+  // (in the standard proto serialization, it is the type URL). In other words,
+  // when registering a `ParametersSerializer`, the registry will invoke this to
+  // get the handled object identifier. In order to serialize an object of
+  // `ParametersT`, the registry will then obtain the object identifier of
+  // this serialization object, and call the serializer corresponding to this
+  // object.
+  virtual absl::string_view ObjectIdentifier() const = 0;
+
+  // Returns an index that can be used to look up the `ParametersSerializer`
+  // object registered for the `ParametersT` type in a registry.
+  virtual SerializerIndex Index() const = 0;
+
+  virtual ~ParametersSerializer() = default;
+};
+
+// Serializes `ParametersT` objects into `SerializationT` objects.
+template <typename ParametersT, typename SerializationT>
+class ParametersSerializerImpl : public ParametersSerializer {
+ public:
+  explicit ParametersSerializerImpl(
+      absl::string_view object_identifier,
+      const std::function<util::StatusOr<SerializationT>(ParametersT)>&
+          function)
+      : object_identifier_(object_identifier), function_(function) {}
+
+  util::StatusOr<std::unique_ptr<Serialization>> SerializeParameters(
+      const Parameters& parameters) const override {
+    const ParametersT* pt = dynamic_cast<const ParametersT*>(&parameters);
+    if (pt == nullptr) {
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Invalid parameters type for this parameters serializer.");
+    }
+    util::StatusOr<SerializationT> serialization = function_(*pt);
+    if (!serialization.ok()) return serialization.status();
+    return {absl::make_unique<SerializationT>(std::move(*serialization))};
+  }
+
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+  SerializerIndex Index() const override {
+    return SerializerIndex::Create<ParametersT, SerializationT>();
+  }
+
+ private:
+  std::string object_identifier_;
+  std::function<util::StatusOr<SerializationT>(ParametersT)> function_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_PARAMETERS_SERIALIZER_H_
diff --git a/cc/internal/parameters_serializer_test.cc b/cc/internal/parameters_serializer_test.cc
new file mode 100644
index 0000000..0c76e08
--- /dev/null
+++ b/cc/internal/parameters_serializer_test.cc
@@ -0,0 +1,79 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/parameters_serializer.h"
+
+#include <memory>
+#include <string_view>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_test_util.h"
+#include "tink/internal/serializer_index.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Eq;
+
+TEST(ParametersSerializerTest, Create) {
+  std::unique_ptr<ParametersSerializer> serializer = absl::make_unique<
+      ParametersSerializerImpl<NoIdParams, NoIdSerialization>>(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  EXPECT_THAT(serializer->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+  EXPECT_THAT(serializer->Index(),
+              Eq(SerializerIndex::Create<NoIdParams, NoIdSerialization>()));
+}
+
+TEST(ParametersSerializerTest, SerializeParameters) {
+  std::unique_ptr<ParametersSerializer> serializer = absl::make_unique<
+      ParametersSerializerImpl<NoIdParams, NoIdSerialization>>(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  NoIdParams parameters;
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      serializer->SerializeParameters(parameters);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(ParametersSerializerTest, SerializeParametersWithInvalidParametersType) {
+  std::unique_ptr<ParametersSerializer> serializer = absl::make_unique<
+      ParametersSerializerImpl<NoIdParams, NoIdSerialization>>(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  IdParams parameters;
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      serializer->SerializeParameters(parameters);
+  ASSERT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/parser_index.h b/cc/internal/parser_index.h
new file mode 100644
index 0000000..2ddb52d
--- /dev/null
+++ b/cc/internal/parser_index.h
@@ -0,0 +1,71 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_PARSER_INDEX_H_
+#define TINK_INTERNAL_PARSER_INDEX_H_
+
+#include <string>
+#include <typeindex>
+
+#include "absl/strings/string_view.h"
+#include "tink/internal/serialization.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+class ParserIndex {
+ public:
+  // Create registry lookup key for a `SerializationT` type object with
+  // `object_identifier`. Useful for key and parameters parsers.
+  template <typename SerializationT>
+  static ParserIndex Create(absl::string_view object_identifier) {
+    return ParserIndex(std::type_index(typeid(SerializationT)),
+                       object_identifier);
+  }
+
+  // Create registry lookup key for `serialization`. Useful for the
+  // serialization registry.
+  static ParserIndex Create(const Serialization& serialization) {
+    return ParserIndex(std::type_index(typeid(serialization)),
+                       serialization.ObjectIdentifier());
+  }
+
+  // Returns true if serialization type index and object identifier match.
+  bool operator==(const ParserIndex& other) const {
+    return index_ == other.index_ &&
+           object_identifier_ == other.object_identifier_;
+  }
+
+  // Required function to make `ParserIndex` hashable for Abseil hash maps.
+  template <typename H>
+  friend H AbslHashValue(H h, const ParserIndex& index) {
+    return H::combine(std::move(h), index.index_, index.object_identifier_);
+  }
+
+ private:
+  ParserIndex(std::type_index index, absl::string_view object_identifier)
+      : index_(index), object_identifier_(object_identifier) {}
+
+  std::type_index index_;
+  std::string object_identifier_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_PARSER_INDEX_H_
diff --git a/cc/internal/parser_index_test.cc b/cc/internal/parser_index_test.cc
new file mode 100644
index 0000000..583b0ff
--- /dev/null
+++ b/cc/internal/parser_index_test.cc
@@ -0,0 +1,79 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/parser_index.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/strings/string_view.h"
+#include "tink/internal/serialization.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::testing::Eq;
+using ::testing::Not;
+
+class ExampleSerialization : public Serialization {
+ public:
+  explicit ExampleSerialization(absl::string_view object_identifier)
+      : object_identifier_(object_identifier) {}
+
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+ protected:
+  std::string object_identifier_;
+};
+
+class DifferentSerialization : public ExampleSerialization {
+ public:
+  explicit DifferentSerialization(absl::string_view object_identifier)
+      : ExampleSerialization(object_identifier) {}
+};
+
+TEST(ParserIndex, CreateEquivalent) {
+  ASSERT_THAT(ParserIndex::Create<ExampleSerialization>("id"),
+              Eq(ParserIndex::Create<ExampleSerialization>("id")));
+  ASSERT_THAT(ParserIndex::Create<ExampleSerialization>("id"),
+              Eq(ParserIndex::Create(ExampleSerialization("id"))));
+  ASSERT_THAT(ParserIndex::Create(ExampleSerialization("id")),
+              Eq(ParserIndex::Create(ExampleSerialization("id"))));
+}
+
+TEST(ParserIndex, CreateWithDifferentObjectIdentifier) {
+  ASSERT_THAT(
+      ParserIndex::Create<ExampleSerialization>("id"),
+      Not(Eq(ParserIndex::Create<ExampleSerialization>("different id"))));
+  ASSERT_THAT(
+      ParserIndex::Create(ExampleSerialization("id")),
+      Not(Eq(ParserIndex::Create(ExampleSerialization("different id")))));
+}
+
+TEST(ParserIndex, CreateWithDifferentSerializationType) {
+  ASSERT_THAT(ParserIndex::Create<ExampleSerialization>("id"),
+              Not(Eq(ParserIndex::Create<DifferentSerialization>("id"))));
+  ASSERT_THAT(ParserIndex::Create(ExampleSerialization("id")),
+              Not(Eq(ParserIndex::Create(DifferentSerialization("id")))));
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/proto_key_serialization.cc b/cc/internal/proto_key_serialization.cc
new file mode 100644
index 0000000..f92eb02
--- /dev/null
+++ b/cc/internal/proto_key_serialization.cc
@@ -0,0 +1,76 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/proto_key_serialization.h"
+
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/internal/util.h"
+#include "tink/restricted_data.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+
+util::StatusOr<ProtoKeySerialization> ProtoKeySerialization::Create(
+    absl::string_view type_url, RestrictedData serialized_key,
+    KeyData::KeyMaterialType key_material_type,
+    OutputPrefixType output_prefix_type, absl::optional<int> id_requirement) {
+  if (!IsPrintableAscii(type_url)) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Non-printable ASCII character in type URL.");
+  }
+  if (output_prefix_type == OutputPrefixType::RAW &&
+      id_requirement.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Keys with a RAW output prefix type should not have an "
+                        "ID requirement.");
+  }
+  if (output_prefix_type != OutputPrefixType::RAW &&
+      !id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Keys without a RAW output prefix type should have an ID requirement.");
+  }
+  return ProtoKeySerialization(type_url, type_url, std::move(serialized_key),
+                               key_material_type, output_prefix_type,
+                               id_requirement);
+}
+
+bool ProtoKeySerialization::EqualsWithPotentialFalseNegatives(
+    const ProtoKeySerialization& other) const {
+  if (type_url_ != other.type_url_) return false;
+  if (object_identifier_ != other.object_identifier_) return false;
+  if (key_material_type_ != other.key_material_type_) return false;
+  if (output_prefix_type_ != other.output_prefix_type_) return false;
+  if (id_requirement_ != other.id_requirement_) return false;
+  // RestrictedData::operator== is a constant-time comparison.
+  return serialized_key_ == other.serialized_key_;
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/proto_key_serialization.h b/cc/internal/proto_key_serialization.h
new file mode 100644
index 0000000..7c3972a
--- /dev/null
+++ b/cc/internal/proto_key_serialization.h
@@ -0,0 +1,110 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_PROTO_KEY_SERIALIZATION_H_
+#define TINK_INTERNAL_PROTO_KEY_SERIALIZATION_H_
+
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/internal/serialization.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Represents a `Key` object serialized with binary protocol buffer
+// serialization.
+class ProtoKeySerialization : public Serialization {
+ public:
+  // Copyable and movable.
+  ProtoKeySerialization(const ProtoKeySerialization& other) = default;
+  ProtoKeySerialization& operator=(const ProtoKeySerialization& other) =
+      default;
+  ProtoKeySerialization(ProtoKeySerialization&& other) = default;
+  ProtoKeySerialization& operator=(ProtoKeySerialization&& other) = default;
+
+  // Creates a `ProtoKeySerialization` object from individual components.
+  static util::StatusOr<ProtoKeySerialization> Create(
+      absl::string_view type_url, RestrictedData serialized_key,
+      google::crypto::tink::KeyData::KeyMaterialType key_material_type,
+      google::crypto::tink::OutputPrefixType output_prefix_type,
+      absl::optional<int> id_requirement);
+
+  // Returned value is only valid for the lifetime of this object.
+  absl::string_view TypeUrl() const { return type_url_; }
+
+  // Returned value is only valid for the lifetime of this object.
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+  // Returned value is only valid for the lifetime of this object.
+  RestrictedData SerializedKeyProto() const { return serialized_key_; }
+
+  google::crypto::tink::KeyData::KeyMaterialType KeyMaterialType() const {
+    return key_material_type_;
+  }
+
+  google::crypto::tink::OutputPrefixType GetOutputPrefixType() const {
+    return output_prefix_type_;
+  }
+
+  absl::optional<int> IdRequirement() const { return id_requirement_; }
+
+ private:
+  friend class ProtoKeySerializationTest;
+  friend class LegacyProtoKey;
+  friend class LegacyProtoKeyTest;
+
+  ProtoKeySerialization(
+      absl::string_view type_url, absl::string_view object_identifier,
+      RestrictedData serialized_key,
+      google::crypto::tink::KeyData::KeyMaterialType key_material_type,
+      google::crypto::tink::OutputPrefixType output_prefix_type,
+      absl::optional<int> id_requirement)
+      : type_url_(type_url),
+        object_identifier_(object_identifier),
+        serialized_key_(std::move(serialized_key)),
+        key_material_type_(key_material_type),
+        output_prefix_type_(output_prefix_type),
+        id_requirement_(id_requirement) {}
+
+  // Returns `true` if this `ProtoKeySerialization` object is equal to
+  // `other` (with the possibility of false negatives due to lack of
+  // determinism during serialization).  Should only be used temporarily by the
+  // `LegacyKeyParameters` class.
+  bool EqualsWithPotentialFalseNegatives(
+      const ProtoKeySerialization& other) const;
+
+  std::string type_url_;
+  std::string object_identifier_;
+  RestrictedData serialized_key_;
+  google::crypto::tink::KeyData::KeyMaterialType key_material_type_;
+  google::crypto::tink::OutputPrefixType output_prefix_type_;
+  absl::optional<int> id_requirement_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_PROTO_KEY_SERIALIZATION_H_
diff --git a/cc/internal/proto_key_serialization_test.cc b/cc/internal/proto_key_serialization_test.cc
new file mode 100644
index 0000000..89eff96
--- /dev/null
+++ b/cc/internal/proto_key_serialization_test.cc
@@ -0,0 +1,213 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/proto_key_serialization.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+class ProtoKeySerializationTest : public ::testing::Test {
+ protected:
+  bool Equals(ProtoKeySerialization serialization,
+              ProtoKeySerialization other) {
+    return serialization.EqualsWithPotentialFalseNegatives(other);
+  }
+};
+
+TEST_F(ProtoKeySerializationTest, CreateWithIdRequirement) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  EXPECT_THAT(serialization->TypeUrl(), Eq("type_url"));
+  EXPECT_THAT(serialization->SerializedKeyProto(), Eq(serialized_key));
+  EXPECT_THAT(serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(serialization->GetOutputPrefixType(), Eq(OutputPrefixType::TINK));
+  EXPECT_THAT(serialization->IdRequirement(), Eq(12345));
+  EXPECT_THAT(serialization->ObjectIdentifier(), Eq("type_url"));
+}
+
+TEST_F(ProtoKeySerializationTest, CreateWithoutIdRequirement) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::RAW,
+                                    /*id_requirement=*/absl::nullopt);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  EXPECT_THAT(serialization->TypeUrl(), Eq("type_url"));
+  EXPECT_THAT(serialization->SerializedKeyProto(), Eq(serialized_key));
+  EXPECT_THAT(serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(serialization->GetOutputPrefixType(), Eq(OutputPrefixType::RAW));
+  EXPECT_THAT(serialization->IdRequirement(), Eq(absl::nullopt));
+  EXPECT_THAT(serialization->ObjectIdentifier(), Eq("type_url"));
+}
+
+TEST_F(ProtoKeySerializationTest, OutputPrefixIncompatibleWithIdRequirement) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> tink_without_id =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/absl::nullopt);
+  ASSERT_THAT(tink_without_id.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+
+  util::StatusOr<ProtoKeySerialization> raw_with_id =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::RAW,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(raw_with_id.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(ProtoKeySerializationTest, Equals) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsTrue());
+}
+
+TEST_F(ProtoKeySerializationTest, TypeUrlAndObjectIdentifierNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("different_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+TEST_F(ProtoKeySerializationTest, SerializedKeyNotEqual) {
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create(
+          "type_url",
+          RestrictedData("serialized_key", InsecureSecretKeyAccess::Get()),
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create(
+          "type_url",
+          RestrictedData("different_key", InsecureSecretKeyAccess::Get()),
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+TEST_F(ProtoKeySerializationTest, KeyMaterialTypeNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key, KeyData::REMOTE,
+                                    OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+TEST_F(ProtoKeySerializationTest, OutputPrefixTypeNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC,
+                                    OutputPrefixType::CRUNCHY,
+                                    /*id_requirement=*/12345);
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+TEST_F(ProtoKeySerializationTest, IdRequirementNotEqual) {
+  RestrictedData serialized_key =
+      RestrictedData("serialized_key", InsecureSecretKeyAccess::Get());
+  util::StatusOr<ProtoKeySerialization> serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/12345);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoKeySerialization> other_serialization =
+      ProtoKeySerialization::Create("type_url", serialized_key,
+                                    KeyData::SYMMETRIC, OutputPrefixType::TINK,
+                                    /*id_requirement=*/6789);
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/proto_parameters_serialization.cc b/cc/internal/proto_parameters_serialization.cc
new file mode 100644
index 0000000..f3bd4bd
--- /dev/null
+++ b/cc/internal/proto_parameters_serialization.cc
@@ -0,0 +1,84 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/proto_parameters_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "tink/internal/util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+
+util::StatusOr<ProtoParametersSerialization>
+ProtoParametersSerialization::Create(absl::string_view type_url,
+                                     OutputPrefixType output_prefix_type,
+                                     absl::string_view serialized_proto) {
+  if (!IsPrintableAscii(type_url)) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Non-printable ASCII character in type URL.");
+  }
+  KeyTemplate key_template;
+  key_template.set_type_url(std::string(type_url));
+  key_template.set_output_prefix_type(output_prefix_type);
+  key_template.set_value(std::string(serialized_proto));
+  return ProtoParametersSerialization(key_template);
+}
+
+util::StatusOr<ProtoParametersSerialization>
+ProtoParametersSerialization::Create(KeyTemplate key_template) {
+  if (!IsPrintableAscii(key_template.type_url())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Non-printable ASCII character in type URL.");
+  }
+  return ProtoParametersSerialization(key_template);
+}
+
+bool ProtoParametersSerialization::EqualsWithPotentialFalseNegatives(
+    const ProtoParametersSerialization& other) const {
+  const ProtoParametersSerialization* that =
+      dynamic_cast<const ProtoParametersSerialization*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (key_template_.type_url() != that->key_template_.type_url()) {
+    return false;
+  }
+  if (key_template_.output_prefix_type() !=
+      that->key_template_.output_prefix_type()) {
+    return false;
+  }
+  if (key_template_.value() != that->key_template_.value()) {
+    return false;
+  }
+  if (object_identifier_ != that->object_identifier_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/proto_parameters_serialization.h b/cc/internal/proto_parameters_serialization.h
new file mode 100644
index 0000000..cb32d8f
--- /dev/null
+++ b/cc/internal/proto_parameters_serialization.h
@@ -0,0 +1,89 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_PROTO_PARAMETERS_SERIALIZATION_H_
+#define TINK_INTERNAL_PROTO_PARAMETERS_SERIALIZATION_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "tink/internal/serialization.h"
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Represents a `Parameters` object serialized with binary protocol buffer
+// serialization.
+class ProtoParametersSerialization : public Serialization {
+ public:
+  // Copyable and movable.
+  ProtoParametersSerialization(const ProtoParametersSerialization& other) =
+      default;
+  ProtoParametersSerialization& operator=(
+      const ProtoParametersSerialization& other) = default;
+  ProtoParametersSerialization(ProtoParametersSerialization&& other) = default;
+  ProtoParametersSerialization& operator=(
+      ProtoParametersSerialization&& other) = default;
+
+  // Creates a `ProtoParametersSerialization` object from individual components.
+  static util::StatusOr<ProtoParametersSerialization> Create(
+      absl::string_view type_url,
+      google::crypto::tink::OutputPrefixType output_prefix_type,
+      absl::string_view serialized_proto);
+
+  // Creates a `ProtoParametersSerialization` object from a key template.
+  static util::StatusOr<ProtoParametersSerialization> Create(
+      google::crypto::tink::KeyTemplate key_template);
+
+  const google::crypto::tink::KeyTemplate& GetKeyTemplate() const {
+    return key_template_;
+  }
+
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+ private:
+  // The following friend classes require access to
+  // `EqualsWithPotentialFalseNegatives()`.
+  friend class ProtoParametersSerializationTest;
+  friend class LegacyProtoParameters;
+  friend class LegacyProtoParametersTest;
+
+  explicit ProtoParametersSerialization(
+      google::crypto::tink::KeyTemplate key_template)
+      : key_template_(key_template),
+        object_identifier_(key_template.type_url()) {}
+
+  // Returns `true` if this `ProtoParametersSerialization` object is equal to
+  // `other` (with the possibility of false negatives due to lack of
+  // determinism during serialization).  Should only be used temporarily by the
+  // `LegacyProtoParameters` class.
+  bool EqualsWithPotentialFalseNegatives(
+      const ProtoParametersSerialization& other) const;
+
+  google::crypto::tink::KeyTemplate key_template_;
+  std::string object_identifier_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_PROTO_PARAMETERS_SERIALIZATION_H_
diff --git a/cc/internal/proto_parameters_serialization_test.cc b/cc/internal/proto_parameters_serialization_test.cc
new file mode 100644
index 0000000..1b13411
--- /dev/null
+++ b/cc/internal/proto_parameters_serialization_test.cc
@@ -0,0 +1,162 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/proto_parameters_serialization.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/test_proto.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::crypto::tink::test::IsOk;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+using ::google::crypto::tink::TestProto;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+class ProtoParametersSerializationTest : public ::testing::Test {
+ protected:
+  bool Equals(ProtoParametersSerialization serialization,
+              ProtoParametersSerialization other) {
+    return serialization.EqualsWithPotentialFalseNegatives(other);
+  }
+};
+
+TEST_F(ProtoParametersSerializationTest, CreateFromIndividualComponents) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  EXPECT_THAT(serialization->ObjectIdentifier(), "type_url");
+  EXPECT_THAT(serialization->GetKeyTemplate().type_url(), "type_url");
+  EXPECT_THAT(serialization->GetKeyTemplate().output_prefix_type(),
+              OutputPrefixType::RAW);
+  EXPECT_THAT(serialization->GetKeyTemplate().value(),
+              test_proto.SerializeAsString());
+  TestProto parsed_proto;
+  parsed_proto.ParseFromString(serialization->GetKeyTemplate().value());
+  EXPECT_THAT(parsed_proto.num(), Eq(12345));
+}
+
+TEST_F(ProtoParametersSerializationTest, CreateFromKeyTemplate) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+  KeyTemplate key_template;
+  key_template.set_value(test_proto.SerializeAsString());
+  key_template.set_output_prefix_type(OutputPrefixType::TINK);
+  key_template.set_type_url("type_url");
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create(key_template);
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  EXPECT_THAT(serialization->ObjectIdentifier(), "type_url");
+  EXPECT_THAT(serialization->GetKeyTemplate().type_url(), "type_url");
+  EXPECT_THAT(serialization->GetKeyTemplate().output_prefix_type(),
+              OutputPrefixType::TINK);
+  EXPECT_THAT(serialization->GetKeyTemplate().value(),
+              test_proto.SerializeAsString());
+  TestProto parsed_proto;
+  parsed_proto.ParseFromString(serialization->GetKeyTemplate().value());
+  EXPECT_THAT(parsed_proto.num(), Eq(12345));
+}
+
+TEST_F(ProtoParametersSerializationTest, Equals) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsTrue());
+}
+
+TEST_F(ProtoParametersSerializationTest, TypeUrlNotEqual) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("other_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+TEST_F(ProtoParametersSerializationTest, OutputPrefixTypeNotEqual) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::TINK,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+TEST_F(ProtoParametersSerializationTest, DifferentValueNotEqual) {
+  TestProto test_proto;
+  test_proto.set_num(12345);
+  TestProto other_proto;
+  other_proto.set_num(67890);
+
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           test_proto.SerializeAsString());
+  ASSERT_THAT(serialization.status(), IsOk());
+
+  util::StatusOr<ProtoParametersSerialization> other_serialization =
+      ProtoParametersSerialization::Create("type_url", OutputPrefixType::RAW,
+                                           other_proto.SerializeAsString());
+  ASSERT_THAT(other_serialization.status(), IsOk());
+
+  EXPECT_THAT(Equals(*serialization, *other_serialization), IsFalse());
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/registry_impl.cc b/cc/internal/registry_impl.cc
index 2b68992..ba74919 100644
--- a/cc/internal/registry_impl.cc
+++ b/cc/internal/registry_impl.cc
@@ -13,58 +13,66 @@
 // limitations under the License.
 //
 ///////////////////////////////////////////////////////////////////////////////
+
 #include "tink/internal/registry_impl.h"
 
+#include <functional>
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/synchronization/mutex.h"
+#include "tink/input_stream.h"
+#include "tink/internal/keyset_wrapper_store.h"
+#include "tink/key_manager.h"
 #include "tink/monitoring/monitoring.h"
 #include "tink/util/errors.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::util::StatusOr;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::KeyTemplate;
-
 namespace crypto {
 namespace tink {
 namespace internal {
 
 using ::crypto::tink::MonitoringClientFactory;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::KeyTemplate;
 
-StatusOr<const RegistryImpl::KeyTypeInfo*> RegistryImpl::get_key_type_info(
+util::StatusOr<const KeyTypeInfoStore::Info*> RegistryImpl::get_key_type_info(
     absl::string_view type_url) const {
   absl::MutexLock lock(&maps_mutex_);
-  auto it = type_url_to_info_.find(type_url);
-  if (it == type_url_to_info_.end()) {
-    return ToStatusF(absl::StatusCode::kNotFound,
-                     "No manager for type '%s' has been registered.", type_url);
-  }
-  return &it->second;
+  return key_type_info_store_.Get(type_url);
 }
 
-StatusOr<std::unique_ptr<KeyData>> RegistryImpl::NewKeyData(
+util::StatusOr<std::unique_ptr<KeyData>> RegistryImpl::NewKeyData(
     const KeyTemplate& key_template) const {
-  auto key_type_info_or = get_key_type_info(key_template.type_url());
-  if (!key_type_info_or.ok()) return key_type_info_or.status();
-  if (!key_type_info_or.value()->new_key_allowed()) {
+  util::StatusOr<const internal::KeyTypeInfoStore::Info*> info =
+      get_key_type_info(key_template.type_url());
+  if (!info.ok()) {
+    return info.status();
+  }
+  if (!(*info)->new_key_allowed()) {
     return crypto::tink::util::Status(
         absl::StatusCode::kInvalidArgument,
         absl::StrCat("KeyManager for type ", key_template.type_url(),
                      " does not allow for creation of new keys."));
   }
-  return key_type_info_or.value()->key_factory().NewKeyData(
-      key_template.value());
+  return (*info)->key_factory().NewKeyData(key_template.value());
 }
 
-StatusOr<std::unique_ptr<KeyData>> RegistryImpl::GetPublicKeyData(
+util::StatusOr<std::unique_ptr<KeyData>> RegistryImpl::GetPublicKeyData(
     absl::string_view type_url,
     absl::string_view serialized_private_key) const {
-  auto key_type_info_or = get_key_type_info(type_url);
-  if (!key_type_info_or.ok()) return key_type_info_or.status();
-  auto factory = dynamic_cast<const PrivateKeyFactory*>(
-      &key_type_info_or.value()->key_factory());
+  util::StatusOr<const internal::KeyTypeInfoStore::Info*> info =
+      get_key_type_info(type_url);
+  if (!info.ok()) {
+    return info.status();
+  }
+  auto factory =
+      dynamic_cast<const PrivateKeyFactory*>(&(*info)->key_factory());
   if (factory == nullptr) {
     return ToStatusF(absl::StatusCode::kInvalidArgument,
                      "KeyManager for type '%s' does not have "
@@ -75,41 +83,20 @@
   return result;
 }
 
-crypto::tink::util::Status RegistryImpl::CheckInsertable(
-    absl::string_view type_url, const std::type_index& key_manager_type_index,
-    bool new_key_allowed) const {
-  auto it = type_url_to_info_.find(type_url);
-
-  if (it == type_url_to_info_.end()) {
-    return crypto::tink::util::OkStatus();
+util::StatusOr<KeyData> RegistryImpl::DeriveKey(const KeyTemplate& key_template,
+                                                InputStream* randomness) const {
+  util::StatusOr<const internal::KeyTypeInfoStore::Info*> info =
+      get_key_type_info(key_template.type_url());
+  if (!info.ok()) {
+    return info.status();
   }
-  if (it->second.key_manager_type_index() != key_manager_type_index) {
-    return ToStatusF(absl::StatusCode::kAlreadyExists,
-                     "A manager for type '%s' has been already registered.",
-                     type_url);
-  }
-  if (!it->second.new_key_allowed() && new_key_allowed) {
-    return ToStatusF(absl::StatusCode::kAlreadyExists,
-                     "A manager for type '%s' has been already registered "
-                     "with forbidden new key operation.",
-                     type_url);
-  }
-  return crypto::tink::util::OkStatus();
-}
-
-crypto::tink::util::StatusOr<google::crypto::tink::KeyData>
-RegistryImpl::DeriveKey(const google::crypto::tink::KeyTemplate& key_template,
-                        InputStream* randomness) const {
-  auto key_type_info_or = get_key_type_info(key_template.type_url());
-  if (!key_type_info_or.ok()) return key_type_info_or.status();
-  if (!key_type_info_or.value()->key_deriver()) {
+  if (!(*info)->key_deriver()) {
     return crypto::tink::util::Status(
         absl::StatusCode::kInvalidArgument,
         absl::StrCat("Manager for type '", key_template.type_url(),
                      "' cannot derive keys."));
   }
-  return key_type_info_or.value()->key_deriver()(key_template.value(),
-                                                 randomness);
+  return (*info)->key_deriver()(key_template.value(), randomness);
 }
 
 util::Status RegistryImpl::RegisterMonitoringClientFactory(
@@ -126,9 +113,8 @@
 void RegistryImpl::Reset() {
   {
     absl::MutexLock lock(&maps_mutex_);
-    type_url_to_info_.clear();
-    name_to_catalogue_map_.clear();
-    primitive_to_wrapper_.clear();
+    key_type_info_store_ = KeyTypeInfoStore();
+    keyset_wrapper_store_ = KeysetWrapperStore();
   }
   {
     absl::MutexLock lock(&monitoring_factory_mutex_);
diff --git a/cc/internal/registry_impl.h b/cc/internal/registry_impl.h
index 5992b82..4bf79c1 100644
--- a/cc/internal/registry_impl.h
+++ b/cc/internal/registry_impl.h
@@ -13,44 +13,34 @@
 // limitations under the License.
 //
 ///////////////////////////////////////////////////////////////////////////////
+
 #ifndef TINK_INTERNAL_REGISTRY_IMPL_H_
 #define TINK_INTERNAL_REGISTRY_IMPL_H_
 
 #include <algorithm>
-#include <functional>
-#include <initializer_list>
 #include <memory>
 #include <string>
-#include <tuple>
-#include <typeindex>
-#include <typeinfo>
 #include <utility>
 
 #include "absl/base/thread_annotations.h"
 #include "absl/container/flat_hash_map.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
-#include "absl/strings/str_join.h"
 #include "absl/strings/string_view.h"
 #include "absl/synchronization/mutex.h"
-#include "absl/types/optional.h"
-#include "tink/catalogue.h"
-#include "tink/core/key_manager_impl.h"
 #include "tink/core/key_type_manager.h"
-#include "tink/core/private_key_manager_impl.h"
 #include "tink/core/private_key_type_manager.h"
+#include "tink/input_stream.h"
 #include "tink/internal/fips_utils.h"
+#include "tink/internal/key_type_info_store.h"
 #include "tink/internal/keyset_wrapper.h"
-#include "tink/internal/keyset_wrapper_impl.h"
+#include "tink/internal/keyset_wrapper_store.h"
 #include "tink/key_manager.h"
 #include "tink/monitoring/monitoring.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
-#include "tink/util/errors.h"
-#include "tink/util/protobuf_helper.h"
 #include "tink/util/status.h"
-#include "tink/util/validation.h"
+#include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
@@ -68,17 +58,9 @@
   RegistryImpl(const RegistryImpl&) = delete;
   RegistryImpl& operator=(const RegistryImpl&) = delete;
 
-  template <class P>
-  crypto::tink::util::StatusOr<const Catalogue<P>*> get_catalogue(
-      absl::string_view catalogue_name) const ABSL_LOCKS_EXCLUDED(maps_mutex_);
-
-  template <class P>
-  crypto::tink::util::Status AddCatalogue(absl::string_view catalogue_name,
-                                          Catalogue<P>* catalogue)
-      ABSL_LOCKS_EXCLUDED(maps_mutex_);
-
   // Registers the given 'manager' for the key type 'manager->get_key_type()'.
-  // Takes ownership of 'manager', which must be non-nullptr.
+  // Takes ownership of 'manager', which must be non-nullptr. KeyManager is the
+  // legacy/internal version of KeyTypeManager.
   template <class P>
   crypto::tink::util::Status RegisterKeyManager(KeyManager<P>* manager,
                                                 bool new_key_allowed = true)
@@ -91,15 +73,15 @@
           manager,
       bool new_key_allowed) ABSL_LOCKS_EXCLUDED(maps_mutex_);
 
-  // Takes ownership of 'private_key_manager' and 'public_key_manager'. Both
-  // must be non-nullptr.
+  // Takes ownership of 'private_manager' and 'public_manager'. Both must be
+  // non-nullptr.
   template <class PrivateKeyProto, class KeyFormatProto, class PublicKeyProto,
             class PrivatePrimitivesList, class PublicPrimitivesList>
   crypto::tink::util::Status RegisterAsymmetricKeyManagers(
       PrivateKeyTypeManager<PrivateKeyProto, KeyFormatProto, PublicKeyProto,
-                            PrivatePrimitivesList>* private_key_manager,
+                            PrivatePrimitivesList>* private_manager,
       KeyTypeManager<PublicKeyProto, void, PublicPrimitivesList>*
-          public_key_manager,
+          public_manager,
       bool new_key_allowed) ABSL_LOCKS_EXCLUDED(maps_mutex_);
 
   template <class P>
@@ -116,11 +98,6 @@
       const google::crypto::tink::KeyData& key_data) const
       ABSL_LOCKS_EXCLUDED(maps_mutex_);
 
-  template <class P>
-  crypto::tink::util::StatusOr<std::unique_ptr<P>> GetPrimitive(
-      absl::string_view type_url, const portable_proto::MessageLite& key) const
-      ABSL_LOCKS_EXCLUDED(maps_mutex_);
-
   crypto::tink::util::StatusOr<std::unique_ptr<google::crypto::tink::KeyData>>
   NewKeyData(const google::crypto::tink::KeyTemplate& key_template) const
       ABSL_LOCKS_EXCLUDED(maps_mutex_);
@@ -166,279 +143,23 @@
   }
 
  private:
-  // All information for a given type url.
-  class KeyTypeInfo {
-   public:
-    // Takes ownership of the 'key_manager'.
-    template <typename P>
-    KeyTypeInfo(KeyManager<P>* key_manager, bool new_key_allowed)
-        : key_manager_type_index_(std::type_index(typeid(*key_manager))),
-          public_key_manager_type_index_(absl::nullopt),
-          new_key_allowed_(new_key_allowed),
-          internal_key_factory_(nullptr),
-          key_factory_(&key_manager->get_key_factory()),
-          key_type_manager_(nullptr) {
-      primitive_to_manager_.emplace(std::type_index(typeid(P)),
-                                    absl::WrapUnique(key_manager));
-    }
-
-    // Takes ownership of the 'key_manager'.
-    template <typename KeyProto, typename KeyFormatProto,
-              typename... Primitives>
-    KeyTypeInfo(KeyTypeManager<KeyProto, KeyFormatProto, List<Primitives...>>*
-                    key_manager,
-                bool new_key_allowed)
-        : key_manager_type_index_(std::type_index(typeid(*key_manager))),
-          public_key_manager_type_index_(absl::nullopt),
-          new_key_allowed_(new_key_allowed),
-          internal_key_factory_(
-              absl::make_unique<internal::KeyFactoryImpl<KeyTypeManager<
-                  KeyProto, KeyFormatProto, List<Primitives...>>>>(
-                  key_manager)),
-          key_factory_(internal_key_factory_.get()),
-          key_deriver_(CreateDeriverFunctionFor(key_manager)),
-          key_type_manager_(absl::WrapUnique(key_manager)) {
-      // TODO(C++17) replace with a fold expression
-      (void)std::initializer_list<int>{
-          0, (primitive_to_manager_.emplace(
-                  std::type_index(typeid(Primitives)),
-                  internal::MakeKeyManager<Primitives>(key_manager)),
-              0)...};
-    }
-
-    // Takes ownership of the 'private_key_manager', but *not* of the
-    // 'public_key_manager'. The public_key_manager must only be alive for the
-    // duration of the constructor.
-    template <typename PrivateKeyProto, typename KeyFormatProto,
-              typename PublicKeyProto, typename PublicPrimitivesList,
-              typename... PrivatePrimitives>
-    KeyTypeInfo(
-        PrivateKeyTypeManager<PrivateKeyProto, KeyFormatProto, PublicKeyProto,
-                              List<PrivatePrimitives...>>* private_key_manager,
-        KeyTypeManager<PublicKeyProto, void, PublicPrimitivesList>*
-            public_key_manager,
-        bool new_key_allowed)
-        : key_manager_type_index_(
-              std::type_index(typeid(*private_key_manager))),
-          public_key_manager_type_index_(
-              std::type_index(typeid(*public_key_manager))),
-          new_key_allowed_(new_key_allowed),
-          internal_key_factory_(
-              absl::make_unique<internal::PrivateKeyFactoryImpl<
-                  PrivateKeyProto, KeyFormatProto, PublicKeyProto,
-                  List<PrivatePrimitives...>, PublicPrimitivesList>>(
-                  private_key_manager, public_key_manager)),
-          key_factory_(internal_key_factory_.get()),
-          key_deriver_(CreateDeriverFunctionFor(private_key_manager)),
-          key_type_manager_(absl::WrapUnique(private_key_manager)) {
-      // TODO(C++17) replace with a fold expression
-      (void)std::initializer_list<int>{
-          0, (primitive_to_manager_.emplace(
-                  std::type_index(typeid(PrivatePrimitives)),
-                  internal::MakePrivateKeyManager<PrivatePrimitives>(
-                      private_key_manager, public_key_manager)),
-              0)...};
-    }
-
-    template <typename P>
-    crypto::tink::util::StatusOr<const KeyManager<P>*> get_key_manager(
-        absl::string_view requested_type_url) const {
-      auto it = primitive_to_manager_.find(std::type_index(typeid(P)));
-      if (it == primitive_to_manager_.end()) {
-        return crypto::tink::util::Status(
-            absl::StatusCode::kInvalidArgument,
-            absl::StrCat(
-                "Primitive type ", typeid(P).name(),
-                " not among supported primitives ",
-                absl::StrJoin(
-                    primitive_to_manager_.begin(), primitive_to_manager_.end(),
-                    ", ",
-                    [](std::string* out,
-                       const std::pair<const std::type_index,
-                                       std::unique_ptr<KeyManagerBase>>& kv) {
-                      absl::StrAppend(out, kv.first.name());
-                    }),
-                " for type URL ", requested_type_url));
-      }
-      return static_cast<const KeyManager<P>*>(it->second.get());
-    }
-
-    const std::type_index& key_manager_type_index() const {
-      return key_manager_type_index_;
-    }
-
-    const absl::optional<std::type_index>& public_key_manager_type_index()
-        const {
-      return public_key_manager_type_index_;
-    }
-
-    bool new_key_allowed() const { return new_key_allowed_; }
-    void set_new_key_allowed(bool b) { new_key_allowed_ = b; }
-
-    const KeyFactory& key_factory() const { return *key_factory_; }
-
-    const std::function<crypto::tink::util::StatusOr<
-        google::crypto::tink::KeyData>(absl::string_view, InputStream*)>&
-    key_deriver() const {
-      return key_deriver_;
-    }
-
-   private:
-    // dynamic std::type_index of the actual key manager class for which this
-    // key was inserted.
-    std::type_index key_manager_type_index_;
-    // dynamic std::type_index of the public key manager corresponding to this
-    // class, in case it was inserted using RegisterAsymmetricKeyManagers,
-    // nullopt otherwise.
-    absl::optional<std::type_index> public_key_manager_type_index_;
-
-    // For each primitive, the corresponding names and key_manager.
-    absl::flat_hash_map<std::type_index, std::unique_ptr<KeyManagerBase>>
-        primitive_to_manager_;
-    // Whether the key manager allows creating new keys.
-    bool new_key_allowed_;
-    // A factory constructed from an internal key manager. Owned version of
-    // key_factory if constructed with a KeyTypeManager. This is nullptr if
-    // constructed with a KeyManager.
-    std::unique_ptr<const KeyFactory> internal_key_factory_;
-    // Unowned copy of internal_key_factory, always different from
-    // nullptr.
-    const KeyFactory* key_factory_;
-    // A function to call to derive a key. If the container was constructed with
-    // a KeyTypeManager which has non-void keyformat type, this will forward to
-    // the function DeriveKey of this container. Otherwise, the function is
-    // 'empty', i.e., "key_deriver_" will cast to false when cast to a bool.
-    std::function<crypto::tink::util::StatusOr<google::crypto::tink::KeyData>(
-        absl::string_view, InputStream*)>
-        key_deriver_;
-    // The owned pointer in case we use a KeyTypeManager, nullptr if
-    // constructed with a KeyManager.
-    const std::shared_ptr<void> key_type_manager_;
-  };
-
-  class WrapperInfo {
-   public:
-    template <typename P, typename Q>
-    explicit WrapperInfo(std::unique_ptr<PrimitiveWrapper<P, Q>> wrapper)
-        : is_same_primitive_wrapping_(std::is_same<P, Q>::value),
-          wrapper_type_index_(std::type_index(typeid(*wrapper))),
-          q_type_index_(std::type_index(typeid(Q))) {
-      auto keyset_wrapper_unique_ptr =
-          absl::make_unique<KeysetWrapperImpl<P, Q>>(
-              wrapper.get(), [](const google::crypto::tink::KeyData& key_data) {
-                return RegistryImpl::GlobalInstance().GetPrimitive<P>(key_data);
-              });
-      keyset_wrapper_ = std::move(keyset_wrapper_unique_ptr);
-      original_wrapper_ = std::move(wrapper);
-    }
-
-    template <typename Q>
-    crypto::tink::util::StatusOr<const KeysetWrapper<Q>*> GetKeysetWrapper()
-        const {
-      if (q_type_index_ != std::type_index(typeid(Q))) {
-        return crypto::tink::util::Status(
-            absl::StatusCode::kInternal,
-            "RegistryImpl::KeysetWrapper() called with wrong type");
-      }
-      return static_cast<KeysetWrapper<Q>*>(keyset_wrapper_.get());
-    }
-
-    template <typename P>
-    crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*>
-    GetLegacyWrapper() const {
-      if (!is_same_primitive_wrapping_) {
-        // This happens if a user uses a legacy method (like Registry::Wrap)
-        // directly or has a custom key manager for a primitive which has a
-        // PrimitiveWrapper<P,Q> with P != Q.
-        return crypto::tink::util::Status(
-            absl::StatusCode::kFailedPrecondition,
-            absl::StrCat("Cannot use primitive type ", typeid(P).name(),
-                         " with a custom key manager."));
-      }
-      if (q_type_index_ != std::type_index(typeid(P))) {
-        return crypto::tink::util::Status(
-            absl::StatusCode::kInternal,
-            "RegistryImpl::LegacyWrapper() called with wrong type");
-      }
-      return static_cast<const PrimitiveWrapper<P, P>*>(
-          original_wrapper_.get());
-    }
-
-    // Returns true if the PrimitiveWrapper is the same class as the one used
-    // to construct this WrapperInfo
-    template <typename P, typename Q>
-    bool HasSameType(const PrimitiveWrapper<P, Q>& wrapper) {
-      return wrapper_type_index_ == std::type_index(typeid(wrapper));
-    }
-
-   private:
-    bool is_same_primitive_wrapping_;
-    // dynamic std::type_index of the actual PrimitiveWrapper<P,Q> class for
-    // which this key was inserted.
-    std::type_index wrapper_type_index_;
-    // dynamic std::type_index of Q, when PrimitiveWrapper<P,Q> was inserted.
-    std::type_index q_type_index_;
-    // The primitive_wrapper passed in. We use a shared_ptr because
-    // unique_ptr<void> is invalid.
-    std::shared_ptr<void> original_wrapper_;
-    // The keyset_wrapper_. We use a shared_ptr because unique_ptr<void> is
-    // invalid.
-    std::shared_ptr<void> keyset_wrapper_;
-  };
-
-  // All information for a given primitive label.
-  struct LabelInfo {
-    LabelInfo(std::shared_ptr<void> catalogue, std::type_index type_index,
-              const char* type_id_name)
-        : catalogue(std::move(catalogue)),
-          type_index(type_index),
-          type_id_name(type_id_name) {}
-    // A pointer to the underlying Catalogue<P>. We use a shared_ptr because
-    // shared_ptr<void> is valid (as opposed to unique_ptr<void>).
-    const std::shared_ptr<void> catalogue;
-    // std::type_index of the primitive for which this key was inserted.
-    std::type_index type_index;
-    // TypeId name of the primitive for which this key was inserted.
-    const std::string type_id_name;
-  };
-
-  template <class P>
-  crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*> GetLegacyWrapper()
-      const ABSL_LOCKS_EXCLUDED(maps_mutex_);
-
-  template <class P>
-  crypto::tink::util::StatusOr<const KeysetWrapper<P>*> GetKeysetWrapper() const
-      ABSL_LOCKS_EXCLUDED(maps_mutex_);
-
   // Returns the key type info for a given type URL. Since we never replace
   // key type infos, the pointers will stay valid for the lifetime of the
   // binary.
-  crypto::tink::util::StatusOr<const KeyTypeInfo*> get_key_type_info(
+  crypto::tink::util::StatusOr<const KeyTypeInfoStore::Info*> get_key_type_info(
       absl::string_view type_url) const ABSL_LOCKS_EXCLUDED(maps_mutex_);
 
-  // Returns OK if the key manager with the given type index can be inserted
-  // for type url type_url and parameter new_key_allowed. Otherwise returns
-  // an error to be returned to the user.
-  crypto::tink::util::Status CheckInsertable(
-      absl::string_view type_url, const std::type_index& key_manager_type_index,
-      bool new_key_allowed) const ABSL_SHARED_LOCKS_REQUIRED(maps_mutex_);
-
   mutable absl::Mutex maps_mutex_;
-  // A map from the type_url to the given KeyTypeInfo. Once emplaced KeyTypeInfo
-  // objects must remain valid throughout the life time of the binary. Hence,
-  // one should /never/ replace any element of the KeyTypeInfo. This is because
-  // get_key_type_manager() needs to guarantee that the returned
-  // key_type_manager remains valid.
-  // NOTE: We require pointer stability of the value, as get_key_type_info
-  // returns a pointer which needs to stay alive.
-  absl::flat_hash_map<std::string, KeyTypeInfo> type_url_to_info_
-      ABSL_GUARDED_BY(maps_mutex_);
-  // A map from the type_id to the corresponding wrapper.
-  absl::flat_hash_map<std::type_index, WrapperInfo> primitive_to_wrapper_
-      ABSL_GUARDED_BY(maps_mutex_);
-
-  absl::flat_hash_map<std::string, LabelInfo> name_to_catalogue_map_
-      ABSL_GUARDED_BY(maps_mutex_);
+  // Stores information about key types constructed from their KeyTypeManager or
+  // KeyManager.
+  // Once inserted, KeyTypeInfoStore::Info objects must remain valid for the
+  // lifetime of the binary, and the Info object's pointer stability is
+  // required. Elements in Info, which include the KeyTypeManager or KeyManager,
+  // must not be replaced.
+  KeyTypeInfoStore key_type_info_store_ ABSL_GUARDED_BY(maps_mutex_);
+  // Stores information about keyset wrappers constructed from their
+  // PrimitiveWrapper.
+  KeysetWrapperStore keyset_wrapper_store_ ABSL_GUARDED_BY(maps_mutex_);
 
   mutable absl::Mutex monitoring_factory_mutex_;
   std::unique_ptr<crypto::tink::MonitoringClientFactory> monitoring_factory_
@@ -446,248 +167,57 @@
 };
 
 template <class P>
-crypto::tink::util::Status RegistryImpl::AddCatalogue(
-    absl::string_view catalogue_name, Catalogue<P>* catalogue) {
-  if (catalogue == nullptr) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInvalidArgument,
-        "Parameter 'catalogue' must be non-null.");
-  }
-  std::shared_ptr<void> entry(catalogue);
-  absl::MutexLock lock(&maps_mutex_);
-  auto curr_catalogue = name_to_catalogue_map_.find(catalogue_name);
-  if (curr_catalogue != name_to_catalogue_map_.end()) {
-    auto existing =
-        static_cast<Catalogue<P>*>(curr_catalogue->second.catalogue.get());
-    if (std::type_index(typeid(*existing)) !=
-        std::type_index(typeid(*catalogue))) {
-      return ToStatusF(absl::StatusCode::kAlreadyExists,
-                       "A catalogue named '%s' has been already added.",
-                       catalogue_name);
-    }
-  } else {
-    name_to_catalogue_map_.emplace(
-        std::piecewise_construct, std::forward_as_tuple(catalogue_name),
-        std::forward_as_tuple(std::move(entry), std::type_index(typeid(P)),
-                              typeid(P).name()));
-  }
-  return crypto::tink::util::OkStatus();
-}
-
-template <class P>
-crypto::tink::util::StatusOr<const Catalogue<P>*> RegistryImpl::get_catalogue(
-    absl::string_view catalogue_name) const {
-  absl::MutexLock lock(&maps_mutex_);
-  auto catalogue_entry = name_to_catalogue_map_.find(catalogue_name);
-  if (catalogue_entry == name_to_catalogue_map_.end()) {
-    return ToStatusF(absl::StatusCode::kNotFound,
-                     "No catalogue named '%s' has been added.", catalogue_name);
-  }
-  if (catalogue_entry->second.type_id_name != typeid(P).name()) {
-    return ToStatusF(absl::StatusCode::kInvalidArgument,
-                     "Wrong Primitive type for catalogue named '%s': "
-                     "got '%s', expected '%s'",
-                     catalogue_name, typeid(P).name(),
-                     catalogue_entry->second.type_id_name);
-  }
-  return static_cast<Catalogue<P>*>(catalogue_entry->second.catalogue.get());
-}
-
-template <class P>
 crypto::tink::util::Status RegistryImpl::RegisterKeyManager(
     KeyManager<P>* manager, bool new_key_allowed) {
   auto owned_manager = absl::WrapUnique(manager);
-  if (owned_manager == nullptr) {
+  if (manager == nullptr) {
     return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
                                       "Parameter 'manager' must be non-null.");
   }
-  std::string type_url = owned_manager->get_key_type();
-  if (!manager->DoesSupport(type_url)) {
-    return ToStatusF(absl::StatusCode::kInvalidArgument,
-                     "The manager does not support type '%s'.", type_url);
-  }
   absl::MutexLock lock(&maps_mutex_);
-  crypto::tink::util::Status status = CheckInsertable(
-      type_url, std::type_index(typeid(*owned_manager)), new_key_allowed);
-  if (!status.ok()) return status;
-
-  auto it = type_url_to_info_.find(type_url);
-  if (it != type_url_to_info_.end()) {
-    it->second.set_new_key_allowed(new_key_allowed);
-  } else {
-    type_url_to_info_.emplace(
-        std::piecewise_construct, std::forward_as_tuple(type_url),
-        std::forward_as_tuple(owned_manager.release(), new_key_allowed));
-  }
-  return crypto::tink::util::OkStatus();
+  return key_type_info_store_.AddKeyManager(std::move(owned_manager),
+                                            new_key_allowed);
 }
 
 template <class KeyProto, class KeyFormatProto, class PrimitiveList>
 crypto::tink::util::Status RegistryImpl::RegisterKeyTypeManager(
     std::unique_ptr<KeyTypeManager<KeyProto, KeyFormatProto, PrimitiveList>>
-        owned_manager,
+        manager,
     bool new_key_allowed) {
-  if (owned_manager == nullptr) {
+  if (manager == nullptr) {
     return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
                                       "Parameter 'manager' must be non-null.");
   }
-  std::string type_url = owned_manager->get_key_type();
   absl::MutexLock lock(&maps_mutex_);
-
-  // Check FIPS status
-  internal::FipsCompatibility fips_compatible = owned_manager->FipsStatus();
-  auto fips_status = internal::ChecksFipsCompatibility(fips_compatible);
-  if (!fips_status.ok()) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInternal,
-        absl::StrCat("Failed registering the key manager for ",
-                     typeid(*owned_manager).name(),
-                     " as it is not FIPS compatible."));
-  }
-
-  crypto::tink::util::Status status = CheckInsertable(
-      type_url, std::type_index(typeid(*owned_manager)), new_key_allowed);
-  if (!status.ok()) return status;
-
-  auto it = type_url_to_info_.find(type_url);
-  if (it != type_url_to_info_.end()) {
-    it->second.set_new_key_allowed(new_key_allowed);
-  } else {
-    type_url_to_info_.emplace(
-        std::piecewise_construct, std::forward_as_tuple(type_url),
-        std::forward_as_tuple(owned_manager.release(), new_key_allowed));
-  }
-  return crypto::tink::util::OkStatus();
+  return key_type_info_store_.AddKeyTypeManager(std::move(manager),
+                                                new_key_allowed);
 }
 
 template <class PrivateKeyProto, class KeyFormatProto, class PublicKeyProto,
           class PrivatePrimitivesList, class PublicPrimitivesList>
 crypto::tink::util::Status RegistryImpl::RegisterAsymmetricKeyManagers(
     PrivateKeyTypeManager<PrivateKeyProto, KeyFormatProto, PublicKeyProto,
-                          PrivatePrimitivesList>* private_key_manager,
-    KeyTypeManager<PublicKeyProto, void, PublicPrimitivesList>*
-        public_key_manager,
+                          PrivatePrimitivesList>* private_manager,
+    KeyTypeManager<PublicKeyProto, void, PublicPrimitivesList>* public_manager,
     bool new_key_allowed) ABSL_LOCKS_EXCLUDED(maps_mutex_) {
-  auto owned_private_key_manager = absl::WrapUnique(private_key_manager);
-  auto owned_public_key_manager = absl::WrapUnique(public_key_manager);
-  if (private_key_manager == nullptr) {
+  auto owned_private_manager = absl::WrapUnique(private_manager);
+  auto owned_public_manager = absl::WrapUnique(public_manager);
+
+  if (private_manager == nullptr) {
     return crypto::tink::util::Status(
         absl::StatusCode::kInvalidArgument,
-        "Parameter 'private_key_manager' must be non-null.");
+        "Parameter 'private_manager' must be non-null.");
   }
-  if (owned_public_key_manager == nullptr) {
+  if (public_manager == nullptr) {
     return crypto::tink::util::Status(
         absl::StatusCode::kInvalidArgument,
-        "Parameter 'public_key_manager' must be non-null.");
+        "Parameter 'public_manager' must be non-null.");
   }
-  std::string private_type_url = private_key_manager->get_key_type();
-  std::string public_type_url = public_key_manager->get_key_type();
 
   absl::MutexLock lock(&maps_mutex_);
-
-  // Check FIPS status
-  auto private_fips_status =
-      internal::ChecksFipsCompatibility(private_key_manager->FipsStatus());
-
-  if (!private_fips_status.ok()) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInternal,
-        absl::StrCat("Failed registering the key manager for ",
-                     typeid(*private_key_manager).name(),
-                     " as it is not FIPS compatible."));
-  }
-
-  auto public_fips_status =
-      internal::ChecksFipsCompatibility(public_key_manager->FipsStatus());
-
-  if (!public_fips_status.ok()) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInternal,
-        absl::StrCat("Failed registering the key manager for ",
-                     typeid(*public_key_manager).name(),
-                     " as it is not FIPS compatible."));
-  }
-
-  crypto::tink::util::Status status = CheckInsertable(
-      private_type_url, std::type_index(typeid(*private_key_manager)),
+  return key_type_info_store_.AddAsymmetricKeyTypeManagers(
+      std::move(owned_private_manager), std::move(owned_public_manager),
       new_key_allowed);
-  if (!status.ok()) return status;
-  status = CheckInsertable(public_type_url,
-                           std::type_index(typeid(*public_key_manager)),
-                           new_key_allowed);
-  if (!status.ok()) return status;
-
-  if (private_type_url == public_type_url) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInvalidArgument,
-        "Passed in key managers must have different get_key_type() results.");
-  }
-
-  auto private_it = type_url_to_info_.find(private_type_url);
-  auto public_it = type_url_to_info_.find(public_type_url);
-  bool private_found = private_it != type_url_to_info_.end();
-  bool public_found = public_it != type_url_to_info_.end();
-
-  if (private_found && !public_found) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat(
-            "Private key manager corresponding to ",
-            typeid(*private_key_manager).name(),
-            " was previously registered, but key manager corresponding to ",
-            typeid(*public_key_manager).name(),
-            " was not, so it's impossible to register them jointly"));
-  }
-  if (!private_found && public_found) {
-    return crypto::tink::util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("Key manager corresponding to ",
-                     typeid(*public_key_manager).name(),
-                     " was previously registered, but private key manager "
-                     "corresponding to ",
-                     typeid(*private_key_manager).name(),
-                     " was not, so it's impossible to register them jointly"));
-  }
-
-  if (private_found) {
-    // implies public_found.
-    if (!private_it->second.public_key_manager_type_index().has_value()) {
-      return crypto::tink::util::Status(
-          absl::StatusCode::kInvalidArgument,
-          absl::StrCat("private key manager corresponding to ",
-                       typeid(*private_key_manager).name(),
-                       " is already registered without public key manager, "
-                       "cannot be re-registered with public key manager. "));
-    }
-    if (*private_it->second.public_key_manager_type_index() !=
-        std::type_index(typeid(*public_key_manager))) {
-      return crypto::tink::util::Status(
-          absl::StatusCode::kInvalidArgument,
-          absl::StrCat(
-              "private key manager corresponding to ",
-              typeid(*private_key_manager).name(),
-              " is already registered with ",
-              private_it->second.public_key_manager_type_index()->name(),
-              ", cannot be re-registered with ",
-              typeid(*public_key_manager).name()));
-    }
-  }
-
-  if (!private_found) {
-    // !public_found must hold.
-    type_url_to_info_.emplace(
-        std::piecewise_construct, std::forward_as_tuple(private_type_url),
-        std::forward_as_tuple(owned_private_key_manager.release(),
-                              owned_public_key_manager.get(), new_key_allowed));
-    type_url_to_info_.emplace(
-        std::piecewise_construct, std::forward_as_tuple(public_type_url),
-        std::forward_as_tuple(owned_public_key_manager.release(),
-                              new_key_allowed));
-  } else {
-    private_it->second.set_new_key_allowed(new_key_allowed);
-  }
-
-  return util::OkStatus();
 }
 
 template <class P, class Q>
@@ -697,35 +227,27 @@
     return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
                                       "Parameter 'wrapper' must be non-null.");
   }
-  std::unique_ptr<PrimitiveWrapper<P, Q>> entry(wrapper);
+  std::unique_ptr<PrimitiveWrapper<P, Q>> owned_wrapper(wrapper);
 
   absl::MutexLock lock(&maps_mutex_);
-  auto it = primitive_to_wrapper_.find(std::type_index(typeid(Q)));
-  if (it != primitive_to_wrapper_.end()) {
-    if (!it->second.HasSameType(*wrapper)) {
-      return util::Status(
-          absl::StatusCode::kAlreadyExists,
-          "A wrapper named for this primitive has already been added.");
-    }
-    return crypto::tink::util::OkStatus();
-  }
-  primitive_to_wrapper_.emplace(
-      std::piecewise_construct,
-      std::forward_as_tuple(std::type_index(typeid(Q))),
-      std::forward_as_tuple(std::move(entry)));
-  return crypto::tink::util::OkStatus();
+  std::function<crypto::tink::util::StatusOr<std::unique_ptr<P>>(
+      const google::crypto::tink::KeyData& key_data)>
+      primitive_getter = [this](const google::crypto::tink::KeyData& key_data) {
+        return this->GetPrimitive<P>(key_data);
+      };
+  return keyset_wrapper_store_.Add(std::move(owned_wrapper), primitive_getter);
 }
 
 template <class P>
 crypto::tink::util::StatusOr<const KeyManager<P>*>
 RegistryImpl::get_key_manager(absl::string_view type_url) const {
-  absl::MutexLock lock(&maps_mutex_);
-  auto it = type_url_to_info_.find(type_url);
-  if (it == type_url_to_info_.end()) {
-    return ToStatusF(absl::StatusCode::kNotFound,
-                     "No manager for type '%s' has been registered.", type_url);
+  crypto::tink::util::StatusOr<
+      const crypto::tink::internal::KeyTypeInfoStore::Info*>
+      info = get_key_type_info(type_url);
+  if (!info.ok()) {
+    return info.status();
   }
-  return it->second.get_key_manager<P>(type_url);
+  return (*info)->get_key_manager<P>(type_url);
 }
 
 template <class P>
@@ -739,42 +261,6 @@
 }
 
 template <class P>
-crypto::tink::util::StatusOr<std::unique_ptr<P>> RegistryImpl::GetPrimitive(
-    absl::string_view type_url, const portable_proto::MessageLite& key) const {
-  auto key_manager_result = get_key_manager<P>(type_url);
-  if (key_manager_result.ok()) {
-    return key_manager_result.value()->GetPrimitive(key);
-  }
-  return key_manager_result.status();
-}
-
-template <class P>
-crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*>
-RegistryImpl::GetLegacyWrapper() const {
-  absl::MutexLock lock(&maps_mutex_);
-  auto it = primitive_to_wrapper_.find(std::type_index(typeid(P)));
-  if (it == primitive_to_wrapper_.end()) {
-    return util::Status(
-        absl::StatusCode::kNotFound,
-        absl::StrCat("No wrapper registered for type ", typeid(P).name()));
-  }
-  return it->second.GetLegacyWrapper<P>();
-}
-
-template <class P>
-crypto::tink::util::StatusOr<const KeysetWrapper<P>*>
-RegistryImpl::GetKeysetWrapper() const {
-  absl::MutexLock lock(&maps_mutex_);
-  auto it = primitive_to_wrapper_.find(std::type_index(typeid(P)));
-  if (it == primitive_to_wrapper_.end()) {
-    return util::Status(
-        absl::StatusCode::kNotFound,
-        absl::StrCat("No wrapper registered for type ", typeid(P).name()));
-  }
-  return it->second.GetKeysetWrapper<P>();
-}
-
-template <class P>
 crypto::tink::util::StatusOr<std::unique_ptr<P>> RegistryImpl::Wrap(
     std::unique_ptr<PrimitiveSet<P>> primitive_set) const {
   if (primitive_set == nullptr) {
@@ -782,29 +268,45 @@
         absl::StatusCode::kInvalidArgument,
         "Parameter 'primitive_set' must be non-null.");
   }
-  util::StatusOr<const PrimitiveWrapper<P, P>*> wrapper_result =
-      GetLegacyWrapper<P>();
-  if (!wrapper_result.ok()) {
-    return wrapper_result.status();
+  const PrimitiveWrapper<P, P>* wrapper = nullptr;
+  {
+    absl::MutexLock lock(&maps_mutex_);
+    crypto::tink::util::StatusOr<const PrimitiveWrapper<P, P>*> wrapper_status =
+        keyset_wrapper_store_.GetPrimitiveWrapper<P>();
+    if (!wrapper_status.ok()) {
+      return wrapper_status.status();
+    }
+    wrapper = *wrapper_status;
   }
-  return wrapper_result.value()->Wrap(std::move(primitive_set));
+  return wrapper->Wrap(std::move(primitive_set));
 }
 
 template <class P>
 crypto::tink::util::StatusOr<std::unique_ptr<P>> RegistryImpl::WrapKeyset(
     const google::crypto::tink::Keyset& keyset,
     const absl::flat_hash_map<std::string, std::string>& annotations) const {
-  crypto::tink::util::StatusOr<const KeysetWrapper<P>*> keyset_wrapper =
-      GetKeysetWrapper<P>();
-  if (!keyset_wrapper.ok()) {
-    return keyset_wrapper.status();
+  const KeysetWrapper<P>* keyset_wrapper = nullptr;
+  {
+    absl::MutexLock lock(&maps_mutex_);
+    crypto::tink::util::StatusOr<const KeysetWrapper<P>*>
+        keyset_wrapper_status = keyset_wrapper_store_.Get<P>();
+    if (!keyset_wrapper_status.ok()) {
+      return keyset_wrapper_status.status();
+    }
+    keyset_wrapper = *keyset_wrapper_status;
   }
-  return (*keyset_wrapper)->Wrap(keyset, annotations);
+  // `maps_mutex_` must be released before calling Wrap or this will deadlock,
+  // as Wrap calls get_key_manager.
+  return keyset_wrapper->Wrap(keyset, annotations);
 }
 
 inline crypto::tink::util::Status RegistryImpl::RestrictToFipsIfEmpty() const {
   absl::MutexLock lock(&maps_mutex_);
-  if (type_url_to_info_.empty()) {
+  // If we are already in FIPS mode, then do nothing..
+  if (IsFipsModeEnabled()) {
+    return util::OkStatus();
+  }
+  if (key_type_info_store_.IsEmpty()) {
     SetFipsRestricted();
     return util::OkStatus();
   }
diff --git a/cc/internal/registry_impl_test.cc b/cc/internal/registry_impl_test.cc
index d6aaf21..7b3ec51 100644
--- a/cc/internal/registry_impl_test.cc
+++ b/cc/internal/registry_impl_test.cc
@@ -14,46 +14,58 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
+#include "tink/internal/registry_impl.h"
+
+#include <stdint.h>
+
 #include <memory>
+#include <sstream>
 #include <string>
 #include <thread>  // NOLINT(build/c++11)
+#include <typeinfo>
 #include <utility>
-#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
 #include "openssl/crypto.h"
 #include "tink/aead.h"
 #include "tink/aead/aead_wrapper.h"
 #include "tink/aead/aes_gcm_key_manager.h"
-#include "tink/catalogue.h"
-#include "tink/config/tink_fips.h"
 #include "tink/core/key_manager_impl.h"
 #include "tink/core/key_type_manager.h"
-#include "tink/crypto_format.h"
+#include "tink/core/private_key_manager_impl.h"
+#include "tink/core/private_key_type_manager.h"
+#include "tink/core/template_util.h"
 #include "tink/hybrid/ecies_aead_hkdf_private_key_manager.h"
 #include "tink/hybrid/ecies_aead_hkdf_public_key_manager.h"
-#include "tink/keyset_manager.h"
-#include "tink/monitoring/monitoring.h"
+#include "tink/hybrid_decrypt.h"
+#include "tink/input_stream.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/key_manager.h"
+#include "tink/mac.h"
 #include "tink/monitoring/monitoring_client_mocks.h"
+#include "tink/primitive_set.h"
+#include "tink/primitive_wrapper.h"
 #include "tink/registry.h"
 #include "tink/subtle/aes_gcm_boringssl.h"
 #include "tink/subtle/random.h"
+#include "tink/util/input_stream_util.h"
 #include "tink/util/istream_input_stream.h"
 #include "tink/util/protobuf_helper.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
-#include "tink/util/test_keyset_handle.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 #include "proto/aes_ctr_hmac_aead.pb.h"
 #include "proto/aes_gcm.pb.h"
 #include "proto/common.pb.h"
 #include "proto/ecdsa.pb.h"
+#include "proto/ecies_aead_hkdf.pb.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
@@ -94,9 +106,7 @@
 
 class RegistryTest : public ::testing::Test {
  protected:
-  void SetUp() override {
-    Registry::Reset();
-  }
+  void SetUp() override { Registry::Reset(); }
 
   void TearDown() override {
     // Reset is needed here to ensure Mock objects get deleted and do not leak.
@@ -137,28 +147,23 @@
   explicit TestAeadKeyManager(const std::string& key_type)
       : key_type_(key_type), key_factory_(key_type) {}
 
-  util::StatusOr<std::unique_ptr<Aead>>
-  GetPrimitive(const KeyData& key) const override {
+  util::StatusOr<std::unique_ptr<Aead>> GetPrimitive(
+      const KeyData& key) const override {
     std::unique_ptr<Aead> aead(new DummyAead(key_type_));
     return std::move(aead);
   }
 
-  util::StatusOr<std::unique_ptr<Aead>>
-  GetPrimitive(const MessageLite& key) const override {
+  util::StatusOr<std::unique_ptr<Aead>> GetPrimitive(
+      const MessageLite& key) const override {
     return util::Status(absl::StatusCode::kUnknown,
                         "TestKeyFactory cannot construct an aead");
   }
 
-
-  uint32_t get_version() const override {
-    return 0;
-  }
+  uint32_t get_version() const override { return 0; }
 
   const std::string& get_key_type() const override { return key_type_; }
 
-  const KeyFactory& get_key_factory() const override {
-    return key_factory_;
-  }
+  const KeyFactory& get_key_factory() const override { return key_factory_; }
 
  private:
   std::string key_type_;
@@ -254,7 +259,7 @@
 template <typename P, typename Q = P>
 class TestWrapper : public PrimitiveWrapper<P, Q> {
  public:
-  TestWrapper() {}
+  TestWrapper() = default;
   crypto::tink::util::StatusOr<std::unique_ptr<Q>> Wrap(
       std::unique_ptr<PrimitiveSet<P>> primitive_set) const override {
     return util::Status(absl::StatusCode::kUnimplemented,
@@ -286,7 +291,8 @@
   for (int i = 0; i < manager_count; i++) {
     std::string key_type = key_type_prefix + std::to_string(i);
     util::Status status = Registry::RegisterKeyManager(
-        new TestAeadKeyManager(key_type));
+        absl::make_unique<TestAeadKeyManager>(key_type),
+        /* new_key_allowed= */ true);
     EXPECT_TRUE(status.ok()) << status;
   }
 }
@@ -375,10 +381,8 @@
   int count_b = 72;
 
   // Register some managers.
-  std::thread register_a(register_test_managers,
-                         key_type_prefix_a, count_a);
-  std::thread register_b(register_test_managers,
-                         key_type_prefix_b, count_b);
+  std::thread register_a(register_test_managers, key_type_prefix_a, count_a);
+  std::thread register_b(register_test_managers, key_type_prefix_b, count_b);
   register_a.join();
   register_b.join();
 
@@ -417,7 +421,6 @@
   auto status = Registry::RegisterKeyManager(
       absl::make_unique<TestAeadKeyManager>(key_type_1), true);
 
-
   EXPECT_TRUE(status.ok()) << status;
 
   status = Registry::RegisterKeyManager(
@@ -475,7 +478,8 @@
 TEST_F(RegistryTest, GetKeyManagerRemainsValid) {
   std::string key_type = AesGcmKeyManager().get_key_type();
   EXPECT_THAT(Registry::RegisterKeyManager(
-      absl::make_unique<TestAeadKeyManager>(key_type), true), IsOk());
+                  absl::make_unique<TestAeadKeyManager>(key_type), true),
+              IsOk());
 
   crypto::tink::util::StatusOr<const KeyManager<Aead>*> key_manager =
       Registry::get_key_manager<Aead>(key_type);
@@ -486,49 +490,6 @@
   EXPECT_THAT(key_manager.value()->get_key_type(), Eq(key_type));
 }
 
-class TestAeadCatalogue : public Catalogue<Aead> {
- public:
-  TestAeadCatalogue() {}
-
-  util::StatusOr<std::unique_ptr<KeyManager<Aead>>> GetKeyManager(
-      const std::string& type_url, const std::string& primitive_name,
-      uint32_t min_version) const override {
-    return util::Status(absl::StatusCode::kUnimplemented,
-                        "This is a test catalogue.");
-  }
-};
-
-class TestAeadCatalogue2 : public TestAeadCatalogue {};
-
-TEST_F(RegistryTest, testAddCatalogue) {
-  std::string catalogue_name = "SomeCatalogue";
-
-  std::unique_ptr<TestAeadCatalogue> null_catalogue = nullptr;
-  auto status =
-      Registry::AddCatalogue(catalogue_name, std::move(null_catalogue));
-  EXPECT_FALSE(status.ok());
-  EXPECT_EQ(absl::StatusCode::kInvalidArgument, status.code()) << status;
-
-  // Add a catalogue.
-  status = Registry::AddCatalogue(catalogue_name,
-                                  absl::make_unique<TestAeadCatalogue>());
-  EXPECT_TRUE(status.ok()) << status;
-
-  // Add the same catalogue again, it should work (idempotence).
-  status = Registry::AddCatalogue(catalogue_name,
-                                  absl::make_unique<TestAeadCatalogue>());
-  EXPECT_TRUE(status.ok()) << status;
-
-  // Try overriding a catalogue.
-  status = Registry::AddCatalogue(catalogue_name,
-                                  absl::make_unique<TestAeadCatalogue2>());
-  EXPECT_FALSE(status.ok());
-  EXPECT_EQ(absl::StatusCode::kAlreadyExists, status.code()) << status;
-
-  // Check the catalogue is still present.
-  EXPECT_THAT(Registry::get_catalogue<Aead>(catalogue_name), IsOk());
-}
-
 TEST_F(RegistryTest, testGettingPrimitives) {
   std::string key_type_1 = "google.crypto.tink.AesCtrHmacAeadKey";
   std::string key_type_2 = "google.crypto.tink.AesGcmKey";
@@ -653,8 +614,7 @@
     key_template.set_value("some totally other value 42");
     auto new_key_data_result = Registry::NewKeyData(key_template);
     EXPECT_FALSE(new_key_data_result.ok());
-    EXPECT_EQ(absl::StatusCode::kNotFound,
-              new_key_data_result.status().code());
+    EXPECT_EQ(absl::StatusCode::kNotFound, new_key_data_result.status().code());
     EXPECT_PRED_FORMAT2(testing::IsSubstring, bad_type_url,
                         std::string(new_key_data_result.status().message()));
   }
@@ -892,7 +852,7 @@
 
 // Tests that wrapping of a keyset works in the usual case.
 TEST_F(RegistryTest, KeysetWrappingTest) {
-  if (!FIPS_mode()) {
+  if (!IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported when BoringSSL is not built in FIPS-mode.";
   }
 
@@ -906,9 +866,9 @@
   ON_CALL(*fips_key_manager, FipsStatus())
       .WillByDefault(testing::Return(FipsCompatibility::kRequiresBoringCrypto));
 
-  ASSERT_THAT(Registry::RegisterKeyTypeManager(
-                  std::move(fips_key_manager), true),
-              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(std::move(fips_key_manager), true),
+      IsOk());
   ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
                   absl::make_unique<AeadVariantWrapper>()),
               IsOk());
@@ -1010,7 +970,7 @@
 }
 
 TEST_F(RegistryTest, RegisterFipsKeyTypeManager) {
-  if (!kUseOnlyFips || !FIPS_mode()) {
+  if (!kUseOnlyFips || !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-mode with BoringCrypto available.";
   }
 
@@ -1025,7 +985,7 @@
 }
 
 TEST_F(RegistryTest, RegisterFipsKeyTypeManagerNoBoringCrypto) {
-  if (!kUseOnlyFips || FIPS_mode()) {
+  if (!kUseOnlyFips || IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Only supported in FIPS-mode with BoringCrypto not available.";
   }
@@ -1228,8 +1188,9 @@
 TEST_F(RegistryTest, KeyManagerDeriveKeyFail) {
   std::string key_type = "type.googleapis.com/google.crypto.tink.AesGcmKey";
   ASSERT_THAT(Registry::RegisterKeyManager(
-      absl::make_unique<TestAeadKeyManager>(key_type),
-      /* new_key_allowed= */ true), IsOk());
+                  absl::make_unique<TestAeadKeyManager>(key_type),
+                  /* new_key_allowed= */ true),
+              IsOk());
 
   KeyTemplate key_template;
   key_template.set_type_url("type.googleapis.com/google.crypto.tink.AesGcmKey");
@@ -1323,9 +1284,15 @@
               StatusIs(absl::StatusCode::kAlreadyExists));
 }
 
+}  // namespace
+
+// NOTE: These are outside of the anonymous namespace to allow compiling with
+// MSVC.
 class PrivatePrimitiveA {};
 class PrivatePrimitiveB {};
 
+namespace {
+
 class TestPrivateKeyTypeManager
     : public PrivateKeyTypeManager<EcdsaPrivateKey, EcdsaKeyFormat,
                                    EcdsaPublicKey,
@@ -1388,9 +1355,15 @@
       "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
 };
 
+}  // namespace
+
+// NOTE: These are outside of the anonymous namespace to allow compiling with
+// MSVC.
 class PublicPrimitiveA {};
 class PublicPrimitiveB {};
 
+namespace {
+
 class TestPublicKeyTypeManager
     : public KeyTypeManager<EcdsaPublicKey, void,
                             List<PublicPrimitiveA, PublicPrimitiveB>> {
@@ -1451,7 +1424,7 @@
 }
 
 TEST_F(RegistryTest, RegisterAsymmetricKeyManagers) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1463,7 +1436,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricMoreRestrictiveNewKey) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1480,7 +1453,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricSameNewKey) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1505,7 +1478,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricLessRestrictiveGivesError) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1526,7 +1499,7 @@
 // remains valid.
 
 TEST_F(RegistryTest, RegisterAsymmetricKeyManagersGetKeyManagerStaysValid) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1554,9 +1527,8 @@
               Eq(TestPublicKeyTypeManager().get_key_type()));
 }
 
-
 TEST_F(RegistryTest, AsymmetricPrivateRegisterAlone) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1583,7 +1555,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricGetPrimitiveA) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1601,7 +1573,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricGetPrimitiveB) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1619,7 +1591,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricGetPublicPrimitiveA) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1637,7 +1609,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricGetPublicPrimitiveB) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1655,7 +1627,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricGetWrongPrimitiveError) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1673,9 +1645,7 @@
 }
 
 class PrivateKeyManagerImplTest : public testing::Test {
-  void SetUp() override {
-    Registry::Reset();
-  }
+  void SetUp() override { Registry::Reset(); }
 
   void TearDown() override {
     // Reset is needed here to ensure Mock objects get deleted and do not leak.
@@ -1684,7 +1654,7 @@
 };
 
 TEST_F(PrivateKeyManagerImplTest, AsymmetricFactoryNewKeyFromMessage) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1711,7 +1681,7 @@
 }
 
 TEST_F(PrivateKeyManagerImplTest, AsymmetricNewKeyDisallowed) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1733,7 +1703,7 @@
 }
 
 TEST_F(RegistryTest, AsymmetricGetPublicKeyData) {
-  if (kUseOnlyFips && !FIPS_mode()) {
+  if (kUseOnlyFips && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -1923,8 +1893,8 @@
               std::move(delegating_key_manager), true);
   EXPECT_THAT(status, IsOk());
   status = registry_impl.RegisterKeyTypeManager<AesGcmKey, AesGcmKeyFormat,
-                                                   List<Aead, AeadVariant>>(
-                  absl::make_unique<ExampleKeyTypeManager>(), true);
+                                                List<Aead, AeadVariant>>(
+      absl::make_unique<ExampleKeyTypeManager>(), true);
   EXPECT_THAT(status, IsOk());
 
   EcdsaKeyFormat format;
@@ -1952,8 +1922,8 @@
               std::move(delegating_key_manager), true);
   EXPECT_THAT(status, IsOk());
   status = registry_impl.RegisterKeyTypeManager<AesGcmKey, AesGcmKeyFormat,
-                                                   List<Aead, AeadVariant>>(
-                  absl::make_unique<ExampleKeyTypeManager>(), true);
+                                                List<Aead, AeadVariant>>(
+      absl::make_unique<ExampleKeyTypeManager>(), true);
   EXPECT_THAT(status, IsOk());
 
   EcdsaKeyFormat format;
@@ -1979,8 +1949,8 @@
       absl::make_unique<TestPublicKeyTypeManager>().release(), true);
   EXPECT_THAT(status, IsOk());
   status = registry_impl.RegisterKeyTypeManager<AesGcmKey, AesGcmKeyFormat,
-                                                   List<Aead, AeadVariant>>(
-                  absl::make_unique<ExampleKeyTypeManager>(), true);
+                                                List<Aead, AeadVariant>>(
+      absl::make_unique<ExampleKeyTypeManager>(), true);
   EXPECT_THAT(status, IsOk());
 
   EcdsaPrivateKey private_key;
@@ -1995,14 +1965,29 @@
                        HasSubstr("GetPublicKey worked")));
 }
 
-TEST_F(RegistryImplTest, FipsSucceedsOnEmptyRegistry) {
+TEST_F(RegistryImplTest, FipsRestrictionSucceedsOnEmptyRegistry) {
+  RegistryImpl registry_impl;
+  EXPECT_THAT(registry_impl.RestrictToFipsIfEmpty(), IsOk());
+}
+
+TEST_F(RegistryImplTest, FipsRestrictionSucceedsWhenSettingMultipleTimes) {
+  RegistryImpl registry_impl;
+  EXPECT_THAT(registry_impl.RestrictToFipsIfEmpty(), IsOk());
+  EXPECT_THAT(registry_impl.RestrictToFipsIfEmpty(), IsOk());
+  EXPECT_THAT(registry_impl.RestrictToFipsIfEmpty(), IsOk());
+}
+
+TEST_F(RegistryImplTest, FipsRestrictionSucceedsIfBuildInFipsMode) {
+  if (!kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported when Tink is not built in FIPS mode.";
+  }
   RegistryImpl registry_impl;
   EXPECT_THAT(registry_impl.RestrictToFipsIfEmpty(), IsOk());
 }
 
 TEST_F(RegistryImplTest, FipsFailsIfNotEmpty) {
-  if (!FIPS_mode()) {
-    GTEST_SKIP() << "Not supported when BoringSSL is not built in FIPS-mode.";
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
   auto fips_key_manager = absl::make_unique<ExampleKeyTypeManager>();
@@ -2011,8 +1996,8 @@
 
   RegistryImpl registry_impl;
   auto status = registry_impl.RegisterKeyTypeManager<AesGcmKey, AesGcmKeyFormat,
-                                                   List<Aead, AeadVariant>>(
-                  std::move(fips_key_manager), true);
+                                                     List<Aead, AeadVariant>>(
+      std::move(fips_key_manager), true);
   EXPECT_THAT(status, IsOk());
   EXPECT_THAT(registry_impl.RestrictToFipsIfEmpty(),
               StatusIs(absl::StatusCode::kInternal));
diff --git a/cc/internal/rsa_util.cc b/cc/internal/rsa_util.cc
index 62398e0..45c8c17 100644
--- a/cc/internal/rsa_util.cc
+++ b/cc/internal/rsa_util.cc
@@ -15,18 +15,26 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/rsa_util.h"
 
+#include <stddef.h>
+
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
 #include "openssl/bn.h"
+#include "openssl/rsa.h"
 #include "tink/config/tink_fips.h"
 #include "tink/internal/bn_util.h"
 #include "tink/internal/err_util.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/internal/ssl_util.h"
 #include "tink/util/errors.h"
+#include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
diff --git a/cc/internal/rsa_util.h b/cc/internal/rsa_util.h
index fbf9c30..ae0a7ba 100644
--- a/cc/internal/rsa_util.h
+++ b/cc/internal/rsa_util.h
@@ -16,8 +16,11 @@
 #ifndef TINK_INTERNAL_RSA_UTIL_H_
 #define TINK_INTERNAL_RSA_UTIL_H_
 
+#include <stddef.h>
+
 #include <string>
 
+#include "absl/strings/string_view.h"
 #include "openssl/bn.h"
 #include "openssl/rsa.h"
 #include "tink/internal/ssl_unique_ptr.h"
diff --git a/cc/internal/rsa_util_test.cc b/cc/internal/rsa_util_test.cc
index 6b9b5c8..b86cd92 100644
--- a/cc/internal/rsa_util_test.cc
+++ b/cc/internal/rsa_util_test.cc
@@ -15,19 +15,27 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/internal/rsa_util.h"
 
+#include <stddef.h>
+#include <stdint.h>
+
 #include <algorithm>
+#include <iterator>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "absl/status/status.h"
 #include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
 #include "openssl/bn.h"
 #include "openssl/rsa.h"
 #include "tink/internal/bn_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/random.h"
+#include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
diff --git a/cc/internal/serialization.h b/cc/internal/serialization.h
new file mode 100644
index 0000000..b392969
--- /dev/null
+++ b/cc/internal/serialization.h
@@ -0,0 +1,53 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_SERIALIZATION_H_
+#define TINK_INTERNAL_SERIALIZATION_H_
+
+#include "absl/strings/string_view.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents either a serialized `Key` or a serialized `Parameters` object.
+//
+// Serialization objects are used within Tink to serialize keys, keysets, and
+// parameters. For each serialization method (e.g., binary protobuf
+// serialization), one subclass of this interface must be defined.
+//
+// This class should eventually be moved to the Tink Public API, but major
+// changes still might be made until then (i.e., don't assume that this API
+// is completely stable yet).
+class Serialization {
+ public:
+  // Identifies which parsing method to use in the registry.
+  //
+  // When registering a parsing function in the registry, one argument will be
+  // this object identifier. When the registry is asked to parse a
+  // `Serialization`, the registry will then dispatch it to the corresponding
+  // method.
+  //
+  // The returned absl::string_view must remain valid for the lifetime of this
+  // `Serialization` object.
+  virtual absl::string_view ObjectIdentifier() const = 0;
+
+  virtual ~Serialization() = default;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_SERIALIZATION_H_
diff --git a/cc/internal/serialization_registry.cc b/cc/internal/serialization_registry.cc
new file mode 100644
index 0000000..4d34473
--- /dev/null
+++ b/cc/internal/serialization_registry.cc
@@ -0,0 +1,140 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/serialization_registry.h"
+
+#include <memory>
+#include <string>
+#include <typeinfo>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_format.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/parser_index.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serializer_index.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+SerializationRegistry::Builder::Builder(const SerializationRegistry& registry)
+    : Builder(registry.parameters_parsers_, registry.parameters_serializers_,
+              registry.key_parsers_, registry.key_serializers_) {}
+
+util::Status SerializationRegistry::Builder::RegisterParametersParser(
+    ParametersParser* parser) {
+  ParserIndex index = parser->Index();
+  auto it = parameters_parsers_.find(index);
+  if (it != parameters_parsers_.end()) {
+    if (parameters_parsers_[index] != parser) {
+      return util::Status(absl::StatusCode::kAlreadyExists,
+                          "Attempted to update existing parameters parser.");
+    }
+  }
+  parameters_parsers_.insert({parser->Index(), parser});
+  return util::OkStatus();
+}
+
+util::Status SerializationRegistry::Builder::RegisterParametersSerializer(
+    ParametersSerializer* serializer) {
+  SerializerIndex index = serializer->Index();
+  auto it = parameters_serializers_.find(index);
+  if (it != parameters_serializers_.end()) {
+    if (parameters_serializers_[index] != serializer) {
+      return util::Status(
+          absl::StatusCode::kAlreadyExists,
+          "Attempted to update existing parameters serializer.");
+    }
+  }
+  parameters_serializers_.insert({serializer->Index(), serializer});
+  return util::OkStatus();
+}
+
+util::Status SerializationRegistry::Builder::RegisterKeyParser(
+    KeyParser* parser) {
+  ParserIndex index = parser->Index();
+  auto it = key_parsers_.find(index);
+  if (it != key_parsers_.end()) {
+    if (key_parsers_[index] != parser) {
+      return util::Status(absl::StatusCode::kAlreadyExists,
+                          "Attempted to update existing key parser.");
+    }
+  }
+  key_parsers_.insert({parser->Index(), parser});
+  return util::OkStatus();
+}
+
+util::Status SerializationRegistry::Builder::RegisterKeySerializer(
+    KeySerializer* serializer) {
+  SerializerIndex index = serializer->Index();
+  auto it = key_serializers_.find(index);
+  if (it != key_serializers_.end()) {
+    if (key_serializers_[index] != serializer) {
+      return util::Status(absl::StatusCode::kAlreadyExists,
+                          "Attempted to update existing key serializer.");
+    }
+  }
+  key_serializers_.insert({serializer->Index(), serializer});
+  return util::OkStatus();
+}
+
+SerializationRegistry SerializationRegistry::Builder::Build() {
+  return SerializationRegistry(parameters_parsers_, parameters_serializers_,
+                               key_parsers_, key_serializers_);
+}
+
+util::StatusOr<std::unique_ptr<Parameters>>
+SerializationRegistry::ParseParameters(
+    const Serialization& serialization) const {
+  ParserIndex index = ParserIndex::Create(serialization);
+  auto it = parameters_parsers_.find(index);
+  if (it == parameters_parsers_.end()) {
+    return util::Status(
+        absl::StatusCode::kNotFound,
+        absl::StrFormat("No parameters parser found for parameters type %s",
+                        typeid(serialization).name()));
+  }
+
+  return parameters_parsers_.at(index)->ParseParameters(serialization);
+}
+
+util::StatusOr<std::unique_ptr<Key>> SerializationRegistry::ParseKey(
+    const Serialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) const {
+  ParserIndex index = ParserIndex::Create(serialization);
+  auto it = key_parsers_.find(index);
+  if (it == key_parsers_.end()) {
+    return util::Status(
+        absl::StatusCode::kNotFound,
+        absl::StrFormat("No key parser found for serialization type %s",
+                        typeid(serialization).name()));
+  }
+
+  return key_parsers_.at(index)->ParseKey(serialization, token);
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/serialization_registry.h b/cc/internal/serialization_registry.h
new file mode 100644
index 0000000..ac90e75
--- /dev/null
+++ b/cc/internal/serialization_registry.h
@@ -0,0 +1,176 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_SERIALIZATION_REGISTRY_H_
+#define TINK_INTERNAL_SERIALIZATION_REGISTRY_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <typeindex>
+#include <typeinfo>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_format.h"
+#include "absl/types/optional.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/parser_index.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serializer_index.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+class SerializationRegistry {
+ public:
+  class Builder {
+   public:
+    // Neither movable nor copyable.
+    Builder(const Builder& other) = delete;
+    Builder& operator=(const Builder& other) = delete;
+
+    // Creates initially empty serialization registry builder.
+    Builder() = default;
+    // Creates serialization registry builder by initially copying entries from
+    // `registry`.
+    explicit Builder(const SerializationRegistry& registry);
+
+    // Registers parameters `parser`. Returns an error if a different parameters
+    // parser has already been registered.
+    util::Status RegisterParametersParser(ParametersParser* parser);
+
+    // Registers parameters `serializer`. Returns an error if a different
+    // parameters serializer has already been registered.
+    util::Status RegisterParametersSerializer(ParametersSerializer* serializer);
+
+    // Registers key `parser`. Returns an error if a different key parser has
+    // already been registered.
+    util::Status RegisterKeyParser(KeyParser* parser);
+
+    // Registers key `serializer`. Returns an error if a different key
+    // serializer has already been registered.
+    util::Status RegisterKeySerializer(KeySerializer* serializer);
+
+    // Creates serialization registry from this builder.
+    SerializationRegistry Build();
+
+   private:
+    Builder(const absl::flat_hash_map<ParserIndex, ParametersParser*>&
+                parameters_parsers,
+            const absl::flat_hash_map<SerializerIndex, ParametersSerializer*>&
+                parameters_serializers,
+            const absl::flat_hash_map<ParserIndex, KeyParser*> key_parsers,
+            const absl::flat_hash_map<SerializerIndex, KeySerializer*>
+                key_serializers)
+        : parameters_parsers_(parameters_parsers),
+          parameters_serializers_(parameters_serializers),
+          key_parsers_(key_parsers),
+          key_serializers_(key_serializers) {}
+
+    absl::flat_hash_map<ParserIndex, ParametersParser*> parameters_parsers_;
+    absl::flat_hash_map<SerializerIndex, ParametersSerializer*>
+        parameters_serializers_;
+    absl::flat_hash_map<ParserIndex, KeyParser*> key_parsers_;
+    absl::flat_hash_map<SerializerIndex, KeySerializer*> key_serializers_;
+  };
+
+  // Movable and copyable.
+  SerializationRegistry(SerializationRegistry&& other) = default;
+  SerializationRegistry& operator=(SerializationRegistry&& other) = default;
+  SerializationRegistry(const SerializationRegistry& other) = default;
+  SerializationRegistry& operator=(const SerializationRegistry& other) =
+      default;
+
+  // Creates empty serialization registry.
+  SerializationRegistry() = default;
+
+  // Parses `serialization` into a `Parameters` instance.
+  util::StatusOr<std::unique_ptr<Parameters>> ParseParameters(
+      const Serialization& serialization) const;
+
+  // Serializes `parameters` into a `Serialization` instance.
+  template <typename SerializationT>
+  util::StatusOr<std::unique_ptr<Serialization>> SerializeParameters(
+      const Parameters& parameters) const {
+    SerializerIndex index = SerializerIndex::Create<SerializationT>(parameters);
+    auto it = parameters_serializers_.find(index);
+    if (it == parameters_serializers_.end()) {
+      return util::Status(
+          absl::StatusCode::kNotFound,
+          absl::StrFormat(
+              "No parameters serializer found for parameters type %s",
+              typeid(parameters).name()));
+    }
+
+    return parameters_serializers_.at(index)->SerializeParameters(parameters);
+  }
+
+  // Parses `serialization` into a `Key` instance.
+  util::StatusOr<std::unique_ptr<Key>> ParseKey(
+      const Serialization& serialization,
+      absl::optional<SecretKeyAccessToken> token) const;
+
+  // Serializes `parameters` into a `Serialization` instance.
+  template <typename SerializationT>
+  util::StatusOr<std::unique_ptr<Serialization>> SerializeKey(
+      const Key& key, absl::optional<SecretKeyAccessToken> token) const {
+    SerializerIndex index = SerializerIndex::Create<SerializationT>(key);
+    auto it = key_serializers_.find(index);
+    if (it == key_serializers_.end()) {
+      return util::Status(
+          absl::StatusCode::kNotFound,
+          absl::StrFormat("No key serializer found for key type %s",
+                          typeid(key).name()));
+    }
+
+    return key_serializers_.at(index)->SerializeKey(key, token);
+  }
+
+ private:
+  SerializationRegistry(
+      const absl::flat_hash_map<ParserIndex, ParametersParser*>&
+          parameters_parsers,
+      const absl::flat_hash_map<SerializerIndex, ParametersSerializer*>&
+          parameters_serializers,
+      const absl::flat_hash_map<ParserIndex, KeyParser*> key_parsers,
+      const absl::flat_hash_map<SerializerIndex, KeySerializer*>
+          key_serializers)
+      : parameters_parsers_(parameters_parsers),
+        parameters_serializers_(parameters_serializers),
+        key_parsers_(key_parsers),
+        key_serializers_(key_serializers) {}
+
+  absl::flat_hash_map<ParserIndex, ParametersParser*> parameters_parsers_;
+  absl::flat_hash_map<SerializerIndex, ParametersSerializer*>
+      parameters_serializers_;
+  absl::flat_hash_map<ParserIndex, KeyParser*> key_parsers_;
+  absl::flat_hash_map<SerializerIndex, KeySerializer*> key_serializers_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_SERIALIZATION_REGISTRY_H_
diff --git a/cc/internal/serialization_registry_test.cc b/cc/internal/serialization_registry_test.cc
new file mode 100644
index 0000000..0e0872b
--- /dev/null
+++ b/cc/internal/serialization_registry_test.cc
@@ -0,0 +1,415 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/serialization_registry.h"
+
+#include <memory>
+#include <string_view>
+#include <typeindex>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/serialization.h"
+#include "tink/internal/serialization_test_util.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+TEST(SerializationRegistryTest, ParseParameters) {
+  SerializationRegistry::Builder builder;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersParserImpl<IdParamsSerialization, IdParams> parser2(kIdTypeUrl,
+                                                                ParseIdParams);
+  ASSERT_THAT(builder.RegisterParametersParser(&parser1), IsOk());
+  ASSERT_THAT(builder.RegisterParametersParser(&parser2), IsOk());
+
+  SerializationRegistry registry = builder.Build();
+
+  util::StatusOr<std::unique_ptr<Parameters>> no_id_params =
+      registry.ParseParameters(NoIdSerialization());
+  ASSERT_THAT(no_id_params, IsOk());
+  EXPECT_THAT((*no_id_params)->HasIdRequirement(), IsFalse());
+  EXPECT_THAT(std::type_index(typeid(**no_id_params)),
+              std::type_index(typeid(NoIdParams)));
+
+  util::StatusOr<std::unique_ptr<Parameters>> id_params =
+      registry.ParseParameters(IdParamsSerialization());
+  ASSERT_THAT(id_params, IsOk());
+  EXPECT_THAT((*id_params)->HasIdRequirement(), IsTrue());
+  EXPECT_THAT(std::type_index(typeid(**id_params)),
+              std::type_index(typeid(IdParams)));
+}
+
+TEST(SerializationRegistryTest, ParseParametersWithoutRegistration) {
+  SerializationRegistry::Builder builder;
+  SerializationRegistry registry = builder.Build();
+
+  ASSERT_THAT(registry.ParseParameters(NoIdSerialization()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(SerializationRegistryTest, RegisterSameParametersParser) {
+  SerializationRegistry::Builder builder;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser(kNoIdTypeUrl,
+                                                             ParseNoIdParams);
+
+  EXPECT_THAT(builder.RegisterParametersParser(&parser), IsOk());
+  EXPECT_THAT(builder.RegisterParametersParser(&parser), IsOk());
+}
+
+TEST(SerializationRegistryTest,
+     RegisterDifferentParametersParserWithSameIndex) {
+  SerializationRegistry::Builder builder;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser2(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+
+  EXPECT_THAT(builder.RegisterParametersParser(&parser1), IsOk());
+  EXPECT_THAT(builder.RegisterParametersParser(&parser2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(SerializationRegistryTest, SerializeParameters) {
+  SerializationRegistry::Builder builder;
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  ParametersSerializerImpl<IdParams, IdParamsSerialization> serializer2(
+      kIdTypeUrl, SerializeIdParams);
+  ASSERT_THAT(builder.RegisterParametersSerializer(&serializer1), IsOk());
+  ASSERT_THAT(builder.RegisterParametersSerializer(&serializer2), IsOk());
+
+  SerializationRegistry registry = builder.Build();
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization1 =
+      registry.SerializeParameters<NoIdSerialization>(NoIdParams());
+  ASSERT_THAT(serialization1, IsOk());
+  EXPECT_THAT((*serialization1)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization2 =
+      registry.SerializeParameters<IdParamsSerialization>(IdParams());
+  ASSERT_THAT(serialization2, IsOk());
+  EXPECT_THAT((*serialization2)->ObjectIdentifier(), Eq(kIdTypeUrl));
+}
+
+TEST(SerializationRegistryTest, SerializeParametersWithoutRegistration) {
+  SerializationRegistry::Builder builder;
+  SerializationRegistry registry = builder.Build();
+
+  ASSERT_THAT(
+      registry.SerializeParameters<NoIdSerialization>(NoIdParams()).status(),
+      StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(SerializationRegistryTest, RegisterSameParametersSerializer) {
+  SerializationRegistry::Builder builder;
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  EXPECT_THAT(builder.RegisterParametersSerializer(&serializer), IsOk());
+  EXPECT_THAT(builder.RegisterParametersSerializer(&serializer), IsOk());
+}
+
+TEST(SerializationRegistryTest,
+     RegisterDifferentParametersSerializerWithSameIndex) {
+  SerializationRegistry::Builder builder;
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer2(
+      kNoIdTypeUrl, SerializeNoIdParams);
+
+  EXPECT_THAT(builder.RegisterParametersSerializer(&serializer1), IsOk());
+  EXPECT_THAT(builder.RegisterParametersSerializer(&serializer2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(SerializationRegistryTest, ParseKey) {
+  SerializationRegistry::Builder builder;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser1(kNoIdTypeUrl, ParseNoIdKey);
+  KeyParserImpl<IdKeySerialization, IdKey> parser2(kIdTypeUrl, ParseIdKey);
+  ASSERT_THAT(builder.RegisterKeyParser(&parser1), IsOk());
+  ASSERT_THAT(builder.RegisterKeyParser(&parser2), IsOk());
+
+  SerializationRegistry registry = builder.Build();
+
+  util::StatusOr<std::unique_ptr<Key>> no_id_key =
+      registry.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(no_id_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**no_id_key)),
+              std::type_index(typeid(NoIdKey)));
+
+  util::StatusOr<std::unique_ptr<Key>> id_key = registry.ParseKey(
+      IdKeySerialization(/*id=*/123), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(id_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**id_key)),
+              std::type_index(typeid(IdKey)));
+  EXPECT_THAT((*id_key)->GetIdRequirement(), Eq(123));
+}
+
+TEST(SerializationRegistryTest, ParseKeyNoSecretAccess) {
+  SerializationRegistry::Builder builder;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser(kNoIdTypeUrl, ParseNoIdKey);
+  ASSERT_THAT(builder.RegisterKeyParser(&parser), IsOk());
+
+  SerializationRegistry registry = builder.Build();
+
+  util::StatusOr<std::unique_ptr<Key>> no_id_public_key =
+      registry.ParseKey(NoIdSerialization(), absl::nullopt);
+  ASSERT_THAT(no_id_public_key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**no_id_public_key)),
+              std::type_index(typeid(NoIdKey)));
+}
+
+TEST(SerializationRegistryTest, ParseKeyWithoutRegistration) {
+  SerializationRegistry::Builder builder;
+  SerializationRegistry registry = builder.Build();
+
+  ASSERT_THAT(
+      registry.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get())
+          .status(),
+      StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(SerializationRegistryTest, RegisterSameKeyParser) {
+  SerializationRegistry::Builder builder;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser(kNoIdTypeUrl, ParseNoIdKey);
+
+  EXPECT_THAT(builder.RegisterKeyParser(&parser), IsOk());
+  EXPECT_THAT(builder.RegisterKeyParser(&parser), IsOk());
+}
+
+TEST(SerializationRegistryTest, RegisterDifferentKeyParserWithSameIndex) {
+  SerializationRegistry::Builder builder;
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser1(kNoIdTypeUrl, ParseNoIdKey);
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser2(kNoIdTypeUrl, ParseNoIdKey);
+
+  EXPECT_THAT(builder.RegisterKeyParser(&parser1), IsOk());
+  EXPECT_THAT(builder.RegisterKeyParser(&parser2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(SerializationRegistryTest, SerializeKey) {
+  SerializationRegistry::Builder builder;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer1(SerializeNoIdKey);
+  KeySerializerImpl<IdKey, IdKeySerialization> serializer2(SerializeIdKey);
+  ASSERT_THAT(builder.RegisterKeySerializer(&serializer1), IsOk());
+  ASSERT_THAT(builder.RegisterKeySerializer(&serializer2), IsOk());
+
+  SerializationRegistry registry = builder.Build();
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization1 =
+      registry.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                               InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization1, IsOk());
+  EXPECT_THAT((*serialization1)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization2 =
+      registry.SerializeKey<IdKeySerialization>(IdKey(123),
+                                                InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization2, IsOk());
+  EXPECT_THAT((*serialization2)->ObjectIdentifier(), Eq(kIdTypeUrl));
+}
+
+TEST(SerializationRegistryTest, SerializeKeyNoSecretAccess) {
+  SerializationRegistry::Builder builder;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer(SerializeNoIdKey);
+  ASSERT_THAT(builder.RegisterKeySerializer(&serializer), IsOk());
+
+  SerializationRegistry registry = builder.Build();
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      registry.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                               absl::nullopt);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(SerializationRegistryTest, SerializeKeyWithoutRegistration) {
+  SerializationRegistry::Builder builder;
+  SerializationRegistry registry = builder.Build();
+
+  ASSERT_THAT(registry
+                  .SerializeKey<NoIdSerialization>(
+                      NoIdKey(), InsecureSecretKeyAccess::Get())
+                  .status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST(SerializationRegistryTest, RegisterSameKeySerializer) {
+  SerializationRegistry::Builder builder;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer(SerializeNoIdKey);
+
+  EXPECT_THAT(builder.RegisterKeySerializer(&serializer), IsOk());
+  EXPECT_THAT(builder.RegisterKeySerializer(&serializer), IsOk());
+}
+
+TEST(SerializationRegistryTest, RegisterDifferentKeySerializerWithSameIndex) {
+  SerializationRegistry::Builder builder;
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer1(SerializeNoIdKey);
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer2(SerializeNoIdKey);
+
+  EXPECT_THAT(builder.RegisterKeySerializer(&serializer1), IsOk());
+  EXPECT_THAT(builder.RegisterKeySerializer(&serializer2),
+              StatusIs(absl::StatusCode::kAlreadyExists));
+}
+
+TEST(SerializationRegistryTest, BuiltFromAnotherRegistry) {
+  SerializationRegistry::Builder builder1;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  ASSERT_THAT(builder1.RegisterParametersParser(&parser1), IsOk());
+  ASSERT_THAT(builder1.RegisterParametersSerializer(&serializer1), IsOk());
+
+  SerializationRegistry registry1 = builder1.Build();
+  SerializationRegistry::Builder builder2(registry1);
+
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser2(kNoIdTypeUrl, ParseNoIdKey);
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer2(SerializeNoIdKey);
+  ASSERT_THAT(builder2.RegisterKeyParser(&parser2), IsOk());
+  ASSERT_THAT(builder2.RegisterKeySerializer(&serializer2), IsOk());
+
+  SerializationRegistry registry2 = builder2.Build();
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      registry2.ParseParameters(NoIdSerialization());
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), IsFalse());
+  EXPECT_THAT(std::type_index(typeid(**params)),
+              std::type_index(typeid(NoIdParams)));
+
+  util::StatusOr<std::unique_ptr<Serialization>> params_serialization =
+      registry2.SerializeParameters<NoIdSerialization>(NoIdParams());
+  ASSERT_THAT(params_serialization, IsOk());
+  EXPECT_THAT((*params_serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      registry2.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**key)), std::type_index(typeid(NoIdKey)));
+
+  util::StatusOr<std::unique_ptr<Serialization>> key_serialization =
+      registry2.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                                InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key_serialization, IsOk());
+  EXPECT_THAT((*key_serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(SerializationRegistryTest, RegistryCopy) {
+  SerializationRegistry::Builder builder;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser2(kNoIdTypeUrl, ParseNoIdKey);
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer2(SerializeNoIdKey);
+  ASSERT_THAT(builder.RegisterParametersParser(&parser1), IsOk());
+  ASSERT_THAT(builder.RegisterParametersSerializer(&serializer1), IsOk());
+  ASSERT_THAT(builder.RegisterKeyParser(&parser2), IsOk());
+  ASSERT_THAT(builder.RegisterKeySerializer(&serializer2), IsOk());
+
+  SerializationRegistry registry1 = builder.Build();
+  SerializationRegistry registry2 = registry1;
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      registry2.ParseParameters(NoIdSerialization());
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), IsFalse());
+  EXPECT_THAT(std::type_index(typeid(**params)),
+              std::type_index(typeid(NoIdParams)));
+
+  util::StatusOr<std::unique_ptr<Serialization>> params_serialization =
+      registry2.SerializeParameters<NoIdSerialization>(NoIdParams());
+  ASSERT_THAT(params_serialization, IsOk());
+  EXPECT_THAT((*params_serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      registry2.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**key)), std::type_index(typeid(NoIdKey)));
+
+  util::StatusOr<std::unique_ptr<Serialization>> key_serialization =
+      registry2.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                                InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key_serialization, IsOk());
+  EXPECT_THAT((*key_serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+TEST(SerializationRegistryTest, RegistryMove) {
+  SerializationRegistry::Builder builder;
+  ParametersParserImpl<NoIdSerialization, NoIdParams> parser1(kNoIdTypeUrl,
+                                                              ParseNoIdParams);
+  ParametersSerializerImpl<NoIdParams, NoIdSerialization> serializer1(
+      kNoIdTypeUrl, SerializeNoIdParams);
+  KeyParserImpl<NoIdSerialization, NoIdKey> parser2(kNoIdTypeUrl, ParseNoIdKey);
+  KeySerializerImpl<NoIdKey, NoIdSerialization> serializer2(SerializeNoIdKey);
+  ASSERT_THAT(builder.RegisterParametersParser(&parser1), IsOk());
+  ASSERT_THAT(builder.RegisterParametersSerializer(&serializer1), IsOk());
+  ASSERT_THAT(builder.RegisterKeyParser(&parser2), IsOk());
+  ASSERT_THAT(builder.RegisterKeySerializer(&serializer2), IsOk());
+
+  SerializationRegistry registry1 = builder.Build();
+  SerializationRegistry registry2 = std::move(registry1);
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      registry2.ParseParameters(NoIdSerialization());
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), IsFalse());
+  EXPECT_THAT(std::type_index(typeid(**params)),
+              std::type_index(typeid(NoIdParams)));
+
+  util::StatusOr<std::unique_ptr<Serialization>> params_serialization =
+      registry2.SerializeParameters<NoIdSerialization>(NoIdParams());
+  ASSERT_THAT(params_serialization, IsOk());
+  EXPECT_THAT((*params_serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      registry2.ParseKey(NoIdSerialization(), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT(std::type_index(typeid(**key)), std::type_index(typeid(NoIdKey)));
+
+  util::StatusOr<std::unique_ptr<Serialization>> key_serialization =
+      registry2.SerializeKey<NoIdSerialization>(NoIdKey(),
+                                                InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key_serialization, IsOk());
+  EXPECT_THAT((*key_serialization)->ObjectIdentifier(), Eq(kNoIdTypeUrl));
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/serialization_test_util.h b/cc/internal/serialization_test_util.h
new file mode 100644
index 0000000..b772a0a
--- /dev/null
+++ b/cc/internal/serialization_test_util.h
@@ -0,0 +1,189 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_SERIALIZATION_TEST_UTIL_H_
+#define TINK_INTERNAL_SERIALIZATION_TEST_UTIL_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/internal/serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+constexpr absl::string_view kNoIdTypeUrl = "NoIdTypeUrl";
+constexpr absl::string_view kIdTypeUrl = "IdTypeUrl";
+
+// Generic serialization for keys or parameters.
+class BaseSerialization : public Serialization {
+ public:
+  explicit BaseSerialization(absl::string_view object_identifier)
+      : object_identifier_(object_identifier) {}
+
+  absl::string_view ObjectIdentifier() const override {
+    return object_identifier_;
+  }
+
+  bool operator==(const BaseSerialization& other) const {
+    return object_identifier_ == other.object_identifier_;
+  }
+
+ private:
+  std::string object_identifier_;
+};
+
+// Serialization for keys or parameters without an ID requirement.
+class NoIdSerialization : public BaseSerialization {
+ public:
+  NoIdSerialization() : BaseSerialization(kNoIdTypeUrl) {}
+};
+
+// Serialization for parameters with an ID requirement.
+class IdParamsSerialization : public BaseSerialization {
+ public:
+  IdParamsSerialization() : BaseSerialization(kIdTypeUrl) {}
+};
+
+// Serialization for keys with an ID requirement.
+class IdKeySerialization : public BaseSerialization {
+ public:
+  explicit IdKeySerialization(int id)
+      : BaseSerialization(kIdTypeUrl), id_(id) {}
+
+  int GetKeyId() const { return id_; }
+
+ private:
+  int id_;
+};
+
+// Parameters without an ID requirement.
+class NoIdParams : public Parameters {
+ public:
+  bool HasIdRequirement() const override { return false; }
+
+  bool operator==(const Parameters& other) const override {
+    return !other.HasIdRequirement();
+  }
+};
+
+// Key without an ID requirement.
+class NoIdKey : public Key {
+ public:
+  const Parameters& GetParameters() const override { return params_; }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return absl::nullopt;
+  }
+
+  bool operator==(const Key& other) const override {
+    return params_ == other.GetParameters() &&
+           absl::nullopt == other.GetIdRequirement();
+  }
+
+ private:
+  NoIdParams params_;
+};
+
+// Parameters with an ID requirement.
+class IdParams : public Parameters {
+ public:
+  bool HasIdRequirement() const override { return true; }
+
+  bool operator==(const Parameters& other) const override {
+    return other.HasIdRequirement();
+  }
+};
+
+// Key with an ID requirement.
+class IdKey : public Key {
+ public:
+  explicit IdKey(int id) : id_(id) {}
+
+  const Parameters& GetParameters() const override { return params_; }
+
+  absl::optional<int> GetIdRequirement() const override { return id_; }
+
+  bool operator==(const Key& other) const override {
+    return params_ == other.GetParameters() && id_ == other.GetIdRequirement();
+  }
+
+ private:
+  IdParams params_;
+  int id_;
+};
+
+// Parse `serialization` into parameters without an ID requirement.
+inline util::StatusOr<NoIdParams> ParseNoIdParams(
+    NoIdSerialization serialization) {
+  return NoIdParams();
+}
+
+// Parse `serialization` into parameters with an ID requirement.
+inline util::StatusOr<IdParams> ParseIdParams(
+    IdParamsSerialization serialization) {
+  return IdParams();
+}
+
+// Serialize `parameters` without an ID requirement.
+inline util::StatusOr<NoIdSerialization> SerializeNoIdParams(
+    NoIdParams parameters) {
+  return NoIdSerialization();
+}
+
+// Serialize `parameters` with an ID requirement.
+inline util::StatusOr<IdParamsSerialization> SerializeIdParams(
+    IdParams parameters) {
+  return IdParamsSerialization();
+}
+
+// Parse `serialization` into a key without an ID requirement.
+inline util::StatusOr<NoIdKey> ParseNoIdKey(
+    NoIdSerialization serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  return NoIdKey();
+}
+
+// Parse `serialization` into a key with an ID requirement.
+inline util::StatusOr<IdKey> ParseIdKey(
+    IdKeySerialization serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  return IdKey(serialization.GetKeyId());
+}
+
+// Serialize `key` without an ID requirement.
+inline util::StatusOr<NoIdSerialization> SerializeNoIdKey(
+    NoIdKey key, absl::optional<SecretKeyAccessToken> token) {
+  return NoIdSerialization();
+}
+
+// Serialize `key` with an ID requirement.
+inline util::StatusOr<IdKeySerialization> SerializeIdKey(
+    IdKey key, absl::optional<SecretKeyAccessToken> token) {
+  return IdKeySerialization(key.GetIdRequirement().value());
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_SERIALIZATION_TEST_UTIL_H_
diff --git a/cc/internal/serialization_test_util_test.cc b/cc/internal/serialization_test_util_test.cc
new file mode 100644
index 0000000..1309af8
--- /dev/null
+++ b/cc/internal/serialization_test_util_test.cc
@@ -0,0 +1,116 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/serialization_test_util.h"
+
+#include <string_view>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/types/optional.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOkAndHolds;
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::Not;
+
+TEST(SerializationTest, Create) {
+  EXPECT_THAT(BaseSerialization("base_type_url").ObjectIdentifier(),
+              Eq("base_type_url"));
+  EXPECT_THAT(NoIdSerialization().ObjectIdentifier(), Eq(kNoIdTypeUrl));
+  EXPECT_THAT(IdParamsSerialization().ObjectIdentifier(), Eq(kIdTypeUrl));
+
+  IdKeySerialization id_key(123);
+  EXPECT_THAT(id_key.ObjectIdentifier(), Eq(kIdTypeUrl));
+  EXPECT_THAT(id_key.GetKeyId(), Eq(123));
+}
+
+TEST(NoIdParamsTest, Create) {
+  NoIdParams params;
+
+  EXPECT_THAT(params.HasIdRequirement(), IsFalse());
+  EXPECT_THAT(params, Eq(NoIdParams()));
+  EXPECT_THAT(params, Not(Eq(IdParams())));
+}
+
+TEST(NoIdParamsTest, ParseAndSerialize) {
+  EXPECT_THAT(ParseNoIdParams(NoIdSerialization()), IsOkAndHolds(NoIdParams()));
+  EXPECT_THAT(SerializeNoIdParams(NoIdParams()),
+              IsOkAndHolds(NoIdSerialization()));
+}
+
+TEST(IdParamsTest, Create) {
+  IdParams params;
+
+  EXPECT_THAT(params.HasIdRequirement(), IsTrue());
+  EXPECT_THAT(params, Eq(IdParams()));
+  EXPECT_THAT(params, Not(Eq(NoIdParams())));
+}
+
+TEST(IdParamsTest, ParseAndSerialize) {
+  EXPECT_THAT(ParseIdParams(IdParamsSerialization()), IsOkAndHolds(IdParams()));
+  EXPECT_THAT(SerializeIdParams(IdParams()),
+              IsOkAndHolds(IdParamsSerialization()));
+}
+
+TEST(NoIdKeyTest, Create) {
+  NoIdKey key;
+
+  EXPECT_THAT(key.GetIdRequirement(), Eq(absl::nullopt));
+  EXPECT_THAT(key.GetParameters(), Eq(NoIdParams()));
+  EXPECT_THAT(key, Eq(NoIdKey()));
+  EXPECT_THAT(key, Not(Eq(IdKey(123))));
+}
+
+TEST(NoIdKeyTest, ParseAndSerialize) {
+  EXPECT_THAT(ParseNoIdKey(NoIdSerialization(), InsecureSecretKeyAccess::Get()),
+              IsOkAndHolds(NoIdKey()));
+  EXPECT_THAT(SerializeNoIdKey(NoIdKey(), InsecureSecretKeyAccess::Get()),
+              IsOkAndHolds(NoIdSerialization()));
+}
+
+TEST(IdKeyTest, Create) {
+  IdKey key(123);
+
+  EXPECT_THAT(key.GetIdRequirement(), Eq(123));
+  EXPECT_THAT(key.GetParameters(), Eq(IdParams()));
+  EXPECT_THAT(key, Eq(IdKey(123)));
+  EXPECT_THAT(key, Not(Eq(IdKey(456))));
+  EXPECT_THAT(key, Not(Eq(NoIdKey())));
+}
+
+TEST(IdKeyTest, ParseAndSerialize) {
+  EXPECT_THAT(ParseIdKey(IdKeySerialization(123),
+                         InsecureSecretKeyAccess::Get()),
+              IsOkAndHolds(IdKey(123)));
+  EXPECT_THAT(SerializeIdKey(IdKey(123), InsecureSecretKeyAccess::Get()),
+              IsOkAndHolds(IdKeySerialization(123)));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/serializer_index.h b/cc/internal/serializer_index.h
new file mode 100644
index 0000000..e0c4e66
--- /dev/null
+++ b/cc/internal/serializer_index.h
@@ -0,0 +1,82 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_INTERNAL_SERIALIZER_INDEX_H_
+#define TINK_INTERNAL_SERIALIZER_INDEX_H_
+
+#include <string>
+#include <typeindex>
+
+#include "tink/internal/serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+class SerializerIndex {
+ public:
+  // Create registry lookup key for the combination of the `KeyOrParameterT` and
+  // `SerializationT` types. Useful for key and parameters serializers.
+  template <typename KeyOrParameterT, typename SerializationT>
+  static SerializerIndex Create() {
+    return SerializerIndex(std::type_index(typeid(KeyOrParameterT)),
+                           std::type_index(typeid(SerializationT)));
+  }
+
+  // Create registry lookup key for `SerializationT` type and `parameters`.
+  // Useful for the serialization registry.
+  template <typename SerializationT>
+  static SerializerIndex Create(const Parameters& parameters) {
+    return SerializerIndex(std::type_index(typeid(parameters)),
+                           std::type_index(typeid(SerializationT)));
+  }
+
+  // Create registry lookup key for `SerializationT` type and `key`. Useful for
+  // the serialization registry.
+  template <typename SerializationT>
+  static SerializerIndex Create(const Key& key) {
+    return SerializerIndex(std::type_index(typeid(key)),
+                           std::type_index(typeid(SerializationT)));
+  }
+
+  // Returns true if key/parameters index and serialization type index match.
+  bool operator==(const SerializerIndex& other) const {
+    return kp_index_ == other.kp_index_ &&
+           serialization_index_ == other.serialization_index_;
+  }
+
+  // Required function to make `SerializerIndex` hashable for Abseil hash maps.
+  template <typename H>
+  friend H AbslHashValue(H h, const SerializerIndex& index) {
+    return H::combine(std::move(h), index.kp_index_,
+                      index.serialization_index_);
+  }
+
+ private:
+  SerializerIndex(std::type_index kp_index, std::type_index serialization_index)
+      : kp_index_(kp_index), serialization_index_(serialization_index) {}
+
+  std::type_index kp_index_;
+  std::type_index serialization_index_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_SERIALIZER_INDEX_H_
diff --git a/cc/internal/serializer_index_test.cc b/cc/internal/serializer_index_test.cc
new file mode 100644
index 0000000..9653ee6
--- /dev/null
+++ b/cc/internal/serializer_index_test.cc
@@ -0,0 +1,91 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/serializer_index.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/internal/serialization_test_util.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::testing::Eq;
+using ::testing::Not;
+
+TEST(SerializerIndex, CreateEquivalentFromParameters) {
+  // Multi-parameter templates require extra surrounding parentheses.
+  ASSERT_THAT((SerializerIndex::Create<NoIdParams, NoIdSerialization>()),
+              Eq((SerializerIndex::Create<NoIdParams, NoIdSerialization>())));
+  ASSERT_THAT((SerializerIndex::Create<NoIdParams, NoIdSerialization>()),
+              Eq((SerializerIndex::Create<NoIdSerialization>(NoIdParams()))));
+  ASSERT_THAT((SerializerIndex::Create<NoIdSerialization>(NoIdParams())),
+              Eq((SerializerIndex::Create<NoIdSerialization>(NoIdParams()))));
+}
+
+TEST(SerializerIndex, CreateFromDifferentParametersType) {
+  // Multi-parameter templates require extra surrounding parentheses.
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdParams, NoIdSerialization>()),
+      Not(Eq((SerializerIndex::Create<IdParams, NoIdSerialization>()))));
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdSerialization>(NoIdParams())),
+      Not(Eq((SerializerIndex::Create<NoIdSerialization>(IdParams())))));
+}
+
+TEST(SerializerIndex, CreateFromSameParametersTypeWithDifferentSerialization) {
+  // Multi-parameter templates require extra surrounding parentheses.
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdParams, NoIdSerialization>()),
+      Not(Eq((SerializerIndex::Create<NoIdParams, IdParamsSerialization>()))));
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdSerialization>(NoIdParams())),
+      Not(Eq((SerializerIndex::Create<IdParamsSerialization>(NoIdParams())))));
+}
+
+TEST(SerializerIndex, CreateEquivalentFromKey) {
+  // Multi-parameter templates require extra surrounding parentheses.
+  ASSERT_THAT((SerializerIndex::Create<NoIdKey, NoIdSerialization>()),
+              Eq((SerializerIndex::Create<NoIdKey, NoIdSerialization>())));
+  ASSERT_THAT((SerializerIndex::Create<NoIdKey, NoIdSerialization>()),
+              Eq((SerializerIndex::Create<NoIdSerialization>(NoIdKey()))));
+  ASSERT_THAT((SerializerIndex::Create<NoIdSerialization>(NoIdKey())),
+              Eq((SerializerIndex::Create<NoIdSerialization>(NoIdKey()))));
+}
+
+TEST(SerializerIndex, CreateFromDifferentKeyType) {
+  // Multi-parameter templates require extra surrounding parentheses.
+  ASSERT_THAT((SerializerIndex::Create<NoIdKey, NoIdSerialization>()),
+              Not(Eq((SerializerIndex::Create<IdKey, NoIdSerialization>()))));
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdSerialization>(NoIdKey())),
+      Not(Eq((SerializerIndex::Create<NoIdSerialization>(IdKey(/*id=*/1))))));
+}
+
+TEST(SerializerIndex, CreateFromSameKeyTypeWithDifferentSerialization) {
+  // Multi-parameter templates require extra surrounding parentheses.
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdKey, NoIdSerialization>()),
+      Not(Eq((SerializerIndex::Create<NoIdKey, IdKeySerialization>()))));
+  ASSERT_THAT(
+      (SerializerIndex::Create<NoIdSerialization>(NoIdKey())),
+      Not(Eq((SerializerIndex::Create<IdKeySerialization>(NoIdKey())))));
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/ssl_unique_ptr.h b/cc/internal/ssl_unique_ptr.h
index d7c0586..7bc6314 100644
--- a/cc/internal/ssl_unique_ptr.h
+++ b/cc/internal/ssl_unique_ptr.h
@@ -17,6 +17,7 @@
 #define TINK_INTERNAL_SSL_UNIQUE_PTR_H_
 
 #include <memory>
+
 // Every header in BoringSSL includes base.h, which in turn defines
 // OPENSSL_IS_BORINGSSL. So we include this common header here to "force" the
 // definition of OPENSSL_IS_BORINGSSL in case BoringSSL is used.
diff --git a/cc/internal/test_file_util.cc b/cc/internal/test_file_util.cc
new file mode 100644
index 0000000..bdb9374
--- /dev/null
+++ b/cc/internal/test_file_util.cc
@@ -0,0 +1,77 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/internal/test_file_util.h"
+
+#include <fstream>
+#include <ios>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "absl/log/check.h"
+#include "absl/status/status.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
+#include "absl/strings/string_view.h"
+#include "tink/subtle/random.h"
+#include "tink/util/status.h"
+#include "tink/util/test_util.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+util::Status CreateTestFile(absl::string_view filename,
+                            absl::string_view file_content) {
+  std::string full_filename = absl::StrCat(test::TmpDir(), "/", filename);
+  std::ofstream output_stream(full_filename, std::ios::binary);
+  if (!output_stream) {
+    return util::Status(absl::StatusCode::kInternal, "Cannot open file");
+  }
+  output_stream.write(file_content.data(), file_content.size());
+  return util::OkStatus();
+}
+
+std::string GetTestFileNamePrefix() {
+  const testing::TestInfo* const test_info =
+      testing::UnitTest::GetInstance()->current_test_info();
+  CHECK(test_info != nullptr);
+  std::string random_string = subtle::Random::GetRandomBytes(/*length=*/16);
+  std::string test_suite_name = test_info->test_suite_name();
+  std::string test_name = test_info->name();
+  // Parametrized tests return test_suite_name of the form <Prefix>/<Test Suite>
+  // and name of the form <Test Name>/<Suffix>.
+  // In this case, get only the prefix and test name. Keeping all of these may
+  // result in a file name that is too long.
+  if (test_info->value_param() != nullptr) {
+    std::vector<std::string> test_suite_parts =
+        absl::StrSplit(test_info->test_suite_name(), '/');
+    CHECK_GE(test_suite_parts.size(), 1);
+    test_suite_name = test_suite_parts[0];
+    std::vector<std::string> test_name_parts =
+        absl::StrSplit(test_info->name(), '/');
+    CHECK_GE(test_name_parts.size(), 1);
+    test_name = test_name_parts[0];
+  }
+  return absl::StrCat(test_suite_name, "_", test_name, "_",
+                      absl::BytesToHexString(random_string));
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/test_file_util.h b/cc/internal/test_file_util.h
index 9f37a56..f3e37aa 100644
--- a/cc/internal/test_file_util.h
+++ b/cc/internal/test_file_util.h
@@ -20,6 +20,7 @@
 #include <string>
 
 #include "absl/strings/string_view.h"
+#include "tink/util/status.h"
 
 namespace crypto {
 namespace tink {
@@ -33,6 +34,13 @@
 // Returns the path of the specified file in the runfiles directory.
 std::string RunfilesPath(absl::string_view path);
 
+crypto::tink::util::Status CreateTestFile(absl::string_view filename,
+                                          absl::string_view file_content);
+
+// Returns the prefix to use for files to use in tests. The result will be of
+// the form: <test name>_<testcase name>_<hex encoded random 32 bytes string>.
+std::string GetTestFileNamePrefix();
+
 }  // namespace internal
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/internal/test_random_access_stream.cc b/cc/internal/test_random_access_stream.cc
new file mode 100644
index 0000000..53918d8
--- /dev/null
+++ b/cc/internal/test_random_access_stream.cc
@@ -0,0 +1,97 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#include "tink/internal/test_random_access_stream.h"
+
+#include <stdint.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "tink/random_access_stream.h"
+#include "tink/util/buffer.h"
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+util::Status TestRandomAccessStream::PRead(int64_t position, int count,
+                                           util::Buffer* dest_buffer) {
+  if (dest_buffer == nullptr) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "dest_buffer must be non-null");
+  }
+  if (count <= 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "count must be positive");
+  }
+  if (count > dest_buffer->allocated_size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument, "buffer too small");
+  }
+  if (position < 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "position cannot be negative");
+  }
+  if (position >= content_.size()) {
+    dest_buffer->set_size(0).IgnoreError();
+    return util::Status(absl::StatusCode::kOutOfRange, "EOF");
+  }
+  util::Status status = dest_buffer->set_size(count);
+  if (!status.ok()) {
+    return status;
+  }
+  int read_count =
+      std::min(count, static_cast<int>(content_.size() - position));
+  std::copy(content_.begin() + position,
+            content_.begin() + position + read_count,
+            dest_buffer->get_mem_block());
+  status = dest_buffer->set_size(read_count);
+  if (!status.ok()) {
+    return status;
+  }
+  if (position + read_count == content_.size()) {
+    // We reached EOF.
+    return util::Status(absl::StatusCode::kOutOfRange, "EOF");
+  }
+  return util::OkStatus();
+}
+
+util::Status ReadAllFromRandomAccessStream(
+    RandomAccessStream* random_access_stream, std::string& contents,
+    int chunk_size) {
+  if (chunk_size < 1) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "chunk_size must be greater than zero");
+  }
+  contents.clear();
+  std::unique_ptr<util::Buffer> buffer =
+      *std::move(util::Buffer::New(chunk_size));
+  int64_t position = 0;
+  auto status = util::OkStatus();
+  while (status.ok()) {
+    status = random_access_stream->PRead(position, chunk_size, buffer.get());
+    contents.append(buffer->get_mem_block(), buffer->size());
+    position = contents.size();
+  }
+  return status;
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/test_random_access_stream.h b/cc/internal/test_random_access_stream.h
new file mode 100644
index 0000000..5e2e8fa
--- /dev/null
+++ b/cc/internal/test_random_access_stream.h
@@ -0,0 +1,67 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef TINK_INTERNAL_TEST_RANDOM_ACCESS_STREAM_H_
+#define TINK_INTERNAL_TEST_RANDOM_ACCESS_STREAM_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "tink/random_access_stream.h"
+#include "tink/util/buffer.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// A simple test-only RandomAccessStream implementation that reads from a
+// std::string.
+class TestRandomAccessStream : public RandomAccessStream {
+ public:
+  explicit TestRandomAccessStream(std::string content)
+      : content_(std::move(content)) {}
+  // Move only.
+  TestRandomAccessStream(TestRandomAccessStream&& other) = default;
+  TestRandomAccessStream& operator=(TestRandomAccessStream&& other) = default;
+  TestRandomAccessStream(const TestRandomAccessStream&) = delete;
+  TestRandomAccessStream& operator=(const TestRandomAccessStream&) = delete;
+
+  util::Status PRead(int64_t position, int count,
+                     util::Buffer* dest_buffer) override;
+
+  util::StatusOr<int64_t> size() override { return content_.size(); }
+
+ private:
+  std::string content_;
+};
+
+// Reads the entire `random_access_stream` using a buffer of size `chunk_size`
+// until no more bytes can be read, and puts the read bytes into `contents`.
+// Returns the status of the last call to random_access_stream->PRead().
+util::Status ReadAllFromRandomAccessStream(
+    RandomAccessStream* random_access_stream, std::string& contents,
+    int chunk_size = 42);
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_TEST_RANDOM_ACCESS_STREAM_H_
diff --git a/cc/internal/test_random_access_stream_test.cc b/cc/internal/test_random_access_stream_test.cc
new file mode 100644
index 0000000..28ddcaa
--- /dev/null
+++ b/cc/internal/test_random_access_stream_test.cc
@@ -0,0 +1,161 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#include "tink/internal/test_random_access_stream.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/subtle/random.h"
+#include "tink/util/buffer.h"
+#include "tink/util/status.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::StatusIs;
+
+TEST(TestRandomAccessStreamTest, ReadAllSucceeds) {
+  const int buffer_size = 4 * 1024;
+  const int stream_size = 100 * 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  auto buffer = *std::move(util::Buffer::New(buffer_size));
+  util::Status pread_status = util::OkStatus();
+  std::string result;
+  do {
+    pread_status =
+        rand_access_stream->PRead(result.size(), buffer_size, buffer.get());
+    result.append(buffer->get_mem_block(), buffer->size());
+  } while (pread_status.ok());
+  EXPECT_THAT(pread_status, StatusIs(absl::StatusCode::kOutOfRange));
+  EXPECT_EQ(result, stream_content);
+}
+
+TEST(TestRandomAccessStreamTest, PreadAllInOnePread) {
+  const int stream_size = 8 * 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  auto buffer = *std::move(util::Buffer::New(stream_size));
+  ASSERT_THAT(
+      rand_access_stream->PRead(/*position=*/0, stream_size, buffer.get()),
+      StatusIs(absl::StatusCode::kOutOfRange));
+  EXPECT_EQ(std::string(buffer->get_mem_block(), buffer->size()),
+            stream_content);
+}
+
+TEST(TestRandomAccessStreamTest, PreadCountLargerThanBufferFails) {
+  const int buffer_size = 4 * 1024;
+  const int stream_size = 100 * 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  auto buffer = *std::move(util::Buffer::New(buffer_size));
+  EXPECT_THAT(
+      rand_access_stream->PRead(/*position=*/0, buffer_size + 1, buffer.get()),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(TestRandomAccessStreamTest, InvalidPosition) {
+  const int buffer_size = 4 * 1024;
+  const int stream_size = 100 * 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  auto buffer = *std::move(util::Buffer::New(buffer_size));
+  EXPECT_THAT(rand_access_stream->PRead(-1, buffer_size, buffer.get()),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(TestRandomAccessStreamTest, PreadWithNullBufferFails) {
+  const int stream_size = 100 * 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  EXPECT_THAT(rand_access_stream->PRead(/*position=*/0, stream_size,
+                                        /*dest_buffer=*/nullptr),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(TestRandomAccessStreamTest, PreadWithEmptyStreamEof) {
+  const int buffer_size = 4 * 1024;
+  std::string stream_content;  // Empty string.
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  auto buffer = *std::move(util::Buffer::New(buffer_size));
+  EXPECT_THAT(
+      rand_access_stream->PRead(/*position=*/0, buffer_size, buffer.get()),
+      StatusIs(absl::StatusCode::kOutOfRange));
+}
+
+// Pread of the last partial block populates the buffer with the remaining
+// bytes and returns an EOF status.
+TEST(TestRandomAccessStreamTest, PreadTheLastPartialBlockReturnsEof) {
+  const int buffer_size = 4 * 1024;
+  const int stream_size = 100 * 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+  auto rand_access_stream =
+      std::make_unique<TestRandomAccessStream>(stream_content);
+  auto buffer = *std::move(util::Buffer::New(buffer_size));
+  // Read at a postion so that only buffer_size - 1 bytes are left.
+  EXPECT_THAT(rand_access_stream->PRead(stream_size - buffer_size + 1,
+                                        buffer_size, buffer.get()),
+              StatusIs(absl::StatusCode::kOutOfRange));
+  EXPECT_EQ(buffer->size(), buffer_size - 1);
+  EXPECT_EQ(std::string(buffer->get_mem_block(), buffer->size()),
+            stream_content.substr(stream_size - buffer_size + 1));
+}
+
+TEST(TestRandomAccessStreamTest, ReadAllFromRandomAccessStreamSucceeds) {
+  std::string content_to_read = subtle::Random::GetRandomBytes(4 * 1024);
+  auto test_random_access_stream =
+      std::make_unique<TestRandomAccessStream>(content_to_read);
+  std::string read_content;
+  EXPECT_THAT(ReadAllFromRandomAccessStream(test_random_access_stream.get(),
+                                            read_content,
+                                            /*chunk_size=*/128),
+              StatusIs(absl::StatusCode::kOutOfRange));
+  EXPECT_EQ(content_to_read, read_content);
+}
+
+TEST(TestRandomAccessStreamTest,
+     ReadAllFromRandomAccessStreamFailsWhenChunkIsLessThanOne) {
+  std::string content_to_read = subtle::Random::GetRandomBytes(4 * 1024);
+  auto test_random_access_stream =
+      std::make_unique<TestRandomAccessStream>(content_to_read);
+  std::string read_content;
+  EXPECT_THAT(ReadAllFromRandomAccessStream(test_random_access_stream.get(),
+                                            read_content,
+                                            /*chunk_size=*/0),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(ReadAllFromRandomAccessStream(test_random_access_stream.get(),
+                                            read_content,
+                                            /*chunk_size=*/-10),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/util.cc b/cc/internal/util.cc
index 25c0aa0..c569d80 100644
--- a/cc/internal/util.cc
+++ b/cc/internal/util.cc
@@ -18,6 +18,8 @@
 #include <iterator>
 #include <functional>
 
+#include "absl/strings/ascii.h"
+#include "absl/log/log.h"
 #include "absl/strings/string_view.h"
 
 namespace crypto {
@@ -57,6 +59,19 @@
              std::prev(first.end()), std::prev(second.end()));
 }
 
+bool IsPrintableAscii(absl::string_view input) {
+  for (char c : input) {
+    if (!absl::ascii_isprint(c) || absl::ascii_isspace(c)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+void LogFatal(absl::string_view msg) {
+  LOG(FATAL) <<  msg;
+}
+
 }  // namespace internal
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/internal/util.h b/cc/internal/util.h
index 247e499..6973189 100644
--- a/cc/internal/util.h
+++ b/cc/internal/util.h
@@ -16,6 +16,7 @@
 #ifndef TINK_INTERNAL_UTIL_H_
 #define TINK_INTERNAL_UTIL_H_
 
+#include "absl/base/attributes.h"
 #include "absl/strings/string_view.h"
 
 namespace crypto {
@@ -31,6 +32,23 @@
 // Returns true if `first` fully overlaps with `second`.
 bool BuffersAreIdentical(absl::string_view first, absl::string_view second);
 
+// Returns true if `input` only contains printable ASCII characters (whitespace
+// is not allowed).
+bool IsPrintableAscii(absl::string_view input);
+
+// Returns true if built on Windows; false otherwise.
+inline bool IsWindows() {
+#if defined(_WIN32)
+  return true;
+#else
+  return false;
+#endif
+}
+
+// Wraps Abseil's LOG(FATAL) macro and sets the [noreturn] attribute, which is
+// useful for avoiding false positive [-Werror=return-type] compiler errors.
+ABSL_ATTRIBUTE_NORETURN void LogFatal(absl::string_view msg);
+
 }  // namespace internal
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/internal/util_test.cc b/cc/internal/util_test.cc
index 1746993..fe470a8 100644
--- a/cc/internal/util_test.cc
+++ b/cc/internal/util_test.cc
@@ -26,6 +26,9 @@
 namespace internal {
 namespace {
 
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
 constexpr absl::string_view kLongString =
     "a long buffer with \n several \n newlines";
 
@@ -97,6 +100,22 @@
   EXPECT_FALSE(BuffersAreIdentical(buffer.substr(10, 5), buffer.substr(0, 10)));
 }
 
+TEST(UtilTest, IsPrintableAscii) {
+  const std::string input =
+      "!\"#$%&'()*+,-./"
+      "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
+      "abcdefghijklmnopqrstuvwxyz{|}~";
+  EXPECT_THAT(IsPrintableAscii(input), IsTrue());
+}
+
+TEST(UtilTest, IsNotPrintableAscii) {
+  EXPECT_THAT(IsPrintableAscii("\n"), IsFalse());
+  EXPECT_THAT(IsPrintableAscii("\t"), IsFalse());
+  EXPECT_THAT(IsPrintableAscii(" "), IsFalse());
+  EXPECT_THAT(IsPrintableAscii(std::string("\x7f", 1)), IsFalse());
+  EXPECT_THAT(IsPrintableAscii("ö"), IsFalse());
+}
+
 }  // namespace
 }  // namespace internal
 }  // namespace tink
diff --git a/cc/json_keyset_reader.h b/cc/json_keyset_reader.h
index 350d7a8..8bcb0cc 100644
--- a/cc/json_keyset_reader.h
+++ b/cc/json_keyset_reader.h
@@ -18,6 +18,7 @@
 #define TINK_JSON_KEYSET_READER_H_
 
 #include <istream>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/json_keyset_writer.h b/cc/json_keyset_writer.h
index 81dd488..d7fbb65 100644
--- a/cc/json_keyset_writer.h
+++ b/cc/json_keyset_writer.h
@@ -17,6 +17,7 @@
 #ifndef TINK_JSON_KEYSET_WRITER_H_
 #define TINK_JSON_KEYSET_WRITER_H_
 
+#include <memory>
 #include <ostream>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_ecdsa_sign_key_manager.cc b/cc/jwt/internal/jwt_ecdsa_sign_key_manager.cc
index 009057e..b264e40 100644
--- a/cc/jwt/internal/jwt_ecdsa_sign_key_manager.cc
+++ b/cc/jwt/internal/jwt_ecdsa_sign_key_manager.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/jwt/internal/jwt_ecdsa_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_ecdsa_sign_key_manager.h b/cc/jwt/internal/jwt_ecdsa_sign_key_manager.h
index 0259a26..4c7e3e2 100644
--- a/cc/jwt/internal/jwt_ecdsa_sign_key_manager.h
+++ b/cc/jwt/internal/jwt_ecdsa_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_ECDSA_SIGN_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_ECDSA_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/jwt_ecdsa_sign_verify_key_manager_test.cc b/cc/jwt/internal/jwt_ecdsa_sign_verify_key_manager_test.cc
index 8172973..2bd3dca 100644
--- a/cc/jwt/internal/jwt_ecdsa_sign_verify_key_manager_test.cc
+++ b/cc/jwt/internal/jwt_ecdsa_sign_verify_key_manager_test.cc
@@ -14,8 +14,10 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_ecdsa_verify_key_manager.cc b/cc/jwt/internal/jwt_ecdsa_verify_key_manager.cc
index e6a51d5..68d8505 100644
--- a/cc/jwt/internal/jwt_ecdsa_verify_key_manager.cc
+++ b/cc/jwt/internal/jwt_ecdsa_verify_key_manager.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/jwt/internal/jwt_ecdsa_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_ecdsa_verify_key_manager.h b/cc/jwt/internal/jwt_ecdsa_verify_key_manager.h
index 5e23d58..530d2ba 100644
--- a/cc/jwt/internal/jwt_ecdsa_verify_key_manager.h
+++ b/cc/jwt/internal/jwt_ecdsa_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_ECDSA_VERIFY_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_ECDSA_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/jwt_format_test.cc b/cc/jwt/internal/jwt_format_test.cc
index f25652e..4d963c6 100644
--- a/cc/jwt/internal/jwt_format_test.cc
+++ b/cc/jwt/internal/jwt_format_test.cc
@@ -17,6 +17,7 @@
 #include "tink/jwt/internal/jwt_format.h"
 
 #include <string>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_hmac_key_manager.h b/cc/jwt/internal/jwt_hmac_key_manager.h
index 03228dd..02f544d 100644
--- a/cc/jwt/internal/jwt_hmac_key_manager.h
+++ b/cc/jwt/internal/jwt_hmac_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_HMAC_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_HMAC_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_hmac_key_manager_test.cc b/cc/jwt/internal/jwt_hmac_key_manager_test.cc
index 00be35c..d4285bf 100644
--- a/cc/jwt/internal/jwt_hmac_key_manager_test.cc
+++ b/cc/jwt/internal/jwt_hmac_key_manager_test.cc
@@ -16,7 +16,10 @@
 
 #include "tink/jwt/internal/jwt_hmac_key_manager.h"
 
+#include <memory>
+#include <sstream>
 #include <string>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_mac_impl.cc b/cc/jwt/internal/jwt_mac_impl.cc
index 5b24c1b..aa2123d 100644
--- a/cc/jwt/internal/jwt_mac_impl.cc
+++ b/cc/jwt/internal/jwt_mac_impl.cc
@@ -18,6 +18,7 @@
 
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/status/status.h"
 #include "absl/strings/escaping.h"
diff --git a/cc/jwt/internal/jwt_mac_impl.h b/cc/jwt/internal/jwt_mac_impl.h
index a5cc64b..f557cd3 100644
--- a/cc/jwt/internal/jwt_mac_impl.h
+++ b/cc/jwt/internal/jwt_mac_impl.h
@@ -17,6 +17,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_MAC_IMPL_H_
 #define TINK_JWT_INTERNAL_JWT_MAC_IMPL_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_mac_impl_test.cc b/cc/jwt/internal/jwt_mac_impl_test.cc
index fb9a7df..077e3ed 100644
--- a/cc/jwt/internal/jwt_mac_impl_test.cc
+++ b/cc/jwt/internal/jwt_mac_impl_test.cc
@@ -16,8 +16,10 @@
 
 #include "tink/jwt/internal/jwt_mac_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_mac_internal.h b/cc/jwt/internal/jwt_mac_internal.h
index 0b83f02..d7f3ec7 100644
--- a/cc/jwt/internal/jwt_mac_internal.h
+++ b/cc/jwt/internal/jwt_mac_internal.h
@@ -65,7 +65,7 @@
       absl::string_view compact, const JwtValidator& validator,
       absl::optional<absl::string_view> kid) const = 0;
 
-  virtual ~JwtMacInternal() {}
+  virtual ~JwtMacInternal() = default;
 };
 
 }  // namespace jwt_internal
diff --git a/cc/jwt/internal/jwt_mac_wrapper.cc b/cc/jwt/internal/jwt_mac_wrapper.cc
index d53fb2d..73ee43f 100644
--- a/cc/jwt/internal/jwt_mac_wrapper.cc
+++ b/cc/jwt/internal/jwt_mac_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/jwt_mac_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -48,7 +49,7 @@
       absl::string_view compact,
       const crypto::tink::JwtValidator& validator) const override;
 
-  ~JwtMacSetWrapper() override {}
+  ~JwtMacSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<JwtMacInternal>> jwt_mac_set_;
diff --git a/cc/jwt/internal/jwt_mac_wrapper.h b/cc/jwt/internal/jwt_mac_wrapper.h
index e764c6c..3a93664 100644
--- a/cc/jwt/internal/jwt_mac_wrapper.h
+++ b/cc/jwt/internal/jwt_mac_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_JWT_INTERNAL_JWT_MAC_WRAPPER_H_
 #define TINK_JWT_INTERNAL_JWT_MAC_WRAPPER_H_
 
+#include <memory>
+
 #include "tink/jwt/internal/jwt_mac_internal.h"
 #include "tink/jwt/jwt_mac.h"
 #include "tink/primitive_set.h"
diff --git a/cc/jwt/internal/jwt_mac_wrapper_test.cc b/cc/jwt/internal/jwt_mac_wrapper_test.cc
index f7e66ed..0d6062b 100644
--- a/cc/jwt/internal/jwt_mac_wrapper_test.cc
+++ b/cc/jwt/internal/jwt_mac_wrapper_test.cc
@@ -16,8 +16,10 @@
 
 #include "tink/jwt/internal/jwt_mac_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/strings/str_split.h"
diff --git a/cc/jwt/internal/jwt_public_key_sign_impl.h b/cc/jwt/internal/jwt_public_key_sign_impl.h
index 7f83103..7f665bd 100644
--- a/cc/jwt/internal/jwt_public_key_sign_impl.h
+++ b/cc/jwt/internal/jwt_public_key_sign_impl.h
@@ -17,6 +17,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_SIGN_IMPL_H_
 #define TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_SIGN_IMPL_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_public_key_sign_internal.h b/cc/jwt/internal/jwt_public_key_sign_internal.h
index 28a095b..f7522e6 100644
--- a/cc/jwt/internal/jwt_public_key_sign_internal.h
+++ b/cc/jwt/internal/jwt_public_key_sign_internal.h
@@ -41,7 +41,7 @@
   virtual crypto::tink::util::StatusOr<std::string> SignAndEncodeWithKid(
       const RawJwt& token, absl::optional<absl::string_view> kid) const = 0;
 
-  virtual ~JwtPublicKeySignInternal() {}
+  virtual ~JwtPublicKeySignInternal() = default;
 };
 
 }  // namespace tink
diff --git a/cc/jwt/internal/jwt_public_key_sign_verify_impl_test.cc b/cc/jwt/internal/jwt_public_key_sign_verify_impl_test.cc
index c69907a..14cb895 100644
--- a/cc/jwt/internal/jwt_public_key_sign_verify_impl_test.cc
+++ b/cc/jwt/internal/jwt_public_key_sign_verify_impl_test.cc
@@ -14,8 +14,10 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_public_key_sign_wrapper.cc b/cc/jwt/internal/jwt_public_key_sign_wrapper.cc
index 316e3ec..5ad41da 100644
--- a/cc/jwt/internal/jwt_public_key_sign_wrapper.cc
+++ b/cc/jwt/internal/jwt_public_key_sign_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/jwt_public_key_sign_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -44,7 +45,7 @@
   crypto::tink::util::StatusOr<std::string> SignAndEncode(
       const crypto::tink::RawJwt& token) const override;
 
-  ~JwtPublicKeySignSetWrapper() override {}
+  ~JwtPublicKeySignSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<JwtPublicKeySignInternal>> jwt_sign_set_;
diff --git a/cc/jwt/internal/jwt_public_key_sign_wrapper.h b/cc/jwt/internal/jwt_public_key_sign_wrapper.h
index 477ccd9..d1e951a 100644
--- a/cc/jwt/internal/jwt_public_key_sign_wrapper.h
+++ b/cc/jwt/internal/jwt_public_key_sign_wrapper.h
@@ -16,6 +16,8 @@
 #ifndef TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_SIGN_WRAPPER_H_
 #define TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_SIGN_WRAPPER_H_
 
+#include <memory>
+
 #include "tink/jwt/internal/jwt_public_key_sign_internal.h"
 #include "tink/jwt/jwt_public_key_sign.h"
 #include "tink/primitive_set.h"
diff --git a/cc/jwt/internal/jwt_public_key_verify_impl.cc b/cc/jwt/internal/jwt_public_key_verify_impl.cc
index 37e02b7..8ea3a6a 100644
--- a/cc/jwt/internal/jwt_public_key_verify_impl.cc
+++ b/cc/jwt/internal/jwt_public_key_verify_impl.cc
@@ -18,6 +18,7 @@
 
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/status/status.h"
 #include "absl/strings/escaping.h"
diff --git a/cc/jwt/internal/jwt_public_key_verify_impl.h b/cc/jwt/internal/jwt_public_key_verify_impl.h
index b6adba1..99c0af3 100644
--- a/cc/jwt/internal/jwt_public_key_verify_impl.h
+++ b/cc/jwt/internal/jwt_public_key_verify_impl.h
@@ -17,6 +17,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_VERIFY_IMPL_H_
 #define TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_VERIFY_IMPL_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_public_key_verify_internal.h b/cc/jwt/internal/jwt_public_key_verify_internal.h
index 9ec5aa2..392dc3c 100644
--- a/cc/jwt/internal/jwt_public_key_verify_internal.h
+++ b/cc/jwt/internal/jwt_public_key_verify_internal.h
@@ -54,7 +54,7 @@
       absl::string_view compact, const JwtValidator& validator,
       absl::optional<absl::string_view> kid) const = 0;
 
-  virtual ~JwtPublicKeyVerifyInternal() {}
+  virtual ~JwtPublicKeyVerifyInternal() = default;
 };
 
 }  // namespace tink
diff --git a/cc/jwt/internal/jwt_public_key_verify_wrapper.cc b/cc/jwt/internal/jwt_public_key_verify_wrapper.cc
index 179de6f..6d4159f 100644
--- a/cc/jwt/internal/jwt_public_key_verify_wrapper.cc
+++ b/cc/jwt/internal/jwt_public_key_verify_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/jwt_public_key_verify_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -45,7 +46,7 @@
       absl::string_view compact,
       const crypto::tink::JwtValidator& validator) const override;
 
-  ~JwtPublicKeyVerifySetWrapper() override {}
+  ~JwtPublicKeyVerifySetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<JwtPublicKeyVerifyInternal>> jwt_verify_set_;
diff --git a/cc/jwt/internal/jwt_public_key_verify_wrapper.h b/cc/jwt/internal/jwt_public_key_verify_wrapper.h
index ac0b807..cc59461 100644
--- a/cc/jwt/internal/jwt_public_key_verify_wrapper.h
+++ b/cc/jwt/internal/jwt_public_key_verify_wrapper.h
@@ -16,6 +16,8 @@
 #ifndef TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_VERIFY_WRAPPER_H_
 #define TINK_JWT_INTERNAL_JWT_PUBLIC_KEY_VERIFY_WRAPPER_H_
 
+#include <memory>
+
 #include "tink/jwt/internal/jwt_public_key_verify_internal.h"
 #include "tink/jwt/jwt_public_key_verify.h"
 #include "tink/primitive_set.h"
diff --git a/cc/jwt/internal/jwt_public_key_wrappers_test.cc b/cc/jwt/internal/jwt_public_key_wrappers_test.cc
index 285aa6e..ce5c8ca 100644
--- a/cc/jwt/internal/jwt_public_key_wrappers_test.cc
+++ b/cc/jwt/internal/jwt_public_key_wrappers_test.cc
@@ -14,8 +14,10 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/strings/str_split.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.cc b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.cc
index 8cd220f..31bd8ad 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.cc
+++ b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.cc
@@ -15,8 +15,9 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.h"
 
-#include <utility>
+#include <memory>
 #include <string>
+#include <utility>
 
 #include "tink/jwt/internal/jwt_public_key_sign_impl.h"
 #include "tink/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.h b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.h
index 5e799e5..0cd598a 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.h
+++ b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_RSA_SSA_PKCS1_SIGN_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_RSA_SSA_PKCS1_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_verify_key_manager_test.cc b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_verify_key_manager_test.cc
index 14f00dc..6741929 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_verify_key_manager_test.cc
+++ b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_sign_verify_key_manager_test.cc
@@ -14,8 +14,10 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.cc b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.cc
index 2d75ae4..3c4432c 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.cc
+++ b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.h b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.h
index be85648..f977489 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.h
+++ b/cc/jwt/internal/jwt_rsa_ssa_pkcs1_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_RSA_SSA_PKCS1_VERIFY_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_RSA_SSA_PKCS1_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.cc b/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.cc
index 470be79..183ebb6 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.cc
+++ b/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.h b/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.h
index 1ac226b..fd8891f 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.h
+++ b/cc/jwt/internal/jwt_rsa_ssa_pss_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_RSA_SSA_PSS_SIGN_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_RSA_SSA_PSS_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pss_sign_verify_key_manager_test.cc b/cc/jwt/internal/jwt_rsa_ssa_pss_sign_verify_key_manager_test.cc
index 5fe31e6..ddc7c0b 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pss_sign_verify_key_manager_test.cc
+++ b/cc/jwt/internal/jwt_rsa_ssa_pss_sign_verify_key_manager_test.cc
@@ -14,8 +14,10 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.cc b/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.cc
index c6813b0..4826e99 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.cc
+++ b/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.h b/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.h
index dd6d5f8..c8ba629 100644
--- a/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.h
+++ b/cc/jwt/internal/jwt_rsa_ssa_pss_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_JWT_RSA_SSA_PSS_VERIFY_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_JWT_RSA_SSA_PSS_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.cc b/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.cc
index 2d917a0..144bf8b 100644
--- a/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_ecdsa_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.h b/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.h
index d90660e..99e8911 100644
--- a/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_RAW_JWT_ECDSA_SIGN_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_RAW_JWT_ECDSA_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager_test.cc b/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager_test.cc
index 6147e88..552b6aa 100644
--- a/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_ecdsa_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_ecdsa_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.cc b/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.cc
index b9e9675..956790a 100644
--- a/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_ecdsa_verify_key_manager.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
diff --git a/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.h b/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.h
index ecfafe1..8179d48 100644
--- a/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_RAW_JWT_ECDSA_VERIFY_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_RAW_JWT_ECDSA_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager_test.cc b/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager_test.cc
index 8a4bc8a..84b7dec 100644
--- a/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_ecdsa_verify_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_ecdsa_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/jwt/internal/raw_jwt_hmac_key_manager.cc b/cc/jwt/internal/raw_jwt_hmac_key_manager.cc
index 0169931..72b4335 100644
--- a/cc/jwt/internal/raw_jwt_hmac_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_hmac_key_manager.cc
@@ -46,8 +46,6 @@
 
 namespace {
 
-constexpr int kMinKeySizeInBytes = 32;
-
 StatusOr<int> MinimumKeySize(const JwtHmacAlgorithm& algorithm) {
   switch (algorithm) {
     case JwtHmacAlgorithm::HS256:
diff --git a/cc/jwt/internal/raw_jwt_hmac_key_manager.h b/cc/jwt/internal/raw_jwt_hmac_key_manager.h
index 62d88a9..7e6b1e3 100644
--- a/cc/jwt/internal/raw_jwt_hmac_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_hmac_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_RAW_JWT_HMAC_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_RAW_JWT_HMAC_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_hmac_key_manager_test.cc b/cc/jwt/internal/raw_jwt_hmac_key_manager_test.cc
index 4235952..88428ee 100644
--- a/cc/jwt/internal/raw_jwt_hmac_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_hmac_key_manager_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/jwt/internal/raw_jwt_hmac_key_manager.h"
 
+#include <memory>
+#include <sstream>
 #include <utility>
 
 #include "gmock/gmock.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.cc
index d0839d7..a4450f3 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.h b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.h
index 22b3f83..efe5f32 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PKCS1_SIGN_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PKCS1_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager_test.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager_test.cc
index e1eaf2d..32395ae 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pkcs1_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.cc
index 10dae49..9e8a415 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.h b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.h
index 7cbb449..aca8285 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.h
@@ -18,6 +18,7 @@
 #define TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PKCS1_VERIFY_KEY_MANAGER_H_
 
 #include <algorithm>
+#include <memory>
 #include <string>
 #include <vector>
 
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager_test.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager_test.cc
index be19b50..3bd1629 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pkcs1_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.cc
index 9fd230c..9b4509b 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.h b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.h
index 12f55e2..bf06792 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PSS_SIGN_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PSS_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager_test.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager_test.cc
index f6adfcf..56e627b 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pss_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.cc
index baabdf9..d5e83b1 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.h b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.h
index 5750194..0e9cc8f 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.h
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PSS_VERIFY_KEY_MANAGER_H_
 #define TINK_JWT_INTERNAL_RAW_JWT_RSA_SSA_PSS_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager_test.cc b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager_test.cc
index 39ec7ec..6cc9688 100644
--- a/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager_test.cc
+++ b/cc/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/internal/raw_jwt_rsa_ssa_pss_verify_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/jwt/jwk_set_converter.cc b/cc/jwt/jwk_set_converter.cc
index 4d8a7af..cf26d38 100644
--- a/cc/jwt/jwk_set_converter.cc
+++ b/cc/jwt/jwk_set_converter.cc
@@ -16,6 +16,9 @@
 
 #include "tink/jwt/jwk_set_converter.h"
 
+#include <memory>
+#include <ostream>
+#include <sstream>
 #include <string>
 
 #include "absl/strings/escaping.h"
diff --git a/cc/jwt/jwk_set_converter.h b/cc/jwt/jwk_set_converter.h
index 6595a23..1d3914d 100644
--- a/cc/jwt/jwk_set_converter.h
+++ b/cc/jwt/jwk_set_converter.h
@@ -17,6 +17,7 @@
 #ifndef TINK_JWT_JWK_SET_CONVERTER_H_
 #define TINK_JWT_JWK_SET_CONVERTER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/jwt/jwk_set_converter_test.cc b/cc/jwt/jwk_set_converter_test.cc
index 743518c..494cdf5 100644
--- a/cc/jwt/jwk_set_converter_test.cc
+++ b/cc/jwt/jwk_set_converter_test.cc
@@ -16,7 +16,9 @@
 
 #include "tink/jwt/jwk_set_converter.h"
 
+#include <memory>
 #include <string>
+#include <tuple>
 #include <utility>
 
 #include "google/protobuf/util/message_differencer.h"
diff --git a/cc/jwt/jwt_key_templates_test.cc b/cc/jwt/jwt_key_templates_test.cc
index f52d2ff..d85c450 100644
--- a/cc/jwt/jwt_key_templates_test.cc
+++ b/cc/jwt/jwt_key_templates_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/jwt/jwt_key_templates.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/jwt/jwt_mac.h b/cc/jwt/jwt_mac.h
index 3589e00..7b4374b 100644
--- a/cc/jwt/jwt_mac.h
+++ b/cc/jwt/jwt_mac.h
@@ -56,7 +56,7 @@
   virtual crypto::tink::util::StatusOr<VerifiedJwt> VerifyMacAndDecode(
       absl::string_view compact, const JwtValidator& validator) const = 0;
 
-  virtual ~JwtMac() {}
+  virtual ~JwtMac() = default;
 };
 
 }  // namespace tink
diff --git a/cc/jwt/jwt_public_key_sign.h b/cc/jwt/jwt_public_key_sign.h
index 840182a..a20f1d1 100644
--- a/cc/jwt/jwt_public_key_sign.h
+++ b/cc/jwt/jwt_public_key_sign.h
@@ -38,7 +38,7 @@
   virtual crypto::tink::util::StatusOr<std::string> SignAndEncode(
       const RawJwt& token) const = 0;
 
-  virtual ~JwtPublicKeySign() {}
+  virtual ~JwtPublicKeySign() = default;
 };
 
 }  // namespace tink
diff --git a/cc/jwt/jwt_public_key_verify.h b/cc/jwt/jwt_public_key_verify.h
index 1e6eb3a..decdc2f 100644
--- a/cc/jwt/jwt_public_key_verify.h
+++ b/cc/jwt/jwt_public_key_verify.h
@@ -48,7 +48,7 @@
   virtual crypto::tink::util::StatusOr<VerifiedJwt> VerifyAndDecode(
       absl::string_view compact, const JwtValidator& validator) const = 0;
 
-  virtual ~JwtPublicKeyVerify() {}
+  virtual ~JwtPublicKeyVerify() = default;
 };
 
 }  // namespace tink
diff --git a/cc/jwt/jwt_validator.cc b/cc/jwt/jwt_validator.cc
index c56a399..881d9e1 100644
--- a/cc/jwt/jwt_validator.cc
+++ b/cc/jwt/jwt_validator.cc
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <string>
+#include <vector>
 
 #include "absl/status/status.h"
 
diff --git a/cc/jwt/raw_jwt.cc b/cc/jwt/raw_jwt.cc
index e1fae19..23ba60d 100644
--- a/cc/jwt/raw_jwt.cc
+++ b/cc/jwt/raw_jwt.cc
@@ -18,6 +18,7 @@
 
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/status/status.h"
 #include "absl/strings/numbers.h"
@@ -185,7 +186,7 @@
   return jwt_internal::ProtoStructToJsonString(json_proto_);
 }
 
-RawJwt::RawJwt() {}
+RawJwt::RawJwt() = default;
 
 RawJwt::RawJwt(absl::optional<std::string> type_header,
                google::protobuf::Struct json_proto) {
diff --git a/cc/jwt/raw_jwt.h b/cc/jwt/raw_jwt.h
index 36f40de..2a83d23 100644
--- a/cc/jwt/raw_jwt.h
+++ b/cc/jwt/raw_jwt.h
@@ -18,6 +18,7 @@
 #define TINK_JWT_RAW_JWT_H_
 
 #include <string>
+#include <vector>
 
 #include "google/protobuf/struct.pb.h"
 #include "absl/strings/string_view.h"
diff --git a/cc/jwt/raw_jwt_test.cc b/cc/jwt/raw_jwt_test.cc
index e7dc361..a191d56 100644
--- a/cc/jwt/raw_jwt_test.cc
+++ b/cc/jwt/raw_jwt_test.cc
@@ -17,6 +17,7 @@
 #include "tink/jwt/raw_jwt.h"
 
 #include <string>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
@@ -473,7 +474,15 @@
       RawJwtBuilder().SetIssuer("issuer").WithoutExpiration().Build();
   ASSERT_THAT(jwt, IsOk());
 
-  ASSERT_THAT(jwt->GetJsonPayload(), IsOkAndHolds(R"({"iss":"issuer"})"));
+  EXPECT_THAT(jwt->GetJsonPayload(), IsOkAndHolds(R"({"iss":"issuer"})"));
+}
+
+TEST(RawJwt, IntegerIsEncodedAsInteger) {
+  util::StatusOr<RawJwt> jwt =
+      RawJwtBuilder().AddNumberClaim("num", 1).WithoutExpiration().Build();
+  ASSERT_THAT(jwt, IsOk());
+
+  EXPECT_THAT(jwt->GetJsonPayload(), IsOkAndHolds(R"({"num":1})"));
 }
 
 TEST(RawJwt, GetExpirationJsonPayload) {
diff --git a/cc/jwt/verified_jwt.cc b/cc/jwt/verified_jwt.cc
index 4374044..c1359f5 100644
--- a/cc/jwt/verified_jwt.cc
+++ b/cc/jwt/verified_jwt.cc
@@ -17,6 +17,7 @@
 #include "tink/jwt/verified_jwt.h"
 
 #include <string>
+#include <vector>
 
 #include "absl/strings/numbers.h"
 #include "absl/strings/str_format.h"
@@ -26,7 +27,7 @@
 namespace crypto {
 namespace tink {
 
-VerifiedJwt::VerifiedJwt() {}
+VerifiedJwt::VerifiedJwt() = default;
 
 VerifiedJwt::VerifiedJwt(const RawJwt& raw_jwt) {
   raw_jwt_ = raw_jwt;
diff --git a/cc/jwt/verified_jwt.h b/cc/jwt/verified_jwt.h
index faa5ba3..e4d4c85 100644
--- a/cc/jwt/verified_jwt.h
+++ b/cc/jwt/verified_jwt.h
@@ -18,6 +18,7 @@
 #define TINK_JWT_VERIFIED_JWT_H_
 
 #include <string>
+#include <vector>
 
 #include "google/protobuf/struct.pb.h"
 #include "absl/strings/string_view.h"
diff --git a/cc/jwt/verified_jwt_test.cc b/cc/jwt/verified_jwt_test.cc
index ff1ba54..ed0c7e3 100644
--- a/cc/jwt/verified_jwt_test.cc
+++ b/cc/jwt/verified_jwt_test.cc
@@ -16,8 +16,10 @@
 
 #include "tink/jwt/verified_jwt.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
@@ -326,7 +328,7 @@
   VerifiedJwt jwt2 = std::move(jwt1);
   // We want that a VerifiedJwt object remains a valid object, even after
   // std::moved has been called.
-  EXPECT_TRUE(jwt1.HasIssuer());
+  EXPECT_TRUE(jwt1.HasIssuer());  // NOLINT(bugprone-use-after-move)
   EXPECT_THAT(jwt1.GetIssuer(), IsOkAndHolds("issuer"));
   EXPECT_TRUE(jwt2.HasIssuer());
   EXPECT_THAT(jwt2.GetIssuer(), IsOkAndHolds("issuer"));
diff --git a/cc/key_access.h b/cc/key_access.h
index 51b7e32..77465a6 100644
--- a/cc/key_access.h
+++ b/cc/key_access.h
@@ -14,8 +14,8 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-#ifndef THIRD_PARTY_TINK_KEY_ACCESS_H_
-#define THIRD_PARTY_TINK_KEY_ACCESS_H_
+#ifndef TINK_KEY_ACCESS_H_
+#define TINK_KEY_ACCESS_H_
 
 namespace crypto {
 namespace tink {
@@ -27,7 +27,7 @@
     return token;
   }
 
-  const bool CanAccessSecret() { return can_access_secret_; }
+  bool CanAccessSecret() { return can_access_secret_; }
 
   // KeyAccess objects are copiable and movable.
   KeyAccess(const KeyAccess&) = default;
@@ -46,4 +46,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // THIRD_PARTY_TINK_KEY_ACCESS_H_
+#endif  // TINK_KEY_ACCESS_H_
diff --git a/cc/key_gen_configuration.h b/cc/key_gen_configuration.h
new file mode 100644
index 0000000..f8e3e09
--- /dev/null
+++ b/cc/key_gen_configuration.h
@@ -0,0 +1,52 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEY_GEN_CONFIGURATION_H_
+#define TINK_KEY_GEN_CONFIGURATION_H_
+
+#include "tink/internal/key_type_info_store.h"
+
+namespace crypto {
+namespace tink {
+
+namespace internal {
+class KeyGenConfigurationImpl;
+}
+
+// KeyGenConfiguration used to generate keys using stored key type managers.
+class KeyGenConfiguration {
+ public:
+  KeyGenConfiguration() = default;
+
+  // Not copyable or movable.
+  KeyGenConfiguration(const KeyGenConfiguration&) = delete;
+  KeyGenConfiguration& operator=(const KeyGenConfiguration&) = delete;
+
+ private:
+  friend class internal::KeyGenConfigurationImpl;
+
+  // When true, KeyGenConfiguration is in global registry mode. For
+  // `some_fn(config)` with a `config` parameter, this indicates to `some_fn` to
+  // use crypto::tink::Registry directly.
+  bool global_registry_mode_ = false;
+
+  crypto::tink::internal::KeyTypeInfoStore key_type_info_store_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEY_GEN_CONFIGURATION_H_
diff --git a/cc/key_manager.h b/cc/key_manager.h
index 871acee..58d2744 100644
--- a/cc/key_manager.h
+++ b/cc/key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_KEY_MANAGER_H_
 #define TINK_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
@@ -56,7 +57,7 @@
       std::unique_ptr<google::crypto::tink::KeyData>>
   NewKeyData(absl::string_view serialized_key_format) const = 0;
 
-  virtual ~KeyFactory() {}
+  virtual ~KeyFactory() = default;
 };
 
 class PrivateKeyFactory : public virtual KeyFactory {
@@ -66,7 +67,7 @@
       std::unique_ptr<google::crypto::tink::KeyData>>
   GetPublicKeyData(absl::string_view serialized_private_key) const = 0;
 
-  virtual ~PrivateKeyFactory() {}
+  ~PrivateKeyFactory() override = default;
 };
 
 /**
@@ -94,7 +95,7 @@
     return (key_type == get_key_type());
   }
 
-  virtual ~KeyManagerBase() {}
+  virtual ~KeyManagerBase() = default;
 };
 
 template <class P>
diff --git a/cc/key_status.h b/cc/key_status.h
new file mode 100644
index 0000000..8d37cb5
--- /dev/null
+++ b/cc/key_status.h
@@ -0,0 +1,36 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEY_STATUS_H_
+#define TINK_KEY_STATUS_H_
+
+namespace crypto {
+namespace tink {
+
+// Enum representation of KeyStatusType in tink/proto/tink.proto. Using an
+// enum class prevents unintentional implicit conversions.
+enum class KeyStatus : int {
+  kEnabled = 1,    // Can be used for cryptographic operations.
+  kDisabled = 2,   // Cannot be used (but can become kEnabled again).
+  kDestroyed = 3,  // Key data does not exist in this Keyset any more.
+  // Added to guard from failures that may be caused by future expansions.
+  kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEY_STATUS_H_
diff --git a/cc/keyderivation/BUILD.bazel b/cc/keyderivation/BUILD.bazel
new file mode 100644
index 0000000..449698a
--- /dev/null
+++ b/cc/keyderivation/BUILD.bazel
@@ -0,0 +1,115 @@
+package(
+    default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+cc_library(
+    name = "key_derivation_config",
+    srcs = ["key_derivation_config.cc"],
+    hdrs = ["key_derivation_config.h"],
+    include_prefix = "tink/keyderivation",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":keyset_deriver_wrapper",
+        "//config:tink_fips",
+        "//keyderivation/internal:prf_based_deriver_key_manager",
+        "//prf:hkdf_prf_key_manager",
+        "//util:status",
+    ],
+)
+
+cc_test(
+    name = "key_derivation_config_test",
+    srcs = ["key_derivation_config_test.cc"],
+    deps = [
+        ":key_derivation_config",
+        ":key_derivation_key_templates",
+        ":keyset_deriver",
+        "//:registry",
+        "//aead:aead_config",
+        "//aead:aead_key_templates",
+        "//aead:aes_gcm_key_manager",
+        "//prf:prf_key_templates",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "key_derivation_key_templates",
+    srcs = ["key_derivation_key_templates.cc"],
+    hdrs = ["key_derivation_key_templates.h"],
+    include_prefix = "tink/keyderivation",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//keyderivation/internal:prf_based_deriver_key_manager",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:statusor",
+    ],
+)
+
+cc_test(
+    name = "key_derivation_key_templates_test",
+    srcs = ["key_derivation_key_templates_test.cc"],
+    deps = [
+        ":key_derivation_key_templates",
+        ":keyset_deriver_wrapper",
+        "//:registry",
+        "//aead:aead_key_templates",
+        "//aead:aes_gcm_key_manager",
+        "//keyderivation/internal:prf_based_deriver_key_manager",
+        "//prf:hkdf_prf_key_manager",
+        "//prf:prf_key_templates",
+        "//proto:prf_based_deriver_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "keyset_deriver",
+    hdrs = ["keyset_deriver.h"],
+    include_prefix = "tink/keyderivation",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:keyset_handle",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "keyset_deriver_wrapper",
+    srcs = ["keyset_deriver_wrapper.cc"],
+    hdrs = ["keyset_deriver_wrapper.h"],
+    include_prefix = "tink/keyderivation",
+    deps = [
+        ":keyset_deriver",
+        "//:cleartext_keyset_handle",
+        "//:primitive_set",
+        "//:primitive_wrapper",
+        "//proto:tink_cc_proto",
+        "@com_google_absl//absl/status",
+    ],
+)
+
+cc_test(
+    name = "keyset_deriver_wrapper_test",
+    srcs = ["keyset_deriver_wrapper_test.cc"],
+    deps = [
+        ":keyset_deriver",
+        ":keyset_deriver_wrapper",
+        "//:cleartext_keyset_handle",
+        "//:primitive_set",
+        "//proto:tink_cc_proto",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/keyderivation/CMakeLists.txt b/cc/keyderivation/CMakeLists.txt
new file mode 100644
index 0000000..fa3d7e8
--- /dev/null
+++ b/cc/keyderivation/CMakeLists.txt
@@ -0,0 +1,109 @@
+tink_module(keyderivation)
+
+add_subdirectory(internal)
+
+tink_cc_library(
+  NAME key_derivation_config
+  SRCS
+    key_derivation_config.cc
+    key_derivation_config.h
+  DEPS
+    tink::keyderivation::keyset_deriver_wrapper
+    tink::config::tink_fips
+    tink::keyderivation::internal::prf_based_deriver_key_manager
+    tink::prf::hkdf_prf_key_manager
+    tink::util::status
+  PUBLIC
+)
+
+tink_cc_test(
+  NAME key_derivation_config_test
+  SRCS
+    key_derivation_config_test.cc
+  DEPS
+    tink::keyderivation::key_derivation_config
+    tink::keyderivation::key_derivation_key_templates
+    tink::keyderivation::keyset_deriver
+    gmock
+    tink::core::registry
+    tink::aead::aead_config
+    tink::aead::aead_key_templates
+    tink::aead::aes_gcm_key_manager
+    tink::prf::prf_key_templates
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME key_derivation_key_templates
+  SRCS
+    key_derivation_key_templates.cc
+    key_derivation_key_templates.h
+  DEPS
+    tink::keyderivation::internal::prf_based_deriver_key_manager
+    tink::subtle::random
+    tink::util::statusor
+    tink::proto::tink_cc_proto
+  PUBLIC
+)
+
+tink_cc_test(
+  NAME key_derivation_key_templates_test
+  SRCS
+    key_derivation_key_templates_test.cc
+  DEPS
+    tink::keyderivation::key_derivation_key_templates
+    tink::keyderivation::keyset_deriver_wrapper
+    gmock
+    absl::status
+    tink::core::registry
+    tink::aead::aead_key_templates
+    tink::aead::aes_gcm_key_manager
+    tink::keyderivation::internal::prf_based_deriver_key_manager
+    tink::prf::hkdf_prf_key_manager
+    tink::prf::prf_key_templates
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::prf_based_deriver_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME keyset_deriver
+  SRCS
+    keyset_deriver.h
+  DEPS
+    absl::strings
+    tink::core::keyset_handle
+    tink::util::statusor
+  PUBLIC
+)
+
+tink_cc_library(
+  NAME keyset_deriver_wrapper
+  SRCS
+    keyset_deriver_wrapper.cc
+    keyset_deriver_wrapper.h
+  DEPS
+    tink::keyderivation::keyset_deriver
+    absl::status
+    tink::core::cleartext_keyset_handle
+    tink::core::primitive_set
+    tink::core::primitive_wrapper
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME keyset_deriver_wrapper_test
+  SRCS
+    keyset_deriver_wrapper_test.cc
+  DEPS
+    tink::keyderivation::keyset_deriver
+    tink::keyderivation::keyset_deriver_wrapper
+    gmock
+    absl::status
+    tink::core::cleartext_keyset_handle
+    tink::core::primitive_set
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/keyderivation/internal/BUILD.bazel b/cc/keyderivation/internal/BUILD.bazel
new file mode 100644
index 0000000..09aa90b
--- /dev/null
+++ b/cc/keyderivation/internal/BUILD.bazel
@@ -0,0 +1,73 @@
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+cc_library(
+    name = "prf_based_deriver",
+    srcs = ["prf_based_deriver.cc"],
+    hdrs = ["prf_based_deriver.h"],
+    include_prefix = "tink/keyderivation/internal",
+    deps = [
+        "//:cleartext_keyset_handle",
+        "//:keyset_handle",
+        "//:registry",
+        "//keyderivation:keyset_deriver",
+        "//proto:tink_cc_proto",
+        "//subtle/prf:streaming_prf",
+    ],
+)
+
+cc_test(
+    name = "prf_based_deriver_test",
+    srcs = ["prf_based_deriver_test.cc"],
+    deps = [
+        ":prf_based_deriver",
+        "//:cleartext_keyset_handle",
+        "//aead:aead_key_templates",
+        "//aead:aes_gcm_key_manager",
+        "//prf:hkdf_prf_key_manager",
+        "//proto:aes_gcm_cc_proto",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "prf_based_deriver_key_manager",
+    hdrs = ["prf_based_deriver_key_manager.h"],
+    include_prefix = "tink/keyderivation/internal",
+    deps = [
+        ":prf_based_deriver",
+        "//keyderivation:keyset_deriver",
+        "//proto:prf_based_deriver_cc_proto",
+        "//proto:tink_cc_proto",
+        "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "prf_based_deriver_key_manager_test",
+    srcs = ["prf_based_deriver_key_manager_test.cc"],
+    deps = [
+        ":prf_based_deriver_key_manager",
+        "//:cleartext_keyset_handle",
+        "//aead:aead_key_templates",
+        "//aead:aes_gcm_key_manager",
+        "//keyderivation:keyset_deriver",
+        "//prf:hkdf_prf_key_manager",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:hkdf_prf_cc_proto",
+        "//proto:prf_based_deriver_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle",
+        "//util:statusor",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/keyderivation/internal/CMakeLists.txt b/cc/keyderivation/internal/CMakeLists.txt
new file mode 100644
index 0000000..2fd4c7e
--- /dev/null
+++ b/cc/keyderivation/internal/CMakeLists.txt
@@ -0,0 +1,69 @@
+tink_module(keyderivation::internal)
+
+tink_cc_library(
+  NAME prf_based_deriver
+  SRCS
+    prf_based_deriver.cc
+    prf_based_deriver.h
+  DEPS
+    tink::core::cleartext_keyset_handle
+    tink::core::keyset_handle
+    tink::core::registry
+    tink::keyderivation::keyset_deriver
+    tink::subtle::prf::streaming_prf
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME prf_based_deriver_test
+  SRCS
+    prf_based_deriver_test.cc
+  DEPS
+    tink::keyderivation::internal::prf_based_deriver
+    gmock
+    absl::status
+    tink::core::cleartext_keyset_handle
+    tink::aead::aead_key_templates
+    tink::aead::aes_gcm_key_manager
+    tink::prf::hkdf_prf_key_manager
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_gcm_cc_proto
+)
+
+tink_cc_library(
+  NAME prf_based_deriver_key_manager
+  SRCS
+    prf_based_deriver_key_manager.h
+  DEPS
+    tink::keyderivation::internal::prf_based_deriver
+    absl::memory
+    absl::status
+    absl::strings
+    tink::keyderivation::keyset_deriver
+    tink::proto::prf_based_deriver_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME prf_based_deriver_key_manager_test
+  SRCS
+    prf_based_deriver_key_manager_test.cc
+  DEPS
+    tink::keyderivation::internal::prf_based_deriver_key_manager
+    gmock
+    absl::status
+    tink::core::cleartext_keyset_handle
+    tink::aead::aead_key_templates
+    tink::aead::aes_gcm_key_manager
+    tink::keyderivation::keyset_deriver
+    tink::prf::hkdf_prf_key_manager
+    tink::subtle::subtle
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::hkdf_prf_cc_proto
+    tink::proto::prf_based_deriver_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/keyderivation/internal/prf_based_deriver.cc b/cc/keyderivation/internal/prf_based_deriver.cc
new file mode 100644
index 0000000..3aa7c72
--- /dev/null
+++ b/cc/keyderivation/internal/prf_based_deriver.cc
@@ -0,0 +1,90 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/internal/prf_based_deriver.h"
+
+#include <memory>
+#include <utility>
+
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/keyset_handle.h"
+#include "tink/registry.h"
+#include "tink/subtle/prf/streaming_prf.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+
+util::StatusOr<std::unique_ptr<KeysetDeriver>> PrfBasedDeriver::New(
+    const KeyData& prf_key, const KeyTemplate& key_template) {
+  // Validate `prf_key`.
+  util::StatusOr<std::unique_ptr<StreamingPrf>> streaming_prf =
+      Registry::GetPrimitive<StreamingPrf>(prf_key);
+  if (!streaming_prf.ok()) {
+    return streaming_prf.status();
+  }
+
+  // Validate `key_template`.
+  std::unique_ptr<InputStream> randomness = (*streaming_prf)->ComputePrf("s");
+  util::StatusOr<KeyData> key_data =
+      internal::RegistryImpl::GlobalInstance().DeriveKey(key_template,
+                                                         randomness.get());
+  if (!key_data.ok()) {
+    return key_data.status();
+  }
+
+  return {absl::WrapUnique<PrfBasedDeriver>(
+      new PrfBasedDeriver(*std::move(streaming_prf), key_template))};
+}
+
+util::StatusOr<std::unique_ptr<KeysetHandle>> PrfBasedDeriver::DeriveKeyset(
+    absl::string_view salt) const {
+  std::unique_ptr<InputStream> randomness = streaming_prf_->ComputePrf(salt);
+
+  util::StatusOr<KeyData> key_data =
+      crypto::tink::internal::RegistryImpl::GlobalInstance().DeriveKey(
+          key_template_, randomness.get());
+  if (!key_data.ok()) {
+    return key_data.status();
+  }
+
+  // Fill in placeholder values for key ID, status, and output prefix type.
+  // These will be populated with the correct values in the keyset deriver
+  // factory. This is acceptable because the keyset as-is will never leave Tink,
+  // and the user only interacts via the keyset deriver factory.
+  Keyset::Key key;
+  *key.mutable_key_data() = *key_data;
+  key.set_status(KeyStatusType::UNKNOWN_STATUS);
+  key.set_key_id(0);
+  key.set_output_prefix_type(OutputPrefixType::UNKNOWN_PREFIX);
+
+  Keyset keyset;
+  *keyset.add_key() = key;
+  keyset.set_primary_key_id(0);
+
+  return CleartextKeysetHandle::GetKeysetHandle(keyset);
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/internal/prf_based_deriver.h b/cc/keyderivation/internal/prf_based_deriver.h
new file mode 100644
index 0000000..0b07d3f
--- /dev/null
+++ b/cc/keyderivation/internal/prf_based_deriver.h
@@ -0,0 +1,55 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYDERIVATION_INTERNAL_PRF_BASED_DERIVER_H_
+#define TINK_KEYDERIVATION_INTERNAL_PRF_BASED_DERIVER_H_
+
+#include <memory>
+#include <utility>
+
+#include "tink/keyderivation/keyset_deriver.h"
+#include "tink/keyset_handle.h"
+#include "tink/subtle/prf/streaming_prf.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// The PrfBasedDeriver first uses a PRF to get some randomness, then gives this
+// to the Tink registry to derive a key.
+class PrfBasedDeriver : public KeysetDeriver {
+ public:
+  static crypto::tink::util::StatusOr<std::unique_ptr<KeysetDeriver>> New(
+      const ::google::crypto::tink::KeyData& prf_key,
+      const ::google::crypto::tink::KeyTemplate& key_template);
+
+  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> DeriveKeyset(
+      absl::string_view salt) const override;
+
+ private:
+  PrfBasedDeriver(std::unique_ptr<StreamingPrf> streaming_prf,
+                  const ::google::crypto::tink::KeyTemplate& key_template)
+      : streaming_prf_(std::move(streaming_prf)), key_template_(key_template) {}
+
+  const ::std::unique_ptr<StreamingPrf> streaming_prf_;
+  const ::google::crypto::tink::KeyTemplate key_template_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYDERIVATION_INTERNAL_PRF_BASED_DERIVER_H_
diff --git a/cc/keyderivation/internal/prf_based_deriver_key_manager.h b/cc/keyderivation/internal/prf_based_deriver_key_manager.h
new file mode 100644
index 0000000..f5725d3
--- /dev/null
+++ b/cc/keyderivation/internal/prf_based_deriver_key_manager.h
@@ -0,0 +1,132 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYDERIVATION_INTERNAL_PRF_BASED_DERIVER_KEY_MANAGER_H_
+#define TINK_KEYDERIVATION_INTERNAL_PRF_BASED_DERIVER_KEY_MANAGER_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "tink/keyderivation/internal/prf_based_deriver.h"
+#include "tink/keyderivation/keyset_deriver.h"
+#include "proto/prf_based_deriver.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+class PrfBasedDeriverKeyManager
+    : public KeyTypeManager<google::crypto::tink::PrfBasedDeriverKey,
+                            google::crypto::tink::PrfBasedDeriverKeyFormat,
+                            List<KeysetDeriver>> {
+ public:
+  class KeysetDeriverFactory : public PrimitiveFactory<KeysetDeriver> {
+    crypto::tink::util::StatusOr<std::unique_ptr<KeysetDeriver>> Create(
+        const google::crypto::tink::PrfBasedDeriverKey& key) const override {
+      return internal::PrfBasedDeriver::New(
+          key.prf_key(), key.params().derived_key_template());
+    }
+  };
+
+  PrfBasedDeriverKeyManager()
+      : KeyTypeManager(absl::make_unique<
+                       PrfBasedDeriverKeyManager::KeysetDeriverFactory>()) {}
+
+  // Returns the version of this key manager.
+  uint32_t get_version() const override { return 0; }
+
+  google::crypto::tink::KeyData::KeyMaterialType key_material_type()
+      const override {
+    return google::crypto::tink::KeyData::SYMMETRIC;
+  }
+
+  const std::string& get_key_type() const override { return key_type_; }
+
+  crypto::tink::util::Status ValidateKey(
+      const google::crypto::tink::PrfBasedDeriverKey& key) const override {
+    crypto::tink::util::Status status =
+        ValidateVersion(key.version(), get_version());
+    if (!status.ok()) return status;
+    if (!key.has_prf_key()) {
+      return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
+                                        "key.prf_key() must be set");
+    }
+    if (!key.params().has_derived_key_template()) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "key.params().derived_key_template() must be set");
+    }
+    return util::OkStatus();
+  }
+
+  crypto::tink::util::Status ValidateKeyFormat(
+      const google::crypto::tink::PrfBasedDeriverKeyFormat& key_format)
+      const override {
+    if (!key_format.has_prf_key_template()) {
+      return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
+                                        "key.prf_key_template() must be set");
+    }
+    if (!key_format.params().has_derived_key_template()) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "key_format.params().derived_key_template() must be set");
+    }
+    return util::OkStatus();
+  }
+
+  crypto::tink::util::StatusOr<google::crypto::tink::PrfBasedDeriverKey>
+  CreateKey(const google::crypto::tink::PrfBasedDeriverKeyFormat& key_format)
+      const override {
+    crypto::tink::util::StatusOr<std::unique_ptr<google::crypto::tink::KeyData>>
+        prf_key = CreateKeyData(key_format.prf_key_template());
+    if (!prf_key.ok()) return prf_key.status();
+
+    // Java and Go implementations perform additional verification by getting a
+    // StreamingPrf primitive from the registry and trying to derive
+    // `key_format.params().derived_key_template()` with a fake salt. This is
+    // currently not possible in C++.
+
+    google::crypto::tink::PrfBasedDeriverKey key;
+    key.set_version(get_version());
+    *key.mutable_params()->mutable_derived_key_template() =
+        key_format.params().derived_key_template();
+    *key.mutable_prf_key() = **std::move(prf_key);
+    return key;
+  }
+
+ protected:
+  virtual crypto::tink::util::StatusOr<
+      std::unique_ptr<google::crypto::tink::KeyData>>
+  CreateKeyData(const google::crypto::tink::KeyTemplate& key_template) const {
+    return Registry::NewKeyData(key_template);
+  }
+
+ private:
+  const std::string key_type_ =
+      absl::StrCat(kTypeGoogleapisCom,
+                   google::crypto::tink::PrfBasedDeriverKey().GetTypeName());
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYDERIVATION_INTERNAL_PRF_BASED_DERIVER_KEY_MANAGER_H_
diff --git a/cc/keyderivation/internal/prf_based_deriver_key_manager_test.cc b/cc/keyderivation/internal/prf_based_deriver_key_manager_test.cc
new file mode 100644
index 0000000..62bb1f1
--- /dev/null
+++ b/cc/keyderivation/internal/prf_based_deriver_key_manager_test.cc
@@ -0,0 +1,287 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/internal/prf_based_deriver_key_manager.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/keyderivation/keyset_deriver.h"
+#include "tink/prf/hkdf_prf_key_manager.h"
+#include "tink/subtle/random.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/hkdf_prf.pb.h"
+#include "proto/prf_based_deriver.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HkdfPrfKey;
+using ::google::crypto::tink::HkdfPrfKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::PrfBasedDeriverKey;
+using ::google::crypto::tink::PrfBasedDeriverKeyFormat;
+using ::testing::Eq;
+using ::testing::SizeIs;
+
+TEST(PrfBasedDeriverKeyManagerTest, Basics) {
+  EXPECT_THAT(PrfBasedDeriverKeyManager().get_version(), Eq(0));
+  EXPECT_THAT(PrfBasedDeriverKeyManager().get_key_type(),
+              Eq("type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"));
+  EXPECT_THAT(PrfBasedDeriverKeyManager().key_material_type(),
+              Eq(KeyData::SYMMETRIC));
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, ValidateKeyEmpty) {
+  EXPECT_THAT(PrfBasedDeriverKeyManager().ValidateKey(PrfBasedDeriverKey()),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, ValidateKey) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.set_key_value("0123456789abcdef");
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+
+  PrfBasedDeriverKey key;
+  key.set_version(0);
+  *key.mutable_prf_key() = test::AsKeyData(prf_key, KeyData::SYMMETRIC);
+  *key.mutable_params()->mutable_derived_key_template() =
+      AeadKeyTemplates::Aes256Gcm();
+
+  EXPECT_THAT(PrfBasedDeriverKeyManager().ValidateKey(key), IsOk());
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, ValidateKeyWithWrongVersion) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.set_key_value("0123456789abcdef");
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+
+  PrfBasedDeriverKey key;
+  key.set_version(1);
+  *key.mutable_prf_key() = test::AsKeyData(prf_key, KeyData::SYMMETRIC);
+  *key.mutable_params()->mutable_derived_key_template() =
+      AeadKeyTemplates::Aes256Gcm();
+
+  EXPECT_THAT(PrfBasedDeriverKeyManager().ValidateKey(key),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, ValidateKeyFormat) {
+  HkdfPrfKeyFormat prf_key_format;
+  prf_key_format.set_key_size(16);
+  prf_key_format.mutable_params()->set_hash(HashType::SHA256);
+
+  PrfBasedDeriverKeyFormat key_format;
+  key_format.mutable_prf_key_template()->set_type_url(
+      HkdfPrfKeyManager().get_key_type());
+  key_format.mutable_prf_key_template()->set_value(
+      prf_key_format.SerializeAsString());
+  *key_format.mutable_params()->mutable_derived_key_template() =
+      AeadKeyTemplates::Aes256Gcm();
+
+  EXPECT_THAT(PrfBasedDeriverKeyManager().ValidateKeyFormat(key_format),
+              IsOk());
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, ValidateKeyFormatEmpty) {
+  EXPECT_THAT(
+      PrfBasedDeriverKeyManager().ValidateKeyFormat(PrfBasedDeriverKeyFormat()),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, CreateKey) {
+  Registry::Reset();
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), true),
+              IsOk());
+
+  HkdfPrfKeyFormat prf_key_format;
+  prf_key_format.set_key_size(32);
+  prf_key_format.mutable_params()->set_hash(HashType::SHA256);
+
+  PrfBasedDeriverKeyFormat key_format;
+  key_format.mutable_prf_key_template()->set_type_url(
+      HkdfPrfKeyManager().get_key_type());
+  key_format.mutable_prf_key_template()->set_value(
+      prf_key_format.SerializeAsString());
+  *key_format.mutable_params()->mutable_derived_key_template() =
+      AeadKeyTemplates::Aes256Gcm();
+
+  util::StatusOr<PrfBasedDeriverKey> key =
+      PrfBasedDeriverKeyManager().CreateKey(key_format);
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key).version(), Eq(0));
+  EXPECT_THAT((*key).prf_key().type_url(),
+              Eq(HkdfPrfKeyManager().get_key_type()));
+  EXPECT_THAT((*key).prf_key().key_material_type(), Eq(KeyData::SYMMETRIC));
+
+  HkdfPrfKey prf_key;
+  ASSERT_TRUE(prf_key.ParseFromString((*key).prf_key().value()));
+  EXPECT_THAT(prf_key.key_value().size(), Eq(32));
+
+  EXPECT_THAT((*key).params().derived_key_template().type_url(),
+              Eq(key_format.params().derived_key_template().type_url()));
+  EXPECT_THAT((*key).params().derived_key_template().value(),
+              Eq(key_format.params().derived_key_template().value()));
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, CreateKeyWithInvalidPrfKey) {
+  Registry::Reset();
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), true),
+              IsOk());
+
+  HkdfPrfKeyFormat prf_key_format;
+  prf_key_format.set_key_size(32);
+  prf_key_format.mutable_params()->set_hash(HashType::UNKNOWN_HASH);
+
+  PrfBasedDeriverKeyFormat key_format;
+  key_format.mutable_prf_key_template()->set_type_url(
+      HkdfPrfKeyManager().get_key_type());
+  key_format.mutable_prf_key_template()->set_value(
+      prf_key_format.SerializeAsString());
+  *key_format.mutable_params()->mutable_derived_key_template() =
+      AeadKeyTemplates::Aes256Gcm();
+
+  EXPECT_THAT(PrfBasedDeriverKeyManager().CreateKey(key_format).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, CreateKeyWithInvalidDerivedKeyTemplate) {
+  Registry::Reset();
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), true),
+              IsOk());
+
+  HkdfPrfKeyFormat prf_key_format;
+  prf_key_format.set_key_size(32);
+  prf_key_format.mutable_params()->set_hash(HashType::SHA256);
+  KeyTemplate derived_template;
+  derived_template.set_type_url("nonexistent.type.url");
+
+  PrfBasedDeriverKeyFormat key_format;
+  key_format.mutable_prf_key_template()->set_type_url(
+      HkdfPrfKeyManager().get_key_type());
+  key_format.mutable_prf_key_template()->set_value(
+      prf_key_format.SerializeAsString());
+  *key_format.mutable_params()->mutable_derived_key_template() =
+      derived_template;
+
+  // See comment in PrfBasedDeriverKeyManager::CreateKey().
+  EXPECT_THAT(PrfBasedDeriverKeyManager().CreateKey(key_format).status(),
+              IsOk());
+}
+
+TEST(PrfBasedDeriverKeyManagerTest, GetPrimitive) {
+  Registry::Reset();
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<PrfBasedDeriverKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<HkdfPrfKeyManager>(), true),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), true),
+              IsOk());
+
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+  prf_key.mutable_params()->set_salt(subtle::Random::GetRandomBytes(15));
+  prf_key.set_key_value(subtle::Random::GetRandomBytes(33));
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+  PrfBasedDeriverKey key;
+  key.set_version(0);
+  *key.mutable_prf_key() = test::AsKeyData(prf_key, KeyData::SYMMETRIC);
+  *key.mutable_params()->mutable_derived_key_template() =
+      AeadKeyTemplates::Aes256Gcm();
+
+  StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      PrfBasedDeriverKeyManager().GetPrimitive<KeysetDeriver>(key);
+  ASSERT_THAT(deriver, IsOk());
+
+  std::string salt = subtle::Random::GetRandomBytes(23);
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      (*deriver)->DeriveKeyset(salt);
+  ASSERT_THAT(handle, IsOk());
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+
+  StatusOr<std::unique_ptr<KeysetDeriver>> direct_deriver =
+      internal::PrfBasedDeriver::New(key.prf_key(),
+                                     key.params().derived_key_template());
+  ASSERT_THAT(direct_deriver, IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> direct_handle =
+      (*direct_deriver)->DeriveKeyset(salt);
+  ASSERT_THAT(direct_handle, IsOk());
+  Keyset direct_keyset = CleartextKeysetHandle::GetKeyset(**direct_handle);
+
+  ASSERT_THAT(keyset.key(), SizeIs(1));
+  ASSERT_THAT(direct_keyset.key(), SizeIs(1));
+
+  ASSERT_THAT(keyset.key(0).key_data().type_url(),
+              Eq(keyset.key(0).key_data().type_url()));
+
+  AesGcmKey derived_key;
+  ASSERT_TRUE(derived_key.ParseFromString(keyset.key(0).key_data().value()));
+  AesGcmKey direct_derived_key;
+  ASSERT_TRUE(direct_derived_key.ParseFromString(
+      direct_keyset.key(0).key_data().value()));
+  EXPECT_THAT(derived_key.key_value(), Eq(direct_derived_key.key_value()));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/internal/prf_based_deriver_test.cc b/cc/keyderivation/internal/prf_based_deriver_test.cc
new file mode 100644
index 0000000..c46e572
--- /dev/null
+++ b/cc/keyderivation/internal/prf_based_deriver_test.cc
@@ -0,0 +1,342 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/internal/prf_based_deriver.h"
+
+#include <memory>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/prf/hkdf_prf_key_manager.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/aes_gcm.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HkdfPrfKey;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::Ne;
+using ::testing::SizeIs;
+
+class PrfBasedDeriverTest : public ::testing::Test {
+ public:
+  static void SetUpTestSuite() {
+    ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                   absl::make_unique<AesGcmKeyManager>(), true),
+               IsOk());
+    ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                   absl::make_unique<HkdfPrfKeyManager>(), true),
+               IsOk());
+  }
+};
+
+TEST_F(PrfBasedDeriverTest, New) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+  prf_key.mutable_params()->set_salt("");
+  prf_key.set_key_value("0123456789abcdef0123456789abcdef");
+
+  EXPECT_THAT(PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                                   AeadKeyTemplates::Aes128Gcm()),
+              IsOk());
+}
+
+TEST_F(PrfBasedDeriverTest, NewWithInvalidPrfKey) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::UNKNOWN_HASH);
+  prf_key.mutable_params()->set_salt("");
+  prf_key.set_key_value("0123456789abcdef0123456789abcdef");
+
+  EXPECT_THAT(PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                                   AeadKeyTemplates::Aes128Gcm())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(PrfBasedDeriverTest, NewWithInvalidDerivedKeyTemplate) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+  prf_key.mutable_params()->set_salt("");
+  prf_key.set_key_value("0123456789abcdef0123456789abcdef");
+
+  KeyTemplate derived_template;
+  derived_template.set_type_url("some non-existent type url");
+  EXPECT_THAT(PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                                   derived_template)
+                  .status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+// Test vector from https://tools.ietf.org/html/rfc5869#appendix-A.2.
+TEST_F(PrfBasedDeriverTest, DeriveKeyset) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+  prf_key.mutable_params()->set_salt(
+      test::HexDecodeOrDie("606162636465666768696a6b6c6d6e6f"
+                           "707172737475767778797a7b7c7d7e7f"
+                           "808182838485868788898a8b8c8d8e8f"
+                           "909192939495969798999a9b9c9d9e9f"
+                           "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"));
+  prf_key.set_key_value(
+      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"
+                           "101112131415161718191a1b1c1d1e1f"
+                           "202122232425262728292a2b2c2d2e2f"
+                           "303132333435363738393a3b3c3d3e3f"
+                           "404142434445464748494a4b4c4d4e4f"));
+
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                           AeadKeyTemplates::Aes256Gcm());
+  ASSERT_THAT(deriver, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      (*deriver)->DeriveKeyset(
+          test::HexDecodeOrDie("b0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
+                               "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                               "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
+                               "e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
+                               "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
+  ASSERT_THAT(handle, IsOk());
+
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+  ASSERT_THAT(keyset.key(), SizeIs(1));
+  EXPECT_THAT(keyset.key(0).key_data().type_url(),
+              Eq(AesGcmKeyManager().get_key_type()));
+  EXPECT_THAT(keyset.key(0).key_data().key_material_type(),
+              Eq(AesGcmKeyManager().key_material_type()));
+
+  AesGcmKey derived_key;
+  ASSERT_TRUE(derived_key.ParseFromString(keyset.key(0).key_data().value()));
+  EXPECT_THAT(derived_key.version(), Eq(AesGcmKeyManager().get_version()));
+  // The derived key value is the first 32 bytes of the test vector's OKM field.
+  EXPECT_THAT(test::HexEncode(derived_key.key_value()),
+              Eq("b11e398dc80327a1c8e7f78c596a4934"
+                 "4f012eda2d4efad8a050cc4c19afa97c"));
+}
+
+TEST_F(PrfBasedDeriverTest, DeriveKeysetHoldsPlaceholderValues) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA256);
+  prf_key.mutable_params()->set_salt("");
+  prf_key.set_key_value("0123456789abcdef0123456789abcdef");
+
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                           AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(deriver, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      (*deriver)->DeriveKeyset("salt");
+  ASSERT_THAT(handle, IsOk());
+
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+  EXPECT_THAT(keyset.primary_key_id(), Eq(0));
+  ASSERT_THAT(keyset.key(), SizeIs(1));
+  EXPECT_THAT(keyset.key(0).status(), Eq(KeyStatusType::UNKNOWN_STATUS));
+  EXPECT_THAT(keyset.key(0).key_id(), Eq(0));
+  EXPECT_THAT(keyset.key(0).output_prefix_type(),
+              Eq(OutputPrefixType::UNKNOWN_PREFIX));
+}
+
+TEST_F(PrfBasedDeriverTest, DeriveKeysetWithDifferentPrfKeys) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA512);
+  prf_key.mutable_params()->set_salt("");
+  prf_key.set_key_value(subtle::Random::GetRandomBytes(32));
+
+  AesGcmKey derived_key_0;
+  AesGcmKey derived_key_1;
+  {
+    util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+        PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                             AeadKeyTemplates::Aes128Gcm());
+    ASSERT_THAT(deriver, IsOk());
+
+    util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+        (*deriver)->DeriveKeyset("salt");
+    ASSERT_THAT(handle, IsOk());
+
+    Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+    ASSERT_TRUE(
+        derived_key_0.ParseFromString(keyset.key(0).key_data().value()));
+  }
+  {
+    prf_key.set_key_value(prf_key.key_value() + '\0');
+    util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+        PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                             AeadKeyTemplates::Aes128Gcm());
+    ASSERT_THAT(deriver, IsOk());
+
+    util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+        (*deriver)->DeriveKeyset("salt");
+    ASSERT_THAT(handle, IsOk());
+
+    Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+    ASSERT_TRUE(
+        derived_key_1.ParseFromString(keyset.key(0).key_data().value()));
+  }
+  EXPECT_THAT(derived_key_0.key_value(), SizeIs(16));
+  EXPECT_THAT(derived_key_1.key_value(), SizeIs(16));
+  EXPECT_THAT(derived_key_0.key_value(), Ne(derived_key_1.key_value()));
+}
+
+TEST_F(PrfBasedDeriverTest, DeriveKeysetWithDifferentSalts) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA512);
+  prf_key.set_key_value(subtle::Random::GetRandomBytes(32));
+
+  AesGcmKey derived_key_0;
+  AesGcmKey derived_key_1;
+  {
+    util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+        PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                             AeadKeyTemplates::Aes128Gcm());
+    ASSERT_THAT(deriver, IsOk());
+
+    util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+        (*deriver)->DeriveKeyset(std::string(10, '\0'));
+    ASSERT_THAT(handle, IsOk());
+
+    Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+    ASSERT_TRUE(
+        derived_key_0.ParseFromString(keyset.key(0).key_data().value()));
+  }
+  {
+    util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+        PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                             AeadKeyTemplates::Aes128Gcm());
+    ASSERT_THAT(deriver, IsOk());
+
+    util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+        (*deriver)->DeriveKeyset(std::string(11, '\0'));
+    ASSERT_THAT(handle, IsOk());
+
+    Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+    ASSERT_TRUE(
+        derived_key_1.ParseFromString(keyset.key(0).key_data().value()));
+  }
+  EXPECT_THAT(derived_key_0.key_value(), SizeIs(16));
+  EXPECT_THAT(derived_key_1.key_value(), SizeIs(16));
+  EXPECT_THAT(derived_key_0.key_value(), Ne(derived_key_1.key_value()));
+}
+
+// Test vector generated with Java implementation.
+TEST_F(PrfBasedDeriverTest, DeriveKeysetWithTestVector0) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA512);
+  prf_key.set_key_value(test::HexDecodeOrDie(
+      "a1a2a3a4a5a6a7a8a9aaabacadaeafb1b2b3b4b5b6b7b8b9babbbcbdbebf"
+      "c1c2c3c4c5c6c7c8c9cacbcccdcecfc1c2c3c4c5c6c7c8c9cacbcccdcecf"
+      "00"));
+
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                           AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(deriver, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      (*deriver)->DeriveKeyset(test::HexDecodeOrDie("1122334455"));
+  ASSERT_THAT(handle, IsOk());
+
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+  AesGcmKey derived_key;
+  ASSERT_TRUE(derived_key.ParseFromString(keyset.key(0).key_data().value()));
+  EXPECT_THAT(derived_key.version(), Eq(AesGcmKeyManager().get_version()));
+  EXPECT_THAT(test::HexEncode(derived_key.key_value()),
+              Eq("31c449af66b669b9963ef2df30dfe5f9"));
+}
+
+// Test vector generated with Java implementation.
+TEST_F(PrfBasedDeriverTest, DeriveKeysetWithTestVector1) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA512);
+  prf_key.set_key_value(test::HexDecodeOrDie(
+      "c1c2c3c4c5c6c7c8c9cacbcccdcecfc1c2c3c4c5c6c7c8c9cacbcccdcecf"
+      "a1a2a3a4a5a6a7a8a9aaabacadaeafb1b2b3b4b5b6b7b8b9babbbcbdbebf"));
+
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                           AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(deriver, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      (*deriver)->DeriveKeyset(test::HexDecodeOrDie("00"));
+  ASSERT_THAT(handle, IsOk());
+
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+  AesGcmKey derived_key;
+  ASSERT_TRUE(derived_key.ParseFromString(keyset.key(0).key_data().value()));
+  EXPECT_THAT(derived_key.version(), Eq(AesGcmKeyManager().get_version()));
+  EXPECT_THAT(test::HexEncode(derived_key.key_value()),
+              Eq("887af0808c1855eba1594bf540adb957"));
+}
+
+// Test vector generated with Java implementation.
+TEST_F(PrfBasedDeriverTest, DeriveKeysetWithEmptySaltTestVector) {
+  HkdfPrfKey prf_key;
+  prf_key.set_version(0);
+  prf_key.mutable_params()->set_hash(HashType::SHA512);
+  prf_key.set_key_value(test::HexDecodeOrDie(
+      "c1c2c3c4c5c6c7c8c9cacbcccdcecfc1c2c3c4c5c6c7c8c9cacbcccdcecf"
+      "a1a2a3a4a5a6a7a8a9aaabacadaeafb1b2b3b4b5b6b7b8b9babbbcbdbebf"));
+
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      PrfBasedDeriver::New(test::AsKeyData(prf_key, KeyData::SYMMETRIC),
+                           AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(deriver, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      (*deriver)->DeriveKeyset("");
+  ASSERT_THAT(handle, IsOk());
+
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**handle);
+  AesGcmKey derived_key;
+  ASSERT_TRUE(derived_key.ParseFromString(keyset.key(0).key_data().value()));
+  EXPECT_THAT(derived_key.version(), Eq(AesGcmKeyManager().get_version()));
+  EXPECT_THAT(test::HexEncode(derived_key.key_value()),
+              Eq("fb2b448c2595caf75129e282af758bf1"));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/key_derivation_config.cc b/cc/keyderivation/key_derivation_config.cc
new file mode 100644
index 0000000..2194e33
--- /dev/null
+++ b/cc/keyderivation/key_derivation_config.cc
@@ -0,0 +1,55 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/key_derivation_config.h"
+
+#include "tink/config/tink_fips.h"
+#include "tink/keyderivation/internal/prf_based_deriver_key_manager.h"
+#include "tink/keyderivation/keyset_deriver_wrapper.h"
+#include "tink/prf/hkdf_prf_key_manager.h"
+
+namespace crypto {
+namespace tink {
+
+// static
+util::Status KeyDerivationConfig::Register() {
+  // Register primitive wrappers.
+  util::Status status = Registry::RegisterPrimitiveWrapper(
+      absl::make_unique<KeysetDeriverWrapper>());
+  if (!status.ok()) {
+    return status;
+  }
+
+  // Currently, no KeysetDeriver key managers only use FIPS-validated
+  // implementations, so none are registered in FIPS-only mode.
+  if (IsFipsModeEnabled()) {
+    return util::OkStatus();
+  }
+
+  // Register required key manager for PrfBasedDeriverKeyManager.
+  status = Registry::RegisterKeyTypeManager(
+      absl::make_unique<HkdfPrfKeyManager>(), true);
+  if (!status.ok()) {
+    return status;
+  }
+
+  // Register key managers.
+  return Registry::RegisterKeyTypeManager(
+      absl::make_unique<internal::PrfBasedDeriverKeyManager>(), true);
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/key_derivation_config.h b/cc/keyderivation/key_derivation_config.h
new file mode 100644
index 0000000..5575024
--- /dev/null
+++ b/cc/keyderivation/key_derivation_config.h
@@ -0,0 +1,46 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYDERIVATION_KEY_DERIVATION_CONFIG_H_
+#define TINK_KEYDERIVATION_KEY_DERIVATION_CONFIG_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+///////////////////////////////////////////////////////////////////////////////
+// Static methods and constants for registering to the Registry all
+// KeysetDeriver key types supported in a particular release of Tink.
+//
+// To register all KeysetDeriver key types, one can do:
+//
+//   crypto::tink::util::Status status = KeyDerivationConfig::Register();
+//
+class KeyDerivationConfig {
+ public:
+  // Registers KeysetDeriver primitive wrapper and key managers for all
+  // KeyDerivation key types from the current Tink release.
+  static crypto::tink::util::Status Register();
+
+ private:
+  KeyDerivationConfig() {}
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYDERIVATION_KEY_DERIVATION_CONFIG_H_
diff --git a/cc/keyderivation/key_derivation_config_test.cc b/cc/keyderivation/key_derivation_config_test.cc
new file mode 100644
index 0000000..5540d26
--- /dev/null
+++ b/cc/keyderivation/key_derivation_config_test.cc
@@ -0,0 +1,83 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/key_derivation_config.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aead_config.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/keyderivation/key_derivation_key_templates.h"
+#include "tink/keyderivation/keyset_deriver.h"
+#include "tink/prf/prf_key_templates.h"
+#include "tink/registry.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::testing::Not;
+
+TEST(KeyDerivationConfigTest, Register) {
+  Registry::Reset();
+
+  EXPECT_THAT(KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+                  PrfKeyTemplates::HkdfSha256(), AeadKeyTemplates::Aes256Gcm()),
+              Not(IsOk()));
+
+  ASSERT_THAT(KeyDerivationConfig::Register(), IsOk());
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<AesGcmKeyManager>(), true),
+              IsOk());
+
+  util::StatusOr<::google::crypto::tink::KeyTemplate> templ =
+      KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+          PrfKeyTemplates::HkdfSha256(), AeadKeyTemplates::Aes256Gcm());
+  ASSERT_THAT(templ, IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(*templ);
+  ASSERT_THAT(handle, IsOk());
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      (*handle)->GetPrimitive<KeysetDeriver>();
+  ASSERT_THAT(deriver, IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> derived_handle =
+      (*deriver)->DeriveKeyset("salty");
+  ASSERT_THAT(derived_handle, IsOk());
+
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      (*derived_handle)->GetPrimitive<Aead>();
+  ASSERT_THAT(aead, IsOk());
+  std::string plaintext = "plaintext";
+  std::string ad = "ad";
+  util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(plaintext, ad);
+  ASSERT_THAT(ciphertext, IsOk());
+  util::StatusOr<std::string> got = (*aead)->Decrypt(*ciphertext, ad);
+  ASSERT_THAT(got, IsOk());
+  EXPECT_EQ(plaintext, *got);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/key_derivation_key_templates.cc b/cc/keyderivation/key_derivation_key_templates.cc
new file mode 100644
index 0000000..8b0e66f
--- /dev/null
+++ b/cc/keyderivation/key_derivation_key_templates.cc
@@ -0,0 +1,62 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/key_derivation_key_templates.h"
+
+#include <memory>
+
+#include "tink/keyderivation/internal/prf_based_deriver_key_manager.h"
+#include "tink/subtle/random.h"
+
+namespace crypto {
+namespace tink {
+
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::PrfBasedDeriverKeyFormat;
+
+util::StatusOr<KeyTemplate>
+KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+    const KeyTemplate& prf_key_template,
+    const KeyTemplate& derived_key_template) {
+  KeyTemplate key_template;
+  key_template.set_type_url(
+      internal::PrfBasedDeriverKeyManager().get_key_type());
+  key_template.set_output_prefix_type(
+      derived_key_template.output_prefix_type());
+
+  PrfBasedDeriverKeyFormat format;
+  *format.mutable_prf_key_template() = prf_key_template;
+  *format.mutable_params()->mutable_derived_key_template() =
+      derived_key_template;
+  format.SerializeToString(key_template.mutable_value());
+
+  // Verify `key_template` is derivable.
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(key_template);
+  if (!handle.ok()) {
+    return handle.status();
+  }
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> deriver =
+      (*handle)->GetPrimitive<KeysetDeriver>();
+  if (!deriver.ok()) {
+    return deriver.status();
+  }
+
+  return key_template;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/key_derivation_key_templates.h b/cc/keyderivation/key_derivation_key_templates.h
new file mode 100644
index 0000000..50af285
--- /dev/null
+++ b/cc/keyderivation/key_derivation_key_templates.h
@@ -0,0 +1,52 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYDERIVATION_KEY_DERIVATION_KEY_TEMPLATES_H_
+#define TINK_KEYDERIVATION_KEY_DERIVATION_KEY_TEMPLATES_H_
+
+#include "tink/util/statusor.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+
+///////////////////////////////////////////////////////////////////////////////
+// Methods to generate KeyTemplates for key derivation.
+class KeyDerivationKeyTemplates {
+ public:
+  // Creates a key template for key derivation that uses a PRF to derive a key
+  // that adheres to `derived_key_template`. The following must be true:
+  //   (1) `prf_key_template` is a PRF key template, i.e.
+  //         `keyset_handle->GetPrimitive<StreamingPrf>()` works.
+  //   (2) `derived_key_template` describes a key type that supports derivation.
+  //
+  // The output prefix type of the derived key will match the output prefix type
+  // of `derived_key_template`.
+  //
+  // This function verifies the newly created key template by creating a
+  // KeysetDeriver primitive from it. This requires both the `prf_key_template`
+  // and `derived_key_template` key types to be in the registry. It also
+  // attempts to derive a key, returning an error on failure.
+  static util::StatusOr<google::crypto::tink::KeyTemplate>
+  CreatePrfBasedKeyTemplate(
+      const google::crypto::tink::KeyTemplate& prf_key_template,
+      const google::crypto::tink::KeyTemplate& derived_key_template);
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYDERIVATION_KEY_DERIVATION_KEY_TEMPLATES_H_
diff --git a/cc/keyderivation/key_derivation_key_templates_test.cc b/cc/keyderivation/key_derivation_key_templates_test.cc
new file mode 100644
index 0000000..a6e01b3
--- /dev/null
+++ b/cc/keyderivation/key_derivation_key_templates_test.cc
@@ -0,0 +1,206 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/key_derivation_key_templates.h"
+
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/keyderivation/internal/prf_based_deriver_key_manager.h"
+#include "tink/keyderivation/keyset_deriver_wrapper.h"
+#include "tink/prf/hkdf_prf_key_manager.h"
+#include "tink/prf/prf_key_templates.h"
+#include "tink/registry.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/prf_based_deriver.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+using ::google::crypto::tink::PrfBasedDeriverKeyFormat;
+using ::testing::Eq;
+using ::testing::Not;
+
+class KeyDerivationKeyTemplatesTest : public ::testing::Test {
+ protected:
+  void TearDown() override { Registry::Reset(); }
+};
+
+TEST_F(KeyDerivationKeyTemplatesTest, CreatePrfBasedKeyTemplate) {
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<KeysetDeriverWrapper>()),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<internal::PrfBasedDeriverKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<HkdfPrfKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+
+  std::vector<OutputPrefixType> output_prefix_types = {
+      OutputPrefixType::RAW, OutputPrefixType::TINK, OutputPrefixType::LEGACY};
+  for (OutputPrefixType output_prefix_type : output_prefix_types) {
+    KeyTemplate derived_key_template = AeadKeyTemplates::Aes256Gcm();
+    derived_key_template.set_output_prefix_type(output_prefix_type);
+    util::StatusOr<KeyTemplate> key_template =
+        KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+            PrfKeyTemplates::HkdfSha256(), derived_key_template);
+
+    ASSERT_THAT(key_template, IsOk());
+    EXPECT_THAT(
+        key_template->type_url(),
+        Eq("type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"));
+    EXPECT_THAT(key_template->type_url(),
+                Eq(internal::PrfBasedDeriverKeyManager().get_key_type()));
+    EXPECT_THAT(key_template->output_prefix_type(), Eq(output_prefix_type));
+
+    PrfBasedDeriverKeyFormat key_format;
+    EXPECT_TRUE(key_format.ParseFromString(key_template->value()));
+    EXPECT_THAT(
+        internal::PrfBasedDeriverKeyManager().ValidateKeyFormat(key_format),
+        IsOk());
+  }
+}
+
+TEST_F(KeyDerivationKeyTemplatesTest, CreatePrfBasedKeyTemplateInvalidPrfKey) {
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<KeysetDeriverWrapper>()),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<internal::PrfBasedDeriverKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<HkdfPrfKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+
+  EXPECT_THAT(KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+                  AeadKeyTemplates::Aes256Gcm(), AeadKeyTemplates::Aes256Gcm())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(KeyDerivationKeyTemplatesTest,
+       CreatePrfBasedKeyTemplateInvalidDerivedKeyTemplate) {
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<KeysetDeriverWrapper>()),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<internal::PrfBasedDeriverKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<HkdfPrfKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+
+  util::StatusOr<KeyTemplate> derived_key_template =
+      KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+          PrfKeyTemplates::HkdfSha256(), AeadKeyTemplates::Aes256Gcm());
+  EXPECT_THAT(KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+                  PrfKeyTemplates::HkdfSha256(), *derived_key_template)
+                  .status(),
+              StatusIs(absl::StatusCode::kUnimplemented));
+}
+
+TEST_F(KeyDerivationKeyTemplatesTest,
+       CreatePrfBasedKeyTemplateNoPrfBasedDeriverKeyManager) {
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<KeysetDeriverWrapper>()),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<HkdfPrfKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+
+  EXPECT_THAT(KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+                  PrfKeyTemplates::HkdfSha256(), AeadKeyTemplates::Aes256Gcm()),
+              Not(IsOk()));
+}
+
+TEST_F(KeyDerivationKeyTemplatesTest,
+       CreatePrfBasedKeyTemplateNoHkdfPrfKeyManager) {
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<KeysetDeriverWrapper>()),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<internal::PrfBasedDeriverKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<AesGcmKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+
+  EXPECT_THAT(KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+                  PrfKeyTemplates::HkdfSha256(), AeadKeyTemplates::Aes256Gcm()),
+              Not(IsOk()));
+}
+
+TEST_F(KeyDerivationKeyTemplatesTest,
+       CreatePrfBasedKeyTemplateNoAesGcmKeyManager) {
+  ASSERT_THAT(Registry::RegisterPrimitiveWrapper(
+                  absl::make_unique<KeysetDeriverWrapper>()),
+              IsOk());
+  ASSERT_THAT(Registry::RegisterKeyTypeManager(
+                  absl::make_unique<internal::PrfBasedDeriverKeyManager>(),
+                  /*new_key_allowed=*/true),
+              IsOk());
+  ASSERT_THAT(
+      Registry::RegisterKeyTypeManager(absl::make_unique<HkdfPrfKeyManager>(),
+                                       /*new_key_allowed=*/true),
+      IsOk());
+
+  EXPECT_THAT(KeyDerivationKeyTemplates::CreatePrfBasedKeyTemplate(
+                  PrfKeyTemplates::HkdfSha256(), AeadKeyTemplates::Aes256Gcm()),
+              Not(IsOk()));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/keyset_deriver.h b/cc/keyderivation/keyset_deriver.h
new file mode 100644
index 0000000..f6f11bd
--- /dev/null
+++ b/cc/keyderivation/keyset_deriver.h
@@ -0,0 +1,46 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYDERIVATION_KEYSET_DERIVER_H_
+#define TINK_KEYDERIVATION_KEYSET_DERIVER_H_
+
+#include <memory>
+
+#include "absl/strings/string_view.h"
+#include "tink/keyset_handle.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// KeysetDeriver is the interface used to derive new keysets based on an
+// additional input, the salt.
+//
+// The salt is used to create the keyset using a pseudorandom function.
+// Implementations must be indistinguishable from ideal KeysetDerivers, which,
+// for every salt, generates a new random keyset and caches it.
+class KeysetDeriver {
+ public:
+  virtual crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+  DeriveKeyset(absl::string_view salt) const = 0;
+
+  virtual ~KeysetDeriver() = default;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYDERIVATION_KEYSET_DERIVER_H_
diff --git a/cc/keyderivation/keyset_deriver_wrapper.cc b/cc/keyderivation/keyset_deriver_wrapper.cc
new file mode 100644
index 0000000..cf74a11
--- /dev/null
+++ b/cc/keyderivation/keyset_deriver_wrapper.cc
@@ -0,0 +1,104 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/keyset_deriver_wrapper.h"
+
+#include <memory>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+
+namespace {
+
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+
+util::Status Validate(PrimitiveSet<KeysetDeriver>* deriver_set) {
+  if (deriver_set == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "deriver_set must be non-NULL");
+  }
+  if (deriver_set->get_primary() == nullptr) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "deriver_set has no primary");
+  }
+  return util::OkStatus();
+}
+
+class KeysetDeriverSetWrapper : public KeysetDeriver {
+ public:
+  explicit KeysetDeriverSetWrapper(
+      std::unique_ptr<PrimitiveSet<KeysetDeriver>> deriver_set)
+      : deriver_set_(std::move(deriver_set)) {}
+
+  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> DeriveKeyset(
+      absl::string_view salt) const override;
+
+  ~KeysetDeriverSetWrapper() override = default;
+
+ private:
+  std::unique_ptr<PrimitiveSet<KeysetDeriver>> deriver_set_;
+};
+
+crypto::tink::util::StatusOr<KeyData> DeriveAndGetKeyData(
+    absl::string_view salt, const KeysetDeriver& deriver) {
+  auto keyset_handle_or = deriver.DeriveKeyset(salt);
+  if (!keyset_handle_or.ok()) return keyset_handle_or.status();
+  const Keyset& keyset =
+      CleartextKeysetHandle::GetKeyset(*keyset_handle_or.value());
+  if (keyset.key_size() != 1) {
+    return util::Status(
+        absl::StatusCode::kInternal,
+        "Wrapper Deriver must create a keyset with exactly one KeyData");
+  }
+  return keyset.key(0).key_data();
+}
+
+crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+KeysetDeriverSetWrapper::DeriveKeyset(absl::string_view salt) const {
+  Keyset keyset;
+  for (const auto* entry : deriver_set_->get_all_in_keyset_order()) {
+    Keyset::Key* key = keyset.add_key();
+
+    crypto::tink::util::StatusOr<KeyData> key_data_or =
+        DeriveAndGetKeyData(salt, entry->get_primitive());
+    if (!key_data_or.ok()) return key_data_or.status();
+    *key->mutable_key_data() = key_data_or.value();
+    key->set_status(entry->get_status());
+    key->set_output_prefix_type(entry->get_output_prefix_type());
+    key->set_key_id(entry->get_key_id());
+  }
+  keyset.set_primary_key_id(deriver_set_->get_primary()->get_key_id());
+  return CleartextKeysetHandle::GetKeysetHandle(keyset);
+}
+
+}  // namespace
+
+crypto::tink::util::StatusOr<std::unique_ptr<KeysetDeriver>>
+KeysetDeriverWrapper::Wrap(
+    std::unique_ptr<PrimitiveSet<KeysetDeriver>> deriver_set) const {
+  util::Status status = Validate(deriver_set.get());
+  if (!status.ok()) return status;
+  return {absl::make_unique<KeysetDeriverSetWrapper>(std::move(deriver_set))};
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/keyset_deriver_wrapper.h b/cc/keyderivation/keyset_deriver_wrapper.h
new file mode 100644
index 0000000..256440a
--- /dev/null
+++ b/cc/keyderivation/keyset_deriver_wrapper.h
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYDERIVATION_KEYSET_DERIVER_WRAPPER_H_
+#define TINK_KEYDERIVATION_KEYSET_DERIVER_WRAPPER_H_
+
+#include <memory>
+
+#include "tink/keyderivation/keyset_deriver.h"
+#include "tink/primitive_set.h"
+#include "tink/primitive_wrapper.h"
+
+namespace crypto {
+namespace tink {
+
+// A KeysetDeriverWrapper wraps the KeysetDeriver primitive.
+//
+// The wrapper derives a key from each key in a keyset. It returns the resulting
+// keys in a new keyset. Each of the derived keys inherits key_id, status, and
+// output_prefix_type from the key from which it was derived.
+class KeysetDeriverWrapper
+    : public PrimitiveWrapper<KeysetDeriver, KeysetDeriver> {
+ public:
+  crypto::tink::util::StatusOr<std::unique_ptr<KeysetDeriver>> Wrap(
+      std::unique_ptr<PrimitiveSet<KeysetDeriver>> deriver_set) const override;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYDERIVATION_KEYSET_DERIVER_WRAPPER_H_
diff --git a/cc/keyderivation/keyset_deriver_wrapper_test.cc b/cc/keyderivation/keyset_deriver_wrapper_test.cc
new file mode 100644
index 0000000..dde29fb
--- /dev/null
+++ b/cc/keyderivation/keyset_deriver_wrapper_test.cc
@@ -0,0 +1,197 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/keyderivation/keyset_deriver_wrapper.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/keyderivation/keyset_deriver.h"
+#include "tink/primitive_set.h"
+#include "tink/util/test_matchers.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeysetInfo;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+
+// TODO(b/255828521): Move this to a shared location once KeysetDeriver is in
+// the public API.
+class DummyDeriver : public KeysetDeriver {
+ public:
+  explicit DummyDeriver(absl::string_view name) : name_(name) {}
+  util::StatusOr<std::unique_ptr<KeysetHandle>> DeriveKeyset(
+      absl::string_view salt) const override {
+    Keyset::Key key;
+    key.mutable_key_data()->set_type_url(
+        absl::StrCat(name_.size(), ":", name_, salt));
+    key.set_status(KeyStatusType::UNKNOWN_STATUS);
+    key.set_key_id(0);
+    key.set_output_prefix_type(OutputPrefixType::UNKNOWN_PREFIX);
+
+    Keyset keyset;
+    *keyset.add_key() = key;
+    keyset.set_primary_key_id(0);
+    return CleartextKeysetHandle::GetKeysetHandle(keyset);
+  }
+
+ private:
+  std::string name_;
+};
+
+TEST(KeysetDeriverWrapperTest, WrapNullptr) {
+  EXPECT_THAT(KeysetDeriverWrapper().Wrap(nullptr).status(),
+              StatusIs(absl::StatusCode::kInternal, HasSubstr("non-NULL")));
+}
+
+TEST(KeysetDeriverWrapperTest, WrapEmpty) {
+  EXPECT_THAT(
+      KeysetDeriverWrapper()
+          .Wrap(absl::make_unique<PrimitiveSet<KeysetDeriver>>())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("no primary")));
+}
+
+TEST(KeysetDeriverWrapperTest, WrapNoPrimary) {
+  auto deriver_set = absl::make_unique<PrimitiveSet<KeysetDeriver>>();
+  KeysetInfo::KeyInfo key_info;
+  key_info.set_key_id(1234);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::TINK);
+
+  EXPECT_THAT(
+      deriver_set->AddPrimitive(absl::make_unique<DummyDeriver>(""), key_info)
+          .status(),
+      IsOk());
+
+  EXPECT_THAT(
+      KeysetDeriverWrapper().Wrap(std::move(deriver_set)).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("no primary")));
+}
+
+TEST(KeysetDeriverWrapperTest, WrapSingle) {
+  auto deriver_set = absl::make_unique<PrimitiveSet<KeysetDeriver>>();
+  KeysetInfo::KeyInfo key_info;
+  key_info.set_key_id(1234);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::TINK);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+
+  auto entry_or = deriver_set->AddPrimitive(
+      absl::make_unique<DummyDeriver>("wrap_single_key"), key_info);
+  ASSERT_THAT(entry_or, IsOk());
+  EXPECT_THAT(deriver_set->set_primary(entry_or.value()), IsOk());
+
+  auto wrapper_deriver_or = KeysetDeriverWrapper().Wrap(std::move(deriver_set));
+
+  ASSERT_THAT(wrapper_deriver_or, IsOk());
+
+  auto derived_keyset_or =
+      wrapper_deriver_or.value()->DeriveKeyset("wrap_single_salt");
+
+  ASSERT_THAT(derived_keyset_or, IsOk());
+
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(*derived_keyset_or.value());
+
+  EXPECT_THAT(keyset.primary_key_id(), Eq(1234));
+  ASSERT_THAT(keyset.key_size(), Eq(1));
+  EXPECT_THAT(keyset.key(0).key_data().type_url(),
+              Eq("15:wrap_single_keywrap_single_salt"));
+  EXPECT_THAT(keyset.key(0).status(), Eq(KeyStatusType::ENABLED));
+  EXPECT_THAT(keyset.key(0).key_id(), Eq(1234));
+  EXPECT_THAT(keyset.key(0).output_prefix_type(), Eq(OutputPrefixType::TINK));
+}
+
+TEST(KeysetDeriverWrapperTest, WrapMultiple) {
+  auto pset = absl::make_unique<PrimitiveSet<KeysetDeriver>>();
+  std::vector<KeysetInfo::KeyInfo> key_infos;
+
+  KeysetInfo::KeyInfo key_info;
+  key_info.set_key_id(1010101);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::RAW);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+  ASSERT_THAT(
+      pset->AddPrimitive(absl::make_unique<DummyDeriver>("k1"), key_info)
+          .status(),
+      IsOk());
+  key_infos.push_back(key_info);
+
+  key_info.set_key_id(2020202);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::LEGACY);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+  util::StatusOr<PrimitiveSet<KeysetDeriver>::Entry<KeysetDeriver>*> entry =
+      pset->AddPrimitive(absl::make_unique<DummyDeriver>("k2"), key_info);
+  ASSERT_THAT(entry, IsOk());
+  ASSERT_THAT(pset->set_primary(*entry), IsOk());
+  key_infos.push_back(key_info);
+
+  key_info.set_key_id(3030303);
+  key_info.set_status(KeyStatusType::ENABLED);
+  key_info.set_output_prefix_type(OutputPrefixType::TINK);
+  key_info.set_type_url(
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+  ASSERT_THAT(
+      pset->AddPrimitive(absl::make_unique<DummyDeriver>("k3"), key_info),
+      IsOk());
+  key_infos.push_back(key_info);
+
+  util::StatusOr<std::unique_ptr<KeysetDeriver>> wrapper_deriver =
+      KeysetDeriverWrapper().Wrap(std::move(pset));
+  ASSERT_THAT(wrapper_deriver, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeysetHandle>> derived_keyset =
+      (*wrapper_deriver)->DeriveKeyset("salt");
+  ASSERT_THAT(derived_keyset, IsOk());
+  Keyset keyset = CleartextKeysetHandle::GetKeyset(**derived_keyset);
+
+  EXPECT_THAT(keyset.primary_key_id(), Eq(2020202));
+  ASSERT_THAT(keyset.key_size(), Eq(3));
+
+  for (int i = 0; i < keyset.key().size(); i++) {
+    std::string type_url = absl::StrCat("2:k", i + 1, "salt");
+    EXPECT_THAT(keyset.key(i).key_data().type_url(), Eq(type_url));
+
+    Keyset::Key key = keyset.key(i);
+    key_info = key_infos[i];
+    EXPECT_THAT(key.status(), Eq(key_info.status()));
+    EXPECT_THAT(key.key_id(), Eq(key_info.key_id()));
+    EXPECT_THAT(key.output_prefix_type(), Eq(key_info.output_prefix_type()));
+  }
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/keyderivation/subtle/BUILD.bazel b/cc/keyderivation/subtle/BUILD.bazel
new file mode 100644
index 0000000..5b01f6e
--- /dev/null
+++ b/cc/keyderivation/subtle/BUILD.bazel
@@ -0,0 +1 @@
+licenses(["notice"])
diff --git a/cc/keyderivation/subtle/CMakeLists.txt b/cc/keyderivation/subtle/CMakeLists.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cc/keyderivation/subtle/CMakeLists.txt
diff --git a/cc/keyset_handle.h b/cc/keyset_handle.h
index 19c25bd..8d594e9 100644
--- a/cc/keyset_handle.h
+++ b/cc/keyset_handle.h
@@ -12,20 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //
-///////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
 
 #ifndef TINK_KEYSET_HANDLE_H_
 #define TINK_KEYSET_HANDLE_H_
 
+#include <cstdint>
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/base/attributes.h"
 #include "absl/container/flat_hash_map.h"
+#include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "tink/aead.h"
+#include "tink/configuration.h"
+#include "tink/internal/configuration_impl.h"
 #include "tink/internal/key_info.h"
+#include "tink/key.h"
+#include "tink/key_gen_configuration.h"
 #include "tink/key_manager.h"
+#include "tink/key_status.h"
 #include "tink/keyset_reader.h"
 #include "tink/keyset_writer.h"
 #include "tink/primitive_set.h"
@@ -40,6 +49,60 @@
 // key material.
 class KeysetHandle {
  public:
+  // Represents a single entry in a `KeysetHandle`. Some current behavior will
+  // be changed in the future.
+  class Entry {
+   public:
+    // May return an internal class in case there is no implementation of the
+    // corresponding key class yet.  Returned value only valid for lifetime
+    // of entry object.
+    std::shared_ptr<const Key> GetKey() const { return key_; }
+
+    // Status indicates whether or not a key should still be used.
+    KeyStatus GetStatus() const { return status_; }
+
+    // ID should be unique (though currently Tink still accepts keysets with
+    // repeated IDs).
+    int GetId() const { return id_; }
+
+    // Should return true for exactly one entry (though currently Tink still
+    // accepts keysets which have no entry marked as primary).
+    bool IsPrimary() const { return is_primary_; }
+
+   private:
+    friend class KeysetHandle;
+    friend class KeysetHandleBuilder;
+
+    Entry(std::shared_ptr<const Key> key, KeyStatus status, int id,
+          bool is_primary)
+        : key_(std::move(key)),
+          status_(status),
+          id_(id),
+          is_primary_(is_primary) {}
+
+    std::shared_ptr<const Key> key_;
+    KeyStatus status_;
+    int id_;
+    bool is_primary_;
+  };
+
+  // Returns the number of entries in this keyset.
+  int size() const { return keyset_.key_size(); }
+  // Validates single `KeysetHandle::Entry` at `index` by making sure that the
+  // key entry's type URL is printable and that it has a valid key status.
+  crypto::tink::util::Status ValidateAt(int index) const;
+  // Validates each individual `KeysetHandle::Entry` in keyset handle by calling
+  // `ValidateAt()`.  Also, checks that there is a single enabled primary key.
+  crypto::tink::util::Status Validate() const;
+  // Returns entry for primary key in this keyset. Crashes if `Validate()`
+  // does not return an OK status.  Call `Validate()` prior to calling this
+  // method to avoid potentially crashing your program.
+  Entry GetPrimary() const;
+  // Returns the `KeysetHandle::Entry` at `index`.  Crashes if
+  // `ValidateAt(index)` does not return an OK status.  Call `ValidateAt(index)`
+  // prior to calling this method to avoid potentially crashing your program.
+  Entry operator[](int index) const;
+
   // Creates a KeysetHandle from an encrypted keyset obtained via `reader`
   // using `master_key_aead` to decrypt the keyset, with monitoring annotations
   // `monitoring_annotations`; by default, `monitoring_annotations` is empty.
@@ -69,10 +132,19 @@
                const absl::flat_hash_map<std::string, std::string>&
                    monitoring_annotations = {});
 
-  // Returns a KeysetHandle for a new keyset that contains a single fresh key
-  // generated according to `key_template`. The keyset is annotated for
-  // monitoring with `monitoring_annotations`; by default,
-  // `monitoring_annotations` is empty.
+  // Returns a KeysetHandle containing a single new key generated according to
+  // `key_template` and using `config`. The keyset is annotated for monitoring
+  // with `monitoring_annotations`, which is empty by default.
+  static crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+  GenerateNew(const google::crypto::tink::KeyTemplate& key_template,
+              const crypto::tink::KeyGenConfiguration& config,
+              const absl::flat_hash_map<std::string, std::string>&
+                  monitoring_annotations = {});
+
+  // TODO(b/265865177): Deprecate.
+  // Returns a KeysetHandle containing a single new key generated according to
+  // `key_template`. The keyset is annotated for monitoring with
+  // `monitoring_annotations`, which is empty by default.
   static crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
   GenerateNew(const google::crypto::tink::KeyTemplate& key_template,
               const absl::flat_hash_map<std::string, std::string>&
@@ -108,10 +180,14 @@
   crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
   GetPublicKeysetHandle() const;
 
-  // Creates a wrapped primitive corresponding to this keyset or fails with
-  // a non-ok status. Uses the KeyManager and PrimitiveWrapper objects in the
-  // global registry to create the primitive. This function is the most common
-  // way of creating a primitive.
+  // Creates a wrapped primitive using this keyset handle and config, which
+  // stores necessary primitive wrappers and key type managers.
+  template <class P>
+  crypto::tink::util::StatusOr<std::unique_ptr<P>> GetPrimitive(
+      const Configuration& config) const;
+
+  // Creates a wrapped primitive using this keyset handle and the global
+  // registry, which stores necessary primitive wrappers and key type managers.
   template <class P>
   crypto::tink::util::StatusOr<std::unique_ptr<P>> GetPrimitive() const;
 
@@ -128,15 +204,27 @@
   // The classes below need access to get_keyset();
   friend class CleartextKeysetHandle;
   friend class KeysetManager;
-  friend class RegistryImpl;
 
   // TestKeysetHandle::GetKeyset() provides access to get_keyset().
   friend class TestKeysetHandle;
 
+  // KeysetHandleBuilder::Build() needs access to KeysetHandle(Keyset).
+  friend class KeysetHandleBuilder;
+
   // Creates a handle that contains the given keyset.
-  explicit KeysetHandle(google::crypto::tink::Keyset keyset);
-  // Creates a handle that contains the given keyset.
-  explicit KeysetHandle(std::unique_ptr<google::crypto::tink::Keyset> keyset);
+  explicit KeysetHandle(google::crypto::tink::Keyset keyset)
+      : keyset_(std::move(keyset)) {}
+  explicit KeysetHandle(std::unique_ptr<google::crypto::tink::Keyset> keyset)
+      : keyset_(std::move(*keyset)) {}
+  // Creates a handle that contains the given `keyset` and `entries`.
+  explicit KeysetHandle(
+      google::crypto::tink::Keyset keyset,
+      const std::vector<std::shared_ptr<const Entry>>& entries)
+      : keyset_(std::move(keyset)), entries_(entries) {}
+  explicit KeysetHandle(
+      std::unique_ptr<google::crypto::tink::Keyset> keyset,
+      const std::vector<std::shared_ptr<const Entry>>& entries)
+      : keyset_(std::move(*keyset)), entries_(entries) {}
   // Creates a handle that contains the given `keyset` and
   // `monitoring_annotations`.
   KeysetHandle(google::crypto::tink::Keyset keyset,
@@ -149,16 +237,45 @@
                    monitoring_annotations)
       : keyset_(std::move(*keyset)),
         monitoring_annotations_(monitoring_annotations) {}
+  // Creates a handle that contains the given `keyset`, `entries`, and
+  // `monitoring_annotations`.
+  KeysetHandle(google::crypto::tink::Keyset keyset,
+               const std::vector<std::shared_ptr<const Entry>>& entries,
+               const absl::flat_hash_map<std::string, std::string>&
+                   monitoring_annotations)
+      : keyset_(std::move(keyset)),
+        entries_(entries),
+        monitoring_annotations_(monitoring_annotations) {}
+  KeysetHandle(std::unique_ptr<google::crypto::tink::Keyset> keyset,
+               const std::vector<std::shared_ptr<const Entry>>& entries,
+               const absl::flat_hash_map<std::string, std::string>&
+                   monitoring_annotations)
+      : keyset_(std::move(*keyset)),
+        entries_(entries),
+        monitoring_annotations_(monitoring_annotations) {}
 
-  // Helper function which generates a key from a template, then adds it
-  // to the keyset. TODO(tholenst): Change this to a proper member operating
-  // on the internal keyset.
+  // Generates a key from `key_template` and adds it `keyset`.
   static crypto::tink::util::StatusOr<uint32_t> AddToKeyset(
       const google::crypto::tink::KeyTemplate& key_template, bool as_primary,
+      const crypto::tink::KeyGenConfiguration& config,
       google::crypto::tink::Keyset* keyset);
 
+  // Creates list of KeysetHandle::Entry entries derived from `keyset` in order.
+  static crypto::tink::util::StatusOr<std::vector<std::shared_ptr<const Entry>>>
+  GetEntriesFromKeyset(const google::crypto::tink::Keyset& keyset);
+
+  // Creates KeysetHandle::Entry for `key`, which will be set to primary if
+  // its key id equals `primary_key_id`.
+  static util::StatusOr<Entry> CreateEntry(
+      const google::crypto::tink::Keyset::Key& key, uint32_t primary_key_id);
+
+  // Generates a key from `key_template` and adds it to the keyset handle.
+  crypto::tink::util::StatusOr<uint32_t> AddKey(
+      const google::crypto::tink::KeyTemplate& key_template, bool as_primary,
+      const crypto::tink::KeyGenConfiguration& config);
+
   // Returns keyset held by this handle.
-  const google::crypto::tink::Keyset& get_keyset() const;
+  const google::crypto::tink::Keyset& get_keyset() const { return keyset_; }
 
   // Creates a set of primitives corresponding to the keys with
   // (status == ENABLED) in the keyset given in 'keyset_handle',
@@ -171,7 +288,17 @@
   crypto::tink::util::StatusOr<std::unique_ptr<PrimitiveSet<P>>> GetPrimitives(
       const KeyManager<P>* custom_manager) const;
 
+  // Creates KeysetHandle::Entry from `keyset_` at `index`.
+  Entry CreateEntryAt(int index) const;
+
   google::crypto::tink::Keyset keyset_;
+  // If this keyset handle has been created with a constructor that does not
+  // accept an entries argument, then `entries` will be empty and operator[]
+  // will fall back to creating the key entry on demand from `keyset_`.
+  //
+  // If `entries_` is not empty, then it should contain exactly one key entry
+  // for each key proto in `keyset_`.
+  std::vector<std::shared_ptr<const Entry>> entries_;
   absl::flat_hash_map<std::string, std::string> monitoring_annotations_;
 };
 
@@ -183,8 +310,8 @@
 KeysetHandle::GetPrimitives(const KeyManager<P>* custom_manager) const {
   crypto::tink::util::Status status = ValidateKeyset(get_keyset());
   if (!status.ok()) return status;
-  std::unique_ptr<PrimitiveSet<P>> primitives(
-      new PrimitiveSet<P>(monitoring_annotations_));
+  typename PrimitiveSet<P>::Builder primitives_builder;
+  primitives_builder.AddAnnotations(monitoring_annotations_);
   for (const google::crypto::tink::Keyset::Key& key : get_keyset().key()) {
     if (key.status() == google::crypto::tink::KeyStatusType::ENABLED) {
       std::unique_ptr<P> primitive;
@@ -198,23 +325,57 @@
         if (!primitive_result.ok()) return primitive_result.status();
         primitive = std::move(primitive_result.value());
       }
-      auto entry_result =
-          primitives->AddPrimitive(std::move(primitive), KeyInfoFromKey(key));
-      if (!entry_result.ok()) return entry_result.status();
       if (key.key_id() == get_keyset().primary_key_id()) {
-        auto primary_result = primitives->set_primary(entry_result.value());
-        if (!primary_result.ok()) return primary_result;
+        primitives_builder.AddPrimaryPrimitive(std::move(primitive),
+                                               KeyInfoFromKey(key));
+      } else {
+        primitives_builder.AddPrimitive(std::move(primitive),
+                                        KeyInfoFromKey(key));
       }
     }
   }
-  return std::move(primitives);
+  auto primitives = std::move(primitives_builder).Build();
+  if (!primitives.ok()) return primitives.status();
+  return absl::make_unique<PrimitiveSet<P>>(*std::move(primitives));
 }
 
 template <class P>
+crypto::tink::util::StatusOr<std::unique_ptr<P>> KeysetHandle::GetPrimitive(
+    const Configuration& config) const {
+  if (crypto::tink::internal::ConfigurationImpl::GetGlobalRegistryMode(
+          config)) {
+    return crypto::tink::internal::RegistryImpl::GlobalInstance().WrapKeyset<P>(
+        keyset_, monitoring_annotations_);
+  }
+
+  crypto::tink::util::StatusOr<
+      const crypto::tink::internal::KeysetWrapperStore*>
+      wrapper_store =
+          crypto::tink::internal::ConfigurationImpl::GetKeysetWrapperStore(
+              config);
+  if (!wrapper_store.ok()) {
+    return wrapper_store.status();
+  }
+  crypto::tink::util::StatusOr<const crypto::tink::internal::KeysetWrapper<P>*>
+      wrapper = (*wrapper_store)->Get<P>();
+  if (!wrapper.ok()) {
+    return wrapper.status();
+  }
+  return (*wrapper)->Wrap(keyset_, monitoring_annotations_);
+}
+
+// TODO(b/265865177): Deprecate.
+template <class P>
 crypto::tink::util::StatusOr<std::unique_ptr<P>> KeysetHandle::GetPrimitive()
     const {
-  return internal::RegistryImpl::GlobalInstance().WrapKeyset<P>(
-      keyset_, monitoring_annotations_);
+  // TODO(b/265705174): Replace with ConfigGlobalRegistry instance.
+  crypto::tink::Configuration config;
+  crypto::tink::util::Status status =
+      crypto::tink::internal::ConfigurationImpl::SetGlobalRegistryMode(config);
+  if (!status.ok()) {
+    return status;
+  }
+  return GetPrimitive<P>(config);
 }
 
 template <class P>
diff --git a/cc/keyset_handle_builder.h b/cc/keyset_handle_builder.h
new file mode 100644
index 0000000..72d3af8
--- /dev/null
+++ b/cc/keyset_handle_builder.h
@@ -0,0 +1,169 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_KEYSET_HANDLE_BUILDER_H_
+#define TINK_KEYSET_HANDLE_BUILDER_H_
+
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "tink/internal/keyset_handle_builder_entry.h"
+#include "tink/key.h"
+#include "tink/key_status.h"
+#include "tink/keyset_handle.h"
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+
+// Creates new `KeysetHandle` objects.
+class KeysetHandleBuilder {
+ public:
+  // Movable, but not copyable.
+  KeysetHandleBuilder(KeysetHandleBuilder&& other) = default;
+  KeysetHandleBuilder& operator=(KeysetHandleBuilder&& other) = default;
+  KeysetHandleBuilder(const KeysetHandleBuilder& other) = delete;
+  KeysetHandleBuilder& operator=(const KeysetHandleBuilder& other) = delete;
+
+  // Creates initially empty keyset handle builder.
+  KeysetHandleBuilder() = default;
+  // Creates keyset handle builder by initially moving keys from `handle`.
+  explicit KeysetHandleBuilder(const KeysetHandle& handle);
+
+  // Represents a single entry in a `KeysetHandleBuilder`.
+  class Entry {
+   public:
+    // Movable, but not copyable.
+    Entry(Entry&& other) = default;
+    Entry& operator=(Entry&& other) = default;
+    Entry(const Entry& other) = delete;
+    Entry& operator=(const Entry& other) = delete;
+
+    // Creates new KeysetHandleBuilder::Entry from a given `key`. Also, sets
+    // key `status` and whether or not the key `is_primary`.
+    static Entry CreateFromKey(std::shared_ptr<const Key> key, KeyStatus status,
+                               bool is_primary);
+
+    template <typename CopyableKey>
+    inline static Entry CreateFromCopyableKey(CopyableKey key, KeyStatus status,
+                                              bool is_primary) {
+      auto copyable_key = absl::make_unique<CopyableKey>(std::move(key));
+      return CreateFromKey(std::move(copyable_key), status, is_primary);
+    }
+
+    // Creates new KeysetHandleBuilder::Entry from given `parameters`. Also,
+    // sets key `status` and whether or not the key `is_primary`. If `id`
+    // does not have a value, then the key will be assigned a random id.
+    static Entry CreateFromParams(std::shared_ptr<const Parameters> parameters,
+                                  KeyStatus status, bool is_primary,
+                                  absl::optional<int> id = absl::nullopt);
+
+    template <typename CopyableParameters>
+    inline static Entry CreateFromCopyableParams(
+        CopyableParameters parameters, KeyStatus status, bool is_primary,
+        absl::optional<int> id = absl::nullopt) {
+      auto copyable_params =
+          absl::make_unique<CopyableParameters>(std::move(parameters));
+      return CreateFromParams(std::move(copyable_params), status, is_primary,
+                              id);
+    }
+
+    // Sets the key status of this entry.
+    void SetStatus(KeyStatus status) { entry_->SetStatus(status); }
+    // Returns key status of this entry.
+    KeyStatus GetStatus() const { return entry_->GetStatus(); }
+
+    // Assigns a fixed id when this keyset is built.
+    void SetFixedId(int id) { entry_->SetFixedId(id); }
+    // Assigns an unused random id when this keyset is built.
+    void SetRandomId() { entry_->SetRandomId(); }
+
+    // Sets this entry as the primary key.
+    void SetPrimary() { entry_->SetPrimary(); }
+    // Unsets this entry as the primary key.
+    void UnsetPrimary() { entry_->UnsetPrimary(); }
+    // Returns whether or not this entry has been marked as a primary.
+    bool IsPrimary() const { return entry_->IsPrimary(); }
+
+   private:
+    friend class KeysetHandleBuilder;
+
+    explicit Entry(std::unique_ptr<internal::KeysetHandleBuilderEntry> entry)
+        : entry_(std::move(entry)) {}
+
+    // Returns whether or not this entry has a randomly assigned id.
+    bool HasRandomId() {
+      return entry_->GetKeyIdStrategyEnum() ==
+             internal::KeyIdStrategyEnum::kRandomId;
+    }
+
+    internal::KeyIdStrategy GetKeyIdStrategy() {
+      return entry_->GetKeyIdStrategy();
+    }
+
+    crypto::tink::util::StatusOr<google::crypto::tink::Keyset::Key>
+    CreateKeysetKey(int id) {
+      return entry_->CreateKeysetKey(id);
+    }
+
+    std::unique_ptr<internal::KeysetHandleBuilderEntry> entry_;
+    bool added_to_builder_ = false;
+  };
+
+  // Adds an `entry` to the keyset builder. Crashes if `entry` has already been
+  // added to a keyset handle builder.
+  KeysetHandleBuilder& AddEntry(KeysetHandleBuilder::Entry entry);
+  // Removes an entry at `index` from keyset builder.
+  KeysetHandleBuilder& RemoveEntry(int index);
+
+  // Returns the number of Entry objects in this keyset builder.
+  int size() const { return entries_.size(); }
+
+  // Returns entry from keyset builder at `index`.
+  KeysetHandleBuilder::Entry& operator[](int index) { return entries_[index]; }
+
+  // Creates a new `KeysetHandle` object.
+  //
+  // Note: Since KeysetHandleBuilder::Entry objects might have randomly
+  // generated IDs, Build() can only be called once on a single
+  // KeysetHandleBuilder object.  Otherwise, the KeysetHandleBuilder::Entry
+  // IDs would randomly change for each call to Build(), which would result
+  // in incompatible keysets.
+  crypto::tink::util::StatusOr<KeysetHandle> Build();
+
+ private:
+  // Select the next key id based on the given strategy.
+  crypto::tink::util::StatusOr<int> NextIdFromKeyIdStrategy(
+      internal::KeyIdStrategy strategy, const std::set<int>& ids_so_far);
+
+  // Unset primary flag on all entries.
+  void ClearPrimary();
+
+  // Verify that entries with fixed IDs do not follow entries with random IDs.
+  crypto::tink::util::Status CheckIdAssignments();
+
+  std::vector<KeysetHandleBuilder::Entry> entries_;
+
+  bool build_called_ = false;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_KEYSET_HANDLE_BUILDER_H_
diff --git a/cc/keyset_manager.h b/cc/keyset_manager.h
index d3bfcff..645fc83 100644
--- a/cc/keyset_manager.h
+++ b/cc/keyset_manager.h
@@ -16,6 +16,8 @@
 #ifndef TINK_KEYSET_MANAGER_H_
 #define TINK_KEYSET_MANAGER_H_
 
+#include <memory>
+
 #include "absl/base/thread_annotations.h"
 #include "absl/synchronization/mutex.h"
 #include "tink/util/status.h"
@@ -34,7 +36,7 @@
 class KeysetManager {
  public:
   // Constructs a KeysetManager with an empty Keyset.
-  KeysetManager() {}
+  KeysetManager() = default;
 
   // Creates a new KeysetManager that contains a Keyset with a single key
   // generated freshly according the specification in 'key_template'.
diff --git a/cc/keyset_reader.h b/cc/keyset_reader.h
index 037697b..02e2659 100644
--- a/cc/keyset_reader.h
+++ b/cc/keyset_reader.h
@@ -17,6 +17,8 @@
 #ifndef TINK_KEYSET_READER_H_
 #define TINK_KEYSET_READER_H_
 
+#include <memory>
+
 #include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
 
@@ -37,7 +39,7 @@
     std::unique_ptr<google::crypto::tink::EncryptedKeyset>>
   ReadEncrypted() = 0;
 
-  virtual ~KeysetReader() {}
+  virtual ~KeysetReader() = default;
 };
 
 }  // namespace tink
diff --git a/cc/keyset_writer.h b/cc/keyset_writer.h
index 3c30bd1..96df7db 100644
--- a/cc/keyset_writer.h
+++ b/cc/keyset_writer.h
@@ -35,7 +35,7 @@
   virtual crypto::tink::util::Status
       Write(const google::crypto::tink::EncryptedKeyset& encrypted_keyset) = 0;
 
-  virtual ~KeysetWriter() {}
+  virtual ~KeysetWriter() = default;
 };
 
 }  // namespace tink
diff --git a/cc/kms_client.h b/cc/kms_client.h
index dc7a59d..6753804 100644
--- a/cc/kms_client.h
+++ b/cc/kms_client.h
@@ -38,7 +38,7 @@
   virtual crypto::tink::util::StatusOr<std::unique_ptr<Aead>>
   GetAead(absl::string_view key_uri) const = 0;
 
-  virtual ~KmsClient() {}
+  virtual ~KmsClient() = default;
 };
 
 }  // namespace tink
diff --git a/cc/kms_clients.h b/cc/kms_clients.h
index 3b2ac13..772360b 100644
--- a/cc/kms_clients.h
+++ b/cc/kms_clients.h
@@ -17,6 +17,7 @@
 #ifndef TINK_KMS_CLIENTS_H_
 #define TINK_KMS_CLIENTS_H_
 
+#include <memory>
 #include <utility>
 #include <vector>
 
diff --git a/cc/mac.h b/cc/mac.h
index 6bc02b8..a50adc9 100644
--- a/cc/mac.h
+++ b/cc/mac.h
@@ -42,7 +42,7 @@
       absl::string_view mac_value,
       absl::string_view data) const = 0;
 
-  virtual ~Mac() {}
+  virtual ~Mac() = default;
 };
 
 }  // namespace tink
diff --git a/cc/mac/BUILD.bazel b/cc/mac/BUILD.bazel
index b681672..77f5534 100644
--- a/cc/mac/BUILD.bazel
+++ b/cc/mac/BUILD.bazel
@@ -32,7 +32,9 @@
     visibility = ["//visibility:public"],
     deps = [
         ":aes_cmac_key_manager",
+        ":aes_cmac_proto_serialization",
         ":hmac_key_manager",
+        ":hmac_proto_serialization",
         ":mac_wrapper",
         "//:registry",
         "//config:config_util",
@@ -172,8 +174,121 @@
     deps = [
         ":mac_parameters",
         "//:crypto_format",
+        "//internal:util",
         "//util:status",
         "//util:statusor",
+        "@com_google_absl//absl/log",
+    ],
+)
+
+cc_library(
+    name = "aes_cmac_key",
+    srcs = ["aes_cmac_key.cc"],
+    hdrs = ["aes_cmac_key.h"],
+    include_prefix = "tink/mac",
+    deps = [
+        ":aes_cmac_parameters",
+        ":mac_key",
+        "//:partial_key_access_token",
+        "//:restricted_data",
+        "//subtle:subtle_util",
+        "//util:status",
+        "//util:statusor",
+        "@boringssl//:crypto",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:str_format",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "aes_cmac_proto_serialization",
+    srcs = ["aes_cmac_proto_serialization.cc"],
+    hdrs = ["aes_cmac_proto_serialization.h"],
+    include_prefix = "tink/mac",
+    deps = [
+        ":aes_cmac_key",
+        ":aes_cmac_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//internal:key_parser",
+        "//internal:key_serializer",
+        "//internal:mutable_serialization_registry",
+        "//internal:parameters_parser",
+        "//internal:parameters_serializer",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_cmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "hmac_parameters",
+    srcs = ["hmac_parameters.cc"],
+    hdrs = ["hmac_parameters.h"],
+    include_prefix = "tink/mac",
+    deps = [
+        ":mac_parameters",
+        "//:crypto_format",
+        "//internal:util",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/log",
+    ],
+)
+
+cc_library(
+    name = "hmac_key",
+    srcs = ["hmac_key.cc"],
+    hdrs = ["hmac_key.h"],
+    include_prefix = "tink/mac",
+    deps = [
+        ":hmac_parameters",
+        ":mac_key",
+        "//:partial_key_access_token",
+        "//:restricted_data",
+        "//subtle:subtle_util",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:str_format",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "hmac_proto_serialization",
+    srcs = ["hmac_proto_serialization.cc"],
+    hdrs = ["hmac_proto_serialization.h"],
+    include_prefix = "tink/mac",
+    deps = [
+        ":hmac_key",
+        ":hmac_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//internal:key_parser",
+        "//internal:key_serializer",
+        "//internal:mutable_serialization_registry",
+        "//internal:parameters_parser",
+        "//internal:parameters_serializer",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:common_cc_proto",
+        "//proto:hmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
     ],
 )
 
@@ -207,16 +322,26 @@
     srcs = ["mac_config_test.cc"],
     tags = ["fips"],
     deps = [
+        ":aes_cmac_key",
         ":aes_cmac_key_manager",
+        ":aes_cmac_parameters",
+        ":hmac_key",
         ":hmac_key_manager",
+        ":hmac_parameters",
         ":mac_config",
         ":mac_key_templates",
         "//:chunked_mac",
-        "//:config",
+        "//:insecure_secret_key_access",
         "//:keyset_handle",
         "//:mac",
+        "//:partial_key_access",
         "//:registry",
-        "//config:tink_fips",
+        "//internal:fips_utils",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:common_cc_proto",
+        "//proto:tink_cc_proto",
         "//util:status",
         "//util:test_matchers",
         "//util:test_util",
@@ -324,3 +449,90 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "aes_cmac_key_test",
+    size = "small",
+    srcs = ["aes_cmac_key_test.cc"],
+    deps = [
+        ":aes_cmac_key",
+        ":aes_cmac_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_cmac_proto_serialization_test",
+    size = "small",
+    srcs = ["aes_cmac_proto_serialization_test.cc"],
+    deps = [
+        ":aes_cmac_key",
+        ":aes_cmac_parameters",
+        ":aes_cmac_proto_serialization",
+        "//:insecure_secret_key_access",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_cmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "hmac_parameters_test",
+    size = "small",
+    srcs = ["hmac_parameters_test.cc"],
+    deps = [
+        ":hmac_parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "hmac_key_test",
+    srcs = ["hmac_key_test.cc"],
+    deps = [
+        ":hmac_key",
+        ":hmac_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "hmac_proto_serialization_test",
+    srcs = ["hmac_proto_serialization_test.cc"],
+    deps = [
+        ":hmac_key",
+        ":hmac_parameters",
+        ":hmac_proto_serialization",
+        "//:insecure_secret_key_access",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:common_cc_proto",
+        "//proto:hmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/mac/BUILD.gn b/cc/mac/BUILD.gn
index 6129f46..601f2fc 100644
--- a/cc/mac/BUILD.gn
+++ b/cc/mac/BUILD.gn
@@ -41,7 +41,9 @@
   ]
   public_deps = [
     ":aes_cmac_key_manager",
+    ":aes_cmac_proto_serialization",
     ":hmac_key_manager",
+    ":hmac_proto_serialization",
     ":mac_wrapper",
     "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/abseil-cpp/absl/memory:memory",
@@ -117,3 +119,175 @@
   ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
+
+# CC Library : mac_parameters
+source_set("mac_parameters") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "mac_parameters.h" ]
+  public_deps = [ "//third_party/tink/cc:parameters" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : mac_key
+source_set("mac_key") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "mac_key.h" ]
+  public_deps = [
+    ":mac_parameters",
+    "//third_party/tink/cc:key",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : aes_cmac_parameters
+source_set("aes_cmac_parameters") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "aes_cmac_parameters.cc",
+    "aes_cmac_parameters.h",
+  ]
+  public_deps = [
+    ":mac_parameters",
+    "//third_party/abseil-cpp/absl/log:log",
+    "//third_party/tink/cc:crypto_format",
+    "//third_party/tink/cc/internal:util",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : aes_cmac_key
+source_set("aes_cmac_key") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "aes_cmac_key.cc",
+    "aes_cmac_key.h",
+  ]
+  public_deps = [
+    ":aes_cmac_parameters",
+    ":mac_key",
+    "//third_party/abseil-cpp/absl/base:core_headers",
+    "//third_party/abseil-cpp/absl/strings:str_format",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/boringssl:crypto",
+    "//third_party/tink/cc:partial_key_access_token",
+    "//third_party/tink/cc:restricted_data",
+    "//third_party/tink/cc/subtle:subtle_util",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : aes_cmac_proto_serialization
+source_set("aes_cmac_proto_serialization") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "aes_cmac_proto_serialization.cc",
+    "aes_cmac_proto_serialization.h",
+  ]
+  public_deps = [
+    ":aes_cmac_key",
+    ":aes_cmac_parameters",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:partial_key_access",
+    "//third_party/tink/cc:restricted_data",
+    "//third_party/tink/cc:secret_key_access_token",
+    "//third_party/tink/cc/internal:key_parser",
+    "//third_party/tink/cc/internal:key_serializer",
+    "//third_party/tink/cc/internal:mutable_serialization_registry",
+    "//third_party/tink/cc/internal:parameters_parser",
+    "//third_party/tink/cc/internal:parameters_serializer",
+    "//third_party/tink/cc/internal:proto_key_serialization",
+    "//third_party/tink/cc/internal:proto_parameters_serialization",
+    "//third_party/tink/cc/proto:aes_cmac_proto",
+    "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : hmac_parameters
+source_set("hmac_parameters") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "hmac_parameters.cc",
+    "hmac_parameters.h",
+  ]
+  public_deps = [
+    ":mac_parameters",
+    "//third_party/abseil-cpp/absl/log:log",
+    "//third_party/tink/cc:crypto_format",
+    "//third_party/tink/cc/internal:util",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : hmac_key
+source_set("hmac_key") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "hmac_key.cc",
+    "hmac_key.h",
+  ]
+  public_deps = [
+    ":hmac_parameters",
+    ":mac_key",
+    "//third_party/abseil-cpp/absl/base:core_headers",
+    "//third_party/abseil-cpp/absl/strings:str_format",
+    "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:partial_key_access_token",
+    "//third_party/tink/cc:restricted_data",
+    "//third_party/tink/cc/subtle:subtle_util",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : hmac_proto_serialization
+source_set("hmac_proto_serialization") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "hmac_proto_serialization.cc",
+    "hmac_proto_serialization.h",
+  ]
+  public_deps = [
+    ":hmac_key",
+    ":hmac_parameters",
+    "//third_party/abseil-cpp/absl/status:status",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc:partial_key_access",
+    "//third_party/tink/cc:restricted_data",
+    "//third_party/tink/cc:secret_key_access_token",
+    "//third_party/tink/cc/internal:key_parser",
+    "//third_party/tink/cc/internal:key_serializer",
+    "//third_party/tink/cc/internal:mutable_serialization_registry",
+    "//third_party/tink/cc/internal:parameters_parser",
+    "//third_party/tink/cc/internal:parameters_serializer",
+    "//third_party/tink/cc/internal:proto_key_serialization",
+    "//third_party/tink/cc/internal:proto_parameters_serialization",
+    "//third_party/tink/cc/proto:common_proto",
+    "//third_party/tink/cc/proto:hmac_proto",
+    "//third_party/tink/cc/proto:tink_proto",
+    "//third_party/tink/cc/util:status",
+    "//third_party/tink/cc/util:statusor",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
diff --git a/cc/mac/CMakeLists.txt b/cc/mac/CMakeLists.txt
index ab763f1..95e5335 100644
--- a/cc/mac/CMakeLists.txt
+++ b/cc/mac/CMakeLists.txt
@@ -30,7 +30,9 @@
     mac_config.h
   DEPS
     tink::mac::aes_cmac_key_manager
+    tink::mac::aes_cmac_proto_serialization
     tink::mac::hmac_key_manager
+    tink::mac::hmac_proto_serialization
     tink::mac::mac_wrapper
     absl::core_headers
     absl::memory
@@ -151,6 +153,7 @@
   DEPS
     absl::strings
     tink::core::mac
+  TESTONLY
 )
 
 tink_cc_library(
@@ -160,11 +163,119 @@
     aes_cmac_parameters.h
   DEPS
     tink::mac::mac_parameters
+    absl::log
     tink::core::crypto_format
+    tink::internal::util
     tink::util::status
     tink::util::statusor
 )
 
+tink_cc_library(
+  NAME aes_cmac_key
+  SRCS
+    aes_cmac_key.cc
+    aes_cmac_key.h
+  DEPS
+    tink::mac::aes_cmac_parameters
+    tink::mac::mac_key
+    absl::core_headers
+    absl::strings
+    absl::str_format
+    absl::optional
+    crypto
+    tink::core::partial_key_access_token
+    tink::core::restricted_data
+    tink::subtle::subtle_util
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_cmac_proto_serialization
+  SRCS
+    aes_cmac_proto_serialization.cc
+    aes_cmac_proto_serialization.h
+  DEPS
+    tink::mac::aes_cmac_key
+    tink::mac::aes_cmac_parameters
+    absl::status
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::mutable_serialization_registry
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::util::status
+    tink::util::statusor
+    tink::proto::aes_cmac_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME hmac_parameters
+  SRCS
+    hmac_parameters.cc
+    hmac_parameters.h
+  DEPS
+    tink::mac::mac_parameters
+    absl::log
+    tink::core::crypto_format
+    tink::internal::util
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME hmac_key
+  SRCS
+    hmac_key.cc
+    hmac_key.h
+  DEPS
+    tink::mac::hmac_parameters
+    tink::mac::mac_key
+    absl::core_headers
+    absl::strings
+    absl::str_format
+    absl::optional
+    tink::core::partial_key_access_token
+    tink::core::restricted_data
+    tink::subtle::subtle_util
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME hmac_proto_serialization
+  SRCS
+    hmac_proto_serialization.cc
+    hmac_proto_serialization.h
+  DEPS
+    tink::mac::hmac_key
+    tink::mac::hmac_parameters
+    absl::status
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::mutable_serialization_registry
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::util::status
+    tink::util::statusor
+    tink::proto::common_cc_proto
+    tink::proto::hmac_cc_proto
+    tink::proto::tink_cc_proto
+)
+
 # tests
 
 tink_cc_test(
@@ -193,22 +304,32 @@
   SRCS
     mac_config_test.cc
   DEPS
+    tink::mac::aes_cmac_key
     tink::mac::aes_cmac_key_manager
+    tink::mac::aes_cmac_parameters
+    tink::mac::hmac_key
     tink::mac::hmac_key_manager
+    tink::mac::hmac_parameters
     tink::mac::mac_config
     tink::mac::mac_key_templates
     gmock
     absl::status
     crypto
     tink::core::chunked_mac
-    tink::core::config
+    tink::core::insecure_secret_key_access
     tink::core::keyset_handle
     tink::core::mac
+    tink::core::partial_key_access
     tink::core::registry
-    tink::config::tink_fips
+    tink::internal::fips_utils
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
     tink::util::status
     tink::util::test_matchers
     tink::util::test_util
+    tink::proto::common_cc_proto
+    tink::proto::tink_cc_proto
 )
 
 tink_cc_test(
@@ -304,3 +425,87 @@
     tink::util::statusor
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME aes_cmac_key_test
+  SRCS
+    aes_cmac_key_test.cc
+  DEPS
+    tink::mac::aes_cmac_key
+    tink::mac::aes_cmac_parameters
+    gmock
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_cmac_proto_serialization_test
+  SRCS
+    aes_cmac_proto_serialization_test.cc
+  DEPS
+    tink::mac::aes_cmac_key
+    tink::mac::aes_cmac_parameters
+    tink::mac::aes_cmac_proto_serialization
+    gmock
+    tink::core::insecure_secret_key_access
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::subtle::random
+    tink::util::test_matchers
+    tink::proto::aes_cmac_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME hmac_parameters_test
+  SRCS
+    hmac_parameters_test.cc
+  DEPS
+    tink::mac::hmac_parameters
+    gmock
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME hmac_key_test
+  SRCS
+    hmac_key_test.cc
+  DEPS
+    tink::mac::hmac_key
+    tink::mac::hmac_parameters
+    gmock
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME hmac_proto_serialization_test
+  SRCS
+    hmac_proto_serialization_test.cc
+  DEPS
+    tink::mac::hmac_key
+    tink::mac::hmac_parameters
+    tink::mac::hmac_proto_serialization
+    gmock
+    tink::core::insecure_secret_key_access
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::subtle::random
+    tink::util::test_matchers
+    tink::proto::common_cc_proto
+    tink::proto::hmac_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/mac/aes_cmac_key.cc b/cc/mac/aes_cmac_key.cc
new file mode 100644
index 0000000..cd16cac
--- /dev/null
+++ b/cc/mac/aes_cmac_key.cc
@@ -0,0 +1,108 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/aes_cmac_key.h"
+
+#include <memory>
+#include <string>
+
+#include "absl/base/attributes.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/str_format.h"
+#include "absl/types/optional.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/subtle_util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+util::StatusOr<AesCmacKey> AesCmacKey::Create(
+    const AesCmacParameters& parameters, const RestrictedData& key_bytes,
+    absl::optional<int> id_requirement, PartialKeyAccessToken token) {
+  if (parameters.KeySizeInBytes() != key_bytes.size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Key size does not match AES-CMAC parameters");
+  }
+  if (parameters.HasIdRequirement() && !id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key without ID requirement with parameters with ID "
+        "requirement");
+  }
+  if (!parameters.HasIdRequirement() && id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key with ID requirement with parameters without ID "
+        "requirement");
+  }
+  util::StatusOr<std::string> output_prefix =
+      ComputeOutputPrefix(parameters, id_requirement);
+  if (!output_prefix.ok()) {
+    return output_prefix.status();
+  }
+  return AesCmacKey(parameters, key_bytes, id_requirement,
+                    *std::move(output_prefix));
+}
+
+util::StatusOr<std::string> AesCmacKey::ComputeOutputPrefix(
+    const AesCmacParameters& parameters, absl::optional<int> id_requirement) {
+  switch (parameters.GetVariant()) {
+    case AesCmacParameters::Variant::kNoPrefix:
+      return std::string("");  // Empty prefix.
+    case AesCmacParameters::Variant::kLegacy:
+      ABSL_FALLTHROUGH_INTENDED;
+    case AesCmacParameters::Variant::kCrunchy:
+      if (!id_requirement.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "id requirement must have value with kCrunchy or kLegacy");
+      }
+      return absl::StrCat(absl::HexStringToBytes("00"),
+                          subtle::BigEndian32(*id_requirement));
+    case AesCmacParameters::Variant::kTink:
+      if (!id_requirement.has_value()) {
+        return util::Status(absl::StatusCode::kInvalidArgument,
+                            "id requirement must have value with kTink");
+      }
+      return absl::StrCat(absl::HexStringToBytes("01"),
+                          subtle::BigEndian32(*id_requirement));
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat("Invalid variant: ", parameters.GetVariant()));
+  }
+}
+
+bool AesCmacKey::operator==(const Key& other) const {
+  const AesCmacKey* that = dynamic_cast<const AesCmacKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (GetParameters() != that->GetParameters()) {
+    return false;
+  }
+  if (id_requirement_ != that->id_requirement_) {
+    return false;
+  }
+  return key_bytes_ == that->key_bytes_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/aes_cmac_key.h b/cc/mac/aes_cmac_key.h
new file mode 100644
index 0000000..c29500b
--- /dev/null
+++ b/cc/mac/aes_cmac_key.h
@@ -0,0 +1,88 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_MAC_AES_CMAC_KEY_H_
+#define TINK_MAC_AES_CMAC_KEY_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/types/optional.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/mac/mac_key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+class AesCmacKey : public MacKey {
+ public:
+  // Copyable and movable.
+  AesCmacKey(const AesCmacKey& other) = default;
+  AesCmacKey& operator=(const AesCmacKey& other) = default;
+  AesCmacKey(AesCmacKey&& other) = default;
+  AesCmacKey& operator=(AesCmacKey&& other) = default;
+
+  // Creates a new AES-CMAC key.  If the parameters specify a variant that uses
+  // a prefix, then the id is used to compute this prefix.
+  static util::StatusOr<AesCmacKey> Create(const AesCmacParameters& parameters,
+                                           const RestrictedData& key_bytes,
+                                           absl::optional<int> id_requirement,
+                                           PartialKeyAccessToken token);
+
+  // Returns the underlying AES key.
+  util::StatusOr<RestrictedData> GetKeyBytes(
+      PartialKeyAccessToken token) const {
+    return key_bytes_;
+  }
+
+  absl::string_view GetOutputPrefix() const override { return output_prefix_; }
+
+  const AesCmacParameters& GetParameters() const override {
+    return parameters_;
+  }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  AesCmacKey(const AesCmacParameters& parameters,
+             const RestrictedData& key_bytes,
+             absl::optional<int> id_requirement, std::string output_prefix)
+      : parameters_(parameters),
+        key_bytes_(key_bytes),
+        id_requirement_(id_requirement),
+        output_prefix_(std::move(output_prefix)) {}
+
+  static util::StatusOr<std::string> ComputeOutputPrefix(
+      const AesCmacParameters& parameters, absl::optional<int> id_requirement);
+
+  AesCmacParameters parameters_;
+  RestrictedData key_bytes_;
+  absl::optional<int> id_requirement_;
+  std::string output_prefix_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_MAC_AES_CMAC_KEY_H_
diff --git a/cc/mac/aes_cmac_key_manager.h b/cc/mac/aes_cmac_key_manager.h
index 8617b46..7ee5a96 100644
--- a/cc/mac/aes_cmac_key_manager.h
+++ b/cc/mac/aes_cmac_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_MAC_AES_CMAC_KEY_MANAGER_H_
 #define TINK_MAC_AES_CMAC_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/mac/aes_cmac_key_test.cc b/cc/mac/aes_cmac_key_test.cc
new file mode 100644
index 0000000..8bb87cf
--- /dev/null
+++ b/cc/mac/aes_cmac_key_test.cc
@@ -0,0 +1,253 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/aes_cmac_key.h"
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/types/optional.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesCmacParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using AesCmacKeyTest = TestWithParam<std::tuple<int, int, TestCase>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesCmacKeyTestSuite, AesCmacKeyTest,
+    Combine(Values(16, 32), Range(10, 16),
+            Values(TestCase{AesCmacParameters::Variant::kTink, 0x02030400,
+                            std::string("\x01\x02\x03\x04\x00", 5)},
+                   TestCase{AesCmacParameters::Variant::kCrunchy, 0x01030005,
+                            std::string("\x00\x01\x03\x00\x05", 5)},
+                   TestCase{AesCmacParameters::Variant::kLegacy, 0x01020304,
+                            std::string("\x00\x01\x02\x03\x04", 5)},
+                   TestCase{AesCmacParameters::Variant::kNoPrefix,
+                            absl::nullopt, ""})));
+
+TEST_P(AesCmacKeyTest, CreateSucceeds) {
+  int key_size;
+  int cryptographic_tag_size;
+  TestCase test_case;
+  std::tie(key_size, cryptographic_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      key_size, cryptographic_tag_size, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  EXPECT_THAT(key->GetParameters(), Eq(*params));
+  EXPECT_THAT(key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(key->GetOutputPrefix(), Eq(test_case.output_prefix));
+}
+
+TEST(AesCmacKeyTest, CreateKeyWithMismatchedKeySizeFails) {
+  // Key size parameter is 32 bytes.
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  // Key material is 16 bytes (another valid key length).
+  RestrictedData mismatched_secret = RestrictedData(/*num_random_bytes=*/16);
+
+  EXPECT_THAT(AesCmacKey::Create(*params, mismatched_secret,
+                                 /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesCmacKeyTest, CreateKeyWithWrongIdRequirementFails) {
+  util::StatusOr<AesCmacParameters> no_prefix_params =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/16,
+                                AesCmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(no_prefix_params, IsOk());
+
+  util::StatusOr<AesCmacParameters> tink_params =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/16,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  EXPECT_THAT(AesCmacKey::Create(*no_prefix_params, secret,
+                                 /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesCmacKey::Create(*tink_params, secret,
+                                 /*id_requirement=*/absl::nullopt,
+                                 GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesCmacKeyTest, GetAesCmacKey) {
+  int key_size;
+  int cryptographic_tag_size;
+  TestCase test_case;
+  std::tie(key_size, cryptographic_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      key_size, cryptographic_tag_size, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  EXPECT_THAT(key->GetKeyBytes(GetPartialKeyAccess()), IsOkAndHolds(secret));
+}
+
+TEST_P(AesCmacKeyTest, KeyEquals) {
+  int key_size;
+  int cryptographic_tag_size;
+  TestCase test_case;
+  std::tie(key_size, cryptographic_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      key_size, cryptographic_tag_size, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesCmacKey> other_key = AesCmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key == *other_key);
+  EXPECT_TRUE(*other_key == *key);
+  EXPECT_FALSE(*key != *other_key);
+  EXPECT_FALSE(*other_key != *key);
+}
+
+TEST(AesCmacKeyTest, DifferentFormatNotEqual) {
+  util::StatusOr<AesCmacParameters> legacy_params =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/16,
+                                AesCmacParameters::Variant::kLegacy);
+  ASSERT_THAT(legacy_params, IsOk());
+
+  util::StatusOr<AesCmacParameters> tink_params =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/16,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesCmacKey> key =
+      AesCmacKey::Create(*legacy_params, secret, /*id_requirement=*/0x01020304,
+                         GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<AesCmacKey> other_key =
+      AesCmacKey::Create(*tink_params, secret, /*id_requirement=*/0x01020304,
+                         GetPartialKeyAccess());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesCmacKeyTest, DifferentSecretDataNotEqual) {
+  util::StatusOr<AesCmacParameters> params =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/16,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret1 = RestrictedData(/*num_random_bytes=*/32);
+  RestrictedData secret2 = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret1, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<AesCmacKey> other_key = AesCmacKey::Create(
+      *params, secret2, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesCmacKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<AesCmacParameters> params =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/16,
+                                AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *params, secret, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<AesCmacKey> other_key = AesCmacKey::Create(
+      *params, secret, /*id_requirement=*/0x02030405, GetPartialKeyAccess());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/aes_cmac_parameters.cc b/cc/mac/aes_cmac_parameters.cc
index c4d8429..4064d2f 100644
--- a/cc/mac/aes_cmac_parameters.cc
+++ b/cc/mac/aes_cmac_parameters.cc
@@ -22,7 +22,10 @@
 #include <ostream>
 #include <set>
 
+#include "absl/log/log.h"
+#include "absl/strings/str_cat.h"
 #include "tink/crypto_format.h"
+#include "tink/internal/util.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
@@ -30,7 +33,14 @@
 namespace tink {
 
 util::StatusOr<AesCmacParameters> AesCmacParameters::Create(
-    int cryptographic_tag_size_in_bytes, Variant variant) {
+    int key_size_in_bytes, int cryptographic_tag_size_in_bytes,
+    Variant variant) {
+  if (key_size_in_bytes != 16 && key_size_in_bytes != 32) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Key size should be either 16 or 32 bytes, got ",
+                     key_size_in_bytes, " bytes."));
+  }
   if (cryptographic_tag_size_in_bytes < 10) {
     return util::Status(
         absl::StatusCode::kInvalidArgument,
@@ -51,7 +61,8 @@
         absl::StatusCode::kInvalidArgument,
         "Cannot create AES-CMAC parameters with unknown variant.");
   }
-  return AesCmacParameters(cryptographic_tag_size_in_bytes, variant);
+  return AesCmacParameters(key_size_in_bytes, cryptographic_tag_size_in_bytes,
+                           variant);
 }
 
 int AesCmacParameters::TotalTagSizeInBytes() const {
@@ -64,8 +75,7 @@
       return CryptographicTagSizeInBytes();
     default:
       // Parameters objects with unknown variants should never be created.
-      std::cerr << "AES-CMAC parameters has an unknown variant." << std::endl;
-      std::exit(1);
+      internal::LogFatal("AES-CMAC parameters has an unknown variant.");
   }
 }
 
@@ -75,9 +85,17 @@
   if (that == nullptr) {
     return false;
   }
-  return cryptographic_tag_size_in_bytes_ ==
-             that->cryptographic_tag_size_in_bytes_ &&
-         variant_ == that->variant_;
+  if (key_size_in_bytes_ != that->key_size_in_bytes_) {
+    return false;
+  }
+  if (cryptographic_tag_size_in_bytes_ !=
+      that->cryptographic_tag_size_in_bytes_) {
+    return false;
+  }
+  if (variant_ != that->variant_) {
+    return false;
+  }
+  return true;
 }
 
 }  // namespace tink
diff --git a/cc/mac/aes_cmac_parameters.h b/cc/mac/aes_cmac_parameters.h
index a4a09a7..06a560a 100644
--- a/cc/mac/aes_cmac_parameters.h
+++ b/cc/mac/aes_cmac_parameters.h
@@ -53,14 +53,18 @@
   AesCmacParameters(AesCmacParameters&& other) = default;
   AesCmacParameters& operator=(AesCmacParameters&& other) = default;
 
-  // Creates a new AES-CMAC parameters object. Returns an error status if
-  // `cryptographic_tag_size_in_bytes` falls outside [10,...,16].  Otherwise,
-  // returns the parameters object.
+  // Creates a new AES-CMAC parameters object unless an error occurs. An error
+  // occurs under one of the following conditions:
+  // 1. `key_size_in_bytes` is a value other than 16 or 32
+  // 2. `cryptographic_tag_size_in_bytes` falls outside [10,...,16]
   static util::StatusOr<AesCmacParameters> Create(
-      int cryptographic_tag_size_in_bytes, Variant variant);
+      int key_size_in_bytes, int cryptographic_tag_size_in_bytes,
+      Variant variant);
 
   Variant GetVariant() const { return variant_; }
 
+  int KeySizeInBytes() const { return key_size_in_bytes_; }
+
   // Returns the size of the tag, which is computed cryptographically from the
   // message. Note that this may differ from the total size of the tag, as for
   // some keys, Tink prefixes the tag with a key dependent output prefix.
@@ -79,10 +83,13 @@
   bool operator==(const Parameters& other) const override;
 
  private:
-  AesCmacParameters(int cryptographic_tag_size_in_bytes, Variant variant)
-      : cryptographic_tag_size_in_bytes_(cryptographic_tag_size_in_bytes),
+  AesCmacParameters(int key_size_in_bytes, int cryptographic_tag_size_in_bytes,
+                    Variant variant)
+      : key_size_in_bytes_(key_size_in_bytes),
+        cryptographic_tag_size_in_bytes_(cryptographic_tag_size_in_bytes),
         variant_(variant) {}
 
+  int key_size_in_bytes_;
   int cryptographic_tag_size_in_bytes_;
   Variant variant_;
 };
diff --git a/cc/mac/aes_cmac_parameters_test.cc b/cc/mac/aes_cmac_parameters_test.cc
index db1f2c3..7284b7c 100644
--- a/cc/mac/aes_cmac_parameters_test.cc
+++ b/cc/mac/aes_cmac_parameters_test.cc
@@ -38,38 +38,48 @@
 
 struct CreateTestCase {
   AesCmacParameters::Variant variant;
-  int cryptographic_tag_size_in_bytes;
-  int total_tag_size_in_bytes;
+  int key_size;
+  int cryptographic_tag_size;
+  int total_tag_size;
   bool has_id_requirement;
 };
 
-class AesCmacParametersCreateTest : public TestWithParam<CreateTestCase> {};
+using AesCmacParametersCreateTest = TestWithParam<CreateTestCase>;
 
 INSTANTIATE_TEST_SUITE_P(
     AesCmacParametersCreateTestSuite, AesCmacParametersCreateTest,
-    Values(CreateTestCase{AesCmacParameters::Variant::kTink, 10, 15, true},
-           CreateTestCase{AesCmacParameters::Variant::kCrunchy, 12, 17, true},
-           CreateTestCase{AesCmacParameters::Variant::kLegacy, 14, 19, true},
-           CreateTestCase{AesCmacParameters::Variant::kNoPrefix, 16, 16,
-                          false}));
+    Values(CreateTestCase{AesCmacParameters::Variant::kTink, /*key_size=*/16,
+                          /*cryptographic_tag_size=*/10, /*total_tag_size=*/15,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{AesCmacParameters::Variant::kCrunchy, /*key_size=*/16,
+                          /*cryptographic_tag_size=*/12, /*total_tag_size=*/17,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{AesCmacParameters::Variant::kLegacy, /*key_size=*/32,
+                          /*cryptographic_tag_size=*/14, /*total_tag_size=*/19,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{AesCmacParameters::Variant::kNoPrefix,
+                          /*key_size=*/32, /*cryptographic_tag_size=*/16,
+                          /*total_tag_size=*/16,
+                          /*has_id_requirement=*/false}));
 
 TEST_P(AesCmacParametersCreateTest, Create) {
   CreateTestCase test_case = GetParam();
 
   util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
-      test_case.cryptographic_tag_size_in_bytes, test_case.variant);
+      test_case.key_size, test_case.cryptographic_tag_size, test_case.variant);
   ASSERT_THAT(parameters, IsOk());
 
   EXPECT_THAT(parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(parameters->KeySizeInBytes(), Eq(test_case.key_size));
   EXPECT_THAT(parameters->CryptographicTagSizeInBytes(),
-              Eq(test_case.cryptographic_tag_size_in_bytes));
-  EXPECT_THAT(parameters->TotalTagSizeInBytes(),
-              Eq(test_case.total_tag_size_in_bytes));
+              Eq(test_case.cryptographic_tag_size));
+  EXPECT_THAT(parameters->TotalTagSizeInBytes(), Eq(test_case.total_tag_size));
   EXPECT_THAT(parameters->HasIdRequirement(), Eq(test_case.has_id_requirement));
 }
 
 TEST(AesCmacParametersTest, CreateWithInvalidVariantFails) {
   EXPECT_THAT(AesCmacParameters::Create(
+                  /*key_size_in_bytes=*/32,
                   /*cryptographic_tag_size_in_bytes=*/12,
                   AesCmacParameters::Variant::
                       kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
@@ -77,39 +87,69 @@
               StatusIs(absl::StatusCode::kInvalidArgument));
 }
 
+TEST(AesCmacParametersTest, CreateWithInvalidKeySizeFails) {
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/15,
+                                        /*cryptographic_tag_size_in_bytes=*/16,
+                                        AesCmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/17,
+                                        /*cryptographic_tag_size_in_bytes=*/16,
+                                        AesCmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/31,
+                                        /*cryptographic_tag_size_in_bytes=*/16,
+                                        AesCmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/33,
+                                        /*cryptographic_tag_size_in_bytes=*/16,
+                                        AesCmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
 TEST(AesCmacParametersTest, CreateWithInvalidTagSizeFails) {
   // Too small.
-  EXPECT_THAT(AesCmacParameters::Create(/*cryptographic_tag_size_in_bytes=*/7,
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                        /*cryptographic_tag_size_in_bytes=*/7,
                                         AesCmacParameters::Variant::kNoPrefix)
                   .status(),
               StatusIs(absl::StatusCode::kInvalidArgument));
-  EXPECT_THAT(AesCmacParameters::Create(/*cryptographic_tag_size_in_bytes=*/8,
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                        /*cryptographic_tag_size_in_bytes=*/8,
                                         AesCmacParameters::Variant::kNoPrefix)
                   .status(),
               StatusIs(absl::StatusCode::kInvalidArgument));
-  EXPECT_THAT(AesCmacParameters::Create(/*cryptographic_tag_size_in_bytes=*/9,
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                        /*cryptographic_tag_size_in_bytes=*/9,
                                         AesCmacParameters::Variant::kNoPrefix)
                   .status(),
               StatusIs(absl::StatusCode::kInvalidArgument));
   // Too big;
-  EXPECT_THAT(AesCmacParameters::Create(/*cryptographic_tag_size_in_bytes=*/17,
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                        /*cryptographic_tag_size_in_bytes=*/17,
                                         AesCmacParameters::Variant::kNoPrefix)
                   .status(),
               StatusIs(absl::StatusCode::kInvalidArgument));
-  EXPECT_THAT(AesCmacParameters::Create(/*cryptographic_tag_size_in_bytes=*/18,
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                        /*cryptographic_tag_size_in_bytes=*/18,
                                         AesCmacParameters::Variant::kNoPrefix)
                   .status(),
               StatusIs(absl::StatusCode::kInvalidArgument));
-  EXPECT_THAT(AesCmacParameters::Create(/*cryptographic_tag_size_in_bytes=*/19,
+  EXPECT_THAT(AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                        /*cryptographic_tag_size_in_bytes=*/19,
                                         AesCmacParameters::Variant::kNoPrefix)
                   .status(),
               StatusIs(absl::StatusCode::kInvalidArgument));
 }
 
 TEST(AesCmacParametersTest, CopyConstructor) {
-  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
-      /*cryptographic_tag_size_in_bytes=*/12,
-      AesCmacParameters::Variant::kTink);
+  util::StatusOr<AesCmacParameters> parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/12,
+                                AesCmacParameters::Variant::kTink);
   ASSERT_THAT(parameters, IsOk());
 
   AesCmacParameters copy(*parameters);
@@ -122,9 +162,10 @@
 }
 
 TEST(AesCmacParametersTest, CopyAssignment) {
-  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
-      /*cryptographic_tag_size_in_bytes=*/12,
-      AesCmacParameters::Variant::kTink);
+  util::StatusOr<AesCmacParameters> parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/12,
+                                AesCmacParameters::Variant::kTink);
   ASSERT_THAT(parameters, IsOk());
 
   AesCmacParameters copy = *parameters;
@@ -136,28 +177,29 @@
   EXPECT_THAT(copy.HasIdRequirement(), Eq(parameters->HasIdRequirement()));
 }
 
-class AesCmacParametersVariantTest
-    : public TestWithParam<std::tuple<AesCmacParameters::Variant, int>> {};
+using AesCmacParametersVariantTest =
+    TestWithParam<std::tuple<int, int, AesCmacParameters::Variant>>;
 
-INSTANTIATE_TEST_SUITE_P(AesCmacParametersVariantTestSuite,
-                         AesCmacParametersVariantTest,
-                         Combine(Values(AesCmacParameters::Variant::kTink,
-                                        AesCmacParameters::Variant::kCrunchy,
-                                        AesCmacParameters::Variant::kLegacy,
-                                        AesCmacParameters::Variant::kNoPrefix),
-                                 Range(10, 16)));
+INSTANTIATE_TEST_SUITE_P(
+    AesCmacParametersVariantTestSuite, AesCmacParametersVariantTest,
+    Combine(Values(16, 32), Range(10, 16),
+            Values(AesCmacParameters::Variant::kTink,
+                   AesCmacParameters::Variant::kCrunchy,
+                   AesCmacParameters::Variant::kLegacy,
+                   AesCmacParameters::Variant::kNoPrefix)));
 
 TEST_P(AesCmacParametersVariantTest, ParametersEquals) {
-  AesCmacParameters::Variant variant;
+  int key_size;
   int cryptographic_tag_size;
-  std::tie(variant, cryptographic_tag_size) = GetParam();
+  AesCmacParameters::Variant variant;
+  std::tie(key_size, cryptographic_tag_size, variant) = GetParam();
 
   util::StatusOr<AesCmacParameters> parameters =
-      AesCmacParameters::Create(cryptographic_tag_size, variant);
+      AesCmacParameters::Create(key_size, cryptographic_tag_size, variant);
   ASSERT_THAT(parameters, IsOk());
 
   util::StatusOr<AesCmacParameters> other_parameters =
-      AesCmacParameters::Create(cryptographic_tag_size, variant);
+      AesCmacParameters::Create(key_size, cryptographic_tag_size, variant);
   ASSERT_THAT(other_parameters, IsOk());
 
   EXPECT_TRUE(*parameters == *other_parameters);
@@ -166,16 +208,34 @@
   EXPECT_FALSE(*other_parameters != *parameters);
 }
 
-TEST(AesCmacParametersTest, TagSizeNotEqual) {
-  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
-      /*cryptographic_tag_size_in_bytes=*/10,
-      AesCmacParameters::Variant::kNoPrefix);
+TEST(AesCmacParametersTest, KeySizeNotEqual) {
+  util::StatusOr<AesCmacParameters> parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/16,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kNoPrefix);
   ASSERT_THAT(parameters, IsOk());
 
   util::StatusOr<AesCmacParameters> other_parameters =
-      AesCmacParameters::Create(
-          /*cryptographic_tag_size_in_bytes=*/11,
-          AesCmacParameters::Variant::kNoPrefix);
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesCmacParametersTest, TagSizeNotEqual) {
+  util::StatusOr<AesCmacParameters> parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesCmacParameters> other_parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/11,
+                                AesCmacParameters::Variant::kNoPrefix);
   ASSERT_THAT(other_parameters, IsOk());
 
   EXPECT_TRUE(*parameters != *other_parameters);
@@ -183,15 +243,16 @@
 }
 
 TEST(AesCmacParametersTest, VariantNotEqual) {
-  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
-      /*cryptographic_tag_size_in_bytes=*/10,
-      AesCmacParameters::Variant::kNoPrefix);
+  util::StatusOr<AesCmacParameters> parameters =
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kNoPrefix);
   ASSERT_THAT(parameters, IsOk());
 
   util::StatusOr<AesCmacParameters> other_parameters =
-      AesCmacParameters::Create(
-          /*cryptographic_tag_size_in_bytes=*/10,
-          AesCmacParameters::Variant::kTink);
+      AesCmacParameters::Create(/*key_size_in_bytes=*/32,
+                                /*cryptographic_tag_size_in_bytes=*/10,
+                                AesCmacParameters::Variant::kTink);
   ASSERT_THAT(other_parameters, IsOk());
 
   EXPECT_TRUE(*parameters != *other_parameters);
diff --git a/cc/mac/aes_cmac_proto_serialization.cc b/cc/mac/aes_cmac_proto_serialization.cc
new file mode 100644
index 0000000..5c7b77b
--- /dev/null
+++ b/cc/mac/aes_cmac_proto_serialization.cc
@@ -0,0 +1,248 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/aes_cmac_proto_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/mac/aes_cmac_key.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/aes_cmac.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::AesCmacKeyFormat;
+using ::google::crypto::tink::AesCmacParams;
+using ::google::crypto::tink::OutputPrefixType;
+
+using AesCmacProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   AesCmacParameters>;
+using AesCmacProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<AesCmacParameters,
+                                       internal::ProtoParametersSerialization>;
+using AesCmacProtoKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, AesCmacKey>;
+using AesCmacProtoKeySerializerImpl =
+    internal::KeySerializerImpl<AesCmacKey, internal::ProtoKeySerialization>;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.AesCmacKey";
+
+util::StatusOr<AesCmacParameters::Variant> ToVariant(
+    OutputPrefixType output_prefix_type) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::CRUNCHY:
+      return AesCmacParameters::Variant::kCrunchy;
+    case OutputPrefixType::LEGACY:
+      return AesCmacParameters::Variant::kLegacy;
+    case OutputPrefixType::RAW:
+      return AesCmacParameters::Variant::kNoPrefix;
+    case OutputPrefixType::TINK:
+      return AesCmacParameters::Variant::kTink;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine AesCmacParameters::Variant");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    AesCmacParameters::Variant variant) {
+  switch (variant) {
+    case AesCmacParameters::Variant::kCrunchy:
+      return OutputPrefixType::CRUNCHY;
+    case AesCmacParameters::Variant::kLegacy:
+      return OutputPrefixType::LEGACY;
+    case AesCmacParameters::Variant::kNoPrefix:
+      return OutputPrefixType::RAW;
+    case AesCmacParameters::Variant::kTink:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine output prefix type");
+  }
+}
+
+util::StatusOr<AesCmacParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesCmacParameters.");
+  }
+
+  AesCmacKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesCmacKeyFormat proto");
+  }
+
+  util::StatusOr<AesCmacParameters::Variant> variant =
+      ToVariant(serialization.GetKeyTemplate().output_prefix_type());
+  if (!variant.ok()) return variant.status();
+
+  return AesCmacParameters::Create(proto_key_format.key_size(),
+                                   proto_key_format.params().tag_size(),
+                                   *variant);
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const AesCmacParameters& parameters) {
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  AesCmacParams proto_params;
+  proto_params.set_tag_size(parameters.CryptographicTagSizeInBytes());
+  AesCmacKeyFormat proto_key_format;
+  proto_key_format.set_key_size(parameters.KeySizeInBytes());
+  *proto_key_format.mutable_params() = proto_params;
+
+  return internal::ProtoParametersSerialization::Create(
+      kTypeUrl, *output_prefix_type, proto_key_format.SerializeAsString());
+}
+
+util::StatusOr<AesCmacKey> ParseKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesCmacKey.");
+  }
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+  google::crypto::tink::AesCmacKey proto_key;
+  RestrictedData restricted_data = serialization.SerializedKeyProto();
+  // OSS proto library complains if input is not converted to a string.
+  if (!proto_key.ParseFromString(
+          std::string(restricted_data.GetSecret(*token)))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesCmacKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesCmacParameters::Variant> variant =
+      ToVariant(serialization.GetOutputPrefixType());
+  if (!variant.ok()) return variant.status();
+
+  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
+      proto_key.key_value().length(), proto_key.params().tag_size(), *variant);
+  if (!parameters.ok()) return parameters.status();
+
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *parameters, RestrictedData(proto_key.key_value(), *token),
+      serialization.IdRequirement(), GetPartialKeyAccess());
+  if (!key.ok()) return key.status();
+
+  return *key;
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializeKey(
+    const AesCmacKey& key, absl::optional<SecretKeyAccessToken> token) {
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetKeyBytes(GetPartialKeyAccess());
+  if (!restricted_input.ok()) return restricted_input.status();
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+
+  AesCmacParams proto_params;
+  proto_params.set_tag_size(key.GetParameters().CryptographicTagSizeInBytes());
+  google::crypto::tink::AesCmacKey proto_key;
+  *proto_key.mutable_params() = proto_params;
+  proto_key.set_version(0);
+  // OSS proto library complains if input is not converted to a string.
+  proto_key.set_key_value(std::string(restricted_input->GetSecret(*token)));
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetParameters().GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  RestrictedData restricted_output =
+      RestrictedData(proto_key.SerializeAsString(), *token);
+  return internal::ProtoKeySerialization::Create(
+      kTypeUrl, restricted_output, google::crypto::tink::KeyData::SYMMETRIC,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+AesCmacProtoParametersParserImpl* AesCmacProtoParametersParser() {
+  static auto* parser =
+      new AesCmacProtoParametersParserImpl(kTypeUrl, ParseParameters);
+  return parser;
+}
+
+AesCmacProtoParametersSerializerImpl* AesCmacProtoParametersSerializer() {
+  static auto* serializer =
+      new AesCmacProtoParametersSerializerImpl(kTypeUrl, SerializeParameters);
+  return serializer;
+}
+
+AesCmacProtoKeyParserImpl* AesCmacProtoKeyParser() {
+  static auto* parser = new AesCmacProtoKeyParserImpl(kTypeUrl, ParseKey);
+  return parser;
+}
+
+AesCmacProtoKeySerializerImpl* AesCmacProtoKeySerializer() {
+  static auto* serializer = new AesCmacProtoKeySerializerImpl(SerializeKey);
+  return serializer;
+}
+
+}  // namespace
+
+util::Status RegisterAesCmacProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(AesCmacProtoParametersParser());
+  if (!status.ok()) return status;
+
+  status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersSerializer(AesCmacProtoParametersSerializer());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(AesCmacProtoKeyParser());
+  if (!status.ok()) return status;
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(AesCmacProtoKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/aes_cmac_proto_serialization.h b/cc/mac/aes_cmac_proto_serialization.h
new file mode 100644
index 0000000..d34dcb6
--- /dev/null
+++ b/cc/mac/aes_cmac_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_MAC_AES_CMAC_PROTO_SERIALIZATION_H_
+#define TINK_MAC_AES_CMAC_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for AES-CMAC parameters and keys.
+crypto::tink::util::Status RegisterAesCmacProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_MAC_AES_CMAC_PROTO_SERIALIZATION_H_
diff --git a/cc/mac/aes_cmac_proto_serialization_test.cc b/cc/mac/aes_cmac_proto_serialization_test.cc
new file mode 100644
index 0000000..cda1093
--- /dev/null
+++ b/cc/mac/aes_cmac_proto_serialization_test.cc
@@ -0,0 +1,373 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/aes_cmac_proto_serialization.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/mac/aes_cmac_key.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
+#include "proto/aes_cmac.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::subtle::Random;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesCmacKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesCmacParameters::Variant variant;
+  OutputPrefixType output_prefix_type;
+  int key_size;
+  int tag_size;
+  int total_size;
+  absl::optional<int> id;
+  std::string output_prefix;
+};
+
+class AesCmacProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  void SetUp() override {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    AesCmacProtoSerializationTestSuite, AesCmacProtoSerializationTest,
+    Values(TestCase{AesCmacParameters::Variant::kTink, OutputPrefixType::TINK,
+                    /*key_size=*/16, /*tag_size=*/10, /*total_size=*/15,
+                    /*id=*/0x02030400,
+                    /*output_prefix=*/std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{AesCmacParameters::Variant::kCrunchy,
+                    OutputPrefixType::CRUNCHY, /*key_size=*/16,
+                    /*tag_size=*/12, /*total_size=*/17, /*id=*/0x01030005,
+                    /*output_prefix=*/std::string("\x00\x01\x03\x00\x05", 5)},
+           TestCase{AesCmacParameters::Variant::kLegacy,
+                    OutputPrefixType::LEGACY, /*key_size=*/32,
+                    /*cryptographic_tag_size=*/14, /*total_tag_size=*/19,
+                    /*id=*/0x01020304,
+                    /*output_prefix=*/std::string("\x00\x01\x02\x03\x04", 5)},
+           TestCase{AesCmacParameters::Variant::kNoPrefix,
+                    OutputPrefixType::RAW, /*key_size=*/32,
+                    /*cryptographic_tag_size=*/16, /*total_tag_size=*/16,
+                    /*id=*/absl::nullopt, /*output_prefix=*/""}));
+
+TEST_P(AesCmacProtoSerializationTest, ParseParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  AesCmacKeyFormat key_format_proto;
+  key_format_proto.set_key_size(test_case.key_size);
+  key_format_proto.mutable_params()->set_tag_size(test_case.tag_size);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey",
+          test_case.output_prefix_type, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), test_case.id.has_value());
+
+  const AesCmacParameters* cmac_params =
+      dynamic_cast<const AesCmacParameters*>(params->get());
+  ASSERT_THAT(cmac_params, NotNull());
+  EXPECT_THAT(cmac_params->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(cmac_params->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(cmac_params->CryptographicTagSizeInBytes(),
+              Eq(test_case.tag_size));
+  EXPECT_THAT(cmac_params->TotalTagSizeInBytes(), Eq(test_case.total_size));
+}
+
+TEST_F(AesCmacProtoSerializationTest, ParseParametersWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  AesCmacKeyFormat key_format_proto;
+  key_format_proto.set_key_size(16);
+  key_format_proto.mutable_params()->set_tag_size(10);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey",
+          OutputPrefixType::RAW, "invalid_serialization");
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesCmacProtoSerializationTest, ParseParametersWithUnkownOutputPrefix) {
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  AesCmacKeyFormat key_format_proto;
+  key_format_proto.set_key_size(16);
+  key_format_proto.mutable_params()->set_tag_size(10);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey",
+          OutputPrefixType::UNKNOWN_PREFIX,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesCmacProtoSerializationTest, SerializeParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
+      test_case.key_size, test_case.tag_size, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesCmacKey"));
+
+  const internal::ProtoParametersSerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoParametersSerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().type_url(),
+              Eq("type.googleapis.com/google.crypto.tink.AesCmacKey"));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  AesCmacKeyFormat key_format;
+  ASSERT_THAT(
+      key_format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  ASSERT_THAT(key_format.key_size(), Eq(test_case.key_size));
+  ASSERT_THAT(key_format.params().tag_size(), Eq(test_case.tag_size));
+}
+
+TEST_P(AesCmacProtoSerializationTest, ParseKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  google::crypto::tink::AesCmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(test_case.tag_size);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey", serialized_key,
+          KeyData::SYMMETRIC, test_case.output_prefix_type, test_case.id);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key)->GetIdRequirement(), Eq(test_case.id));
+  EXPECT_THAT((*key)->GetParameters().HasIdRequirement(),
+              test_case.id.has_value());
+
+  const AesCmacKey* cmac_key = dynamic_cast<const AesCmacKey*>(key->get());
+  ASSERT_THAT(cmac_key, NotNull());
+  util::StatusOr<RestrictedData> parsed_key =
+      cmac_key->GetKeyBytes(GetPartialKeyAccess());
+  ASSERT_THAT(parsed_key, IsOk());
+  EXPECT_THAT(parsed_key->GetSecret(InsecureSecretKeyAccess::Get()),
+              Eq(raw_key_bytes));
+  EXPECT_THAT(cmac_key->GetOutputPrefix(), Eq(test_case.output_prefix));
+  EXPECT_THAT(cmac_key->GetParameters().GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(cmac_key->GetParameters().KeySizeInBytes(),
+              Eq(test_case.key_size));
+  EXPECT_THAT(cmac_key->GetParameters().CryptographicTagSizeInBytes(),
+              Eq(test_case.tag_size));
+  EXPECT_THAT(cmac_key->GetParameters().TotalTagSizeInBytes(),
+              test_case.total_size);
+  EXPECT_THAT(cmac_key->GetParameters().HasIdRequirement(),
+              test_case.id.has_value());
+}
+
+TEST_F(AesCmacProtoSerializationTest, ParseKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesCmacProtoSerializationTest, ParseKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::AesCmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(10);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, absl::nullopt);
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesCmacProtoSerializationTest, ParseKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::AesCmacKey key_proto;
+  key_proto.set_version(1);  // Invalid version number.
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(10);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesCmacProtoSerializationTest, SerializeKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
+      test_case.key_size, test_case.tag_size, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesCmacKey"));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(),
+              Eq("type.googleapis.com/google.crypto.tink.AesCmacKey"));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(), Eq(test_case.id));
+
+  google::crypto::tink::AesCmacKey proto_key;
+  // OSS proto library complains if input is not converted to a string.
+  ASSERT_THAT(proto_key.ParseFromString(std::string(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get()))),
+              IsTrue());
+  EXPECT_THAT(proto_key.key_value().size(), Eq(test_case.key_size));
+  EXPECT_THAT(proto_key.params().tag_size(), Eq(test_case.tag_size));
+}
+
+TEST_F(AesCmacProtoSerializationTest, SerializeKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesCmacProtoSerialization(), IsOk());
+
+  util::StatusOr<AesCmacParameters> parameters = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/16, /*cryptographic_tag_size_in_bytes=*/10,
+      AesCmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  util::StatusOr<AesCmacKey> key = AesCmacKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(*key, absl::nullopt);
+  ASSERT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/failing_mac.cc b/cc/mac/failing_mac.cc
index 999bb83..5a1b49c 100644
--- a/cc/mac/failing_mac.cc
+++ b/cc/mac/failing_mac.cc
@@ -15,6 +15,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/mac/failing_mac.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/mac/failing_mac.h b/cc/mac/failing_mac.h
index 4eed37d..84bde37 100644
--- a/cc/mac/failing_mac.h
+++ b/cc/mac/failing_mac.h
@@ -16,6 +16,7 @@
 #ifndef TINK_MAC_FAILING_MAC_H_
 #define TINK_MAC_FAILING_MAC_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/mac/hmac_key.cc b/cc/mac/hmac_key.cc
new file mode 100644
index 0000000..388dc31
--- /dev/null
+++ b/cc/mac/hmac_key.cc
@@ -0,0 +1,109 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/hmac_key.h"
+
+#include <memory>
+#include <string>
+
+#include "absl/base/attributes.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/str_format.h"
+#include "absl/types/optional.h"
+#include "tink/mac/hmac_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/subtle_util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+util::StatusOr<HmacKey> HmacKey::Create(const HmacParameters& parameters,
+                                        const RestrictedData& key_bytes,
+                                        absl::optional<int> id_requirement,
+                                        PartialKeyAccessToken token) {
+  if (parameters.KeySizeInBytes() != key_bytes.size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Key size does not match HMAC parameters");
+  }
+  if (parameters.HasIdRequirement() && !id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key without ID requirement with parameters with ID "
+        "requirement");
+  }
+  if (!parameters.HasIdRequirement() && id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key with ID requirement with parameters without ID "
+        "requirement");
+  }
+  util::StatusOr<std::string> output_prefix =
+      ComputeOutputPrefix(parameters, id_requirement);
+  if (!output_prefix.ok()) {
+    return output_prefix.status();
+  }
+  return HmacKey(parameters, key_bytes, id_requirement,
+                 *std::move(output_prefix));
+}
+
+util::StatusOr<std::string> HmacKey::ComputeOutputPrefix(
+    const HmacParameters& parameters, absl::optional<int> id_requirement) {
+  switch (parameters.GetVariant()) {
+    case HmacParameters::Variant::kNoPrefix:
+      return std::string("");  // Empty prefix.
+    case HmacParameters::Variant::kLegacy:
+      ABSL_FALLTHROUGH_INTENDED;
+    case HmacParameters::Variant::kCrunchy:
+      if (!id_requirement.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "id requirement must have value with kCrunchy or kLegacy");
+      }
+      return absl::StrCat(absl::HexStringToBytes("00"),
+                          subtle::BigEndian32(*id_requirement));
+    case HmacParameters::Variant::kTink:
+      if (!id_requirement.has_value()) {
+        return util::Status(absl::StatusCode::kInvalidArgument,
+                            "id requirement must have value with kTink");
+      }
+      return absl::StrCat(absl::HexStringToBytes("01"),
+                          subtle::BigEndian32(*id_requirement));
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat("Invalid variant: ", parameters.GetVariant()));
+  }
+}
+
+bool HmacKey::operator==(const Key& other) const {
+  const HmacKey* that = dynamic_cast<const HmacKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (GetParameters() != that->GetParameters()) {
+    return false;
+  }
+  if (id_requirement_ != that->id_requirement_) {
+    return false;
+  }
+  return key_bytes_ == that->key_bytes_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/hmac_key.h b/cc/mac/hmac_key.h
new file mode 100644
index 0000000..30f6bf5
--- /dev/null
+++ b/cc/mac/hmac_key.h
@@ -0,0 +1,85 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_MAC_HMAC_KEY_H_
+#define TINK_MAC_HMAC_KEY_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/types/optional.h"
+#include "tink/mac/hmac_parameters.h"
+#include "tink/mac/mac_key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+class HmacKey : public MacKey {
+ public:
+  // Copyable and movable.
+  HmacKey(const HmacKey& other) = default;
+  HmacKey& operator=(const HmacKey& other) = default;
+  HmacKey(HmacKey&& other) = default;
+  HmacKey& operator=(HmacKey&& other) = default;
+
+  // Creates a new HMAC key.  If the parameters specify a variant that uses
+  // a prefix, then the id is used to compute this prefix.
+  static util::StatusOr<HmacKey> Create(const HmacParameters& parameters,
+                                        const RestrictedData& key_bytes,
+                                        absl::optional<int> id_requirement,
+                                        PartialKeyAccessToken token);
+
+  // Returns the underlying HMAC key bytes.
+  util::StatusOr<RestrictedData> GetKeyBytes(
+      PartialKeyAccessToken token) const {
+    return key_bytes_;
+  }
+
+  absl::string_view GetOutputPrefix() const override { return output_prefix_; }
+
+  const HmacParameters& GetParameters() const override { return parameters_; }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  HmacKey(const HmacParameters& parameters, const RestrictedData& key_bytes,
+          absl::optional<int> id_requirement, std::string output_prefix)
+      : parameters_(parameters),
+        key_bytes_(key_bytes),
+        id_requirement_(id_requirement),
+        output_prefix_(std::move(output_prefix)) {}
+
+  static util::StatusOr<std::string> ComputeOutputPrefix(
+      const HmacParameters& parameters, absl::optional<int> id_requirement);
+
+  HmacParameters parameters_;
+  RestrictedData key_bytes_;
+  absl::optional<int> id_requirement_;
+  std::string output_prefix_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_MAC_HMAC_KEY_H_
diff --git a/cc/mac/hmac_key_manager.h b/cc/mac/hmac_key_manager.h
index b696a53..0dc34a9 100644
--- a/cc/mac/hmac_key_manager.h
+++ b/cc/mac/hmac_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_MAC_HMAC_KEY_MANAGER_H_
 #define TINK_MAC_HMAC_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/mac/hmac_key_manager_test.cc b/cc/mac/hmac_key_manager_test.cc
index db17da7..7367623 100644
--- a/cc/mac/hmac_key_manager_test.cc
+++ b/cc/mac/hmac_key_manager_test.cc
@@ -17,13 +17,14 @@
 #include "tink/mac/hmac_key_manager.h"
 
 #include <memory>
+#include <sstream>
 #include <string>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/core/key_manager_impl.h"
 #include "tink/chunked_mac.h"
+#include "tink/core/key_manager_impl.h"
 #include "tink/mac.h"
 #include "tink/util/istream_input_stream.h"
 #include "tink/util/secret_data.h"
@@ -42,7 +43,6 @@
 using ::google::crypto::tink::HashType;
 using ::google::crypto::tink::HmacKey;
 using ::google::crypto::tink::HmacKeyFormat;
-using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
diff --git a/cc/mac/hmac_key_test.cc b/cc/mac/hmac_key_test.cc
new file mode 100644
index 0000000..e35aca7
--- /dev/null
+++ b/cc/mac/hmac_key_test.cc
@@ -0,0 +1,255 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/hmac_key.h"
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/types/optional.h"
+#include "tink/mac/hmac_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  HmacParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using HmacKeyTest =
+    TestWithParam<std::tuple<int, int, HmacParameters::HashType, TestCase>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    HmacKeyTestSuite, HmacKeyTest,
+    Combine(Values(16, 32), Range(10, 20),
+            Values(HmacParameters::HashType::kSha1,
+                   HmacParameters::HashType::kSha224,
+                   HmacParameters::HashType::kSha256,
+                   HmacParameters::HashType::kSha384,
+                   HmacParameters::HashType::kSha512),
+            Values(TestCase{HmacParameters::Variant::kTink, 0x02030400,
+                            std::string("\x01\x02\x03\x04\x00", 5)},
+                   TestCase{HmacParameters::Variant::kCrunchy, 0x01030005,
+                            std::string("\x00\x01\x03\x00\x05", 5)},
+                   TestCase{HmacParameters::Variant::kLegacy, 0x01020304,
+                            std::string("\x00\x01\x02\x03\x04", 5)},
+                   TestCase{HmacParameters::Variant::kNoPrefix, absl::nullopt,
+                            ""})));
+
+TEST_P(HmacKeyTest, CreateSucceeds) {
+  int key_size;
+  int cryptographic_tag_size;
+  HmacParameters::HashType hash_type;
+  TestCase test_case;
+  std::tie(key_size, cryptographic_tag_size, hash_type, test_case) = GetParam();
+
+  util::StatusOr<HmacParameters> params = HmacParameters::Create(
+      key_size, cryptographic_tag_size, hash_type, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  EXPECT_THAT(key->GetParameters(), Eq(*params));
+  EXPECT_THAT(key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(key->GetOutputPrefix(), Eq(test_case.output_prefix));
+}
+
+TEST(HmacKeyTest, CreateKeyWithMismatchedKeySizeFails) {
+  // Key size parameter is 32 bytes.
+  util::StatusOr<HmacParameters> params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha256, HmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  // Key material is 16 bytes (another valid key length).
+  RestrictedData mismatched_secret = RestrictedData(/*num_random_bytes=*/16);
+
+  EXPECT_THAT(HmacKey::Create(*params, mismatched_secret,
+                              /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(HmacKeyTest, CreateKeyWithWrongIdRequirementFails) {
+  util::StatusOr<HmacParameters> no_prefix_params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha512, HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(no_prefix_params, IsOk());
+
+  util::StatusOr<HmacParameters> tink_params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha512, HmacParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  EXPECT_THAT(HmacKey::Create(*no_prefix_params, secret,
+                              /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(
+      HmacKey::Create(*tink_params, secret,
+                      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(HmacKeyTest, GetKeyBytes) {
+  int key_size;
+  int cryptographic_tag_size;
+  HmacParameters::HashType hash_type;
+  TestCase test_case;
+  std::tie(key_size, cryptographic_tag_size, hash_type, test_case) = GetParam();
+
+  util::StatusOr<HmacParameters> params = HmacParameters::Create(
+      key_size, cryptographic_tag_size, hash_type, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  EXPECT_THAT(key->GetKeyBytes(GetPartialKeyAccess()), IsOkAndHolds(secret));
+}
+
+TEST_P(HmacKeyTest, KeyEquals) {
+  int key_size;
+  int cryptographic_tag_size;
+  HmacParameters::HashType hash_type;
+  TestCase test_case;
+  std::tie(key_size, cryptographic_tag_size, hash_type, test_case) = GetParam();
+
+  util::StatusOr<HmacParameters> params = HmacParameters::Create(
+      key_size, cryptographic_tag_size, hash_type, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<HmacKey> other_key = HmacKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key == *other_key);
+  EXPECT_TRUE(*other_key == *key);
+  EXPECT_FALSE(*key != *other_key);
+  EXPECT_FALSE(*other_key != *key);
+}
+
+TEST(HmacKeyTest, DifferentFormatNotEqual) {
+  util::StatusOr<HmacParameters> legacy_params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha256, HmacParameters::Variant::kLegacy);
+  ASSERT_THAT(legacy_params, IsOk());
+
+  util::StatusOr<HmacParameters> tink_params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha256, HmacParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<HmacKey> key =
+      HmacKey::Create(*legacy_params, secret, /*id_requirement=*/0x01020304,
+                      GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<HmacKey> other_key =
+      HmacKey::Create(*tink_params, secret, /*id_requirement=*/0x01020304,
+                      GetPartialKeyAccess());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(HmacKeyTest, DifferentSecretDataNotEqual) {
+  util::StatusOr<HmacParameters> params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha384, HmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret1 = RestrictedData(/*num_random_bytes=*/32);
+  RestrictedData secret2 = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *params, secret1, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<HmacKey> other_key = HmacKey::Create(
+      *params, secret2, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(HmacKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<HmacParameters> params = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      HmacParameters::HashType::kSha224, HmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *params, secret, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key.status(), IsOk());
+
+  util::StatusOr<HmacKey> other_key = HmacKey::Create(
+      *params, secret, /*id_requirement=*/0x02030405, GetPartialKeyAccess());
+  ASSERT_THAT(other_key.status(), IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/hmac_parameters.cc b/cc/mac/hmac_parameters.cc
new file mode 100644
index 0000000..ad94b7b
--- /dev/null
+++ b/cc/mac/hmac_parameters.cc
@@ -0,0 +1,126 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/hmac_parameters.h"
+
+#include <cstdlib>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <ostream>
+#include <set>
+
+#include "absl/log/log.h"
+#include "absl/strings/str_cat.h"
+#include "tink/crypto_format.h"
+#include "tink/internal/util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::Status ValidateTagSizeBytes(int cryptographic_tag_size_in_bytes,
+                                  HmacParameters::HashType hash_type) {
+  if (cryptographic_tag_size_in_bytes < 10) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Tag size should be at least 10 bytes, got ",
+                     cryptographic_tag_size_in_bytes, " bytes."));
+  }
+  std::map<HmacParameters::HashType, uint32_t> max_tag_size = {
+      {HmacParameters::HashType::kSha1, 20},
+      {HmacParameters::HashType::kSha224, 28},
+      {HmacParameters::HashType::kSha256, 32},
+      {HmacParameters::HashType::kSha384, 48},
+      {HmacParameters::HashType::kSha512, 64}};
+  if (max_tag_size.find(hash_type) == max_tag_size.end()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Cannot create HMAC parameters with given hash type. ",
+                     hash_type, " not supported."));
+  }
+  if (cryptographic_tag_size_in_bytes > max_tag_size[hash_type]) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Tag size is too big for given ", hash_type, " , got ",
+                     cryptographic_tag_size_in_bytes, " bytes."));
+  }
+  return util::OkStatus();
+}
+
+}  // namespace
+
+util::StatusOr<HmacParameters> HmacParameters::Create(
+    int key_size_in_bytes, int cryptographic_tag_size_in_bytes,
+    HashType hash_type, Variant variant) {
+  if (key_size_in_bytes < 16) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("Key size must be at least 16 bytes, got ",
+                                     key_size_in_bytes, " bytes."));
+  }
+  util::Status status =
+      ValidateTagSizeBytes(cryptographic_tag_size_in_bytes, hash_type);
+  if (!status.ok()) return status;
+  static const std::set<Variant>* supported_variants =
+      new std::set<Variant>({Variant::kTink, Variant::kCrunchy,
+                             Variant::kLegacy, Variant::kNoPrefix});
+  if (supported_variants->find(variant) == supported_variants->end()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Cannot create HMAC parameters with unknown variant.");
+  }
+  return HmacParameters(key_size_in_bytes, cryptographic_tag_size_in_bytes,
+                        hash_type, variant);
+}
+
+int HmacParameters::TotalTagSizeInBytes() const {
+  switch (variant_) {
+    case Variant::kTink:
+    case Variant::kCrunchy:
+    case Variant::kLegacy:
+      return CryptographicTagSizeInBytes() + CryptoFormat::kNonRawPrefixSize;
+    case Variant::kNoPrefix:
+      return CryptographicTagSizeInBytes();
+    default:
+      // Parameters objects with unknown variants should never be created.
+      internal::LogFatal("HMAC parameters has an unknown variant.");
+  }
+}
+
+bool HmacParameters::operator==(const Parameters& other) const {
+  const HmacParameters* that = dynamic_cast<const HmacParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (key_size_in_bytes_ != that->key_size_in_bytes_) {
+    return false;
+  }
+  if (cryptographic_tag_size_in_bytes_ !=
+      that->cryptographic_tag_size_in_bytes_) {
+    return false;
+  }
+  if (hash_type_ != that->hash_type_) {
+    return false;
+  }
+  if (variant_ != that->variant_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/hmac_parameters.h b/cc/mac/hmac_parameters.h
new file mode 100644
index 0000000..1dc7a39
--- /dev/null
+++ b/cc/mac/hmac_parameters.h
@@ -0,0 +1,115 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_MAC_HMAC_PARAMETERS_H_
+#define TINK_MAC_HMAC_PARAMETERS_H_
+
+#include <memory>
+
+#include "tink/mac/mac_parameters.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes the parameters of an `HmacKey`.
+class HmacParameters : public MacParameters {
+ public:
+  // Describes the details of a MAC computation.
+  //
+  // The usual HMAC key is used for variant `NO_PREFIX`. Other variants
+  // slightly change how the MAC is computed, or add a prefix to every
+  // computation depending on the key id.
+  enum class Variant : int {
+    // Prepends '0x01<big endian key id>' to tag.
+    kTink = 1,
+    // Prepends '0x00<big endian key id>' to tag.
+    kCrunchy = 2,
+    // Appends a 0-byte to input message BEFORE computing the tag, then
+    // prepends '0x00<big endian key id>' to tag.
+    kLegacy = 3,
+    // Does not prepend any prefix (i.e., keys must have no ID requirement).
+    kNoPrefix = 4,
+    // Added to guard from failures that may be caused by future expansions.
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Describes the hash algorithm used.
+  enum class HashType : int {
+    kSha1 = 1,
+    kSha224 = 2,
+    kSha256 = 3,
+    kSha384 = 4,
+    kSha512 = 5,
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Copyable and movable.
+  HmacParameters(const HmacParameters& other) = default;
+  HmacParameters& operator=(const HmacParameters& other) = default;
+  HmacParameters(HmacParameters&& other) = default;
+  HmacParameters& operator=(HmacParameters&& other) = default;
+
+  // Creates a new HMAC parameters object unless an error occurs. An error
+  // occurs under one of the following conditions:
+  // 1. `key_size_in_bytes` is a value smaller than 16 bytes
+  // 2. `cryptographic_tag_size_in_bytes` is either less than 10 bytes or
+  // greater than the maximum value accepted by the corresponding hash algorithm
+  static util::StatusOr<HmacParameters> Create(
+      int key_size_in_bytes, int cryptographic_tag_size_in_bytes,
+      HashType hash_type, Variant variant);
+
+  Variant GetVariant() const { return variant_; }
+
+  HashType GetHashType() const { return hash_type_; }
+
+  int KeySizeInBytes() const { return key_size_in_bytes_; }
+
+  // Returns the size of the tag, which is computed cryptographically from the
+  // message. Note that this may differ from the total size of the tag, as for
+  // some keys, Tink prefixes the tag with a key dependent output prefix.
+  int CryptographicTagSizeInBytes() const {
+    return cryptographic_tag_size_in_bytes_;
+  }
+
+  // Returns the size of the cryptographic tag plus the size of the prefix with
+  // which this key prefixes every cryptographic tag.
+  int TotalTagSizeInBytes() const;
+
+  bool HasIdRequirement() const override {
+    return variant_ != Variant::kNoPrefix;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  HmacParameters(int key_size_in_bytes, int cryptographic_tag_size_in_bytes,
+                 HashType hash_type, Variant variant)
+      : key_size_in_bytes_(key_size_in_bytes),
+        cryptographic_tag_size_in_bytes_(cryptographic_tag_size_in_bytes),
+        hash_type_(hash_type),
+        variant_(variant) {}
+
+  int key_size_in_bytes_;
+  int cryptographic_tag_size_in_bytes_;
+  HashType hash_type_;
+  Variant variant_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_MAC_HMAC_PARAMETERS_H_
diff --git a/cc/mac/hmac_parameters_test.cc b/cc/mac/hmac_parameters_test.cc
new file mode 100644
index 0000000..e9dfeb0
--- /dev/null
+++ b/cc/mac/hmac_parameters_test.cc
@@ -0,0 +1,308 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/hmac_parameters.h"
+
+#include <memory>
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct CreateTestCase {
+  HmacParameters::Variant variant;
+  int key_size;
+  int cryptographic_tag_size;
+  int total_tag_size;
+  HmacParameters::HashType hash_type;
+  bool has_id_requirement;
+};
+
+using HmacParametersCreateTest = TestWithParam<CreateTestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    HmacParametersCreateTestSuite, HmacParametersCreateTest,
+    Values(CreateTestCase{HmacParameters::Variant::kNoPrefix, /*key_size=*/16,
+                          /*cryptographic_tag_size=*/20, /*total_tag_size=*/20,
+                          HmacParameters::HashType::kSha1,
+                          /*has_id_requirement=*/false},
+           CreateTestCase{HmacParameters::Variant::kTink, /*key_size=*/16,
+                          /*cryptographic_tag_size=*/28, /*total_tag_size=*/33,
+                          HmacParameters::HashType::kSha224,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{HmacParameters::Variant::kCrunchy, /*key_size=*/16,
+                          /*cryptographic_tag_size=*/32, /*total_tag_size=*/37,
+                          HmacParameters::HashType::kSha256,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{HmacParameters::Variant::kLegacy, /*key_size=*/32,
+                          /*cryptographic_tag_size=*/48, /*total_tag_size=*/53,
+                          HmacParameters::HashType::kSha384,
+                          /*has_id_requirement=*/true},
+           CreateTestCase{HmacParameters::Variant::kNoPrefix,
+                          /*key_size=*/32, /*cryptographic_tag_size=*/64,
+                          /*total_tag_size=*/64,
+                          HmacParameters::HashType::kSha512,
+                          /*has_id_requirement=*/false}));
+
+TEST_P(HmacParametersCreateTest, Create) {
+  CreateTestCase test_case = GetParam();
+
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      test_case.key_size, test_case.cryptographic_tag_size, test_case.hash_type,
+      test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  EXPECT_THAT(parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(parameters->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(parameters->CryptographicTagSizeInBytes(),
+              Eq(test_case.cryptographic_tag_size));
+  EXPECT_THAT(parameters->TotalTagSizeInBytes(), Eq(test_case.total_tag_size));
+  EXPECT_THAT(parameters->GetHashType(), Eq(test_case.hash_type));
+  EXPECT_THAT(parameters->HasIdRequirement(), Eq(test_case.has_id_requirement));
+}
+
+TEST(HmacParametersTest, CreateWithInvalidVariantFails) {
+  EXPECT_THAT(HmacParameters::Create(
+                  /*key_size_in_bytes=*/16,
+                  /*cryptographic_tag_size_in_bytes=*/12,
+                  HmacParameters::HashType::kSha256,
+                  HmacParameters::Variant::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(HmacParametersTest, CreateWithInvalidHashTypeFails) {
+  EXPECT_THAT(HmacParameters::Create(
+                  /*key_size_in_bytes=*/32,
+                  /*cryptographic_tag_size_in_bytes=*/12,
+                  HmacParameters::HashType::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements,
+                  HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(HmacParametersTest, CreateWithInvalidKeySizeFails) {
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/15,
+                                     /*cryptographic_tag_size_in_bytes=*/16,
+                                     HmacParameters::HashType::kSha256,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(HmacParametersTest, CreateWithInvalidTagSizeFails) {
+  // Too small.
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/32,
+                                     /*cryptographic_tag_size_in_bytes=*/7,
+                                     HmacParameters::HashType::kSha224,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big for kSha1.
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/32,
+                                     /*cryptographic_tag_size_in_bytes=*/21,
+                                     HmacParameters::HashType::kSha1,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big for kSha224.
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/32,
+                                     /*cryptographic_tag_size_in_bytes=*/29,
+                                     HmacParameters::HashType::kSha224,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big for kSha256;
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/32,
+                                     /*cryptographic_tag_size_in_bytes=*/33,
+                                     HmacParameters::HashType::kSha256,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big for kSha384;
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/32,
+                                     /*cryptographic_tag_size_in_bytes=*/49,
+                                     HmacParameters::HashType::kSha384,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big for kSha512;
+  EXPECT_THAT(HmacParameters::Create(/*key_size_in_bytes=*/32,
+                                     /*cryptographic_tag_size_in_bytes=*/65,
+                                     HmacParameters::HashType::kSha512,
+                                     HmacParameters::Variant::kNoPrefix)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(HmacParametersTest, CopyConstructor) {
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/12, HmacParameters::HashType::kSha256,
+      HmacParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  HmacParameters copy(*parameters);
+  EXPECT_THAT(copy.GetVariant(), Eq(parameters->GetVariant()));
+  EXPECT_THAT(copy.CryptographicTagSizeInBytes(),
+              Eq(parameters->CryptographicTagSizeInBytes()));
+  EXPECT_THAT(copy.TotalTagSizeInBytes(),
+              Eq(parameters->TotalTagSizeInBytes()));
+  EXPECT_THAT(copy.GetHashType(), Eq(parameters->GetHashType()));
+  EXPECT_THAT(copy.HasIdRequirement(), Eq(parameters->HasIdRequirement()));
+}
+
+TEST(HmacParametersTest, CopyAssignment) {
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/12, HmacParameters::HashType::kSha512,
+      HmacParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  HmacParameters copy = *parameters;
+  EXPECT_THAT(copy.GetVariant(), Eq(parameters->GetVariant()));
+  EXPECT_THAT(copy.CryptographicTagSizeInBytes(),
+              Eq(parameters->CryptographicTagSizeInBytes()));
+  EXPECT_THAT(copy.TotalTagSizeInBytes(),
+              Eq(parameters->TotalTagSizeInBytes()));
+  EXPECT_THAT(copy.GetHashType(), Eq(parameters->GetHashType()));
+  EXPECT_THAT(copy.HasIdRequirement(), Eq(parameters->HasIdRequirement()));
+}
+
+using HmacParametersVariantTest = TestWithParam<
+    std::tuple<int, int, HmacParameters::HashType, HmacParameters::Variant>>;
+
+INSTANTIATE_TEST_SUITE_P(HmacParametersVariantTestSuite,
+                         HmacParametersVariantTest,
+                         Combine(Range(16, 32), Range(10, 20),
+                                 Values(HmacParameters::HashType::kSha1,
+                                        HmacParameters::HashType::kSha224,
+                                        HmacParameters::HashType::kSha256,
+                                        HmacParameters::HashType::kSha384,
+                                        HmacParameters::HashType::kSha512),
+                                 Values(HmacParameters::Variant::kTink,
+                                        HmacParameters::Variant::kCrunchy,
+                                        HmacParameters::Variant::kLegacy,
+                                        HmacParameters::Variant::kNoPrefix)));
+
+TEST_P(HmacParametersVariantTest, ParametersEquals) {
+  int key_size;
+  int cryptographic_tag_size;
+  HmacParameters::HashType hash_type;
+  HmacParameters::Variant variant;
+  std::tie(key_size, cryptographic_tag_size, hash_type, variant) = GetParam();
+
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      key_size, cryptographic_tag_size, hash_type, variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<HmacParameters> other_parameters = HmacParameters::Create(
+      key_size, cryptographic_tag_size, hash_type, variant);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters == *other_parameters);
+  EXPECT_TRUE(*other_parameters == *parameters);
+  EXPECT_FALSE(*parameters != *other_parameters);
+  EXPECT_FALSE(*other_parameters != *parameters);
+}
+
+TEST(HmacParametersTest, KeySizeNotEqual) {
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/16,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha224,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<HmacParameters> other_parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha224,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(HmacParametersTest, HashTypeNotEqual) {
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha256,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<HmacParameters> other_parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha512,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(HmacParametersTest, TagSizeNotEqual) {
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha256,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<HmacParameters> other_parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/11, HmacParameters::HashType::kSha256,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(HmacParametersTest, VariantNotEqual) {
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha256,
+      HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<HmacParameters> other_parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      /*cryptographic_tag_size_in_bytes=*/10, HmacParameters::HashType::kSha256,
+      HmacParameters::Variant::kTink);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/hmac_proto_serialization.cc b/cc/mac/hmac_proto_serialization.cc
new file mode 100644
index 0000000..aaa6ced
--- /dev/null
+++ b/cc/mac/hmac_proto_serialization.cc
@@ -0,0 +1,305 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/hmac_proto_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/mac/hmac_key.h"
+#include "tink/mac/hmac_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/common.pb.h"
+#include "proto/hmac.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HmacKeyFormat;
+using ::google::crypto::tink::HmacParams;
+using ::google::crypto::tink::OutputPrefixType;
+
+using HmacProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   HmacParameters>;
+using HmacProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<HmacParameters,
+                                       internal::ProtoParametersSerialization>;
+using HmacProtoKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, HmacKey>;
+using HmacProtoKeySerializerImpl =
+    internal::KeySerializerImpl<HmacKey, internal::ProtoKeySerialization>;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.HmacKey";
+
+util::StatusOr<HmacParameters::Variant> ToVariant(
+    OutputPrefixType output_prefix_type) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::CRUNCHY:
+      return HmacParameters::Variant::kCrunchy;
+    case OutputPrefixType::LEGACY:
+      return HmacParameters::Variant::kLegacy;
+    case OutputPrefixType::RAW:
+      return HmacParameters::Variant::kNoPrefix;
+    case OutputPrefixType::TINK:
+      return HmacParameters::Variant::kTink;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine HmacParameters::Variant");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    HmacParameters::Variant variant) {
+  switch (variant) {
+    case HmacParameters::Variant::kCrunchy:
+      return OutputPrefixType::CRUNCHY;
+    case HmacParameters::Variant::kLegacy:
+      return OutputPrefixType::LEGACY;
+    case HmacParameters::Variant::kNoPrefix:
+      return OutputPrefixType::RAW;
+    case HmacParameters::Variant::kTink:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine output prefix type");
+  }
+}
+
+util::StatusOr<HmacParameters::HashType> ToHashType(HashType hash_type) {
+  switch (hash_type) {
+    case HashType::SHA1:
+      return HmacParameters::HashType::kSha1;
+    case HashType::SHA224:
+      return HmacParameters::HashType::kSha224;
+    case HashType::SHA256:
+      return HmacParameters::HashType::kSha256;
+    case HashType::SHA384:
+      return HmacParameters::HashType::kSha384;
+    case HashType::SHA512:
+      return HmacParameters::HashType::kSha512;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine HashType");
+  }
+}
+
+util::StatusOr<HashType> ToProtoHashType(HmacParameters::HashType hash_type) {
+  switch (hash_type) {
+    case HmacParameters::HashType::kSha1:
+      return HashType::SHA1;
+    case HmacParameters::HashType::kSha224:
+      return HashType::SHA224;
+    case HmacParameters::HashType::kSha256:
+      return HashType::SHA256;
+    case HmacParameters::HashType::kSha384:
+      return HashType::SHA384;
+    case HmacParameters::HashType::kSha512:
+      return HashType::SHA512;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine HmacParameters::HashType");
+  }
+}
+
+util::StatusOr<HmacParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing HmacParameters.");
+  }
+
+  HmacKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse HmacKeyFormat proto");
+  }
+  if (proto_key_format.version() != 0) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Parsing HmacParameters failed: only version 0 is accepted");
+  }
+
+  util::StatusOr<HmacParameters::Variant> variant =
+      ToVariant(serialization.GetKeyTemplate().output_prefix_type());
+  if (!variant.ok()) return variant.status();
+
+  util::StatusOr<HmacParameters::HashType> hash_type =
+      ToHashType(proto_key_format.params().hash());
+  if (!hash_type.ok()) return variant.status();
+
+  return HmacParameters::Create(proto_key_format.key_size(),
+                                proto_key_format.params().tag_size(),
+                                *hash_type, *variant);
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const HmacParameters& parameters) {
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+  util::StatusOr<HashType> proto_hash_type =
+      ToProtoHashType(parameters.GetHashType());
+  if (!proto_hash_type.ok()) return proto_hash_type.status();
+
+  HmacParams proto_params;
+  proto_params.set_tag_size(parameters.CryptographicTagSizeInBytes());
+  proto_params.set_hash(*proto_hash_type);
+  HmacKeyFormat proto_key_format;
+  proto_key_format.set_key_size(parameters.KeySizeInBytes());
+  proto_key_format.set_version(0);
+  *proto_key_format.mutable_params() = proto_params;
+
+  return internal::ProtoParametersSerialization::Create(
+      kTypeUrl, *output_prefix_type, proto_key_format.SerializeAsString());
+}
+
+util::StatusOr<HmacKey> ParseKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing HmacKey.");
+  }
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+
+  google::crypto::tink::HmacKey proto_key;
+  RestrictedData restricted_data = serialization.SerializedKeyProto();
+  // OSS proto library complains if input is not converted to a string.
+  if (!proto_key.ParseFromString(
+          std::string(restricted_data.GetSecret(*token)))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse HmacKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<HmacParameters::Variant> variant =
+      ToVariant(serialization.GetOutputPrefixType());
+  if (!variant.ok()) return variant.status();
+  util::StatusOr<HmacParameters::HashType> hash_type =
+      ToHashType(proto_key.params().hash());
+  if (!hash_type.ok()) return variant.status();
+
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      proto_key.key_value().length(), proto_key.params().tag_size(), *hash_type,
+      *variant);
+  if (!parameters.ok()) return parameters.status();
+
+  return HmacKey::Create(*parameters,
+                         RestrictedData(proto_key.key_value(), *token),
+                         serialization.IdRequirement(), GetPartialKeyAccess());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializeKey(
+    const HmacKey& key, absl::optional<SecretKeyAccessToken> token) {
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetKeyBytes(GetPartialKeyAccess());
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+  if (!restricted_input.ok()) return restricted_input.status();
+  util::StatusOr<HashType> proto_hash_type =
+      ToProtoHashType(key.GetParameters().GetHashType());
+  if (!proto_hash_type.ok()) return proto_hash_type.status();
+
+  HmacParams proto_params;
+  proto_params.set_tag_size(key.GetParameters().CryptographicTagSizeInBytes());
+  proto_params.set_hash(*proto_hash_type);
+  google::crypto::tink::HmacKey proto_key;
+  *proto_key.mutable_params() = proto_params;
+  proto_key.set_version(0);
+  // OSS proto library complains if input is not converted to a string.
+  proto_key.set_key_value(std::string(restricted_input->GetSecret(*token)));
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetParameters().GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  RestrictedData restricted_output =
+      RestrictedData(proto_key.SerializeAsString(), *token);
+  return internal::ProtoKeySerialization::Create(
+      kTypeUrl, restricted_output, google::crypto::tink::KeyData::SYMMETRIC,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+HmacProtoParametersParserImpl* HmacProtoParametersParser() {
+  static auto* parser =
+      new HmacProtoParametersParserImpl(kTypeUrl, ParseParameters);
+  return parser;
+}
+
+HmacProtoParametersSerializerImpl* HmacProtoParametersSerializer() {
+  static auto* serializer =
+      new HmacProtoParametersSerializerImpl(kTypeUrl, SerializeParameters);
+  return serializer;
+}
+
+HmacProtoKeyParserImpl* HmacProtoKeyParser() {
+  static auto* parser = new HmacProtoKeyParserImpl(kTypeUrl, ParseKey);
+  return parser;
+}
+
+HmacProtoKeySerializerImpl* HmacProtoKeySerializer() {
+  static auto* serializer = new HmacProtoKeySerializerImpl(SerializeKey);
+  return serializer;
+}
+
+}  // namespace
+
+util::Status RegisterHmacProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(HmacProtoParametersParser());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterParametersSerializer(HmacProtoParametersSerializer());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(HmacProtoKeyParser());
+  if (!status.ok()) return status;
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(HmacProtoKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/hmac_proto_serialization.h b/cc/mac/hmac_proto_serialization.h
new file mode 100644
index 0000000..2996827
--- /dev/null
+++ b/cc/mac/hmac_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_MAC_HMAC_PROTO_SERIALIZATION_H_
+#define TINK_MAC_HMAC_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for HMAC parameters and keys.
+crypto::tink::util::Status RegisterHmacProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_MAC_HMAC_PROTO_SERIALIZATION_H_
diff --git a/cc/mac/hmac_proto_serialization_test.cc b/cc/mac/hmac_proto_serialization_test.cc
new file mode 100644
index 0000000..4bcc684
--- /dev/null
+++ b/cc/mac/hmac_proto_serialization_test.cc
@@ -0,0 +1,409 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/mac/hmac_proto_serialization.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/mac/hmac_key.h"
+#include "tink/mac/hmac_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
+#include "proto/common.pb.h"
+#include "proto/hmac.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::subtle::Random;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HmacKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  HmacParameters::Variant variant;
+  OutputPrefixType output_prefix_type;
+  HmacParameters::HashType hash_type;
+  HashType proto_hash_type;
+  int key_size;
+  int tag_size;
+  int total_size;
+  absl::optional<int> id;
+  std::string output_prefix;
+};
+
+class HmacProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  void SetUp() override {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    HmacProtoSerializationTestSuite, HmacProtoSerializationTest,
+    Values(TestCase{HmacParameters::Variant::kTink, OutputPrefixType::TINK,
+                    HmacParameters::HashType::kSha1, HashType::SHA1,
+                    /*key_size=*/16, /*cryptographic_tag_size=*/10,
+                    /*total_size=*/15, /*id=*/0x02030400,
+                    /*output_prefix=*/std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{HmacParameters::Variant::kCrunchy,
+                    OutputPrefixType::CRUNCHY,
+                    HmacParameters::HashType::kSha224, HashType::SHA224,
+                    /*key_size=*/16, /*tag_size=*/12, /*total_size=*/17,
+                    /*id=*/0x01030005,
+                    /*output_prefix=*/std::string("\x00\x01\x03\x00\x05", 5)},
+           TestCase{HmacParameters::Variant::kLegacy, OutputPrefixType::LEGACY,
+                    HmacParameters::HashType::kSha256, HashType::SHA256,
+                    /*key_size=*/32, /*cryptographic_tag_size=*/14,
+                    /*total_tag_size=*/19, /*id=*/0x01020304,
+                    /*output_prefix=*/std::string("\x00\x01\x02\x03\x04", 5)},
+           TestCase{HmacParameters::Variant::kNoPrefix, OutputPrefixType::RAW,
+                    HmacParameters::HashType::kSha384, HashType::SHA384,
+                    /*key_size=*/32, /*cryptographic_tag_size=*/16,
+                    /*total_tag_size=*/16, /*id=*/absl::nullopt,
+                    /*output_prefix=*/""},
+           TestCase{HmacParameters::Variant::kNoPrefix, OutputPrefixType::RAW,
+                    HmacParameters::HashType::kSha512, HashType::SHA512,
+                    /*key_size=*/32, /*cryptographic_tag_size=*/20,
+                    /*total_tag_size=*/20, /*id=*/absl::nullopt,
+                    /*output_prefix=*/""}));
+
+TEST_P(HmacProtoSerializationTest, ParseParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  HmacKeyFormat key_format_proto;
+  key_format_proto.set_key_size(test_case.key_size);
+  key_format_proto.mutable_params()->set_tag_size(test_case.tag_size);
+  key_format_proto.mutable_params()->set_hash(test_case.proto_hash_type);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey",
+          test_case.output_prefix_type, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(parsed_parameters, IsOk());
+  EXPECT_THAT((*parsed_parameters)->HasIdRequirement(),
+              test_case.id.has_value());
+
+  util::StatusOr<HmacParameters> expected_parameters =
+      HmacParameters::Create(test_case.key_size, test_case.tag_size,
+                             test_case.hash_type, test_case.variant);
+  ASSERT_THAT(expected_parameters, IsOk());
+  ASSERT_THAT(**parsed_parameters, Eq(*expected_parameters));
+}
+
+TEST_F(HmacProtoSerializationTest, ParseParametersWithInvalidSerialization) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey",
+          OutputPrefixType::RAW, "invalid_serialization");
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(HmacProtoSerializationTest, ParseParametersWithInvalidVersion) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  HmacKeyFormat key_format_proto;
+  key_format_proto.set_key_size(16);
+  key_format_proto.set_version(1);  // Invalid version.
+  key_format_proto.mutable_params()->set_tag_size(10);
+  key_format_proto.mutable_params()->set_hash(HashType::SHA256);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey",
+          OutputPrefixType::RAW, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(HmacProtoSerializationTest, ParseParametersWithUnkownOutputPrefix) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  HmacKeyFormat key_format_proto;
+  key_format_proto.set_key_size(16);
+  key_format_proto.mutable_params()->set_tag_size(10);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey",
+          OutputPrefixType::UNKNOWN_PREFIX,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(HmacProtoSerializationTest, SerializeParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<HmacParameters> parameters =
+      HmacParameters::Create(test_case.key_size, test_case.tag_size,
+                             test_case.hash_type, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.HmacKey"));
+
+  const internal::ProtoParametersSerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoParametersSerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().type_url(),
+              Eq("type.googleapis.com/google.crypto.tink.HmacKey"));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  HmacKeyFormat key_format;
+  ASSERT_THAT(
+      key_format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  ASSERT_THAT(key_format.key_size(), Eq(test_case.key_size));
+  ASSERT_THAT(key_format.params().tag_size(), Eq(test_case.tag_size));
+  ASSERT_THAT(key_format.params().hash(), Eq(test_case.proto_hash_type));
+}
+
+TEST_P(HmacProtoSerializationTest, ParseKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  google::crypto::tink::HmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(test_case.tag_size);
+  key_proto.mutable_params()->set_hash(test_case.proto_hash_type);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey", serialized_key,
+          KeyData::SYMMETRIC, test_case.output_prefix_type, test_case.id);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key, IsOk());
+  EXPECT_THAT((*parsed_key)->GetParameters().HasIdRequirement(),
+              test_case.id.has_value());
+  EXPECT_THAT((*parsed_key)->GetIdRequirement(), Eq(test_case.id));
+
+  util::StatusOr<HmacParameters> expected_parameters =
+      HmacParameters::Create(test_case.key_size, test_case.tag_size,
+                             test_case.hash_type, test_case.variant);
+  ASSERT_THAT(expected_parameters, IsOk());
+  util::StatusOr<HmacKey> expected_key = HmacKey::Create(
+      *expected_parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+
+  ASSERT_THAT(expected_key, IsOk());
+  ASSERT_THAT(**parsed_key, Eq(*expected_key));
+}
+
+TEST_F(HmacProtoSerializationTest, ParseKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::HmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(10);
+  key_proto.mutable_params()->set_hash(HashType::SHA256);
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(HmacProtoSerializationTest, ParseKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::HmacKey key_proto;
+  key_proto.set_version(1);  // Invalid version number.
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(10);
+  key_proto.mutable_params()->set_hash(HashType::SHA256);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(HmacProtoSerializationTest, ParseKeyWithoutSecretKeyAccess) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::HmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_params()->set_tag_size(10);
+  key_proto.mutable_params()->set_hash(HashType::SHA256);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, absl::nullopt);
+  ASSERT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(HmacProtoSerializationTest, SerializeKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<HmacParameters> parameters =
+      HmacParameters::Create(test_case.key_size, test_case.tag_size,
+                             test_case.hash_type, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.HmacKey"));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(),
+              Eq("type.googleapis.com/google.crypto.tink.HmacKey"));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(), Eq(test_case.id));
+
+  google::crypto::tink::HmacKey proto_key;
+  // OSS proto library complains if input is not converted to a string.
+  ASSERT_THAT(proto_key.ParseFromString(std::string(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get()))),
+              IsTrue());
+  EXPECT_THAT(proto_key.key_value().size(), Eq(test_case.key_size));
+  EXPECT_THAT(proto_key.params().tag_size(), Eq(test_case.tag_size));
+  EXPECT_THAT(proto_key.params().hash(), Eq(test_case.proto_hash_type));
+}
+
+TEST_F(HmacProtoSerializationTest, SerializeKeyWithoutSecretKeyAccess) {
+  ASSERT_THAT(RegisterHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/16, /*cryptographic_tag_size_in_bytes=*/10,
+      HmacParameters::HashType::kSha256, HmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  util::StatusOr<HmacKey> key = HmacKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(*key, absl::nullopt);
+  ASSERT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/mac/internal/chunked_mac_impl.h b/cc/mac/internal/chunked_mac_impl.h
index 940cf47..6a5d0a8 100644
--- a/cc/mac/internal/chunked_mac_impl.h
+++ b/cc/mac/internal/chunked_mac_impl.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_CHUNKEDMAC_INTERNAL_CHUNKED_MAC_IMPL_H_
-#define TINK_CHUNKEDMAC_INTERNAL_CHUNKED_MAC_IMPL_H_
+#ifndef TINK_MAC_INTERNAL_CHUNKED_MAC_IMPL_H_
+#define TINK_MAC_INTERNAL_CHUNKED_MAC_IMPL_H_
 
 #include <memory>
 #include <string>
@@ -92,4 +92,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_CHUNKEDMAC_INTERNAL_CHUNKED_MAC_IMPL_H_
+#endif  // TINK_MAC_INTERNAL_CHUNKED_MAC_IMPL_H_
diff --git a/cc/mac/internal/chunked_mac_wrapper.cc b/cc/mac/internal/chunked_mac_wrapper.cc
index 5988daa..c95853c 100644
--- a/cc/mac/internal/chunked_mac_wrapper.cc
+++ b/cc/mac/internal/chunked_mac_wrapper.cc
@@ -155,7 +155,7 @@
   util::StatusOr<std::unique_ptr<ChunkedMacVerification>> CreateVerification(
       absl::string_view tag) const override;
 
-  ~ChunkedMacSetWrapper() override {}
+  ~ChunkedMacSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<ChunkedMac>> mac_set_;
diff --git a/cc/mac/internal/chunked_mac_wrapper.h b/cc/mac/internal/chunked_mac_wrapper.h
index 2e8e0dd..ad86442 100644
--- a/cc/mac/internal/chunked_mac_wrapper.h
+++ b/cc/mac/internal/chunked_mac_wrapper.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_CHUNKEDMAC_INTERNAL_CHUNKED_MAC_WRAPPER_H_
-#define TINK_CHUNKEDMAC_INTERNAL_CHUNKED_MAC_WRAPPER_H_
+#ifndef TINK_MAC_INTERNAL_CHUNKED_MAC_WRAPPER_H_
+#define TINK_MAC_INTERNAL_CHUNKED_MAC_WRAPPER_H_
 
 #include <memory>
 
@@ -47,4 +47,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_CHUNKEDMAC_INTERNAL_CHUNKED_MAC_WRAPPER_H_
+#endif  // TINK_MAC_INTERNAL_CHUNKED_MAC_WRAPPER_H_
diff --git a/cc/mac/mac_config.cc b/cc/mac/mac_config.cc
index 6b87fd3..5f9997a 100644
--- a/cc/mac/mac_config.cc
+++ b/cc/mac/mac_config.cc
@@ -20,7 +20,9 @@
 #include "tink/config/config_util.h"
 #include "tink/config/tink_fips.h"
 #include "tink/mac/aes_cmac_key_manager.h"
+#include "tink/mac/aes_cmac_proto_serialization.h"
 #include "tink/mac/hmac_key_manager.h"
+#include "tink/mac/hmac_proto_serialization.h"
 #include "tink/mac/internal/chunked_mac_wrapper.h"
 #include "tink/mac/mac_wrapper.h"
 #include "tink/registry.h"
@@ -33,12 +35,6 @@
 namespace tink {
 
 // static
-const RegistryConfig& MacConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
-// static
 util::Status MacConfig::Register() {
   // Register primitive wrappers.
   auto status =
@@ -55,6 +51,9 @@
                                             true);
   if (!status.ok()) return status;
 
+  status = RegisterHmacProtoSerialization();
+  if (!status.ok()) return status;
+
   if (IsFipsModeEnabled()) {
     return util::OkStatus();
   }
@@ -64,6 +63,9 @@
       absl::make_unique<AesCmacKeyManager>(), true);
   if (!status.ok()) return status;
 
+  status = RegisterAesCmacProtoSerialization();
+  if (!status.ok()) return status;
+
   return util::OkStatus();
 }
 
diff --git a/cc/mac/mac_config.h b/cc/mac/mac_config.h
index 7213e61..1e482b0 100644
--- a/cc/mac/mac_config.h
+++ b/cc/mac/mac_config.h
@@ -34,14 +34,6 @@
 //
 class MacConfig {
  public:
-  static constexpr char kCatalogueName[] = "TinkMac";
-  static constexpr char kPrimitiveName[] = "Mac";
-
-  // Returns config of Mac implementations supported
-  // in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers Mac primitive wrapper and key managers for all Mac key types
   // from the current Tink release.
   static crypto::tink::util::Status Register();
diff --git a/cc/mac/mac_config_test.cc b/cc/mac/mac_config_test.cc
index 6c63278..4f002f7 100644
--- a/cc/mac/mac_config_test.cc
+++ b/cc/mac/mac_config_test.cc
@@ -24,17 +24,27 @@
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "tink/chunked_mac.h"
-#include "tink/config.h"
-#include "tink/config/tink_fips.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
 #include "tink/keyset_handle.h"
 #include "tink/mac.h"
+#include "tink/mac/aes_cmac_key.h"
 #include "tink/mac/aes_cmac_key_manager.h"
+#include "tink/mac/aes_cmac_parameters.h"
+#include "tink/mac/hmac_key.h"
 #include "tink/mac/hmac_key_manager.h"
+#include "tink/mac/hmac_parameters.h"
 #include "tink/mac/mac_key_templates.h"
+#include "tink/partial_key_access.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
+#include "proto/common.pb.h"
+#include "proto/tink.pb.h"
 
 namespace crypto {
 namespace tink {
@@ -43,8 +53,10 @@
 using ::crypto::tink::test::DummyMac;
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
 using ::google::crypto::tink::KeysetInfo;
 using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::HashType;
 using ::google::crypto::tink::KeyTemplate;
 using ::google::crypto::tink::OutputPrefixType;
 using ::testing::Values;
@@ -53,11 +65,12 @@
  protected:
   void SetUp() override {
     Registry::Reset();
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
   }
 };
 
 TEST_F(MacConfigTest, Basic) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -97,7 +110,7 @@
 // Tests that the MacWrapper has been properly registered and we can wrap
 // primitives.
 TEST_F(MacConfigTest, MacWrappersRegistered) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -128,6 +141,201 @@
       DummyMac("dummy").VerifyMac(mac_result.value(), "faked text").ok());
 }
 
+TEST_F(MacConfigTest, AesCmacProtoParamsSerializationRegistered) {
+  if (internal::IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  util::StatusOr<internal::ProtoParametersSerialization>
+      proto_params_serialization =
+          internal::ProtoParametersSerialization::Create(
+              MacKeyTemplates::AesCmac());
+  ASSERT_THAT(proto_params_serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *proto_params_serialization);
+  ASSERT_THAT(parsed_params.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_params =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(*params);
+  ASSERT_THAT(serialized_params.status(),
+              StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(MacConfig::Register(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_params2 =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *proto_params_serialization);
+  ASSERT_THAT(parsed_params2, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_params2 =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(*params);
+  ASSERT_THAT(serialized_params2, IsOk());
+}
+
+TEST_F(MacConfigTest, AesCmacProtoKeySerializationRegistered) {
+  if (internal::IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  google::crypto::tink::AesCmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(subtle::Random::GetRandomBytes(32));
+  key_proto.mutable_params()->set_tag_size(16);
+
+  util::StatusOr<internal::ProtoKeySerialization> proto_key_serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesCmacKey",
+          RestrictedData(key_proto.SerializeAsString(),
+                         InsecureSecretKeyAccess::Get()),
+          KeyData::SYMMETRIC, OutputPrefixType::TINK, /*id_requirement=*/123);
+  ASSERT_THAT(proto_key_serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *proto_key_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  util::StatusOr<AesCmacKey> key =
+      AesCmacKey::Create(*params,
+                         RestrictedData(subtle::Random::GetRandomBytes(32),
+                                        InsecureSecretKeyAccess::Get()),
+                         /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_key =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialized_key.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  ASSERT_THAT(MacConfig::Register(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key2 =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *proto_key_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key2, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_key2 =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialized_key2, IsOk());
+}
+
+TEST_F(MacConfigTest, HmacProtoParamsSerializationRegistered) {
+  if (internal::IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  util::StatusOr<internal::ProtoParametersSerialization>
+      proto_params_serialization =
+          internal::ProtoParametersSerialization::Create(
+              MacKeyTemplates::HmacSha256());
+  ASSERT_THAT(proto_params_serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *proto_params_serialization);
+  ASSERT_THAT(parsed_params.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/32,
+      HmacParameters::HashType::kSha256, HmacParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_parameters =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialized_parameters.status(),
+              StatusIs(absl::StatusCode::kNotFound));
+
+  // Register parser and serializer.
+  ASSERT_THAT(MacConfig::Register(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed_params2 =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *proto_params_serialization);
+  ASSERT_THAT(parsed_params2, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_params2 =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialized_params2, IsOk());
+}
+
+TEST_F(MacConfigTest, HmacProtoKeySerializationRegistered) {
+  if (internal::IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  google::crypto::tink::HmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(subtle::Random::GetRandomBytes(32));
+  key_proto.mutable_params()->set_tag_size(32);
+  key_proto.mutable_params()->set_hash(HashType::SHA256);
+
+  util::StatusOr<internal::ProtoKeySerialization> proto_key_serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.HmacKey",
+          RestrictedData(key_proto.SerializeAsString(),
+                         InsecureSecretKeyAccess::Get()),
+          KeyData::SYMMETRIC, OutputPrefixType::TINK, /*id_requirement=*/123);
+  ASSERT_THAT(proto_key_serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *proto_key_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  util::StatusOr<HmacParameters> parameters = HmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/32,
+      HmacParameters::HashType::kSha256, HmacParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<HmacKey> key =
+      HmacKey::Create(*parameters,
+                      RestrictedData(subtle::Random::GetRandomBytes(32),
+                                     InsecureSecretKeyAccess::Get()),
+                      /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_key =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialized_key.status(), StatusIs(absl::StatusCode::kNotFound));
+
+  // Register parser and serializer.
+  ASSERT_THAT(MacConfig::Register(), IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> parsed_key2 =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *proto_key_serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_key2, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialized_key2 =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialized_key2, IsOk());
+}
+
 class ChunkedMacConfigTest : public ::testing::TestWithParam<KeyTemplate> {
  protected:
   void SetUp() override { Registry::Reset(); }
@@ -140,7 +348,7 @@
 // Tests that the ChunkedMacWrapper has been properly registered and we can get
 // primitives.
 TEST_P(ChunkedMacConfigTest, ChunkedMacWrappersRegistered) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -172,7 +380,7 @@
 
 // FIPS-only mode tests
 TEST_F(MacConfigTest, RegisterNonFipsTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode";
   }
 
@@ -188,7 +396,7 @@
 }
 
 TEST_F(MacConfigTest, RegisterFipsValidTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode";
   }
 
diff --git a/cc/mac/mac_factory.cc b/cc/mac/mac_factory.cc
index 954ac82..6f4a420 100644
--- a/cc/mac/mac_factory.cc
+++ b/cc/mac/mac_factory.cc
@@ -16,13 +16,14 @@
 
 #include "tink/mac/mac_factory.h"
 
+#include <memory>
+
 #include "tink/mac.h"
-#include "tink/registry.h"
 #include "tink/mac/mac_wrapper.h"
+#include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
-
 namespace crypto {
 namespace tink {
 
diff --git a/cc/mac/mac_factory.h b/cc/mac/mac_factory.h
index 1fc427b..bf30bcb 100644
--- a/cc/mac/mac_factory.h
+++ b/cc/mac/mac_factory.h
@@ -17,6 +17,8 @@
 #ifndef TINK_MAC_MAC_FACTORY_H_
 #define TINK_MAC_MAC_FACTORY_H_
 
+#include <memory>
+
 #include "absl/base/macros.h"
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
diff --git a/cc/mac/mac_factory_test.cc b/cc/mac/mac_factory_test.cc
index 7e70f5c..27b5d6c 100644
--- a/cc/mac/mac_factory_test.cc
+++ b/cc/mac/mac_factory_test.cc
@@ -33,7 +33,6 @@
 #include "proto/hmac.pb.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddRawKey;
 using crypto::tink::test::AddTinkKey;
 using google::crypto::tink::HashType;
diff --git a/cc/mac/mac_key.h b/cc/mac/mac_key.h
index 5e14e5f..d02933c 100644
--- a/cc/mac/mac_key.h
+++ b/cc/mac/mac_key.h
@@ -40,7 +40,7 @@
   // may be a prefix of another). To avoid this, built-in Tink keys use the
   // convention that the prefix is either '0x00<big endian key id>' or
   // '0x01<big endian key id>'.
-  virtual util::StatusOr<std::string> GetOutputPrefix() const = 0;
+  virtual absl::string_view GetOutputPrefix() const = 0;
 
   const MacParameters& GetParameters() const override = 0;
 
diff --git a/cc/mac/mac_wrapper.cc b/cc/mac/mac_wrapper.cc
index f1140bd..039b332 100644
--- a/cc/mac/mac_wrapper.cc
+++ b/cc/mac/mac_wrapper.cc
@@ -16,18 +16,19 @@
 
 #include "tink/mac/mac_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "tink/crypto_format.h"
-#include "tink/internal/util.h"
-#include "tink/mac.h"
-#include "tink/primitive_set.h"
 #include "tink/internal/monitoring_util.h"
 #include "tink/internal/registry_impl.h"
+#include "tink/internal/util.h"
+#include "tink/mac.h"
 #include "tink/monitoring/monitoring.h"
+#include "tink/primitive_set.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
@@ -59,7 +60,7 @@
   crypto::tink::util::Status VerifyMac(absl::string_view mac_value,
                                        absl::string_view data) const override;
 
-  ~MacSetWrapper() override {}
+  ~MacSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<Mac>> mac_set_;
diff --git a/cc/mac/mac_wrapper.h b/cc/mac/mac_wrapper.h
index c7379dc..3876544 100644
--- a/cc/mac/mac_wrapper.h
+++ b/cc/mac/mac_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_MAC_MAC_WRAPPER_H_
 #define TINK_MAC_MAC_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/mac.h"
 #include "tink/primitive_set.h"
diff --git a/cc/mac/mac_wrapper_test.cc b/cc/mac/mac_wrapper_test.cc
index 66069f1..d240e50 100644
--- a/cc/mac/mac_wrapper_test.cc
+++ b/cc/mac/mac_wrapper_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/mac/mac_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/monitoring/BUILD.bazel b/cc/monitoring/BUILD.bazel
index 22d3886..63fc32f 100644
--- a/cc/monitoring/BUILD.bazel
+++ b/cc/monitoring/BUILD.bazel
@@ -7,6 +7,8 @@
     hdrs = ["monitoring.h"],
     include_prefix = "tink/monitoring",
     deps = [
+        "//:key_status",
+        "//internal:key_status_util",
         "//util:statusor",
         "@com_google_absl//absl/container:flat_hash_map",
     ],
diff --git a/cc/monitoring/BUILD.gn b/cc/monitoring/BUILD.gn
index b7ce0fc..eae3e72 100644
--- a/cc/monitoring/BUILD.gn
+++ b/cc/monitoring/BUILD.gn
@@ -12,6 +12,8 @@
   sources = [ "monitoring.h" ]
   public_deps = [
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
+    "//third_party/tink/cc:key_status",
+    "//third_party/tink/cc/internal:key_status_util",
     "//third_party/tink/cc/util:statusor",
   ]
   public_configs = [ "//third_party/tink:tink_config" ]
diff --git a/cc/monitoring/CMakeLists.txt b/cc/monitoring/CMakeLists.txt
index 9613885..60ad02c 100644
--- a/cc/monitoring/CMakeLists.txt
+++ b/cc/monitoring/CMakeLists.txt
@@ -6,6 +6,8 @@
     monitoring.h
   DEPS
     absl::flat_hash_map
+    tink::core::key_status
+    tink::internal::key_status_util
     tink::util::statusor
 )
 
@@ -16,4 +18,5 @@
   DEPS
     tink::monitoring::monitoring
     gmock
+  TESTONLY
 )
diff --git a/cc/monitoring/monitoring.h b/cc/monitoring/monitoring.h
index e25e99a..4c4dcc5 100644
--- a/cc/monitoring/monitoring.h
+++ b/cc/monitoring/monitoring.h
@@ -17,10 +17,13 @@
 #define TINK_MONITORING_MONITORING_H_
 
 #include <cstdint>
+#include <memory>
 #include <string>
 #include <vector>
 
 #include "absl/container/flat_hash_map.h"
+#include "tink/internal/key_status_util.h"
+#include "tink/key_status.h"
 #include "tink/util/statusor.h"
 
 namespace crypto {
@@ -33,41 +36,32 @@
   // Description about each entry of the KeySet.
   class Entry {
    public:
-    // Enum representation of KeyStatusType in tink/proto/tink.proto. Using an
-    // enum class prevents unintentional implicit conversions.
-    enum class KeyStatus : int {
-      kEnabled = 1,    // Can be used for cryptographic operations.
-      kDisabled = 2,   // Cannot be used (but can become kEnabled again).
-      kDestroyed = 3,  // Key data does not exist in this Keyset any more.
-      // Added to guard from failures that may be caused by future expansions.
-      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
-    };
-
-    // Constructs a new KeySet entry with a given `status`, `key_id` and key
-    // format `parameters_as_string`.
-    Entry(KeyStatus status, uint32_t key_id,
-          absl::string_view parameters_as_string)
+    // Constructs a new KeySet entry with a given `status`, `key_id`,
+    // `key_type`, and `key_prefix`.
+    Entry(KeyStatus status, uint32_t key_id, absl::string_view key_type,
+          absl::string_view key_prefix)
         : status_(status),
           key_id_(key_id),
-          parameters_as_string_(parameters_as_string) {}
+          key_type_(key_type),
+          key_prefix_(key_prefix) {}
 
     // Returns the status of this entry.
-    KeyStatus GetStatus() const { return status_; }
+    std::string GetStatus() const { return internal::ToKeyStatusName(status_); }
     // Returns the ID of the entry within the keyset.
     uint32_t GetKeyId() const { return key_id_; }
-    // Returns the parameters in a serialized form.
-    //
-    // *WARNING* the actual content of `parameters_as_string_` is considered
-    // unstable and might change in future versions of Tink. A user should not
-    // rely on a specific representation of the key_format.
-    std::string GetParametersAsString() const { return parameters_as_string_; }
+    // Returns the key type.
+    std::string GetKeyType() const { return key_type_; }
+    // Returns the key prefix.
+    std::string GetKeyPrefix() const { return key_prefix_; }
 
    private:
     const KeyStatus status_;
     // Identifies a key within a keyset.
     const uint32_t key_id_;
-    // This field stores information about the parameters.
-    const std::string parameters_as_string_;
+    // This field stores the key type.
+    const std::string key_type_;
+    // Stores the key output prefix.
+    const std::string key_prefix_;
   };
 
   // Constructs a MonitoringKeySetInfo object with the given
diff --git a/cc/monitoring/monitoring_client_mocks.h b/cc/monitoring/monitoring_client_mocks.h
index 8a59b47..34b5cbd 100644
--- a/cc/monitoring/monitoring_client_mocks.h
+++ b/cc/monitoring/monitoring_client_mocks.h
@@ -13,8 +13,8 @@
 // limitations under the License.
 //
 ///////////////////////////////////////////////////////////////////////////////
-#ifndef TINK_MONITORING_MOCK_MONITORING_CLIENT_FACTORY_H_
-#define TINK_MONITORING_MOCK_MONITORING_CLIENT_FACTORY_H_
+#ifndef TINK_MONITORING_MONITORING_CLIENT_MOCKS_H_
+#define TINK_MONITORING_MONITORING_CLIENT_MOCKS_H_
 
 #include <cstdint>
 
@@ -42,4 +42,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_MONITORING_MOCK_MONITORING_CLIENT_FACTORY_H_
+#endif  // TINK_MONITORING_MONITORING_CLIENT_MOCKS_H_
diff --git a/cc/output_stream.h b/cc/output_stream.h
index c7de9b3..969e153 100644
--- a/cc/output_stream.h
+++ b/cc/output_stream.h
@@ -27,8 +27,8 @@
 // Protocol Buffers' google::protobuf::io::ZeroCopyOutputStream.
 class OutputStream {
  public:
-  OutputStream() {}
-  virtual ~OutputStream() {}
+  OutputStream() = default;
+  virtual ~OutputStream() = default;
 
   // Obtains a buffer into which data can be written.  Any data written
   // into this buffer will eventually (maybe instantly, maybe later on)
diff --git a/cc/output_stream_with_result.h b/cc/output_stream_with_result.h
index 882a075..ee7759f 100644
--- a/cc/output_stream_with_result.h
+++ b/cc/output_stream_with_result.h
@@ -59,7 +59,7 @@
 class OutputStreamWithResult : public OutputStream {
  public:
   OutputStreamWithResult() : closed_(false) {}
-  ~OutputStreamWithResult() override {}
+  ~OutputStreamWithResult() override = default;
 
   // The return type is StatusOr<T> if T != Status, and Status otherwise.
   using ResultType =
diff --git a/cc/partial_key_access.h b/cc/partial_key_access.h
new file mode 100644
index 0000000..f029faf
--- /dev/null
+++ b/cc/partial_key_access.h
@@ -0,0 +1,41 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_PARTIAL_KEY_ACCESS_H_
+#define TINK_PARTIAL_KEY_ACCESS_H_
+
+#include "tink/partial_key_access_token.h"
+
+namespace crypto {
+namespace tink {
+
+// Returns a `PartialKeyAccessToken`.
+//
+// Accessing parts of keys can produce unexpected incompatibilities:
+// https://developers.google.com/tink/design/access_control#accessing_partial_keys
+//
+// This function can be used to access partial key material. Within Google,
+// access to this function is restricted by the build system. Outside of Google,
+// users can search their codebase for `GetPartialKeyAccess()` to find
+// instances where it is used.
+inline PartialKeyAccessToken GetPartialKeyAccess() {
+  return PartialKeyAccessToken();
+}
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_PARTIAL_KEY_ACCESS_H_
diff --git a/cc/partial_key_access_token.h b/cc/partial_key_access_token.h
new file mode 100644
index 0000000..fe5a180
--- /dev/null
+++ b/cc/partial_key_access_token.h
@@ -0,0 +1,42 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_PARTIAL_KEY_ACCESS_TOKEN_H_
+#define TINK_PARTIAL_KEY_ACCESS_TOKEN_H_
+
+namespace crypto {
+namespace tink {
+
+class PartialKeyAccessToken {
+ public:
+  // Copyable and movable.
+  PartialKeyAccessToken(const PartialKeyAccessToken& other) = default;
+  PartialKeyAccessToken& operator=(const PartialKeyAccessToken& other) =
+      default;
+  PartialKeyAccessToken(PartialKeyAccessToken&& other) = default;
+  PartialKeyAccessToken& operator=(PartialKeyAccessToken&& other) = default;
+
+ private:
+  // `GetPartialKeyAccess()` requires access to the constructor.
+  friend PartialKeyAccessToken GetPartialKeyAccess();
+
+  PartialKeyAccessToken() = default;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_PARTIAL_KEY_ACCESS_TOKEN_H_
diff --git a/cc/prf/BUILD.bazel b/cc/prf/BUILD.bazel
index cf4d7b6..40eb6ab 100644
--- a/cc/prf/BUILD.bazel
+++ b/cc/prf/BUILD.bazel
@@ -90,11 +90,15 @@
         ":prf_set",
         "//:primitive_set",
         "//:primitive_wrapper",
+        "//internal:monitoring_util",
+        "//internal:registry_impl",
+        "//monitoring",
         "//proto:tink_cc_proto",
         "//util:status",
         "//util:statusor",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
     ],
 )
 
@@ -195,10 +199,14 @@
         ":prf_set",
         ":prf_set_wrapper",
         "//:primitive_set",
+        "//:registry",
+        "//monitoring:monitoring_client_mocks",
         "//proto:tink_cc_proto",
+        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -265,9 +273,8 @@
         ":prf_config",
         ":prf_key_templates",
         ":prf_set",
-        "//:config",
         "//:tink_cc",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:status",
         "//util:test_matchers",
         "//util:test_util",
diff --git a/cc/prf/CMakeLists.txt b/cc/prf/CMakeLists.txt
index 028587e..ebe61bc 100644
--- a/cc/prf/CMakeLists.txt
+++ b/cc/prf/CMakeLists.txt
@@ -79,8 +79,12 @@
     tink::prf::prf_set
     absl::memory
     absl::status
+    absl::statusor
     tink::core::primitive_set
     tink::core::primitive_wrapper
+    tink::internal::monitoring_util
+    tink::internal::registry_impl
+    tink::monitoring::monitoring
     tink::util::status
     tink::util::statusor
     tink::proto::tink_cc_proto
@@ -183,8 +187,12 @@
     tink::prf::prf_set_wrapper
     gmock
     absl::memory
+    absl::status
     absl::strings
     tink::core::primitive_set
+    tink::core::registry
+    tink::monitoring::monitoring_client_mocks
+    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
     tink::proto::tink_cc_proto
@@ -255,8 +263,7 @@
     absl::status
     crypto
     tink::core::cc
-    tink::core::config
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::status
     tink::util::test_matchers
     tink::util::test_util
diff --git a/cc/prf/aes_cmac_prf_key_manager.h b/cc/prf/aes_cmac_prf_key_manager.h
index fdb5844..3d7c26a 100644
--- a/cc/prf/aes_cmac_prf_key_manager.h
+++ b/cc/prf/aes_cmac_prf_key_manager.h
@@ -17,6 +17,7 @@
 #define TINK_PRF_AES_CMAC_PRF_KEY_MANAGER_H_
 
 #include <algorithm>
+#include <memory>
 #include <string>
 #include <vector>
 
diff --git a/cc/prf/aes_cmac_prf_key_manager_test.cc b/cc/prf/aes_cmac_prf_key_manager_test.cc
index e08f861..7ca8c40 100644
--- a/cc/prf/aes_cmac_prf_key_manager_test.cc
+++ b/cc/prf/aes_cmac_prf_key_manager_test.cc
@@ -15,6 +15,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 #include "tink/prf/aes_cmac_prf_key_manager.h"
 
+#include <memory>
 #include <sstream>
 #include <string>
 
diff --git a/cc/prf/hkdf_prf_key_manager.h b/cc/prf/hkdf_prf_key_manager.h
index fe860a3..0140e4b 100644
--- a/cc/prf/hkdf_prf_key_manager.h
+++ b/cc/prf/hkdf_prf_key_manager.h
@@ -17,6 +17,7 @@
 #ifndef TINK_PRF_HKDF_PRF_KEY_MANAGER_H_
 #define TINK_PRF_HKDF_PRF_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/prf/hkdf_prf_key_manager_test.cc b/cc/prf/hkdf_prf_key_manager_test.cc
index d985694..155cd5c 100644
--- a/cc/prf/hkdf_prf_key_manager_test.cc
+++ b/cc/prf/hkdf_prf_key_manager_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/prf/hkdf_prf_key_manager.h"
 
+#include <memory>
+#include <sstream>
 #include <string>
 #include <utility>
 
diff --git a/cc/prf/hmac_prf_key_manager.h b/cc/prf/hmac_prf_key_manager.h
index 4074cfc..89113d0 100644
--- a/cc/prf/hmac_prf_key_manager.h
+++ b/cc/prf/hmac_prf_key_manager.h
@@ -17,6 +17,7 @@
 #define TINK_PRF_HMAC_PRF_KEY_MANAGER_H_
 
 #include <algorithm>
+#include <map>
 #include <memory>
 #include <string>
 #include <vector>
diff --git a/cc/prf/hmac_prf_key_manager_test.cc b/cc/prf/hmac_prf_key_manager_test.cc
index d1453f0..e5ad56d 100644
--- a/cc/prf/hmac_prf_key_manager_test.cc
+++ b/cc/prf/hmac_prf_key_manager_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/prf/hmac_prf_key_manager.h"
 
+#include <sstream>
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
@@ -39,7 +41,6 @@
 using ::google::crypto::tink::HashType;
 using ::google::crypto::tink::HmacPrfKey;
 using ::google::crypto::tink::HmacPrfKeyFormat;
-using ::google::crypto::tink::KeyData;
 using ::testing::HasSubstr;
 using ::testing::Not;
 using ::testing::SizeIs;
diff --git a/cc/prf/prf_config_test.cc b/cc/prf/prf_config_test.cc
index eb685de..f86df0b 100644
--- a/cc/prf/prf_config_test.cc
+++ b/cc/prf/prf_config_test.cc
@@ -20,8 +20,7 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/config.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/keyset_handle.h"
 #include "tink/prf/hmac_prf_key_manager.h"
 #include "tink/prf/prf_key_templates.h"
@@ -44,7 +43,7 @@
 };
 
 TEST_F(PrfConfigTest, RegisterWorks) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
   }
 
@@ -59,7 +58,7 @@
 
 // FIPS-only mode tests
 TEST_F(PrfConfigTest, RegisterNonFipsTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode";
   }
 
@@ -77,7 +76,7 @@
 }
 
 TEST_F(PrfConfigTest, RegisterFipsValidTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode";
   }
 
diff --git a/cc/prf/prf_key_templates.cc b/cc/prf/prf_key_templates.cc
index 92c0378..fb8003f 100644
--- a/cc/prf/prf_key_templates.cc
+++ b/cc/prf/prf_key_templates.cc
@@ -15,6 +15,8 @@
 ////////////////////////////////////////////////////////////////////////////////
 #include "tink/prf/prf_key_templates.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "tink/prf/aes_cmac_prf_key_manager.h"
 #include "tink/prf/hkdf_prf_key_manager.h"
diff --git a/cc/prf/prf_set.h b/cc/prf/prf_set.h
index 51c0636..ddf630b 100644
--- a/cc/prf/prf_set.h
+++ b/cc/prf/prf_set.h
@@ -46,7 +46,7 @@
 // for non-deterministic MAC algorithms.
 class Prf {
  public:
-  virtual ~Prf() {}
+  virtual ~Prf() = default;
   // Computes the PRF selected by the underlying key on input and
   // returns the first outputLength bytes.
   // When choosing this parameter keep the birthday paradox in mind.
@@ -67,7 +67,7 @@
 // the Keyset.
 class PrfSet {
  public:
-  virtual ~PrfSet() {}
+  virtual ~PrfSet() = default;
   // The primary ID of the keyset.
   virtual uint32_t GetPrimaryId() const = 0;
   // A map of the PRFs represented by the keys in this keyset.
diff --git a/cc/prf/prf_set_test.cc b/cc/prf/prf_set_test.cc
index f76ed88..00e5038 100644
--- a/cc/prf/prf_set_test.cc
+++ b/cc/prf/prf_set_test.cc
@@ -17,8 +17,10 @@
 #include "tink/prf/prf_set.h"
 
 #include <map>
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/prf/prf_set_wrapper.cc b/cc/prf/prf_set_wrapper.cc
index f244a27..af81b56 100644
--- a/cc/prf/prf_set_wrapper.cc
+++ b/cc/prf/prf_set_wrapper.cc
@@ -15,10 +15,20 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/prf/prf_set_wrapper.h"
 
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "tink/internal/monitoring_util.h"
+#include "tink/internal/registry_impl.h"
+#include "tink/monitoring/monitoring.h"
+#include "tink/prf/prf_set.h"
 #include "tink/util/status.h"
 #include "proto/tink.pb.h"
 
@@ -29,12 +39,59 @@
 
 namespace {
 
+constexpr absl::string_view kPrimitive = "prf";
+constexpr absl::string_view kComputeApi = "compute";
+
+class MonitoredPrf : public Prf {
+ public:
+  explicit MonitoredPrf(uint32_t key_id, const Prf* prf,
+                        MonitoringClient* monitoring_client)
+      : key_id_(key_id), prf_(prf), monitoring_client_(monitoring_client) {}
+  ~MonitoredPrf() override = default;
+
+  MonitoredPrf(MonitoredPrf&& other) = default;
+  MonitoredPrf& operator=(MonitoredPrf&& other) = default;
+
+  MonitoredPrf(const MonitoredPrf&) = delete;
+  MonitoredPrf& operator=(const MonitoredPrf&) = delete;
+
+  util::StatusOr<std::string> Compute(absl::string_view input,
+                                      size_t output_length) const override {
+    util::StatusOr<std::string> result = prf_->Compute(input, output_length);
+    if (!result.ok()) {
+      if (monitoring_client_ != nullptr) {
+        monitoring_client_->LogFailure();
+      }
+      return result.status();
+    }
+
+    if (monitoring_client_ != nullptr) {
+      monitoring_client_->Log(key_id_, input.size());
+    }
+    return result.value();
+  }
+
+ private:
+  uint32_t key_id_;
+  const Prf* prf_;
+  MonitoringClient* monitoring_client_;
+};
+
 class PrfSetPrimitiveWrapper : public PrfSet {
  public:
-  explicit PrfSetPrimitiveWrapper(std::unique_ptr<PrimitiveSet<Prf>> prf_set)
-      : prf_set_(std::move(prf_set)) {
+  explicit PrfSetPrimitiveWrapper(
+      std::unique_ptr<PrimitiveSet<Prf>> prf_set,
+      std::unique_ptr<MonitoringClient> monitoring_client = nullptr)
+      : prf_set_(std::move(prf_set)),
+        monitoring_client_(std::move(monitoring_client)) {
+    wrapped_prfs_.reserve(prf_set_->get_raw_primitives().value()->size());
     for (const auto& prf : *prf_set_->get_raw_primitives().value()) {
-      prfs_.insert({prf->get_key_id(), &prf->get_primitive()});
+      std::unique_ptr<Prf> wrapped_prf = std::make_unique<MonitoredPrf>(
+                                  prf->get_key_id(), &prf->get_primitive(),
+                                  monitoring_client_.get());
+
+      prfs_.insert({prf->get_key_id(), wrapped_prf.get()});
+      wrapped_prfs_.push_back(std::move(wrapped_prf));
     }
   }
 
@@ -43,10 +100,12 @@
   }
   const std::map<uint32_t, Prf*>& GetPrfs() const override { return prfs_; }
 
-  ~PrfSetPrimitiveWrapper() override {}
+  ~PrfSetPrimitiveWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<Prf>> prf_set_;
+  std::unique_ptr<MonitoringClient> monitoring_client_;
+  std::vector<std::unique_ptr<Prf>> wrapped_prfs_;
   std::map<uint32_t, Prf*> prfs_;
 };
 
@@ -74,7 +133,26 @@
     std::unique_ptr<PrimitiveSet<Prf>> prf_set) const {
   util::Status status = Validate(prf_set.get());
   if (!status.ok()) return status;
-  return {absl::make_unique<PrfSetPrimitiveWrapper>(std::move(prf_set))};
+
+  MonitoringClientFactory* const monitoring_factory =
+      internal::RegistryImpl::GlobalInstance().GetMonitoringClientFactory();
+  // Monitoring is not enabled. Create a wrapper without monitoring clients.
+  if (monitoring_factory == nullptr) {
+    return {absl::make_unique<PrfSetPrimitiveWrapper>(std::move(prf_set))};
+  }
+  util::StatusOr<MonitoringKeySetInfo> keyset_info =
+      internal::MonitoringKeySetInfoFromPrimitiveSet(*prf_set);
+  if (!keyset_info.ok()) {
+    return keyset_info.status();
+  }
+  util::StatusOr<std::unique_ptr<MonitoringClient>> monitoring_client =
+      monitoring_factory->New(
+          MonitoringContext(kPrimitive, kComputeApi, *keyset_info));
+  if (!monitoring_client.ok()) {
+    return monitoring_client.status();
+  }
+  return {absl::make_unique<PrfSetPrimitiveWrapper>(
+      std::move(prf_set), *std::move(monitoring_client))};
 }
 
 }  // namespace tink
diff --git a/cc/prf/prf_set_wrapper_test.cc b/cc/prf/prf_set_wrapper_test.cc
index 095c672..b100de1 100644
--- a/cc/prf/prf_set_wrapper_test.cc
+++ b/cc/prf/prf_set_wrapper_test.cc
@@ -16,6 +16,7 @@
 #include "tink/prf/prf_set_wrapper.h"
 
 #include <cstdint>
+#include <map>
 #include <memory>
 #include <string>
 #include <utility>
@@ -23,9 +24,13 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
+#include "absl/status/status.h"
 #include "absl/strings/string_view.h"
+#include "tink/monitoring/monitoring_client_mocks.h"
 #include "tink/prf/prf_set.h"
 #include "tink/primitive_set.h"
+#include "tink/registry.h"
+#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 #include "proto/tink.pb.h"
@@ -38,11 +43,24 @@
 using ::crypto::tink::test::IsOkAndHolds;
 using ::google::crypto::tink::KeysetInfo;
 using ::google::crypto::tink::KeyStatusType;
+using ::testing::_;
+using ::testing::ByMove;
 using ::testing::Key;
+using ::testing::NiceMock;
 using ::testing::Not;
+using ::testing::Return;
 using ::testing::StrEq;
+using ::testing::Test;
 using ::testing::UnorderedElementsAre;
 
+KeysetInfo::KeyInfo MakeKey(uint32_t id) {
+  KeysetInfo::KeyInfo key;
+  key.set_output_prefix_type(google::crypto::tink::OutputPrefixType::RAW);
+  key.set_key_id(id);
+  key.set_status(KeyStatusType::ENABLED);
+  return key;
+}
+
 class FakePrf : public Prf {
  public:
   explicit FakePrf(const std::string& output) : output_(output) {}
@@ -65,14 +83,6 @@
     return prf_set_->AddPrimitive(std::move(prf), key_info);
   }
 
-  KeysetInfo::KeyInfo MakeKey(uint32_t id) {
-    KeysetInfo::KeyInfo key;
-    key.set_output_prefix_type(google::crypto::tink::OutputPrefixType::RAW);
-    key.set_key_id(id);
-    key.set_status(KeyStatusType::ENABLED);
-    return key;
-  }
-
   std::unique_ptr<PrimitiveSet<Prf>>& PrfSet() { return prf_set_; }
 
  private:
@@ -134,6 +144,104 @@
               IsOkAndHolds(StrEq("different")));
 }
 
+// Tests for the monitoring behavior.
+class PrfSetWrapperWithMonitoringTest : public Test {
+ protected:
+  // Reset the global registry.
+  void SetUp() override {
+    Registry::Reset();
+    // Setup mocks for catching Monitoring calls.
+    auto monitoring_client_factory =
+        absl::make_unique<MockMonitoringClientFactory>();
+    auto monitoring_client =
+        absl::make_unique<NiceMock<MockMonitoringClient>>();
+    monitoring_client_ref_ = monitoring_client.get();
+    // Monitoring tests expect that the client factory will create the
+    // corresponding MockMonitoringClients.
+    EXPECT_CALL(*monitoring_client_factory, New(_))
+        .WillOnce(
+            Return(ByMove(util::StatusOr<std::unique_ptr<MonitoringClient>>(
+                std::move(monitoring_client)))));
+
+    ASSERT_THAT(internal::RegistryImpl::GlobalInstance()
+                    .RegisterMonitoringClientFactory(
+                        std::move(monitoring_client_factory)),
+                IsOk());
+    ASSERT_THAT(
+        internal::RegistryImpl::GlobalInstance().GetMonitoringClientFactory(),
+        Not(testing::IsNull()));
+  }
+
+  // Cleanup the registry to avoid mock leaks.
+  ~PrfSetWrapperWithMonitoringTest() override { Registry::Reset(); }
+
+  NiceMock<MockMonitoringClient>* monitoring_client_ref_;
+};
+
+class AlwaysFailingPrf : public Prf {
+ public:
+  AlwaysFailingPrf() = default;
+
+  util::StatusOr<std::string> Compute(absl::string_view input,
+                                      size_t output_length) const override {
+    return util::Status(absl::StatusCode::kOutOfRange, "AlwaysFailingPrf");
+  }
+};
+
+TEST_F(PrfSetWrapperWithMonitoringTest, WrapKeysetWithMonitoringFailure) {
+  const absl::flat_hash_map<std::string, std::string> annotations = {
+      {"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
+  auto primitive_set = absl::make_unique<PrimitiveSet<Prf>>(annotations);
+  util::StatusOr<PrimitiveSet<Prf>::Entry<Prf>*> entry =
+      primitive_set->AddPrimitive(absl::make_unique<AlwaysFailingPrf>(),
+                                  MakeKey(/*id=*/1));
+  ASSERT_THAT(entry, IsOk());
+  ASSERT_THAT(primitive_set->set_primary(entry.value()), IsOk());
+  ASSERT_THAT(primitive_set
+                  ->AddPrimitive(absl::make_unique<FakePrf>("output"),
+                                 MakeKey(/*id=*/1))
+                  .status(),
+              IsOk());
+  util::StatusOr<std::unique_ptr<PrfSet>> prf_set =
+      PrfSetWrapper().Wrap(std::move(primitive_set));
+  ASSERT_THAT(prf_set, IsOk());
+  EXPECT_CALL(*monitoring_client_ref_, LogFailure());
+  EXPECT_THAT((*prf_set)->ComputePrimary("input", /*output_length=*/16),
+              Not(IsOk()));
+}
+
+TEST_F(PrfSetWrapperWithMonitoringTest, WrapKeysetWithMonitoringVerifySuccess) {
+  const absl::flat_hash_map<std::string, std::string> annotations = {
+      {"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
+  auto primitive_set = absl::make_unique<PrimitiveSet<Prf>>(annotations);
+
+  util::StatusOr<PrimitiveSet<Prf>::Entry<Prf>*> entry =
+      primitive_set->AddPrimitive(absl::make_unique<FakePrf>("output"),
+                                  MakeKey(/*id=*/1));
+  ASSERT_THAT(entry, IsOk());
+  ASSERT_THAT(primitive_set->set_primary(entry.value()), IsOk());
+  ASSERT_THAT(primitive_set
+                  ->AddPrimitive(absl::make_unique<FakePrf>("output"),
+                                 MakeKey(/*id=*/1))
+                  .status(),
+              IsOk());
+
+  util::StatusOr<std::unique_ptr<PrfSet>> prf_set =
+      PrfSetWrapper().Wrap(std::move(primitive_set));
+  ASSERT_THAT(prf_set, IsOk());
+  std::map<uint32_t, Prf*> prf_map = (*prf_set)->GetPrfs();
+  std::string input = "input";
+  for (const auto& entry : prf_map) {
+    EXPECT_CALL(*monitoring_client_ref_, Log(entry.first, input.size()));
+    EXPECT_THAT((entry.second)->Compute(input, /*output_length=*/16).status(),
+                IsOk());
+  }
+  input = "hello_world";
+  EXPECT_CALL(*monitoring_client_ref_,
+              Log((*prf_set)->GetPrimaryId(), input.size()));
+  EXPECT_THAT((*prf_set)->ComputePrimary(input, /*output_length=*/16), IsOk());
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/primitive_set.h b/cc/primitive_set.h
index 76ae5ce..9f6439c 100644
--- a/cc/primitive_set.h
+++ b/cc/primitive_set.h
@@ -17,10 +17,13 @@
 #ifndef TINK_PRIMITIVE_SET_H_
 #define TINK_PRIMITIVE_SET_H_
 
+#include <algorithm>
+#include <memory>
 #include <string>
-#include <unordered_map>
+#include <utility>
 #include <vector>
 
+#include "absl/base/thread_annotations.h"
 #include "absl/container/flat_hash_map.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
@@ -113,48 +116,16 @@
   };
 
   typedef std::vector<std::unique_ptr<Entry<P>>> Primitives;
+  typedef absl::flat_hash_map<std::string, Primitives>
+      CiphertextPrefixToPrimitivesMap;
 
-  // Constructs an empty PrimitiveSet.
-  // Note: This is equivalent to PrimitiveSet<P>(/*annotations=*/{}).
-  PrimitiveSet<P>() = default;
-  // Constructs an empty PrimitiveSet with `annotations`.
-  explicit PrimitiveSet<P>(
-      const absl::flat_hash_map<std::string, std::string>& annotations)
-      : annotations_(annotations) {}
+ private:
+  // Helper methods for mutations, used by the Builder and the deprecated
+  // mutation methods on PrimitiveSet.
 
-  // Adds 'primitive' to this set for the specified 'key'.
-  crypto::tink::util::StatusOr<Entry<P>*> AddPrimitive(
-      std::unique_ptr<P> primitive,
-      const google::crypto::tink::KeysetInfo::KeyInfo& key_info) {
-    auto entry_or = Entry<P>::New(std::move(primitive), key_info);
-    if (!entry_or.ok()) return entry_or.status();
-
-    absl::MutexLock lock(&primitives_mutex_);
-    std::string identifier = entry_or.value()->get_identifier();
-    primitives_[identifier].push_back(std::move(entry_or.value()));
-    return primitives_[identifier].back().get();
-  }
-
-  // Returns the entries with primitives identifed by 'identifier'.
-  crypto::tink::util::StatusOr<const Primitives*> get_primitives(
-      absl::string_view identifier) {
-    absl::MutexLock lock(&primitives_mutex_);
-    typename CiphertextPrefixToPrimitivesMap::iterator found =
-        primitives_.find(std::string(identifier));
-    if (found == primitives_.end()) {
-      return ToStatusF(absl::StatusCode::kNotFound,
-                       "No primitives found for identifier '%s'.", identifier);
-    }
-    return &(found->second);
-  }
-
-  // Returns all primitives that use RAW prefix.
-  crypto::tink::util::StatusOr<const Primitives*> get_raw_primitives() {
-    return get_primitives(CryptoFormat::kRawPrefix);
-  }
-
-  // Sets the given 'primary' as the primary primitive of this set.
-  crypto::tink::util::Status set_primary(Entry<P>* primary) {
+  static crypto::tink::util::Status SetPrimaryImpl(
+      Entry<P>** output, Entry<P>* primary,
+      const CiphertextPrefixToPrimitivesMap& primitives) {
     if (!primary) {
       return util::Status(absl::StatusCode::kInvalidArgument,
                           "The primary primitive must be non-null.");
@@ -163,23 +134,190 @@
       return util::Status(absl::StatusCode::kInvalidArgument,
                           "Primary has to be enabled.");
     }
-    auto entries_result = get_primitives(primary->get_identifier());
-    if (!entries_result.ok()) {
+
+    if (primitives.count(primary->get_identifier()) == 0) {
       return util::Status(absl::StatusCode::kInvalidArgument,
                           "Primary cannot be set to an entry which is "
                           "not held by this primitive set.");
     }
 
-    primary_ = primary;
+    *output = primary;
     return crypto::tink::util::OkStatus();
   }
 
-  // Returns the entry with the primary primitive.
-  const Entry<P>* get_primary() const { return primary_; }
+  static crypto::tink::util::StatusOr<Entry<P>*> AddPrimitiveImpl(
+      std::unique_ptr<P> primitive,
+      const google::crypto::tink::KeysetInfo::KeyInfo& key_info,
+      CiphertextPrefixToPrimitivesMap& primitives,
+      std::vector<Entry<P>*>& primitives_in_keyset_order) {
+    auto entry_or = Entry<P>::New(std::move(primitive), key_info);
+    if (!entry_or.ok()) return entry_or.status();
 
-  // Returns all entries currently in this primitive set.
-  const std::vector<Entry<P>*> get_all() const {
-    absl::MutexLock lock(&primitives_mutex_);
+    std::string identifier = entry_or.value()->get_identifier();
+    primitives[identifier].push_back(std::move(entry_or.value()));
+
+    Entry<P>* stored_entry = primitives[identifier].back().get();
+    primitives_in_keyset_order.push_back(stored_entry);
+    return stored_entry;
+  }
+
+ public:
+  // Builder is used to construct PrimitiveSet objects. Objects returned by
+  // the builder are immutable. Calling any of the non-const methods on them
+  // will fail.
+  class Builder {
+   public:
+    // Adds 'primitive' to this set for the specified 'key'.
+    Builder& AddPrimitive(
+        std::unique_ptr<P> primitive,
+        const google::crypto::tink::KeysetInfo::KeyInfo& key_info) & {
+      absl::MutexLock lock(&mutex_);
+      if (!status_.ok()) return *this;
+      status_ = AddPrimitiveImpl(std::move(primitive), key_info, primitives_,
+                                 primitives_in_keyset_order_)
+                    .status();
+      return *this;
+    }
+
+    Builder&& AddPrimitive(
+        std::unique_ptr<P> primitive,
+        const google::crypto::tink::KeysetInfo::KeyInfo& key_info) && {
+      return std::move(AddPrimitive(std::move(primitive), key_info));
+    }
+
+    // Adds 'primitive' to this set for the specified 'key' and marks it
+    // primary.
+    Builder& AddPrimaryPrimitive(
+        std::unique_ptr<P> primitive,
+        const google::crypto::tink::KeysetInfo::KeyInfo& key_info) & {
+      absl::MutexLock lock(&mutex_);
+      if (!status_.ok()) return *this;
+      auto entry_result =
+          AddPrimitiveImpl(std::move(primitive), key_info, primitives_,
+                           primitives_in_keyset_order_);
+      if (!entry_result.ok()) {
+        status_ = entry_result.status();
+        return *this;
+      }
+      status_ = SetPrimaryImpl(&primary_, entry_result.value(), primitives_);
+      return *this;
+    }
+
+    Builder&& AddPrimaryPrimitive(
+        std::unique_ptr<P> primitive,
+        const google::crypto::tink::KeysetInfo::KeyInfo& key_info) && {
+      return std::move(AddPrimaryPrimitive(std::move(primitive), key_info));
+    }
+
+    // Add the given annotations. Existing annotations will not be overwritten.
+    Builder& AddAnnotations(
+        absl::flat_hash_map<std::string, std::string> annotations) & {
+      absl::MutexLock lock(&mutex_);
+      annotations_.merge(std::move(annotations));
+      return *this;
+    }
+
+    Builder&& AddAnnotations(
+        absl::flat_hash_map<std::string, std::string> annotations) && {
+      return std::move(AddAnnotations(std::move(annotations)));
+    }
+
+    crypto::tink::util::StatusOr<PrimitiveSet<P>> Build() && {
+      absl::MutexLock lock(&mutex_);
+      if (!status_.ok()) return status_;
+      return PrimitiveSet<P>(std::move(primitives_), primary_,
+                             std::move(primitives_in_keyset_order_),
+                             std::move(annotations_));
+    }
+
+   private:
+    // Owned by primitives_.
+    Entry<P>* primary_ ABSL_GUARDED_BY(mutex_) = nullptr;
+    CiphertextPrefixToPrimitivesMap primitives_ ABSL_GUARDED_BY(mutex_);
+    // Entries in the original keyset key order, all owned by primitives_.
+    std::vector<Entry<P>*> primitives_in_keyset_order_ ABSL_GUARDED_BY(mutex_);
+    absl::flat_hash_map<std::string, std::string> annotations_
+        ABSL_GUARDED_BY(mutex_);
+    absl::Mutex mutex_;
+    crypto::tink::util::Status status_ ABSL_GUARDED_BY(mutex_);
+  };
+
+  // PrimitiveSet is movable, but not copyable
+  PrimitiveSet(PrimitiveSet&&) = default;
+  PrimitiveSet<P>& operator=(PrimitiveSet&&) = default;
+  PrimitiveSet(const PrimitiveSet&) = delete;
+  PrimitiveSet<P>& operator=(const PrimitiveSet&) = delete;
+
+  // Constructs an empty PrimitiveSet.
+  // Note: This is equivalent to PrimitiveSet<P>(/*annotations=*/{}).
+  ABSL_DEPRECATED(
+      "Constructing PrimitiveSet using constructors is deprecated. Use "
+      "PrimitiveSet<>::Builder instead.")
+  PrimitiveSet<P>() = default;
+  // Constructs an empty PrimitiveSet with `annotations`.
+  ABSL_DEPRECATED(
+      "Constructing PrimitiveSet using constructors is deprecated. Use "
+      "PrimitiveSet<>::Builder instead.")
+  explicit PrimitiveSet<P>(
+      const absl::flat_hash_map<std::string, std::string>& annotations)
+      : annotations_(annotations) {}
+
+  // Adds 'primitive' to this set for the specified 'key'.
+  ABSL_DEPRECATED(
+      "Mutating PrimitiveSets after construction is deprecated. Use "
+      "PrimitiveSet<>::Builder instead.")
+  crypto::tink::util::StatusOr<Entry<P>*> AddPrimitive(
+      std::unique_ptr<P> primitive,
+      const google::crypto::tink::KeysetInfo::KeyInfo& key_info) {
+    if (!is_mutable()) {
+      return util::Status(absl::StatusCode::kFailedPrecondition,
+                          "PrimitiveSet is not mutable.");
+    }
+
+    absl::MutexLock lock(primitives_mutex_.get());
+    return AddPrimitiveImpl(std::move(primitive), key_info, primitives_,
+                            primitives_in_keyset_order_);
+  }
+
+  // Returns the entries with primitives identified by 'identifier'.
+  crypto::tink::util::StatusOr<const Primitives*> get_primitives(
+      absl::string_view identifier) const {
+    absl::MutexLockMaybe lock(primitives_mutex_.get());
+    auto found = primitives_.find(std::string(identifier));
+    if (found == primitives_.end()) {
+      return ToStatusF(absl::StatusCode::kNotFound,
+                       "No primitives found for identifier '%s'.", identifier);
+    }
+    return &(found->second);
+  }
+
+  // Returns all primitives that use RAW prefix.
+  crypto::tink::util::StatusOr<const Primitives*> get_raw_primitives() const {
+    return get_primitives(CryptoFormat::kRawPrefix);
+  }
+
+  // Sets the given 'primary' as the primary primitive of this set.
+  ABSL_DEPRECATED(
+      "Mutating PrimitiveSets after construction is deprecated. Use "
+      "PrimitiveSet<>::Builder instead.")
+  crypto::tink::util::Status set_primary(Entry<P>* primary) {
+    if (!is_mutable()) {
+      return util::Status(absl::StatusCode::kFailedPrecondition,
+                          "PrimitiveSet is not mutable.");
+    }
+    absl::MutexLock lock(primitives_mutex_.get());
+    return SetPrimaryImpl(&primary_, primary, primitives_);
+  }
+
+  // Returns the entry with the primary primitive.
+  const Entry<P>* get_primary() const {
+    absl::MutexLockMaybe lock(primitives_mutex_.get());
+    return primary_;
+  }
+
+  // Returns all entries.
+  std::vector<Entry<P>*> get_all() const {
+    absl::MutexLockMaybe lock(primitives_mutex_.get());
     std::vector<Entry<P>*> result;
     for (const auto& prefix_and_vector : primitives_) {
       for (const auto& primitive : prefix_and_vector.second) {
@@ -189,21 +327,43 @@
     return result;
   }
 
+  // Returns all entries in the original keyset key order.
+  std::vector<Entry<P>*> get_all_in_keyset_order() const {
+    absl::MutexLockMaybe lock(primitives_mutex_.get());
+    return primitives_in_keyset_order_;
+  }
+
   const absl::flat_hash_map<std::string, std::string>& get_annotations() const {
     return annotations_;
   }
 
+  bool is_mutable() const { return primitives_mutex_ != nullptr; }
+
  private:
-  typedef std::unordered_map<std::string, Primitives>
-      CiphertextPrefixToPrimitivesMap;
-  // The Entry<P> object is owned by primitives_
-  Entry<P>* primary_ = nullptr;
-  mutable absl::Mutex primitives_mutex_;
+  // Constructs an empty PrimitiveSet.
+  // Note: This is equivalent to PrimitiveSet<P>(/*annotations=*/{}).
+  PrimitiveSet(CiphertextPrefixToPrimitivesMap primitives, Entry<P>* primary,
+               std::vector<Entry<P>*> primitives_in_keyset_order,
+               absl::flat_hash_map<std::string, std::string> annotations)
+      : primary_(primary),
+        primitives_mutex_(nullptr),
+        primitives_(std::move(primitives)),
+        primitives_in_keyset_order_(std::move(primitives_in_keyset_order)),
+        annotations_(std::move(annotations)) {}
+
+  // Owned by primitives_.
+  Entry<P>* primary_ ABSL_GUARDED_BY(primitives_mutex_) = nullptr;
+  // If primitives_mutex_ is a nullptr, PrimitiveSet is immutable and lock-free.
+  // If not nullptr, primitives_mutex_ guards all read and write access.
+  mutable std::unique_ptr<absl::Mutex> primitives_mutex_ =
+      absl::make_unique<absl::Mutex>();
   CiphertextPrefixToPrimitivesMap primitives_
       ABSL_GUARDED_BY(primitives_mutex_);
+  // Entries in the original keyset key order, all owned by primitives_.
+  std::vector<Entry<P>*> primitives_in_keyset_order_
+      ABSL_GUARDED_BY(primitives_mutex_);
 
-  // Annotations for the set of primitives.
-  const absl::flat_hash_map<std::string, std::string> annotations_;
+  absl::flat_hash_map<std::string, std::string> annotations_;
 };
 
 }  // namespace tink
diff --git a/cc/primitive_wrapper.h b/cc/primitive_wrapper.h
index beadd8b..1ba6f22 100644
--- a/cc/primitive_wrapper.h
+++ b/cc/primitive_wrapper.h
@@ -31,10 +31,15 @@
 //
 // PrimitiveWrappers need to be written for every new primitive. They can be
 // registered in the registry to be fully integrated in Tink.
-template <typename InputPrimitive, typename Primitive>
+template <typename InputPrimitiveParam, typename PrimitiveParam>
 class PrimitiveWrapper {
  public:
-  virtual ~PrimitiveWrapper() {}
+  virtual ~PrimitiveWrapper() = default;
+
+  // Useful when writing templated code.
+  using InputPrimitive = InputPrimitiveParam;
+  using Primitive = PrimitiveParam;
+
   virtual crypto::tink::util::StatusOr<std::unique_ptr<Primitive>> Wrap(
       std::unique_ptr<PrimitiveSet<InputPrimitive>> primitive_set) const = 0;
 };
diff --git a/cc/proto/aes_cmac.proto b/cc/proto/aes_cmac.proto
index b214834..541ff58 100644
--- a/cc/proto/aes_cmac.proto
+++ b/cc/proto/aes_cmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_go_proto";
 
 message AesCmacParams {
   uint32 tag_size = 1;
diff --git a/cc/proto/aes_cmac_prf.proto b/cc/proto/aes_cmac_prf.proto
index 58e5f67..b2efc6d 100644
--- a/cc/proto/aes_cmac_prf.proto
+++ b/cc/proto/aes_cmac_prf.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_prf_go_proto";
 
 // key_type: type.googleapis.com/google.crypto.tink.AesCmacPrfKey
 message AesCmacPrfKey {
diff --git a/cc/proto/aes_ctr.proto b/cc/proto/aes_ctr.proto
index ecdb256..721699c 100644
--- a/cc/proto/aes_ctr.proto
+++ b/cc/proto/aes_ctr.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_go_proto";
 
 message AesCtrParams {
   uint32 iv_size = 1;
diff --git a/cc/proto/aes_ctr_hmac_aead.proto b/cc/proto/aes_ctr_hmac_aead.proto
index dcf541d..91ccb9b 100644
--- a/cc/proto/aes_ctr_hmac_aead.proto
+++ b/cc/proto/aes_ctr_hmac_aead.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_aead_go_proto";
 
 message AesCtrHmacAeadKeyFormat {
   AesCtrKeyFormat aes_ctr_key_format = 1;
diff --git a/cc/proto/aes_ctr_hmac_streaming.proto b/cc/proto/aes_ctr_hmac_streaming.proto
index 064b630..776e9bd 100644
--- a/cc/proto/aes_ctr_hmac_streaming.proto
+++ b/cc/proto/aes_ctr_hmac_streaming.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_streaming_go_proto";
 
 message AesCtrHmacStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/cc/proto/aes_eax.proto b/cc/proto/aes_eax.proto
index c673306..c1bf500 100644
--- a/cc/proto/aes_eax.proto
+++ b/cc/proto/aes_eax.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_eax_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_eax_go_proto";
 
 // only allowing tag size in bytes = 16
 message AesEaxParams {
diff --git a/cc/proto/aes_gcm.proto b/cc/proto/aes_gcm.proto
index fba7a89..2551aa4 100644
--- a/cc/proto/aes_gcm.proto
+++ b/cc/proto/aes_gcm.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_go_proto";
 option objc_class_prefix = "TINKPB";
 
 message AesGcmKeyFormat {
diff --git a/cc/proto/aes_gcm_hkdf_streaming.proto b/cc/proto/aes_gcm_hkdf_streaming.proto
index 61fb479..5ec7ca4 100644
--- a/cc/proto/aes_gcm_hkdf_streaming.proto
+++ b/cc/proto/aes_gcm_hkdf_streaming.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto";
 
 message AesGcmHkdfStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/cc/proto/aes_gcm_siv.proto b/cc/proto/aes_gcm_siv.proto
index df9fada..220d79f 100644
--- a/cc/proto/aes_gcm_siv.proto
+++ b/cc/proto/aes_gcm_siv.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_siv_go_proto";
 
 // The only allowed IV size is 12 bytes and tag size is 16 bytes.
 // Thus, accept no params.
diff --git a/cc/proto/aes_siv.proto b/cc/proto/aes_siv.proto
index 0023027..ccb8d3c 100644
--- a/cc/proto/aes_siv.proto
+++ b/cc/proto/aes_siv.proto
@@ -20,7 +20,15 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_siv_go_proto";
+
+// Tink implements RFC 5297 (https://www.rfc-editor.org/rfc/rfc5297) for
+// AES-SIV, putting the SIV/Tag at the beginning of the ciphertext.
+//
+// While the RFC 5297 supports a list of associated datas, Tink only supports
+// exactly one associated data, which corresponds to a list with one element in
+// RFC 5297. An empty associated data is a list with one empty element, and not
+// an empty list.
 
 message AesSivKeyFormat {
   // Only valid value is: 64.
diff --git a/cc/proto/cached_dek_aead.proto b/cc/proto/cached_dek_aead.proto
index 10bcde5..9b1a33f 100644
--- a/cc/proto/cached_dek_aead.proto
+++ b/cc/proto/cached_dek_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/cached_dek_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_aead_go_proto";
 
 message CachedDekAeadKeyFormat {
   // Required.
diff --git a/cc/proto/cached_dek_envelope.proto b/cc/proto/cached_dek_envelope.proto
index 1b096ad..8739b83 100644
--- a/cc/proto/cached_dek_envelope.proto
+++ b/cc/proto/cached_dek_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_multiple_files = true;
 option java_package = "com.google.crypto.tink.proto";
-option go_package = "github.com/google/tink/proto/cached_dek_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_envelope_go_proto";
 
 message CachedDekEnvelopeAeadKeyFormat {
   // Required.
diff --git a/cc/proto/chacha20_poly1305.proto b/cc/proto/chacha20_poly1305.proto
index 2cd6ead..ef8ab6e 100644
--- a/cc/proto/chacha20_poly1305.proto
+++ b/cc/proto/chacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/chacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/chacha20_poly1305_go_proto";
 
 message ChaCha20Poly1305KeyFormat {}
 
diff --git a/cc/proto/common.proto b/cc/proto/common.proto
index eaff8d3..4546064 100644
--- a/cc/proto/common.proto
+++ b/cc/proto/common.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/common_go_proto";
+option go_package = "github.com/google/tink/go/proto/common_go_proto";
 
 enum EllipticCurveType {
   UNKNOWN_CURVE = 0;
diff --git a/cc/proto/config.proto b/cc/proto/config.proto
index ebbd742..cff6506 100644
--- a/cc/proto/config.proto
+++ b/cc/proto/config.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/config_go_proto";
+option go_package = "github.com/google/tink/go/proto/config_go_proto";
 
 // An entry that describes a key type to be used with Tink library,
 // specifying the corresponding primitive, key manager, and deprecation status.
diff --git a/cc/proto/ecdsa.proto b/cc/proto/ecdsa.proto
index 6ba3970..2ce461f 100644
--- a/cc/proto/ecdsa.proto
+++ b/cc/proto/ecdsa.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecdsa_go_proto";
 
 enum EcdsaSignatureEncoding {
   UNKNOWN_ENCODING = 0;
@@ -80,4 +80,5 @@
 message EcdsaKeyFormat {
   // Required.
   EcdsaParams params = 2;
+  uint32 version = 3;
 }
diff --git a/cc/proto/ecies_aead_hkdf.proto b/cc/proto/ecies_aead_hkdf.proto
index 9470991..0c06ee3 100644
--- a/cc/proto/ecies_aead_hkdf.proto
+++ b/cc/proto/ecies_aead_hkdf.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecies_aead_hkdf_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecies_aead_hkdf_go_proto";
 
 // Protos for keys for ECIES with HKDF and AEAD encryption.
 //
diff --git a/cc/proto/ed25519.proto b/cc/proto/ed25519.proto
index 669f33a..613c59f 100644
--- a/cc/proto/ed25519.proto
+++ b/cc/proto/ed25519.proto
@@ -23,10 +23,10 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ed25519_go_proto";
+option go_package = "github.com/google/tink/go/proto/ed25519_go_proto";
 
 message Ed25519KeyFormat {
-    uint32 version = 1;
+  uint32 version = 1;
 }
 
 // key_type: type.googleapis.com/google.crypto.tink.Ed25519PublicKey
diff --git a/cc/proto/empty.proto b/cc/proto/empty.proto
index 33831a9..beeba07 100644
--- a/cc/proto/empty.proto
+++ b/cc/proto/empty.proto
@@ -20,6 +20,6 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/empty_go_proto";
+option go_package = "github.com/google/tink/go/proto/empty_go_proto";
 
 message Empty {}
diff --git a/cc/proto/hkdf_prf.proto b/cc/proto/hkdf_prf.proto
index 3d3cbe9..38e69c5 100644
--- a/cc/proto/hkdf_prf.proto
+++ b/cc/proto/hkdf_prf.proto
@@ -22,12 +22,16 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hkdf_prf_proto";
+option go_package = "github.com/google/tink/go/proto/hkdf_prf_proto";
 
 message HkdfPrfParams {
   HashType hash = 1;
-  // Salt, optional in RFC 5869. Using "" is equivalent to zeros of length up to
-  // the block length of the HMac.
+  // Optional.
+  //
+  // An unspecified or zero-length value is equivalent to a sequence of zeros
+  // (0x00) with a length equal to the output size of hash.
+  //
+  // See https://rfc-editor.org/rfc/rfc5869.
   bytes salt = 2;
 }
 
diff --git a/cc/proto/hmac.proto b/cc/proto/hmac.proto
index 2733e51..bdd4e8a 100644
--- a/cc/proto/hmac.proto
+++ b/cc/proto/hmac.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_go_proto";
 
 message HmacParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/cc/proto/hmac_prf.proto b/cc/proto/hmac_prf.proto
index 7b3c52d..87ef97d 100644
--- a/cc/proto/hmac_prf.proto
+++ b/cc/proto/hmac_prf.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_prf_go_proto";
 
 message HmacPrfParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/cc/proto/hpke.proto b/cc/proto/hpke.proto
index 847864a..f794e77 100644
--- a/cc/proto/hpke.proto
+++ b/cc/proto/hpke.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hpke_proto";
+option go_package = "github.com/google/tink/go/proto/hpke_proto";
 
 enum HpkeKem {
   KEM_UNKNOWN = 0;
diff --git a/cc/proto/jwt_ecdsa.proto b/cc/proto/jwt_ecdsa.proto
index 4c80fe1..ce78b04 100644
--- a/cc/proto/jwt_ecdsa.proto
+++ b/cc/proto/jwt_ecdsa.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_ecdsa_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.4
 enum JwtEcdsaAlgorithm {
diff --git a/cc/proto/jwt_hmac.proto b/cc/proto/jwt_hmac.proto
index e54a51d..a499638 100644
--- a/cc/proto/jwt_hmac.proto
+++ b/cc/proto/jwt_hmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_hmac_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
 enum JwtHmacAlgorithm {
diff --git a/cc/proto/jwt_rsa_ssa_pkcs1.proto b/cc/proto/jwt_rsa_ssa_pkcs1.proto
index adf31c8..54a9731 100644
--- a/cc/proto/jwt_rsa_ssa_pkcs1.proto
+++ b/cc/proto/jwt_rsa_ssa_pkcs1.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.3
 enum JwtRsaSsaPkcs1Algorithm {
diff --git a/cc/proto/jwt_rsa_ssa_pss.proto b/cc/proto/jwt_rsa_ssa_pss.proto
index 4312645..eb2d454 100644
--- a/cc/proto/jwt_rsa_ssa_pss.proto
+++ b/cc/proto/jwt_rsa_ssa_pss.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.5
 enum JwtRsaSsaPssAlgorithm {
diff --git a/cc/proto/kms_aead.proto b/cc/proto/kms_aead.proto
index e818788..16de8ee 100644
--- a/cc/proto/kms_aead.proto
+++ b/cc/proto/kms_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_aead_go_proto";
 
 message KmsAeadKeyFormat {
   // Required.
diff --git a/cc/proto/kms_envelope.proto b/cc/proto/kms_envelope.proto
index fa806e6..8b0fd83 100644
--- a/cc/proto/kms_envelope.proto
+++ b/cc/proto/kms_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_envelope_go_proto";
 
 message KmsEnvelopeAeadKeyFormat {
   // Required.
diff --git a/cc/proto/prf_based_deriver.proto b/cc/proto/prf_based_deriver.proto
index 06dd334..e58a2cd 100644
--- a/cc/proto/prf_based_deriver.proto
+++ b/cc/proto/prf_based_deriver.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/prf_based_deriver_go_proto";
+option go_package = "github.com/google/tink/go/proto/prf_based_deriver_go_proto";
 
 message PrfBasedDeriverParams {
   KeyTemplate derived_key_template = 1;
diff --git a/cc/proto/rsa_ssa_pkcs1.proto b/cc/proto/rsa_ssa_pkcs1.proto
index 961189d..9797ee0 100644
--- a/cc/proto/rsa_ssa_pkcs1.proto
+++ b/cc/proto/rsa_ssa_pkcs1.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 message RsaSsaPkcs1Params {
   // Hash function used in computing hash of the signing message
diff --git a/cc/proto/rsa_ssa_pss.proto b/cc/proto/rsa_ssa_pss.proto
index 8e7903f..1150057 100644
--- a/cc/proto/rsa_ssa_pss.proto
+++ b/cc/proto/rsa_ssa_pss.proto
@@ -25,7 +25,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto";
 
 message RsaSsaPssParams {
   // Hash function used in computing hash of the signing message
diff --git a/cc/proto/tink.proto b/cc/proto/tink.proto
index 1787581..8b3d100 100644
--- a/cc/proto/tink.proto
+++ b/cc/proto/tink.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/tink_go_proto";
+option go_package = "github.com/google/tink/go/proto/tink_go_proto";
 option objc_class_prefix = "TINKPB";
 
 // Each instantiation of a Tink primitive is identified by type_url,
diff --git a/cc/proto/xchacha20_poly1305.proto b/cc/proto/xchacha20_poly1305.proto
index cc52624..a2613f1 100644
--- a/cc/proto/xchacha20_poly1305.proto
+++ b/cc/proto/xchacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/xchacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto";
 
 message XChaCha20Poly1305KeyFormat {
   uint32 version = 1;
diff --git a/cc/proto_keyset_format.cc b/cc/proto_keyset_format.cc
new file mode 100644
index 0000000..e666ed9
--- /dev/null
+++ b/cc/proto_keyset_format.cc
@@ -0,0 +1,101 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/proto_keyset_format.h"
+
+#include <ios>
+#include <iostream>
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "tink/binary_keyset_reader.h"
+#include "tink/binary_keyset_writer.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/util/secret_data.h"
+
+namespace crypto {
+namespace tink {
+
+crypto::tink::util::StatusOr<KeysetHandle> ParseKeysetFromProtoKeysetFormat(
+    absl::string_view serialized_keyset, SecretKeyAccessToken token) {
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetReader>>
+      keyset_reader = BinaryKeysetReader::New(serialized_keyset);
+  if (!keyset_reader.ok()) {
+    return keyset_reader.status();
+  }
+  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> result =
+    CleartextKeysetHandle::Read(std::move(*keyset_reader));
+  if (!result.ok()) {
+    return result.status();
+  }
+  return std::move(**result);
+}
+
+crypto::tink::util::StatusOr<util::SecretData>
+SerializeKeysetToProtoKeysetFormat(const KeysetHandle& keyset_handle,
+                                   SecretKeyAccessToken token) {
+  std::stringbuf string_buf(std::ios_base::out);
+  crypto::tink::util::StatusOr<std::unique_ptr<BinaryKeysetWriter>>
+      keyset_writer = BinaryKeysetWriter::New(
+          std::make_unique<std::ostream>(&string_buf));
+  if (!keyset_writer.ok()) {
+    return keyset_writer.status();
+  }
+  crypto::tink::util::Status status =
+      CleartextKeysetHandle::Write(keyset_writer->get(), keyset_handle);
+  if (!status.ok()) {
+    return status;
+  }
+  // TODO(tholenst): directly write into a secret data.
+  return util::SecretDataFromStringView(string_buf.str());
+}
+
+crypto::tink::util::StatusOr<KeysetHandle>
+ParseKeysetWithoutSecretFromProtoKeysetFormat(
+    absl::string_view serialized_keyset) {
+  std::string keyset_copy = std::string(serialized_keyset);
+  crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>> result =
+    KeysetHandle::ReadNoSecret(keyset_copy);
+  if (!result.ok()) {
+    return result.status();
+  }
+  return std::move(**result);
+}
+
+crypto::tink::util::StatusOr<std::string>
+SerializeKeysetWithoutSecretToProtoKeysetFormat(
+    const KeysetHandle& keyset_handle) {
+  std::stringbuf string_buf(std::ios_base::out);
+  crypto::tink::util::StatusOr<std::unique_ptr<BinaryKeysetWriter>>
+      keyset_writer = BinaryKeysetWriter::New(
+          std::make_unique<std::ostream>(&string_buf));
+  if (!keyset_writer.ok()) {
+    return keyset_writer.status();
+  }
+  crypto::tink::util::Status status =
+      keyset_handle.WriteNoSecret(keyset_writer->get());
+  if (!status.ok()) {
+    return status;
+  }
+  return string_buf.str();
+}
+
+}  // namespace tink
+}  // namespace crypto
+
diff --git a/cc/proto_keyset_format.h b/cc/proto_keyset_format.h
new file mode 100644
index 0000000..cab89eb
--- /dev/null
+++ b/cc/proto_keyset_format.h
@@ -0,0 +1,56 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_PROTO_KEYSET_FORMAT_H_
+#define TINK_PROTO_KEYSET_FORMAT_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "tink/keyset_handle.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/secret_data.h"
+
+namespace crypto {
+namespace tink {
+
+// Serializes a keyset into a binary string in "ProtoKeysetFormat".
+// This function can serialize both keyset with or without secret key material.
+crypto::tink::util::StatusOr<util::SecretData>
+SerializeKeysetToProtoKeysetFormat(const KeysetHandle& keyset_handle,
+                                   SecretKeyAccessToken token);
+
+// Parses a keyset from a binary string in "ProtoKeysetFormat".
+// This function can parse both keyset with or without secret key material.
+crypto::tink::util::StatusOr<KeysetHandle> ParseKeysetFromProtoKeysetFormat(
+    absl::string_view serialized_keyset, SecretKeyAccessToken token);
+
+// Serializes a keyset into a binary string in "ProtoKeysetFormat".
+// This function will fail if the keyset contains secret key material.
+crypto::tink::util::StatusOr<std::string>
+SerializeKeysetWithoutSecretToProtoKeysetFormat(
+    const KeysetHandle& keyset_handle);
+
+// Parses a keyset from a binary string in "ProtoKeysetFormat".
+// This function will fail if the keyset contains secret key material.
+crypto::tink::util::StatusOr<KeysetHandle>
+ParseKeysetWithoutSecretFromProtoKeysetFormat(
+    absl::string_view serialized_keyset);
+
+
+}  // namespace tink
+}  // namespace crypto
+#endif  // TINK_PROTO_KEYSET_FORMAT_H_
diff --git a/cc/proto_keyset_format_test.cc b/cc/proto_keyset_format_test.cc
new file mode 100644
index 0000000..5ac23e6
--- /dev/null
+++ b/cc/proto_keyset_format_test.cc
@@ -0,0 +1,279 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/proto_keyset_format.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/strings/escaping.h"
+#include "tink/config/tink_config.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/legacy_proto_parameters.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/keyset_handle_builder.h"
+#include "tink/mac.h"
+#include "tink/mac/mac_key_templates.h"
+#include "tink/signature/signature_key_templates.h"
+#include "tink/util/secret_data.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+
+namespace {
+
+using ::crypto::tink::internal::LegacyProtoParameters;
+using ::crypto::tink::internal::ProtoParametersSerialization;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::util::SecretData;
+using ::crypto::tink::util::SecretDataAsStringView;
+using ::testing::Eq;
+using ::testing::Not;
+
+class SerializeKeysetToProtoKeysetFormatTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    auto status = TinkConfig::Register();
+    ASSERT_THAT(status, IsOk());
+  }
+};
+
+util::StatusOr<LegacyProtoParameters> CmacParameters() {
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create(MacKeyTemplates::AesCmac());
+  if (!serialization.ok()) return serialization.status();
+
+  return LegacyProtoParameters(*serialization);
+}
+
+util::StatusOr<LegacyProtoParameters> EcdsaParameters() {
+  util::StatusOr<ProtoParametersSerialization> serialization =
+      ProtoParametersSerialization::Create(SignatureKeyTemplates::EcdsaP256());
+  if (!serialization.ok()) return serialization.status();
+
+  return LegacyProtoParameters(*serialization);
+}
+
+TEST_F(SerializeKeysetToProtoKeysetFormatTest, SerializeAndParseSingleKey) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CmacParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+
+  crypto::tink::util::StatusOr<SecretData> serialization =
+      SerializeKeysetToProtoKeysetFormat(*handle,
+                                         InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<KeysetHandle> parsed_handle = ParseKeysetFromProtoKeysetFormat(
+      SecretDataAsStringView(*serialization), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_handle, IsOk());
+  ASSERT_THAT(handle->size(), Eq(1));
+  ASSERT_THAT(parsed_handle->size(), Eq(1));
+
+  EXPECT_TRUE(*(*handle)[0].GetKey() == *(*parsed_handle)[0].GetKey());
+  EXPECT_TRUE((*handle)[0].GetId() == (*parsed_handle)[0].GetId());
+  EXPECT_TRUE((*handle)[0].GetStatus() == (*parsed_handle)[0].GetStatus());
+}
+
+TEST_F(SerializeKeysetToProtoKeysetFormatTest, SerializeAndParseMultipleKeys) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CmacParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled, /*is_primary=*/false,
+              /*id=*/123))
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/125))
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kDisabled, /*is_primary=*/true,
+              /*id=*/127))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+
+  crypto::tink::util::StatusOr<SecretData> serialization =
+      SerializeKeysetToProtoKeysetFormat(*handle,
+                                         InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<KeysetHandle> parsed_handle = ParseKeysetFromProtoKeysetFormat(
+      SecretDataAsStringView(*serialization), InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_handle, IsOk());
+  ASSERT_THAT(handle->size(), Eq(3));
+  ASSERT_THAT(parsed_handle->size(), Eq(3));
+
+  EXPECT_TRUE(*(*handle)[0].GetKey() == *(*parsed_handle)[0].GetKey());
+  EXPECT_TRUE((*handle)[0].GetId() == (*parsed_handle)[0].GetId());
+  EXPECT_TRUE((*handle)[0].GetStatus() == (*parsed_handle)[0].GetStatus());
+
+  EXPECT_TRUE(*(*handle)[1].GetKey() == *(*parsed_handle)[1].GetKey());
+  EXPECT_TRUE((*handle)[1].GetId() == (*parsed_handle)[1].GetId());
+  EXPECT_TRUE((*handle)[1].GetStatus() == (*parsed_handle)[1].GetStatus());
+
+  EXPECT_TRUE(*(*handle)[2].GetKey() == *(*parsed_handle)[2].GetKey());
+  EXPECT_TRUE((*handle)[2].GetId() == (*parsed_handle)[2].GetId());
+  EXPECT_TRUE((*handle)[2].GetStatus() == (*parsed_handle)[2].GetStatus());
+}
+
+TEST_F(SerializeKeysetToProtoKeysetFormatTest, SerializeNoAccessFails) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CmacParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+
+  crypto::tink::util::StatusOr<std::string> serialization =
+      SerializeKeysetWithoutSecretToProtoKeysetFormat(*handle);
+  ASSERT_THAT(serialization, Not(IsOk()));
+}
+
+TEST_F(SerializeKeysetToProtoKeysetFormatTest, ParseNoAccessFails) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      CmacParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+
+  crypto::tink::util::StatusOr<SecretData> serialization =
+      SerializeKeysetToProtoKeysetFormat(*handle,
+                                         InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<KeysetHandle> parsed_handle =
+      ParseKeysetWithoutSecretFromProtoKeysetFormat(
+          SecretDataAsStringView(*serialization));
+  ASSERT_THAT(parsed_handle, Not(IsOk()));
+}
+
+TEST_F(SerializeKeysetToProtoKeysetFormatTest, TestVector) {
+  std::string serialized_keyset = absl::HexStringToBytes(
+      "0895e59bcc0612680a5c0a2e747970652e676f6f676c65617069732e636f6d2f676f6f67"
+      "6c652e63727970746f2e74696e6b2e486d61634b657912281a20cca20f02278003b3513f"
+      "5d01759ac1302f7d883f2f4a40025532ee1b11f9e587120410100803180110011895e59b"
+      "cc062001");
+  crypto::tink::util::StatusOr<KeysetHandle> keyset_handle =
+      ParseKeysetFromProtoKeysetFormat(serialized_keyset,
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(keyset_handle.status(), IsOk());
+  crypto::tink::util::StatusOr<std::unique_ptr<Mac>> mac =
+      (*keyset_handle).GetPrimitive<Mac>();
+  ASSERT_THAT(mac.status(), IsOk());
+  ASSERT_THAT(
+      (*mac)->VerifyMac(
+          absl::HexStringToBytes("016986f2956092d259136923c6f4323557714ec499"),
+          "data"),
+      IsOk());
+}
+
+TEST_F(SerializeKeysetToProtoKeysetFormatTest, SerializeAndParsePublicKey) {
+  util::StatusOr<internal::LegacyProtoParameters> parameters =
+      EcdsaParameters();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<KeysetHandle> handle =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableParams(
+              *parameters, KeyStatus::kEnabled, /*is_primary=*/true,
+              /*id=*/123))
+          .Build();
+  ASSERT_THAT(handle, IsOk());
+  util::StatusOr<std::unique_ptr<KeysetHandle>> public_handle =
+      handle->GetPublicKeysetHandle();
+  ASSERT_THAT(public_handle, IsOk());
+
+
+  crypto::tink::util::StatusOr<SecretData> serialization1 =
+      SerializeKeysetToProtoKeysetFormat(**public_handle,
+                                         InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization1, IsOk());
+  crypto::tink::util::StatusOr<std::string> serialization2 =
+      SerializeKeysetWithoutSecretToProtoKeysetFormat(**public_handle);
+  ASSERT_THAT(serialization2, IsOk());
+
+  util::StatusOr<KeysetHandle> parsed_handle1 =
+      ParseKeysetFromProtoKeysetFormat(SecretDataAsStringView(*serialization1),
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_handle1, IsOk());
+  util::StatusOr<KeysetHandle> parsed_handle2 =
+      ParseKeysetWithoutSecretFromProtoKeysetFormat(
+          SecretDataAsStringView(*serialization1));
+  ASSERT_THAT(parsed_handle2, IsOk());
+  util::StatusOr<KeysetHandle> parsed_handle3 =
+      ParseKeysetFromProtoKeysetFormat(*serialization2,
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(parsed_handle3, IsOk());
+  util::StatusOr<KeysetHandle> parsed_handle4 =
+      ParseKeysetWithoutSecretFromProtoKeysetFormat(*serialization2);
+  ASSERT_THAT(parsed_handle4, IsOk());
+
+  ASSERT_THAT((*public_handle)->size(), Eq(1));
+  ASSERT_THAT(parsed_handle1->size(), Eq(1));
+  ASSERT_THAT(parsed_handle2->size(), Eq(1));
+  ASSERT_THAT(parsed_handle3->size(), Eq(1));
+  ASSERT_THAT(parsed_handle4->size(), Eq(1));
+
+  // TODO(b/277791403): Replace with KeysetHandle::Entry equality checks.
+  EXPECT_TRUE(*(**public_handle)[0].GetKey() == *(*parsed_handle1)[0].GetKey());
+  EXPECT_TRUE(*(**public_handle)[0].GetKey() == *(*parsed_handle2)[0].GetKey());
+  EXPECT_TRUE(*(**public_handle)[0].GetKey() == *(*parsed_handle3)[0].GetKey());
+  EXPECT_TRUE(*(**public_handle)[0].GetKey() == *(*parsed_handle4)[0].GetKey());
+
+  EXPECT_TRUE((**public_handle)[0].GetId() == (*parsed_handle1)[0].GetId());
+  EXPECT_TRUE((**public_handle)[0].GetId() == (*parsed_handle2)[0].GetId());
+  EXPECT_TRUE((**public_handle)[0].GetId() == (*parsed_handle3)[0].GetId());
+  EXPECT_TRUE((**public_handle)[0].GetId() == (*parsed_handle4)[0].GetId());
+
+  EXPECT_TRUE((**public_handle)[0].GetStatus() ==
+              (*parsed_handle1)[0].GetStatus());
+  EXPECT_TRUE((**public_handle)[0].GetStatus() ==
+              (*parsed_handle2)[0].GetStatus());
+  EXPECT_TRUE((**public_handle)[0].GetStatus() ==
+              (*parsed_handle3)[0].GetStatus());
+  EXPECT_TRUE((**public_handle)[0].GetStatus() ==
+              (*parsed_handle4)[0].GetStatus());
+}
+
+
+}  // namespace
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/public_key_sign.h b/cc/public_key_sign.h
index 4fdb4b1..7c4b110 100644
--- a/cc/public_key_sign.h
+++ b/cc/public_key_sign.h
@@ -39,7 +39,7 @@
   virtual crypto::tink::util::StatusOr<std::string> Sign(
       absl::string_view data) const = 0;
 
-  virtual ~PublicKeySign() {}
+  virtual ~PublicKeySign() = default;
 };
 
 }  // namespace tink
diff --git a/cc/public_key_verify.h b/cc/public_key_verify.h
index 7883caf..0119d1d 100644
--- a/cc/public_key_verify.h
+++ b/cc/public_key_verify.h
@@ -38,7 +38,7 @@
       absl::string_view signature,
       absl::string_view data) const = 0;
 
-  virtual ~PublicKeyVerify() {}
+  virtual ~PublicKeyVerify() = default;
 };
 
 }  // namespace tink
diff --git a/cc/random_access_stream.h b/cc/random_access_stream.h
index b40445a..c5a4dec 100644
--- a/cc/random_access_stream.h
+++ b/cc/random_access_stream.h
@@ -28,8 +28,8 @@
 // like regular files.
 class RandomAccessStream {
  public:
-  RandomAccessStream() {}
-  virtual ~RandomAccessStream() {}
+  RandomAccessStream() = default;
+  virtual ~RandomAccessStream() = default;
 
   // Reads up to 'count' bytes starting at 'position' and writes them
   // to 'dest_buffer'.  'position' must be within the size of the stream,
diff --git a/cc/registry.h b/cc/registry.h
index 29e38d2..5fb7933 100644
--- a/cc/registry.h
+++ b/cc/registry.h
@@ -50,43 +50,6 @@
 // and KeyManagers.
 class Registry {
  public:
-  // Returns a catalogue with the given name (if any found).
-  // Keeps the ownership of the catalogue.
-  template <class P>
-  ABSL_DEPRECATED("Catalogues are not supported anymore.")
-  static crypto::tink::util::StatusOr<const Catalogue<P>*> get_catalogue(
-      absl::string_view catalogue_name) {
-    return internal::RegistryImpl::GlobalInstance().get_catalogue<P>(
-        catalogue_name);
-  }
-
-  // Adds the given 'catalogue' under the specified 'catalogue_name',
-  // to enable custom configuration of key types and key managers.
-  //
-  // Adding a custom catalogue should be a one-time operation,
-  // and fails if the given 'catalogue' tries to override
-  // an existing, different catalogue for the specified name.
-  template <class ConcreteCatalogue>
-  ABSL_DEPRECATED("Catalogues are not supported anymore.")
-  static crypto::tink::util::Status
-      AddCatalogue(absl::string_view catalogue_name,
-                   std::unique_ptr<ConcreteCatalogue> catalogue) {
-    return internal::RegistryImpl::GlobalInstance().AddCatalogue(
-        catalogue_name, catalogue.release());
-  }
-
-  // AddCatalogue has the same functionality as the overload which uses a
-  // unique_ptr and which should be preferred.
-  //
-  // Takes ownership of 'catalogue', which must be non-nullptr (in case of
-  // failure, 'catalogue' is deleted).
-  template <class P>
-  ABSL_DEPRECATED("Use AddCatalogue with a unique_ptr input instead.")
-  static crypto::tink::util::Status
-      AddCatalogue(absl::string_view catalogue_name, Catalogue<P>* catalogue) {
-    return AddCatalogue(catalogue_name, absl::WrapUnique(catalogue));
-  }
-
   // Registers the given 'manager' for the key type 'manager->get_key_type()'.
   template <class ConcreteKeyManager>
   static crypto::tink::util::Status RegisterKeyManager(
@@ -95,23 +58,6 @@
         manager.release(), new_key_allowed);
   }
 
-  // Same functionality as the overload which takes a unique pointer, for
-  // new_key_allowed = true.
-  template <class P>
-  ABSL_DEPRECATED(
-      "Use RegisterKeyManager with a unique_ptr manager and new_key_allowed = "
-      "true instead.")
-  static crypto::tink::util::Status RegisterKeyManager(KeyManager<P>* manager) {
-    return RegisterKeyManager(absl::WrapUnique(manager), true);
-  }
-
-  template <class P>
-  ABSL_DEPRECATED("Use RegisterKeyManager with a unique_ptr manager instead.")
-  static crypto::tink::util::Status RegisterKeyManager(KeyManager<P>* manager,
-                                                       bool new_key_allowed) {
-    return RegisterKeyManager(absl::WrapUnique(manager), new_key_allowed);
-  }
-
   template <class KTManager>
   static crypto::tink::util::Status RegisterKeyTypeManager(
       std::unique_ptr<KTManager> manager, bool new_key_allowed) {
@@ -161,15 +107,6 @@
       const google::crypto::tink::KeyData& key_data) {
     return internal::RegistryImpl::GlobalInstance().GetPrimitive<P>(key_data);
   }
-  // Convenience method for creating a new primitive for the key given
-  // in 'key'.  It looks up a KeyManager identified by type_url,
-  // and calls manager's GetPrimitive(key)-method.
-  template <class P>
-  static crypto::tink::util::StatusOr<std::unique_ptr<P>> GetPrimitive(
-      absl::string_view type_url, const portable_proto::MessageLite& key) {
-    return internal::RegistryImpl::GlobalInstance().GetPrimitive<P>(type_url,
-                                                                    key);
-  }
 
   // Generates a new KeyData for the specified 'key_template'.
   // It looks up a KeyManager identified by key_template.type_url,
diff --git a/cc/restricted_data.h b/cc/restricted_data.h
new file mode 100644
index 0000000..02902b2
--- /dev/null
+++ b/cc/restricted_data.h
@@ -0,0 +1,74 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_RESTRICTED_DATA_H_
+#define TINK_RESTRICTED_DATA_H_
+
+#include "tink/secret_key_access_token.h"
+#include "tink/util/secret_data.h"
+
+namespace crypto {
+namespace tink {
+
+// Stores secret (sensitive) data that is safely destroyed in the event of
+// core dumps (similar to `util::SecretData`) and access restricted via
+// `SecurityKeyAccessToken`.  This class is particularly useful for
+// encapsulating cryptographic key material.
+//
+// Example:
+//     RestrictedData restricted_data(/*num_random_bytes=*/32);
+//     absl::string_view raw_secret =
+//         restricted_data.GetSecret(InsecureSecretKeyAccess::Get());
+class RestrictedData {
+ public:
+  // Copyable and movable.
+  RestrictedData(const RestrictedData& other) = default;
+  RestrictedData& operator=(const RestrictedData& other) = default;
+  RestrictedData(RestrictedData&& other) = default;
+  RestrictedData& operator=(RestrictedData&& other) = default;
+
+  // Creates a new RestrictedData object that wraps `secret`. Note that creating
+  // a `token` requires access to `InsecureSecretKeyAccess::Get()`.
+  explicit RestrictedData(absl::string_view secret, SecretKeyAccessToken token)
+      : secret_(util::SecretDataFromStringView(secret)) {}
+
+  // Creates a new RestrictedData object that wraps a secret containing
+  // `num_random_bytes`. The program will terminate if `num_random_bytes` is a
+  // negative value.
+  explicit RestrictedData(int64_t num_random_bytes);
+
+  // Returns the secret for this RestrictedData object. Note that creating a
+  // `token` requires access to `InsecureSecretKeyAccess::Get()`.
+  absl::string_view GetSecret(SecretKeyAccessToken token) const {
+    return util::SecretDataAsStringView(secret_);
+  }
+
+  int64_t size() const { return secret_.size(); }
+
+  // Constant-time comparison operators.
+  bool operator==(const RestrictedData& other) const;
+  bool operator!=(const RestrictedData& other) const {
+    return !(*this == other);
+  }
+
+ private:
+  util::SecretData secret_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_RESTRICTED_DATA_H_
diff --git a/cc/secret_key_access.h b/cc/secret_key_access.h
index d51e144..52655ad 100644
--- a/cc/secret_key_access.h
+++ b/cc/secret_key_access.h
@@ -14,8 +14,8 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-#ifndef THIRD_PARTY_TINK_SECRET_KEY_ACCESS_H_
-#define THIRD_PARTY_TINK_SECRET_KEY_ACCESS_H_
+#ifndef TINK_SECRET_KEY_ACCESS_H_
+#define TINK_SECRET_KEY_ACCESS_H_
 
 #include "tink/key_access.h"
 
@@ -30,4 +30,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // THIRD_PARTY_TINK_SECRET_KEY_ACCESS_H_
+#endif  // TINK_SECRET_KEY_ACCESS_H_
diff --git a/cc/signature/BUILD.bazel b/cc/signature/BUILD.bazel
index 2e5af69..b882901 100644
--- a/cc/signature/BUILD.bazel
+++ b/cc/signature/BUILD.bazel
@@ -418,7 +418,6 @@
         ":ecdsa_verify_key_manager",
         ":public_key_verify_factory",
         ":signature_config",
-        "//:config",
         "//:crypto_format",
         "//:keyset_handle",
         "//:public_key_verify",
@@ -462,7 +461,6 @@
         ":ecdsa_sign_key_manager",
         ":public_key_sign_factory",
         ":signature_config",
-        "//:config",
         "//:crypto_format",
         "//:keyset_handle",
         "//:public_key_sign",
@@ -680,12 +678,11 @@
         ":rsa_ssa_pss_verify_key_manager",
         ":signature_config",
         ":signature_key_templates",
-        "//:config",
         "//:keyset_handle",
         "//:public_key_sign",
         "//:public_key_verify",
         "//:registry",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:status",
         "//util:test_matchers",
         "//util:test_util",
diff --git a/cc/signature/CMakeLists.txt b/cc/signature/CMakeLists.txt
index 7cbee77..e8db1e7 100644
--- a/cc/signature/CMakeLists.txt
+++ b/cc/signature/CMakeLists.txt
@@ -1,5 +1,7 @@
 tink_module(signature)
 
+add_subdirectory(internal)
+
 tink_cc_library(
   NAME public_key_verify_wrapper
   SRCS
@@ -320,7 +322,6 @@
     signature_config.cc
     signature_config.h
   DEPS
-    tink::signature::ecdsa_sign_key_manager
     tink::signature::ecdsa_verify_key_manager
     tink::signature::ed25519_sign_key_manager
     tink::signature::ed25519_verify_key_manager
@@ -336,6 +337,7 @@
     tink::config::config_util
     tink::config::tink_fips
     tink::util::status
+    tink::signature::ecdsa_sign_key_manager
     tink::proto::config_cc_proto
 )
 
@@ -398,7 +400,6 @@
     tink::signature::public_key_verify_factory
     tink::signature::signature_config
     gmock
-    tink::core::config
     tink::core::crypto_format
     tink::core::keyset_handle
     tink::core::public_key_verify
@@ -440,7 +441,6 @@
     tink::signature::public_key_sign_factory
     tink::signature::signature_config
     gmock
-    tink::core::config
     tink::core::crypto_format
     tink::core::keyset_handle
     tink::core::public_key_sign
@@ -651,12 +651,11 @@
     absl::memory
     absl::status
     crypto
-    tink::core::config
     tink::core::keyset_handle
     tink::core::public_key_sign
     tink::core::public_key_verify
     tink::core::registry
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::status
     tink::util::test_matchers
     tink::util::test_util
diff --git a/cc/signature/ecdsa_sign_key_manager.cc b/cc/signature/ecdsa_sign_key_manager.cc
index bea5312..0165feb 100644
--- a/cc/signature/ecdsa_sign_key_manager.cc
+++ b/cc/signature/ecdsa_sign_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/ecdsa_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -76,6 +77,12 @@
         "Deriving EC keys is not allowed in FIPS mode.");
   }
 
+  util::Status status =
+      ValidateVersion(ecdsa_key_format.version(), get_version());
+  if (!status.ok()) {
+    return status;
+  }
+
   // Extract enough random bytes from the input_stream to match the security
   // level of the EC. Note that the input_stream here must come from a PRF
   // and will not use more bytes than required by the security level of the EC.
diff --git a/cc/signature/ecdsa_sign_key_manager.h b/cc/signature/ecdsa_sign_key_manager.h
index 8f5e195..e618e79 100644
--- a/cc/signature/ecdsa_sign_key_manager.h
+++ b/cc/signature/ecdsa_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_ECDSA_SIGN_KEY_MANAGER_H_
 #define TINK_SIGNATURE_ECDSA_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/ecdsa_sign_key_manager_test.cc b/cc/signature/ecdsa_sign_key_manager_test.cc
index 2fe8a97..859925d 100644
--- a/cc/signature/ecdsa_sign_key_manager_test.cc
+++ b/cc/signature/ecdsa_sign_key_manager_test.cc
@@ -316,6 +316,25 @@
               test::StatusIs(absl::StatusCode::kInvalidArgument));
 }
 
+TEST(EcdsaSignKeyManagerTest, DeriveKeyWithInvalidKeyTemplateVersionFails) {
+  if (!internal::IsBoringSsl()) {
+    GTEST_SKIP()
+        << "Key derivation from an input stream is not supported with OpenSSL";
+  }
+  EcdsaKeyFormat format;
+  format.set_version(1);
+  EcdsaParams* params = format.mutable_params();
+  params->set_hash_type(HashType::SHA256);
+  params->set_curve(EllipticCurveType::NIST_P256);
+  params->set_encoding(EcdsaSignatureEncoding::DER);
+
+  util::IstreamInputStream input_stream{
+      absl::make_unique<std::stringstream>("tooshort")};
+
+  ASSERT_THAT(EcdsaSignKeyManager().DeriveKey(format, &input_stream).status(),
+              test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
 TEST(EcdsaSignKeyManagerTest, DeriveKeyInvalidCurve) {
   if (!internal::IsBoringSsl()) {
     GTEST_SKIP()
diff --git a/cc/signature/ecdsa_verify_key_manager.cc b/cc/signature/ecdsa_verify_key_manager.cc
index f72c05c..7660fb1 100644
--- a/cc/signature/ecdsa_verify_key_manager.cc
+++ b/cc/signature/ecdsa_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/ecdsa_verify_key_manager.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
diff --git a/cc/signature/ecdsa_verify_key_manager.h b/cc/signature/ecdsa_verify_key_manager.h
index 5445c20..075a691 100644
--- a/cc/signature/ecdsa_verify_key_manager.h
+++ b/cc/signature/ecdsa_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_ECDSA_VERIFY_KEY_MANAGER_H_
 #define TINK_SIGNATURE_ECDSA_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/ed25519_sign_key_manager.cc b/cc/signature/ed25519_sign_key_manager.cc
index 661e0e7..962b138 100644
--- a/cc/signature/ed25519_sign_key_manager.cc
+++ b/cc/signature/ed25519_sign_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/signature/ed25519_sign_key_manager.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
diff --git a/cc/signature/ed25519_sign_key_manager.h b/cc/signature/ed25519_sign_key_manager.h
index a02b2ad..12ff8bf 100644
--- a/cc/signature/ed25519_sign_key_manager.h
+++ b/cc/signature/ed25519_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_ED25519_SIGN_KEY_MANAGER_H_
 #define TINK_SIGNATURE_ED25519_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/ed25519_sign_key_manager_test.cc b/cc/signature/ed25519_sign_key_manager_test.cc
index 65250de..060042d 100644
--- a/cc/signature/ed25519_sign_key_manager_test.cc
+++ b/cc/signature/ed25519_sign_key_manager_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/ed25519_sign_key_manager.h"
 
+#include <sstream>
 #include <string>
 
 #include "gmock/gmock.h"
diff --git a/cc/signature/ed25519_verify_key_manager.cc b/cc/signature/ed25519_verify_key_manager.cc
index a4ba197..92f52ab 100644
--- a/cc/signature/ed25519_verify_key_manager.cc
+++ b/cc/signature/ed25519_verify_key_manager.cc
@@ -16,6 +16,8 @@
 
 #include "tink/signature/ed25519_verify_key_manager.h"
 
+#include <memory>
+
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
 #include "tink/public_key_verify.h"
diff --git a/cc/signature/ed25519_verify_key_manager.h b/cc/signature/ed25519_verify_key_manager.h
index 2d0640f..25b1b2d 100644
--- a/cc/signature/ed25519_verify_key_manager.h
+++ b/cc/signature/ed25519_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_ED25519_VERIFY_KEY_MANAGER_H_
 #define TINK_SIGNATURE_ED25519_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/internal/BUILD.bazel b/cc/signature/internal/BUILD.bazel
index 5b01f6e..3d679d8 100644
--- a/cc/signature/internal/BUILD.bazel
+++ b/cc/signature/internal/BUILD.bazel
@@ -1 +1,49 @@
+package(default_visibility = ["//:__subpackages__"])
+
 licenses(["notice"])
+
+cc_library(
+    name = "ecdsa_raw_sign_boringssl",
+    srcs = ["ecdsa_raw_sign_boringssl.cc"],
+    hdrs = ["ecdsa_raw_sign_boringssl.h"],
+    include_prefix = "tink/signature/internal",
+    deps = [
+        "//:public_key_sign",
+        "//internal:bn_util",
+        "//internal:ec_util",
+        "//internal:err_util",
+        "//internal:fips_utils",
+        "//internal:md_util",
+        "//internal:ssl_unique_ptr",
+        "//internal:util",
+        "//subtle:common_enums",
+        "//subtle:subtle_util_boringssl",
+        "//util:errors",
+        "//util:statusor",
+        "@boringssl//:crypto",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "ecdsa_raw_sign_boringssl_test",
+    size = "small",
+    srcs = ["ecdsa_raw_sign_boringssl_test.cc"],
+    tags = ["fips"],
+    deps = [
+        ":ecdsa_raw_sign_boringssl",
+        "//:public_key_sign",
+        "//:public_key_verify",
+        "//internal:ec_util",
+        "//internal:fips_utils",
+        "//subtle:common_enums",
+        "//subtle:ecdsa_verify_boringssl",
+        "//subtle:subtle_util_boringssl",
+        "//util:status",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/signature/internal/CMakeLists.txt b/cc/signature/internal/CMakeLists.txt
index e69de29..0b898c1 100644
--- a/cc/signature/internal/CMakeLists.txt
+++ b/cc/signature/internal/CMakeLists.txt
@@ -0,0 +1,44 @@
+tink_module(signature::internal)
+
+tink_cc_library(
+  NAME ecdsa_raw_sign_boringssl
+  SRCS
+    ecdsa_raw_sign_boringssl.cc
+    ecdsa_raw_sign_boringssl.h
+  DEPS
+    absl::status
+    absl::strings
+    crypto
+    tink::core::public_key_sign
+    tink::internal::bn_util
+    tink::internal::ec_util
+    tink::internal::err_util
+    tink::internal::fips_utils
+    tink::internal::md_util
+    tink::internal::ssl_unique_ptr
+    tink::internal::util
+    tink::subtle::common_enums
+    tink::subtle::subtle_util_boringssl
+    tink::util::errors
+    tink::util::statusor
+)
+
+tink_cc_test(
+  NAME ecdsa_raw_sign_boringssl_test
+  SRCS
+    ecdsa_raw_sign_boringssl_test.cc
+  DEPS
+    tink::signature::internal::ecdsa_raw_sign_boringssl
+    gmock
+    absl::status
+    tink::core::public_key_sign
+    tink::core::public_key_verify
+    tink::internal::ec_util
+    tink::internal::fips_utils
+    tink::subtle::common_enums
+    tink::subtle::ecdsa_verify_boringssl
+    tink::subtle::subtle_util_boringssl
+    tink::util::status
+    tink::util::statusor
+    tink::util::test_matchers
+)
diff --git a/cc/signature/internal/ecdsa_raw_sign_boringssl.cc b/cc/signature/internal/ecdsa_raw_sign_boringssl.cc
new file mode 100644
index 0000000..366c966
--- /dev/null
+++ b/cc/signature/internal/ecdsa_raw_sign_boringssl.cc
@@ -0,0 +1,159 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/signature/internal/ecdsa_raw_sign_boringssl.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "openssl/bn.h"
+#include "openssl/ec.h"
+#include "openssl/ecdsa.h"
+#include "openssl/evp.h"
+#include "tink/internal/bn_util.h"
+#include "tink/internal/ec_util.h"
+#include "tink/internal/err_util.h"
+#include "tink/internal/md_util.h"
+#include "tink/internal/ssl_unique_ptr.h"
+#include "tink/internal/util.h"
+#include "tink/subtle/common_enums.h"
+#include "tink/subtle/subtle_util_boringssl.h"
+#include "tink/util/errors.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+// Transforms ECDSA DER signature encoding to IEEE_P1363 encoding.
+//
+// The IEEE_P1363 signature's format is r || s, where r and s are zero-padded
+// and have the same size in bytes as the order of the curve. For example, for
+// NIST P-256 curve, r and s are zero-padded to 32 bytes.
+//
+// The DER signature is encoded using ASN.1
+// (https://tools.ietf.org/html/rfc5480#appendix-A): ECDSA-Sig-Value :: =
+// SEQUENCE { r INTEGER, s INTEGER }. In particular, the encoding is: 0x30 ||
+// totalLength || 0x02 || r's length || r || 0x02 || s's length || s.
+crypto::tink::util::StatusOr<std::string> DerToIeee(absl::string_view der,
+                                                    const EC_KEY* key) {
+  size_t field_size_in_bytes =
+      (EC_GROUP_get_degree(EC_KEY_get0_group(key)) + 7) / 8;
+
+  const uint8_t* der_ptr = reinterpret_cast<const uint8_t*>(der.data());
+  // Note: d2i_ECDSA_SIG is deprecated in BoringSSL, but it isn't in OpenSSL.
+  internal::SslUniquePtr<ECDSA_SIG> ecdsa(
+      d2i_ECDSA_SIG(nullptr, &der_ptr, der.size()));
+  if (ecdsa == nullptr ||
+      der_ptr != reinterpret_cast<const uint8_t*>(der.data() + der.size())) {
+    return util::Status(absl::StatusCode::kInternal, "d2i_ECDSA_SIG failed");
+  }
+
+  const BIGNUM* r_bn;
+  const BIGNUM* s_bn;
+  ECDSA_SIG_get0(ecdsa.get(), &r_bn, &s_bn);
+  util::StatusOr<std::string> r =
+      internal::BignumToString(r_bn, field_size_in_bytes);
+  if (!r.ok()) {
+    return r.status();
+  }
+  util::StatusOr<std::string> s =
+      internal::BignumToString(s_bn, field_size_in_bytes);
+  if (!s.ok()) {
+    return s.status();
+  }
+  return absl::StrCat(*r, *s);
+}
+
+}  // namespace
+
+// static
+util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>>
+EcdsaRawSignBoringSsl::New(const subtle::SubtleUtilBoringSSL::EcKey& ec_key,
+                           subtle::EcdsaSignatureEncoding encoding) {
+  auto status = internal::CheckFipsCompatibility<EcdsaRawSignBoringSsl>();
+  if (!status.ok()) return status;
+
+  // Check curve.
+  util::StatusOr<internal::SslUniquePtr<EC_GROUP>> group =
+      internal::EcGroupFromCurveType(ec_key.curve);
+  if (!group.ok()) {
+    return group.status();
+  }
+  internal::SslUniquePtr<EC_KEY> key(EC_KEY_new());
+  EC_KEY_set_group(key.get(), group->get());
+
+  // Check key.
+  util::StatusOr<internal::SslUniquePtr<EC_POINT>> pub_key =
+      internal::GetEcPoint(ec_key.curve, ec_key.pub_x, ec_key.pub_y);
+  if (!pub_key.ok()) {
+    return pub_key.status();
+  }
+
+  if (!EC_KEY_set_public_key(key.get(), pub_key->get())) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Invalid public key: ", internal::GetSslErrors()));
+  }
+
+  internal::SslUniquePtr<BIGNUM> priv_key(
+      BN_bin2bn(ec_key.priv.data(), ec_key.priv.size(), nullptr));
+  if (!EC_KEY_set_private_key(key.get(), priv_key.get())) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Invalid private key: ", internal::GetSslErrors()));
+  }
+
+  return {
+      absl::WrapUnique(new EcdsaRawSignBoringSsl(std::move(key), encoding))};
+}
+
+util::StatusOr<std::string> EcdsaRawSignBoringSsl::Sign(
+    absl::string_view data) const {
+  // BoringSSL expects a non-null pointer for data,
+  // regardless of whether the size is 0.
+  data = internal::EnsureStringNonNull(data);
+
+  // Compute the raw signature.
+  std::vector<uint8_t> buffer(ECDSA_size(key_.get()));
+  unsigned int sig_length;
+  if (1 != ECDSA_sign(0 /* unused */,
+                      reinterpret_cast<const uint8_t*>(data.data()),
+                      data.size(), buffer.data(), &sig_length, key_.get())) {
+    return util::Status(absl::StatusCode::kInternal, "Signing failed.");
+  }
+
+  if (encoding_ == subtle::EcdsaSignatureEncoding::IEEE_P1363) {
+    auto status_or_sig = DerToIeee(
+        absl::string_view(reinterpret_cast<char*>(buffer.data()), sig_length),
+        key_.get());
+    if (!status_or_sig.ok()) {
+      return status_or_sig.status();
+    }
+    return status_or_sig.value();
+  }
+
+  return std::string(reinterpret_cast<char*>(buffer.data()), sig_length);
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/signature/internal/ecdsa_raw_sign_boringssl.h b/cc/signature/internal/ecdsa_raw_sign_boringssl.h
new file mode 100644
index 0000000..7c917c4
--- /dev/null
+++ b/cc/signature/internal/ecdsa_raw_sign_boringssl.h
@@ -0,0 +1,65 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_SIGNATURE_INTERNAL_ECDSA_RAW_SIGN_BORINGSSL_H_
+#define TINK_SIGNATURE_INTERNAL_ECDSA_RAW_SIGN_BORINGSSL_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "openssl/ec.h"
+#include "openssl/evp.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/ssl_unique_ptr.h"
+#include "tink/public_key_sign.h"
+#include "tink/subtle/common_enums.h"
+#include "tink/subtle/subtle_util_boringssl.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// ECDSA raw signing using Boring SSL, generating signatures in DER-encoding.
+class EcdsaRawSignBoringSsl : public PublicKeySign {
+ public:
+  static crypto::tink::util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>>
+  New(const crypto::tink::internal::EcKey& ec_key,
+      subtle::EcdsaSignatureEncoding encoding);
+
+  // Computes the signature for 'data'.
+  crypto::tink::util::StatusOr<std::string> Sign(
+      absl::string_view data) const override;
+
+  static constexpr crypto::tink::internal::FipsCompatibility kFipsStatus =
+      crypto::tink::internal::FipsCompatibility::kRequiresBoringCrypto;
+
+ private:
+  EcdsaRawSignBoringSsl(internal::SslUniquePtr<EC_KEY> key,
+                        subtle::EcdsaSignatureEncoding encoding)
+      : key_(std::move(key)), encoding_(encoding) {}
+
+  internal::SslUniquePtr<EC_KEY> key_;
+  subtle::EcdsaSignatureEncoding encoding_;
+};
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_SIGNATURE_INTERNAL_ECDSA_RAW_SIGN_BORINGSSL_H_
diff --git a/cc/signature/internal/ecdsa_raw_sign_boringssl_test.cc b/cc/signature/internal/ecdsa_raw_sign_boringssl_test.cc
new file mode 100644
index 0000000..aaf3221
--- /dev/null
+++ b/cc/signature/internal/ecdsa_raw_sign_boringssl_test.cc
@@ -0,0 +1,296 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/signature/internal/ecdsa_raw_sign_boringssl.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "tink/internal/ec_util.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/public_key_sign.h"
+#include "tink/public_key_verify.h"
+#include "tink/subtle/common_enums.h"
+#include "tink/subtle/ecdsa_verify_boringssl.h"
+#include "tink/subtle/subtle_util_boringssl.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Eq;
+using ::testing::Not;
+using ::testing::SizeIs;
+
+util::StatusOr<std::string> ComputeDigest(subtle::HashType hash_type,
+                                          absl::string_view data) {
+  util::StatusOr<const EVP_MD*> hash = internal::EvpHashFromHashType(hash_type);
+  if (!hash.ok()) return hash.status();
+
+  unsigned int digest_size;
+  uint8_t digest[EVP_MAX_MD_SIZE];
+  if (1 != EVP_Digest(data.data(), data.size(), digest, &digest_size, *hash,
+                      nullptr)) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Could not compute digest.");
+  }
+
+  return std::string(reinterpret_cast<const char*>(digest), digest_size);
+}
+
+TEST(EcdsaRawSignBoringSslTest, VerifySignature) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+  subtle::EcdsaSignatureEncoding encodings[2] = {
+      subtle::EcdsaSignatureEncoding::DER,
+      subtle::EcdsaSignatureEncoding::IEEE_P1363};
+  for (subtle::EcdsaSignatureEncoding encoding : encodings) {
+    util::StatusOr<EcKey> ec_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+        subtle::EllipticCurveType::NIST_P256);
+    ASSERT_THAT(ec_key, IsOk());
+
+    util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>> signer =
+        EcdsaRawSignBoringSsl::New(*ec_key, encoding);
+    ASSERT_THAT(signer, IsOk());
+
+    util::StatusOr<std::unique_ptr<subtle::EcdsaVerifyBoringSsl>> verifier =
+        subtle::EcdsaVerifyBoringSsl::New(*ec_key, subtle::HashType::SHA256,
+                                          encoding);
+    ASSERT_THAT(verifier, IsOk());
+
+    std::string message = "some data to be signed";
+    util::StatusOr<std::string> message_digest =
+        ComputeDigest(subtle::HashType::SHA256, message);
+    ASSERT_THAT(message_digest, IsOk());
+    util::StatusOr<std::string> signature = (*signer)->Sign(*message_digest);
+    ASSERT_THAT(signature, IsOkAndHolds(Not(Eq(message))));
+    EXPECT_THAT((*verifier)->Verify(*signature, message), IsOk());
+  }
+}
+
+TEST(EcdsaRawSignBoringSslTest, VerifySignatureWithEmptyMessage) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+  subtle::EcdsaSignatureEncoding encodings[2] = {
+      subtle::EcdsaSignatureEncoding::DER,
+      subtle::EcdsaSignatureEncoding::IEEE_P1363};
+  for (subtle::EcdsaSignatureEncoding encoding : encodings) {
+    util::StatusOr<EcKey> ec_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+        subtle::EllipticCurveType::NIST_P256);
+    ASSERT_THAT(ec_key, IsOk());
+
+    util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>> signer =
+        EcdsaRawSignBoringSsl::New(*ec_key, encoding);
+    ASSERT_THAT(signer, IsOk());
+
+    util::StatusOr<std::unique_ptr<subtle::EcdsaVerifyBoringSsl>> verifier =
+        subtle::EcdsaVerifyBoringSsl::New(*ec_key, subtle::HashType::SHA256,
+                                          encoding);
+    ASSERT_THAT(verifier, IsOk());
+
+    // Message is a null string_view.
+    const absl::string_view empty_message;
+    util::StatusOr<std::string> empty_message_digest =
+        ComputeDigest(subtle::HashType::SHA256, empty_message);
+    ASSERT_THAT(empty_message_digest, IsOk());
+    util::StatusOr<std::string> empty_msg_signature =
+        (*signer)->Sign(*empty_message_digest);
+    ASSERT_THAT(empty_msg_signature, IsOkAndHolds(Not(Eq(empty_message))));
+    EXPECT_THAT((*verifier)->Verify(*empty_msg_signature, empty_message),
+                IsOk());
+  }
+}
+
+TEST(EcdsaRawSignBoringSslTest, VerifyFailsWithInvalidMessageOrSignature) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+  subtle::EcdsaSignatureEncoding encodings[2] = {
+      subtle::EcdsaSignatureEncoding::DER,
+      subtle::EcdsaSignatureEncoding::IEEE_P1363};
+  for (subtle::EcdsaSignatureEncoding encoding : encodings) {
+    util::StatusOr<EcKey> ec_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+        subtle::EllipticCurveType::NIST_P256);
+    ASSERT_THAT(ec_key, IsOk());
+
+    util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>> signer =
+        EcdsaRawSignBoringSsl::New(*ec_key, encoding);
+    ASSERT_THAT(signer, IsOk());
+
+    util::StatusOr<std::unique_ptr<subtle::EcdsaVerifyBoringSsl>> verifier =
+        subtle::EcdsaVerifyBoringSsl::New(*ec_key, subtle::HashType::SHA256,
+                                          encoding);
+    ASSERT_THAT(verifier, IsOk());
+
+    std::string message = "some data to be signed";
+    util::StatusOr<std::string> message_digest =
+        ComputeDigest(subtle::HashType::SHA256, message);
+    ASSERT_THAT(message_digest, IsOk());
+    util::StatusOr<std::string> signature = (*signer)->Sign(*message_digest);
+    ASSERT_THAT(signature, IsOkAndHolds(Not(Eq(message))));
+    EXPECT_THAT((*verifier)->Verify(*signature, message), IsOk());
+
+    EXPECT_THAT((*verifier)->Verify("some bad signature", message),
+                Not(IsOk()));
+    EXPECT_THAT((*verifier)->Verify(*signature, "some bad message"),
+                Not(IsOk()));
+  }
+}
+
+TEST(EcdsaRawSignBoringSslTest, VerifyFailsWhenEncodingDoesNotMatch) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+  subtle::EcdsaSignatureEncoding encodings[2] = {
+      subtle::EcdsaSignatureEncoding::DER,
+      subtle::EcdsaSignatureEncoding::IEEE_P1363};
+  for (subtle::EcdsaSignatureEncoding encoding : encodings) {
+    util::StatusOr<EcKey> ec_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+        subtle::EllipticCurveType::NIST_P256);
+    ASSERT_THAT(ec_key, IsOk());
+
+    util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>> signer =
+        EcdsaRawSignBoringSsl::New(*ec_key, encoding);
+    ASSERT_THAT(signer, IsOk());
+
+    util::StatusOr<std::unique_ptr<subtle::EcdsaVerifyBoringSsl>> verifier =
+        subtle::EcdsaVerifyBoringSsl::New(
+            *ec_key, subtle::HashType::SHA256,
+            encoding == subtle::EcdsaSignatureEncoding::DER
+                ? subtle::EcdsaSignatureEncoding::IEEE_P1363
+                : subtle::EcdsaSignatureEncoding::DER);
+    ASSERT_THAT(verifier, IsOk());
+
+    std::string message = "some data to be signed";
+    util::StatusOr<std::string> message_digest =
+        ComputeDigest(subtle::HashType::SHA256, message);
+    ASSERT_THAT(message_digest, IsOk());
+    util::StatusOr<std::string> signature = (*signer)->Sign(*message_digest);
+    ASSERT_THAT(signature, IsOkAndHolds(Not(Eq(message))));
+    EXPECT_THAT((*verifier)->Verify(*signature, message), Not(IsOk()));
+  }
+}
+
+TEST(EcdsaRawSignBoringSslTest,
+     SignatureSizesAreCorrectWhenUsingIeeeP136Encoding) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+  subtle::EllipticCurveType curves[3] = {subtle::EllipticCurveType::NIST_P256,
+                                         subtle::EllipticCurveType::NIST_P384,
+                                         subtle::EllipticCurveType::NIST_P521};
+  for (subtle::EllipticCurveType curve : curves) {
+    util::StatusOr<EcKey> ec_key =
+        subtle::SubtleUtilBoringSSL::GetNewEcKey(curve);
+    ASSERT_THAT(ec_key, IsOk());
+
+    util::StatusOr<std::unique_ptr<EcdsaRawSignBoringSsl>> signer =
+        EcdsaRawSignBoringSsl::New(*ec_key,
+                                   subtle::EcdsaSignatureEncoding::IEEE_P1363);
+    ASSERT_THAT(signer, IsOk());
+
+    util::StatusOr<std::unique_ptr<subtle::EcdsaVerifyBoringSsl>> verifier =
+        subtle::EcdsaVerifyBoringSsl::New(
+            *ec_key, subtle::HashType::SHA256,
+            subtle::EcdsaSignatureEncoding::IEEE_P1363);
+    ASSERT_THAT(verifier, IsOk());
+
+    std::string message = "some data to be signed";
+    util::StatusOr<std::string> message_digest =
+        ComputeDigest(subtle::HashType::SHA256, message);
+    ASSERT_THAT(message_digest, IsOk());
+    util::StatusOr<std::string> signature = (*signer)->Sign(*message_digest);
+    ASSERT_THAT(signature, IsOkAndHolds(Not(Eq(message))));
+    EXPECT_THAT((*verifier)->Verify(*signature, message), IsOk());
+
+    // Check signature size.
+    util::StatusOr<int32_t> field_size_in_bytes =
+        internal::EcFieldSizeInBytes(curve);
+    ASSERT_THAT(field_size_in_bytes, IsOk());
+    EXPECT_THAT(*signature, SizeIs(2 * (*field_size_in_bytes)));
+  }
+}
+
+TEST(EcdsaRawSignBoringSslTest, CreateFailsWithBadPublicKey) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+  util::StatusOr<EcKey> ec_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+      subtle::EllipticCurveType::NIST_P256);
+  ASSERT_THAT(ec_key, IsOk());
+
+  ec_key->pub_x += "corrupted public key x coordinate";
+  EXPECT_THAT(
+      EcdsaRawSignBoringSsl::New(*ec_key, subtle::EcdsaSignatureEncoding::DER),
+      Not(IsOk()));
+}
+
+// TODO(bleichen): add Wycheproof tests.
+
+// FIPS-only mode test
+TEST(EcdsaRawSignBoringSslTest, FipsFailWithoutBoringCrypto) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  util::StatusOr<EcKey> p256_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+      subtle::EllipticCurveType::NIST_P256);
+  ASSERT_THAT(p256_key, IsOk());
+  EXPECT_THAT(
+      EcdsaRawSignBoringSsl::New(*p256_key, subtle::EcdsaSignatureEncoding::DER)
+          .status(),
+      StatusIs(absl::StatusCode::kInternal));
+
+  util::StatusOr<EcKey> p384_key = subtle::SubtleUtilBoringSSL::GetNewEcKey(
+      subtle::EllipticCurveType::NIST_P384);
+  ASSERT_THAT(p384_key, IsOk());
+  EXPECT_THAT(
+      EcdsaRawSignBoringSsl::New(*p384_key, subtle::EcdsaSignatureEncoding::DER)
+          .status(),
+      StatusIs(absl::StatusCode::kInternal));
+
+  util::StatusOr<EcKey> p521_key = *subtle::SubtleUtilBoringSSL::GetNewEcKey(
+      subtle::EllipticCurveType::NIST_P521);
+  ASSERT_THAT(p521_key, IsOk());
+  EXPECT_THAT(
+      EcdsaRawSignBoringSsl::New(*p521_key, subtle::EcdsaSignatureEncoding::DER)
+          .status(),
+      StatusIs(absl::StatusCode::kInternal));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/signature/public_key_sign_factory.cc b/cc/signature/public_key_sign_factory.cc
index e0fe718..f44be10 100644
--- a/cc/signature/public_key_sign_factory.cc
+++ b/cc/signature/public_key_sign_factory.cc
@@ -16,6 +16,8 @@
 
 #include "tink/signature/public_key_sign_factory.h"
 
+#include <memory>
+
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
 #include "tink/public_key_sign.h"
diff --git a/cc/signature/public_key_sign_factory.h b/cc/signature/public_key_sign_factory.h
index 35e6e78..30c4b82 100644
--- a/cc/signature/public_key_sign_factory.h
+++ b/cc/signature/public_key_sign_factory.h
@@ -17,6 +17,8 @@
 #ifndef TINK_SIGNATURE_PUBLIC_KEY_SIGN_FACTORY_H_
 #define TINK_SIGNATURE_PUBLIC_KEY_SIGN_FACTORY_H_
 
+#include <memory>
+
 #include "absl/base/macros.h"
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
diff --git a/cc/signature/public_key_sign_factory_test.cc b/cc/signature/public_key_sign_factory_test.cc
index 804002a..e5b2eb4 100644
--- a/cc/signature/public_key_sign_factory_test.cc
+++ b/cc/signature/public_key_sign_factory_test.cc
@@ -20,7 +20,6 @@
 #include <utility>
 
 #include "gtest/gtest.h"
-#include "tink/config.h"
 #include "tink/crypto_format.h"
 #include "tink/keyset_handle.h"
 #include "tink/public_key_sign.h"
@@ -33,7 +32,6 @@
 #include "proto/ecdsa.pb.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddTinkKey;
 using google::crypto::tink::EcdsaPrivateKey;
 using google::crypto::tink::EcdsaSignatureEncoding;
diff --git a/cc/signature/public_key_sign_wrapper.cc b/cc/signature/public_key_sign_wrapper.cc
index 8743396..aeab1cf 100644
--- a/cc/signature/public_key_sign_wrapper.cc
+++ b/cc/signature/public_key_sign_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/public_key_sign_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -63,7 +64,7 @@
   crypto::tink::util::StatusOr<std::string> Sign(
       absl::string_view data) const override;
 
-  ~PublicKeySignSetWrapper() override {}
+  ~PublicKeySignSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<PublicKeySign>> public_key_sign_set_;
diff --git a/cc/signature/public_key_sign_wrapper.h b/cc/signature/public_key_sign_wrapper.h
index ec58c48..f258dad 100644
--- a/cc/signature/public_key_sign_wrapper.h
+++ b/cc/signature/public_key_sign_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_SIGNATURE_PUBLIC_KEY_SIGN_WRAPPER_H_
 #define TINK_SIGNATURE_PUBLIC_KEY_SIGN_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
diff --git a/cc/signature/public_key_sign_wrapper_test.cc b/cc/signature/public_key_sign_wrapper_test.cc
index 4059d61..db7f099 100644
--- a/cc/signature/public_key_sign_wrapper_test.cc
+++ b/cc/signature/public_key_sign_wrapper_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/public_key_sign_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/signature/public_key_verify_factory.cc b/cc/signature/public_key_verify_factory.cc
index dffa6d5..8747e44 100644
--- a/cc/signature/public_key_verify_factory.cc
+++ b/cc/signature/public_key_verify_factory.cc
@@ -16,6 +16,8 @@
 
 #include "tink/signature/public_key_verify_factory.h"
 
+#include <memory>
+
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
 #include "tink/public_key_verify.h"
diff --git a/cc/signature/public_key_verify_factory.h b/cc/signature/public_key_verify_factory.h
index e039a04..af21525 100644
--- a/cc/signature/public_key_verify_factory.h
+++ b/cc/signature/public_key_verify_factory.h
@@ -17,6 +17,8 @@
 #ifndef TINK_SIGNATURE_PUBLIC_KEY_VERIFY_FACTORY_H_
 #define TINK_SIGNATURE_PUBLIC_KEY_VERIFY_FACTORY_H_
 
+#include <memory>
+
 #include "absl/base/macros.h"
 #include "tink/key_manager.h"
 #include "tink/keyset_handle.h"
diff --git a/cc/signature/public_key_verify_factory_test.cc b/cc/signature/public_key_verify_factory_test.cc
index 94564c6..f302e43 100644
--- a/cc/signature/public_key_verify_factory_test.cc
+++ b/cc/signature/public_key_verify_factory_test.cc
@@ -20,7 +20,6 @@
 #include <utility>
 
 #include "gtest/gtest.h"
-#include "tink/config.h"
 #include "tink/crypto_format.h"
 #include "tink/keyset_handle.h"
 #include "tink/public_key_verify.h"
@@ -33,7 +32,6 @@
 #include "proto/ecdsa.pb.h"
 #include "proto/tink.pb.h"
 
-using crypto::tink::TestKeysetHandle;
 using crypto::tink::test::AddTinkKey;
 using google::crypto::tink::EcdsaPublicKey;
 using google::crypto::tink::EcdsaSignatureEncoding;
diff --git a/cc/signature/public_key_verify_wrapper.cc b/cc/signature/public_key_verify_wrapper.cc
index 840aadd..784c0d3 100644
--- a/cc/signature/public_key_verify_wrapper.cc
+++ b/cc/signature/public_key_verify_wrapper.cc
@@ -16,18 +16,19 @@
 
 #include "tink/signature/public_key_verify_wrapper.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "tink/crypto_format.h"
-#include "tink/internal/util.h"
-#include "tink/primitive_set.h"
-#include "tink/public_key_verify.h"
 #include "tink/internal/monitoring_util.h"
 #include "tink/internal/registry_impl.h"
+#include "tink/internal/util.h"
 #include "tink/monitoring/monitoring.h"
+#include "tink/primitive_set.h"
+#include "tink/public_key_verify.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
@@ -65,7 +66,7 @@
   crypto::tink::util::Status Verify(absl::string_view signature,
                                     absl::string_view data) const override;
 
-  ~PublicKeyVerifySetWrapper() override {}
+  ~PublicKeyVerifySetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<PublicKeyVerify>> public_key_verify_set_;
diff --git a/cc/signature/public_key_verify_wrapper.h b/cc/signature/public_key_verify_wrapper.h
index e36d923..3563ec5 100644
--- a/cc/signature/public_key_verify_wrapper.h
+++ b/cc/signature/public_key_verify_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_SIGNATURE_PUBLIC_KEY_VERIFY_WRAPPER_H_
 #define TINK_SIGNATURE_PUBLIC_KEY_VERIFY_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
diff --git a/cc/signature/public_key_verify_wrapper_test.cc b/cc/signature/public_key_verify_wrapper_test.cc
index 7a85945..90f4558 100644
--- a/cc/signature/public_key_verify_wrapper_test.cc
+++ b/cc/signature/public_key_verify_wrapper_test.cc
@@ -115,12 +115,12 @@
                                                     keyset_info.key_info(0));
     ASSERT_TRUE(entry_result.ok());
 
-    pk_verify.reset(new DummyPublicKeyVerify(signature_name_1));
+    pk_verify = std::make_unique<DummyPublicKeyVerify>(signature_name_1);
     entry_result = pk_verify_set->AddPrimitive(std::move(pk_verify),
                                                keyset_info.key_info(1));
     ASSERT_TRUE(entry_result.ok());
 
-    pk_verify.reset(new DummyPublicKeyVerify(signature_name_2));
+    pk_verify = std::make_unique<DummyPublicKeyVerify>(signature_name_2);
     entry_result = pk_verify_set->AddPrimitive(std::move(pk_verify),
                                                keyset_info.key_info(2));
     ASSERT_TRUE(entry_result.ok());
diff --git a/cc/signature/rsa_ssa_pkcs1_sign_key_manager.cc b/cc/signature/rsa_ssa_pkcs1_sign_key_manager.cc
index cd4c1fa..43c2753 100644
--- a/cc/signature/rsa_ssa_pkcs1_sign_key_manager.cc
+++ b/cc/signature/rsa_ssa_pkcs1_sign_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/rsa_ssa_pkcs1_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/rsa_ssa_pkcs1_sign_key_manager.h b/cc/signature/rsa_ssa_pkcs1_sign_key_manager.h
index ea42ca7..2448b76 100644
--- a/cc/signature/rsa_ssa_pkcs1_sign_key_manager.h
+++ b/cc/signature/rsa_ssa_pkcs1_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_RSA_SSA_PKCS1_SIGN_KEY_MANAGER_H_
 #define TINK_SIGNATURE_RSA_SSA_PKCS1_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/rsa_ssa_pkcs1_verify_key_manager.cc b/cc/signature/rsa_ssa_pkcs1_verify_key_manager.cc
index 06f4964..8a62115 100644
--- a/cc/signature/rsa_ssa_pkcs1_verify_key_manager.cc
+++ b/cc/signature/rsa_ssa_pkcs1_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/strings/str_cat.h"
diff --git a/cc/signature/rsa_ssa_pkcs1_verify_key_manager.h b/cc/signature/rsa_ssa_pkcs1_verify_key_manager.h
index 8519021..7c1d0cd 100644
--- a/cc/signature/rsa_ssa_pkcs1_verify_key_manager.h
+++ b/cc/signature/rsa_ssa_pkcs1_verify_key_manager.h
@@ -18,6 +18,7 @@
 #define TINK_SIGNATURE_RSA_SSA_PKCS1_VERIFY_KEY_MANAGER_H_
 
 #include <algorithm>
+#include <memory>
 #include <string>
 #include <vector>
 
diff --git a/cc/signature/rsa_ssa_pss_sign_key_manager.cc b/cc/signature/rsa_ssa_pss_sign_key_manager.cc
index 8d6bd25..18058ce 100644
--- a/cc/signature/rsa_ssa_pss_sign_key_manager.cc
+++ b/cc/signature/rsa_ssa_pss_sign_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/rsa_ssa_pss_sign_key_manager.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/signature/rsa_ssa_pss_sign_key_manager.h b/cc/signature/rsa_ssa_pss_sign_key_manager.h
index 14618de..61ad38b 100644
--- a/cc/signature/rsa_ssa_pss_sign_key_manager.h
+++ b/cc/signature/rsa_ssa_pss_sign_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_RSA_SSA_PSS_SIGN_KEY_MANAGER_H_
 #define TINK_SIGNATURE_RSA_SSA_PSS_SIGN_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/rsa_ssa_pss_verify_key_manager.cc b/cc/signature/rsa_ssa_pss_verify_key_manager.cc
index 7c7aa8c..abe4806 100644
--- a/cc/signature/rsa_ssa_pss_verify_key_manager.cc
+++ b/cc/signature/rsa_ssa_pss_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
diff --git a/cc/signature/rsa_ssa_pss_verify_key_manager.h b/cc/signature/rsa_ssa_pss_verify_key_manager.h
index 3fc234a..5642dcd 100644
--- a/cc/signature/rsa_ssa_pss_verify_key_manager.h
+++ b/cc/signature/rsa_ssa_pss_verify_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_SIGNATURE_RSA_SSA_PSS_VERIFY_KEY_MANAGER_H_
 #define TINK_SIGNATURE_RSA_SSA_PSS_VERIFY_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/memory/memory.h"
diff --git a/cc/signature/signature_config.cc b/cc/signature/signature_config.cc
index ec7f920..206b3f1 100644
--- a/cc/signature/signature_config.cc
+++ b/cc/signature/signature_config.cc
@@ -20,7 +20,6 @@
 #include "tink/config/config_util.h"
 #include "tink/config/tink_fips.h"
 #include "tink/registry.h"
-#include "tink/signature/ecdsa_sign_key_manager.h"
 #include "tink/signature/ecdsa_verify_key_manager.h"
 #include "tink/signature/ed25519_sign_key_manager.h"
 #include "tink/signature/ed25519_verify_key_manager.h"
@@ -31,20 +30,13 @@
 #include "tink/signature/rsa_ssa_pss_sign_key_manager.h"
 #include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
 #include "tink/util/status.h"
+#include "tink/signature/ecdsa_sign_key_manager.h"
 #include "proto/config.pb.h"
 
-using google::crypto::tink::RegistryConfig;
-
 namespace crypto {
 namespace tink {
 
 // static
-const google::crypto::tink::RegistryConfig& SignatureConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
-// static
 util::Status SignatureConfig::Register() {
   // Register primitive wrappers.
   auto status = Registry::RegisterPrimitiveWrapper(
diff --git a/cc/signature/signature_config.h b/cc/signature/signature_config.h
index ca9dab4..d1c333a 100644
--- a/cc/signature/signature_config.h
+++ b/cc/signature/signature_config.h
@@ -37,16 +37,6 @@
 //
 class SignatureConfig {
  public:
-  static constexpr char kPublicKeySignCatalogueName[] = "TinkPublicKeySign";
-  static constexpr char kPublicKeyVerifyCatalogueName[] = "TinkPublicKeyVerify";
-  static constexpr char kPublicKeySignPrimitiveName[] = "PublicKeySign";
-  static constexpr char kPublicKeyVerifyPrimitiveName[] = "PublicKeyVerify";
-
-  // Returns config with implementations of PublicKeySign and PublicKeyVerify
-  // supported in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers PublicKeySign and PublicKeyVerify primitive wrappers, and key
   // managers for all implementations of PublicKeySign and PublicKeyVerify from
   // the current Tink release.
diff --git a/cc/signature/signature_config_test.cc b/cc/signature/signature_config_test.cc
index dbeb022..d0f9b02 100644
--- a/cc/signature/signature_config_test.cc
+++ b/cc/signature/signature_config_test.cc
@@ -25,8 +25,7 @@
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "openssl/crypto.h"
-#include "tink/config.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/keyset_handle.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
@@ -54,7 +53,7 @@
 };
 
 TEST_F(SignatureConfigTest, testBasic) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -81,7 +80,7 @@
 // Tests that the PublicKeySignWrapper has been properly registered and we
 // can wrap primitives.
 TEST_F(SignatureConfigTest, PublicKeySignWrapperRegistered) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -117,7 +116,7 @@
 // Tests that the PublicKeyVerifyWrapper has been properly registered and we
 // can wrap primitives.
 TEST_F(SignatureConfigTest, PublicKeyVerifyWrapperRegistered) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Not supported if FIPS-mode is used and BoringCrypto is "
                     "not available";
   }
@@ -148,7 +147,7 @@
 
 // FIPS-only mode tests
 TEST_F(SignatureConfigTest, RegisterNonFipsTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto.";
   }
 
@@ -173,7 +172,7 @@
 }
 
 TEST_F(SignatureConfigTest, RegisterFipsValidTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto.";
   }
 
diff --git a/cc/signature/signature_key_templates.cc b/cc/signature/signature_key_templates.cc
index a95c3bf..e2e33c9 100644
--- a/cc/signature/signature_key_templates.cc
+++ b/cc/signature/signature_key_templates.cc
@@ -16,6 +16,8 @@
 
 #include "tink/signature/signature_key_templates.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
 #include "openssl/bn.h"
diff --git a/cc/signature/signature_pem_keyset_reader.cc b/cc/signature/signature_pem_keyset_reader.cc
index 0802e0b..62421be 100644
--- a/cc/signature/signature_pem_keyset_reader.cc
+++ b/cc/signature/signature_pem_keyset_reader.cc
@@ -51,7 +51,6 @@
 
 using ::google::crypto::tink::EcdsaParams;
 using ::google::crypto::tink::EcdsaPublicKey;
-using ::google::crypto::tink::EcdsaSignatureEncoding;
 using ::google::crypto::tink::EllipticCurveType;
 using ::google::crypto::tink::EncryptedKeyset;
 using ::google::crypto::tink::HashType;
diff --git a/cc/signature/signature_pem_keyset_reader.h b/cc/signature/signature_pem_keyset_reader.h
index bc26ac7..d0f47df 100644
--- a/cc/signature/signature_pem_keyset_reader.h
+++ b/cc/signature/signature_pem_keyset_reader.h
@@ -17,6 +17,7 @@
 #ifndef TINK_SIGNATURE_SIGNATURE_PEM_KEYSET_READER_H_
 #define TINK_SIGNATURE_SIGNATURE_PEM_KEYSET_READER_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/cc/signature/signature_pem_keyset_reader_test.cc b/cc/signature/signature_pem_keyset_reader_test.cc
index da8ec90..ff4092b 100644
--- a/cc/signature/signature_pem_keyset_reader_test.cc
+++ b/cc/signature/signature_pem_keyset_reader_test.cc
@@ -227,6 +227,18 @@
   return private_key_proto;
 }
 
+PemKey CreatePemKey(absl::string_view serialized_key,
+                    crypto::tink::PemKeyType key_type,
+                    crypto::tink::PemAlgorithm algorithm,
+                    size_t key_size_in_bits,
+                    google::crypto::tink::HashType hash_type) {
+  PemKey pem_key = {
+      /*serialized_key=*/std::string(serialized_key),
+      /*parameters=*/{key_type, algorithm, key_size_in_bits, hash_type},
+  };
+  return pem_key;
+}
+
 // Verify check on PEM array size not zero before creating a reader.
 TEST(SignaturePemKeysetReaderTest, BuildEmptyPemArray) {
   auto builder = SignaturePemKeysetReaderBuilder(
@@ -240,11 +252,9 @@
 TEST(SignaturePemKeysetReaderTest, ReadEncryptedUnsupported) {
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
-  builder.Add({.serialized_key = std::string(kRsaPublicKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA384}});
+  builder.Add(CreatePemKey(kRsaPublicKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA384));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -260,16 +270,12 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kRsaPublicKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA384}});
-  builder.Add({.serialized_key = std::string(kRsaPublicKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kRsaPublicKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA384));
+  builder.Add(CreatePemKey(kRsaPublicKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA256));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -330,16 +336,12 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_SIGN);
 
-  builder.Add({.serialized_key = std::string(kRsaPrivateKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA256}});
-  builder.Add({.serialized_key = std::string(kRsaPrivateKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA384}});
+  builder.Add(CreatePemKey(kRsaPrivateKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA256));
+  builder.Add(CreatePemKey(kRsaPrivateKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA384));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -399,11 +401,9 @@
 TEST(SignaturePemKeysetReaderTest, ReadRsaPrivateKeyKeyTypeMismatch) {
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_SIGN);
-  builder.Add({.serialized_key = std::string(kRsaPublicKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA384}});
+  builder.Add(CreatePemKey(kRsaPublicKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA384));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -420,11 +420,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kRsaPrivateKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kRsaPrivateKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA256));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -440,11 +438,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kRsaPublicKey1024),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 1024,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kRsaPublicKey1024, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/1024,
+                           HashType::SHA256));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -461,11 +457,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kRsaPublicKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 3072,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kRsaPublicKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/3072,
+                           HashType::SHA256));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -481,11 +475,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kRsaPublicKey2048),
-               .parameters = {.key_type = PemKeyType::PEM_RSA,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 2048,
-                              .hash_type = HashType::SHA1}});
+  builder.Add(CreatePemKey(kRsaPublicKey2048, PemKeyType::PEM_RSA,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/2048,
+                           HashType::SHA1));
 
   auto keyset_reader_or = builder.Build();
   ASSERT_THAT(keyset_reader_or, IsOk());
@@ -500,17 +492,13 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kEcdsaP256PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_IEEE,
-                              .key_size_in_bits = 256,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kEcdsaP256PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_IEEE, /*key_size_in_bits=*/256,
+                           HashType::SHA256));
 
-  builder.Add({.serialized_key = std::string(kEcdsaP256PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_DER,
-                              .key_size_in_bits = 256,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kEcdsaP256PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_DER, /*key_size_in_bits=*/256,
+                           HashType::SHA256));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
@@ -565,11 +553,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kEcdsaP256PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_IEEE,
-                              .key_size_in_bits = 256,
-                              .hash_type = HashType::SHA512}});
+  builder.Add(CreatePemKey(kEcdsaP256PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_IEEE, /*key_size_in_bits=*/256,
+                           HashType::SHA512));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
@@ -582,11 +568,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kEcdsaP256PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_IEEE,
-                              .key_size_in_bits = 512,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kEcdsaP256PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_IEEE, /*key_size_in_bits=*/512,
+                           HashType::SHA256));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
@@ -599,11 +583,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kEcdsaP256PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::RSASSA_PSS,
-                              .key_size_in_bits = 256,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kEcdsaP256PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::RSASSA_PSS, /*key_size_in_bits=*/256,
+                           HashType::SHA256));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
@@ -616,11 +598,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kEd25519PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_IEEE,
-                              .key_size_in_bits = 256,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kEd25519PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_IEEE, /*key_size_in_bits=*/256,
+                           HashType::SHA256));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
@@ -633,11 +613,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kSecp256k1PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_IEEE,
-                              .key_size_in_bits = 256,
-                              .hash_type = HashType::SHA256}});
+  builder.Add(CreatePemKey(kSecp256k1PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_IEEE, /*key_size_in_bits=*/256,
+                           HashType::SHA256));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
@@ -664,11 +642,9 @@
   auto builder = SignaturePemKeysetReaderBuilder(
       SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY);
 
-  builder.Add({.serialized_key = std::string(kEcdsaP384PublicKey),
-               .parameters = {.key_type = PemKeyType::PEM_EC,
-                              .algorithm = PemAlgorithm::ECDSA_IEEE,
-                              .key_size_in_bits = 384,
-                              .hash_type = HashType::SHA384}});
+  builder.Add(CreatePemKey(kEcdsaP384PublicKey, PemKeyType::PEM_EC,
+                           PemAlgorithm::ECDSA_IEEE, /*key_size_in_bits=*/384,
+                           HashType::SHA384));
 
   auto reader = builder.Build();
   ASSERT_THAT(reader, IsOk());
diff --git a/cc/streaming_aead.h b/cc/streaming_aead.h
index 139bfdb..81b6333 100644
--- a/cc/streaming_aead.h
+++ b/cc/streaming_aead.h
@@ -77,7 +77,7 @@
       std::unique_ptr<crypto::tink::RandomAccessStream> ciphertext_source,
       absl::string_view associated_data) const = 0;
 
-  virtual ~StreamingAead() {}
+  virtual ~StreamingAead() = default;
 };
 
 }  // namespace tink
diff --git a/cc/streaming_mac.h b/cc/streaming_mac.h
index 037b148..3ca4f69 100644
--- a/cc/streaming_mac.h
+++ b/cc/streaming_mac.h
@@ -44,7 +44,7 @@
   virtual util::StatusOr<std::unique_ptr<OutputStreamWithResult<util::Status>>>
   NewVerifyMacOutputStream(const std::string& mac_value) const = 0;
 
-  virtual ~StreamingMac() {}
+  virtual ~StreamingMac() = default;
 };
 
 }  // namespace tink
diff --git a/cc/streamingaead/BUILD.bazel b/cc/streamingaead/BUILD.bazel
index a1d469b..8cf9667 100644
--- a/cc/streamingaead/BUILD.bazel
+++ b/cc/streamingaead/BUILD.bazel
@@ -202,17 +202,24 @@
     size = "small",
     srcs = ["streaming_aead_wrapper_test.cc"],
     deps = [
+        ":aes_gcm_hkdf_streaming_key_manager",
+        ":streaming_aead_config",
         ":streaming_aead_wrapper",
         "//:input_stream",
+        "//:insecure_secret_key_access",
         "//:output_stream",
         "//:primitive_set",
+        "//:proto_keyset_format",
         "//:random_access_stream",
         "//:streaming_aead",
+        "//internal:test_random_access_stream",
+        "//proto:aes_gcm_hkdf_streaming_cc_proto",
+        "//proto:common_cc_proto",
         "//proto:tink_cc_proto",
         "//subtle:random",
+        "//subtle:streaming_aead_test_util",
         "//subtle:test_util",
         "//util:buffer",
-        "//util:file_random_access_stream",
         "//util:istream_input_stream",
         "//util:ostream_output_stream",
         "//util:status",
@@ -308,7 +315,6 @@
         ":aes_gcm_hkdf_streaming_key_manager",
         ":streaming_aead_config",
         ":streaming_aead_key_templates",
-        "//:config",
         "//:keyset_handle",
         "//:registry",
         "//:streaming_aead",
@@ -376,10 +382,10 @@
         "//:primitive_set",
         "//:random_access_stream",
         "//:streaming_aead",
+        "//internal:test_random_access_stream",
         "//proto:tink_cc_proto",
         "//subtle:random",
         "//subtle:test_util",
-        "//util:file_random_access_stream",
         "//util:ostream_output_stream",
         "//util:status",
         "//util:test_matchers",
@@ -418,10 +424,8 @@
     deps = [
         ":shared_random_access_stream",
         "//:random_access_stream",
-        "//util:buffer",
-        "//util:file_random_access_stream",
-        "//util:status",
-        "//util:test_util",
+        "//internal:test_random_access_stream",
+        "//subtle:random",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
diff --git a/cc/streamingaead/CMakeLists.txt b/cc/streamingaead/CMakeLists.txt
index d4d817e..889e017 100644
--- a/cc/streamingaead/CMakeLists.txt
+++ b/cc/streamingaead/CMakeLists.txt
@@ -188,25 +188,32 @@
   SRCS
     streaming_aead_wrapper_test.cc
   DEPS
+    tink::streamingaead::aes_gcm_hkdf_streaming_key_manager
+    tink::streamingaead::streaming_aead_config
     tink::streamingaead::streaming_aead_wrapper
     gmock
     absl::memory
     absl::status
     absl::strings
     tink::core::input_stream
+    tink::core::insecure_secret_key_access
     tink::core::output_stream
     tink::core::primitive_set
+    tink::core::proto_keyset_format
     tink::core::random_access_stream
     tink::core::streaming_aead
+    tink::internal::test_random_access_stream
     tink::subtle::random
+    tink::subtle::streaming_aead_test_util
     tink::subtle::test_util
     tink::util::buffer
-    tink::util::file_random_access_stream
     tink::util::istream_input_stream
     tink::util::ostream_output_stream
     tink::util::status
     tink::util::test_matchers
     tink::util::test_util
+    tink::proto::aes_gcm_hkdf_streaming_cc_proto
+    tink::proto::common_cc_proto
     tink::proto::tink_cc_proto
 )
 
@@ -292,7 +299,6 @@
     gmock
     absl::memory
     absl::status
-    tink::core::config
     tink::core::keyset_handle
     tink::core::registry
     tink::core::streaming_aead
@@ -358,9 +364,9 @@
     tink::core::primitive_set
     tink::core::random_access_stream
     tink::core::streaming_aead
+    tink::internal::test_random_access_stream
     tink::subtle::random
     tink::subtle::test_util
-    tink::util::file_random_access_stream
     tink::util::ostream_output_stream
     tink::util::status
     tink::util::test_matchers
@@ -397,8 +403,6 @@
     absl::memory
     absl::strings
     tink::core::random_access_stream
-    tink::util::buffer
-    tink::util::file_random_access_stream
-    tink::util::status
-    tink::util::test_util
+    tink::internal::test_random_access_stream
+    tink::subtle::random
 )
diff --git a/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.cc b/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.cc
index a5e5eee..020e4ba 100644
--- a/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.cc
+++ b/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.cc
@@ -71,6 +71,10 @@
     return Status(absl::StatusCode::kInvalidArgument,
                   "ciphertext_segment_size too small");
   }
+  if (params.ciphertext_segment_size() > 0x7fffffff) {
+    return Status(absl::StatusCode::kInvalidArgument,
+                  "ciphertext_segment_size too big");
+  }
   return ValidateAesKeySize(params.derived_key_size());
 }
 
diff --git a/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.h b/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.h
index 859dd18..1c0ea91 100644
--- a/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.h
+++ b/cc/streamingaead/aes_ctr_hmac_streaming_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_STREAMINGAEAD_AES_CTR_HMAC_STREAMING_KEY_MANAGER_H_
 #define TINK_STREAMINGAEAD_AES_CTR_HMAC_STREAMING_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -97,7 +98,7 @@
       const google::crypto::tink::AesCtrHmacStreamingKeyFormat& key_format,
       InputStream* input_stream) const override;
 
-  ~AesCtrHmacStreamingKeyManager() override {}
+  ~AesCtrHmacStreamingKeyManager() override = default;
 
  private:
   const std::string key_type_ = absl::StrCat(
diff --git a/cc/streamingaead/aes_ctr_hmac_streaming_key_manager_test.cc b/cc/streamingaead/aes_ctr_hmac_streaming_key_manager_test.cc
index f962c3f..231ea46 100644
--- a/cc/streamingaead/aes_ctr_hmac_streaming_key_manager_test.cc
+++ b/cc/streamingaead/aes_ctr_hmac_streaming_key_manager_test.cc
@@ -48,7 +48,6 @@
 using ::google::crypto::tink::AesCtrHmacStreamingKey;
 using ::google::crypto::tink::AesCtrHmacStreamingKeyFormat;
 using ::google::crypto::tink::HashType;
-using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
@@ -226,6 +225,20 @@
                        HasSubstr("ciphertext_segment_size")));
 }
 
+TEST(AesCtrHmacStreamingKeyManagerTest, ValidateKeyFormatTooLargeSegment) {
+  AesCtrHmacStreamingKeyFormat key_format;
+  key_format.set_key_size(32);
+  key_format.mutable_params()->set_derived_key_size(32);
+  key_format.mutable_params()->set_hkdf_hash_type(HashType::SHA256);
+  key_format.mutable_params()->set_ciphertext_segment_size(2147483648);
+  key_format.mutable_params()->mutable_hmac_params()->
+      set_hash(HashType::SHA256);
+  key_format.mutable_params()->mutable_hmac_params()->set_tag_size(32);
+  EXPECT_THAT(AesCtrHmacStreamingKeyManager().ValidateKeyFormat(key_format),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("ciphertext_segment_size too big")));
+}
+
 TEST(AesCtrHmacStreamingKeyManagerTest, CreateKey) {
   AesCtrHmacStreamingKeyFormat key_format;
   key_format.set_key_size(32);
diff --git a/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager.h b/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager.h
index 418e1a0..03f5a79 100644
--- a/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager.h
+++ b/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager.h
@@ -16,6 +16,7 @@
 #ifndef TINK_STREAMINGAEAD_AES_GCM_HKDF_STREAMING_KEY_MANAGER_H_
 #define TINK_STREAMINGAEAD_AES_GCM_HKDF_STREAMING_KEY_MANAGER_H_
 
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -93,7 +94,7 @@
       const google::crypto::tink::AesGcmHkdfStreamingKeyFormat& key_format,
       InputStream* input_stream) const override;
 
-  ~AesGcmHkdfStreamingKeyManager() override {}
+  ~AesGcmHkdfStreamingKeyManager() override = default;
 
  private:
   const std::string key_type_ = absl::StrCat(
diff --git a/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager_test.cc b/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager_test.cc
index 183012c..ea2d10f 100644
--- a/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager_test.cc
+++ b/cc/streamingaead/aes_gcm_hkdf_streaming_key_manager_test.cc
@@ -51,7 +51,6 @@
 using ::google::crypto::tink::AesGcmHkdfStreamingKey;
 using ::google::crypto::tink::AesGcmHkdfStreamingKeyFormat;
 using ::google::crypto::tink::HashType;
-using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
diff --git a/cc/streamingaead/buffered_input_stream.cc b/cc/streamingaead/buffered_input_stream.cc
index 5654a4f..acbba47 100644
--- a/cc/streamingaead/buffered_input_stream.cc
+++ b/cc/streamingaead/buffered_input_stream.cc
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <cstring>
+#include <memory>
 #include <utility>
 #include <vector>
 
@@ -135,9 +136,7 @@
   return status_;
 }
 
-
-BufferedInputStream::~BufferedInputStream() {
-}
+BufferedInputStream::~BufferedInputStream() = default;
 
 int64_t BufferedInputStream::Position() const {
   if (direct_access_) return input_stream_->Position();
diff --git a/cc/streamingaead/buffered_input_stream_test.cc b/cc/streamingaead/buffered_input_stream_test.cc
index ff80d8b..14792a3 100644
--- a/cc/streamingaead/buffered_input_stream_test.cc
+++ b/cc/streamingaead/buffered_input_stream_test.cc
@@ -17,6 +17,7 @@
 #include "tink/streamingaead/buffered_input_stream.h"
 
 #include <algorithm>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
diff --git a/cc/streamingaead/decrypting_input_stream.cc b/cc/streamingaead/decrypting_input_stream.cc
index d09d998..c5e459d 100644
--- a/cc/streamingaead/decrypting_input_stream.cc
+++ b/cc/streamingaead/decrypting_input_stream.cc
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <cstring>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -42,6 +43,8 @@
 using util::Status;
 using util::StatusOr;
 
+using StreamingAeadEntry = PrimitiveSet<StreamingAead>::Entry<StreamingAead>;
+
 // static
 StatusOr<std::unique_ptr<InputStream>> DecryptingInputStream::New(
     std::shared_ptr<PrimitiveSet<StreamingAead>> primitives,
@@ -68,12 +71,10 @@
   }
   // Matching has not been attempted yet, so try it now.
   attempted_matching_ = true;
-  auto raw_primitives_result = primitives_->get_raw_primitives();
-  if (!raw_primitives_result.ok()) {
-    return Status(absl::StatusCode::kInternal, "No RAW primitives found");
-  }
-  for (auto& primitive : *(raw_primitives_result.value())) {
-    StreamingAead& streaming_aead = primitive->get_primitive();
+  std::vector<StreamingAeadEntry*> all_primitives = primitives_->get_all();
+
+  for (const StreamingAeadEntry* entry : all_primitives) {
+    StreamingAead& streaming_aead = entry->get_primitive();
     auto shared_ct = absl::make_unique<SharedInputStream>(
         buffered_ct_source_.get());
     auto decrypting_stream_result = streaming_aead.NewDecryptingStream(
diff --git a/cc/streamingaead/decrypting_input_stream.h b/cc/streamingaead/decrypting_input_stream.h
index 61bb2e6..ae3072f 100644
--- a/cc/streamingaead/decrypting_input_stream.h
+++ b/cc/streamingaead/decrypting_input_stream.h
@@ -48,7 +48,7 @@
       std::unique_ptr<crypto::tink::InputStream> ciphertext_source,
       absl::string_view associated_data);
 
-  ~DecryptingInputStream() override {}
+  ~DecryptingInputStream() override = default;
   util::StatusOr<int> Next(const void** data) override;
   void BackUp(int count) override;
   int64_t Position() const override;
diff --git a/cc/streamingaead/decrypting_input_stream_test.cc b/cc/streamingaead/decrypting_input_stream_test.cc
index 0918ac8..04368ce 100644
--- a/cc/streamingaead/decrypting_input_stream_test.cc
+++ b/cc/streamingaead/decrypting_input_stream_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/streamingaead/decrypting_input_stream.h"
 
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
diff --git a/cc/streamingaead/decrypting_random_access_stream.cc b/cc/streamingaead/decrypting_random_access_stream.cc
index df7e9a1..6a5e72e 100644
--- a/cc/streamingaead/decrypting_random_access_stream.cc
+++ b/cc/streamingaead/decrypting_random_access_stream.cc
@@ -16,7 +16,9 @@
 
 #include "tink/streamingaead/decrypting_random_access_stream.h"
 
+#include <memory>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
@@ -39,6 +41,8 @@
 using util::Status;
 using util::StatusOr;
 
+using StreamingAeadEntry = PrimitiveSet<StreamingAead>::Entry<StreamingAead>;
+
 // static
 StatusOr<std::unique_ptr<RandomAccessStream>> DecryptingRandomAccessStream::New(
     std::shared_ptr<PrimitiveSet<StreamingAead>> primitives,
@@ -97,12 +101,9 @@
                   "Did not find a decrypter matching the ciphertext stream.");
   }
   attempted_matching_ = true;
-  auto raw_primitives_result = primitives_->get_raw_primitives();
-  if (!raw_primitives_result.ok()) {
-    return Status(absl::StatusCode::kInternal, "No RAW primitives found");
-  }
-  for (auto& primitive : *(raw_primitives_result.value())) {
-    StreamingAead& streaming_aead = primitive->get_primitive();
+  std::vector<StreamingAeadEntry*> all_primitives = primitives_->get_all();
+  for (const StreamingAeadEntry* entry : all_primitives) {
+    StreamingAead& streaming_aead = entry->get_primitive();
     auto shared_ct = absl::make_unique<SharedRandomAccessStream>(
         ciphertext_source_.get());
     auto decrypting_stream_result =
diff --git a/cc/streamingaead/decrypting_random_access_stream.h b/cc/streamingaead/decrypting_random_access_stream.h
index a78a719..ad409a1 100644
--- a/cc/streamingaead/decrypting_random_access_stream.h
+++ b/cc/streamingaead/decrypting_random_access_stream.h
@@ -50,7 +50,7 @@
       std::unique_ptr<crypto::tink::RandomAccessStream> ciphertext_source,
       absl::string_view associated_data);
 
-  ~DecryptingRandomAccessStream() override {}
+  ~DecryptingRandomAccessStream() override = default;
   crypto::tink::util::Status PRead(int64_t position, int count,
       crypto::tink::util::Buffer* dest_buffer) override;
   crypto::tink::util::StatusOr<int64_t> size() override;
diff --git a/cc/streamingaead/decrypting_random_access_stream_test.cc b/cc/streamingaead/decrypting_random_access_stream_test.cc
index 496fbbf..232e2db 100644
--- a/cc/streamingaead/decrypting_random_access_stream_test.cc
+++ b/cc/streamingaead/decrypting_random_access_stream_test.cc
@@ -16,23 +16,25 @@
 
 #include "tink/streamingaead/decrypting_random_access_stream.h"
 
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
 #include <vector>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_random_access_stream.h"
 #include "tink/output_stream.h"
 #include "tink/primitive_set.h"
 #include "tink/random_access_stream.h"
 #include "tink/streaming_aead.h"
 #include "tink/subtle/random.h"
 #include "tink/subtle/test_util.h"
-#include "tink/util/file_random_access_stream.h"
 #include "tink/util/ostream_output_stream.h"
 #include "tink/util/status.h"
 #include "tink/util/test_matchers.h"
@@ -53,16 +55,6 @@
 using subtle::test::WriteToStream;
 using testing::HasSubstr;
 
-// Creates a RandomAccessStream with the specified contents.
-std::unique_ptr<RandomAccessStream> GetRandomAccessStream(
-    absl::string_view contents) {
-  static int index = 1;
-  std::string filename = absl::StrCat("stream_data_file_", index, ".txt");
-  index++;
-  int input_fd = test::GetTestFileDescriptor(filename, contents);
-  return {absl::make_unique<util::FileRandomAccessStream>(input_fd)};
-}
-
 // Creates an RandomAccessStream that contains ciphertext resulting
 // from encryption of 'pt' with 'aad' as associated data, using 'saead'.
 std::unique_ptr<RandomAccessStream> GetCiphertextSource(
@@ -81,27 +73,7 @@
   EXPECT_THAT(WriteToStream(enc_stream_result.value().get(), pt), IsOk());
 
   // Return the ciphertext as RandomAccessStream.
-  return GetRandomAccessStream(ct_buf->str());
-}
-
-// Reads the entire 'ra_stream', until no more bytes can be read,
-// and puts the read bytes into 'contents'.
-// Returns the status of the last ra_stream->PRead()-operation.
-util::Status ReadAll(RandomAccessStream* ra_stream, std::string* contents) {
-  int chunk_size = 42;
-  contents->clear();
-  auto buffer = std::move(util::Buffer::New(chunk_size).value());
-  int64_t position = 0;
-  auto status = ra_stream->PRead(position, chunk_size, buffer.get());
-  while (status.ok()) {
-    contents->append(buffer->get_mem_block(), buffer->size());
-    position = contents->size();
-    status = ra_stream->PRead(position, chunk_size, buffer.get());
-  }
-  if (status.code() == absl::StatusCode::kOutOfRange) {  // EOF
-    EXPECT_EQ(0, buffer->size());
-  }
-  return status;
+  return std::make_unique<internal::TestRandomAccessStream>(ct_buf->str());
 }
 
 // A container for specification of instances of DummyStreamingAead
@@ -172,7 +144,8 @@
         EXPECT_THAT(dec_stream_result, IsOk());
         auto dec_stream = std::move(dec_stream_result.value());
         std::string decrypted;
-        auto status = ReadAll(dec_stream.get(), &decrypted);
+        auto status = internal::ReadAllFromRandomAccessStream(dec_stream.get(),
+                                                              decrypted);
         EXPECT_THAT(status, StatusIs(absl::StatusCode::kOutOfRange,
                                      HasSubstr("EOF")));
         EXPECT_EQ(pt_size, dec_stream->size().value());
@@ -224,8 +197,11 @@
                                       ", position = ", position,
                                       ", chunk_size = ", chunk_size));
             auto buffer = std::move(util::Buffer::New(chunk_size).value());
-            auto status = dec_stream->PRead(position, chunk_size, buffer.get());
-            EXPECT_THAT(status, IsOk());
+            util::Status status =
+                dec_stream->PRead(position, chunk_size, buffer.get());
+            EXPECT_THAT(status,
+                        testing::AnyOf(
+                            IsOk(), StatusIs(absl::StatusCode::kOutOfRange)));
             EXPECT_EQ(std::min(chunk_size, pt_size - position), buffer->size());
             EXPECT_EQ(0, std::memcmp(plaintext.data() + position,
                                      buffer->get_mem_block(), buffer->size()));
@@ -320,7 +296,8 @@
           saead_set, std::move(ct), "wrong aad");
       EXPECT_THAT(dec_stream_result, IsOk());
       std::string decrypted;
-      auto status = ReadAll(dec_stream_result.value().get(), &decrypted);
+      auto status = internal::ReadAllFromRandomAccessStream(
+          dec_stream_result.value().get(), decrypted);
       EXPECT_THAT(status, StatusIs(absl::StatusCode::kInvalidArgument));
     }
   }
@@ -344,20 +321,22 @@
       SCOPED_TRACE(absl::StrCat("pt_size = ", pt_size,
                                 ", aad = '", aad, "'"));
       // Try decrypting a wrong ciphertext.
-      auto wrong_ct =
-          GetRandomAccessStream(subtle::Random::GetRandomBytes(pt_size));
+      auto wrong_ct = std::make_unique<internal::TestRandomAccessStream>(
+          subtle::Random::GetRandomBytes(pt_size));
       auto dec_stream_result = DecryptingRandomAccessStream::New(
           saead_set, std::move(wrong_ct), aad);
       EXPECT_THAT(dec_stream_result, IsOk());
       std::string decrypted;
-      auto status = ReadAll(dec_stream_result.value().get(), &decrypted);
+      auto status = internal::ReadAllFromRandomAccessStream(
+          dec_stream_result.value().get(), decrypted);
       EXPECT_THAT(status, StatusIs(absl::StatusCode::kInvalidArgument));
     }
   }
 }
 
 TEST(DecryptingRandomAccessStreamTest, NullPrimitiveSet) {
-  auto ct_stream = GetRandomAccessStream("some ciphertext contents");
+  auto ct_stream = std::make_unique<internal::TestRandomAccessStream>(
+      "some ciphertext contents");
   auto dec_stream_result = DecryptingRandomAccessStream::New(
           nullptr, std::move(ct_stream), "some aad");
   EXPECT_THAT(dec_stream_result.status(),
diff --git a/cc/streamingaead/shared_input_stream.h b/cc/streamingaead/shared_input_stream.h
index 32c9755..597d4a8 100644
--- a/cc/streamingaead/shared_input_stream.h
+++ b/cc/streamingaead/shared_input_stream.h
@@ -35,7 +35,7 @@
       crypto::tink::InputStream* input_stream)
       : input_stream_(input_stream) {}
 
-  ~SharedInputStream() override {}
+  ~SharedInputStream() override = default;
 
   crypto::tink::util::StatusOr<int> Next(const void** data) override {
     return input_stream_->Next(data);
diff --git a/cc/streamingaead/shared_input_stream_test.cc b/cc/streamingaead/shared_input_stream_test.cc
index 27a0f19..aef6638 100644
--- a/cc/streamingaead/shared_input_stream_test.cc
+++ b/cc/streamingaead/shared_input_stream_test.cc
@@ -17,6 +17,7 @@
 #include "tink/streamingaead/shared_input_stream.h"
 
 #include <algorithm>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
diff --git a/cc/streamingaead/shared_random_access_stream.h b/cc/streamingaead/shared_random_access_stream.h
index df84d0f..701be1a 100644
--- a/cc/streamingaead/shared_random_access_stream.h
+++ b/cc/streamingaead/shared_random_access_stream.h
@@ -38,7 +38,7 @@
       crypto::tink::RandomAccessStream* random_access_stream)
       : random_access_stream_(random_access_stream) {}
 
-  ~SharedRandomAccessStream() override {}
+  ~SharedRandomAccessStream() override = default;
 
   crypto::tink::util::Status PRead(
       int64_t position, int count,
diff --git a/cc/streamingaead/shared_random_access_stream_test.cc b/cc/streamingaead/shared_random_access_stream_test.cc
index f0b3269..ddbb5b3 100644
--- a/cc/streamingaead/shared_random_access_stream_test.cc
+++ b/cc/streamingaead/shared_random_access_stream_test.cc
@@ -22,53 +22,28 @@
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
+#include "tink/internal/test_random_access_stream.h"
 #include "tink/random_access_stream.h"
-#include "tink/util/buffer.h"
-#include "tink/util/file_random_access_stream.h"
-#include "tink/util/status.h"
-#include "tink/util/test_util.h"
+#include "tink/subtle/random.h"
 
 namespace crypto {
 namespace tink {
 namespace streamingaead {
 namespace {
 
-// Reads the entire 'ra_stream' in chunks of size 'chunk_size',
-// until no more bytes can be read, and puts the read bytes into 'contents'.
-// Returns the status of the last ra_stream->Next()-operation.
-util::Status ReadAll(RandomAccessStream* ra_stream, int chunk_size,
-                     std::string* contents) {
-  contents->clear();
-  auto buffer = std::move(util::Buffer::New(chunk_size).value());
-  int64_t position = 0;
-  auto status = ra_stream->PRead(position, chunk_size, buffer.get());
-  while (status.ok()) {
-    contents->append(buffer->get_mem_block(), buffer->size());
-    position = contents->size();
-    status = ra_stream->PRead(position, chunk_size, buffer.get());
-  }
-  if (status.code() == absl::StatusCode::kOutOfRange) {  // EOF
-    EXPECT_EQ(0, buffer->size());
-  }
-  return status;
-}
-
 TEST(SharedRandomAccessStreamTest, ReadingStreams) {
   for (auto stream_size : {0, 10, 100, 1000, 10000, 1000000}) {
     SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd = test::GetTestFileDescriptor(
-        filename, stream_size, &file_contents);
-    EXPECT_EQ(stream_size, file_contents.size());
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+    auto ra_stream =
+        absl::make_unique<internal::TestRandomAccessStream>(stream_content);
     SharedRandomAccessStream shared_stream(ra_stream.get());
     std::string stream_contents;
-    auto status = ReadAll(&shared_stream, 1 + (stream_size / 10),
-                          &stream_contents);
+    auto status = internal::ReadAllFromRandomAccessStream(
+        &shared_stream, stream_contents, /*chunk_size=*/1 + (stream_size / 10));
     EXPECT_EQ(absl::StatusCode::kOutOfRange, status.code());
     EXPECT_EQ("EOF", status.message());
-    EXPECT_EQ(file_contents, stream_contents);
+    EXPECT_EQ(stream_content, stream_contents);
     EXPECT_EQ(stream_size, shared_stream.size().value());
   }
 }
diff --git a/cc/streamingaead/streaming_aead_config.cc b/cc/streamingaead/streaming_aead_config.cc
index 69ad21e..0ce14fd 100644
--- a/cc/streamingaead/streaming_aead_config.cc
+++ b/cc/streamingaead/streaming_aead_config.cc
@@ -25,18 +25,10 @@
 #include "tink/streamingaead/streaming_aead_wrapper.h"
 #include "tink/util/status.h"
 
-using google::crypto::tink::RegistryConfig;
-
 namespace crypto {
 namespace tink {
 
 // static
-const RegistryConfig& StreamingAeadConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
-// static
 util::Status StreamingAeadConfig::Register() {
   // Register primitive wrapper.
   auto status = Registry::RegisterPrimitiveWrapper(
diff --git a/cc/streamingaead/streaming_aead_config.h b/cc/streamingaead/streaming_aead_config.h
index 179647d..0ed5ea5 100644
--- a/cc/streamingaead/streaming_aead_config.h
+++ b/cc/streamingaead/streaming_aead_config.h
@@ -35,14 +35,6 @@
 //
 class StreamingAeadConfig {
  public:
-  static constexpr char kCatalogueName[] = "TinkStreamingAead";
-  static constexpr char kPrimitiveName[] = "StreamingAead";
-
-  // Returns config of StreamingAead implementations supported
-  // in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers StreamingAead primitive wrapper and key managers for all
   // StreamingAead key types from the current Tink release.
   static crypto::tink::util::Status Register();
diff --git a/cc/streamingaead/streaming_aead_config_test.cc b/cc/streamingaead/streaming_aead_config_test.cc
index d50018f..c3401e5 100644
--- a/cc/streamingaead/streaming_aead_config_test.cc
+++ b/cc/streamingaead/streaming_aead_config_test.cc
@@ -24,7 +24,6 @@
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "tink/config.h"
 #include "tink/config/tink_fips.h"
 #include "tink/keyset_handle.h"
 #include "tink/registry.h"
diff --git a/cc/streamingaead/streaming_aead_key_templates.cc b/cc/streamingaead/streaming_aead_key_templates.cc
index 7605392..ca67d6b 100644
--- a/cc/streamingaead/streaming_aead_key_templates.cc
+++ b/cc/streamingaead/streaming_aead_key_templates.cc
@@ -49,7 +49,8 @@
   return key_template;
 }
 
-KeyTemplate* NewAesCtrHmacStreamingKeyTemplate(int ikm_size_in_bytes) {
+KeyTemplate* NewAesCtrHmacStreamingKeyTemplate(int ikm_size_in_bytes,
+                                               int segment_size_in_bytes) {
   KeyTemplate* key_template = new KeyTemplate;
   key_template->set_type_url(
       "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey");
@@ -57,7 +58,7 @@
   AesCtrHmacStreamingKeyFormat key_format;
   key_format.set_key_size(ikm_size_in_bytes);
   auto params = key_format.mutable_params();
-  params->set_ciphertext_segment_size(4096);
+  params->set_ciphertext_segment_size(segment_size_in_bytes);
   params->set_derived_key_size(ikm_size_in_bytes);
   params->set_hkdf_hash_type(HashType::SHA256);
   auto hmac_params = params->mutable_hmac_params();
@@ -92,15 +93,29 @@
 
 // static
 const KeyTemplate& StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment4KB() {
-  static const KeyTemplate* key_template =
-      NewAesCtrHmacStreamingKeyTemplate(/* ikm_size_in_bytes= */ 16);
+  static const KeyTemplate* key_template = NewAesCtrHmacStreamingKeyTemplate(
+      /* ikm_size_in_bytes= */ 16, /* segment_size_in_bytes= */ 4096);
+  return *key_template;
+}
+
+// static
+const KeyTemplate& StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB() {
+  static const KeyTemplate* key_template = NewAesCtrHmacStreamingKeyTemplate(
+      /* ikm_size_in_bytes= */ 16, /* segment_size_in_bytes= */ 1048576);
   return *key_template;
 }
 
 // static
 const KeyTemplate& StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment4KB() {
-  static const KeyTemplate* key_template =
-      NewAesCtrHmacStreamingKeyTemplate(/* ikm_size_in_bytes= */ 32);
+  static const KeyTemplate* key_template = NewAesCtrHmacStreamingKeyTemplate(
+      /* ikm_size_in_bytes= */ 32, /* segment_size_in_bytes= */ 4096);
+  return *key_template;
+}
+
+// static
+const KeyTemplate& StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB() {
+  static const KeyTemplate* key_template = NewAesCtrHmacStreamingKeyTemplate(
+      /* ikm_size_in_bytes= */ 32, /* segment_size_in_bytes= */ 1048576);
   return *key_template;
 }
 
diff --git a/cc/streamingaead/streaming_aead_key_templates.h b/cc/streamingaead/streaming_aead_key_templates.h
index 5725fdb..bc7aded 100644
--- a/cc/streamingaead/streaming_aead_key_templates.h
+++ b/cc/streamingaead/streaming_aead_key_templates.h
@@ -76,6 +76,18 @@
 
   // Returns a KeyTemplate that generates new instances of
   // AesCtrHmacStreamingKey with the following parameters:
+  //   - main key (ikm) size: 16 bytes
+  //   - HKDF algorithm: HMAC-SHA256
+  //   - size of derived AES-CTR keys: 16 bytes
+  //   - tag algorithm: HMAC-SHA256
+  //   - tag size: 32 bytes
+  //   - ciphertext segment size: 1048576 bytes (1 MB)
+  //   - OutputPrefixType: RAW
+  static const google::crypto::tink::KeyTemplate&
+  Aes128CtrHmacSha256Segment1MB();
+
+  // Returns a KeyTemplate that generates new instances of
+  // AesCtrHmacStreamingKey with the following parameters:
   //   - main key (ikm) size: 32 bytes
   //   - HKDF algorithm: HMAC-SHA256
   //   - size of derived AES-CTR keys: 32 bytes
@@ -85,6 +97,18 @@
   //   - OutputPrefixType: RAW
   static const google::crypto::tink::KeyTemplate&
   Aes256CtrHmacSha256Segment4KB();
+
+  // Returns a KeyTemplate that generates new instances of
+  // AesCtrHmacStreamingKey with the following parameters:
+  //   - main key (ikm) size: 32 bytes
+  //   - HKDF algorithm: HMAC-SHA256
+  //   - size of derived AES-CTR keys: 32 bytes
+  //   - tag algorithm: HMAC-SHA256
+  //   - tag size: 32 bytes
+  //   - ciphertext segment size: 1048576 bytes (1 MB)
+  //   - OutputPrefixType: RAW
+  static const google::crypto::tink::KeyTemplate&
+  Aes256CtrHmacSha256Segment1MB();
 };
 
 }  // namespace tink
diff --git a/cc/streamingaead/streaming_aead_key_templates_test.cc b/cc/streamingaead/streaming_aead_key_templates_test.cc
index 31098b8..e0c9f91 100644
--- a/cc/streamingaead/streaming_aead_key_templates_test.cc
+++ b/cc/streamingaead/streaming_aead_key_templates_test.cc
@@ -203,6 +203,49 @@
   EXPECT_THAT(key_format.params().hmac_params().tag_size(), Eq(32));
 }
 
+TEST(Aes128CtrHmacSha256Segment1MBTest, TypeUrl) {
+  EXPECT_THAT(
+      StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB().type_url(),
+      Eq("type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey"));
+  EXPECT_THAT(
+      StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB().type_url(),
+      Eq(AesCtrHmacStreamingKeyManager().get_key_type()));
+}
+
+TEST(Aes128CtrHmacSha256Segment1MBTest, OutputPrefixType) {
+  EXPECT_THAT(StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB()
+                  .output_prefix_type(),
+              Eq(OutputPrefixType::RAW));
+}
+
+TEST(Aes128CtrHmacSha256Segment1MBTest, SameReference) {
+  // Check that reference to the same object is returned.
+  EXPECT_THAT(StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB(),
+              Ref(StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB()));
+}
+
+TEST(Aes128CtrHmacSha256Segment1MBTest, WorksWithKeyTypeManager) {
+  const KeyTemplate& key_template =
+      StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB();
+  AesCtrHmacStreamingKeyFormat key_format;
+  EXPECT_TRUE(key_format.ParseFromString(key_template.value()));
+  EXPECT_THAT(AesCtrHmacStreamingKeyManager().ValidateKeyFormat(key_format),
+              IsOk());
+}
+
+TEST(Aes128CtrHmacSha256Segment1MBTest, CheckValues) {
+  const KeyTemplate& key_template =
+      StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB();
+  AesCtrHmacStreamingKeyFormat key_format;
+  EXPECT_TRUE(key_format.ParseFromString(key_template.value()));
+  EXPECT_THAT(key_format.key_size(), Eq(16));
+  EXPECT_THAT(key_format.params().ciphertext_segment_size(), Eq(1048576));
+  EXPECT_THAT(key_format.params().derived_key_size(), Eq(16));
+  EXPECT_THAT(key_format.params().hkdf_hash_type(), Eq(HashType::SHA256));
+  EXPECT_THAT(key_format.params().hmac_params().hash(), Eq(HashType::SHA256));
+  EXPECT_THAT(key_format.params().hmac_params().tag_size(), Eq(32));
+}
+
 TEST(Aes256CtrHmacSha256Segment4KBTest, TypeUrl) {
   EXPECT_THAT(
       StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment4KB().type_url(),
@@ -246,6 +289,49 @@
   EXPECT_THAT(key_format.params().hmac_params().tag_size(), Eq(32));
 }
 
+TEST(Aes256CtrHmacSha256Segment1MBTest, TypeUrl) {
+  EXPECT_THAT(
+      StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB().type_url(),
+      Eq("type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey"));
+  EXPECT_THAT(
+      StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB().type_url(),
+      Eq(AesCtrHmacStreamingKeyManager().get_key_type()));
+}
+
+TEST(Aes256CtrHmacSha256Segment1MBTest, OutputPrefixType) {
+  EXPECT_THAT(StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB()
+                  .output_prefix_type(),
+              Eq(OutputPrefixType::RAW));
+}
+
+TEST(Aes256CtrHmacSha256Segment1MBTest, SameReference) {
+  // Check that reference to the same object is returned.
+  EXPECT_THAT(StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB(),
+              Ref(StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB()));
+}
+
+TEST(Aes256CtrHmacSha256Segment1MBTest, WorksWithKeyTypeManager) {
+  const KeyTemplate& key_template =
+      StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB();
+  AesCtrHmacStreamingKeyFormat key_format;
+  EXPECT_TRUE(key_format.ParseFromString(key_template.value()));
+  EXPECT_THAT(AesCtrHmacStreamingKeyManager().ValidateKeyFormat(key_format),
+              IsOk());
+}
+
+TEST(Aes256CtrHmacSha256Segment1MBTest, CheckValues) {
+  const KeyTemplate& key_template =
+      StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB();
+  AesCtrHmacStreamingKeyFormat key_format;
+  EXPECT_TRUE(key_format.ParseFromString(key_template.value()));
+  EXPECT_THAT(key_format.key_size(), Eq(32));
+  EXPECT_THAT(key_format.params().ciphertext_segment_size(), Eq(1048576));
+  EXPECT_THAT(key_format.params().derived_key_size(), Eq(32));
+  EXPECT_THAT(key_format.params().hkdf_hash_type(), Eq(HashType::SHA256));
+  EXPECT_THAT(key_format.params().hmac_params().hash(), Eq(HashType::SHA256));
+  EXPECT_THAT(key_format.params().hmac_params().tag_size(), Eq(32));
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/streamingaead/streaming_aead_wrapper.cc b/cc/streamingaead/streaming_aead_wrapper.cc
index 6f59f45..ae49413 100644
--- a/cc/streamingaead/streaming_aead_wrapper.cc
+++ b/cc/streamingaead/streaming_aead_wrapper.cc
@@ -16,7 +16,9 @@
 
 #include "tink/streamingaead/streaming_aead_wrapper.h"
 
+#include <memory>
 #include <utility>
+#include <vector>
 
 #include "absl/status/status.h"
 #include "tink/crypto_format.h"
@@ -47,12 +49,6 @@
     return Status(absl::StatusCode::kInvalidArgument,
                   "primitive set has no primary");
   }
-  auto raw_primitives_result = primitives->get_raw_primitives();
-  if (!raw_primitives_result.ok()) {
-    return Status(absl::StatusCode::kInvalidArgument,
-                  "primitive set has no raw primitives");
-  }
-  // TODO(b/129044084)
   return util::OkStatus();
 }
 
@@ -78,7 +74,7 @@
       std::unique_ptr<crypto::tink::RandomAccessStream> ciphertext_source,
       absl::string_view associated_data) const override;
 
-  ~StreamingAeadSetWrapper() override {}
+  ~StreamingAeadSetWrapper() override = default;
 
  private:
   // We use a shared_ptr here to ensure that primitives_ stays alive
diff --git a/cc/streamingaead/streaming_aead_wrapper.h b/cc/streamingaead/streaming_aead_wrapper.h
index 578be85..2084bb3 100644
--- a/cc/streamingaead/streaming_aead_wrapper.h
+++ b/cc/streamingaead/streaming_aead_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_STREAMINGAEAD_STREAMING_AEAD_WRAPPER_H_
 #define TINK_STREAMINGAEAD_STREAMING_AEAD_WRAPPER_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
diff --git a/cc/streamingaead/streaming_aead_wrapper_test.cc b/cc/streamingaead/streaming_aead_wrapper_test.cc
index e262c92..aa6aa72 100644
--- a/cc/streamingaead/streaming_aead_wrapper_test.cc
+++ b/cc/streamingaead/streaming_aead_wrapper_test.cc
@@ -16,9 +16,11 @@
 
 #include "tink/streamingaead/streaming_aead_wrapper.h"
 
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
@@ -26,64 +28,41 @@
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "tink/input_stream.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/test_random_access_stream.h"
 #include "tink/output_stream.h"
 #include "tink/primitive_set.h"
+#include "tink/proto_keyset_format.h"
 #include "tink/random_access_stream.h"
 #include "tink/streaming_aead.h"
+#include "tink/streamingaead/aes_gcm_hkdf_streaming_key_manager.h"
+#include "tink/streamingaead/streaming_aead_config.h"
 #include "tink/subtle/random.h"
+#include "tink/subtle/streaming_aead_test_util.h"
 #include "tink/subtle/test_util.h"
 #include "tink/util/buffer.h"
-#include "tink/util/file_random_access_stream.h"
 #include "tink/util/istream_input_stream.h"
 #include "tink/util/ostream_output_stream.h"
 #include "tink/util/status.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
+#include "proto/aes_gcm_hkdf_streaming.pb.h"
+#include "proto/common.pb.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-using crypto::tink::test::DummyStreamingAead;
-using crypto::tink::test::IsOk;
-using crypto::tink::test::StatusIs;
-using google::crypto::tink::KeysetInfo;
-using google::crypto::tink::KeyStatusType;
-using google::crypto::tink::OutputPrefixType;
-using subtle::test::ReadFromStream;
-using subtle::test::WriteToStream;
-using testing::HasSubstr;
-
-// Creates a RandomAccessStream with the specified contents.
-std::unique_ptr<RandomAccessStream> GetRandomAccessStream(
-    absl::string_view contents) {
-  static int index = 1;
-  std::string filename = absl::StrCat("stream_data_file_", index, ".txt");
-  index++;
-  int input_fd = test::GetTestFileDescriptor(filename, contents);
-  return {absl::make_unique<util::FileRandomAccessStream>(input_fd)};
-}
-
-// Reads the entire 'ra_stream', until no more bytes can be read,
-// and puts the read bytes into 'contents'.
-// Returns the status of the last ra_stream->PRead()-operation.
-util::Status ReadAll(RandomAccessStream* ra_stream, std::string* contents) {
-  int chunk_size = 42;
-  contents->clear();
-  auto buffer = std::move(util::Buffer::New(chunk_size).value());
-  int64_t position = 0;
-  auto status = ra_stream->PRead(position, chunk_size, buffer.get());
-  while (status.ok()) {
-    contents->append(buffer->get_mem_block(), buffer->size());
-    position = contents->size();
-    status = ra_stream->PRead(position, chunk_size, buffer.get());
-  }
-  if (status.code() == absl::StatusCode::kOutOfRange) {  // EOF
-    EXPECT_EQ(0, buffer->size());
-  }
-  return status;
-}
+using ::crypto::tink::test::DummyStreamingAead;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeysetInfo;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::OutputPrefixType;
+using ::crypto::tink::subtle::test::ReadFromStream;
+using ::crypto::tink::subtle::test::WriteToStream;
+using ::testing::HasSubstr;
 
 // A container for specification of instances of DummyStreamingAead
 // to be created for testing.
@@ -232,12 +211,14 @@
       EXPECT_EQ(absl::StrCat(saead_name_2, aad, plaintext), ct_buf->str());
 
       // Decrypt the ciphertext.
-      auto ct_source = GetRandomAccessStream(ct_buf->str());
+      auto ct_source =
+          std::make_unique<internal::TestRandomAccessStream>(ct_buf->str());
       auto dec_stream_result =
           saead->NewDecryptingRandomAccessStream(std::move(ct_source), aad);
       EXPECT_THAT(dec_stream_result, IsOk());
       std::string decrypted;
-      status = ReadAll(dec_stream_result.value().get(), &decrypted);
+      status = internal::ReadAllFromRandomAccessStream(
+          dec_stream_result.value().get(), decrypted);
       EXPECT_THAT(status, StatusIs(absl::StatusCode::kOutOfRange,
                                    HasSubstr("EOF")));
       EXPECT_EQ(plaintext, decrypted);
@@ -303,24 +284,63 @@
   EXPECT_EQ(plaintext, decrypted);
 }
 
-TEST(StreamingAeadSetWrapperTest, MissingRawPrimitives) {
-  uint32_t key_id_0 = 1234543;
-  uint32_t key_id_1 = 726329;
-  uint32_t key_id_2 = 7213743;
-  std::string saead_name_0 = "streaming_aead0";
-  std::string saead_name_1 = "streaming_aead1";
-  std::string saead_name_2 = "streaming_aead2";
+TEST(StreamingAeadSetWrapperTest, EncryptWithTink) {
+  ASSERT_THAT(StreamingAeadConfig::Register(), IsOk());
 
-  auto saead_set = GetTestStreamingAeadSet(
-      {{key_id_0, saead_name_0, OutputPrefixType::TINK},
-       {key_id_1, saead_name_1, OutputPrefixType::LEGACY},
-       {key_id_2, saead_name_2, OutputPrefixType::TINK}});
+  google::crypto::tink::AesGcmHkdfStreamingKey key;
+  key.set_key_value("0123456789012345");
+  google::crypto::tink::AesGcmHkdfStreamingParams& params =
+      *key.mutable_params();
+  params.set_hkdf_hash_type(google::crypto::tink::HashType::SHA1);
+  params.set_derived_key_size(16);
+  params.set_ciphertext_segment_size(1024);
 
-  // Wrap saead_set and test the resulting StreamingAead.
-  StreamingAeadWrapper wrapper;
-  auto wrap_result = wrapper.Wrap(std::move(saead_set));
-  EXPECT_THAT(wrap_result.status(), StatusIs(absl::StatusCode::kInvalidArgument,
-                                             HasSubstr("no raw primitives")));
+  std::string serialized_key_1 = key.SerializeAsString();
+
+  key.set_key_value("0123456789abcdef");
+  std::string serialized_key_2 = key.SerializeAsString();
+
+  google::crypto::tink::Keyset keyset;
+  {
+    google::crypto::tink::Keyset::Key& keyset_key = *keyset.add_key();
+    google::crypto::tink::KeyData& key_data = *keyset_key.mutable_key_data();
+    key_data.set_type_url(AesGcmHkdfStreamingKeyManager().get_key_type());
+    key_data.set_value(serialized_key_1);
+    key_data.set_key_material_type(google::crypto::tink::KeyData::SYMMETRIC);
+    keyset_key.set_key_id(1);
+    keyset_key.set_output_prefix_type(
+        google::crypto::tink::OutputPrefixType::TINK);
+    keyset_key.set_status(google::crypto::tink::KeyStatusType::ENABLED);
+
+    keyset.set_primary_key_id(1);
+  }
+  {
+    google::crypto::tink::Keyset::Key& keyset_key = *keyset.add_key();
+    google::crypto::tink::KeyData& key_data = *keyset_key.mutable_key_data();
+    key_data.set_type_url(AesGcmHkdfStreamingKeyManager().get_key_type());
+    key_data.set_value(serialized_key_2);
+    key_data.set_key_material_type(google::crypto::tink::KeyData::SYMMETRIC);
+    keyset_key.set_key_id(2);
+    keyset_key.set_output_prefix_type(
+        google::crypto::tink::OutputPrefixType::RAW);
+    keyset_key.set_status(google::crypto::tink::KeyStatusType::ENABLED);
+  }
+
+  crypto::tink::util::StatusOr<KeysetHandle> handle =
+      ParseKeysetFromProtoKeysetFormat(keyset.SerializeAsString(),
+                                       InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(handle.status(), IsOk());
+
+  crypto::tink::util::StatusOr<std::unique_ptr<StreamingAead>> streaming_aead =
+      handle->GetPrimitive<StreamingAead>();
+
+  ASSERT_THAT(streaming_aead.status(), IsOk());
+
+  EXPECT_THAT(EncryptThenDecrypt(streaming_aead.value().get(),
+                                 streaming_aead.value().get(),
+                                 subtle::Random::GetRandomBytes(10000),
+                                 "some associated data", 0),
+              IsOk());
 }
 
 }  // namespace
diff --git a/cc/subtle/BUILD.bazel b/cc/subtle/BUILD.bazel
index 4998753..caa9741 100644
--- a/cc/subtle/BUILD.bazel
+++ b/cc/subtle/BUILD.bazel
@@ -228,14 +228,10 @@
         ":common_enums",
         ":subtle_util_boringssl",
         "//:public_key_sign",
-        "//internal:bn_util",
-        "//internal:ec_util",
-        "//internal:err_util",
         "//internal:fips_utils",
         "//internal:md_util",
-        "//internal:ssl_unique_ptr",
         "//internal:util",
-        "//util:errors",
+        "//signature/internal:ecdsa_raw_sign_boringssl",
         "//util:statusor",
         "@boringssl//:crypto",
         "@com_google_absl//absl/status",
@@ -797,12 +793,11 @@
         ":test_util",
         "//:random_access_stream",
         "//:streaming_aead",
+        "//internal:test_random_access_stream",
         "//util:buffer",
-        "//util:file_random_access_stream",
         "//util:istream_input_stream",
         "//util:ostream_output_stream",
         "//util:status",
-        "//util:test_util",
         "@com_google_absl//absl/strings",
     ],
 )
@@ -1004,7 +999,7 @@
         ":common_enums",
         ":hmac_boringssl",
         "//:mac",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -1019,14 +1014,13 @@
     name = "aes_gcm_boringssl_test",
     size = "small",
     srcs = ["aes_gcm_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_gcm"],
+    data = ["//testvectors:aes_gcm"],
     tags = ["fips"],
     deps = [
         ":aes_gcm_boringssl",
         "//aead/internal:wycheproof_aead",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:secret_data",
-        "//util:status",
         "//util:statusor",
         "//util:test_matchers",
         "@com_google_absl//absl/status",
@@ -1122,7 +1116,7 @@
     name = "aes_eax_boringssl_test",
     size = "small",
     srcs = ["aes_eax_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_eax"],
+    data = ["//testvectors:aes_eax"],
     tags = ["fips"],
     deps = [
         ":aes_eax_boringssl",
@@ -1142,7 +1136,6 @@
 
 cc_test(
     name = "encrypt_then_authenticate_test",
-    size = "small",
     srcs = ["encrypt_then_authenticate_test.cc"],
     deps = [
         ":aes_ctr_boringssl",
@@ -1167,7 +1160,7 @@
     deps = [
         ":aes_ctr_boringssl",
         ":random",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -1182,7 +1175,7 @@
     name = "aes_siv_boringssl_test",
     size = "small",
     srcs = ["aes_siv_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_siv_cmac"],
+    data = ["//testvectors:aes_siv_cmac"],
     tags = ["fips"],
     deps = [
         ":aes_siv_boringssl",
@@ -1210,8 +1203,8 @@
         ":subtle_util_boringssl",
         "//:public_key_sign",
         "//:public_key_verify",
-        "//config:tink_fips",
         "//internal:ec_util",
+        "//internal:fips_utils",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
@@ -1225,8 +1218,8 @@
     size = "small",
     srcs = ["ecdsa_verify_boringssl_test.cc"],
     data = [
-        "@wycheproof//testvectors:ecdsa",
-        "@wycheproof//testvectors:ecdsa_webcrypto",
+        "//testvectors:ecdsa",
+        "//testvectors:ecdsa_webcrypto",
     ],
     tags = ["fips"],
     deps = [
@@ -1237,7 +1230,7 @@
         ":wycheproof_util",
         "//:public_key_sign",
         "//:public_key_verify",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
@@ -1277,7 +1270,7 @@
     name = "ed25519_verify_boringssl_test",
     size = "small",
     srcs = ["ed25519_verify_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:eddsa"],
+    data = ["//testvectors:eddsa"],
     tags = ["fips"],
     deps = [
         ":ed25519_verify_boringssl",
@@ -1300,7 +1293,7 @@
     name = "rsa_ssa_pss_verify_boringssl_test",
     size = "small",
     srcs = ["rsa_ssa_pss_verify_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:rsa_pss"],
+    data = ["//testvectors:rsa_pss"],
     tags = ["fips"],
     deps = [
         ":common_enums",
@@ -1308,8 +1301,8 @@
         ":wycheproof_util",
         "//:public_key_sign",
         "//:public_key_verify",
-        "//config:tink_fips",
         "//internal:err_util",
+        "//internal:fips_utils",
         "//internal:rsa_util",
         "//internal:ssl_unique_ptr",
         "//util:status",
@@ -1330,7 +1323,7 @@
     deps = [
         ":rsa_ssa_pss_sign_boringssl",
         ":rsa_ssa_pss_verify_boringssl",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//internal:rsa_util",
         "//internal:ssl_unique_ptr",
         "//util:test_matchers",
@@ -1345,7 +1338,7 @@
     name = "rsa_ssa_pkcs1_verify_boringssl_test",
     size = "small",
     srcs = ["rsa_ssa_pkcs1_verify_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:rsa_signature"],
+    data = ["//testvectors:rsa_signature"],
     tags = ["fips"],
     deps = [
         ":common_enums",
@@ -1353,8 +1346,8 @@
         ":wycheproof_util",
         "//:public_key_sign",
         "//:public_key_verify",
-        "//config:tink_fips",
         "//internal:err_util",
+        "//internal:fips_utils",
         "//internal:rsa_util",
         "//internal:ssl_unique_ptr",
         "//util:status",
@@ -1375,7 +1368,7 @@
     deps = [
         ":rsa_ssa_pkcs1_sign_boringssl",
         ":rsa_ssa_pkcs1_verify_boringssl",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//internal:rsa_util",
         "//internal:ssl_unique_ptr",
         "//util:test_matchers",
@@ -1390,7 +1383,7 @@
     name = "aes_gcm_siv_boringssl_test",
     size = "small",
     srcs = ["aes_gcm_siv_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_gcm_siv"],
+    data = ["//testvectors:aes_gcm_siv"],
     tags = ["fips"],
     deps = [
         ":aes_gcm_siv_boringssl",
@@ -1446,7 +1439,7 @@
     name = "xchacha20_poly1305_boringssl_test",
     size = "small",
     srcs = ["xchacha20_poly1305_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:chacha20_poly1305"],
+    data = ["//testvectors:chacha20_poly1305"],
     tags = ["fips"],
     deps = [
         ":subtle_util",
@@ -1545,11 +1538,10 @@
         "//:output_stream",
         "//:random_access_stream",
         "//:streaming_aead",
-        "//util:file_random_access_stream",
+        "//internal:test_random_access_stream",
         "//util:ostream_output_stream",
         "//util:status",
         "//util:test_matchers",
-        "//util:test_util",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
@@ -1578,7 +1570,7 @@
     name = "stateful_hmac_boringssl_test",
     size = "small",
     srcs = ["stateful_hmac_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:hmac"],
+    data = ["//testvectors:hmac"],
     deps = [
         ":common_enums",
         ":stateful_hmac_boringssl",
@@ -1598,7 +1590,7 @@
     name = "stateful_cmac_boringssl_test",
     size = "small",
     srcs = ["stateful_cmac_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_cmac"],
+    data = ["//testvectors:aes_cmac"],
     deps = [
         ":common_enums",
         ":stateful_cmac_boringssl",
diff --git a/cc/subtle/CMakeLists.txt b/cc/subtle/CMakeLists.txt
index 7ac3580..ff38349 100644
--- a/cc/subtle/CMakeLists.txt
+++ b/cc/subtle/CMakeLists.txt
@@ -216,14 +216,10 @@
     absl::strings
     crypto
     tink::core::public_key_sign
-    tink::internal::bn_util
-    tink::internal::ec_util
-    tink::internal::err_util
     tink::internal::fips_utils
     tink::internal::md_util
-    tink::internal::ssl_unique_ptr
     tink::internal::util
-    tink::util::errors
+    tink::signature::internal::ecdsa_raw_sign_boringssl
     tink::util::statusor
 )
 
@@ -648,6 +644,7 @@
     tink::internal::test_file_util
     tink::util::status
     tink::util::statusor
+  TESTONLY
 )
 
 tink_cc_library(
@@ -728,6 +725,7 @@
     tink::core::output_stream
     tink::util::status
     tink::util::statusor
+  TESTONLY
 )
 
 tink_cc_library(
@@ -741,6 +739,7 @@
     tink::core::aead
     tink::aead::cord_aead
     tink::util::status
+  TESTONLY
 )
 
 tink_cc_library(
@@ -753,12 +752,12 @@
     absl::strings
     tink::core::random_access_stream
     tink::core::streaming_aead
+    tink::internal::test_random_access_stream
     tink::util::buffer
-    tink::util::file_random_access_stream
     tink::util::istream_input_stream
     tink::util::ostream_output_stream
     tink::util::status
-    tink::util::test_util
+  TESTONLY
 )
 
 tink_cc_library(
@@ -771,6 +770,7 @@
     tink::core::hybrid_decrypt
     tink::core::hybrid_encrypt
     tink::util::status
+  TESTONLY
 )
 
 tink_cc_library(
@@ -946,7 +946,7 @@
     absl::status
     absl::strings
     tink::core::mac
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -965,9 +965,8 @@
     absl::status
     absl::strings
     tink::aead::internal::wycheproof_aead
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::secret_data
-    tink::util::status
     tink::util::statusor
     tink::util::test_matchers
 )
@@ -1097,7 +1096,7 @@
     tink::subtle::random
     gmock
     absl::status
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -1137,8 +1136,8 @@
     absl::status
     tink::core::public_key_sign
     tink::core::public_key_verify
-    tink::config::tink_fips
     tink::internal::ec_util
+    tink::internal::fips_utils
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
@@ -1162,7 +1161,7 @@
     rapidjson
     tink::core::public_key_sign
     tink::core::public_key_verify
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
@@ -1230,8 +1229,8 @@
     rapidjson
     tink::core::public_key_sign
     tink::core::public_key_verify
-    tink::config::tink_fips
     tink::internal::err_util
+    tink::internal::fips_utils
     tink::internal::rsa_util
     tink::internal::ssl_unique_ptr
     tink::util::status
@@ -1250,7 +1249,7 @@
     absl::status
     absl::strings
     crypto
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::internal::rsa_util
     tink::internal::ssl_unique_ptr
     tink::util::test_matchers
@@ -1273,8 +1272,8 @@
     rapidjson
     tink::core::public_key_sign
     tink::core::public_key_verify
-    tink::config::tink_fips
     tink::internal::err_util
+    tink::internal::fips_utils
     tink::internal::rsa_util
     tink::internal::ssl_unique_ptr
     tink::util::status
@@ -1293,7 +1292,7 @@
     absl::status
     absl::strings
     crypto
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::internal::rsa_util
     tink::internal::ssl_unique_ptr
     tink::util::test_matchers
@@ -1457,11 +1456,10 @@
     tink::core::output_stream
     tink::core::random_access_stream
     tink::core::streaming_aead
-    tink::util::file_random_access_stream
+    tink::internal::test_random_access_stream
     tink::util::ostream_output_stream
     tink::util::status
     tink::util::test_matchers
-    tink::util::test_util
 )
 
 tink_cc_test(
diff --git a/cc/subtle/aes_cmac_boringssl.cc b/cc/subtle/aes_cmac_boringssl.cc
index 9e8cc9c..4afbba1 100644
--- a/cc/subtle/aes_cmac_boringssl.cc
+++ b/cc/subtle/aes_cmac_boringssl.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/aes_cmac_boringssl.h"
 
 #include <cstdint>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/aes_cmac_boringssl_test.cc b/cc/subtle/aes_cmac_boringssl_test.cc
index d1a6f60..a836211 100644
--- a/cc/subtle/aes_cmac_boringssl_test.cc
+++ b/cc/subtle/aes_cmac_boringssl_test.cc
@@ -16,7 +16,9 @@
 
 #include "tink/subtle/aes_cmac_boringssl.h"
 
+#include <memory>
 #include <string>
+#include <utility>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/subtle/aes_ctr_boringssl.cc b/cc/subtle/aes_ctr_boringssl.cc
index 25d1418..3f6160c 100644
--- a/cc/subtle/aes_ctr_boringssl.cc
+++ b/cc/subtle/aes_ctr_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_ctr_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/aes_ctr_boringssl_test.cc b/cc/subtle/aes_ctr_boringssl_test.cc
index c812c30..e1d57db 100644
--- a/cc/subtle/aes_ctr_boringssl_test.cc
+++ b/cc/subtle/aes_ctr_boringssl_test.cc
@@ -22,7 +22,7 @@
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/subtle/random.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
@@ -39,7 +39,7 @@
 using ::crypto::tink::test::StatusIs;
 
 TEST(AesCtrBoringSslTest, TestEncryptDecrypt) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -60,7 +60,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestEncryptDecrypt_randomMessage) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -83,7 +83,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestEncryptDecrypt_randomKey_randomMessage) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -105,7 +105,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestEncryptDecrypt_invalidIvSize) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -122,7 +122,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestNistTestVector) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -143,7 +143,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestMultipleEncrypt) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -160,7 +160,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestFipsOnly) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -175,7 +175,7 @@
 }
 
 TEST(AesCtrBoringSslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
diff --git a/cc/subtle/aes_ctr_hmac_streaming.cc b/cc/subtle/aes_ctr_hmac_streaming.cc
index 597efd3..891ff4c 100644
--- a/cc/subtle/aes_ctr_hmac_streaming.cc
+++ b/cc/subtle/aes_ctr_hmac_streaming.cc
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <limits>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/cc/subtle/aes_ctr_hmac_streaming.h b/cc/subtle/aes_ctr_hmac_streaming.h
index 4276650..572e5de 100644
--- a/cc/subtle/aes_ctr_hmac_streaming.h
+++ b/cc/subtle/aes_ctr_hmac_streaming.h
@@ -183,7 +183,7 @@
     return ciphertext_segment_size_;
   }
   int get_ciphertext_offset() const override { return ciphertext_offset_; }
-  ~AesCtrHmacStreamSegmentDecrypter() override {}
+  ~AesCtrHmacStreamSegmentDecrypter() override = default;
 
  private:
   AesCtrHmacStreamSegmentDecrypter(util::SecretData ikm, HashType hkdf_algo,
diff --git a/cc/subtle/aes_eax_aesni.cc b/cc/subtle/aes_eax_aesni.cc
deleted file mode 100644
index 5ad53fb..0000000
--- a/cc/subtle/aes_eax_aesni.cc
+++ /dev/null
@@ -1,582 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include <utility>
-#ifdef __SSE4_1__
-#ifdef __AES__
-
-#include "tink/subtle/aes_eax_aesni.h"
-
-#include <emmintrin.h>  // SSE2: used for _mm_sub_epi64 _mm_unpacklo_epi64 etc.
-#include <smmintrin.h>  // SSE4: used for _mm_cmpeq_epi64
-#include <tmmintrin.h>  // SSE3: used for _mm_shuffle_epi8
-#include <wmmintrin.h>  // AES_NI instructions.
-#include <xmmintrin.h>  // Datatype _mm128i
-
-#include <algorithm>
-#include <array>
-#include <memory>
-#include <string>
-
-#include "absl/algorithm/container.h"
-#include "absl/status/status.h"
-#include "tink/internal/util.h"
-#include "tink/subtle/random.h"
-#include "tink/subtle/subtle_util.h"
-
-namespace crypto {
-namespace tink {
-namespace subtle {
-
-namespace {
-inline bool EqualBlocks(__m128i x, __m128i y) {
-  // Compare byte wise.
-  // A byte in eq is 0xff if the corresponding byte in x and y are equal
-  // and 0x00 if the corresponding byte in x and y are not equal.
-  __m128i eq = _mm_cmpeq_epi8(x, y);
-  // Extract the 16 most significant bits of each byte in eq.
-  int bits = _mm_movemask_epi8(eq);
-  return 0xFFFF == bits;
-}
-
-// Reverse the order of the bytes in x.
-inline __m128i Reverse(__m128i x) {
-  const __m128i reverse_order =
-      _mm_set_epi32(0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f);
-  return _mm_shuffle_epi8(x, reverse_order);
-}
-
-// Increment x by 1.
-// This function assumes that the bytes of x are in little endian order.
-// Hence before using the result in EAX the bytes must be reversed, since EAX
-// requires a counter value in big endian order.
-inline __m128i Increment(__m128i x) {
-  const __m128i mask =
-      _mm_set_epi32(0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff);
-  // Determine which of the two 64-bit parts of x overflow.
-  // The result is 0xff..ff if the corresponding integers overflows and 0
-  // otherwise.
-  __m128i carries = _mm_cmpeq_epi64(x, mask);  // SSE4
-  // Move the least significant 64 carry bits into the most significant 64 bits
-  // of diff and fill the least significant bits of diff with 0xff..ff.
-  __m128i diff = _mm_unpacklo_epi64(mask, carries);
-  // Use subtraction since the 64-bit parts that must be incremented contain
-  // the value -1.
-  return _mm_sub_epi64(x, diff);
-}
-
-// Add y to x.
-// This assumes that x is in little endian order.
-// So far I've not found a simple way to compute and add the carry using
-// xmm instructions. However, optimizing this function is not important,
-// since it is used just once during decryption.
-inline __m128i Add(__m128i x, uint64 y) {
-  // Convert to a vector of two uint64.
-  uint64 vec[2];
-  _mm_storeu_si128(reinterpret_cast<__m128i*>(vec), x);
-  // Perform the addition on the vector.
-  vec[0] += y;
-  if (y > vec[0]) {
-    vec[1]++;
-  }
-  // Convert back to xmm.
-  return _mm_loadu_si128(reinterpret_cast<__m128i*>(vec));
-}
-
-// Decrement x by 1.
-// This function assumes that the bytes of x are in little endian order.
-// Hence before using the result in EAX the bytes must be reversed, since EAX
-// requires a counter value in big endian order.
-inline __m128i Decrement(__m128i x) {
-  const __m128i zero = _mm_setzero_si128();
-  // Moves lower 64 bit of x into higher 64 bits and set the lower 64 bits to 0.
-  __m128i shifted = _mm_slli_si128(x, 8);
-  // Determines whether the lower and upper parts must be decremented.
-  // I.e. the lower 64 bits must always be decremented.
-  // The upper 64 bits must be decremented if the lower 64 bits of x are 0.
-  __m128i carries = _mm_cmpeq_epi64(shifted, zero);  // SSE4
-  // Use add since _mm_cmpeq_epi64 returns -1 for 64-bit parts that are equal.
-  return _mm_add_epi64(x, carries);
-}
-
-// Rotate a value by 32 bit to the left (assuming little endian order).
-inline __m128i RotLeft32(__m128i value) {
-  return _mm_shuffle_epi32(value, _MM_SHUFFLE(2, 1, 0, 3));
-}
-
-// Multiply a binary polynomial given in big endian order by x
-// and reduce modulo x^128 + x^7 + x^2 + x + 1
-inline __m128i MultiplyByX(__m128i value) {
-  // Convert big endian to little endian.,
-  value = Reverse(value);
-  // Sets each dword to 0xffffffff if the most significant bit of the same
-  // dword in value is set.
-  __m128i msb = _mm_srai_epi32(value, 31);
-  __m128i msb_rotated = RotLeft32(msb);
-  // Determines the carries. If the most signigicant bit in value is set,
-  // then this bit is reduced to x^7 + x^2 + x + 1
-  // (which corresponds to the constant 0x87).
-  __m128i carry = _mm_and_si128(msb_rotated, _mm_set_epi32(1, 1, 1, 0x87));
-  __m128i res = _mm_xor_si128(_mm_slli_epi32(value, 1), carry);
-  // Converts the result back to big endian order.
-  return Reverse(res);
-}
-
-// Load block[0]..block[block_size-1] into the least significant bytes of
-// a register and set the remaining bytes to 0. The efficiency of this function
-// is not critical.
-__m128i LoadPartialBlock(const uint8_t* block, size_t block_size) {
-  std::array<uint8_t, 16> tmp;
-  tmp.fill(0);
-  std::copy_n(block, block_size, tmp.begin());
-  return _mm_loadu_si128(reinterpret_cast<__m128i*>(tmp.data()));
-}
-
-// Store the block_size least significant bytes from value in
-// block[0] .. block[block_size - 1]. The efficiency of this procedure is not
-// critical.
-void StorePartialBlock(uint8_t* block, size_t block_size, __m128i value) {
-  std::array<uint8_t, 16> tmp;
-  _mm_storeu_si128(reinterpret_cast<__m128i*>(tmp.data()), value);
-  std::copy_n(tmp.begin(), block_size, block);
-}
-
-static const uint8_t kRoundConstant[11] =
-    {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36};
-// Returns the round constant for round i.
-uint8_t Rcon(int round) {
-  return kRoundConstant[round];
-}
-
-// Call the AESKEYGENASSIST operation on a 32-bit input.
-// This performs a rotation and a substitution with an S-box.
-// This implementation uses AESKEYGENASSIST to compute the result twice
-// and checks that the two results match.
-inline uint32 SubRot(uint32 tmp) {
-  __m128i inp = _mm_set_epi32(0, 0, tmp, 0);
-  __m128i out = _mm_aeskeygenassist_si128(inp, 0x00);
-  return _mm_extract_epi32(out, 1);
-}
-
-// Apply the S-box to the 4 bytes in a word.
-// This operation is used in the key expansion of 256-bit keys.
-// This implementation computes the result twice and checks equality.
-inline uint32 SubWord(uint32 tmp) {
-  __m128i inp = _mm_set_epi32(0, 0, tmp, 0);
-  __m128i out = _mm_aeskeygenassist_si128(inp, 0x00);
-  return _mm_extract_epi32(out, 0);
-}
-
-// The following code uses a key expansion that closely follows FIPS 197.
-// If necessary it is possible to unroll the loops.
-void Aes128KeyExpansion(const uint8_t* key, __m128i *round_key) {
-  const int Nk = 4;  // Number of words in the key
-  const int Nb = 4;  // Number of words per round key
-  const int Nr = 10;  // Number or rounds
-  uint32 *w = reinterpret_cast<uint32*>(round_key);
-  const uint32 *keywords = reinterpret_cast<const uint32*>(key);
-  for (int i = 0; i < Nk; i++) {
-    w[i] = keywords[i];
-  }
-  uint32 tmp = w[Nk - 1];
-  for (int i = Nk; i < Nb * (Nr + 1); i++) {
-    if (i % Nk == 0) {
-      tmp = SubRot(tmp) ^ Rcon(i / Nk);
-    }
-    tmp ^= w[i - Nk];
-    w[i] = tmp;
-  }
-}
-
-void Aes256KeyExpansion(const uint8_t* key, __m128i *round_key) {
-  const int Nk = 8;  // Number of words in the key
-  const int Nb = 4;  // Number of words per round key
-  const int Nr = 14;  // Number or rounds
-  uint32 *w = reinterpret_cast<uint32*>(round_key);
-  const uint32 *keywords = reinterpret_cast<const uint32*>(key);
-  for (int i = 0; i < Nk; i++) {
-    w[i] = keywords[i];
-  }
-  uint32 tmp = w[Nk - 1];
-  for (int i = Nk; i < Nb * (Nr + 1); i++) {
-    if (i % Nk == 0) {
-      tmp = SubRot(tmp) ^ Rcon(i / Nk);
-    } else if (i % 4 == 0) {
-      tmp = SubWord(tmp);
-    }
-    tmp ^= w[i - Nk];
-    w[i] = tmp;
-  }
-}
-
-bool IsValidNonceSize(size_t nonce_size) {
-  return nonce_size == 12 || nonce_size == 16;
-}
-
-bool IsValidKeySize(size_t key_size) {
-  return key_size == 16 || key_size == 32;
-}
-
-}  // namespace
-
-crypto::tink::util::StatusOr<std::unique_ptr<Aead>> AesEaxAesni::New(
-    const util::SecretData& key, size_t nonce_size_in_bytes) {
-  if (!IsValidKeySize(key.size())) {
-    return util::Status(absl::StatusCode::kInvalidArgument, "Invalid key size");
-  }
-  if (!IsValidNonceSize(nonce_size_in_bytes)) {
-    return util::Status(absl::StatusCode::kInvalidArgument,
-                        "Invalid nonce size");
-  }
-  auto eax = absl::WrapUnique(new AesEaxAesni(nonce_size_in_bytes));
-  if (!eax->SetKey(key)) {
-    return util::Status(absl::StatusCode::kInternal, "Setting AES key failed");
-  }
-  return {std::move(eax)};
-}
-
-bool AesEaxAesni::SetKey(const util::SecretData& key) {
-  size_t key_size = key.size();
-  if (key_size == 16) {
-    rounds_ = 10;
-    Aes128KeyExpansion(key.data(), round_key_->data());
-  } else if (key_size == 32) {
-    rounds_ = 14;
-    Aes256KeyExpansion(key.data(), round_key_->data());
-  } else {
-    return false;
-  }
-  // Determine the round keys for decryption.
-  (*round_dec_key_)[0] = (*round_key_)[rounds_];
-  (*round_dec_key_)[rounds_] = (*round_key_)[0];
-  for (int i = 1; i < rounds_; i++) {
-    (*round_dec_key_)[i] = _mm_aesimc_si128((*round_key_)[rounds_ - i]);
-  }
-
-  // Derive the paddings from the key.
-  __m128i zero = _mm_setzero_si128();
-  __m128i zero_encrypted = EncryptBlock(zero);
-  *B_ = MultiplyByX(zero_encrypted);
-  *P_ = MultiplyByX(*B_);
-  return true;
-}
-
-inline void AesEaxAesni::Encrypt3Decrypt1(
-    const __m128i in0,
-    const __m128i in1,
-    const __m128i in2,
-    const __m128i in_dec,
-    __m128i* out0,
-    __m128i* out1,
-    __m128i* out2,
-    __m128i* out_dec) const {
-  __m128i first_round = (*round_key_)[0];
-  __m128i tmp0 = _mm_xor_si128(in0, first_round);
-  __m128i tmp1 = _mm_xor_si128(in1, first_round);
-  __m128i tmp2 = _mm_xor_si128(in2, first_round);
-  __m128i tmp3 = _mm_xor_si128(in_dec, (*round_dec_key_)[0]);
-  for (int i = 1; i < rounds_; i++){
-    __m128i round_key = (*round_key_)[i];
-    tmp0 = _mm_aesenc_si128(tmp0, round_key);
-    tmp1 = _mm_aesenc_si128(tmp1, round_key);
-    tmp2 = _mm_aesenc_si128(tmp2, round_key);
-    tmp3 = _mm_aesdec_si128(tmp3, (*round_dec_key_)[i]);
-  }
-  __m128i last_round = (*round_key_)[rounds_];
-  *out0 = _mm_aesenclast_si128(tmp0, last_round);
-  *out1 = _mm_aesenclast_si128(tmp1, last_round);
-  *out2 = _mm_aesenclast_si128(tmp2, last_round);
-  *out_dec = _mm_aesdeclast_si128(tmp3, (*round_dec_key_)[rounds_]);
-}
-
-inline __m128i AesEaxAesni::EncryptBlock(__m128i block) const {
-  __m128i tmp = _mm_xor_si128(block, (*round_key_)[0]);
-  for (int i = 1; i < rounds_; i++){
-    tmp = _mm_aesenc_si128(tmp, (*round_key_)[i]);
-  }
-  return _mm_aesenclast_si128(tmp, (*round_key_)[rounds_]);
-}
-
-inline void AesEaxAesni::Encrypt2Blocks(
-    const __m128i in0, const __m128i in1, __m128i *out0, __m128i *out1) const {
-  __m128i tmp0 = _mm_xor_si128(in0, (*round_key_)[0]);
-  __m128i tmp1 = _mm_xor_si128(in1, (*round_key_)[0]);
-  for (int i = 1; i < rounds_; i++){
-    __m128i round_key = (*round_key_)[i];
-    tmp0 = _mm_aesenc_si128(tmp0, round_key);
-    tmp1 = _mm_aesenc_si128(tmp1, round_key);
-  }
-  __m128i last_round = (*round_key_)[rounds_];
-  *out0 = _mm_aesenclast_si128(tmp0, last_round);
-  *out1 = _mm_aesenclast_si128(tmp1, last_round);
-}
-
-__m128i AesEaxAesni::Pad(const uint8_t* data, int len) const {
-  // CHECK(0 <= len && len <= kBlockSize);
-  // TODO(bleichen): Is there a better way to load n bytes into a register
-  std::array<uint8_t, kBlockSize> tmp;
-  tmp.fill(0);
-  std::copy_n(data, len, tmp.begin());
-  if (len == kBlockSize) {
-    __m128i block = _mm_loadu_si128(reinterpret_cast<__m128i*>(tmp.data()));
-    return _mm_xor_si128(block, *B_);
-  } else {
-    tmp[len] = 0x80;
-    __m128i block = _mm_loadu_si128(reinterpret_cast<__m128i*>(tmp.data()));
-    return _mm_xor_si128(block, *P_);
-  }
-}
-
-__m128i AesEaxAesni::OMAC(absl::string_view blob, int tag) const {
-  const uint8_t* data = reinterpret_cast<const uint8_t*>(blob.data());
-  size_t len = blob.size();
-  __m128i state = _mm_set_epi32(tag << 24, 0, 0, 0);
-  if (len == 0) {
-    state = _mm_xor_si128(state, *B_);
-  } else {
-    state = EncryptBlock(state);
-    size_t idx = 0;
-    while (len - idx > kBlockSize) {
-      __m128i in = _mm_loadu_si128((__m128i*) (data + idx));
-      state = _mm_xor_si128(in, state);
-      state = EncryptBlock(state);
-      idx += kBlockSize;
-    }
-    state = _mm_xor_si128(state, Pad(data + idx, len - idx));
-  }
-  return EncryptBlock(state);
-}
-
-bool AesEaxAesni::RawEncrypt(absl::string_view nonce, absl::string_view in,
-                             absl::string_view associated_data,
-                             absl::Span<uint8_t> ciphertext) const {
-  // Sanity check
-  if (in.size() + kTagSize != ciphertext.size()) {
-    return false;
-  }
-  const uint8_t* plaintext = reinterpret_cast<const uint8_t*>(in.data());
-
-  // NOTE(bleichen): The author of EAX designed this mode, so that
-  //   it would be possible to compute N and H independently of the encryption.
-  //   So far this possiblity is not used in this implementation.
-  const __m128i N = OMAC(nonce, 0);
-  const __m128i H = OMAC(associated_data, 1);
-
-  // Compute the initial counter in little endian order.
-  // EAX uses big endian order, but it is easier to increment
-  // a counter if it is in little endian order.
-  __m128i ctr = Reverse(N);
-
-  // Initialize mac with the header of the input for the MAC.
-  __m128i mac = _mm_set_epi32(0x2000000, 0, 0, 0);
-
-  uint8_t* out = ciphertext.data();
-  size_t idx = 0;
-  __m128i key_stream;
-  while (idx + kBlockSize < in.size()) {
-    __m128i ctr_big_endian = Reverse(ctr);
-    // Get the key stream for one message block and compute
-    // the MAC for the previous ciphertext block or header.
-    Encrypt2Blocks(mac, ctr_big_endian, &mac, &key_stream);
-    __m128i pt = _mm_loadu_si128(reinterpret_cast<const __m128i*>(plaintext));
-    __m128i ct = _mm_xor_si128(pt, key_stream);
-    mac = _mm_xor_si128(mac, ct);
-    ctr = Increment(ctr);
-    _mm_storeu_si128(reinterpret_cast<__m128i*>(out), ct);
-    plaintext += kBlockSize;
-    out += kBlockSize;
-    idx += kBlockSize;
-  }
-
-  // Last block
-  size_t last_block_size = in.size() - idx;
-  if (last_block_size > 0) {
-    __m128i ctr_big_endian = Reverse(ctr);
-    Encrypt2Blocks(mac, ctr_big_endian, &mac, &key_stream);
-    __m128i pt = LoadPartialBlock(plaintext, last_block_size);
-    __m128i ct = _mm_xor_si128(pt, key_stream);
-    StorePartialBlock(out, last_block_size, ct);
-    __m128i padded_last_block = Pad(out, last_block_size);
-    out += last_block_size;
-    mac = _mm_xor_si128(mac, padded_last_block);
-  } else {
-    // Special code for plaintexts of size 0.
-    mac = _mm_xor_si128(mac, *B_);
-  }
-  mac = EncryptBlock(mac);
-  __m128i tag = _mm_xor_si128(mac, N);
-  tag = _mm_xor_si128(tag, H);
-  StorePartialBlock(out, kTagSize, tag);
-  return true;
-}
-
-bool AesEaxAesni::RawDecrypt(absl::string_view nonce, absl::string_view in,
-                             absl::string_view associated_data,
-                             absl::Span<uint8_t> plaintext) const {
-  __m128i N = OMAC(nonce, 0);
-  __m128i H = OMAC(associated_data, 1);
-
-  const uint8_t* ciphertext = reinterpret_cast<const uint8_t*>(in.data());
-  const size_t ciphertext_size = in.size();
-
-  // Sanity checks: RawDecrypt should always be called with valid sizes.
-  if (ciphertext_size < kTagSize) {
-    return false;
-  }
-  if (ciphertext_size - kTagSize != plaintext.size()) {
-    return false;
-  }
-
-  // Get the tag from the ciphertext.
-  const __m128i tag = _mm_loadu_si128(
-      reinterpret_cast<const __m128i*>(&ciphertext[plaintext.size()]));
-
-  // A CBC-MAC is reversible. This allows to pipeline the MAC verification
-  // by recomputing the MAC for the first half of the ciphertext and
-  // reversion the MAC for the second half.
-  __m128i mac_forward = _mm_set_epi32(0x2000000, 0, 0, 0);
-  __m128i mac_backward = _mm_xor_si128(tag, N);
-  mac_backward = _mm_xor_si128(mac_backward, H);
-
-  // Special case code for empty messages of size 0.
-  if (plaintext.empty()) {
-    mac_forward = _mm_xor_si128(mac_forward, *B_);
-    mac_forward = EncryptBlock(mac_forward);
-    return EqualBlocks(mac_forward, mac_backward);
-  }
-
-  const size_t last_block = (plaintext.size() - 1) / kBlockSize;
-  const size_t last_block_size = ((plaintext.size() - 1) % kBlockSize) + 1;
-  const __m128i* ciphertext_blocks =
-      reinterpret_cast<const __m128i*>(ciphertext);
-  __m128i* plaintext_blocks = reinterpret_cast<__m128i*>(plaintext.data());
-  __m128i ctr_forward = Reverse(N);
-  __m128i ctr_backward = Add(ctr_forward, last_block);
-  __m128i unused = _mm_setzero_si128();
-  __m128i stream_forward;
-  __m128i stream_backward;
-  Encrypt3Decrypt1(
-      Reverse(ctr_backward), mac_forward, unused, mac_backward,
-      &stream_backward, &mac_forward, &unused, &mac_backward);
-  __m128i ct = LoadPartialBlock(&ciphertext[plaintext.size() - last_block_size],
-                                last_block_size);
-  __m128i pt = _mm_xor_si128(ct, stream_backward);
-  StorePartialBlock(&plaintext[plaintext.size() - last_block_size],
-                    last_block_size, pt);
-  __m128i padded_last_block =
-      Pad(&ciphertext[plaintext.size() - last_block_size], last_block_size);
-  mac_backward = _mm_xor_si128(mac_backward, padded_last_block);
-  const size_t mid_block = last_block / 2;
-  // Decrypts two blocks concurrently as long as there are at least two
-  // blocks to decrypt. The two blocks are the first block not yet decrypted
-  // and the last block not yet decrypted. The reason for this is that the
-  // OMAC can be verified at the same time. mac_forward is the OMAC of leading
-  // ciphertext blocks that have already been decrypted. mac_backward is the
-  // partial result for the OMAC up to block last_block - i - 1 that is
-  // necessary so OMAC of the full encryption results in the tag received from
-  // the ciphertext.
-  for (size_t i = 0; i < mid_block; i++) {
-    ctr_backward = Decrement(ctr_backward);
-    __m128i ct_forward = _mm_loadu_si128(&ciphertext_blocks[i]);
-    __m128i ct_backward =
-        _mm_loadu_si128(&ciphertext_blocks[last_block - i - 1]);
-    mac_forward = _mm_xor_si128(mac_forward, ct_forward);
-    Encrypt3Decrypt1(
-       Reverse(ctr_forward), Reverse(ctr_backward), mac_forward, mac_backward,
-        &stream_forward, &stream_backward, &mac_forward, &mac_backward);
-    __m128i plaintext_forward = _mm_xor_si128(ct_forward, stream_forward);
-    __m128i plaintext_backward = _mm_xor_si128(ct_backward, stream_backward);
-    _mm_storeu_si128(&plaintext_blocks[i], plaintext_forward);
-    _mm_storeu_si128(&plaintext_blocks[last_block - i - 1], plaintext_backward);
-    mac_backward = _mm_xor_si128(mac_backward, ct_backward);
-    ctr_forward = Increment(ctr_forward);
-  }
-  // Decrypts and MACs another block, if there is a single block in the middle.
-  if (last_block & 1) {
-    __m128i ct = _mm_loadu_si128(&ciphertext_blocks[mid_block]);
-    mac_forward = _mm_xor_si128(mac_forward, ct);
-    Encrypt2Blocks(
-        Reverse(ctr_forward), mac_forward, &stream_forward, &mac_forward);
-    __m128i pt = _mm_xor_si128(ct, stream_forward);
-    _mm_storeu_si128(&plaintext_blocks[mid_block], pt);
-  }
-  if (!EqualBlocks(mac_forward, mac_backward)) {
-    absl::c_fill(plaintext, 0);
-    return false;
-  }
-  return true;
-}
-
-crypto::tink::util::StatusOr<std::string> AesEaxAesni::Encrypt(
-    absl::string_view plaintext, absl::string_view associated_data) const {
-  // BoringSSL expects a non-null pointer for plaintext and associated_data,
-  // regardless of whether the size is 0.
-  plaintext = internal::EnsureStringNonNull(plaintext);
-  associated_data = internal::EnsureStringNonNull(associated_data);
-
-  if (SIZE_MAX - nonce_size_ - kTagSize <= plaintext.size()) {
-    return util::Status(absl::StatusCode::kInvalidArgument,
-                        "Plaintext too long");
-  }
-  size_t ciphertext_size = plaintext.size() + nonce_size_ + kTagSize;
-  std::string ciphertext;
-  ResizeStringUninitialized(&ciphertext, ciphertext_size);
-  const std::string nonce = Random::GetRandomBytes(nonce_size_);
-  absl::c_copy(nonce, ciphertext.begin());
-  bool result = RawEncrypt(
-      nonce, plaintext, associated_data,
-      absl::MakeSpan(reinterpret_cast<uint8_t*>(&ciphertext[nonce_size_]),
-                     ciphertext_size - nonce_size_));
-  if (!result) {
-    return util::Status(absl::StatusCode::kInternal, "Encryption failed");
-  }
-  return ciphertext;
-}
-
-crypto::tink::util::StatusOr<std::string> AesEaxAesni::Decrypt(
-    absl::string_view ciphertext, absl::string_view associated_data) const {
-  // BoringSSL expects a non-null pointer for associated_data,
-  // regardless of whether the size is 0.
-  associated_data = internal::EnsureStringNonNull(associated_data);
-
-  size_t ct_size = ciphertext.size();
-  if (ct_size < nonce_size_ + kTagSize) {
-    return util::Status(absl::StatusCode::kInvalidArgument,
-                        "Ciphertext too short");
-  }
-  size_t out_size = ct_size - kTagSize - nonce_size_;
-  absl::string_view nonce = ciphertext.substr(0, nonce_size_);
-  absl::string_view encrypted =
-      ciphertext.substr(nonce_size_, ct_size - nonce_size_);
-  std::string res;
-  ResizeStringUninitialized(&res, out_size);
-  bool result = RawDecrypt(
-      nonce, encrypted, associated_data,
-      absl::MakeSpan(reinterpret_cast<uint8_t*>(&res[0]), res.size()));
-  if (!result) {
-    return util::Status(absl::StatusCode::kInternal, "Decryption failed");
-  }
-  return res;
-}
-
-}  // namespace subtle
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // __AES__
-#endif  // __SSE4_1__
-
-
diff --git a/cc/subtle/aes_eax_aesni.h b/cc/subtle/aes_eax_aesni.h
deleted file mode 100644
index 1a0021e..0000000
--- a/cc/subtle/aes_eax_aesni.h
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_SUBTLE_AES_EAX_AESNI_H_
-#define TINK_SUBTLE_AES_EAX_AESNI_H_
-
-#ifdef __SSE4_1__
-#ifdef __AES__
-
-#include <xmmintrin.h>
-
-#include <array>
-#include <memory>
-#include <string>
-
-#include "absl/strings/string_view.h"
-#include "absl/types/span.h"
-#include "tink/aead.h"
-#include "tink/util/secret_data.h"
-#include "tink/util/status.h"
-#include "tink/util/statusor.h"
-
-namespace crypto {
-namespace tink {
-namespace subtle {
-
-// This class implements AES-EAX on CPUs that support the AESNI instruction set
-// (as well as SSE 4.1).
-// Currently the implementation supports 128 and 256 bit keys and 96 or 128 bit
-// nonces. AES-EAX allows arbitrary nonce sizes. Allowing only 96 or 128 bits
-// is a tink specific restriction.
-class AesEaxAesni : public Aead {
- public:
-  static crypto::tink::util::StatusOr<std::unique_ptr<Aead>> New(
-      const util::SecretData& key, size_t nonce_size_in_bytes);
-
-  crypto::tink::util::StatusOr<std::string> Encrypt(
-      absl::string_view plaintext,
-      absl::string_view associated_data) const override;
-
-  crypto::tink::util::StatusOr<std::string> Decrypt(
-      absl::string_view ciphertext,
-      absl::string_view associated_data) const override;
-
- protected:
-  // The tag size is fixed for this implementation.
-  // Using the full 128-bits of the tag allows an efficient verification.
-  static constexpr size_t kTagSize = 16;
-  static constexpr size_t kBlockSize = 16;
-
-  virtual bool RawEncrypt(absl::string_view nonce, absl::string_view in,
-                          absl::string_view associated_data,
-                          absl::Span<uint8_t> ciphertext) const;
-
-  virtual bool RawDecrypt(absl::string_view nonce, absl::string_view in,
-                          absl::string_view associated_data,
-                          absl::Span<uint8_t> plaintext) const;
-
- private:
-  explicit AesEaxAesni(size_t nonce_size) : nonce_size_(nonce_size) {}
-
-  // AesEaxAesni instances are immutable objects.
-  // Therefore, the only place where SetKey should be called is in the
-  // construction, i.e. in New().
-  bool SetKey(const util::SecretData& key);
-
-  // Encrypt a single block.
-  __m128i EncryptBlock(const __m128i block) const;
-
-  // Encrypt 2 blocks with plain AES.
-  void Encrypt2Blocks(
-      const __m128i in0,
-      const __m128i in1,
-      __m128i *out0,
-      __m128i *out1) const;
-
-  // Encrypt 3 blocks and decrypts 1 block.
-  // This is used to decrypt a ciphertext and verify the MAC concurrently.
-  void Encrypt3Decrypt1(
-      const __m128i in0,
-      const __m128i in1,
-      const __m128i in2,
-      const __m128i in_dec,
-      __m128i* out0,
-      __m128i* out1,
-      __m128i* out2,
-      __m128i* out_dec) const;
-
-  // Pads a partial block of size 1 .. 16.
-  __m128i Pad(const uint8_t* data, int len) const;
-
-  // Computes an OMAC.
-  __m128i OMAC(absl::string_view blob, int tag) const;
-
-  static constexpr int kMaxRounds = 14;  // maximal number of rounds
-  static constexpr int kMaxRoundKeys =
-      kMaxRounds + 1;  // max number of round keys
-  using RoundKeys = std::array<__m128i, kMaxRoundKeys>;
-  util::SecretUniquePtr<RoundKeys> round_key_ =
-      util::MakeSecretUniquePtr<RoundKeys>();
-  util::SecretUniquePtr<RoundKeys> round_dec_key_ =
-      util::MakeSecretUniquePtr<RoundKeys>();
-  util::SecretUniquePtr<__m128i> B_ =
-      util::MakeSecretUniquePtr<__m128i>();  // Used for padding
-  util::SecretUniquePtr<__m128i> P_ =
-      util::MakeSecretUniquePtr<__m128i>();  // Used for padding
-  int rounds_;
-  const size_t nonce_size_;
-};
-
-}  // namespace subtle
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // __AES__
-#endif  // __SSE4_1__
-#endif  // TINK_SUBTLE_AES_EAX_AESNI_H_
-
diff --git a/cc/subtle/aes_eax_aesni_test.cc b/cc/subtle/aes_eax_aesni_test.cc
deleted file mode 100644
index 9f4f627..0000000
--- a/cc/subtle/aes_eax_aesni_test.cc
+++ /dev/null
@@ -1,280 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-#include <utility>
-#ifdef __SSE4_1__
-#ifdef __AES__
-
-#include "tink/subtle/aes_eax_aesni.h"
-
-#include <string>
-#include <vector>
-
-#include "gtest/gtest.h"
-#include "tink/subtle/wycheproof_util.h"
-#include "tink/util/secret_data.h"
-#include "tink/util/statusor.h"
-#include "tink/util/test_util.h"
-
-namespace crypto {
-namespace tink {
-namespace subtle {
-namespace {
-
-TEST(AesEaxAesniTest, testBasic) {
-  util::SecretData key = util::SecretDataFromStringView(
-      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
-  size_t nonce_size = 12;
-  auto res = AesEaxAesni::New(key, nonce_size);
-  EXPECT_TRUE(res.ok()) << res.status();
-  auto cipher = std::move(res.value());
-  std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto ct = cipher->Encrypt(message, aad);
-  EXPECT_TRUE(ct.ok()) << ct.status();
-  EXPECT_EQ(ct.value().size(), message.size() + nonce_size + 16);
-  auto pt = cipher->Decrypt(ct.value(), aad);
-  EXPECT_TRUE(pt.ok()) << pt.status();
-  EXPECT_EQ(pt.value(), message);
-}
-
-TEST(AesEaxAesniTest, testMessageSize) {
-  util::SecretData key = util::SecretDataFromStringView(
-      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
-  size_t nonce_size = 12;
-  auto res = AesEaxAesni::New(key, nonce_size);
-  EXPECT_TRUE(res.ok()) << res.status();
-  auto cipher = std::move(res.value());
-  for (size_t size = 0; size < 260; size++) {
-    std::string message(size, 'x');
-    std::string aad = "";
-    auto ct = cipher->Encrypt(message, aad);
-    EXPECT_TRUE(ct.ok()) << ct.status();
-    EXPECT_EQ(ct.value().size(), message.size() + nonce_size + 16);
-    auto pt = cipher->Decrypt(ct.value(), aad);
-    EXPECT_TRUE(pt.ok()) << pt.status();
-    EXPECT_EQ(pt.value(), message);
-  }
-}
-
-TEST(AesEaxAesniTest, testAadSize) {
-  util::SecretData key = util::SecretDataFromStringView(
-      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
-  size_t nonce_size = 12;
-  auto res = AesEaxAesni::New(key, nonce_size);
-  EXPECT_TRUE(res.ok()) << res.status();
-  auto cipher = std::move(res.value());
-  for (size_t size = 0; size < 260; size++) {
-    std::string message("Some message");
-    std::string aad(size, 'x');
-    auto ct = cipher->Encrypt(message, aad);
-    EXPECT_TRUE(ct.ok()) << ct.status();
-    EXPECT_EQ(ct.value().size(), message.size() + nonce_size + 16);
-    auto pt = cipher->Decrypt(ct.value(), aad);
-    EXPECT_TRUE(pt.ok()) << pt.status();
-    EXPECT_EQ(pt.value(), message);
-  }
-}
-
-TEST(AesEaxAesniTest, testLongNonce) {
-  util::SecretData key = util::SecretDataFromStringView(
-      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
-  size_t nonce_size = 16;
-  auto res = AesEaxAesni::New(key, nonce_size);
-  EXPECT_TRUE(res.ok()) << res.status();
-  auto cipher = std::move(res.value());
-  std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto ct = cipher->Encrypt(message, aad);
-  EXPECT_TRUE(ct.ok()) << ct.status();
-  EXPECT_EQ(ct.value().size(), message.size() + nonce_size + 16);
-  auto pt = cipher->Decrypt(ct.value(), aad);
-  EXPECT_TRUE(pt.ok()) << pt.status();
-  EXPECT_EQ(pt.value(), message);
-}
-
-TEST(AesEaxAesniTest, testModification) {
-  size_t nonce_size = 12;
-  util::SecretData key = util::SecretDataFromStringView(
-      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
-  auto cipher = std::move(AesEaxAesni::New(key, nonce_size).value());
-  std::string message = "Some data to encrypt.";
-  std::string associated_data = "Some data to authenticate.";
-  std::string ct = cipher->Encrypt(message, associated_data).value();
-  EXPECT_TRUE(cipher->Decrypt(ct, associated_data).ok());
-  // Modify the ciphertext
-  for (size_t i = 0; i < ct.size() * 8; i++) {
-    std::string modified_ct = ct;
-    modified_ct[i / 8] ^= 1 << (i % 8);
-    EXPECT_FALSE(cipher->Decrypt(modified_ct, associated_data).ok()) << i;
-  }
-  // Modify the associated data
-  for (size_t i = 0; i < associated_data.size() * 8; i++) {
-    std::string modified_associated_data = associated_data;
-    modified_associated_data[i / 8] ^= 1 << (i % 8);
-    auto decrypted = cipher->Decrypt(ct, modified_associated_data);
-    EXPECT_FALSE(decrypted.ok()) << i << " pt:" << decrypted.value();
-  }
-  // Truncate the ciphertext
-  for (size_t i = 0; i < ct.size(); i++) {
-    std::string truncated_ct(ct, 0, i);
-    EXPECT_FALSE(cipher->Decrypt(truncated_ct, associated_data).ok()) << i;
-  }
-}
-
-TEST(AesEaxAesniTest, testInvalidKeySizes) {
-  size_t nonce_size = 12;
-  for (int keysize = 0; keysize < 65; keysize++) {
-    if (keysize == 16 || keysize == 32) {
-      continue;
-    }
-    util::SecretData key(keysize, 'x');
-    auto cipher = AesEaxAesni::New(key, nonce_size);
-    EXPECT_FALSE(cipher.ok());
-  }
-}
-
-TEST(AesEaxAesniTest, testEmpty) {
-  size_t nonce_size = 12;
-  util::SecretData key = util::SecretDataFromStringView(
-      test::HexDecodeOrDie("bedcfb5a011ebc84600fcb296c15af0d"));
-  std::string nonce(test::HexDecodeOrDie("438a547a94ea88dce46c6c85"));
-  // Expected tag is an empty string with an empty tag is encrypted with
-  // the nonce above;
-  std::string tag(test::HexDecodeOrDie("9607977cd7556b1dfedf0c73a35a5197"));
-  std::string ciphertext = nonce + tag;
-  auto res = AesEaxAesni::New(key, nonce_size);
-  EXPECT_TRUE(res.ok()) << res.status();
-  auto cipher = std::move(res.value());
-
-  // Test decryption of the arguments above.
-  std::string empty_string("");
-  absl::string_view empty_string_view("");
-  absl::string_view null_string_view;
-
-  auto pt = cipher->Decrypt(ciphertext, empty_string);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-
-  pt = cipher->Decrypt(ciphertext, empty_string_view);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-
-  pt = cipher->Decrypt(ciphertext, null_string_view);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-
-  // Test encryption.
-  auto ct = cipher->Encrypt(empty_string, empty_string);
-  EXPECT_TRUE(ct.ok());
-  pt = cipher->Decrypt(ct.value(), empty_string);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-
-  ct = cipher->Encrypt(empty_string_view, empty_string_view);
-  EXPECT_TRUE(ct.ok());
-  pt = cipher->Decrypt(ct.value(), empty_string);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-
-  ct = cipher->Encrypt(empty_string_view, empty_string_view);
-  EXPECT_TRUE(ct.ok());
-  pt = cipher->Decrypt(ct.value(), empty_string);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-
-  ct = cipher->Encrypt(null_string_view, null_string_view);
-  EXPECT_TRUE(ct.ok());
-  pt = cipher->Decrypt(ct.value(), empty_string);
-  EXPECT_TRUE(pt.ok());
-  EXPECT_EQ(0, pt.value().size());
-}
-
-// Test with test vectors from project Wycheproof.
-// AesEaxAesni does not allow to pass in IVs. Therefore this test
-// can only test decryption.
-// Currently AesEaxAesni is restricted to encryption with 12 byte
-// IVs and 16 byte tags. Therefore it is necessary to skip tests with
-// other parameter sizes.
-bool WycheproofTest(const rapidjson::Document &root) {
-  int errors = 0;
-  for (const rapidjson::Value& test_group : root["testGroups"].GetArray()) {
-    const size_t iv_size = test_group["ivSize"].GetInt();
-    const size_t key_size = test_group["keySize"].GetInt();
-    const size_t tag_size = test_group["tagSize"].GetInt();
-    if (key_size != 128 && key_size != 256) {
-      // Not supported
-      continue;
-    }
-    if (iv_size != 128 && iv_size != 96) {
-      // Not supported
-      continue;
-    }
-    if (tag_size != 128) {
-      // Not supported
-      continue;
-    }
-    for (const rapidjson::Value& test : test_group["tests"].GetArray()) {
-      std::string comment = test["comment"].GetString();
-      util::SecretData key =
-          util::SecretDataFromStringView(WycheproofUtil::GetBytes(test["key"]));
-      std::string iv = WycheproofUtil::GetBytes(test["iv"]);
-      std::string msg = WycheproofUtil::GetBytes(test["msg"]);
-      std::string ct = WycheproofUtil::GetBytes(test["ct"]);
-      std::string aad = WycheproofUtil::GetBytes(test["aad"]);
-      std::string tag = WycheproofUtil::GetBytes(test["tag"]);
-      int id = test["tcId"].GetInt();
-      std::string expected = test["result"].GetString();
-      auto cipher = std::move(AesEaxAesni::New(key, iv_size / 8).value());
-      auto result = cipher->Decrypt(iv + ct + tag, aad);
-      bool success = result.ok();
-      if (success) {
-        std::string decrypted = result.value();
-        if (expected == "invalid") {
-          ADD_FAILURE() << "decrypted invalid ciphertext:" << id;
-          errors++;
-        } else if (msg != decrypted) {
-          ADD_FAILURE() << "Incorrect decryption:" << id;
-          errors++;
-        }
-      } else {
-        if (expected == "valid" || expected == "acceptable") {
-          ADD_FAILURE()
-              << "Could not decrypt test with tcId:" << id
-              << " iv_size:" << iv_size
-              << " tag_size:" << tag_size
-              << " key_size:" << key_size;
-          errors++;
-        }
-      }
-    }
-  }
-  return errors == 0;
-}
-
-TEST(AesEaxAesniTest, TestVectors) {
-  std::unique_ptr<rapidjson::Document> root =
-      WycheproofUtil::ReadTestVectors("aes_eax_test.json");
-  ASSERT_TRUE(WycheproofTest(*root));
-}
-
-}  // namespace
-}  // namespace subtle
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // __AES__
-#endif  // __SSE4_1__
diff --git a/cc/subtle/aes_eax_boringssl.cc b/cc/subtle/aes_eax_boringssl.cc
index 11eafa0..3805cab 100644
--- a/cc/subtle/aes_eax_boringssl.cc
+++ b/cc/subtle/aes_eax_boringssl.cc
@@ -66,7 +66,7 @@
 #if defined(ABSL_IS_LITTLE_ENDIAN)
   return ByteSwap(Load64(src));
 #elif defined(ABSL_IS_BIG_ENDIAN)
-  return val;
+  return Load64(src);
 #else
 #error Unknown endianness
 #endif
diff --git a/cc/subtle/aes_eax_boringssl_test.cc b/cc/subtle/aes_eax_boringssl_test.cc
index 82c5067..9e94fe3 100644
--- a/cc/subtle/aes_eax_boringssl_test.cc
+++ b/cc/subtle/aes_eax_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_eax_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/cc/subtle/aes_gcm_boringssl_test.cc b/cc/subtle/aes_gcm_boringssl_test.cc
index dd1ad04..b18294a 100644
--- a/cc/subtle/aes_gcm_boringssl_test.cc
+++ b/cc/subtle/aes_gcm_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_gcm_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -26,7 +27,7 @@
 #include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
 #include "tink/aead/internal/wycheproof_aead.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
@@ -54,7 +55,7 @@
 class AesGcmBoringSslTest : public Test {
  protected:
   void SetUp() override {
-    if (IsFipsModeEnabled() && !FIPS_mode()) {
+    if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
       GTEST_SKIP() << "Test should not run in FIPS mode when BoringCrypto is "
                       "unavailable.";
     }
@@ -224,7 +225,7 @@
 }
 
 TEST(AesGcmBoringSslFipsTest, FipsOnly) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -239,7 +240,7 @@
 }
 
 TEST(AesGcmBoringSslFipsTest, FipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -258,7 +259,7 @@
 class AesGcmBoringSslWycheproofTest
     : public TestWithParam<internal::WycheproofTestVector> {
   void SetUp() override {
-    if (IsFipsModeEnabled() && !FIPS_mode()) {
+    if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
       GTEST_SKIP() << "Test should not run in FIPS mode when BoringCrypto is "
                       "unavailable.";
     }
diff --git a/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter.cc b/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter.cc
index 1d570f4..7f14243 100644
--- a/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter.cc
+++ b/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter.cc
@@ -21,6 +21,7 @@
 #include <limits>
 #include <memory>
 #include <utility>
+#include <vector>
 
 #include "absl/algorithm/container.h"
 #include "absl/base/config.h"
diff --git a/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter_test.cc b/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter_test.cc
index 8c3ed14..41910fe 100644
--- a/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter_test.cc
+++ b/cc/subtle/aes_gcm_hkdf_stream_segment_decrypter_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_gcm_hkdf_stream_segment_decrypter.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/cc/subtle/aes_gcm_hkdf_stream_segment_encrypter.cc b/cc/subtle/aes_gcm_hkdf_stream_segment_encrypter.cc
index aef5e5e..970918e 100644
--- a/cc/subtle/aes_gcm_hkdf_stream_segment_encrypter.cc
+++ b/cc/subtle/aes_gcm_hkdf_stream_segment_encrypter.cc
@@ -22,6 +22,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/algorithm/container.h"
 #include "absl/base/config.h"
diff --git a/cc/subtle/aes_gcm_hkdf_streaming.cc b/cc/subtle/aes_gcm_hkdf_streaming.cc
index cd6fbfd..ab59d5d 100644
--- a/cc/subtle/aes_gcm_hkdf_streaming.cc
+++ b/cc/subtle/aes_gcm_hkdf_streaming.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_gcm_hkdf_streaming.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/aes_gcm_siv_boringssl_test.cc b/cc/subtle/aes_gcm_siv_boringssl_test.cc
index 836ac24..2b39f2b 100644
--- a/cc/subtle/aes_gcm_siv_boringssl_test.cc
+++ b/cc/subtle/aes_gcm_siv_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_gcm_siv_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <vector>
 
diff --git a/cc/subtle/aes_siv_boringssl.cc b/cc/subtle/aes_siv_boringssl.cc
index 6a2ff7e..3d9ac86 100644
--- a/cc/subtle/aes_siv_boringssl.cc
+++ b/cc/subtle/aes_siv_boringssl.cc
@@ -19,6 +19,7 @@
 #include <algorithm>
 #include <cstdint>
 #include <iterator>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/aes_siv_boringssl_test.cc b/cc/subtle/aes_siv_boringssl_test.cc
index 6b4d081..fd91061 100644
--- a/cc/subtle/aes_siv_boringssl_test.cc
+++ b/cc/subtle/aes_siv_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/aes_siv_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -115,7 +116,7 @@
           "812731321de508761437195ff231765aa4913219873ac6918639816312130011"
           "abc900bba11400187984719827431246bbab1231eb4145215ff7141436616beb"
           "9817298148712fed3aab61000ff123313e"));
-  for (int keysize = 0; keysize <= keymaterial.size(); ++keysize){
+  for (int keysize = 0; keysize < keymaterial.size(); ++keysize){
     util::SecretData key(&keymaterial[0], &keymaterial[keysize]);
     auto cipher = AesSivBoringSsl::New(key);
     if (keysize == 64) {
diff --git a/cc/subtle/decrypting_random_access_stream.cc b/cc/subtle/decrypting_random_access_stream.cc
index 0b6d33e..5a99a53 100644
--- a/cc/subtle/decrypting_random_access_stream.cc
+++ b/cc/subtle/decrypting_random_access_stream.cc
@@ -18,6 +18,8 @@
 
 #include <algorithm>
 #include <cstring>
+#include <limits>
+#include <memory>
 #include <utility>
 #include <vector>
 
diff --git a/cc/subtle/decrypting_random_access_stream_test.cc b/cc/subtle/decrypting_random_access_stream_test.cc
index 1d404f3..8c67e2e 100644
--- a/cc/subtle/decrypting_random_access_stream_test.cc
+++ b/cc/subtle/decrypting_random_access_stream_test.cc
@@ -17,6 +17,9 @@
 #include "tink/subtle/decrypting_random_access_stream.h"
 
 #include <algorithm>
+#include <cstring>
+#include <limits>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
@@ -27,25 +30,24 @@
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_random_access_stream.h"
 #include "tink/output_stream.h"
 #include "tink/random_access_stream.h"
 #include "tink/streaming_aead.h"
 #include "tink/subtle/random.h"
 #include "tink/subtle/test_util.h"
-#include "tink/util/file_random_access_stream.h"
 #include "tink/util/ostream_output_stream.h"
 #include "tink/util/status.h"
 #include "tink/util/test_matchers.h"
-#include "tink/util/test_util.h"
 
 namespace crypto {
 namespace tink {
 namespace subtle {
 namespace {
 
+using ::crypto::tink::internal::TestRandomAccessStream;
 using crypto::tink::subtle::test::DummyStreamingAead;
 using crypto::tink::subtle::test::DummyStreamSegmentDecrypter;
-using crypto::tink::test::GetTestFileDescriptor;
 using crypto::tink::test::IsOk;
 using crypto::tink::test::StatusIs;
 using subtle::test::WriteToStream;
@@ -77,16 +79,6 @@
   int ct_offset_;
 };
 
-// Creates a RandomAccessStream with the specified contents.
-std::unique_ptr<RandomAccessStream> GetRandomAccessStream(
-    absl::string_view contents) {
-  static int index = 1;
-  std::string filename = absl::StrCat("stream_data_file_", index, ".txt");
-  index++;
-  int input_fd = GetTestFileDescriptor(filename, contents);
-  return {absl::make_unique<util::FileRandomAccessStream>(input_fd)};
-}
-
 // Returns a ciphertext resulting from encryption of 'pt' with 'aad' as
 // associated data, using 'saead'.
 std::string GetCiphertext(StreamingAead* saead, absl::string_view pt,
@@ -115,24 +107,8 @@
                                                         absl::string_view pt,
                                                         absl::string_view aad,
                                                         int ct_offset) {
-  return GetRandomAccessStream(GetCiphertext(saead, pt, aad, ct_offset));
-}
-
-// Reads the entire 'ra_stream', until no more bytes can be read,
-// and puts the read bytes into 'contents'.
-// Returns the status of the last ra_stream->PRead()-operation.
-util::Status ReadAll(RandomAccessStream* ra_stream, std::string* contents) {
-  int chunk_size = 42;
-  contents->clear();
-  auto buffer = std::move(util::Buffer::New(chunk_size).value());
-  int64_t position = 0;
-  auto status = util::OkStatus();
-  while (status.ok()) {
-    status = ra_stream->PRead(position, chunk_size, buffer.get());
-    contents->append(buffer->get_mem_block(), buffer->size());
-    position = contents->size();
-  }
-  return status;
+  return std::make_unique<TestRandomAccessStream>(
+      GetCiphertext(saead, pt, aad, ct_offset));
 }
 
 TEST(DecryptingRandomAccessStreamTest, NegativeCiphertextOffset) {
@@ -222,7 +198,8 @@
           auto dec_stream = std::move(dec_stream_result.value());
           EXPECT_EQ(pt_size, dec_stream->size().value());
           std::string decrypted;
-          auto status = ReadAll(dec_stream.get(), &decrypted);
+          auto status = internal::ReadAllFromRandomAccessStream(
+              dec_stream.get(), decrypted);
           EXPECT_THAT(status, StatusIs(absl::StatusCode::kOutOfRange,
                                        HasSubstr("EOF")));
           EXPECT_EQ(plaintext, decrypted);
@@ -304,8 +281,8 @@
               SCOPED_TRACE(absl::StrCat("ct_size = ", ct.size(),
                                         ", trunc_ct_size = ", trunc_ct_size,
                                         ", chunk_size = ", chunk_size));
-              auto trunc_ct =
-                  GetRandomAccessStream(ct.substr(0, trunc_ct_size));
+              auto trunc_ct = std::make_unique<TestRandomAccessStream>(
+                  ct.substr(0, trunc_ct_size));
               int position = 0;
               auto per_stream_seg_decrypter =
                   absl::make_unique<DummyStreamSegmentDecrypter>(
@@ -376,8 +353,8 @@
   for (int ct_size : {0, 10, 100}) {
     SCOPED_TRACE(absl::StrCat("ct_size = ", ct_size));
     // Try decrypting a wrong ciphertext.
-    auto wrong_ct =
-        GetRandomAccessStream(subtle::Random::GetRandomBytes(ct_size));
+    auto wrong_ct = std::make_unique<TestRandomAccessStream>(
+        subtle::Random::GetRandomBytes(ct_size));
     auto seg_decrypter = absl::make_unique<DummyStreamSegmentDecrypter>(
         pt_segment_size, header_size, ct_offset);
     auto dec_stream_result = DecryptingRandomAccessStream::New(
@@ -394,7 +371,8 @@
 }
 
 TEST(DecryptingRandomAccessStreamTest, NullSegmentDecrypter) {
-  auto ct_stream = GetRandomAccessStream("some ciphertext contents");
+  auto ct_stream =
+      std::make_unique<TestRandomAccessStream>("some ciphertext contents");
   auto dec_stream_result =
       DecryptingRandomAccessStream::New(nullptr, std::move(ct_stream));
   EXPECT_THAT(dec_stream_result.status(),
diff --git a/cc/subtle/ecdsa_sign_boringssl.cc b/cc/subtle/ecdsa_sign_boringssl.cc
index e524033..a14346a 100644
--- a/cc/subtle/ecdsa_sign_boringssl.cc
+++ b/cc/subtle/ecdsa_sign_boringssl.cc
@@ -16,76 +16,24 @@
 
 #include "tink/subtle/ecdsa_sign_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
 
 #include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
-#include "openssl/bn.h"
-#include "openssl/ec.h"
-#include "openssl/ecdsa.h"
 #include "openssl/evp.h"
-#include "tink/internal/bn_util.h"
-#include "tink/internal/ec_util.h"
-#include "tink/internal/err_util.h"
 #include "tink/internal/md_util.h"
-#include "tink/internal/ssl_unique_ptr.h"
 #include "tink/internal/util.h"
+#include "tink/signature/internal/ecdsa_raw_sign_boringssl.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
-#include "tink/util/errors.h"
 #include "tink/util/statusor.h"
 
 namespace crypto {
 namespace tink {
 namespace subtle {
 
-namespace {
-
-// Transforms ECDSA DER signature encoding to IEEE_P1363 encoding.
-//
-// The IEEE_P1363 signature's format is r || s, where r and s are zero-padded
-// and have the same size in bytes as the order of the curve. For example, for
-// NIST P-256 curve, r and s are zero-padded to 32 bytes.
-//
-// The DER signature is encoded using ASN.1
-// (https://tools.ietf.org/html/rfc5480#appendix-A): ECDSA-Sig-Value :: =
-// SEQUENCE { r INTEGER, s INTEGER }. In particular, the encoding is: 0x30 ||
-// totalLength || 0x02 || r's length || r || 0x02 || s's length || s.
-crypto::tink::util::StatusOr<std::string> DerToIeee(absl::string_view der,
-                                                    const EC_KEY* key) {
-  size_t field_size_in_bytes =
-      (EC_GROUP_get_degree(EC_KEY_get0_group(key)) + 7) / 8;
-
-  ECDSA_SIG* ecdsa_ptr = nullptr;
-  const uint8_t* der_ptr = reinterpret_cast<const uint8_t*>(der.data());
-  // Note: d2i_ECDSA_SIG is deprecated in BoringSSL, but it isn't in OpenSSL.
-  internal::SslUniquePtr<ECDSA_SIG> ecdsa(
-      d2i_ECDSA_SIG(&ecdsa_ptr, &der_ptr, der.size()));
-  if (ecdsa == nullptr) {
-    return util::Status(absl::StatusCode::kInternal, "d2i_ECDSA_SIG failed");
-  }
-
-  const BIGNUM* r_bn;
-  const BIGNUM* s_bn;
-  ECDSA_SIG_get0(ecdsa.get(), &r_bn, &s_bn);
-  util::StatusOr<std::string> r =
-      internal::BignumToString(r_bn, field_size_in_bytes);
-  if (!r.ok()) {
-    return r.status();
-  }
-  util::StatusOr<std::string> s =
-      internal::BignumToString(s_bn, field_size_in_bytes);
-  if (!s.ok()) {
-    return s.status();
-  }
-  return absl::StrCat(*r, *s);
-}
-
-}  // namespace
-
-// static
 util::StatusOr<std::unique_ptr<EcdsaSignBoringSsl>> EcdsaSignBoringSsl::New(
     const SubtleUtilBoringSSL::EcKey& ec_key, HashType hash_type,
     EcdsaSignatureEncoding encoding) {
@@ -102,47 +50,14 @@
     return hash.status();
   }
 
-  // Check curve.
-  util::StatusOr<internal::SslUniquePtr<EC_GROUP>> group =
-      internal::EcGroupFromCurveType(ec_key.curve);
-  if (!group.ok()) {
-    return group.status();
-  }
-  internal::SslUniquePtr<EC_KEY> key(EC_KEY_new());
-  EC_KEY_set_group(key.get(), group->get());
+  util::StatusOr<std::unique_ptr<internal::EcdsaRawSignBoringSsl>> raw_sign =
+      internal::EcdsaRawSignBoringSsl::New(ec_key, encoding);
+  if (!raw_sign.ok()) return raw_sign.status();
 
-  // Check key.
-  util::StatusOr<internal::SslUniquePtr<EC_POINT>> pub_key =
-      internal::GetEcPoint(ec_key.curve, ec_key.pub_x, ec_key.pub_y);
-  if (!pub_key.ok()) {
-    return pub_key.status();
-  }
-
-  if (!EC_KEY_set_public_key(key.get(), pub_key->get())) {
-    return util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("Invalid public key: ", internal::GetSslErrors()));
-  }
-
-  internal::SslUniquePtr<BIGNUM> priv_key(
-      BN_bin2bn(ec_key.priv.data(), ec_key.priv.size(), nullptr));
-  if (!EC_KEY_set_private_key(key.get(), priv_key.get())) {
-    return util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("Invalid private key: ", internal::GetSslErrors()));
-  }
-
-  // Sign.
-  std::unique_ptr<EcdsaSignBoringSsl> sign(
-      new EcdsaSignBoringSsl(std::move(key), *hash, encoding));
-  return std::move(sign);
+  return {
+      absl::WrapUnique(new EcdsaSignBoringSsl(*hash, std::move(*raw_sign)))};
 }
 
-EcdsaSignBoringSsl::EcdsaSignBoringSsl(internal::SslUniquePtr<EC_KEY> key,
-                                       const EVP_MD* hash,
-                                       EcdsaSignatureEncoding encoding)
-    : key_(std::move(key)), hash_(hash), encoding_(encoding) {}
-
 util::StatusOr<std::string> EcdsaSignBoringSsl::Sign(
     absl::string_view data) const {
   // BoringSSL expects a non-null pointer for data,
@@ -159,24 +74,8 @@
   }
 
   // Compute the signature.
-  std::vector<uint8_t> buffer(ECDSA_size(key_.get()));
-  unsigned int sig_length;
-  if (1 != ECDSA_sign(0 /* unused */, digest, digest_size, buffer.data(),
-                      &sig_length, key_.get())) {
-    return util::Status(absl::StatusCode::kInternal, "Signing failed.");
-  }
-
-  if (encoding_ == subtle::EcdsaSignatureEncoding::IEEE_P1363) {
-    auto status_or_sig = DerToIeee(
-        std::string(reinterpret_cast<char*>(buffer.data()), sig_length),
-        key_.get());
-    if (!status_or_sig.ok()) {
-      return status_or_sig.status();
-    }
-    return status_or_sig.value();
-  }
-
-  return std::string(reinterpret_cast<char*>(buffer.data()), sig_length);
+  return raw_signer_->Sign(
+      absl::string_view(reinterpret_cast<char*>(digest), digest_size));
 }
 
 }  // namespace subtle
diff --git a/cc/subtle/ecdsa_sign_boringssl.h b/cc/subtle/ecdsa_sign_boringssl.h
index c3323ca..a05d39e 100644
--- a/cc/subtle/ecdsa_sign_boringssl.h
+++ b/cc/subtle/ecdsa_sign_boringssl.h
@@ -19,13 +19,13 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "absl/strings/string_view.h"
-#include "openssl/ec.h"
 #include "openssl/evp.h"
 #include "tink/internal/fips_utils.h"
-#include "tink/internal/ssl_unique_ptr.h"
 #include "tink/public_key_sign.h"
+#include "tink/signature/internal/ecdsa_raw_sign_boringssl.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/util/statusor.h"
@@ -49,12 +49,13 @@
       crypto::tink::internal::FipsCompatibility::kRequiresBoringCrypto;
 
  private:
-  EcdsaSignBoringSsl(internal::SslUniquePtr<EC_KEY> key, const EVP_MD* hash,
-                     EcdsaSignatureEncoding encoding);
+  explicit EcdsaSignBoringSsl(
+      const EVP_MD* hash,
+      std::unique_ptr<internal::EcdsaRawSignBoringSsl> raw_signer)
+      : hash_(hash), raw_signer_(std::move(raw_signer)) {}
 
-  internal::SslUniquePtr<EC_KEY> key_;
   const EVP_MD* hash_;  // Owned by BoringSSL.
-  EcdsaSignatureEncoding encoding_;
+  std::unique_ptr<internal::EcdsaRawSignBoringSsl> raw_signer_;
 };
 
 }  // namespace subtle
diff --git a/cc/subtle/ecdsa_sign_boringssl_test.cc b/cc/subtle/ecdsa_sign_boringssl_test.cc
index d2a236b..666b8db 100644
--- a/cc/subtle/ecdsa_sign_boringssl_test.cc
+++ b/cc/subtle/ecdsa_sign_boringssl_test.cc
@@ -21,8 +21,8 @@
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
-#include "tink/config/tink_fips.h"
 #include "tink/internal/ec_util.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
 #include "tink/subtle/common_enums.h"
@@ -43,7 +43,7 @@
 class EcdsaSignBoringSslTest : public ::testing::Test {};
 
 TEST_F(EcdsaSignBoringSslTest, testBasicSigning) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -84,7 +84,7 @@
 }
 
 TEST_F(EcdsaSignBoringSslTest, testEncodingsMismatch) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -115,7 +115,7 @@
 }
 
 TEST_F(EcdsaSignBoringSslTest, testSignatureSizesWithIEEE_P1364Encoding) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -149,7 +149,7 @@
 }
 
 TEST_F(EcdsaSignBoringSslTest, testNewErrors) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -164,7 +164,7 @@
 
 // FIPS-only mode test
 TEST_F(EcdsaSignBoringSslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
diff --git a/cc/subtle/ecdsa_verify_boringssl.cc b/cc/subtle/ecdsa_verify_boringssl.cc
index 962d6d4..b14e475 100644
--- a/cc/subtle/ecdsa_verify_boringssl.cc
+++ b/cc/subtle/ecdsa_verify_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/ecdsa_verify_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/ecdsa_verify_boringssl_test.cc b/cc/subtle/ecdsa_verify_boringssl_test.cc
index b82dadd..292218f 100644
--- a/cc/subtle/ecdsa_verify_boringssl_test.cc
+++ b/cc/subtle/ecdsa_verify_boringssl_test.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/ecdsa_verify_boringssl.h"
 
 #include <iostream>
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -24,7 +25,7 @@
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "include/rapidjson/document.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
 #include "tink/subtle/common_enums.h"
@@ -46,7 +47,7 @@
 class EcdsaVerifyBoringSslTest : public ::testing::Test {};
 
 TEST_F(EcdsaVerifyBoringSslTest, BasicSigning) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -88,7 +89,7 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, EncodingsMismatch) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -124,7 +125,7 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, NewErrors) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -221,7 +222,7 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofCurveP256) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -230,7 +231,7 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofCurveP384) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -239,7 +240,7 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofCurveP521) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -248,7 +249,7 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofWithIeeeP1363Encoding) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -258,7 +259,7 @@
 
 // FIPS-only mode test
 TEST_F(EcdsaVerifyBoringSslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
diff --git a/cc/subtle/ecies_hkdf_recipient_kem_boringssl.cc b/cc/subtle/ecies_hkdf_recipient_kem_boringssl.cc
index 105672f..0415a99 100644
--- a/cc/subtle/ecies_hkdf_recipient_kem_boringssl.cc
+++ b/cc/subtle/ecies_hkdf_recipient_kem_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/ecies_hkdf_recipient_kem_boringssl.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/memory/memory.h"
diff --git a/cc/subtle/ecies_hkdf_recipient_kem_boringssl_test.cc b/cc/subtle/ecies_hkdf_recipient_kem_boringssl_test.cc
index d957a5f..3733920 100644
--- a/cc/subtle/ecies_hkdf_recipient_kem_boringssl_test.cc
+++ b/cc/subtle/ecies_hkdf_recipient_kem_boringssl_test.cc
@@ -18,6 +18,7 @@
 
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
diff --git a/cc/subtle/ecies_hkdf_sender_kem_boringssl.cc b/cc/subtle/ecies_hkdf_sender_kem_boringssl.cc
index 86f505c..8815a06 100644
--- a/cc/subtle/ecies_hkdf_sender_kem_boringssl.cc
+++ b/cc/subtle/ecies_hkdf_sender_kem_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/ecies_hkdf_sender_kem_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/ecies_hkdf_sender_kem_boringssl_test.cc b/cc/subtle/ecies_hkdf_sender_kem_boringssl_test.cc
index 01df86f..0e9c815 100644
--- a/cc/subtle/ecies_hkdf_sender_kem_boringssl_test.cc
+++ b/cc/subtle/ecies_hkdf_sender_kem_boringssl_test.cc
@@ -17,8 +17,10 @@
 #include "tink/subtle/ecies_hkdf_sender_kem_boringssl.h"
 
 #include <iostream>
+#include <ostream>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
diff --git a/cc/subtle/ed25519_sign_boringssl.cc b/cc/subtle/ed25519_sign_boringssl.cc
index f8b32d4..2b715e2 100644
--- a/cc/subtle/ed25519_sign_boringssl.cc
+++ b/cc/subtle/ed25519_sign_boringssl.cc
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <iterator>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/ed25519_sign_boringssl_test.cc b/cc/subtle/ed25519_sign_boringssl_test.cc
index c432afc..16e34ee 100644
--- a/cc/subtle/ed25519_sign_boringssl_test.cc
+++ b/cc/subtle/ed25519_sign_boringssl_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/ed25519_sign_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/ed25519_verify_boringssl.cc b/cc/subtle/ed25519_verify_boringssl.cc
index 750c603..853bb22 100644
--- a/cc/subtle/ed25519_verify_boringssl.cc
+++ b/cc/subtle/ed25519_verify_boringssl.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/ed25519_verify_boringssl.h"
 
 #include <cstring>
+#include <memory>
 #include <utility>
 
 #include "absl/memory/memory.h"
diff --git a/cc/subtle/ed25519_verify_boringssl_test.cc b/cc/subtle/ed25519_verify_boringssl_test.cc
index b3ed053..a086cc4 100644
--- a/cc/subtle/ed25519_verify_boringssl_test.cc
+++ b/cc/subtle/ed25519_verify_boringssl_test.cc
@@ -16,9 +16,11 @@
 
 #include "tink/subtle/ed25519_verify_boringssl.h"
 
+#include <iostream>
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
diff --git a/cc/subtle/encrypt_then_authenticate.cc b/cc/subtle/encrypt_then_authenticate.cc
index 9d84dce..a499a77 100644
--- a/cc/subtle/encrypt_then_authenticate.cc
+++ b/cc/subtle/encrypt_then_authenticate.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/encrypt_then_authenticate.h"
 
 #include <cstdint>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -36,7 +37,7 @@
 namespace tink {
 namespace subtle {
 
-static const std::string longToBigEndianStr(uint64_t value) {
+static std::string longToBigEndianStr(uint64_t value) {
   uint8_t bytes[8];
   for (int i = sizeof(bytes) - 1; i >= 0; i--) {
     bytes[i] = value & 0xff;
@@ -72,23 +73,20 @@
                         "associated data too long");
   }
 
-  auto ct = ind_cpa_cipher_->Encrypt(plaintext);
-  if (!ct.ok()) {
-    return ct.status();
+  auto ciphertext = ind_cpa_cipher_->Encrypt(plaintext);
+  if (!ciphertext.ok()) {
+    return ciphertext.status();
   }
-  std::string ciphertext(ct.value());
-  std::string toAuthData =
-      absl::StrCat(associated_data, ciphertext,
-                   longToBigEndianStr(associated_data_size_in_bits));
-
-  auto tag = mac_->ComputeMac(toAuthData);
+  auto tag = mac_->ComputeMac(
+      absl::StrCat(associated_data, *ciphertext,
+                   longToBigEndianStr(associated_data_size_in_bits)));
   if (!tag.ok()) {
     return tag.status();
   }
-  if (tag.value().size() != tag_size_) {
+  if (tag->size() != tag_size_) {
     return util::Status(absl::StatusCode::kInternal, "invalid tag size");
   }
-  return ciphertext.append(tag.value());
+  return ciphertext->append(tag.value());
 }
 
 util::StatusOr<std::string> EncryptThenAuthenticate::Decrypt(
@@ -112,11 +110,10 @@
 
   auto payload = ciphertext.substr(0, ciphertext.size() - tag_size_);
   auto tag = ciphertext.substr(ciphertext.size() - tag_size_, tag_size_);
-  std::string toAuthData =
-      absl::StrCat(associated_data, payload,
-                   longToBigEndianStr(associated_data_size_in_bits));
 
-  auto verified = mac_->VerifyMac(tag, toAuthData);
+  auto verified = mac_->VerifyMac(
+      tag, absl::StrCat(associated_data, payload,
+                        longToBigEndianStr(associated_data_size_in_bits)));
   if (!verified.ok()) {
     return verified;
   }
diff --git a/cc/subtle/encrypt_then_authenticate_test.cc b/cc/subtle/encrypt_then_authenticate_test.cc
index d0dfc2a..fcaa3d3 100644
--- a/cc/subtle/encrypt_then_authenticate_test.cc
+++ b/cc/subtle/encrypt_then_authenticate_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/encrypt_then_authenticate.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -274,9 +275,6 @@
 // test ensures that the overflow issue and the auth bypass vulnerability are
 // fixed.
 TEST(EncryptThenAuthenticateTest, testAuthBypassShouldNotWork) {
-// Disable this test when running with ASYLO, because it allocates more memory
-// than ASYLO can handle.
-#ifndef __ASYLO__
   int encryption_key_size = 16;
   int iv_size = 12;
   int mac_key_size = 16;
@@ -289,7 +287,11 @@
   const std::string message = "Some data to encrypt.";
   // ...with a long associated_data whose size in bits converted to an unsigned
   // 32-bit integer is 0.
-  const std::string associated_data = std::string(1 << 29, 'a');
+  std::string associated_data;
+  constexpr size_t kAssociatedDataSize = 1 << 29;
+  constexpr size_t kCiphertextSpace = 1000;
+  associated_data.reserve(kAssociatedDataSize + kCiphertextSpace);
+  associated_data.resize(kAssociatedDataSize, 'a');
   auto encrypted = cipher->Encrypt(message, associated_data);
   EXPECT_TRUE(encrypted.ok()) << encrypted.status();
   auto ct = encrypted.value();
@@ -299,10 +301,9 @@
   // Test that the 2^29-byte associated_data is NOT considered equal to an empty
   // associated_data. That is, test that a valid tag for (ciphertext,
   // associated_data) is INVALID for (associated_data + ciphertext, "").
-  ct = associated_data + ct;
+  ct = std::move(associated_data) + ct;
   decrypted = cipher->Decrypt(ct, "");
   EXPECT_FALSE(decrypted.ok());
-#endif  // __ASYLO__
 }
 
 }  // namespace
diff --git a/cc/subtle/hkdf.cc b/cc/subtle/hkdf.cc
index ef96eb7..b62da4a 100644
--- a/cc/subtle/hkdf.cc
+++ b/cc/subtle/hkdf.cc
@@ -16,20 +16,21 @@
 
 #include "tink/subtle/hkdf.h"
 
+#include <cstdint>
 #include <string>
 
 #include "absl/algorithm/container.h"
 #include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "openssl/evp.h"
-// BoringSSL and OpenSSL have incompatible ways to compute HDKF: BoringSSL
+// BoringSSL and OpenSSL have incompatible ways to compute HKDF: BoringSSL
 // provides a one-shot API HKDF, while OpenSSL doesn't make that API public, but
 // instead provides this functionality over the EVP interface, which in turn
 // doesn't provide means to compute HKDF in BoringSSL. As a consequence, we need
 // to selectively include the correct header and use different implementations.
 #ifdef OPENSSL_IS_BORINGSSL
+#include "openssl/base.h"
 #include "openssl/hkdf.h"
 #else
 #include "openssl/kdf.h"
@@ -52,11 +53,12 @@
 util::Status SslHkdf(const EVP_MD *evp_md, absl::string_view ikm,
                      absl::string_view salt, absl::string_view info,
                      absl::Span<uint8_t> out_key) {
+  const uint8_t *ikm_ptr = reinterpret_cast<const uint8_t *>(ikm.data());
+  const uint8_t *salt_ptr = reinterpret_cast<const uint8_t *>(salt.data());
+  const uint8_t *info_ptr = reinterpret_cast<const uint8_t *>(info.data());
 #ifdef OPENSSL_IS_BORINGSSL
-  if (HKDF(out_key.data(), out_key.size(), evp_md,
-           reinterpret_cast<const uint8_t *>(ikm.data()), ikm.size(),
-           reinterpret_cast<const uint8_t *>(salt.data()), salt.size(),
-           reinterpret_cast<const uint8_t *>(info.data()), info.size()) != 1) {
+  if (HKDF(out_key.data(), out_key.size(), evp_md, ikm_ptr, ikm.size(),
+           salt_ptr, salt.size(), info_ptr, info.size()) != 1) {
     return util::Status(absl::StatusCode::kInternal, "HKDF failed");
   }
   return util::OkStatus();
@@ -65,11 +67,9 @@
       EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, /*e=*/nullptr));
   if (pctx == nullptr || EVP_PKEY_derive_init(pctx.get()) <= 0 ||
       EVP_PKEY_CTX_set_hkdf_md(pctx.get(), evp_md) <= 0 ||
-      EVP_PKEY_CTX_set1_hkdf_salt(pctx.get(), salt.data(), salt.size()) <= 0 ||
-      EVP_PKEY_CTX_set1_hkdf_key(pctx.get(),
-                                 reinterpret_cast<const uint8_t *>(ikm.data()),
-                                 ikm.size()) <= 0 ||
-      EVP_PKEY_CTX_add1_hkdf_info(pctx.get(), info.data(), info.size()) <= 0) {
+      EVP_PKEY_CTX_set1_hkdf_salt(pctx.get(), salt_ptr, salt.size()) <= 0 ||
+      EVP_PKEY_CTX_set1_hkdf_key(pctx.get(), ikm_ptr, ikm.size()) <= 0 ||
+      EVP_PKEY_CTX_add1_hkdf_info(pctx.get(), info_ptr, info.size()) <= 0) {
     return util::Status(absl::StatusCode::kInternal,
                         "EVP_PKEY_CTX setup failed");
   }
diff --git a/cc/subtle/hkdf_test.cc b/cc/subtle/hkdf_test.cc
index 77d272c..1cf665c 100644
--- a/cc/subtle/hkdf_test.cc
+++ b/cc/subtle/hkdf_test.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/hkdf.h"
 
 #include <string>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
diff --git a/cc/subtle/hmac_boringssl.cc b/cc/subtle/hmac_boringssl.cc
index 9cd609b..da2bed3 100644
--- a/cc/subtle/hmac_boringssl.cc
+++ b/cc/subtle/hmac_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/hmac_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/hmac_boringssl_test.cc b/cc/subtle/hmac_boringssl_test.cc
index 79a45d4..5e4b905 100644
--- a/cc/subtle/hmac_boringssl_test.cc
+++ b/cc/subtle/hmac_boringssl_test.cc
@@ -22,7 +22,7 @@
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "absl/strings/escaping.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/mac.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/util/secret_data.h"
@@ -57,7 +57,7 @@
 };
 
 TEST_F(HmacBoringSslTest, testBasic) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -71,29 +71,29 @@
   { // Test with some example data.
     std::string data = "Some data to test.";
     auto res = hmac->ComputeMac(data);
-    EXPECT_TRUE(res.ok()) << res.status().ToString();
+    EXPECT_TRUE(res.ok()) << res.status();
     std::string tag = res.value();
     EXPECT_EQ(tag_size, tag.size());
     EXPECT_EQ(tag, absl::HexStringToBytes("9ccdca5b7fffb690df396e4ac49b9cd4"));
     auto status = hmac->VerifyMac(tag, data);
-    EXPECT_TRUE(status.ok()) << "tag:" << absl::BytesToHexString(tag)
-                             << " status:" << status.ToString();
+    EXPECT_TRUE(status.ok())
+        << "tag:" << absl::BytesToHexString(tag) << " status:" << status;
   }
   { // Test with empty example data.
     absl::string_view data;
     auto res = hmac->ComputeMac(data);
-    EXPECT_TRUE(res.ok()) << res.status().ToString();
+    EXPECT_TRUE(res.ok()) << res.status();
     std::string tag = res.value();
     EXPECT_EQ(tag_size, tag.size());
     EXPECT_EQ(tag, absl::HexStringToBytes("5433122f77bcf8a4d9b874b4149823ef"));
     auto status = hmac->VerifyMac(tag, data);
-    EXPECT_TRUE(status.ok()) << "tag:" << absl::BytesToHexString(tag)
-                             << " status:" << status.ToString();
+    EXPECT_TRUE(status.ok())
+        << "tag:" << absl::BytesToHexString(tag) << " status:" << status;
   }
 }
 
 TEST_F(HmacBoringSslTest, testModification) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -106,7 +106,7 @@
   std::string data = "Some data to test";
   std::string tag = hmac->ComputeMac(data).value();
   auto status = hmac->VerifyMac(tag, data);
-  EXPECT_TRUE(status.ok()) << status.ToString();
+  EXPECT_TRUE(status.ok()) << status;
   size_t bits = tag.size() * 8;
   for (size_t i = 0; i < bits; i++) {
     std::string modified_tag = tag;
@@ -118,7 +118,7 @@
 }
 
 TEST_F(HmacBoringSslTest, testTruncation) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -131,7 +131,7 @@
   std::string data = "Some data to test";
   std::string tag = hmac->ComputeMac(data).value();
   auto status = hmac->VerifyMac(tag, data);
-  EXPECT_TRUE(status.ok()) << status.ToString();
+  EXPECT_TRUE(status.ok()) << status;
   for (size_t i = 0; i < tag.size(); i++) {
     std::string modified_tag(tag, 0, i);
     EXPECT_FALSE(hmac->VerifyMac(modified_tag, data).ok())
@@ -141,7 +141,7 @@
 }
 
 TEST_F(HmacBoringSslTest, testInvalidKeySizes) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
   }
@@ -160,7 +160,7 @@
 }
 
 TEST_F(HmacBoringSslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
diff --git a/cc/subtle/ind_cpa_cipher.h b/cc/subtle/ind_cpa_cipher.h
index 6a6fc77..373f6b8 100644
--- a/cc/subtle/ind_cpa_cipher.h
+++ b/cc/subtle/ind_cpa_cipher.h
@@ -42,7 +42,7 @@
   virtual crypto::tink::util::StatusOr<std::string> Decrypt(
       absl::string_view ciphertext) const = 0;
 
-  virtual ~IndCpaCipher() {}
+  virtual ~IndCpaCipher() = default;
 };
 
 }  // namespace subtle
diff --git a/cc/subtle/mac/stateful_mac.h b/cc/subtle/mac/stateful_mac.h
index e150a3a..d0a9b4e 100644
--- a/cc/subtle/mac/stateful_mac.h
+++ b/cc/subtle/mac/stateful_mac.h
@@ -27,6 +27,7 @@
 #ifndef TINK_SUBTLE_MAC_STATEFUL_MAC_H_
 #define TINK_SUBTLE_MAC_STATEFUL_MAC_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
@@ -39,8 +40,8 @@
 
 class StatefulMac {
  public:
-  StatefulMac() {}
-  virtual ~StatefulMac() {}
+  StatefulMac() = default;
+  virtual ~StatefulMac() = default;
 
   virtual util::Status Update(absl::string_view data) = 0;
   virtual util::StatusOr<std::string> Finalize() = 0;
@@ -48,7 +49,7 @@
 
 class StatefulMacFactory {
  public:
-  virtual ~StatefulMacFactory() {}
+  virtual ~StatefulMacFactory() = default;
 
   virtual util::StatusOr<std::unique_ptr<StatefulMac>> Create() const = 0;
 };
diff --git a/cc/subtle/nonce_based_streaming_aead.cc b/cc/subtle/nonce_based_streaming_aead.cc
index 15e9768..4a346d3 100644
--- a/cc/subtle/nonce_based_streaming_aead.cc
+++ b/cc/subtle/nonce_based_streaming_aead.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/nonce_based_streaming_aead.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/subtle/pem_parser_boringssl.cc b/cc/subtle/pem_parser_boringssl.cc
index bebc04e..202e7c1 100644
--- a/cc/subtle/pem_parser_boringssl.cc
+++ b/cc/subtle/pem_parser_boringssl.cc
@@ -358,7 +358,7 @@
                         "PEM Public Key parsing failed");
   }
   // No need to free bssl_ecdsa_key after use.
-  EC_KEY* bssl_ecdsa_key = EVP_PKEY_get0_EC_KEY(evp_ecdsa_key.get());
+  const EC_KEY* bssl_ecdsa_key = EVP_PKEY_get0_EC_KEY(evp_ecdsa_key.get());
   auto is_valid = VerifyEcdsaKey(bssl_ecdsa_key);
   if (!is_valid.ok()) {
     return is_valid;
@@ -413,7 +413,7 @@
   }
 
   // No need to free bssl_ecdsa_key after use.
-  EC_KEY* bssl_ecdsa_key = EVP_PKEY_get0_EC_KEY(evp_ecdsa_key.get());
+  const EC_KEY* bssl_ecdsa_key = EVP_PKEY_get0_EC_KEY(evp_ecdsa_key.get());
   util::Status verify_result = VerifyEcdsaKey(bssl_ecdsa_key);
   if (!verify_result.ok()) {
     return verify_result;
diff --git a/cc/subtle/prf/hkdf_streaming_prf.cc b/cc/subtle/prf/hkdf_streaming_prf.cc
index 63410f7..e39e745 100644
--- a/cc/subtle/prf/hkdf_streaming_prf.cc
+++ b/cc/subtle/prf/hkdf_streaming_prf.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/prf/hkdf_streaming_prf.h"
 
 #include <algorithm>
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -90,8 +91,8 @@
     }
     ti_.resize(digest_size);
 
-    // BoringSSL's `HDKF_extract` function is implemented as an HMAC [1]. We
-    // replace calls to `HDKF_extract` with a direct call to `HMAC` to make this
+    // BoringSSL's `HKDF_extract` function is implemented as an HMAC [1]. We
+    // replace calls to `HKDF_extract` with a direct call to `HMAC` to make this
     // compatible to OpenSSL, which doesn't expose `HKDF*` functions.
     //
     // [1] https://github.com/google/boringssl/blob/master/crypto/hkdf/hkdf.c#L42
diff --git a/cc/subtle/prf/hkdf_streaming_prf_test.cc b/cc/subtle/prf/hkdf_streaming_prf_test.cc
index 867bd20..fb2e45f 100644
--- a/cc/subtle/prf/hkdf_streaming_prf_test.cc
+++ b/cc/subtle/prf/hkdf_streaming_prf_test.cc
@@ -16,6 +16,9 @@
 
 #include "tink/subtle/prf/hkdf_streaming_prf.h"
 
+#include <algorithm>
+#include <iterator>
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -23,6 +26,7 @@
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "tink/config/tink_fips.h"
+#include "tink/subtle/common_enums.h"
 #include "tink/subtle/hkdf.h"
 #include "tink/subtle/random.h"
 #include "tink/util/input_stream_util.h"
@@ -65,6 +69,43 @@
   EXPECT_THAT(result_or.value(), SizeIs(10));
 }
 
+TEST(HkdfStreamingPrf, EmptySalt) {
+  if (IsFipsModeEnabled()) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
+  crypto::tink::subtle::HashType hash_type = SHA512;
+  const int hash_length = 64;
+  util::SecretData secret = util::SecretDataFromStringView("key0123456");
+  absl::string_view input = "input";
+  int num_bytes = 10;
+
+  std::string prf_empty_salt;
+  std::string prf_zeroed_salt;
+  {
+    auto streaming_prf = HkdfStreamingPrf::New(hash_type, secret, "");
+    ASSERT_THAT(streaming_prf, IsOk());
+    std::unique_ptr<InputStream> stream = (*streaming_prf)->ComputePrf(input);
+    auto result = ReadBytesFromStream(num_bytes, stream.get());
+    ASSERT_THAT(result, IsOk());
+    prf_empty_salt = *result;
+  }
+  {
+    uint8_t zeroedSalt[hash_length];
+    std::fill(std::begin(zeroedSalt), std::end(zeroedSalt), 0);
+    auto streaming_prf = HkdfStreamingPrf::New(hash_type, secret,
+                                               std::string((char*)zeroedSalt));
+    ASSERT_THAT(streaming_prf, IsOk());
+    std::unique_ptr<InputStream> stream = (*streaming_prf)->ComputePrf(input);
+    auto result = ReadBytesFromStream(num_bytes, stream.get());
+    ASSERT_THAT(result, IsOk());
+    prf_zeroed_salt = *result;
+  }
+  EXPECT_THAT(prf_empty_salt, SizeIs(num_bytes));
+  EXPECT_THAT(prf_zeroed_salt, SizeIs(num_bytes));
+  ASSERT_EQ(prf_empty_salt, prf_zeroed_salt);
+}
+
 TEST(HkdfStreamingPrf, DifferentInputsGiveDifferentvalues) {
   if (IsFipsModeEnabled()) {
     GTEST_SKIP() << "Not supported in FIPS-only mode";
@@ -528,12 +569,11 @@
   std::string salt = Random::GetRandomBytes(234);
   std::string info = Random::GetRandomBytes(345);
 
-  auto streaming_result_or = ComputeWithHkdfStreamingPrf(
-      hash, ikm, salt, info, 456);
+  auto streaming_result_or =
+      ComputeWithHkdfStreamingPrf(hash, ikm, salt, info, 456);
   ASSERT_THAT(streaming_result_or, IsOk());
 
-  auto compute_hkdf_result_or =  Hkdf::ComputeHkdf(
-      hash, ikm, salt, info, 456);
+  auto compute_hkdf_result_or = Hkdf::ComputeHkdf(hash, ikm, salt, info, 456);
   ASSERT_THAT(compute_hkdf_result_or, IsOk());
 
   util::SecretData compute_hkdf_result =
diff --git a/cc/subtle/prf/streaming_prf.h b/cc/subtle/prf/streaming_prf.h
index 7ba055d..76eeff8 100644
--- a/cc/subtle/prf/streaming_prf.h
+++ b/cc/subtle/prf/streaming_prf.h
@@ -38,7 +38,7 @@
   virtual std::unique_ptr<InputStream> ComputePrf(
       absl::string_view input) const = 0;
 
-  virtual ~StreamingPrf() {}
+  virtual ~StreamingPrf() = default;
 };
 
 }  // namespace tink
diff --git a/cc/subtle/prf/streaming_prf_wrapper.cc b/cc/subtle/prf/streaming_prf_wrapper.cc
index 85e4a93..fafc370 100644
--- a/cc/subtle/prf/streaming_prf_wrapper.cc
+++ b/cc/subtle/prf/streaming_prf_wrapper.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/prf/streaming_prf_wrapper.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/status/status.h"
@@ -36,7 +37,7 @@
     return streaming_prf_set_->get_primary()->get_primitive().ComputePrf(input);
   }
 
-  ~StreamingPrfSetWrapper() override {}
+  ~StreamingPrfSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<StreamingPrf>> streaming_prf_set_;
diff --git a/cc/subtle/prf/streaming_prf_wrapper.h b/cc/subtle/prf/streaming_prf_wrapper.h
index 5d8f0c8..82d5dc4 100644
--- a/cc/subtle/prf/streaming_prf_wrapper.h
+++ b/cc/subtle/prf/streaming_prf_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_SUBTLE_PRF_STREAMING_PRF_WRAPPER_H_
 #define TINK_SUBTLE_PRF_STREAMING_PRF_WRAPPER_H_
 
+#include <memory>
+
 #include "tink/primitive_set.h"
 #include "tink/primitive_wrapper.h"
 #include "tink/subtle/prf/streaming_prf.h"
diff --git a/cc/subtle/prf/streaming_prf_wrapper_test.cc b/cc/subtle/prf/streaming_prf_wrapper_test.cc
index fa8a90c..eb5ee6c 100644
--- a/cc/subtle/prf/streaming_prf_wrapper_test.cc
+++ b/cc/subtle/prf/streaming_prf_wrapper_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/subtle/prf/streaming_prf_wrapper.h"
 
+#include <memory>
+#include <sstream>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc
index 4cbc58f..ec1d9bf 100644
--- a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/rsa_ssa_pkcs1_sign_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc
index dab0d7d..e1ae90b 100644
--- a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc
@@ -25,7 +25,7 @@
 #include "openssl/bn.h"
 #include "openssl/crypto.h"
 #include "openssl/rsa.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/rsa_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/rsa_ssa_pkcs1_verify_boringssl.h"
@@ -58,7 +58,7 @@
 };
 
 TEST_F(RsaPkcs1SignBoringsslTest, EncodesPkcs1) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -78,7 +78,7 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, EncodesPkcs1WithSeparateHashes) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -98,7 +98,7 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, RejectsUnsafeHash) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -108,7 +108,7 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, RejectsInvalidCrtParams) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -143,7 +143,7 @@
 
 // FIPS-only mode test
 TEST_F(RsaPkcs1SignBoringsslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -154,7 +154,7 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, TestRestrictedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
 
@@ -171,7 +171,7 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, TestAllowedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
 
diff --git a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc
index 7d8ee33..8164591 100644
--- a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/rsa_ssa_pkcs1_verify_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc
index b4d8d82..a5e560a 100644
--- a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/rsa_ssa_pkcs1_verify_boringssl.h"
 
 #include <iostream>
+#include <memory>
 #include <string>
 #include <utility>
 
@@ -26,8 +27,8 @@
 #include "absl/strings/str_cat.h"
 #include "openssl/bn.h"
 #include "include/rapidjson/document.h"
-#include "tink/config/tink_fips.h"
 #include "tink/internal/err_util.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/rsa_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/public_key_sign.h"
@@ -86,7 +87,7 @@
     HashType::SHA256};
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, BasicVerify) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -102,7 +103,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, NewErrors) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -132,7 +133,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, Modification) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -257,7 +258,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs12048SHA256) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
   ASSERT_TRUE(TestSignatures("rsa_signature_2048_sha256_test.json",
@@ -265,7 +266,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs13072SHA256) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -274,7 +275,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs13072SHA512) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (internal::IsFipsModeEnabled() && !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -283,7 +284,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs14096SHA512) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
   ASSERT_TRUE(TestSignatures("rsa_signature_4096_sha512_test.json",
@@ -292,7 +293,7 @@
 
 // FIPS-only mode test
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -304,7 +305,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, TestAllowedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
 
@@ -323,7 +324,7 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, TestRestrictedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
 
diff --git a/cc/subtle/rsa_ssa_pss_sign_boringssl.cc b/cc/subtle/rsa_ssa_pss_sign_boringssl.cc
index cd873b2..43736a2 100644
--- a/cc/subtle/rsa_ssa_pss_sign_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pss_sign_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/rsa_ssa_pss_sign_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc b/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc
index dddc52c..1b6feee 100644
--- a/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc
@@ -22,7 +22,7 @@
 #include "absl/strings/escaping.h"
 #include "openssl/bn.h"
 #include "openssl/rsa.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/rsa_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/rsa_ssa_pss_verify_boringssl.h"
@@ -55,7 +55,7 @@
 };
 
 TEST_F(RsaPssSignBoringsslTest, EncodesPss) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -77,7 +77,7 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, EncodesPssWithSeparateHashes) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -99,7 +99,7 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, RejectsInvalidPaddingHash) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -111,7 +111,7 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, RejectsUnsafePaddingHash) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -123,7 +123,7 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, RejectsInvalidCrtParams) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
 
@@ -160,7 +160,7 @@
 
 // FIPS-only mode test
 TEST_F(RsaPssSignBoringsslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -173,7 +173,7 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, TestRestrictedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
   internal::RsaPrivateKey private_key;
@@ -191,7 +191,7 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, TestAllowedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
   internal::RsaPrivateKey private_key;
diff --git a/cc/subtle/rsa_ssa_pss_verify_boringssl.cc b/cc/subtle/rsa_ssa_pss_verify_boringssl.cc
index f5e5b76..e1dbe02 100644
--- a/cc/subtle/rsa_ssa_pss_verify_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pss_verify_boringssl.cc
@@ -17,8 +17,10 @@
 #include "tink/subtle/rsa_ssa_pss_verify_boringssl.h"
 
 #include <cstdint>
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
diff --git a/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc b/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc
index f654bda..20c8921 100644
--- a/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc
@@ -19,6 +19,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
@@ -28,8 +29,8 @@
 #include "absl/strings/string_view.h"
 #include "openssl/bn.h"
 #include "include/rapidjson/document.h"
-#include "tink/config/tink_fips.h"
 #include "tink/internal/err_util.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/rsa_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/public_key_sign.h"
@@ -64,7 +65,7 @@
   int salt_length;
 };
 
-const NistTestVector GetNistTestVector() {
+NistTestVector GetNistTestVector() {
   NistTestVector test_vector = {
       absl::HexStringToBytes(
           "a47d04e7cacdba4ea26eca8a4c6e14563c2ce03b623b768c0d49868a57121301dbf7"
@@ -98,7 +99,7 @@
 }
 
 TEST(RsaSsaPssVerifyBoringSslTest, BasicVerify) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
   const NistTestVector kNistTestVector = GetNistTestVector();
@@ -118,7 +119,7 @@
 }
 
 TEST(RsaSsaPssVerifyBoringSslTest, NewErrors) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
   const NistTestVector kNistTestVector = GetNistTestVector();
@@ -152,7 +153,7 @@
 }
 
 TEST(RsaSsaPssVerifyBoringSslTest, Modification) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
   const NistTestVector kNistTestVector = GetNistTestVector();
@@ -252,7 +253,7 @@
 
 // Tests signature verification using a test vector.
 TEST_P(RsaSsaPssWycheproofTest, SignatureVerify) {
-  if (IsFipsModeEnabled()) {
+  if (internal::IsFipsModeEnabled()) {
     GTEST_SKIP() << "Test not run in FIPS-only mode";
   }
   RsaSsaPssWycheproofTestVector params = GetParam();
@@ -304,7 +305,7 @@
 
 // FIPS-only mode test
 TEST(RsaSsaPssVerifyBoringSslTest, TestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
@@ -320,7 +321,7 @@
 }
 
 TEST(RsaSsaPssVerifyBoringSslTest, TestAllowedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
 
@@ -341,7 +342,7 @@
 }
 
 TEST(RsaSsaPssVerifyBoringSslTest, TestRestrictedFipsModuli) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!internal::IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
   }
 
diff --git a/cc/subtle/stateful_cmac_boringssl.cc b/cc/subtle/stateful_cmac_boringssl.cc
index 951e126..09279c4 100644
--- a/cc/subtle/stateful_cmac_boringssl.cc
+++ b/cc/subtle/stateful_cmac_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/stateful_cmac_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/stateful_cmac_boringssl_test.cc b/cc/subtle/stateful_cmac_boringssl_test.cc
index f85d017..946676f 100644
--- a/cc/subtle/stateful_cmac_boringssl_test.cc
+++ b/cc/subtle/stateful_cmac_boringssl_test.cc
@@ -19,6 +19,7 @@
 #include <cstddef>
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
diff --git a/cc/subtle/stateful_hmac_boringssl.cc b/cc/subtle/stateful_hmac_boringssl.cc
index 631b630..16c3e54 100644
--- a/cc/subtle/stateful_hmac_boringssl.cc
+++ b/cc/subtle/stateful_hmac_boringssl.cc
@@ -16,6 +16,7 @@
 
 #include "tink/subtle/stateful_hmac_boringssl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/stream_segment_decrypter.h b/cc/subtle/stream_segment_decrypter.h
index 55425e3..6604125 100644
--- a/cc/subtle/stream_segment_decrypter.h
+++ b/cc/subtle/stream_segment_decrypter.h
@@ -72,7 +72,7 @@
   //   segment_overhead = ciphertext_segment_size - get_plaintext_segment_size()
   virtual int get_ciphertext_offset() const = 0;
 
-  virtual ~StreamSegmentDecrypter() {}
+  virtual ~StreamSegmentDecrypter() = default;
 };
 
 }  // namespace subtle
diff --git a/cc/subtle/stream_segment_encrypter.h b/cc/subtle/stream_segment_encrypter.h
index 93f6952..ff8aeba 100644
--- a/cc/subtle/stream_segment_encrypter.h
+++ b/cc/subtle/stream_segment_encrypter.h
@@ -98,7 +98,7 @@
   //   segment_overhead = ciphertext_segment_size - get_plaintext_segment_size()
   virtual int get_ciphertext_offset() const = 0;
 
-  virtual ~StreamSegmentEncrypter() {}
+  virtual ~StreamSegmentEncrypter() = default;
 
  protected:
   // Increments the segment number.
diff --git a/cc/subtle/streaming_aead_decrypting_stream.cc b/cc/subtle/streaming_aead_decrypting_stream.cc
index 01c26b8..c46293b 100644
--- a/cc/subtle/streaming_aead_decrypting_stream.cc
+++ b/cc/subtle/streaming_aead_decrypting_stream.cc
@@ -18,7 +18,9 @@
 
 #include <algorithm>
 #include <cstring>
+#include <memory>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
diff --git a/cc/subtle/streaming_aead_decrypting_stream_test.cc b/cc/subtle/streaming_aead_decrypting_stream_test.cc
index f8579f7..9569caa 100644
--- a/cc/subtle/streaming_aead_decrypting_stream_test.cc
+++ b/cc/subtle/streaming_aead_decrypting_stream_test.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/streaming_aead_decrypting_stream.h"
 
 #include <algorithm>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
diff --git a/cc/subtle/streaming_aead_encrypting_stream.cc b/cc/subtle/streaming_aead_encrypting_stream.cc
index 5fb4741..419bc06 100644
--- a/cc/subtle/streaming_aead_encrypting_stream.cc
+++ b/cc/subtle/streaming_aead_encrypting_stream.cc
@@ -18,7 +18,9 @@
 
 #include <algorithm>
 #include <cstring>
+#include <memory>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
diff --git a/cc/subtle/streaming_aead_encrypting_stream_test.cc b/cc/subtle/streaming_aead_encrypting_stream_test.cc
index aa2a5e8..cba162f 100644
--- a/cc/subtle/streaming_aead_encrypting_stream_test.cc
+++ b/cc/subtle/streaming_aead_encrypting_stream_test.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/streaming_aead_encrypting_stream.h"
 
 #include <algorithm>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
diff --git a/cc/subtle/streaming_aead_test_util.cc b/cc/subtle/streaming_aead_test_util.cc
index 9c21c00..bc4019c 100644
--- a/cc/subtle/streaming_aead_test_util.cc
+++ b/cc/subtle/streaming_aead_test_util.cc
@@ -16,39 +16,30 @@
 #include "tink/subtle/streaming_aead_test_util.h"
 
 #include <algorithm>
+#include <cstring>
+#include <memory>
 #include <sstream>
 #include <string>
 #include <utility>
 
+#include "tink/internal/test_random_access_stream.h"
 #include "tink/random_access_stream.h"
 #include "tink/subtle/test_util.h"
 #include "tink/util/buffer.h"
-#include "tink/util/file_random_access_stream.h"
 #include "tink/util/istream_input_stream.h"
 #include "tink/util/ostream_output_stream.h"
 #include "tink/util/status.h"
-#include "tink/util/test_util.h"
 
 namespace crypto {
 namespace tink {
 
-using ::crypto::tink::test::GetTestFileDescriptor;
+using ::crypto::tink::internal::TestRandomAccessStream;
 using ::crypto::tink::util::IstreamInputStream;
 using ::crypto::tink::util::OstreamOutputStream;
 using ::crypto::tink::util::Status;
 
 namespace {
 
-// Creates a RandomAccessStream with the specified contents.
-std::unique_ptr<RandomAccessStream> GetRandomAccessStreamContaining(
-    absl::string_view contents) {
-  static int index = 1;
-  std::string filename = absl::StrCat("stream_data_file_", index, ".txt");
-  index++;
-  int input_fd = GetTestFileDescriptor(filename, contents);
-  return {absl::make_unique<util::FileRandomAccessStream>(input_fd)};
-}
-
 // Reads up to 'count' bytes from 'ras' starting at position 'pos'
 // and verifies that the read bytes are equal to the corresponding
 // subsequence in 'full_contents'.
@@ -136,7 +127,8 @@
   }
 
   // Prepare a RandomAccessStream with the ciphertext.
-  auto ct_ras = GetRandomAccessStreamContaining(std::string(ct_buf->str()));
+  auto ct_ras =
+      std::make_unique<TestRandomAccessStream>(std::string(ct_buf->str()));
 
   // Decrypt fragments of the ciphertext using the decrypter.
   auto dec_ras_result = decrypter->NewDecryptingRandomAccessStream(
diff --git a/cc/subtle/streaming_mac_impl.cc b/cc/subtle/streaming_mac_impl.cc
index 029c7ec..f200e69 100644
--- a/cc/subtle/streaming_mac_impl.cc
+++ b/cc/subtle/streaming_mac_impl.cc
@@ -17,6 +17,7 @@
 #include "tink/subtle/streaming_mac_impl.h"
 
 #include <algorithm>
+#include <memory>
 #include <string>
 #include <utility>
 
diff --git a/cc/subtle/streaming_mac_impl_test.cc b/cc/subtle/streaming_mac_impl_test.cc
index 0672f12..d7b8f04 100644
--- a/cc/subtle/streaming_mac_impl_test.cc
+++ b/cc/subtle/streaming_mac_impl_test.cc
@@ -16,8 +16,10 @@
 
 #include "tink/subtle/streaming_mac_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
@@ -40,8 +42,8 @@
 
 class DummyStatefulMacFactory : public StatefulMacFactory {
  public:
-  DummyStatefulMacFactory() {}
-  ~DummyStatefulMacFactory() override {}
+  DummyStatefulMacFactory() = default;
+  ~DummyStatefulMacFactory() override = default;
 
   // Constructs a StatefulMac using the DummyStatefulMac, which creates
   // returns a MAC of the header concatenated with the plaintext.
diff --git a/cc/subtle/test_util.h b/cc/subtle/test_util.h
index 962bec5..9403b1a 100644
--- a/cc/subtle/test_util.h
+++ b/cc/subtle/test_util.h
@@ -138,7 +138,7 @@
     return ct_offset_;
   }
 
-  ~DummyStreamSegmentEncrypter() override {}
+  ~DummyStreamSegmentEncrypter() override = default;
 
   int get_generated_output_size() {
     return generated_output_size_;
@@ -227,7 +227,7 @@
     return ct_offset_;
   }
 
-  ~DummyStreamSegmentDecrypter() override {}
+  ~DummyStreamSegmentDecrypter() override = default;
 
   int get_generated_output_size() {
     return generated_output_size_;
diff --git a/cc/subtle/wycheproof_util.cc b/cc/subtle/wycheproof_util.cc
index a69a147..6289057 100644
--- a/cc/subtle/wycheproof_util.cc
+++ b/cc/subtle/wycheproof_util.cc
@@ -19,6 +19,7 @@
 #include <fstream>
 #include <iostream>
 #include <memory>
+#include <ostream>
 #include <string>
 
 #include "absl/status/status.h"
@@ -79,8 +80,7 @@
 std::unique_ptr<rapidjson::Document> WycheproofUtil::ReadTestVectors(
     const std::string &filename) {
   std::string test_vectors_path = crypto::tink::internal::RunfilesPath(
-      absl::StrCat(
-          "external/wycheproof/testvectors/", filename));
+      absl::StrCat("testvectors/", filename));
   std::ifstream input_stream;
   input_stream.open(test_vectors_path);
   rapidjson::IStreamWrapper input(input_stream);
diff --git a/cc/testvectors/BUILD.bazel b/cc/testvectors/BUILD.bazel
new file mode 100644
index 0000000..b602435
--- /dev/null
+++ b/cc/testvectors/BUILD.bazel
@@ -0,0 +1,200 @@
+"""Defines a set of genrules to copy test vectors from Wycheproof.
+
+This is needed to assist the transition to using Bazel Modules, in that Bazel
+Modules packages use a different folder naming for dependencies compared to
+WORKSPACE-based packages.
+"""
+
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+genrule(
+    name = "aes_cmac",
+    srcs = ["@wycheproof//testvectors:aes_cmac"],
+    outs = ["aes_cmac_test.json"],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "aes_gcm",
+    srcs = ["@wycheproof//testvectors:aes_gcm"],
+    outs = ["aes_gcm_test.json"],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "aes_gcm_siv",
+    srcs = ["@wycheproof//testvectors:aes_gcm_siv"],
+    outs = ["aes_gcm_siv_test.json"],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "aes_eax",
+    srcs = ["@wycheproof//testvectors:aes_eax"],
+    outs = ["aes_eax_test.json"],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "aes_siv_cmac",
+    srcs = ["@wycheproof//testvectors:aes_siv_cmac"],
+    outs = [
+        "aead_aes_siv_cmac_test.json",
+        "aes_siv_cmac_test.json",
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "chacha20_poly1305",
+    srcs = ["@wycheproof//testvectors:chacha20_poly1305"],
+    outs = [
+        "chacha20_poly1305_test.json",
+        "xchacha20_poly1305_test.json",
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "hmac",
+    srcs = ["@wycheproof//testvectors:hmac"],
+    outs = [
+        "hmac_sha1_test.json",
+        "hmac_sha224_test.json",
+        "hmac_sha256_test.json",
+        "hmac_sha384_test.json",
+        "hmac_sha3_224_test.json",
+        "hmac_sha3_256_test.json",
+        "hmac_sha3_384_test.json",
+        "hmac_sha3_512_test.json",
+        "hmac_sha512_test.json",
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "rsa_pss",
+    srcs = ["@wycheproof//testvectors:rsa_pss"],
+    outs = [
+        "rsa_pss_2048_sha1_mgf1_20_test.json",
+        "rsa_pss_2048_sha256_mgf1_0_test.json",
+        "rsa_pss_2048_sha256_mgf1_32_test.json",
+        "rsa_pss_3072_sha256_mgf1_32_test.json",
+        "rsa_pss_4096_sha256_mgf1_32_test.json",
+        "rsa_pss_4096_sha512_mgf1_32_test.json",
+        "rsa_pss_misc_test.json",
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "rsa_signature",
+    srcs = ["@wycheproof//testvectors:rsa_signature"],
+    outs = [
+        # Signature verification
+        "rsa_signature_2048_sha224_test.json",
+        "rsa_signature_2048_sha256_test.json",
+        "rsa_signature_2048_sha512_test.json",
+        "rsa_signature_3072_sha256_test.json",
+        "rsa_signature_3072_sha384_test.json",
+        "rsa_signature_3072_sha512_test.json",
+        "rsa_signature_4096_sha384_test.json",
+        "rsa_signature_4096_sha512_test.json",
+        "rsa_signature_2048_sha3_224_test.json",
+        "rsa_signature_2048_sha3_256_test.json",
+        "rsa_signature_2048_sha3_384_test.json",
+        "rsa_signature_2048_sha3_512_test.json",
+        "rsa_signature_3072_sha3_256_test.json",
+        "rsa_signature_3072_sha3_384_test.json",
+        "rsa_signature_3072_sha3_512_test.json",
+        "rsa_signature_test.json",
+        # Signature generation
+        "rsa_sig_gen_misc_test.json",
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "ecdsa_webcrypto",
+    srcs = ["@wycheproof//testvectors:ecdsa_webcrypto"],
+    outs = ["ecdsa_webcrypto_test.json"],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "ecdsa",
+    srcs = ["@wycheproof//testvectors:ecdsa"],
+    outs = [
+        "ecdsa_brainpoolP224r1_sha224_test.json",
+        "ecdsa_brainpoolP256r1_sha256_test.json",
+        "ecdsa_brainpoolP320r1_sha384_test.json",
+        "ecdsa_brainpoolP384r1_sha384_test.json",
+        "ecdsa_brainpoolP512r1_sha512_test.json",
+        "ecdsa_secp224r1_sha224_test.json",
+        "ecdsa_secp224r1_sha256_test.json",
+        "ecdsa_secp224r1_sha3_224_test.json",
+        "ecdsa_secp224r1_sha3_256_test.json",
+        "ecdsa_secp224r1_sha3_512_test.json",
+        "ecdsa_secp224r1_sha512_test.json",
+        "ecdsa_secp256k1_sha256_test.json",
+        "ecdsa_secp256k1_sha3_256_test.json",
+        "ecdsa_secp256k1_sha3_512_test.json",
+        "ecdsa_secp256k1_sha512_test.json",
+        "ecdsa_secp256r1_sha256_test.json",
+        "ecdsa_secp256r1_sha3_256_test.json",
+        "ecdsa_secp256r1_sha3_512_test.json",
+        "ecdsa_secp256r1_sha512_test.json",
+        "ecdsa_secp384r1_sha384_test.json",
+        "ecdsa_secp384r1_sha3_384_test.json",
+        "ecdsa_secp384r1_sha3_512_test.json",
+        "ecdsa_secp384r1_sha512_test.json",
+        "ecdsa_secp521r1_sha3_512_test.json",
+        "ecdsa_secp521r1_sha512_test.json",
+        "ecdsa_test.json",  # deprecated: use the files above
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "eddsa",
+    srcs = ["@wycheproof//testvectors:eddsa"],
+    outs = [
+        "ed448_test.json",
+        "eddsa_test.json",
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
+
+genrule(
+    name = "ecdh",
+    srcs = ["@wycheproof//testvectors:ecdh"],
+    outs = [
+        "ecdh_brainpoolP224r1_test.json",
+        "ecdh_brainpoolP256r1_test.json",
+        "ecdh_brainpoolP320r1_test.json",
+        "ecdh_brainpoolP384r1_test.json",
+        "ecdh_brainpoolP512r1_test.json",
+        "ecdh_secp224r1_test.json",
+        "ecdh_secp256k1_test.json",
+        "ecdh_secp256r1_test.json",
+        "ecdh_secp384r1_test.json",
+        "ecdh_secp521r1_test.json",
+        "ecdh_test.json",  # deprecated use the files above
+    ],
+    cmd = "cp $(SRCS) $(@D)/",
+    testonly = 1,
+)
diff --git a/cc/tink_cc_deps.bzl b/cc/tink_cc_deps.bzl
index 9a24734..d1de209 100644
--- a/cc/tink_cc_deps.bzl
+++ b/cc/tink_cc_deps.bzl
@@ -7,14 +7,14 @@
 
     # Basic rules we need to add to bazel.
     if not native.existing_rule("bazel_skylib"):
-        # Release from 2021-09-27.
+        # Release from 2022-09-01: https://github.com/bazelbuild/bazel-skylib/releases/tag/1.3.0
         http_archive(
             name = "bazel_skylib",
             urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
-                "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+                "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz",
+                "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz",
             ],
-            sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
+            sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506",
         )
 
     # -------------------------------------------------------------------------
@@ -27,51 +27,36 @@
     #   * @com_google_protobuf//:java_toolchain
     # This statement defines the @com_google_protobuf repo.
     if not native.existing_rule("com_google_protobuf"):
-        # Release from 2021-06-08.
+        # Release X.21.9 from 2022-10-26.
         http_archive(
             name = "com_google_protobuf",
-            strip_prefix = "protobuf-3.19.3",
-            urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.3.zip"],
-            sha256 = "6b6bf5cd8d0cca442745c4c3c9f527c83ad6ef35a405f64db5215889ac779b42",
-        )
-
-    # -------------------------------------------------------------------------
-    # Remote Build Execution (RBE).
-    # -------------------------------------------------------------------------
-    if not native.existing_rule("bazel_toolchains"):
-        # Latest bazel_toolchains package on 2021-10-13.
-        http_archive(
-            name = "bazel_toolchains",
-            sha256 = "179ec02f809e86abf56356d8898c8bd74069f1bd7c56044050c2cd3d79d0e024",
-            strip_prefix = "bazel-toolchains-4.1.0",
-            urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-                "https://github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-            ],
+            strip_prefix = "protobuf-21.9",
+            urls = ["https://github.com/protocolbuffers/protobuf/archive/refs/tags/v21.9.zip"],
+            sha256 = "5babb8571f1cceafe0c18e13ddb3be556e87e12ceea3463d6b0d0064e6cc1ac3",
         )
 
     # -------------------------------------------------------------------------
     # Abseil.
     # -------------------------------------------------------------------------
     if not native.existing_rule("com_google_absl"):
-        # Commit from 2021-12-03.
+        # Release from 2023-05-04.
         http_archive(
             name = "com_google_absl",
-            strip_prefix = "abseil-cpp-9336be04a242237cd41a525bedfcf3be1bb55377",
-            url = "https://github.com/abseil/abseil-cpp/archive/9336be04a242237cd41a525bedfcf3be1bb55377.zip",
-            sha256 = "368be019fc8d69a566ac2cf7a75262d5ba8f6409e3ef3cdbcf0106bdeb32e91c",
+            strip_prefix = "abseil-cpp-20230125.3",
+            url = "https://github.com/abseil/abseil-cpp/archive/refs/tags/20230125.3.zip",
+            sha256 = "51d676b6846440210da48899e4df618a357e6e44ecde7106f1e44ea16ae8adc7",
         )
 
     # -------------------------------------------------------------------------
     # BoringSSL.
     # -------------------------------------------------------------------------
     if not native.existing_rule("boringssl"):
-        # Commit from 2022-02-25.
+        # Commit from 2023-02-15.
         http_archive(
             name = "boringssl",
-            strip_prefix = "boringssl-88cdf7dd2dbce1ecb9057c183095103d83373abe",
-            url = "https://github.com/google/boringssl/archive/88cdf7dd2dbce1ecb9057c183095103d83373abe.zip",
-            sha256 = "24092815136f956069fcfa5172166ad4e025166ce6fe500420c9e3e3c4f3da38",
+            strip_prefix = "boringssl-5c22014ca513807ed03c657e8ede076164663979",
+            url = "https://github.com/google/boringssl/archive/5c22014ca513807ed03c657e8ede076164663979.zip",
+            sha256 = "863fc670c456f30923740c1639305132fdfb9d1b25ba385a67ae3862ef12a8af",
         )
 
     # -------------------------------------------------------------------------
diff --git a/cc/util/BUILD.bazel b/cc/util/BUILD.bazel
index f57b17d..f92575e 100644
--- a/cc/util/BUILD.bazel
+++ b/cc/util/BUILD.bazel
@@ -28,8 +28,10 @@
     name = "secret_data_internal",
     hdrs = ["secret_data_internal.h"],
     include_prefix = "tink/util",
+    visibility = ["//visibility:private"],
     deps = [
         "@boringssl//:crypto",
+        "@com_google_absl//absl/base:config",
         "@com_google_absl//absl/base:core_headers",
     ],
 )
@@ -55,7 +57,7 @@
         ":status",
         ":statusor",
         "@com_google_absl//absl/memory",
-        "@com_google_protobuf//:protobuf_lite",
+        "@com_google_protobuf//:protobuf",
     ],
 )
 
@@ -76,6 +78,7 @@
     include_prefix = "tink/util",
     visibility = ["//visibility:public"],
     deps = [
+        ":status",
         ":statusor",
         "//proto:common_cc_proto",
         "//proto:ecdsa_cc_proto",
@@ -88,39 +91,15 @@
 
 cc_library(
     name = "status",
-    srcs = ["status.cc"],
     hdrs = ["status.h"],
-    defines = select({
-        "//config:absl_status_enabled": ["TINK_USE_ABSL_STATUS"],
-        "//conditions:default": [],
-    }),
     include_prefix = "tink/util",
     visibility = ["//visibility:public"],
-    deps = [
-        "@com_google_absl//absl/base:core_headers",
-        "@com_google_absl//absl/status",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
-cc_test(
-    name = "status_test",
-    srcs = ["status_test.cc"],
-    deps = [
-        ":status",
-        "@com_google_absl//absl/status",
-        "@com_google_googletest//:gtest_main",
-    ],
+    deps = ["@com_google_absl//absl/status"],
 )
 
 cc_library(
     name = "statusor",
-    srcs = ["statusor.h"],
     hdrs = ["statusor.h"],
-    defines = select({
-        "//config:absl_statusor_enabled": ["TINK_USE_ABSL_STATUSOR"],
-        "//conditions:default": [],
-    }),
     include_prefix = "tink/util",
     visibility = ["//visibility:public"],
     deps = [
@@ -129,20 +108,6 @@
     ],
 )
 
-cc_test(
-    name = "statusor_test",
-    srcs = ["statusor_test.cc"],
-    deps = [
-        ":status",
-        ":statusor",
-        ":test_matchers",
-        "@com_google_absl//absl/memory",
-        "@com_google_absl//absl/status",
-        "@com_google_absl//absl/status:statusor",
-        "@com_google_googletest//:gtest_main",
-    ],
-)
-
 cc_library(
     name = "validation",
     srcs = ["validation.cc"],
@@ -161,13 +126,16 @@
     srcs = ["file_input_stream.cc"],
     hdrs = ["file_input_stream.h"],
     include_prefix = "tink/util",
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
     visibility = ["//visibility:public"],
     deps = [
         ":errors",
         ":status",
         ":statusor",
         "//:input_stream",
-        "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
     ],
 )
@@ -177,6 +145,10 @@
     srcs = ["file_output_stream.cc"],
     hdrs = ["file_output_stream.h"],
     include_prefix = "tink/util",
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
     visibility = ["//visibility:public"],
     deps = [
         ":errors",
@@ -193,6 +165,10 @@
     srcs = ["file_random_access_stream.cc"],
     hdrs = ["file_random_access_stream.h"],
     include_prefix = "tink/util",
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
     visibility = ["//visibility:public"],
     deps = [
         ":buffer",
@@ -312,7 +288,7 @@
     name = "protobuf_helper",
     hdrs = ["protobuf_helper.h"],
     include_prefix = "tink/util",
-    deps = ["@com_google_protobuf//:protobuf_lite"],
+    deps = ["@com_google_protobuf//:protobuf"],
 )
 
 cc_library(
@@ -446,10 +422,19 @@
 cc_test(
     name = "file_input_stream_test",
     srcs = ["file_input_stream_test.cc"],
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
     deps = [
         ":file_input_stream",
+        ":status",
+        ":test_matchers",
         ":test_util",
+        "//internal:test_file_util",
+        "//subtle:random",
         "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -458,9 +443,15 @@
 cc_test(
     name = "file_output_stream_test",
     srcs = ["file_output_stream_test.cc"],
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
     deps = [
         ":file_output_stream",
+        ":test_matchers",
         ":test_util",
+        "//internal:test_file_util",
         "//subtle:random",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/strings",
@@ -471,11 +462,19 @@
 cc_test(
     name = "file_random_access_stream_test",
     srcs = ["file_random_access_stream_test.cc"],
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
     deps = [
         ":buffer",
         ":file_random_access_stream",
+        ":test_matchers",
         ":test_util",
+        "//internal:test_file_util",
+        "//subtle:random",
         "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -487,8 +486,11 @@
     deps = [
         ":istream_input_stream",
         ":test_util",
+        "//internal:test_file_util",
         "//subtle:random",
         "@com_google_absl//absl/memory",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -500,6 +502,7 @@
     deps = [
         ":ostream_output_stream",
         ":test_util",
+        "//internal:test_file_util",
         "//subtle:random",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/strings",
@@ -533,11 +536,19 @@
     name = "test_util_test",
     srcs = ["test_util_test.cc"],
     deps = [
+        ":buffer",
+        ":ostream_output_stream",
+        ":statusor",
         ":test_matchers",
         ":test_util",
+        "//:output_stream",
+        "//:random_access_stream",
+        "//internal:test_random_access_stream",
         "//proto:aes_gcm_cc_proto",
         "//proto:tink_cc_proto",
         "//subtle",
+        "//subtle:test_util",
+        "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
 )
diff --git a/cc/util/BUILD.gn b/cc/util/BUILD.gn
index 3e0cff4..5bedeca 100644
--- a/cc/util/BUILD.gn
+++ b/cc/util/BUILD.gn
@@ -23,6 +23,7 @@
   configs -= [ "//build/config:no_rtti" ]
   sources = [ "secret_data_internal.h" ]
   public_deps = [
+    "//third_party/abseil-cpp/absl/base:config",
     "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/boringssl:crypto",
   ]
@@ -62,6 +63,7 @@
     "enums.h",
   ]
   public_deps = [
+    ":status",
     ":statusor",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/abseil-cpp/absl/strings:strings",
@@ -77,15 +79,8 @@
 source_set("status") {
   configs += [ "//build/config:no_rtti" ]
   configs -= [ "//build/config:no_rtti" ]
-  sources = [
-    "status.cc",
-    "status.h",
-  ]
-  public_deps = [
-    "//third_party/abseil-cpp/absl/base:core_headers",
-    "//third_party/abseil-cpp/absl/status:status",
-    "//third_party/abseil-cpp/absl/strings:strings",
-  ]
+  sources = [ "status.h" ]
+  public_deps = [ "//third_party/abseil-cpp/absl/status:status" ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
 
@@ -93,10 +88,7 @@
 source_set("statusor") {
   configs += [ "//build/config:no_rtti" ]
   configs -= [ "//build/config:no_rtti" ]
-  sources = [
-    "statusor.h",
-    "statusor.h",
-  ]
+  sources = [ "statusor.h" ]
   public_deps = [
     ":status",
     "//third_party/abseil-cpp/absl/status:statusor",
diff --git a/cc/util/CMakeLists.txt b/cc/util/CMakeLists.txt
index 0836633..c906230 100644
--- a/cc/util/CMakeLists.txt
+++ b/cc/util/CMakeLists.txt
@@ -36,6 +36,7 @@
     enums.cc
     enums.h
   DEPS
+    tink::util::status
     tink::util::statusor
     absl::status
     absl::strings
@@ -48,12 +49,9 @@
 tink_cc_library(
   NAME status
   SRCS
-    status.cc
     status.h
   DEPS
-    absl::core_headers
     absl::status
-    absl::strings
   PUBLIC
 )
 
@@ -61,7 +59,6 @@
   NAME statusor
   SRCS
     statusor.h
-    statusor.h
   DEPS
     tink::util::status
     absl::statusor
@@ -89,9 +86,10 @@
     tink::util::errors
     tink::util::status
     tink::util::statusor
-    absl::memory
     absl::status
     tink::core::input_stream
+  TAGS
+    exclude_if_windows
 )
 
 tink_cc_library(
@@ -106,6 +104,8 @@
     absl::memory
     absl::status
     tink::core::output_stream
+  TAGS
+    exclude_if_windows
 )
 
 tink_cc_library(
@@ -121,6 +121,8 @@
     absl::memory
     absl::status
     tink::core::random_access_stream
+  TAGS
+    exclude_if_windows
 )
 
 tink_cc_library(
@@ -203,6 +205,7 @@
     tink::proto::hmac_cc_proto
     tink::proto::tink_cc_proto
     tink::proto::xchacha20_poly1305_cc_proto
+  TESTONLY
 )
 
 tink_cc_library(
@@ -214,6 +217,7 @@
     tink::util::statusor
     gmock
     absl::status
+  TESTONLY
 )
 
 tink_cc_library(
@@ -233,6 +237,7 @@
     absl::memory
     tink::core::keyset_handle
     tink::proto::tink_cc_proto
+  TESTONLY
 )
 
 tink_cc_library(
@@ -303,10 +308,17 @@
     file_input_stream_test.cc
   DEPS
     tink::util::file_input_stream
+    tink::util::status
+    tink::util::test_matchers
     tink::util::test_util
     gmock
     absl::memory
+    absl::status
     absl::strings
+    tink::internal::test_file_util
+    tink::subtle::random
+  TAGS
+    exclude_if_windows
 )
 
 tink_cc_test(
@@ -315,11 +327,15 @@
     file_output_stream_test.cc
   DEPS
     tink::util::file_output_stream
+    tink::util::test_matchers
     tink::util::test_util
     gmock
     absl::memory
     absl::strings
+    tink::internal::test_file_util
     tink::subtle::random
+  TAGS
+    exclude_if_windows
 )
 
 tink_cc_test(
@@ -329,10 +345,16 @@
   DEPS
     tink::util::buffer
     tink::util::file_random_access_stream
+    tink::util::test_matchers
     tink::util::test_util
     gmock
     absl::memory
+    absl::status
     absl::strings
+    tink::internal::test_file_util
+    tink::subtle::random
+  TAGS
+    exclude_if_windows
 )
 
 tink_cc_test(
@@ -344,7 +366,10 @@
     tink::util::test_util
     gmock
     absl::memory
+    absl::status
+    absl::statusor
     absl::strings
+    tink::internal::test_file_util
     tink::subtle::random
 )
 
@@ -358,6 +383,7 @@
     gmock
     absl::memory
     absl::strings
+    tink::internal::test_file_util
     tink::subtle::random
 )
 
@@ -366,10 +392,18 @@
   SRCS
     test_util_test.cc
   DEPS
+    tink::util::buffer
+    tink::util::ostream_output_stream
+    tink::util::statusor
     tink::util::test_matchers
     tink::util::test_util
     gmock
+    absl::strings
+    tink::core::output_stream
+    tink::core::random_access_stream
+    tink::internal::test_random_access_stream
     tink::subtle::subtle
+    tink::subtle::test_util
     tink::proto::aes_gcm_cc_proto
     tink::proto::tink_cc_proto
 )
@@ -404,8 +438,8 @@
   SRCS
     secret_data_internal.h
   DEPS
-    absl::strings
-    absl::base
+    absl::config
+    absl::core_headers
     crypto
 )
 
@@ -471,6 +505,7 @@
     tink::core::kms_client
     tink::core::kms_clients
     tink::aead::aead_key_templates
+  TESTONLY
 )
 
 tink_cc_test(
@@ -489,27 +524,3 @@
     tink::proto::kms_aead_cc_proto
     tink::proto::kms_envelope_cc_proto
 )
-
-tink_cc_test(
-  NAME status_test
-  SRCS
-    status_test.cc
-  DEPS
-    tink::util::status
-    gmock
-    absl::status
-)
-
-tink_cc_test(
-  NAME statusor_test
-  SRCS
-    statusor_test.cc
-  DEPS
-    tink::util::status
-    tink::util::statusor
-    tink::util::test_matchers
-    gmock
-    absl::memory
-    absl::status
-    absl::statusor
-)
diff --git a/cc/util/buffer.cc b/cc/util/buffer.cc
index f98adac..2fa67e5 100644
--- a/cc/util/buffer.cc
+++ b/cc/util/buffer.cc
@@ -16,6 +16,8 @@
 
 #include "tink/util/buffer.h"
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "tink/util/status.h"
@@ -54,7 +56,7 @@
     return OkStatus();
   }
 
-  ~OwningBuffer() override {}
+  ~OwningBuffer() override = default;
 
  private:
   std::unique_ptr<char[]> owned_mem_block_;
@@ -91,7 +93,7 @@
     return OkStatus();
   }
 
-  ~NonOwningBuffer() override {}
+  ~NonOwningBuffer() override = default;
 
  private:
   char* const mem_block_;
diff --git a/cc/util/buffer.h b/cc/util/buffer.h
index f46de54..3eaa558 100644
--- a/cc/util/buffer.h
+++ b/cc/util/buffer.h
@@ -17,6 +17,8 @@
 #ifndef TINK_UTIL_BUFFER_H_
 #define TINK_UTIL_BUFFER_H_
 
+#include <memory>
+
 #include "absl/memory/memory.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -58,7 +60,7 @@
   // Returns OK iff 0 <= new_size <= allocated_size();
   virtual util::Status set_size(int new_size) = 0;
 
-  virtual ~Buffer() {}
+  virtual ~Buffer() = default;
 };
 
 }  // namespace util
diff --git a/cc/util/enums.cc b/cc/util/enums.cc
index 201ca1b..5100162 100644
--- a/cc/util/enums.cc
+++ b/cc/util/enums.cc
@@ -18,6 +18,7 @@
 
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
+#include "tink/util/status.h"
 #include "proto/common.pb.h"
 #include "proto/ecdsa.pb.h"
 #include "proto/tink.pb.h"
diff --git a/cc/util/errors.h b/cc/util/errors.h
index cbb0ebe..5086ce4 100644
--- a/cc/util/errors.h
+++ b/cc/util/errors.h
@@ -23,17 +23,6 @@
 namespace crypto {
 namespace tink {
 
-#ifndef TINK_USE_ABSL_STATUS
-// Constructs a Status with formatted error message.
-template <typename... Args>
-ABSL_DEPRECATED("Prefer using absl::StatusCode as a first argument.")
-util::Status ToStatusF(util::error::Code code,
-                       const absl::FormatSpec<Args...>& format,
-                       const Args&... args) {
-  return util::Status(code, absl::StrFormat(format, args...));
-}
-#endif
-
 // Constructs a Status with formatted error message using absl::StatusCode.
 template <typename... Args>
 util::Status ToStatusF(absl::StatusCode code,
diff --git a/cc/util/errors_test.cc b/cc/util/errors_test.cc
index 73e74c2..cec75e6 100644
--- a/cc/util/errors_test.cc
+++ b/cc/util/errors_test.cc
@@ -24,49 +24,13 @@
 namespace tink {
 namespace {
 
-#ifndef TINK_USE_ABSL_STATUS
-TEST(ErrorsTest, ToStatusFTest) {
-  const char* const msg1 = "test message 1";
-  const char* const msg2 = "test message %s 2 %d";
-  crypto::tink::util::Status status;
-
-  status = util::Status(crypto::tink::util::error::OK, msg1);
-  EXPECT_TRUE(status.ok());
-  // if status is OK, error message is ignored
-  EXPECT_EQ("", status.message());
-  EXPECT_EQ(crypto::tink::util::error::OK, status.error_code());
-
-  const char* expected_msg2 = "test message asdf 2 42";
-  status = ToStatusF(crypto::tink::util::error::UNKNOWN, msg2, "asdf", 42);
-  EXPECT_FALSE(status.ok());
-  EXPECT_EQ(expected_msg2, status.message());
-  EXPECT_EQ(crypto::tink::util::error::UNKNOWN, status.error_code());
-}
-
-TEST(ErrorsTest, ToAbslStatus) {
-  crypto::tink::util::Status tink_status(util::error::INVALID_ARGUMENT,
-                                         "error");
-  ::absl::Status g3_status(tink_status);
-  EXPECT_FALSE(g3_status.ok());
-  EXPECT_EQ(g3_status.message(), "error");
-
-  EXPECT_EQ(::absl::Status(crypto::tink::util::OkStatus()), ::absl::OkStatus());
-}
-#endif
-
 TEST(ErrorsTest, ToStatusFAbslStatusCodeTest) {
   const char* const msg = "test message %s 2 %d";
   const char* expected_msg = "test message asdf 2 42";
-  crypto::tink::util::Status status =
-      ToStatusF(absl::StatusCode::kUnknown, msg, "asdf", 42);
+  util::Status status = ToStatusF(absl::StatusCode::kUnknown, msg, "asdf", 42);
   EXPECT_FALSE(status.ok());
   EXPECT_EQ(expected_msg, status.message());
   EXPECT_EQ(absl::StatusCode::kUnknown, status.code());
-
-  #ifndef TINK_USE_ABSL_STATUS
-  EXPECT_EQ(expected_msg, status.error_message());
-  EXPECT_EQ(crypto::tink::util::error::UNKNOWN, status.error_code());
-  #endif
 }
 
 }  // namespace
diff --git a/cc/util/fake_kms_client.cc b/cc/util/fake_kms_client.cc
index cb5f9ed..1f7922f 100644
--- a/cc/util/fake_kms_client.cc
+++ b/cc/util/fake_kms_client.cc
@@ -17,6 +17,8 @@
 
 #include <fstream>
 #include <iostream>
+#include <memory>
+#include <ostream>
 #include <sstream>
 #include <string>
 #include <utility>
diff --git a/cc/util/fake_kms_client_test.cc b/cc/util/fake_kms_client_test.cc
index b1039c7..4436915 100644
--- a/cc/util/fake_kms_client_test.cc
+++ b/cc/util/fake_kms_client_test.cc
@@ -31,7 +31,6 @@
 #include "proto/kms_aead.pb.h"
 #include "proto/kms_envelope.pb.h"
 
-using ::crypto::tink::test::IsOk;
 using google::crypto::tink::KeyTemplate;
 using google::crypto::tink::KmsAeadKeyFormat;
 using google::crypto::tink::KmsEnvelopeAeadKeyFormat;
diff --git a/cc/util/file_input_stream.cc b/cc/util/file_input_stream.cc
index 3903f20..695651a 100644
--- a/cc/util/file_input_stream.cc
+++ b/cc/util/file_input_stream.cc
@@ -19,9 +19,7 @@
 #include <unistd.h>
 #include <algorithm>
 
-#include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "tink/input_stream.h"
 #include "tink/util/errors.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -29,9 +27,10 @@
 namespace crypto {
 namespace tink {
 namespace util {
-
 namespace {
 
+constexpr int kDefaultBufferSize = 128 * 1024;
+
 // Attempts to close file descriptor fd, while ignoring EINTR.
 // (code borrowed from ZeroCopy-streams)
 int close_ignoring_eintr(int fd) {
@@ -42,7 +41,6 @@
   return result;
 }
 
-
 // Attempts to read 'count' bytes of data data from file descriptor fd
 // to 'buf' while ignoring EINTR.
 int read_ignoring_eintr(int fd, void *buf, size_t count) {
@@ -55,29 +53,27 @@
 
 }  // anonymous namespace
 
-FileInputStream::FileInputStream(int file_descriptor, int buffer_size) :
-    buffer_size_(buffer_size > 0 ? buffer_size : 128 * 1024) {  // 128 KB
-  fd_ = file_descriptor;
-  count_in_buffer_ = 0;
-  count_backedup_ = 0;
-  position_ = 0;
-  buffer_ = absl::make_unique<uint8_t[]>(buffer_size_);
-  buffer_offset_ = 0;
-  status_ = util::OkStatus();
-}
+FileInputStream::FileInputStream(int file_descriptor, int buffer_size)
+    : status_(util::OkStatus()),
+      fd_(file_descriptor),
+      buffer_(buffer_size > 0 ? buffer_size : kDefaultBufferSize) {}
 
-crypto::tink::util::StatusOr<int> FileInputStream::Next(const void** data) {
+util::StatusOr<int> FileInputStream::Next(const void** data) {
+  if (data == nullptr) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Data pointer must not be nullptr");
+  }
   if (!status_.ok()) return status_;
   if (count_backedup_ > 0) {  // Return the backed-up bytes.
     buffer_offset_ = buffer_offset_ + (count_in_buffer_ - count_backedup_);
     count_in_buffer_ = count_backedup_;
     count_backedup_ = 0;
-    *data = buffer_.get() + buffer_offset_;
+    *data = buffer_.data() + buffer_offset_;
     position_ = position_ + count_in_buffer_;
     return count_in_buffer_;
   }
   // Read new bytes to buffer_.
-  int read_result = read_ignoring_eintr(fd_, buffer_.get(), buffer_size_);
+  int read_result = read_ignoring_eintr(fd_, buffer_.data(), buffer_.size());
   if (read_result <= 0) {  // EOF or an I/O error.
     if (read_result == 0) {
       status_ = Status(absl::StatusCode::kOutOfRange, "EOF");
@@ -91,7 +87,7 @@
   count_backedup_ = 0;
   count_in_buffer_ = read_result;
   position_ = position_ + count_in_buffer_;
-  *data = buffer_.get();
+  *data = buffer_.data();
   return count_in_buffer_;
 }
 
@@ -102,13 +98,9 @@
   position_ = position_ - actual_count;
 }
 
-FileInputStream::~FileInputStream() {
-  close_ignoring_eintr(fd_);
-}
+FileInputStream::~FileInputStream() { close_ignoring_eintr(fd_); }
 
-int64_t FileInputStream::Position() const {
-  return position_;
-}
+int64_t FileInputStream::Position() const { return position_; }
 
 }  // namespace util
 }  // namespace tink
diff --git a/cc/util/file_input_stream.h b/cc/util/file_input_stream.h
index 5c816b0..f01c362 100644
--- a/cc/util/file_input_stream.h
+++ b/cc/util/file_input_stream.h
@@ -17,7 +17,9 @@
 #ifndef TINK_UTIL_FILE_INPUT_STREAM_H_
 #define TINK_UTIL_FILE_INPUT_STREAM_H_
 
+#include <cstdint>
 #include <memory>
+#include <vector>
 
 #include "tink/input_stream.h"
 #include "tink/util/status.h"
@@ -28,11 +30,13 @@
 namespace util {
 
 // An InputStream that reads from a file descriptor.
+//
+// NOTE: This class in not available when building on Windows.
 class FileInputStream : public crypto::tink::InputStream {
  public:
   // Constructs an InputStream that will read from the file specified
-  // via 'file_descriptor', using a buffer of the specified size, if any
-  // (if no legal 'buffer_size' is given, a reasonable default will be used).
+  // via `file_descriptor`, using a buffer of the specified size, if any
+  // (if no legal `buffer_size` is given, a reasonable default will be used).
   // Takes the ownership of the file, and will close it upon destruction.
   explicit FileInputStream(int file_descriptor, int buffer_size = -1);
 
@@ -45,16 +49,20 @@
   int64_t Position() const override;
 
  private:
-  util::Status status_;
+  // Status of the stream.
+  util::Status status_ = util::OkStatus();
   int fd_;
-  std::unique_ptr<uint8_t[]> buffer_;
-  const int buffer_size_;
-  int64_t position_;     // current position in the file (from the beginning)
+  std::vector<uint8_t> buffer_;
 
+  // Current position in the stream (from the beginning).
+  int64_t position_ = 0;
   // Counters that describe the state of the data in buffer_.
-  int count_in_buffer_;  // # of bytes available in buffer_
-  int count_backedup_;   // # of bytes available in buffer_ that were backed up
-  int buffer_offset_;    // offset at which the returned bytes start in buffer_
+  // # of bytes available in buffer_.
+  int count_in_buffer_ = 0;
+  // # of bytes available in buffer_ that were backed up.
+  int count_backedup_ = 0;
+  // offset at which the returned bytes start in buffer_.
+  int buffer_offset_ = 0;
 };
 
 }  // namespace util
diff --git a/cc/util/file_input_stream_test.cc b/cc/util/file_input_stream_test.cc
index 14ecc1a..2f7a172 100644
--- a/cc/util/file_input_stream_test.cc
+++ b/cc/util/file_input_stream_test.cc
@@ -13,27 +13,56 @@
 // limitations under the License.
 //
 ///////////////////////////////////////////////////////////////////////////////
-
 #include "tink/util/file_input_stream.h"
 
+#include <fcntl.h>
+
 #include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <iostream>
+#include <ostream>
 #include <string>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
+#include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_file_util.h"
+#include "tink/subtle/random.h"
+#include "tink/util/status.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-// Reads the specified 'input_stream' until no more bytes can be read,
-// and puts the read bytes into 'contents'.
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+
+constexpr int kDefaultTestStreamSize = 100 * 1024;  // 100 KB.
+
+// Opens test file `filename` and returns a file descriptor to it.
+util::StatusOr<int> OpenTestFileToRead(absl::string_view filename) {
+  std::string full_filename = absl::StrCat(test::TmpDir(), "/", filename);
+  int fd = open(full_filename.c_str(), O_RDONLY);
+  if (fd == -1) {
+    return util::Status(absl::StatusCode::kInternal,
+                        absl::StrCat("Cannot open file ", full_filename,
+                                     " error: ", std::strerror(errno)));
+  }
+  return fd;
+}
+
+// Reads the specified `input_stream` until no more bytes can be read,
+// and puts the read bytes into `contents`.
 // Returns the status of the last input_stream->Next()-operation.
-util::Status ReadTillEnd(util::FileInputStream* input_stream,
-                         std::string* contents) {
+util::Status ReadAll(util::FileInputStream* input_stream,
+                     std::string* contents) {
   contents->clear();
   const void* buffer;
   auto next_result = input_stream->Next(&buffer);
@@ -44,128 +73,241 @@
   return next_result.status();
 }
 
-class FileInputStreamTest : public ::testing::Test {
-};
+using FileInputStreamTestDefaultBufferSize = testing::TestWithParam<int>;
 
-TEST_F(FileInputStreamTest, testReadingStreams) {
-  for (auto stream_size : {0, 10, 100, 1000, 10000, 100000, 1000000}) {
-    SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
-    EXPECT_EQ(stream_size, file_contents.size());
-    auto input_stream = absl::make_unique<util::FileInputStream>(input_fd);
-    std::string stream_contents;
-    auto status = ReadTillEnd(input_stream.get(), &stream_contents);
-    EXPECT_EQ(absl::StatusCode::kOutOfRange, status.code());
-    EXPECT_EQ("EOF", status.message());
-    EXPECT_EQ(file_contents, stream_contents);
-  }
-}
-
-TEST_F(FileInputStreamTest, testCustomBufferSizes) {
-  int stream_size = 100000;
-  for (auto buffer_size : {1, 10, 100, 1000, 10000}) {
-    SCOPED_TRACE(absl::StrCat("buffer_size = ", buffer_size));
-    std::string file_contents;
-    std::string filename = absl::StrCat(buffer_size, "_buffer_size_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
-    EXPECT_EQ(stream_size, file_contents.size());
-    auto input_stream =
-        absl::make_unique<util::FileInputStream>(input_fd, buffer_size);
-    const void* buffer;
-    auto next_result = input_stream->Next(&buffer);
-    EXPECT_TRUE(next_result.ok()) << next_result.status();
-    EXPECT_EQ(buffer_size, next_result.value());
-    EXPECT_EQ(file_contents.substr(0, buffer_size),
-              std::string(static_cast<const char*>(buffer), buffer_size));
-  }
-}
-
-TEST_F(FileInputStreamTest, testBackupAndPosition) {
-  int stream_size = 100000;
-  int buffer_size = 1234;
-  const void* buffer;
-  std::string file_contents;
-  std::string filename = absl::StrCat(buffer_size, "_backup_test.bin");
-  int input_fd =
-      test::GetTestFileDescriptor(filename, stream_size, &file_contents);
+TEST_P(FileInputStreamTestDefaultBufferSize, ReadAllfFromInputStreamSucceeds) {
+  int stream_size = GetParam();
+  SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
+  std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+  std::string filename = absl::StrCat(
+      stream_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
   EXPECT_EQ(stream_size, file_contents.size());
+  auto input_stream = absl::make_unique<util::FileInputStream>(*input_fd);
+  std::string stream_contents;
+  auto status = ReadAll(input_stream.get(), &stream_contents);
+  EXPECT_THAT(status, StatusIs(absl::StatusCode::kOutOfRange));
+  EXPECT_EQ(status.message(), "EOF");
+  EXPECT_EQ(file_contents, stream_contents);
+}
 
-  // Prepare the stream and do the first call to Next().
+INSTANTIATE_TEST_SUITE_P(FileInputStreamTest,
+                         FileInputStreamTestDefaultBufferSize,
+                         testing::ValuesIn({0, 10, 100, 1000, 10000, 100000,
+                                            1000000}));
+
+using FileInputStreamTestCustomBufferSizes = testing::TestWithParam<int>;
+
+TEST_P(FileInputStreamTestCustomBufferSizes,
+       ReadAllWithCustomBufferSizeSucceeds) {
+  int buffer_size = GetParam();
+  SCOPED_TRACE(absl::StrCat("buffer_size = ", buffer_size));
+  std::string file_contents =
+      subtle::Random::GetRandomBytes(kDefaultTestStreamSize);
+  std::string filename = absl::StrCat(
+      buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
+  EXPECT_EQ(kDefaultTestStreamSize, file_contents.size());
   auto input_stream =
-      absl::make_unique<util::FileInputStream>(input_fd, buffer_size);
-  EXPECT_EQ(0, input_stream->Position());
+      absl::make_unique<util::FileInputStream>(*input_fd, buffer_size);
+  const void* buffer;
   auto next_result = input_stream->Next(&buffer);
-  EXPECT_TRUE(next_result.ok()) << next_result.status();
+  ASSERT_THAT(next_result, IsOk());
   EXPECT_EQ(buffer_size, next_result.value());
-  EXPECT_EQ(buffer_size, input_stream->Position());
   EXPECT_EQ(file_contents.substr(0, buffer_size),
             std::string(static_cast<const char*>(buffer), buffer_size));
+}
 
-  // BackUp several times, but in total fewer bytes than returned by Next().
-  int total_backup_size = 0;
-  for (auto backup_size : {0, 1, 5, 0, 10, 100, -42, 400, 20, -100}) {
-    SCOPED_TRACE(absl::StrCat("backup_size = ", backup_size));
-    input_stream->BackUp(backup_size);
-    total_backup_size += std::max(0, backup_size);
-    EXPECT_EQ(buffer_size - total_backup_size, input_stream->Position());
-  }
-  // Call Next(), it should return exactly the backed up bytes.
-  next_result = input_stream->Next(&buffer);
-  EXPECT_TRUE(next_result.ok()) << next_result.status();
-  EXPECT_EQ(total_backup_size, next_result.value());
-  EXPECT_EQ(buffer_size, input_stream->Position());
-  EXPECT_EQ(
-      file_contents.substr(buffer_size - total_backup_size, total_backup_size),
-      std::string(static_cast<const char*>(buffer), total_backup_size));
+INSTANTIATE_TEST_SUITE_P(FileInputStreamTest,
+                         FileInputStreamTestCustomBufferSizes,
+                         testing::ValuesIn({1, 10, 100, 1000, 10000}));
 
-  // BackUp() some bytes, again fewer than returned by Next().
-  total_backup_size = 0;
-  for (auto backup_size : {0, 72, -94, 37, 82}) {
-    SCOPED_TRACE(absl::StrCat("backup_size = ", backup_size));
-    input_stream->BackUp(backup_size);
-    total_backup_size += std::max(0, backup_size);
-    EXPECT_EQ(buffer_size - total_backup_size, input_stream->Position());
-  }
+TEST(FileInputStreamTest, NextFailsIfFdIsInvalid) {
+  int buffer_size = 4 * 1024;
+  auto input_stream = absl::make_unique<util::FileInputStream>(-1, buffer_size);
+  const void* buffer = nullptr;
+  EXPECT_THAT(input_stream->Next(&buffer).status(),
+              StatusIs(absl::StatusCode::kInternal));
+}
 
-  // Call Next(), it should return exactly the backed up bytes.
-  next_result = input_stream->Next(&buffer);
-  EXPECT_TRUE(next_result.ok()) << next_result.status();
-  EXPECT_EQ(total_backup_size, next_result.value());
-  EXPECT_EQ(buffer_size, input_stream->Position());
-  EXPECT_EQ(
-      file_contents.substr(buffer_size - total_backup_size, total_backup_size),
-      std::string(static_cast<const char*>(buffer), total_backup_size));
+TEST(FileInputStreamTest, NextFailsIfDataIsNull) {
+  int buffer_size = 4 * 1024;
+  std::string file_contents =
+      subtle::Random::GetRandomBytes(kDefaultTestStreamSize);
+  std::string filename = absl::StrCat(
+      buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
+  EXPECT_EQ(kDefaultTestStreamSize, file_contents.size());
+  auto input_stream =
+      absl::make_unique<util::FileInputStream>(*input_fd, buffer_size);
 
-  // Call Next() again, it should return the second block.
-  next_result = input_stream->Next(&buffer);
-  EXPECT_TRUE(next_result.ok()) << next_result.status();
-  EXPECT_EQ(buffer_size, next_result.value());
-  EXPECT_EQ(2 * buffer_size, input_stream->Position());
-  EXPECT_EQ(file_contents.substr(buffer_size, buffer_size),
-            std::string(static_cast<const char*>(buffer), buffer_size));
+  EXPECT_THAT(input_stream->Next(nullptr).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
 
-  // BackUp a few times, with total over the returned buffer_size.
-  total_backup_size = 0;
-  for (auto backup_size :
-           {0, 72, -100, buffer_size/2, 200, -25, buffer_size, 42}) {
-    SCOPED_TRACE(absl::StrCat("backup_size = ", backup_size));
-    input_stream->BackUp(backup_size);
-    total_backup_size = std::min(buffer_size,
-                                 total_backup_size + std::max(0, backup_size));
-    EXPECT_EQ(2 * buffer_size - total_backup_size, input_stream->Position());
-  }
+TEST(FileInputStreamTest, NextReadsExactlyOneBlockOfData) {
+  int buffer_size = 4 * 1024;
+  std::string file_contents =
+      subtle::Random::GetRandomBytes(kDefaultTestStreamSize);
+  std::string filename = absl::StrCat(
+      buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
+  EXPECT_EQ(kDefaultTestStreamSize, file_contents.size());
+  auto input_stream =
+      absl::make_unique<util::FileInputStream>(*input_fd, buffer_size);
 
-  // Call Next() again, it should return the second block.
-  next_result = input_stream->Next(&buffer);
-  EXPECT_TRUE(next_result.ok()) << next_result.status();
-  EXPECT_EQ(buffer_size, next_result.value());
-  EXPECT_EQ(2 * buffer_size, input_stream->Position());
-  EXPECT_EQ(file_contents.substr(buffer_size, buffer_size),
-            std::string(static_cast<const char*>(buffer), buffer_size));
+  auto expected_file_content_block =
+      absl::string_view(file_contents).substr(0, buffer_size);
+  const void* buffer = nullptr;
+  util::StatusOr<int> next_result = input_stream->Next(&buffer);
+  ASSERT_THAT(next_result, IsOkAndHolds(buffer_size));
+  // Check that we advanced of buffer_size bytes.
+  EXPECT_EQ(input_stream->Position(), buffer_size);
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_file_content_block);
+}
+
+TEST(FileInputStreamTest, BackupForNegativeOrZeroBytesIsANoop) {
+  int buffer_size = 4 * 1024;
+  std::string file_contents =
+      subtle::Random::GetRandomBytes(kDefaultTestStreamSize);
+  std::string filename = absl::StrCat(
+      buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
+  EXPECT_EQ(kDefaultTestStreamSize, file_contents.size());
+  auto input_stream =
+      absl::make_unique<util::FileInputStream>(*input_fd, buffer_size);
+  EXPECT_EQ(input_stream->Position(), 0);
+
+  auto expected_file_content_block =
+      absl::string_view(file_contents).substr(0, buffer_size);
+  const void* buffer = nullptr;
+  ASSERT_THAT(input_stream->Next(&buffer), IsOkAndHolds(buffer_size));
+  // Check that we advanced of buffer_size bytes.
+  EXPECT_EQ(input_stream->Position(), buffer_size);
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_file_content_block);
+
+  // The calls below are noops.
+  input_stream->BackUp(0);
+  EXPECT_EQ(input_stream->Position(), buffer_size);
+  input_stream->BackUp(-12);
+  EXPECT_EQ(input_stream->Position(), buffer_size);
+
+  // A subsequent call to `Next` returns the 2nd block.
+  auto expected_2nd_file_content_block =
+      absl::string_view(file_contents).substr(buffer_size, buffer_size);
+  ASSERT_THAT(input_stream->Next(&buffer), IsOkAndHolds(buffer_size));
+  // Check that we advanced of buffer_size bytes.
+  EXPECT_EQ(input_stream->Position(), 2 * buffer_size);
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_2nd_file_content_block);
+}
+
+TEST(FileInputStreamTest, BackupForLessThanOneBlockOfData) {
+  int buffer_size = 4 * 1024;
+  std::string file_contents =
+      subtle::Random::GetRandomBytes(kDefaultTestStreamSize);
+  std::string filename = absl::StrCat(
+      buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
+  EXPECT_EQ(kDefaultTestStreamSize, file_contents.size());
+  auto input_stream =
+      absl::make_unique<util::FileInputStream>(*input_fd, buffer_size);
+
+  auto expected_file_content_block =
+      absl::string_view(file_contents).substr(0, buffer_size);
+  const void* buffer = nullptr;
+  ASSERT_THAT(input_stream->Next(&buffer), IsOkAndHolds(buffer_size));
+  // Check that we advanced of buffer_size bytes.
+  EXPECT_EQ(input_stream->Position(), buffer_size);
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_file_content_block);
+
+  int64_t position_after_next = input_stream->Position();
+  // Number of bytes that were backed up.
+  int num_backed_up_bytes = 0;
+  input_stream->BackUp(0);  // This should be a noop.
+  EXPECT_EQ(input_stream->Position(), position_after_next);
+  input_stream->BackUp(-12);  // This should be a noop.
+  EXPECT_EQ(input_stream->Position(), position_after_next);
+  input_stream->BackUp(10);
+  num_backed_up_bytes += 10;
+  EXPECT_EQ(input_stream->Position(),
+            position_after_next - num_backed_up_bytes);
+  input_stream->BackUp(5);
+  num_backed_up_bytes += 5;
+  EXPECT_EQ(input_stream->Position(),
+            position_after_next - num_backed_up_bytes);
+
+  // A subsequent call to Next should return only the backed up bytes.
+  auto expected_backed_up_bytes =
+      absl::string_view(file_contents)
+          .substr(buffer_size - num_backed_up_bytes, num_backed_up_bytes);
+  ASSERT_THAT(input_stream->Next(&buffer),
+              IsOkAndHolds(expected_backed_up_bytes.size()));
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer),
+                              expected_backed_up_bytes.size()),
+            expected_backed_up_bytes);
+}
+
+// When backing up of a number of bytes larger than the size of a block, backup
+// of one block.
+TEST(FileInputStreamTest, BackupAtMostOfOneBlock) {
+  int buffer_size = 4 * 1024;
+  std::string file_contents =
+      subtle::Random::GetRandomBytes(kDefaultTestStreamSize);
+  std::string filename = absl::StrCat(
+      buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, file_contents), IsOk());
+  util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+  ASSERT_THAT(input_fd.status(), IsOk());
+  EXPECT_EQ(kDefaultTestStreamSize, file_contents.size());
+  auto input_stream =
+      absl::make_unique<util::FileInputStream>(*input_fd, buffer_size);
+
+  // Read two blocks of size buffer_size, then back up of more than buffer_size
+  // bytes.
+  auto expected_1st_file_content_block =
+      absl::string_view(file_contents).substr(0, buffer_size);
+  const void* buffer = nullptr;
+  ASSERT_THAT(input_stream->Next(&buffer), IsOkAndHolds(buffer_size));
+  // Check that we advanced of buffer_size bytes.
+  EXPECT_EQ(input_stream->Position(), buffer_size);
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_1st_file_content_block);
+
+  auto expected_2nd_file_content_block =
+      absl::string_view(file_contents).substr(buffer_size, buffer_size);
+  ASSERT_THAT(input_stream->Next(&buffer), IsOkAndHolds(buffer_size));
+  // Check that we advanced of buffer_size bytes.
+  EXPECT_EQ(input_stream->Position(), 2 * buffer_size);
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_2nd_file_content_block);
+
+  int64_t position_after_next = input_stream->Position();
+  EXPECT_EQ(input_stream->Position(), position_after_next);
+  input_stream->BackUp(10);
+  EXPECT_EQ(input_stream->Position(), position_after_next - 10);
+  input_stream->BackUp(buffer_size);
+  EXPECT_EQ(input_stream->Position(), position_after_next - buffer_size);
+
+  // This call to Next is expected to read the second block again.
+  ASSERT_THAT(input_stream->Next(&buffer), IsOkAndHolds(buffer_size));
+  EXPECT_EQ(absl::string_view(static_cast<const char*>(buffer), buffer_size),
+            expected_2nd_file_content_block);
 }
 
 }  // namespace
diff --git a/cc/util/file_output_stream.h b/cc/util/file_output_stream.h
index 43b638a..2a6dd0f 100644
--- a/cc/util/file_output_stream.h
+++ b/cc/util/file_output_stream.h
@@ -28,6 +28,8 @@
 namespace util {
 
 // An OutputStream that writes to a file descriptor.
+//
+// NOTE: This class in not available when building on Windows.
 class FileOutputStream : public crypto::tink::OutputStream {
  public:
   // Constructs an OutputStream that will write to the file specified
diff --git a/cc/util/file_output_stream_test.cc b/cc/util/file_output_stream_test.cc
index dbdf7f2..6e46c7b 100644
--- a/cc/util/file_output_stream_test.cc
+++ b/cc/util/file_output_stream_test.cc
@@ -16,20 +16,42 @@
 
 #include "tink/util/file_output_stream.h"
 
+#include <fcntl.h>
+
 #include <algorithm>
+#include <cstring>
+#include <iostream>
+#include <ostream>
 #include <string>
 
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_file_util.h"
 #include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
+using ::crypto::tink::test::IsOk;
+
+// Opens test file `filename` and returns a file descriptor to it.
+util::StatusOr<int> OpenTestFileToWrite(absl::string_view filename) {
+  std::string full_filename = absl::StrCat(test::TmpDir(), "/", filename);
+  mode_t mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH;
+  int fd = open(full_filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode);
+  if (fd == -1) {
+    return util::Status(absl::StatusCode::kInternal,
+                        absl::StrCat("Cannot open file ", full_filename,
+                                     " error: ", std::strerror(errno)));
+  }
+  return fd;
+}
+
 // Writes 'contents' the specified 'output_stream', and closes the stream.
 // Returns the status of output_stream->Close()-operation, or a non-OK status
 // of a prior output_stream->Next()-operation, if any.
@@ -62,9 +84,12 @@
   for (auto stream_size : {0, 10, 100, 1000, 10000, 100000, 1000000}) {
     SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
     std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
-    std::string filename = absl::StrCat(stream_size, "_writing_test.bin");
-    int output_fd = test::GetTestFileDescriptor(filename);
-    auto output_stream = absl::make_unique<util::FileOutputStream>(output_fd);
+    std::string filename = absl::StrCat(
+        stream_size, internal::GetTestFileNamePrefix(), "_test.bin");
+    ASSERT_THAT(internal::CreateTestFile(filename, stream_contents), IsOk());
+    util::StatusOr<int> output_fd = OpenTestFileToWrite(filename);
+    ASSERT_THAT(output_fd.status(), IsOk());
+    auto output_stream = absl::make_unique<util::FileOutputStream>(*output_fd);
     auto status = WriteToStream(output_stream.get(), stream_contents);
     EXPECT_TRUE(status.ok()) << status;
     std::string file_contents = test::ReadTestFile(filename);
@@ -78,10 +103,13 @@
   std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
   for (auto buffer_size : {1, 10, 100, 1000, 10000, 100000, 1000000}) {
     SCOPED_TRACE(absl::StrCat("buffer_size = ", buffer_size));
-    std::string filename = absl::StrCat(buffer_size, "_buffer_size_test.bin");
-    int output_fd = test::GetTestFileDescriptor(filename);
+    std::string filename = absl::StrCat(
+        buffer_size, internal::GetTestFileNamePrefix(), "_test.bin");
+    ASSERT_THAT(internal::CreateTestFile(filename, stream_contents), IsOk());
+    util::StatusOr<int> output_fd = OpenTestFileToWrite(filename);
+    ASSERT_THAT(output_fd.status(), IsOk());
     auto output_stream =
-        absl::make_unique<util::FileOutputStream>(output_fd, buffer_size);
+        absl::make_unique<util::FileOutputStream>(*output_fd, buffer_size);
     void* buffer;
     auto next_result = output_stream->Next(&buffer);
     EXPECT_TRUE(next_result.ok()) << next_result.status();
@@ -101,12 +129,15 @@
   int buffer_size = 1234;
   void* buffer;
   std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
-  std::string filename = absl::StrCat(buffer_size, "_backup_test.bin");
-  int output_fd = test::GetTestFileDescriptor(filename);
+  std::string filename =
+      absl::StrCat(buffer_size, internal::GetTestFileNamePrefix(), "_test.bin");
+  ASSERT_THAT(internal::CreateTestFile(filename, stream_contents), IsOk());
+  util::StatusOr<int> output_fd = OpenTestFileToWrite(filename);
+  ASSERT_THAT(output_fd.status(), IsOk());
 
   // Prepare the stream and do the first call to Next().
   auto output_stream =
-      absl::make_unique<util::FileOutputStream>(output_fd, buffer_size);
+      absl::make_unique<util::FileOutputStream>(*output_fd, buffer_size);
   EXPECT_EQ(0, output_stream->Position());
   auto next_result = output_stream->Next(&buffer);
   EXPECT_TRUE(next_result.ok()) << next_result.status();
diff --git a/cc/util/file_random_access_stream.h b/cc/util/file_random_access_stream.h
index 47bfc13..75966c9 100644
--- a/cc/util/file_random_access_stream.h
+++ b/cc/util/file_random_access_stream.h
@@ -29,6 +29,8 @@
 namespace util {
 
 // An RandomAccessStream that reads from a file descriptor.
+//
+// NOTE: This class in not available when building on Windows.
 class FileRandomAccessStream : public crypto::tink::RandomAccessStream {
  public:
   // Constructs a FileRandomAccessStream that will read from the file specified
diff --git a/cc/util/file_random_access_stream_test.cc b/cc/util/file_random_access_stream_test.cc
index 70c510a..3aebe63 100644
--- a/cc/util/file_random_access_stream_test.cc
+++ b/cc/util/file_random_access_stream_test.cc
@@ -16,15 +16,25 @@
 
 #include "tink/util/file_random_access_stream.h"
 
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <iostream>
+#include <ostream>
 #include <string>
 #include <thread>  // NOLINT(build/c++11)
 #include <utility>
 
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
+#include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_file_util.h"
+#include "tink/subtle/random.h"
 #include "tink/util/buffer.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -32,6 +42,20 @@
 namespace util {
 namespace {
 
+using ::crypto::tink::test::IsOk;
+
+// Opens test file `filename` and returns a file descriptor to it.
+util::StatusOr<int> OpenTestFileToRead(absl::string_view filename) {
+  std::string full_filename = absl::StrCat(test::TmpDir(), "/", filename);
+  int fd = open(full_filename.c_str(), O_RDONLY);
+  if (fd == -1) {
+    return util::Status(absl::StatusCode::kInternal,
+                        absl::StrCat("Cannot open file ", full_filename,
+                                     " error: ", std::strerror(errno)));
+  }
+  return fd;
+}
+
 // Reads the entire 'ra_stream' in chunks of size 'chunk_size',
 // until no more bytes can be read, and puts the read bytes into 'contents'.
 // Returns the status of the last ra_stream->Next()-operation.
@@ -79,15 +103,19 @@
 TEST(FileRandomAccessStreamTest, ReadingStreams) {
   for (auto stream_size : {1, 10, 100, 1000, 10000, 1000000}) {
     SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
+    std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+    std::string filename = absl::StrCat(
+        stream_size, crypto::tink::internal::GetTestFileNamePrefix(),
+        "_file.bin");
+    ASSERT_THAT(crypto::tink::internal::CreateTestFile(filename, file_contents),
+                IsOk());
+    util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+    ASSERT_THAT(input_fd.status(), IsOk());
     EXPECT_EQ(stream_size, file_contents.size());
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(*input_fd);
     std::string stream_contents;
-    auto status = ReadAll(ra_stream.get(), 1 + (stream_size / 10),
-                          &stream_contents);
+    auto status =
+        ReadAll(ra_stream.get(), 1 + (stream_size / 10), &stream_contents);
     EXPECT_EQ(absl::StatusCode::kOutOfRange, status.code());
     EXPECT_EQ("EOF", status.message());
     EXPECT_EQ(file_contents, stream_contents);
@@ -98,12 +126,16 @@
 TEST(FileRandomAccessStreamTest, ReadingStreamsTillLastByte) {
   for (auto stream_size : {1, 10, 100, 1000, 10000}) {
     SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
+    std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+    std::string filename = absl::StrCat(
+        stream_size, crypto::tink::internal::GetTestFileNamePrefix(),
+        "_file.bin");
+    ASSERT_THAT(crypto::tink::internal::CreateTestFile(filename, file_contents),
+                IsOk());
+    util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+    ASSERT_THAT(input_fd.status(), IsOk());
     EXPECT_EQ(stream_size, file_contents.size());
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(*input_fd);
     auto buffer = std::move(Buffer::New(stream_size).value());
 
     // Read from the beginning till the last byte.
@@ -116,15 +148,18 @@
   }
 }
 
-
 TEST(FileRandomAccessStreamTest, ConcurrentReads) {
   for (auto stream_size : {100, 1000, 10000, 100000}) {
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
+    std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+    std::string filename = absl::StrCat(
+        stream_size, crypto::tink::internal::GetTestFileNamePrefix(),
+        "_file.bin");
+    ASSERT_THAT(crypto::tink::internal::CreateTestFile(filename, file_contents),
+                IsOk());
+    util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+    ASSERT_THAT(input_fd.status(), IsOk());
     EXPECT_EQ(stream_size, file_contents.size());
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(*input_fd);
     std::thread read_0(ReadAndVerifyChunk,
         ra_stream.get(), 0, stream_size / 2, file_contents);
     std::thread read_1(ReadAndVerifyChunk,
@@ -142,11 +177,15 @@
 
 TEST(FileRandomAccessStreamTest, NegativeReadPosition) {
   for (auto stream_size : {0, 10, 100, 1000, 10000}) {
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+    std::string filename = absl::StrCat(
+        stream_size, crypto::tink::internal::GetTestFileNamePrefix(),
+        "_file.bin");
+    ASSERT_THAT(crypto::tink::internal::CreateTestFile(filename, file_contents),
+                IsOk());
+    util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+    ASSERT_THAT(input_fd.status(), IsOk());
+    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(*input_fd);
     int count = 42;
     auto buffer = std::move(Buffer::New(count).value());
     for (auto position : {-100, -10, -1}) {
@@ -161,11 +200,15 @@
 
 TEST(FileRandomAccessStreamTest, NotPositiveReadCount) {
   for (auto stream_size : {0, 10, 100, 1000, 10000}) {
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+    std::string filename = absl::StrCat(
+        stream_size, crypto::tink::internal::GetTestFileNamePrefix(),
+        "_file.bin");
+    ASSERT_THAT(crypto::tink::internal::CreateTestFile(filename, file_contents),
+                IsOk());
+    util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+    ASSERT_THAT(input_fd.status(), IsOk());
+    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(*input_fd);
     auto buffer = std::move(Buffer::New(42).value());
     int64_t position = 0;
     for (auto count : {-100, -10, -1, 0}) {
@@ -179,11 +222,15 @@
 
 TEST(FileRandomAccessStreamTest, ReadPositionAfterEof) {
   for (auto stream_size : {0, 10, 100, 1000, 10000}) {
-    std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
-    int input_fd =
-        test::GetTestFileDescriptor(filename, stream_size, &file_contents);
-    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(input_fd);
+    std::string file_contents = subtle::Random::GetRandomBytes(stream_size);
+    std::string filename = absl::StrCat(
+        stream_size, crypto::tink::internal::GetTestFileNamePrefix(),
+        "_file.bin");
+    ASSERT_THAT(crypto::tink::internal::CreateTestFile(filename, file_contents),
+                IsOk());
+    util::StatusOr<int> input_fd = OpenTestFileToRead(filename);
+    ASSERT_THAT(input_fd.status(), IsOk());
+    auto ra_stream = absl::make_unique<util::FileRandomAccessStream>(*input_fd);
     int count = 42;
     auto buffer = std::move(Buffer::New(count).value());
     for (auto position : {stream_size + 1, stream_size + 10}) {
diff --git a/cc/util/input_stream_util_test.cc b/cc/util/input_stream_util_test.cc
index 1700588..e54f8a7 100644
--- a/cc/util/input_stream_util_test.cc
+++ b/cc/util/input_stream_util_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/util/input_stream_util.h"
 
+#include <sstream>
 #include <string>
 #include <utility>
 
diff --git a/cc/util/istream_input_stream.cc b/cc/util/istream_input_stream.cc
index 71c642b..45a0449 100644
--- a/cc/util/istream_input_stream.cc
+++ b/cc/util/istream_input_stream.cc
@@ -16,16 +16,17 @@
 
 #include "tink/util/istream_input_stream.h"
 
-#include <unistd.h>
+#include <errno.h>
+#include <stdint.h>
 
 #include <algorithm>
 #include <cstring>
 #include <istream>
+#include <memory>
 #include <utility>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
-#include "tink/input_stream.h"
 #include "tink/util/errors.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -85,8 +86,7 @@
   position_ = position_ - actual_count;
 }
 
-IstreamInputStream::~IstreamInputStream() {
-}
+IstreamInputStream::~IstreamInputStream() = default;
 
 int64_t IstreamInputStream::Position() const {
   return position_;
diff --git a/cc/util/istream_input_stream.h b/cc/util/istream_input_stream.h
index 530192a..5ed369a 100644
--- a/cc/util/istream_input_stream.h
+++ b/cc/util/istream_input_stream.h
@@ -17,6 +17,8 @@
 #ifndef TINK_UTIL_ISTREAM_INPUT_STREAM_H_
 #define TINK_UTIL_ISTREAM_INPUT_STREAM_H_
 
+#include <stdint.h>
+
 #include <istream>
 #include <memory>
 
diff --git a/cc/util/istream_input_stream_test.cc b/cc/util/istream_input_stream_test.cc
index 2586348..de5c44f 100644
--- a/cc/util/istream_input_stream_test.cc
+++ b/cc/util/istream_input_stream_test.cc
@@ -16,18 +16,25 @@
 
 #include "tink/util/istream_input_stream.h"
 
-#include <unistd.h>
+#include <errno.h>
+#include <stdlib.h>
 
 #include <algorithm>
 #include <fstream>
 #include <iostream>
 #include <istream>
+#include <memory>
+#include <ostream>
 #include <string>
 #include <utility>
 
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_file_util.h"
 #include "tink/subtle/random.h"
 #include "tink/util/test_util.h"
 
@@ -76,9 +83,10 @@
 };
 
 TEST_F(IstreamInputStreamTest, testReadingStreams) {
-  for (int stream_size : {0, 10, 100, 1000, 10000, 100000, 1000000}) {
+    for (int stream_size : {0, 10, 100, 1000, 10000, 100000, 1000000}) {
     std::string file_contents;
-    std::string filename = absl::StrCat(stream_size, "_reading_test.bin");
+    std::string filename = absl::StrCat(
+        stream_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
     auto input = GetTestIstream(filename, stream_size, &file_contents);
     EXPECT_EQ(stream_size, file_contents.size());
     auto input_stream = absl::make_unique<util::IstreamInputStream>(
@@ -95,7 +103,8 @@
   int stream_size = 100000;
   for (int buffer_size : {1, 10, 100, 1000, 10000}) {
     std::string file_contents;
-    std::string filename = absl::StrCat(buffer_size, "_buffer_size_test.bin");
+    std::string filename = absl::StrCat(
+        buffer_size, "_", internal::GetTestFileNamePrefix(), "_file.bin");
     auto input = GetTestIstream(filename, stream_size, &file_contents);
     EXPECT_EQ(stream_size, file_contents.size());
     auto input_stream = absl::make_unique<util::IstreamInputStream>(
@@ -114,7 +123,8 @@
   int buffer_size = 1234;
   const void* buffer;
   std::string file_contents;
-  std::string filename = absl::StrCat(buffer_size, "_backup_test.bin");
+  std::string filename =
+      absl::StrCat(buffer_size, internal::GetTestFileNamePrefix(), "_file.bin");
   auto input = GetTestIstream(filename, stream_size, &file_contents);
   EXPECT_EQ(stream_size, file_contents.size());
 
diff --git a/cc/util/ostream_output_stream_test.cc b/cc/util/ostream_output_stream_test.cc
index 05db8e54..8a7cf6a 100644
--- a/cc/util/ostream_output_stream_test.cc
+++ b/cc/util/ostream_output_stream_test.cc
@@ -17,9 +17,11 @@
 #include "tink/util/ostream_output_stream.h"
 
 #include <algorithm>
+#include <cstring>
 #include <fstream>
 #include <iostream>
 #include <memory>
+#include <ostream>
 #include <string>
 #include <utility>
 
@@ -27,6 +29,7 @@
 #include "absl/memory/memory.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "tink/internal/test_file_util.h"
 #include "tink/subtle/random.h"
 #include "tink/util/test_util.h"
 
@@ -75,7 +78,8 @@
   for (size_t stream_size : {0, 10, 100, 1000, 10000, 100000, 1000000}) {
     SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
     std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
-    std::string filename = absl::StrCat(stream_size, "_writing_test.bin");
+    std::string filename = absl::StrCat(
+        stream_size, internal::GetTestFileNamePrefix(), "_file.bin");
     auto output = GetTestOstream(filename);
     auto output_stream = absl::make_unique<util::OstreamOutputStream>(
         std::move(output));
@@ -92,7 +96,8 @@
   std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
   for (int buffer_size : {1, 10, 100, 1000, 10000, 100000, 1000000}) {
     SCOPED_TRACE(absl::StrCat("buffer_size = ", buffer_size));
-    std::string filename = absl::StrCat(buffer_size, "_buffer_size_test.bin");
+    std::string filename = absl::StrCat(
+        buffer_size, internal::GetTestFileNamePrefix(), "_file.bin");
     auto output = GetTestOstream(filename);
     auto output_stream = absl::make_unique<util::OstreamOutputStream>(
         std::move(output), buffer_size);
@@ -114,7 +119,8 @@
   int buffer_size = 1234;
   void* buffer;
   std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
-  std::string filename = absl::StrCat(buffer_size, "_backup_test.bin");
+  std::string filename =
+      absl::StrCat(buffer_size, internal::GetTestFileNamePrefix(), "_file.bin");
   auto output = GetTestOstream(filename);
 
   // Prepare the stream and do the first call to Next().
diff --git a/cc/util/secret_data.h b/cc/util/secret_data.h
index ab87500..54ce234 100644
--- a/cc/util/secret_data.h
+++ b/cc/util/secret_data.h
@@ -28,6 +28,17 @@
 namespace crypto {
 namespace tink {
 namespace util {
+namespace internal {
+
+template <typename T>
+struct SanitizingDeleter {
+  void operator()(T* ptr) {
+    ptr->~T();  // Invoke destructor. Must do this before sanitize.
+    SanitizingAllocator<T>().deallocate(ptr, 1);
+  }
+};
+
+}  // namespace internal
 
 // Stores secret (sensitive) data and makes sure it's marked as such and
 // destroyed in a safe way.
@@ -74,7 +85,7 @@
   using element_type = typename Value::element_type;
   using deleter_type = typename Value::deleter_type;
 
-  SecretUniquePtr() {}
+  SecretUniquePtr() = default;
 
   pointer get() const { return value_.get(); }
   deleter_type& get_deleter() { return value_.get_deleter(); }
diff --git a/cc/util/secret_data_internal.h b/cc/util/secret_data_internal.h
index ddcacd5..7d88b93 100644
--- a/cc/util/secret_data_internal.h
+++ b/cc/util/secret_data_internal.h
@@ -18,7 +18,8 @@
 #define TINK_UTIL_SECRET_DATA_INTERNAL_H_
 
 #include <cstddef>
-#include <memory>
+#include <cstdlib>
+#include <limits>
 #include <new>
 
 #include "absl/base/attributes.h"
@@ -30,15 +31,12 @@
 namespace util {
 namespace internal {
 
-// placeholder for sanitization_functions, please ignore
 inline void SafeZeroMemory(void* ptr, std::size_t size) {
   OPENSSL_cleanse(ptr, size);
 }
 
 template <typename T>
-struct SanitizingAllocator {
-  typedef T value_type;
-
+struct SanitizingAllocatorImpl {
   // If aligned operator new is not supported this only supports under aligned
   // types.
 #ifndef __cpp_aligned_new
@@ -47,12 +45,7 @@
                 "before C++17");
 #endif
 
-  SanitizingAllocator() = default;
-  template <class U>
-  explicit constexpr SanitizingAllocator(
-      const SanitizingAllocator<U>&) noexcept {}
-
-  ABSL_MUST_USE_RESULT T* allocate(std::size_t n) {
+  static T* allocate(std::size_t n) {
     if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
 #ifdef ABSL_HAVE_EXCEPTIONS
       throw std::bad_array_new_length();
@@ -62,14 +55,13 @@
     }
     std::size_t size = n * sizeof(T);
 #ifdef __cpp_aligned_new
-    void* result = ::operator new(size, std::align_val_t(alignof(T)));
+    return static_cast<T*>(::operator new(size, std::align_val_t(alignof(T))));
 #else
-    void* result = ::operator new(size);
+    return static_cast<T*>(::operator new(size));
 #endif
-    return static_cast<T*>(result);
   }
 
-  void deallocate(T* ptr, std::size_t n) noexcept {
+  static void deallocate(void* ptr, std::size_t n) {
     SafeZeroMemory(ptr, n * sizeof(T));
 #ifdef __cpp_aligned_new
     ::operator delete(ptr, std::align_val_t(alignof(T)));
@@ -77,42 +69,39 @@
     ::operator delete(ptr);
 #endif
   }
-
-  // Allocator requirements mandate definition of eq and neq operators
-  bool operator==(const SanitizingAllocator&) { return true; }
-  bool operator!=(const SanitizingAllocator&) { return false; }
 };
 
 // Specialization for malloc-like aligned storage.
 template <>
-struct SanitizingAllocator<void> {
-  typedef void value_type;
+struct SanitizingAllocatorImpl<void> {
+  static void* allocate(std::size_t n) { return std::malloc(n); }
+  static void deallocate(void* ptr, std::size_t n) {
+    SafeZeroMemory(ptr, n);
+    return std::free(ptr);
+  }
+};
+
+template <typename T>
+struct SanitizingAllocator {
+  typedef T value_type;
 
   SanitizingAllocator() = default;
   template <class U>
   explicit constexpr SanitizingAllocator(
       const SanitizingAllocator<U>&) noexcept {}
 
-  ABSL_MUST_USE_RESULT void* allocate(std::size_t n) { return std::malloc(n); }
+  ABSL_MUST_USE_RESULT T* allocate(std::size_t n) {
+    return SanitizingAllocatorImpl<T>::allocate(n);
+  }
 
-  void deallocate(void* ptr, std::size_t n) noexcept {
-    SafeZeroMemory(ptr, n);
-    std::free(ptr);
+  void deallocate(T* ptr, std::size_t n) noexcept {
+    SanitizingAllocatorImpl<T>::deallocate(ptr, n);
   }
 
   // Allocator requirements mandate definition of eq and neq operators
   bool operator==(const SanitizingAllocator&) { return true; }
   bool operator!=(const SanitizingAllocator&) { return false; }
 };
-// placeholder 2 for sanitization_functions, please ignore
-
-template <typename T>
-struct SanitizingDeleter {
-  void operator()(T* ptr) {
-    ptr->~T();  // Invoke destructor. Must do this before sanitize.
-    SanitizingAllocator<T>().deallocate(ptr, 1);
-  }
-};
 
 }  // namespace internal
 }  // namespace util
diff --git a/cc/util/secret_data_test.cc b/cc/util/secret_data_test.cc
index 72f1167..3de92e9 100644
--- a/cc/util/secret_data_test.cc
+++ b/cc/util/secret_data_test.cc
@@ -121,6 +121,7 @@
   SecretValue<int> s(102);
   SecretValue<int> t(std::move(s));
   EXPECT_THAT(t.value(), Eq(102));
+  // NOLINTNEXTLINE(bugprone-use-after-move)
   EXPECT_THAT(s.value(), AnyOf(Eq(0), Eq(102)));
 }
 
@@ -129,6 +130,7 @@
   SecretValue<int> t;
   t = std::move(s);
   EXPECT_THAT(t.value(), Eq(102));
+  // NOLINTNEXTLINE(bugprone-use-after-move)
   EXPECT_THAT(s.value(), AnyOf(Eq(0), Eq(102)));
 }
 
diff --git a/cc/util/secret_proto.h b/cc/util/secret_proto.h
index af4c9f2..ea67556 100644
--- a/cc/util/secret_proto.h
+++ b/cc/util/secret_proto.h
@@ -61,7 +61,7 @@
     return proto;
   }
 
-  SecretProto() {}
+  SecretProto() = default;
 
   SecretProto(const SecretProto& other) { *value_ = *other.value_; }
 
diff --git a/cc/util/status.cc b/cc/util/status.cc
deleted file mode 100644
index bc7b389..0000000
--- a/cc/util/status.cc
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/util/status.h"
-
-#include <sstream>
-#include <string>
-
-#include "absl/status/status.h"
-#include "absl/strings/str_cat.h"
-
-using ::std::ostream;
-
-namespace crypto {
-namespace tink {
-namespace util {
-
-#ifndef TINK_USE_ABSL_STATUS
-namespace {
-
-
-const Status& GetCancelled() {
-  static const Status* status =
-      new Status(::crypto::tink::util::error::CANCELLED, "");
-  return *status;
-}
-
-const Status& GetUnknown() {
-  static const Status* status =
-      new Status(::crypto::tink::util::error::UNKNOWN, "");
-  return *status;
-}
-
-const Status& GetOk() {
-  static const Status* status = new Status;
-  return *status;
-}
-
-}  // namespace
-
-Status::Status(const ::absl::Status& status)
-    : code_(absl::StatusCode::kOk) {
-  if (status.ok()) return;
-  code_ = status.code();
-  message_ = std::string(status.message());
-}
-
-Status::operator ::absl::Status() const {
-  if (ok()) return ::absl::OkStatus();
-  return ::absl::Status(code_, message_);
-}
-
-Status::Status() : code_(absl::StatusCode::kOk), message_("") {
-}
-
-Status::Status(::crypto::tink::util::error::Code error,
-               const std::string& error_message)
-    : code_(static_cast<absl::StatusCode>(error)), message_(error_message) {
-  if (code_ == absl::StatusCode::kOk) {
-    message_.clear();
-  }
-}
-
-Status::Status(absl::StatusCode code, absl::string_view error_message)
-    : code_(code),
-      message_(error_message) {
-  if (code_ == absl::StatusCode::kOk) {
-    message_.clear();
-  }
-}
-
-Status& Status::operator=(const Status& other) {
-  code_ = other.code_;
-  message_ = other.message_;
-  return *this;
-}
-
-const Status& Status::CANCELLED = GetCancelled();
-const Status& Status::UNKNOWN = GetUnknown();
-const Status& Status::OK = GetOk();
-
-std::string Status::ToString() const {
-  if (code_ == absl::StatusCode::kOk) {
-    return "OK";
-  }
-
-  std::ostringstream oss;
-  oss << code_ << ": " << message_;
-  return oss.str();
-}
-
-std::string ErrorCodeString(crypto::tink::util::error::Code error) {
-  switch (error) {
-    case crypto::tink::util::error::OK:
-      return "OK";
-    case crypto::tink::util::error::CANCELLED:
-      return "CANCELLED";
-    case crypto::tink::util::error::UNKNOWN:
-      return "UNKNOWN";
-    case crypto::tink::util::error::INVALID_ARGUMENT:
-      return "INVALID_ARGUMENT";
-    case crypto::tink::util::error::DEADLINE_EXCEEDED:
-      return "DEADLINE_EXCEEDED";
-    case crypto::tink::util::error::NOT_FOUND:
-      return "NOT_FOUND";
-    case crypto::tink::util::error::ALREADY_EXISTS:
-      return "ALREADY_EXISTS";
-    case crypto::tink::util::error::PERMISSION_DENIED:
-      return "PERMISSION_DENIED";
-    case crypto::tink::util::error::RESOURCE_EXHAUSTED:
-      return "RESOURCE_EXHAUSTED";
-    case crypto::tink::util::error::FAILED_PRECONDITION:
-      return "FAILED_PRECONDITION";
-    case crypto::tink::util::error::ABORTED:
-      return "ABORTED";
-    case crypto::tink::util::error::OUT_OF_RANGE:
-      return "OUT_OF_RANGE";
-    case crypto::tink::util::error::UNIMPLEMENTED:
-      return "UNIMPLEMENTED";
-    case crypto::tink::util::error::INTERNAL:
-      return "INTERNAL";
-    case crypto::tink::util::error::UNAVAILABLE:
-      return "UNAVAILABLE";
-    case crypto::tink::util::error::DATA_LOSS:
-      return "DATA_LOSS";
-    case crypto::tink::util::error::UNAUTHENTICATED:
-      return "UNAUTHENTICATED";
-  }
-  // Avoid using a "default" in the switch, so that the compiler can
-  // give us a warning, but still provide a fallback here.
-  return absl::StrCat(error);
-}
-
-extern ostream& operator<<(ostream& os, crypto::tink::util::error::Code code) {
-  os << ErrorCodeString(code);
-  return os;
-}
-
-extern ostream& operator<<(ostream& os, const Status& other) {
-  os << other.ToString();
-  return os;
-}
-
-#endif  // TINK_USE_ABSL_STATUS
-
-
-}  // namespace util
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/util/status.h b/cc/util/status.h
index a130e2e..ef58949 100644
--- a/cc/util/status.h
+++ b/cc/util/status.h
@@ -20,206 +20,16 @@
 #ifndef TINK_UTIL_STATUS_H_
 #define TINK_UTIL_STATUS_H_
 
-#include <ostream>
-#include <string>
-
-#include "absl/base/attributes.h"
 #include "absl/status/status.h"
 
+#define TINK_USE_ABSL_STATUS
+
 namespace crypto {
 namespace tink {
 namespace util {
 
-#ifndef TINK_USE_ABSL_STATUS
-
-namespace error {
-
-// These values match the error codes in the codes.proto file of the original.
-enum ABSL_DEPRECATED("Prefer using absl::StatusCode instead.") Code {
-  // Not an error; returned on success
-  OK = 0,
-
-  // The operation was cancelled (typically by the caller).
-  CANCELLED = 1,
-
-  // Unknown error.
-  UNKNOWN = 2,
-
-  // Client specified an invalid argument.  Note that this differs
-  // from FAILED_PRECONDITION.  INVALID_ARGUMENT indicates arguments
-  // that are problematic regardless of the state of the system
-  // (e.g., a malformed file name).
-  INVALID_ARGUMENT = 3,
-
-  // Deadline expired before operation could complete.
-  DEADLINE_EXCEEDED = 4,
-
-  // Some requested entity (e.g., file or directory) was not found.
-  NOT_FOUND = 5,
-
-  // Some entity that we attempted to create (e.g., file or directory)
-  // already exists.
-  ALREADY_EXISTS = 6,
-
-  // The caller does not have permission to execute the specified
-  // operation.
-  PERMISSION_DENIED = 7,
-
-  // Some resource has been exhausted, perhaps a per-user quota, or
-  // perhaps the entire file system is out of space.
-  RESOURCE_EXHAUSTED = 8,
-
-  // Operation was rejected because the system is not in a state
-  // required for the operation's execution.  For example, directory
-  // to be deleted may be non-empty, an rmdir operation is applied to
-  // a non-directory, etc.
-  //
-  // A litmus test that may help a service implementor in deciding
-  // between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE:
-  //  (a) Use UNAVAILABLE if the client can retry just the failing call.
-  //  (b) Use ABORTED if the client should retry at a higher-level
-  //      (e.g., restarting a read-modify-write sequence).
-  //  (c) Use FAILED_PRECONDITION if the client should not retry until
-  //      the system state has been explicitly fixed.  E.g., if an "rmdir"
-  //      fails because the directory is non-empty, FAILED_PRECONDITION
-  //      should be returned since the client should not retry unless
-  //      they have first fixed up the directory by deleting files from it.
-  FAILED_PRECONDITION = 9,
-
-  // The operation was aborted, typically due to a concurrency issue
-  // like sequencer check failures, transaction aborts, etc.
-  //
-  // See litmus test above for deciding between FAILED_PRECONDITION,
-  // ABORTED, and UNAVAILABLE.
-  ABORTED = 10,
-
-  // Operation was attempted past the valid range.  E.g., seeking or
-  // reading past end of file.
-  //
-  // Unlike INVALID_ARGUMENT, this error indicates a problem that may
-  // be fixed if the system state changes. For example, a 32-bit file
-  // system will generate INVALID_ARGUMENT if asked to read at an
-  // offset that is not in the range [0,2^32-1], but it will generate
-  // OUT_OF_RANGE if asked to read from an offset past the current
-  // file size.
-  OUT_OF_RANGE = 11,
-
-  // Operation is not implemented or not supported/enabled in this service.
-  UNIMPLEMENTED = 12,
-
-  // Internal errors.  Means some invariants expected by underlying
-  // system has been broken.  If you see one of these errors,
-  // something is very broken.
-  INTERNAL = 13,
-
-  // The service is currently unavailable.  This is a most likely a
-  // transient condition and may be corrected by retrying with
-  // a backoff.
-  //
-  // See litmus test above for deciding between FAILED_PRECONDITION,
-  // ABORTED, and UNAVAILABLE.
-  UNAVAILABLE = 14,
-
-  // Unrecoverable data loss or corruption.
-  DATA_LOSS = 15,
-
-  // Invalid authentication credentials.
-  UNAUTHENTICATED = 16,
-};
-
-}  // namespace error
-
-// TODO(tholenst) Remove this compile time flag in Tink 1.5. This should not be
-// used, except as a temporary measure.
-#ifndef CPP_TINK_TEMPORARY_STATUS_MUST_NOT_USE_RESULT
-class ABSL_MUST_USE_RESULT Status;
-#endif
-
-// A Status is a combination of an error code and a string message (for non-OK
-// error codes).
-class Status {
- public:
-  // Creates an OK status
-  Status();
-
-  // Make a Status from the specified error and message.
-  Status(::crypto::tink::util::error::Code error,
-         const std::string& error_message);
-  // Abseil-compatible constructor from an error and a message
-  Status(absl::StatusCode code, absl::string_view error_message);
-
-  Status(const Status& other) = default;
-
-  Status& operator=(const Status& other);
-
-  // Some pre-defined Status objects
-  ABSL_DEPRECATED("Use OkStatus() instead.")
-  static const Status& OK;  // Identical to 0-arg constructor
-  ABSL_DEPRECATED("Use Status(absl::StatusCode::kCancelled, "") instead.")
-  static const Status& CANCELLED;
-  ABSL_DEPRECATED("Use Status(absl::StatusCode::kUnknown, "") instead.")
-  static const Status& UNKNOWN;
-
-  // Accessors
-  bool ok() const {
-    return code_ == absl::StatusCode::kOk;
-  }
-  ABSL_DEPRECATED("Use its absl-compatible version code() instead.")
-  int error_code() const {
-    return static_cast<int>(code_);
-  }
-  ABSL_DEPRECATED("Use its absl-compatible version code() instead.")
-  ::crypto::tink::util::error::Code CanonicalCode() const {
-    return static_cast<::crypto::tink::util::error::Code>(code_);
-  }
-  ABSL_DEPRECATED("Use its absl-compatible version message() instead.")
-  const std::string& error_message() const { return message_; }
-
-  // Abseil-compatible accessors
-  absl::StatusCode code() const {
-    return static_cast<absl::StatusCode>(code_);
-  }
-  absl::string_view message() const {
-    return message_;
-  }
-
-  bool operator==(const Status& other) const;
-  bool operator!=(const Status& other) const;
-
-  // NoOp
-  void IgnoreError() const {
-  }
-
-  std::string ToString() const;
-
-  Status(const ::absl::Status& status);
-  operator ::absl::Status() const;
-
- private:
-  absl::StatusCode code_;
-  std::string message_;
-};
-
-inline bool Status::operator==(const Status& other) const {
-  return (this->code_ == other.code_) && (this->message_ == other.message_);
-}
-
-inline bool Status::operator!=(const Status& other) const {
-  return !(*this == other);
-}
-
-extern std::string ErrorCodeString(crypto::tink::util::error::Code error);
-
-extern ::std::ostream& operator<<(::std::ostream& os,
-                                  ::crypto::tink::util::error::Code code);
-extern ::std::ostream& operator<<(::std::ostream& os, const Status& other);
-
-#else
-
 using Status = absl::Status;
 
-#endif  // TINK_USE_ABSL_STATUS
-
 // Returns an OK status, equivalent to a default constructed instance.
 inline Status OkStatus() { return Status(); }
 
diff --git a/cc/util/status_test.cc b/cc/util/status_test.cc
deleted file mode 100644
index 4be7542..0000000
--- a/cc/util/status_test.cc
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/util/status.h"
-
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
-#include "absl/status/status.h"
-
-namespace crypto {
-namespace tink {
-namespace util {
-namespace {
-
-#ifndef TINK_USE_ABSL_STATUS
-TEST(StatusTest, CreateNonOkStatusWithAbslStatusCode) {
-  Status util_status = Status(error::Code::CANCELLED, "message");
-  Status absl_status = Status(absl::StatusCode::kCancelled, "message");
-  ASSERT_EQ(util_status, absl_status);
-}
-
-TEST(StatusTest, CreateOkStatusWithAbslStatusCode) {
-  Status util_status = Status(error::Code::OK, "message");
-  Status absl_status = Status(absl::StatusCode::kOk, "message");
-  ASSERT_EQ(util_status, absl_status);
-  ASSERT_EQ(absl_status.message(), "");
-}
-
-TEST(StatusTest, ConvertNonOkStatus) {
-  Status util_status = Status(error::Code::RESOURCE_EXHAUSTED, "message");
-  absl::Status absl_status = util_status;
-  ASSERT_EQ(util_status.code(), absl_status.code());
-  ASSERT_EQ(util_status.message(), absl_status.message());
-}
-
-TEST(StatusTest, ConvertOkStatus) {
-  Status util_status = OkStatus();
-  absl::Status absl_status = util_status;
-  ASSERT_TRUE(absl_status.ok());
-  ASSERT_EQ(absl_status.message(), "");
-}
-#endif
-
-}  // namespace
-}  // namespace util
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/util/statusor.h b/cc/util/statusor.h
index 28cc35e..859cf11 100644
--- a/cc/util/statusor.h
+++ b/cc/util/statusor.h
@@ -17,254 +17,18 @@
 #ifndef TINK_UTIL_STATUSOR_H_
 #define TINK_UTIL_STATUSOR_H_
 
-#include <cstdlib>
-#include <iostream>
-#include <utility>
-
 #include "absl/status/statusor.h"
 #include "tink/util/status.h"
 
+#define TINK_USE_ABSL_STATUSOR
+
 namespace crypto {
 namespace tink {
 namespace util {
 
-#ifndef TINK_USE_ABSL_STATUSOR
-
-#ifndef CPP_TINK_TEMPORARY_STATUS_MUST_NOT_USE_RESULT
-template <typename T>
-class ABSL_MUST_USE_RESULT StatusOr;
-#endif
-
-// TODO(b/122292096): Migrate this to absl::StatusOr
-// A StatusOr holds a Status (in the case of an error), or a value T.
-template <typename T>
-class StatusOr {
- public:
-  // StatusOr<T>::value_type
-  //
-  // This instance data provides a generic `value_type` member for use within
-  // generic programming. This usage is analogous to that of
-  // `optional::value_type` in the case of `std::optional`.
-  using value_type = T;
-
-  using type = T;
-  // Has status UNKNOWN.
-  inline StatusOr();
-
-  // Builds from a non-OK status. Crashes if an OK status is specified.
-  inline StatusOr(const ::crypto::tink::util::Status& status);  // NOLINT
-
-  // Builds from the specified value.
-  inline StatusOr(const T& value);  // NOLINT
-  inline StatusOr(T&& value);       // NOLINT
-
-  // Copy constructor.
-  inline StatusOr(const StatusOr& other);
-
-  // Move constructor.
-  inline StatusOr(StatusOr&& other);
-
-  // Conversion copy constructor, T must be copy constructible from U.
-  template <typename U>
-  inline StatusOr(const StatusOr<U>& other);
-
-  // Assignment operator.
-  inline const StatusOr& operator=(const StatusOr& other);
-
-  // Conversion assignment operator, T must be assignable from U
-  template <typename U>
-  inline const StatusOr& operator=(const StatusOr<U>& other);
-
-  // Accessors.
-  inline const ::crypto::tink::util::Status& status() const {
-    return status_;
-  }
-
-  // Shorthand for status().ok().
-  inline bool ok() const {
-    return status_.ok();
-  }
-
-  // Returns value or crashes if ok() is false.
-  inline const T& ValueOrDie() const& {
-    EnsureOk();
-    return *value_;
-  }
-  inline T& ValueOrDie() & {
-    EnsureOk();
-    return *value_;
-  }
-  inline const T&& ValueOrDie() const&& {
-    EnsureOk();
-    return *std::move(value_);
-  }
-  inline T&& ValueOrDie() && {
-    EnsureOk();
-    return *std::move(value_);
-  }
-
-  // Returns value if ok(), otherwise crashes if exceptions are disabled OR
-  // throws if exceptions are enabled.
-  inline const T& value() const& {
-    if (!ok()) AbortWithMessageFrom(status_);
-    return *value_;
-  }
-  inline T& value() & {
-    if (!ok()) AbortWithMessageFrom(status_);
-    return *value_;
-  }
-  inline const T&& value() const&& {
-    if (!ok()) AbortWithMessageFrom(std::move(status_));
-    return *std::move(value_);
-  }
-  inline T&& value() && {
-    if (!ok()) AbortWithMessageFrom(std::move(status_));
-    return *std::move(value_);
-  }
-
-  // Implicitly convertible to absl::StatusOr. Implicit conversions explicitly
-  // allowed by style arbiter waiver in cl/351594378.
-  operator ::absl::StatusOr<T>() const&;  // NOLINT
-  operator ::absl::StatusOr<T>() &&;      // NOLINT
-
-  // Returns value or crashes if ok() is false.
-  inline const T& operator*() const& {
-    EnsureOk();
-    return *value_;
-  }
-
-  inline T& operator*() & {
-    EnsureOk();
-    return *value_;
-  }
-
-  inline T&& operator*() && {
-    EnsureOk();
-    return *std::move(value_);
-  }
-
-  inline const T&& operator*() const&& {
-    EnsureOk();
-    return *std::move(value_);
-  }
-
-  // Returns reference to value or crashes if ok() is false.
-  T* operator->() {
-    EnsureOk();
-    return &(value_.value());
-  }
-
-  const T* operator->() const {
-    EnsureOk();
-    return &(value_.value());
-  }
-
-  template <typename U>
-  friend class StatusOr;
-
- private:
-  void EnsureOk() const {
-    if (ABSL_PREDICT_FALSE(!ok())) {
-      std::cerr << "Attempting to fetch value of non-OK StatusOr\n";
-      std::cerr << status() << std::endl;
-      std::_Exit(1);
-    }
-  }
-
-  void AbortWithMessageFrom(crypto::tink::util::Status status) const {
-    std::cerr << "Attempting to fetch value instead of handling error\n";
-    std::cerr << status.ToString();
-    std::abort();
-  }
-
-
-  Status status_;
-  absl::optional<T> value_;
-};
-
-// Implementation.
-
-template <typename T>
-inline StatusOr<T>::StatusOr()
-    : status_(absl::StatusCode::kUnknown, "") {
-}
-
-template <typename T>
-inline StatusOr<T>::StatusOr(
-    const ::crypto::tink::util::Status& status) : status_(status) {
-  if (status.ok()) {
-    std::cerr << "::crypto::tink::util::OkStatus() "
-              << "is not a valid argument to StatusOr\n";
-    std::_Exit(1);
-  }
-}
-
-template <typename T>
-inline StatusOr<T>::StatusOr(const T& value) : value_(value) {
-}
-
-template <typename T>
-inline StatusOr<T>::StatusOr(T&& value) : value_(std::move(value)) {
-}
-
-template <typename T>
-inline StatusOr<T>::StatusOr(const StatusOr& other)
-    : status_(other.status_), value_(other.value_) {
-}
-
-template <typename T>
-inline StatusOr<T>::StatusOr(StatusOr&& other)
-    : status_(other.status_), value_(std::move(other.value_)) {
-}
-
-template <typename T>
-template <typename U>
-inline StatusOr<T>::StatusOr(const StatusOr<U>& other)
-    : status_(other.status_), value_(other.value_) {
-}
-
-template <typename T>
-inline const StatusOr<T>& StatusOr<T>::operator=(const StatusOr& other) {
-  status_ = other.status_;
-  if (status_.ok()) {
-    value_ = *other.value_;
-  } else {
-    value_ = absl::nullopt;
-  }
-  return *this;
-}
-
-template <typename T>
-template <typename U>
-inline const StatusOr<T>& StatusOr<T>::operator=(const StatusOr<U>& other) {
-  status_ = other.status_;
-  if (status_.ok()) {
-    value_ = *other.value_;
-  } else {
-    value_ = absl::nullopt;
-  }
-  return *this;
-}
-
-template <typename T>
-StatusOr<T>::operator ::absl::StatusOr<T>() const& {
-  if (!ok()) return ::absl::Status(status_);
-  return *value_;
-}
-
-template <typename T>
-StatusOr<T>::operator ::absl::StatusOr<T>() && {
-  if (!ok()) return ::absl::Status(std::move(status_));
-  return std::move(*value_);
-}
-
-#else
-
 template <typename T>
 using StatusOr = absl::StatusOr<T>;
 
-#endif  // TINK_USE_ABSL_STATUSOR
-
 }  // namespace util
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/util/statusor_test.cc b/cc/util/statusor_test.cc
deleted file mode 100644
index 7aa8583..0000000
--- a/cc/util/statusor_test.cc
+++ /dev/null
@@ -1,242 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/util/statusor.h"
-
-#include <memory>
-#include <string>
-#include <utility>
-
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
-#include "absl/memory/memory.h"
-#include "absl/status/status.h"
-#include "absl/status/statusor.h"
-#include "tink/util/status.h"
-#include "tink/util/test_matchers.h"
-
-namespace crypto {
-namespace tink {
-namespace util {
-namespace {
-
-using ::crypto::tink::test::IsOk;
-using ::testing::Eq;
-using ::testing::Not;
-using ::testing::Pointee;
-
-TEST(StatusOrTest, ConvertOkToAbsl) {
-  StatusOr<int> instance = 1;
-
-  absl::StatusOr<int> converted = instance;
-  ASSERT_TRUE(converted.ok());
-  EXPECT_EQ(*converted, 1);
-}
-
-TEST(StatusOrTest, ConvertErrorToAbsl) {
-  #ifndef TINK_USE_ABSL_STATUS
-  StatusOr<int> instance{
-      Status(error::Code::INVALID_ARGUMENT, "Error message")};
-  #else
-  StatusOr<int> instance{
-      Status(absl::StatusCode::kInvalidArgument, "Error message")};
-  #endif
-
-  absl::StatusOr<int> converted = instance;
-  ASSERT_FALSE(converted.ok());
-  EXPECT_EQ(converted.status().code(), absl::StatusCode::kInvalidArgument);
-  EXPECT_EQ(converted.status().message(), "Error message");
-}
-
-TEST(StatusOrTest, ConvertUncopyableToAbsl) {
-  StatusOr<std::unique_ptr<int>> instance = absl::make_unique<int>(1);
-
-  absl::StatusOr<std::unique_ptr<int>> converted = std::move(instance);
-  ASSERT_TRUE(converted.ok());
-  EXPECT_THAT(*converted, Pointee(Eq(1)));
-}
-
-class NoDefaultConstructor {
- public:
-  explicit NoDefaultConstructor(int i) {}
-
-  NoDefaultConstructor() = delete;
-  NoDefaultConstructor(const NoDefaultConstructor&) = default;
-  NoDefaultConstructor& operator=(const NoDefaultConstructor&) =
-      default;
-  NoDefaultConstructor(NoDefaultConstructor&&) = default;
-  NoDefaultConstructor& operator=(NoDefaultConstructor&&) = default;
-};
-
-// Tests that we can construct a StatusOr<T> even if there is no default
-// constructor for T.
-TEST(StatusOrTest, WithNoDefaultConstructor) {
-  StatusOr<NoDefaultConstructor> value = NoDefaultConstructor(13);
-  StatusOr<NoDefaultConstructor> error =
-      Status(absl::StatusCode::kInvalidArgument, "Error message");
-}
-
-// This tests that when we assign to something which is previously an error,
-// we create a new optional inside the StatusOr, and do not try to assign to
-// the value of the optional instead.
-TEST(StatusOrTest, AssignToErrorStatus) {
-  StatusOr<std::string> error_initially =
-      Status(absl::StatusCode::kInvalidArgument, "Error message");
-  ASSERT_THAT(error_initially, Not(IsOk()));
-  StatusOr<std::string> ok_initially = std::string("Hi");
-  error_initially = ok_initially;
-  ASSERT_THAT(error_initially, IsOk());
-  ASSERT_THAT(error_initially.value(), Eq("Hi"));
-
-#ifndef TINK_USE_ABSL_STATUSOR
-  ASSERT_THAT(error_initially.ValueOrDie(), Eq("Hi"));
-#endif
-}
-
-// This tests that when we assign to something which is previously an error and
-// at the same time use the implicit conversion operator, we create a new
-// optional inside the StatusOr, and do not try to assign to the value of the
-// optional instead.
-TEST(StatusOrTest, AssignToErrorStatusImplicitConvertible) {
-  StatusOr<std::string> error_initially =
-      Status(absl::StatusCode::kInvalidArgument, "Error message");
-  ASSERT_THAT(error_initially, Not(IsOk()));
-  StatusOr<char const*> ok_initially = "Hi";
-  error_initially = ok_initially;
-  ASSERT_THAT(error_initially, IsOk());
-  ASSERT_THAT(error_initially.value(), Eq("Hi"));
-
-#ifndef TINK_USE_ABSL_STATUSOR
-  ASSERT_THAT(error_initially.ValueOrDie(), Eq("Hi"));
-#endif
-}
-
-#ifndef TINK_USE_ABSL_STATUSOR
-TEST(StatusOrTest, MoveOutMoveOnlyValueOrDie) {
-  StatusOr<std::unique_ptr<int>> status_or_unique_ptr_int =
-      absl::make_unique<int>(10);
-  std::unique_ptr<int> ten = std::move(status_or_unique_ptr_int.ValueOrDie());
-  ASSERT_THAT(*ten, Eq(10));
-}
-#endif
-
-TEST(StatusOrTest, MoveOutMoveOnlyValue) {
-  StatusOr<std::unique_ptr<int>> status_or_unique_ptr_int =
-      absl::make_unique<int>(10);
-  std::unique_ptr<int> ten = std::move(status_or_unique_ptr_int.value());
-  ASSERT_THAT(*ten, Eq(10));
-}
-
-TEST(STatusOrTest, CallValueOnConst) {
-  const StatusOr<int> const_status_or_ten = 10;
-  ASSERT_THAT(const_status_or_ten.value(), Eq(10));
-}
-
-TEST(StatusOrTest, CallValueOnConstTemp) {
-  const StatusOr<int> const_status_or_ten = 10;
-  ASSERT_THAT(std::move(const_status_or_ten).value(), Eq(10));
-}
-
-TEST(StatusOrTest, TestValueConst) {
-  const int kI = 4;
-  const absl::StatusOr<int> thing(kI);
-  EXPECT_EQ(kI, *thing);
-}
-
-TEST(StatusOrTest, TestPointerValue) {
-  const int kI = 0;
-  absl::StatusOr<const int*> thing(&kI);
-  EXPECT_EQ(&kI, *thing);
-}
-
-TEST(StatusOrTest, TestPointerValueConst) {
-  const int kI = 0;
-  const absl::StatusOr<const int*> thing(&kI);
-  EXPECT_EQ(&kI, *thing);
-}
-
-TEST(StatusOrTest, OperatorStarRefQualifiers) {
-  static_assert(
-      std::is_same<const int&,
-                   decltype(*std::declval<const absl::StatusOr<int>&>())>(),
-      "Unexpected ref-qualifiers");
-  static_assert(
-      std::is_same<int&, decltype(*std::declval<absl::StatusOr<int>&>())>(),
-      "Unexpected ref-qualifiers");
-  static_assert(
-      std::is_same<const int&&,
-                   decltype(*std::declval<const absl::StatusOr<int>&&>())>(),
-      "Unexpected ref-qualifiers");
-  static_assert(
-      std::is_same<int&&, decltype(*std::declval<absl::StatusOr<int>&&>())>(),
-      "Unexpected ref-qualifiers");
-}
-
-TEST(StatusOrTest, OperatorStar) {
-  const util::StatusOr<std::string> const_lvalue("hello");
-  EXPECT_EQ("hello", *const_lvalue);
-
-  util::StatusOr<std::string> lvalue("hello");
-  EXPECT_EQ("hello", *lvalue);
-
-  // Note: Recall that std::move() is equivalent to a static_cast to an rvalue
-  // reference type.
-  const util::StatusOr<std::string> const_rvalue("hello");
-  EXPECT_EQ("hello", *std::move(const_rvalue));  // NOLINT
-
-  util::StatusOr<std::string> rvalue("hello");
-  EXPECT_EQ("hello", *std::move(rvalue));
-}
-
-TEST(StatusOrTest, OperatorArrowQualifiers) {
-  static_assert(
-      std::is_same<
-          const int*,
-          decltype(std::declval<const util::StatusOr<int>&>().operator->())>(),
-      "Unexpected qualifiers");
-  static_assert(
-      std::is_same<
-          int*, decltype(std::declval<util::StatusOr<int>&>().operator->())>(),
-      "Unexpected qualifiers");
-  static_assert(
-      std::is_same<
-          const int*,
-          decltype(std::declval<const util::StatusOr<int>&&>().operator->())>(),
-      "Unexpected qualifiers");
-  static_assert(
-      std::is_same<
-          int*, decltype(std::declval<util::StatusOr<int>&&>().operator->())>(),
-      "Unexpected qualifiers");
-}
-
-TEST(StatusOrTest, OperatorArrow) {
-  const util::StatusOr<std::string> const_lvalue("hello");
-  EXPECT_EQ(std::string("hello"), const_lvalue->c_str());
-
-  util::StatusOr<std::string> lvalue("hello");
-  EXPECT_EQ(std::string("hello"), lvalue->c_str());
-}
-
-TEST(StatusOr, ElementType) {
-  static_assert(std::is_same<absl::StatusOr<int>::value_type, int>(), "");
-  static_assert(std::is_same<absl::StatusOr<char>::value_type, char>(), "");
-}
-
-}  // namespace
-
-}  // namespace util
-}  // namespace tink
-}  // namespace crypto
diff --git a/cc/util/test_keyset_handle.cc b/cc/util/test_keyset_handle.cc
index f60ca75..dd2de49 100644
--- a/cc/util/test_keyset_handle.cc
+++ b/cc/util/test_keyset_handle.cc
@@ -16,6 +16,7 @@
 
 #include "tink/util/test_keyset_handle.h"
 
+#include <memory>
 #include <utility>
 
 #include "absl/memory/memory.h"
diff --git a/cc/util/test_keyset_handle.h b/cc/util/test_keyset_handle.h
index dc086da..da13136 100644
--- a/cc/util/test_keyset_handle.h
+++ b/cc/util/test_keyset_handle.h
@@ -17,6 +17,8 @@
 #ifndef TINK_UTIL_TEST_KEYSET_HANDLE_H_
 #define TINK_UTIL_TEST_KEYSET_HANDLE_H_
 
+#include <memory>
+
 #include "tink/keyset_handle.h"
 #include "proto/tink.pb.h"
 
diff --git a/cc/util/test_matchers.h b/cc/util/test_matchers.h
index 9f42156..0a83996 100644
--- a/cc/util/test_matchers.h
+++ b/cc/util/test_matchers.h
@@ -17,6 +17,7 @@
 #ifndef TINK_UTIL_TEST_MATCHERS_H_
 #define TINK_UTIL_TEST_MATCHERS_H_
 
+#include <ostream>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -114,7 +115,8 @@
 // Matches a util::StatusOk() value.
 // This is better than EXPECT_TRUE(status.ok())
 // because the error message is a part of the failure messsage.
-MATCHER(IsOk, "is a Status with an OK value") {
+MATCHER(IsOk,
+        absl::StrCat(negation ? "isn't" : "is", " a Status with an OK value")) {
   if (arg.ok()) {
     return true;
   }
@@ -131,26 +133,20 @@
       std::forward<InnerMatcher>(inner_matcher));
 }
 
-// Matches a Status with the specified 'code' as error_code().
-// TODO(lizatretyakova): remove the static_cast and fix the comment above to
-// use code() after all StatusIs usages are migrated to use absl::StatusCode.
+// Matches a Status with the specified 'code' as code().
 MATCHER_P(StatusIs, code,
-          "is a Status with a " +
-              absl::StatusCodeToString(static_cast<absl::StatusCode>(code)) +
-              " code") {
-  if (arg.code() == static_cast<absl::StatusCode>(code)) {
+          "is a Status with a " + absl::StatusCodeToString(code) + " code") {
+  if (arg.code() == code) {
     return true;
   }
   *result_listener << ::testing::PrintToString(arg);
   return false;
 }
 
-// Matches a Status whose error_code() equals 'code', and whose
-// error_message() matches 'message_macher'.
-// TODO(lizatretyakova): remove the static_cast and fix the comment above to
-// use code() after all StatusIs usages are migrated to use absl::StatusCode.
+// Matches a Status whose code() equals 'code', and whose message() matches
+// 'message_macher'.
 MATCHER_P2(StatusIs, code, message_matcher, "") {
-  return (arg.code() == static_cast<absl::StatusCode>(code)) &&
+  return (arg.code() == code) &&
          testing::Matches(message_matcher)(std::string(arg.message()));
 }
 
diff --git a/cc/util/test_util.cc b/cc/util/test_util.cc
index ad6a6d9..2e58dae 100644
--- a/cc/util/test_util.cc
+++ b/cc/util/test_util.cc
@@ -16,15 +16,20 @@
 
 #include "tink/util/test_util.h"
 
-#include <fcntl.h>
 #include <stdarg.h>
 #include <stdlib.h>
-#include <unistd.h>
 
 #include <cmath>
 #include <cstdint>
 #include <cstdlib>
+#include <fstream>
+#include <ios>
+#include <iostream>
+#include <memory>
+#include <ostream>
+#include <sstream>
 #include <string>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
@@ -59,7 +64,6 @@
 using crypto::tink::util::Status;
 using google::crypto::tink::AesGcmKeyFormat;
 using google::crypto::tink::EcdsaPrivateKey;
-using google::crypto::tink::EcdsaSignatureEncoding;
 using google::crypto::tink::EciesAeadHkdfPrivateKey;
 using google::crypto::tink::Ed25519PrivateKey;
 using google::crypto::tink::Keyset;
@@ -69,80 +73,16 @@
 namespace tink {
 namespace test {
 
-int GetTestFileDescriptor(absl::string_view filename, int size,
-                          std::string* file_contents) {
-  (*file_contents) = subtle::Random::GetRandomBytes(size);
-  return GetTestFileDescriptor(filename, *file_contents);
-}
-
-int GetTestFileDescriptor(
-    absl::string_view filename, absl::string_view file_contents) {
-  std::string full_filename =
-      absl::StrCat(crypto::tink::test::TmpDir(), "/", filename);
-  mode_t mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH;
-  int fd = open(full_filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode);
-  if (fd == -1) {
-    std::clog << "Cannot create file " << full_filename
-              << " error: " << errno << std::endl;
+std::string ReadTestFile(absl::string_view filename) {
+  std::string full_filename = absl::StrCat(test::TmpDir(), "/", filename);
+  std::ifstream input_stream(full_filename, std::ios::binary);
+  if (!input_stream) {
+    std::clog << "Cannot open file " << full_filename << std::endl;
     exit(1);
   }
-  auto size = file_contents.size();
-  if (write(fd, file_contents.data(), size) != size) {
-    std::clog << "Failed to write " << size << " bytes to file "
-              << full_filename << " error: " << errno << std::endl;
-
-    exit(1);
-  }
-  close(fd);
-  fd = open(full_filename.c_str(), O_RDONLY);
-  if (fd == -1) {
-    std::clog << "Cannot re-open file " << full_filename
-              << " error: " << errno << std::endl;
-    exit(1);
-  }
-  return fd;
-}
-
-
-int GetTestFileDescriptor(absl::string_view filename) {
-  std::string full_filename =
-      absl::StrCat(crypto::tink::test::TmpDir(), "/", filename);
-  mode_t mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH;
-  int fd = open(full_filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode);
-  if (fd == -1) {
-    std::clog << "Cannot create file " << full_filename
-              << " error: " << errno << std::endl;
-    exit(1);
-  }
-  return fd;
-}
-
-std::string ReadTestFile(std::string filename) {
-  std::string full_filename =
-      absl::StrCat(crypto::tink::test::TmpDir(), "/", filename);
-  int fd = open(full_filename.c_str(), O_RDONLY);
-  if (fd == -1) {
-    std::clog << "Cannot open file " << full_filename
-              << " error: " << errno << std::endl;
-    exit(1);
-  }
-  std::string contents;
-  int buffer_size = 128 * 1024;
-  auto buffer = absl::make_unique<uint8_t[]>(buffer_size);
-  int read_result = read(fd, buffer.get(), buffer_size);
-  while (read_result > 0) {
-    std::clog << "Read " << read_result << " bytes" << std::endl;
-    contents.append(reinterpret_cast<const char*>(buffer.get()), read_result);
-    read_result = read(fd, buffer.get(), buffer_size);
-  }
-  if (read_result < 0) {
-    std::clog << "Error reading file " << full_filename
-              << " error: " << errno << std::endl;
-    exit(1);
-  }
-  close(fd);
-  std::clog << "Read in total " << contents.length() << " bytes" << std::endl;
-  return contents;
+  std::stringstream buffer;
+  buffer << input_stream.rdbuf();
+  return buffer.str();
 }
 
 util::StatusOr<std::string> HexDecode(absl::string_view hex) {
@@ -184,24 +124,25 @@
 }
 
 std::string TmpDir() {
-  // The Bazel 'test' command sets TEST_TMPDIR.
-  const char* env = getenv("TEST_TMPDIR");
-  if (env && env[0] != '\0') {
-    return env;
+  // Try the following environment variables in order:
+  //  - TEST_TMPDIR: Set by `bazel test`.
+  //  - TMPDIR: Set by some Tink tests.
+  //  - TEMP, TMP: Set on Windows; they contain the tmp dir's path.
+  for (const std::string& tmp_env_variable :
+       {"TEST_TMPDIR", "TMPDIR", "TEMP", "TMP"}) {
+    const char* env = getenv(tmp_env_variable.c_str());
+    if (env && env[0] != '\0') {
+      return env;
+    }
   }
-  env = getenv("TMPDIR");
-  if (env && env[0] != '\0') {
-    return env;
-  }
+  // Tmp dir on Linux/macOS.
   return "/tmp";
 }
 
-void AddKeyData(
-    const google::crypto::tink::KeyData& key_data,
-    uint32_t key_id,
-    google::crypto::tink::OutputPrefixType output_prefix,
-    google::crypto::tink::KeyStatusType key_status,
-    google::crypto::tink::Keyset* keyset) {
+void AddKeyData(const google::crypto::tink::KeyData& key_data, uint32_t key_id,
+                google::crypto::tink::OutputPrefixType output_prefix,
+                google::crypto::tink::KeyStatusType key_status,
+                google::crypto::tink::Keyset* keyset) {
   Keyset::Key* key = keyset->add_key();
   key->set_output_prefix_type(output_prefix);
   key->set_key_id(key_id);
diff --git a/cc/util/test_util.h b/cc/util/test_util.h
index 10de4e3..cba8433 100644
--- a/cc/util/test_util.h
+++ b/cc/util/test_util.h
@@ -18,6 +18,8 @@
 #define TINK_UTIL_TEST_UTIL_H_
 
 #include <limits>
+#include <memory>
+#include <ostream>
 #include <string>
 #include <utility>
 
@@ -62,22 +64,8 @@
 // Various utilities for testing.
 ///////////////////////////////////////////////////////////////////////////////
 
-// Creates a new test file with the specified 'filename', writes 'size' random
-// bytes to the file, and returns a file descriptor for reading from the file.
-// A copy of the bytes written to the file is returned in 'file_contents'.
-int GetTestFileDescriptor(absl::string_view filename, int size,
-                          std::string* file_contents);
-
-// Creates a new test file with the specified 'filename', with contents from
-// 'file_contents', and returns a file descriptor for reading from the file.
-int GetTestFileDescriptor(absl::string_view filename,
-                          absl::string_view file_contents);
-
-// Creates a new test file with the specified 'filename', ready for writing.
-int GetTestFileDescriptor(absl::string_view filename);
-
-// Reads the test file specified by 'filename', and returns its contents.
-std::string ReadTestFile(std::string filename);
+// Reads the test file specified by `filename`, and returns its contents.
+std::string ReadTestFile(absl::string_view filename);
 
 // Converts a hexadecimal string into a string of bytes.
 // Returns a status if the size of the input is odd or if the input contains
@@ -493,40 +481,35 @@
     util::Status status_;
   };  // class DummyDecryptingStream
 
-  // Upon first call to PRead() tries to read from 'ct_source' a header
-  // that is expected to be equal to 'expected_header'.  If this
+  // Upon first call to PRead() tries to read from `ct_source` a header
+  // that is expected to be equal to `expected_header`.  If this
   // header matching succeeds, all subsequent method calls are forwarded
-  // to the corresponding methods of 'cd_source'.
+  // to `ct_source->PRead`.
   class DummyDecryptingRandomAccessStream
       : public crypto::tink::RandomAccessStream {
    public:
     DummyDecryptingRandomAccessStream(
         std::unique_ptr<crypto::tink::RandomAccessStream> ct_source,
         absl::string_view expected_header)
-        : ct_source_(std::move(ct_source)),
-          exp_header_(expected_header),
-          status_(util::Status(absl::StatusCode::kUnavailable,
-                               "not initialized")) {}
+        : ct_source_(std::move(ct_source)), exp_header_(expected_header) {}
 
     crypto::tink::util::Status PRead(
         int64_t position, int count,
         crypto::tink::util::Buffer* dest_buffer) override {
-      {  // Initialize, if not initialized yet.
-        absl::MutexLock lock(&status_mutex_);
-        if (status_.code() == absl::StatusCode::kUnavailable) Initialize();
-        if (!status_.ok()) return status_;
+      util::Status status = CheckHeader();
+      if (!status.ok()) {
+        return status;
       }
-      auto status = dest_buffer->set_size(0);
+      status = dest_buffer->set_size(0);
       if (!status.ok()) return status;
       return ct_source_->PRead(position + exp_header_.size(), count,
                                dest_buffer);
     }
 
     util::StatusOr<int64_t> size() override {
-      {  // Initialize, if not initialized yet.
-        absl::MutexLock lock(&status_mutex_);
-        if (status_.code() == absl::StatusCode::kUnavailable) Initialize();
-        if (!status_.ok()) return status_;
+      util::Status status = CheckHeader();
+      if (!status.ok()) {
+        return status;
       }
       auto ct_size_result = ct_source_->size();
       if (!ct_size_result.ok()) return ct_size_result.status();
@@ -536,25 +519,39 @@
     }
 
    private:
-    void Initialize() ABSL_EXCLUSIVE_LOCKS_REQUIRED(status_mutex_) {
+    util::Status CheckHeader()
+        ABSL_LOCKS_EXCLUDED(header_check_status_mutex_) {
+      absl::MutexLock lock(&header_check_status_mutex_);
+      if (header_check_status_.code() != absl::StatusCode::kUnavailable) {
+        return header_check_status_;
+      }
       auto buf = std::move(util::Buffer::New(exp_header_.size()).value());
-      status_ = ct_source_->PRead(0, exp_header_.size(), buf.get());
-      if (!status_.ok() && status_.code() != absl::StatusCode::kOutOfRange)
-        return;
+      header_check_status_ =
+          ct_source_->PRead(0, exp_header_.size(), buf.get());
+      if (!header_check_status_.ok() &&
+          header_check_status_.code() != absl::StatusCode::kOutOfRange) {
+        return header_check_status_;
+      }
+      // EOF or Ok indicate a valid read has happened.
+      header_check_status_ = util::OkStatus();
+      // Invalid header.
       if (buf->size() < exp_header_.size()) {
-        status_ = util::Status(absl::StatusCode::kInvalidArgument,
+        header_check_status_ = util::Status(absl::StatusCode::kInvalidArgument,
                                "Could not read header");
       } else if (memcmp(buf->get_mem_block(), exp_header_.data(),
                         static_cast<int>(exp_header_.size()))) {
-        status_ = util::Status(absl::StatusCode::kInvalidArgument,
+        header_check_status_ = util::Status(absl::StatusCode::kInvalidArgument,
                                "Corrupted header");
       }
+      return header_check_status_;
     }
 
     std::unique_ptr<crypto::tink::RandomAccessStream> ct_source_;
     std::string exp_header_;
-    mutable absl::Mutex status_mutex_;
-    util::Status status_ ABSL_GUARDED_BY(status_mutex_);
+    mutable absl::Mutex header_check_status_mutex_;
+    util::Status header_check_status_
+        ABSL_GUARDED_BY(header_check_status_mutex_) =
+            util::Status(absl::StatusCode::kUnavailable, "Uninitialized");
   };  // class DummyDecryptingRandomAccessStream
 
  private:
@@ -733,7 +730,7 @@
     return {absl::make_unique<DummyAead>(key_uri)};
   }
 
-  ~DummyKmsClient() override {}
+  ~DummyKmsClient() override = default;
 
  private:
   std::string uri_prefix_;
diff --git a/cc/util/test_util_test.cc b/cc/util/test_util_test.cc
index 4b8fdf9..34171c3 100644
--- a/cc/util/test_util_test.cc
+++ b/cc/util/test_util_test.cc
@@ -15,9 +15,24 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/util/test_util.h"
 
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <utility>
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "absl/strings/string_view.h"
+#include "tink/internal/test_random_access_stream.h"
+#include "tink/output_stream.h"
+#include "tink/random_access_stream.h"
 #include "tink/subtle/random.h"
+#include "tink/subtle/test_util.h"
+#include "tink/util/buffer.h"
+#include "tink/util/ostream_output_stream.h"
+#include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 #include "proto/aes_gcm.pb.h"
 #include "proto/tink.pb.h"
@@ -27,6 +42,8 @@
 namespace test {
 namespace {
 
+using ::crypto::tink::internal::TestRandomAccessStream;
+using ::crypto::tink::test::StatusIs;
 using ::google::crypto::tink::AesGcmKey;
 using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
@@ -107,6 +124,170 @@
       IsOk());
 }
 
+TEST(DummyStreamingAead, DummyDecryptingStreamPreadAllAtOnceSucceeds) {
+  const int stream_size = 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+
+  auto ostream = std::make_unique<std::ostringstream>();
+  auto string_stream_buffer = ostream->rdbuf();
+  auto output_stream =
+      std::make_unique<util::OstreamOutputStream>(std::move(ostream));
+
+  DummyStreamingAead streaming_aead("Some AEAD");
+  util::StatusOr<std::unique_ptr<OutputStream>> encrypting_output_stream =
+      streaming_aead.NewEncryptingStream(std::move(output_stream), "Some AAD");
+  ASSERT_THAT(encrypting_output_stream.status(), IsOk());
+  ASSERT_THAT(subtle::test::WriteToStream(
+                  encrypting_output_stream.value().get(), stream_content),
+              IsOk());
+
+  std::string ciphertext = string_stream_buffer->str();
+  auto test_random_access_stream =
+      std::make_unique<TestRandomAccessStream>(ciphertext);
+  util::StatusOr<std::unique_ptr<RandomAccessStream>>
+      decrypting_random_access_stream =
+          streaming_aead.NewDecryptingRandomAccessStream(
+              std::move(test_random_access_stream), "Some AAD");
+  ASSERT_THAT(decrypting_random_access_stream.status(), IsOk());
+
+  auto buffer = util::Buffer::New(ciphertext.size());
+  EXPECT_THAT((*decrypting_random_access_stream)
+                  ->PRead(/*position=*/0, ciphertext.size(), buffer->get()),
+              StatusIs(absl::StatusCode::kOutOfRange));
+  EXPECT_EQ(stream_content,
+            std::string((*buffer)->get_mem_block(), (*buffer)->size()));
+}
+
+TEST(DummyStreamingAead, DummyDecryptingStreamPreadInChunksSucceeds) {
+  const int stream_size = 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+
+  auto ostream = std::make_unique<std::ostringstream>();
+  auto string_stream_buffer = ostream->rdbuf();
+  auto output_stream =
+      std::make_unique<util::OstreamOutputStream>(std::move(ostream));
+
+  DummyStreamingAead streaming_aead("Some AEAD");
+  util::StatusOr<std::unique_ptr<OutputStream>> encrypting_output_stream =
+      streaming_aead.NewEncryptingStream(std::move(output_stream), "Some AAD");
+  ASSERT_THAT(encrypting_output_stream.status(), IsOk());
+  ASSERT_THAT(subtle::test::WriteToStream(
+                  encrypting_output_stream.value().get(), stream_content),
+              IsOk());
+
+  std::string ciphertext = string_stream_buffer->str();
+  auto test_random_access_stream =
+      std::make_unique<TestRandomAccessStream>(ciphertext);
+  util::StatusOr<std::unique_ptr<RandomAccessStream>>
+      decrypting_random_access_stream =
+          streaming_aead.NewDecryptingRandomAccessStream(
+              std::move(test_random_access_stream), "Some AAD");
+  ASSERT_THAT(decrypting_random_access_stream.status(), IsOk());
+
+  int chunk_size = 10;
+  auto buffer = util::Buffer::New(chunk_size);
+  std::string plaintext;
+  int64_t position = 0;
+  util::Status status = (*decrypting_random_access_stream)
+                            ->PRead(position, chunk_size, buffer->get());
+  while (status.ok()) {
+    plaintext.append((*buffer)->get_mem_block(), (*buffer)->size());
+    position += (*buffer)->size();
+    status = (*decrypting_random_access_stream)
+                 ->PRead(position, chunk_size, buffer->get());
+  }
+  EXPECT_THAT(status, StatusIs(absl::StatusCode::kOutOfRange));
+  plaintext.append((*buffer)->get_mem_block(), (*buffer)->size());
+  EXPECT_EQ(stream_content, plaintext);
+}
+
+TEST(DummyStreamingAead, DummyDecryptingStreamPreadWithSmallerHeaderFails) {
+  const int stream_size = 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+
+  auto ostream = std::make_unique<std::ostringstream>();
+  auto output_stream =
+      std::make_unique<util::OstreamOutputStream>(std::move(ostream));
+
+  constexpr absl::string_view kStreamingAeadName = "Some AEAD";
+  constexpr absl::string_view kStreamingAeadAad = "Some associated data";
+
+  DummyStreamingAead streaming_aead(kStreamingAeadName);
+  util::StatusOr<std::unique_ptr<OutputStream>> encrypting_output_stream =
+      streaming_aead.NewEncryptingStream(std::move(output_stream),
+                                         kStreamingAeadAad);
+  ASSERT_THAT(encrypting_output_stream.status(), IsOk());
+  ASSERT_THAT(subtle::test::WriteToStream(
+                  encrypting_output_stream.value().get(), stream_content),
+              IsOk());
+  // Stream content size is too small; DummyDecryptingStream expects
+  // absl::StrCat(kStreamingAeadName, kStreamingAeadAad).
+  std::string ciphertext = "Invalid header";
+  auto test_random_access_stream =
+      std::make_unique<TestRandomAccessStream>(ciphertext);
+  util::StatusOr<std::unique_ptr<RandomAccessStream>>
+      decrypting_random_access_stream =
+          streaming_aead.NewDecryptingRandomAccessStream(
+              std::move(test_random_access_stream), kStreamingAeadAad);
+  ASSERT_THAT(decrypting_random_access_stream.status(), IsOk());
+
+  int chunk_size = 10;
+  auto buffer = util::Buffer::New(chunk_size);
+  EXPECT_THAT(
+      (*decrypting_random_access_stream)
+          ->PRead(/*position=*/0, chunk_size, buffer->get()),
+      StatusIs(absl::StatusCode::kInvalidArgument, "Could not read header"));
+  EXPECT_THAT(
+      (*decrypting_random_access_stream)
+          ->PRead(/*position=*/0, chunk_size, buffer->get()),
+      StatusIs(absl::StatusCode::kInvalidArgument, "Could not read header"));
+  EXPECT_THAT(
+      (*decrypting_random_access_stream)->size().status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, "Could not read header"));
+}
+
+TEST(DummyStreamingAead, DummyDecryptingStreamPreadWithCorruptedAadFails) {
+  const int stream_size = 1024;
+  std::string stream_content = subtle::Random::GetRandomBytes(stream_size);
+
+  auto ostream = std::make_unique<std::ostringstream>();
+  auto string_stream_buffer = ostream->rdbuf();
+  auto output_stream =
+      std::make_unique<util::OstreamOutputStream>(std::move(ostream));
+
+  constexpr absl::string_view kStreamingAeadName = "Some AEAD";
+  constexpr absl::string_view kStreamingAeadAad = "Some associated data";
+
+  DummyStreamingAead streaming_aead(kStreamingAeadName);
+  util::StatusOr<std::unique_ptr<OutputStream>> encrypting_output_stream =
+      streaming_aead.NewEncryptingStream(std::move(output_stream),
+                                         kStreamingAeadAad);
+  ASSERT_THAT(encrypting_output_stream.status(), IsOk());
+  ASSERT_THAT(subtle::test::WriteToStream(
+                  encrypting_output_stream.value().get(), stream_content),
+              IsOk());
+  // Invalid associated data.
+  std::string ciphertext = string_stream_buffer->str();
+  auto test_random_access_stream =
+      std::make_unique<TestRandomAccessStream>(ciphertext);
+  util::StatusOr<std::unique_ptr<RandomAccessStream>>
+      decrypting_random_access_stream =
+          streaming_aead.NewDecryptingRandomAccessStream(
+              std::move(test_random_access_stream), "Some wrong AAD");
+  ASSERT_THAT(decrypting_random_access_stream.status(), IsOk());
+
+  int chunk_size = 10;
+  auto buffer = util::Buffer::New(chunk_size);
+  EXPECT_THAT((*decrypting_random_access_stream)
+                  ->PRead(/*position=*/0, chunk_size, buffer->get()),
+              StatusIs(absl::StatusCode::kInvalidArgument, "Corrupted header"));
+  EXPECT_THAT((*decrypting_random_access_stream)
+                  ->PRead(/*position=*/0, chunk_size, buffer->get()),
+              StatusIs(absl::StatusCode::kInvalidArgument, "Corrupted header"));
+  EXPECT_THAT((*decrypting_random_access_stream)->size().status(),
+              StatusIs(absl::StatusCode::kInvalidArgument, "Corrupted header"));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace tink
diff --git a/cc/util/validation_test.cc b/cc/util/validation_test.cc
index 1d5b01c..db7d2d2 100644
--- a/cc/util/validation_test.cc
+++ b/cc/util/validation_test.cc
@@ -16,6 +16,8 @@
 
 #include "tink/util/validation.h"
 
+#include <limits>
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
diff --git a/cc/version_script.lds b/cc/version_script.lds
index fbee2ab..6684cbf 100644
--- a/cc/version_script.lds
+++ b/cc/version_script.lds
@@ -1,4 +1,4 @@
-VERS_1.7.0 {
+VERS_2.0.0 {
   global:
     *tink*;
     *absl*;
diff --git a/cmake/HttpArchive.cmake b/cmake/HttpArchive.cmake
index d13a103..75fd0d3 100644
--- a/cmake/HttpArchive.cmake
+++ b/cmake/HttpArchive.cmake
@@ -12,21 +12,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-include(ExternalProject)
+include(FetchContent)
 include(CMakeParseArguments)
 
-if (NOT DEFINED TINK_THIRD_PARTY_DIR)
-  set(TINK_THIRD_PARTY_DIR "${CMAKE_CURRENT_BINARY_DIR}/__third_party")
-endif()
-
 # Download, unpack and configure a dependency.
 #
 # The project is added as a subdirectory of Tink, unless DATA_ONLY is
 # specified. This makes all target defined by it available as dependencies.
 #
-# This rule also defines a <NAME>_SOURCE_DIR variable, which points to the
-# root directory of the downloaded package and can be used to reference data in
-# tests, or append extra include/link paths in the Workspace file.
+# This rule also defines two variables:
+#   - <NAME>_SOURCE_DIR points to the root directory of the downloaded package;
+#     it can be used to reference data in tests, or append extra include/link
+#     paths in the Workspace file.
+#   - <NAME>_BINARY_DIR points to the build directory.
 #
 # Parameters:
 #   NAME name of the dependency.
@@ -49,40 +47,29 @@
     "NAME;URL;SHA256;CMAKE_SUBDIR"
     "CMAKE_ARGS"
   )
-
+  FetchContent_Declare(
+    ${http_archive_NAME}
+    URL       ${http_archive_URL}
+    URL_HASH  SHA256=${http_archive_SHA256}
+  )
   message(STATUS "Fetching ${http_archive_NAME}")
-
-  set(http_archive_PREFIX "${TINK_THIRD_PARTY_DIR}/${http_archive_NAME}")
-  set(http_archive_SOURCE_DIR "${http_archive_PREFIX}/src")
-  set(http_archive_BINARY_DIR "${http_archive_PREFIX}/build")
-
-  configure_file(
-    cmake/HttpArchiveDownloader.cmake.in
-    "${http_archive_PREFIX}/CMakeLists.txt")
-
-  execute_process(
-    COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" .
-    RESULT_VARIABLE errors
-    WORKING_DIRECTORY "${http_archive_PREFIX}")
-
-  if (errors)
-    message(FATAL_ERROR "While configuring ${http_archive_NAME}: ${errors}")
-  endif()
-
-  set(${http_archive_NAME}_SOURCE_DIR "${http_archive_SOURCE_DIR}" PARENT_SCOPE)
-
-  execute_process(
-    COMMAND ${CMAKE_COMMAND} --build .
-    RESULT_VARIABLE errors
-    WORKING_DIRECTORY "${http_archive_PREFIX}")
-
-  if (errors)
-    message(FATAL_ERROR "While fetching ${http_archive_NAME}: ${errors}")
-  endif()
-
-  if (NOT http_archive_DATA_ONLY)
-    add_subdirectory(
-      "${http_archive_SOURCE_DIR}/${http_archive_CMAKE_SUBDIR}"
-      "${http_archive_BINARY_DIR}" EXCLUDE_FROM_ALL)
+  FetchContent_GetProperties(${http_archive_NAME})
+  if(NOT ${http_archive_NAME}_POPULATED)
+    FetchContent_Populate(${http_archive_NAME})
+    if (NOT http_archive_DATA_ONLY)
+      add_subdirectory(
+        ${${http_archive_NAME}_SOURCE_DIR}/${http_archive_CMAKE_SUBDIR}
+        ${${http_archive_NAME}_BINARY_DIR}
+        EXCLUDE_FROM_ALL)
+    endif()
+    # Expose these variables to the caller.
+    set(
+      "${http_archive_NAME}_SOURCE_DIR"
+      "${${http_archive_NAME}_SOURCE_DIR}"
+      PARENT_SCOPE)
+    set(
+      "${http_archive_NAME}_BINARY_DIR"
+      "${${http_archive_NAME}_BINARY_DIR}"
+      PARENT_SCOPE)
   endif()
 endfunction(http_archive)
diff --git a/cmake/HttpArchiveDownloader.cmake.in b/cmake/HttpArchiveDownloader.cmake.in
deleted file mode 100644
index 3aac815..0000000
--- a/cmake/HttpArchiveDownloader.cmake.in
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This template is used by http_archive() to download, unpack and configure Tink
-# dependencies. You shouldn't need to use it directly.
-
-cmake_minimum_required(VERSION 3.5)
-project(http-archive-${http_archive_NAME})
-
-include(ExternalProject)
-
-ExternalProject_Add(${http_archive_NAME}
-  URL "${http_archive_URL}"
-  URL_HASH SHA256=${http_archive_SHA256}
-  TLS_VERIFY true
-  SOURCE_DIR "${http_archive_SOURCE_DIR}"
-  BINARY_DIR "${http_archive_BINARY_DIR}"
-  SOURCE_SUBDIR "${http_archive_CMAKE_SUBDIR}"
-  CMAKE_ARGS ${http_archive_CMAKE_ARGS}
-  CONFIGURE_COMMAND ""
-  BUILD_COMMAND ""
-  TEST_COMMAND ""
-  INSTALL_COMMAND ""
-  ${http_archive_EXTRA_OPTIONS}
-  EXCLUDE_FROM_ALL
-)
diff --git a/cmake/TinkBuildRules.cmake b/cmake/TinkBuildRules.cmake
index 2a6ef1f..9fd4328 100644
--- a/cmake/TinkBuildRules.cmake
+++ b/cmake/TinkBuildRules.cmake
@@ -43,7 +43,7 @@
 endif()
 
 if (NOT DEFINED TINK_CXX_STANDARD)
-  set(TINK_CXX_STANDARD 11)
+  set(TINK_CXX_STANDARD 14)
   if (DEFINED CMAKE_CXX_STANDARD_REQUIRED AND CMAKE_CXX_STANDARD_REQUIRED AND DEFINED CMAKE_CXX_STANDARD)
     set(TINK_CXX_STANDARD ${CMAKE_CXX_STANDARD})
   endif()
@@ -54,6 +54,7 @@
 set(TINK_IDE_FOLDER "Tink")
 
 set(TINK_TARGET_EXCLUDE_IF_OPENSSL "exclude_if_openssl")
+set(TINK_TARGET_EXCLUDE_IF_WINDOWS "exclude_if_windows")
 
 # Declare the beginning of a new Tink library namespace.
 #
@@ -74,10 +75,12 @@
 # a way to organise code and speed up compilation.
 #
 # Arguments:
-#   NAME base name of the target. See below for target naming conventions.
-#   SRCS list of source files, including headers.
-#   DEPS list of dependency targets.
-#   PUBLIC flag, signal that this target is intended for external use.
+#   NAME      base name of the target. See below for target naming conventions.
+#   SRCS      list of source files, including headers.
+#   DEPS      list of dependency targets.
+#   PUBLIC    flag, signals that this target is intended for external use.
+#   TESTONLY  flag, signals that this target should be ignored if
+#             TINK_BUILD_TESTS=OFF.
 #
 # If SRCS contains only headers, an INTERFACE rule is created. This rule carries
 # include path and link library information, but is not directly buildable.
@@ -94,22 +97,30 @@
 #
 function(tink_cc_library)
   cmake_parse_arguments(PARSE_ARGV 0 tink_cc_library
-    "PUBLIC"
+    "PUBLIC;TESTONLY"
     "NAME"
     "SRCS;DEPS;TAGS"
   )
 
+  if (tink_cc_library_TESTONLY AND NOT TINK_BUILD_TESTS)
+    return()
+  endif()
+
   if (NOT DEFINED TINK_MODULE)
     message(FATAL_ERROR
             "TINK_MODULE not defined, perhaps you are missing a tink_module() statement?")
   endif()
 
-  # Check if this target must be skipped. Currently the only reason for this to
-  # happen is incompatibility with OpenSSL, when used.
+  # Check if this target must be skipped.
   foreach(_tink_cc_library_tag ${tink_cc_library_TAGS})
+    # Exclude if we use OpenSSL.
     if (${_tink_cc_library_tag} STREQUAL ${TINK_TARGET_EXCLUDE_IF_OPENSSL} AND TINK_USE_SYSTEM_OPENSSL)
       return()
     endif()
+    # Exclude if building on Windows.
+    if (${_tink_cc_library_tag} STREQUAL ${TINK_TARGET_EXCLUDE_IF_WINDOWS} AND WIN32)
+      return()
+    endif()
   endforeach()
 
   # We replace :: with __ in targets, because :: may not appear in target names.
@@ -157,10 +168,10 @@
 # Declare a Tink test using googletest, with a syntax similar to Bazel.
 #
 # Parameters:
-#   NAME base name of the test.
-#   SRCS list of test source files, headers included.
-#   DEPS list of dependencies, see tink_cc_library above.
-#   DATA list of non-code dependencies, such as test vectors.
+#   NAME  base name of the test.
+#   SRCS  list of test source files, headers included.
+#   DEPS  list of dependencies, see tink_cc_library above.
+#   DATA  list of non-code dependencies, such as test vectors.
 #
 # Tests added with this macro are automatically registered.
 # Each test produces a build target named tink_test_<MODULE>_<NAME>.
@@ -180,12 +191,16 @@
     message(FATAL_ERROR "TINK_MODULE not defined")
   endif()
 
-  # Check if this target must be skipped. Currently the only reason for this to
-  # happen is incompatibility with OpenSSL, when used.
+  # Check if this target must be skipped.
   foreach(_tink_cc_test_tag ${tink_cc_test_TAGS})
+    # Exclude if we use OpenSSL.
     if (${_tink_cc_test_tag} STREQUAL ${TINK_TARGET_EXCLUDE_IF_OPENSSL} AND TINK_USE_SYSTEM_OPENSSL)
       return()
     endif()
+    # Exclude if building on Windows.
+    if (${_tink_cc_test_tag} STREQUAL ${TINK_TARGET_EXCLUDE_IF_WINDOWS} AND WIN32)
+      return()
+    endif()
   endforeach()
 
   # We replace :: with __ in targets, because :: may not appear in target names.
@@ -276,8 +291,8 @@
 # to group dependencies that are logically related and give them a single name.
 #
 # Parameters:
-#   NAME base name of the target.
-#   DEPS list of dependencies to group.
+#   NAME  base name of the target.
+#   DEPS  list of dependencies to group.
 #
 # Each tink_target_group produces a target named tink_<MODULE>_<NAME>.
 function(tink_target_group)
diff --git a/cmake/TinkWorkspace.cmake b/cmake/TinkWorkspace.cmake
index 2404a5d..f871893 100644
--- a/cmake/TinkWorkspace.cmake
+++ b/cmake/TinkWorkspace.cmake
@@ -50,27 +50,40 @@
 
 set(gtest_force_shared_crt ON CACHE BOOL "Tink dependency override" FORCE)
 
-if (NOT TINK_USE_INSTALLED_GOOGLETEST)
+if (TINK_BUILD_TESTS)
+  if (TINK_USE_INSTALLED_GOOGLETEST)
+    # This uses the CMake's FindGTest module; if successful, this call to
+    # find_package generates the targets GTest::gmock, GTest::gtest and
+    # GTest::gtest_main.
+    find_package(GTest CONFIG REQUIRED)
+    _create_interface_target(gmock GTest::gmock)
+    _create_interface_target(gtest_main GTest::gtest_main)
+  else()
+    http_archive(
+      NAME googletest
+      URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz
+      SHA256 b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5
+    )
+  endif()
+
   http_archive(
-    NAME com_google_googletest
-    URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.tar.gz
-    SHA256 b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5
+    NAME wycheproof
+    URL https://github.com/google/wycheproof/archive/d8ed1ba95ac4c551db67f410c06131c3bc00a97c.zip
+    SHA256 eb1d558071acf1aa6d677d7f1cabec2328d1cf8381496c17185bd92b52ce7545
+    DATA_ONLY
   )
-else()
-  # This uses the CMake's FindGTest module; if successful, this call to
-  # find_package generates the targets GTest::gmock, GTest::gtest and
-  # GTest::gtest_main.
-  find_package(GTest CONFIG REQUIRED)
-  _create_interface_target(gmock GTest::gmock)
-  _create_interface_target(gtest_main GTest::gtest_main)
+  # Symlink the Wycheproof test data.
+  # Tests expect Wycheproof test vectors to be in a local testvectors/ folder.
+  add_directory_alias("${wycheproof_SOURCE_DIR}/testvectors"
+    "${CMAKE_BINARY_DIR}/testvectors")
 endif()
 
 if (NOT TINK_USE_INSTALLED_ABSEIL)
-  # Commit from 2021-12-03
+  # Release from 2023-05-04.
   http_archive(
-    NAME com_google_absl
-    URL https://github.com/abseil/abseil-cpp/archive/9336be04a242237cd41a525bedfcf3be1bb55377.zip
-    SHA256 368be019fc8d69a566ac2cf7a75262d5ba8f6409e3ef3cdbcf0106bdeb32e91c
+    NAME abseil
+    URL https://github.com/abseil/abseil-cpp/archive/refs/tags/20230125.3.zip
+    SHA256 51d676b6846440210da48899e4df618a357e6e44ecde7106f1e44ea16ae8adc7
   )
 else()
   # This is everything that needs to be done here. Abseil already defines its
@@ -78,31 +91,32 @@
   find_package(absl REQUIRED)
 endif()
 
-http_archive(
-  NAME wycheproof
-  URL https://github.com/google/wycheproof/archive/d8ed1ba95ac4c551db67f410c06131c3bc00a97c.zip
-  SHA256 eb1d558071acf1aa6d677d7f1cabec2328d1cf8381496c17185bd92b52ce7545
-  DATA_ONLY
-)
-
-# Symlink the Wycheproof test data.
-# Paths are hard-coded in tests, which expects wycheproof/ in this location.
-add_directory_alias("${wycheproof_SOURCE_DIR}" "${CMAKE_BINARY_DIR}/external/wycheproof")
-
-if (NOT TINK_USE_SYSTEM_OPENSSL)
-  http_archive(
-    NAME boringssl
-    URL https://github.com/google/boringssl/archive/88cdf7dd2dbce1ecb9057c183095103d83373abe.zip
-    SHA256 24092815136f956069fcfa5172166ad4e025166ce6fe500420c9e3e3c4f3da38
-    CMAKE_SUBDIR src
-  )
-
-  # BoringSSL targets do not carry include directory info, this fixes it.
-  target_include_directories(crypto PUBLIC "${boringssl_SOURCE_DIR}/src/include")
+# Don't fetch BoringSSL or look for OpenSSL if target `crypto` is already
+# defined.
+if (NOT TARGET crypto)
+  if (NOT TINK_USE_SYSTEM_OPENSSL)
+    # Commit from Feb 15, 2023.
+    # NOTE: This is one commit ahead of Bazel; the commit fixes a CMake issue,
+    # which made build fail on CMake 3.10.
+    # See https://github.com/google/boringssl/compare/5c22014...e27ff0e.
+    http_archive(
+      NAME boringssl
+      URL https://github.com/google/boringssl/archive/e27ff0e4312c91357778b36bbd8a7ec7bfc67be3.zip
+      SHA256 11d3c87906bed215a915b0db11cefd0fc7b939ddbec4952a29e343a83ce3bc50
+      CMAKE_SUBDIR src
+    )
+    # BoringSSL targets do not carry include directory info, this fixes it.
+    target_include_directories(crypto PUBLIC
+      "$<BUILD_INTERFACE:${boringssl_SOURCE_DIR}/src/include>")
+  else()
+    # Support for ED25519 was added from 1.1.1.
+    find_package(OpenSSL 1.1.1 REQUIRED)
+    _create_interface_target(crypto OpenSSL::Crypto)
+  endif()
 else()
-  # Support for ED25519 was added from 1.1.1.
-  find_package(OpenSSL 1.1.1 REQUIRED)
-  _create_interface_target(crypto OpenSSL::Crypto)
+  message(STATUS "Using an already declared `crypto` target")
+  get_target_property(crypto_INCLUDE_DIR crypto INTERFACE_INCLUDE_DIRECTORIES)
+  message(STATUS "crypto Include Dir: ${crypto_INCLUDE_DIR}")
 endif()
 
 set(RAPIDJSON_BUILD_DOC OFF CACHE BOOL "Tink dependency override" FORCE)
@@ -114,17 +128,16 @@
   URL https://github.com/Tencent/rapidjson/archive/v1.1.0.tar.gz
   SHA256 bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e
 )
-
 # Rapidjson is a header-only library with no explicit target. Here we create one.
 add_library(rapidjson INTERFACE)
 target_include_directories(rapidjson INTERFACE "${rapidjson_SOURCE_DIR}")
 
 set(protobuf_BUILD_TESTS OFF CACHE BOOL "Tink dependency override" FORCE)
 set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "Tink dependency override" FORCE)
-
+## Use protobuf X.21.9.
 http_archive(
   NAME com_google_protobuf
-  URL https://github.com/protocolbuffers/protobuf/archive/v3.19.3.zip
-  SHA256 6b6bf5cd8d0cca442745c4c3c9f527c83ad6ef35a405f64db5215889ac779b42
+  URL https://github.com/protocolbuffers/protobuf/archive/v21.9.zip
+  SHA256 5babb8571f1cceafe0c18e13ddb3be556e87e12ceea3463d6b0d0064e6cc1ac3
   CMAKE_SUBDIR cmake
 )
diff --git a/docs/CMAKE-HOWTO.md b/docs/CMAKE-HOWTO.md
index 91e30eb..95d5664 100644
--- a/docs/CMAKE-HOWTO.md
+++ b/docs/CMAKE-HOWTO.md
@@ -13,10 +13,10 @@
 source tree has been copied in the `third_party/tink` directory of your project,
 your top-level CMake script should look like this:
 
-    cmake_minimum_required(VERSION 3.5)
+    cmake_minimum_required(VERSION 3.13)
     project(YourProject CXX)
     set(CMAKE_CXX_STANDARD_REQUIRED ON)
-    set(CMAKE_CXX_STANDARD 11)
+    set(CMAKE_CXX_STANDARD 14)
 
     add_subdirectory(third_party/tink)
 
@@ -25,9 +25,9 @@
 
 NOTES:
 
-*   You need at least CMake 3.5 to build Tink and its dependencies.
+*   You need at least CMake 3.13 to build Tink and its dependencies.
 *   Tink defines the C++ standard to use via the `TINK_CXX_STANDARD` variable,
-    which is `11` by default. If you want to propagate to the value of
+    which is `14` by default. If you want to propagate to the value of
     `CMAKE_CXX_STANDARD` to Tink use `set(CMAKE_CXX_STANDARD_REQUIRED ON)`.
 
 Include Tink headers in `your_app.cc` as follows:
diff --git a/docs/CPP-HOWTO.md b/docs/CPP-HOWTO.md
index 7e237c3..dac6f46 100644
--- a/docs/CPP-HOWTO.md
+++ b/docs/CPP-HOWTO.md
@@ -11,9 +11,9 @@
 
 ### Bazel
 
-Using Tink in projects built with Bazel is straightforward and is the recommended
-approach. For reference, see [the C++
-examples](https://github.com/google/tink/tree/master/examples/cc).
+Using Tink in projects built with Bazel is straightforward and is the
+recommended approach. For reference, see
+[the C++ examples](https://github.com/google/tink/tree/master/cc/examples).
 
 ### CMake
 
diff --git a/docs/GOLANG-HOWTO.md b/docs/GOLANG-HOWTO.md
index 8f25527..15b8935 100644
--- a/docs/GOLANG-HOWTO.md
+++ b/docs/GOLANG-HOWTO.md
@@ -115,36 +115,37 @@
 )
 
 func main() {
-  // Generate a new key.
-  kh1, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+  // Generate a new keyset handle.
+  handle1, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
   if err != nil {
     log.Fatal(err)
   }
 
-  // Fetch the master key from a KMS.
+  // Get the key encryption AEAD from a KMS.
   gcpClient, err := gcpkms.NewClientWithCredentials(keyURI, credentialsPath)
   if err != nil {
     log.Fatal(err)
   }
   registry.RegisterKMSClient(gcpClient)
-  masterKey, err := gcpClient.GetAEAD(keyURI)
+  keyEncryptionAEAD, err := gcpClient.GetAEAD(keyURI)
   if err != nil {
     log.Fatal(err)
   }
 
-  // An io.Reader and io.Writer implementation which simply writes to memory.
-  memKeyset := &keyset.MemReaderWriter{}
-
-  // Write encrypts the keyset handle with the master key and writes to the
-  // io.Writer implementation (memKeyset). We recommend that you encrypt the
-  // keyset handle before persisting it.
-  if err := kh1.Write(memKeyset, masterKey); err != nil {
+  // Serialize and encrypt the keyset handle using the key encryption AEAD.
+  // We strongly recommend that you encrypt the keyset handle before persisting
+  // it.
+  buf := new(bytes.Buffer)
+  writer := keyset.NewBinaryWriter(buf)
+  err = handle1.Write(writer, keyEncryptionAEAD)
+  if err != nil {
     log.Fatal(err)
   }
+  encryptedHandle := buf.Bytes()
 
-  // Read reads the encrypted keyset handle back from the io.Reader
-  // implementation and decrypts it using the master key.
-  kh2, err := keyset.Read(memKeyset, masterKey)
+  // Decrypt and parse the encrypted keyset using the key encryption AEAD.
+  reader := keyset.NewBinaryReader(bytes.NewReader(encryptedHandle))
+  handle2, err := keyset.Read(reader, keyEncryptionAEAD)
   if err != nil {
     log.Fatal(err)
   }
diff --git a/docs/JAVA-HOWTO.md b/docs/JAVA-HOWTO.md
index b502318..21056a0 100644
--- a/docs/JAVA-HOWTO.md
+++ b/docs/JAVA-HOWTO.md
@@ -1,134 +1,23 @@
 # Tink for Java HOW-TO
 
 This document contains instructions and Java code snippets for common tasks in
-[Tink](https://github.com/google/tink).
+[Tink](https://github.com/tink-crypto/tink-java).
 
 If you want to contribute code to the Java implementation, please read the [Java
 hacking guide](JAVA-HACKING.md).
 
 ## Setup instructions
 
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09.
-
-In addition to the versioned releases, snapshots of Tink are regularly built
-using the master branch of the Tink GitHub repository.
-
-Tink for Java has two primary build targets specified:
-
-- "tink": the default, for general purpose use
-- "android": which is optimized for use in Android projects
-
-### Maven
-
-You can can include Tink in Java projects projects using
-[Maven](https://maven.apache.org/).
-
-The Maven group ID is `com.google.crypto.tink`, and the artifact ID is `tink`.
-
-You can specify the current release of Tink as a project dependency using the
-following configuration:
-
-```xml
-<dependencies>
-  <dependency>
-    <groupId>com.google.crypto.tink</groupId>
-    <artifactId>tink</artifactId>
-    <version>1.7.0</version>
-  </dependency>
-</dependencies>
-```
-
-You can specify the latest snapshot as a project dependency by using the version
-`HEAD-SNAPSHOT`:
-
-```xml
-<repositories>
-  <repository>
-    <id>sonatype-snapshots</id>
-    <name>sonatype-snapshots</name>
-    <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
-    <snapshots>
-      <enabled>true</enabled>
-      <updatePolicy>always</updatePolicy>
-    </snapshots>
-    <releases>
-      <updatePolicy>always</updatePolicy>
-    </releases>
-  </repository>
-</repositories>
-
-...
-
-<dependencies>
-  <dependency>
-    <groupId>com.google.crypto.tink</groupId>
-    <artifactId>tink</artifactId>
-    <version>HEAD-SNAPSHOT</version>
-  </dependency>
-</dependencies>
-```
-
-### AWS/GCP integration
-
-Since 1.3.0 the support for AWS/GCP KMS has been moved to a separate package. To
-use AWS KMS, one should also add dependency on `tink-awskms`, and similarly
-`tink-gcpkms` for GCP KMS.
-
-```xml
-<dependencies>
-  <dependency>
-    <groupId>com.google.crypto.tink</groupId>
-    <artifactId>tink-awskms</artifactId>
-    <version>1.7.0</version>
-  </dependency>
-</dependencies>
-```
-
-```xml
-<dependencies>
-  <dependency>
-    <groupId>com.google.crypto.tink</groupId>
-    <artifactId>tink-gcpkms</artifactId>
-    <version>1.7.0</version>
-  </dependency>
-</dependencies>
-```
-
-### Gradle
-
-You can include Tink in Android projects using [Gradle](https://gradle.org).
-
-You can specify the current release of Tink as a project dependency using the
-following configuration:
-
-```
-dependencies {
-  implementation 'com.google.crypto.tink:tink-android:1.7.0'
-}
-```
-
-You can specify the latest snapshot as a project dependency using the following
-configuration:
-
-```
-repositories {
-    maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
-}
-
-dependencies {
-  implementation 'com.google.crypto.tink:tink-android:HEAD-SNAPSHOT'
-}
-```
+See https://developers.devsite.corp.google.com/tink/tink-setup#java for setup
+instructions.
 
 ## API documentation
 
 *   Java:
-    *   [1.7.0](https://google.github.io/tink/javadoc/tink/1.7.0)
+    *   [1.9.0](https://google.github.io/tink/javadoc/tink/1.9.0)
     *   [HEAD-SNAPSHOT](https://google.github.io/tink/javadoc/tink/HEAD-SNAPSHOT)
 *   Android:
-    *   [1.7.0](https://google.github.io/tink/javadoc/tink-android/1.7.0)
+    *   [1.9.0](https://google.github.io/tink/javadoc/tink-android/1.9.0)
     *   [HEAD-SNAPSHOT](https://google.github.io/tink/javadoc/tink-android/HEAD-SNAPSHOT)
 
 ## Important warnings
@@ -188,97 +77,48 @@
 
 Still, if there is a need to generate a KeysetHandle with fresh key material
 directly in Java code, you can use
-[`KeysetHandle`](https://github.com/google/tink/blob/master/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java).
+[`KeysetHandle`](https://github.com/tink-crypto/tink-java/blob/main/src/main/java/com/google/crypto/tink/KeysetHandle.java).
 For example, you can generate a keyset containing a randomly generated
 AES128-GCM key as follows.
 
 ```java
-    import com.google.crypto.tink.KeyTemplates;
     import com.google.crypto.tink.KeysetHandle;
+    import com.google.crypto.tink.aead.PredefinedAeadParameters;
 
     KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("AES128_GCM"));
+        PredefinedAeadParameters.AES128_GCM);
 ```
 
-## Storing keysets
+## Serializing keysets
 
-After generating key material, you might want to persist it to a storage system,
-e.g., writing to a file:
+After generating key material, you might want to serialize it in order to
+persist it to a storage system, e.g., writing to a file.
 
 ```java
-    import com.google.crypto.tink.CleartextKeysetHandle;
+    import com.google.crypto.tink.InsecureSecretKeyAccess;
     import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
-    import com.google.crypto.tink.JsonKeysetWriter;
-    import java.io.File;
+    import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+    import com.google.crypto.tink.aead.PredefinedAeadParameters;
+    import java.nio.Files;
 
     // Generate the key material...
     KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("AES128_GCM"));
+        PredefinedAeadParameters.AES128_GCM);
 
-    // and write it to a file.
+    // and serialize it to a string.
     String keysetFilename = "my_keyset.json";
-    CleartextKeysetHandle.write(keysetHandle, JsonKeysetWriter.withFile(
-        new File(keysetFilename)));
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get());
 ```
 
-Storing cleartext keysets on disk is not recommended. Tink supports encrypting
-keysets with master keys stored in remote [key management
-systems](KEY-MANAGEMENT.md).
+Parsing can be done with `TinkJsonProtoKeysetFormat.parseKeyset`. If the keyset
+has no secret key material, the method `serializeKeysetWithoutSecret` can be
+used (which does not require `InsecureSecretKeyAccess`).
 
-For example, you can encrypt the key material with a key stored in Google Cloud
-KMS key as follows:
-
-```java
-    import com.google.crypto.tink.JsonKeysetWriter;
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
-    import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
-    import java.io.File;
-
-    // Generate the key material...
-    KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("AES128_GCM"));
-
-    // and write it to a file...
-    String keysetFilename = "my_keyset.json";
-    // encrypted with the this key in GCP KMS
-    String masterKeyUri = "gcp-kms://projects/tink-examples/locations/global/keyRings/foo/cryptoKeys/bar";
-    keysetHandle.write(JsonKeysetWriter.withFile(new File(keysetFilename)),
-        new GcpKmsClient().getAead(masterKeyUri));
-```
-
-## Loading existing keysets
-
-To load encrypted keysets, use
-[`KeysetHandle`](https://github.com/google/tink/blob/master/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java):
-
-```java
-    import com.google.crypto.tink.JsonKeysetReader;
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.integration.awskms.AwsKmsClient;
-    import java.io.File;
-
-    String keysetFilename = "my_keyset.json";
-    // The keyset is encrypted with the this key in AWS KMS.
-    String masterKeyUri = "aws-kms://arn:aws:kms:us-east-1:007084425826:key/84a65985-f868-4bfc-83c2-366618acf147";
-    KeysetHandle keysetHandle = KeysetHandle.read(
-        JsonKeysetReader.withFile(new File(keysetFilename)),
-        new AwsKmsClient().getAead(masterKeyUri));
-```
-
-To load cleartext keysets, use
-[`CleartextKeysetHandle`](https://github.com/google/tink/blob/master/java_src/src/main/java/com/google/crypto/tink/CleartextKeysetHandle.java):
-
-```java
-    import com.google.crypto.tink.CleartextKeysetHandle;
-    import com.google.crypto.tink.KeysetHandle;
-    import java.io.File;
-
-    String keysetFilename = "my_keyset.json";
-    KeysetHandle keysetHandle = CleartextKeysetHandle.read(
-        JsonKeysetReader.withFile(new File(keysetFilename)));
-```
+Storing keysets unencrypted on disk is not recommended. Tink supports encrypting
+keysets with master keys stored in remote key management systems, see for
+example
+https://developers.devsite.corp.google.com/tink/client-side-encryption#java.
 
 ## Obtaining and using primitives
 
@@ -305,12 +145,11 @@
 
 ```java
     import com.google.crypto.tink.Aead;
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
+    import com.google.crypto.tink.aead.PredefinedAeadParameters;
 
     // 1. Generate the key material.
     KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("AES128_GCM"));
+        PredefinedAeadParameters.AES128_GCM);
 
     // 2. Get the primitive.
     Aead aead = keysetHandle.getPrimitive(Aead.class);
@@ -330,13 +169,12 @@
 encrypt or decrypt data:
 
 ```java
-    import com.google.crypto.tink.DeterministicAead;
+    import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
     import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
 
     // 1. Generate the key material.
     KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("AES256_SIV"));
+        PredefinedDeterministicAeadParameters.AES256_SIV);
 
     // 2. Get the primitive.
     DeterministicAead daead =
@@ -351,160 +189,21 @@
 
 ### Symmetric key encryption of streaming data
 
-You can obtain and use a
-[Streaming AEAD](PRIMITIVES.md#streaming-authenticated-encryption-with-associated-data)
-(Streaming Authenticated Encryption with Associated Data) primitive to encrypt
-or decrypt data streams:
-
-```java
-    import com.google.crypto.tink.StreamingAead;
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
-    import java.nio.ByteBuffer;
-    import java.nio.channels.FileChannel;
-    import java.nio.channels.SeekableByteChannel;
-    import java.nio.channels.WritableByteChannel;
-
-    // 1. Generate the key material.
-    KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("AES128_GCM_HKDF_1MB"));
-
-    // 2. Get the primitive.
-    StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
-
-    // 3. Use the primitive to encrypt some data and write the ciphertext to a file,
-    FileChannel ciphertextDestination =
-        new FileOutputStream(ciphertextFileName).getChannel();
-    byte[] aad = ...
-    WritableByteChannel encryptingChannel =
-        streamingAead.newEncryptingChannel(ciphertextDestination, aad);
-    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
-    while ( bufferContainsDataToEncrypt ) {
-      int r = encryptingChannel.write(buffer);
-      // Try to get into buffer more data for encryption.
-    }
-    // Complete the encryption (process the remaining plaintext, if any, and close the channel).
-    encryptingChannel.close();
-
-    // ... or to decrypt an existing ciphertext stream.
-    FileChannel ciphertextSource =
-        new FileInputStream(ciphertextFileName).getChannel();
-    byte[] aad = ...
-    ReadableByteChannel decryptingChannel =
-        s.newDecryptingChannel(ciphertextSource, aad);
-    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
-    do {
-      buffer.clear();
-      int cnt = decryptingChannel.read(buffer);
-      if (cnt > 0) {
-        // Process cnt bytes of plaintext.
-      } else if (read == -1) {
-        // End of plaintext detected.
-        break;
-      } else if (read == 0) {
-        // No ciphertext is available at the moment.
-      }
-   }
-```
+See
+https://developers.devsite.corp.google.com/tink/encrypt-large-files-or-data-streams#java
 
 ### Message Authentication Code
 
-You can compute or verify a [MAC](PRIMITIVES.md#message-authentication-code)
-(Message Authentication Code):
-
-```java
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
-    import com.google.crypto.tink.Mac;
-
-    // 1. Generate the key material.
-    KeysetHandle keysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("HMAC_SHA256_128BITTAG"));
-
-    // 2. Get the primitive.
-    Mac mac = keysetHandle.getPrimitive(Mac.class);
-
-    // 3. Use the primitive to compute a tag,
-    byte[] tag = mac.computeMac(data);
-
-    // ... or to verify a tag.
-    mac.verifyMac(tag, data);
-```
+See
+https://developers.devsite.corp.google.com/tink/protect-data-from-tampering#java
 
 ### Digital signatures
 
-You can sign or verify a [digital
-signature](PRIMITIVES.md#digital-signatures):
-
-```java
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
-    import com.google.crypto.tink.PublicKeySign;
-    import com.google.crypto.tink.PublicKeyVerify;
-
-    // SIGNING
-
-    // 1. Generate the private key material.
-    KeysetHandle privateKeysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("ECDSA_P256"));
-
-    // 2. Get the primitive.
-    PublicKeySign signer = privateKeysetHandle.getPrimitive(PublicKeySign.class);
-
-    // 3. Use the primitive to sign.
-    byte[] signature = signer.sign(data);
-
-    // VERIFYING
-
-    // 1. Obtain a handle for the public key material.
-    KeysetHandle publicKeysetHandle =
-        privateKeysetHandle.getPublicKeysetHandle();
-
-    // 2. Get the primitive.
-    PublicKeyVerify verifier = publicKeysetHandle.getPrimitive(PublicKeyVerify.class);
-
-    // 4. Use the primitive to verify.
-    verifier.verify(signature, data);
-```
+See https://developers.devsite.corp.google.com/tink/digitally-sign-data
 
 ### Hybrid encryption
 
-To encrypt or decrypt using [a combination of public key encryption and
-symmetric key encryption](PRIMITIVES.md#hybrid-encryption) one can
-use the following:
-
-```java
-    import com.google.crypto.tink.HybridDecrypt;
-    import com.google.crypto.tink.HybridEncrypt;
-    import com.google.crypto.tink.KeysetHandle;
-    import com.google.crypto.tink.KeyTemplates;
-
-    // 1. Generate the private key material.
-    KeysetHandle privateKeysetHandle = KeysetHandle.generateNew(
-        KeyTemplates.get("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM"));
-
-    // Obtain the public key material.
-    KeysetHandle publicKeysetHandle =
-        privateKeysetHandle.getPublicKeysetHandle();
-
-    // ENCRYPTING
-
-    // 2. Get the primitive.
-    HybridEncrypt hybridEncrypt =
-        publicKeysetHandle.getPrimitive(HybridEncrypt.class);
-
-    // 3. Use the primitive.
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, contextInfo);
-
-    // DECRYPTING
-
-    // 2. Get the primitive.
-    HybridDecrypt hybridDecrypt = privateKeysetHandle.getPrimitive(
-        HybridDecrypt.class);
-
-    // 3. Use the primitive.
-    byte[] plaintext = hybridDecrypt.decrypt(ciphertext, contextInfo);
-```
+See https://developers.devsite.corp.google.com/tink/exchange-data#java
 
 ### Envelope encryption
 
@@ -545,33 +244,29 @@
 ## Key rotation
 
 Support for key rotation in Tink is provided via the
-[`KeysetManager`](https://github.com/google/tink/blob/master/java_src/src/main/java/com/google/crypto/tink/KeysetManager.java)
+[`KeysetHandle.Builder`](https://github.com/tink-crypto/tink-java/blob/main/src/main/java/com/google/crypto/tink/KeysetHandle.java)
 class.
 
 You have to provide a `KeysetHandle`-object that contains the keyset that should
 be rotated, and a specification of the new key via a
-[`KeyTemplate`](https://github.com/google/tink/blob/master/proto/tink.proto#L50)
-message.
+[`Parameters`](https://github.com/tink-crypto/tink-java/blob/main/src/main/java/com/google/crypto/tink/Parameters.java)
+object.
 
 ```java
-    import com.google.crypto.tink.KeyTemplate;
-    import com.google.crypto.tink.KeyTemplates;
     import com.google.crypto.tink.KeysetHandle;
     import com.google.crypto.tink.KeysetManager;
 
     KeysetHandle keysetHandle = ...;   // existing keyset
-    KeyTemplate keyTemplate = KeyTemplates.get("AES256_GCM"); // template for the new key
-
-    KeysetHandle rotatedKeysetHandle = KeysetManager
-        .withKeysetHandle(keysetHandle)
-        .rotate(keyTemplate)
-        .getKeysetHandle();
+    KeysetHandle.Builder builder = KeysetHandle.newBuilder(keysetHandle);
+    builder.addEntry(KeysetHandle.generateEntryFromParameters(
+      ChaCha20Poly1305Parameters.create()).withRandomId());
+    KeysetHandle keysetHandleWithAdditionalEntry = builder.build();
 ```
 
 After a successful rotation, the resulting keyset contains a new key generated
-according to the specification in `keyTemplate`, and the new key becomes the
-_primary key_ of the keyset.  For the rotation to succeed the `Registry` must
-contain a key manager for the key type specified in `keyTemplate`.
+according to the specification in the parameters object. For the rotation to
+succeed the `Registry` must contain a key manager for the key type specified in
+`keyTemplate`.
 
 Alternatively, you can use [Tinkey](TINKEY.md) to rotate or manage a keyset.
 
diff --git a/docs/JWT-HOWTO.md b/docs/JWT-HOWTO.md
index fdb947e..417c0ec 100644
--- a/docs/JWT-HOWTO.md
+++ b/docs/JWT-HOWTO.md
@@ -170,7 +170,6 @@
 
 Here are some small examples on how to use Tink's JWT library:
 
-* [examples/cc/jwt](https://github.com/google/tink/tree/master/examples/cc/jwt)
-* [examples/java_src/jwt](https://github.com/google/tink/tree/master/examples/java_src/jwt)
-* [examples/python/jwt](https://github.com/google/tink/tree/master/examples/python/jwt)
-
+* [examples/cc/jwt](https://github.com/google/tink/tree/master/cc/examples/jwt)
+* [examples/java_src/jwt](https://github.com/google/tink/tree/master/java_src/examples/jwt)
+* [examples/python/jwt](https://github.com/google/tink/tree/master/python/examples/jwt)
diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md
index 7378857..3bbe819 100644
--- a/docs/KNOWN-ISSUES.md
+++ b/docs/KNOWN-ISSUES.md
@@ -1,88 +1,3 @@
 # Known Issues in Tink
 
-This doc lists known issues in Tink. Please report new issues by opening new
-tickets or emailing the maintainers at `[email protected]`.
-
-## C++
-
-*   Before 1.4.0, AES-CTR-HMAC-AEAD keys and the
-    [EncryptThenAuthenticate](https://github.com/google/tink/blob/master/cc/subtle/encrypt_then_authenticate.cc)
-    subtle implementation may be vulnerable to chosen-ciphertext attacks. An
-    attacker can generate ciphertexts that bypass the HMAC verification if and
-    only if all of the following conditions are true:
-
-    -   Tink C++ is used on systems where `size_t` is a 32-bit integer. This is
-        usually the case on 32-bit machines.
-    -   The attacker can specify long (>= 2^29 bytes ~ 536MB) associated data.
-
-    This issue was reported by Quan Nguyen of Snap security team.
-
-## Java
-
-*   Tink supports Java 8 or newer. Java 7 support was removed since 1.4.0.
-
-*   Tink is built on top of Java security providers, but, via
-    [Project Wycheproof](https://github.com/google/wycheproof), we found many
-    security issues in popular providers. Tink provides countermeasures for most
-    problems, and we've also helped upstream fix many issues. Still, there are
-    some issues in old providers that we cannot fix. We recommend using Tink
-    with the latest version of Conscrypt, Oracle JDK, OpenJDK or Bouncy Castle.
-    If you cannot use the latest version, you might want to avoid using ECDSA
-    (alternative: ED25519) or AES-GCM (alternatives: AES-EAX, AES-CTR-HMAC-AEAD
-    or XChaCha20-Poly1305).
-
-## Android
-
-*   The minimum API level that Tink supports is 19 (Android KitKat). This covers
-    more than 90% of all Android phones. Tink hasn't been tested on older
-    versions. It might or might not work. Drop us a line if you really need to
-    support ancient Android phones.
-
-*   On Android Marshmallow (API level 23) or older, the
-    `newSeekableDecryptingChannel` method in implementations of `StreamingAead`
-    doesn't work. It depends on
-    [SeekableByteChannel](https://developer.android.com/reference/java/nio/channels/SeekableByteChannel.html),
-    which is only available on API level 24 or newer. Users should use
-    `newEncryptingStream` instead.
-
-*   On Android Lollipop (API level 21) or older, `AndroidKeysetManager` does not
-    support wrapping keysets with Android Keystore, but it'd store keysets in
-    cleartext in private preference. This is secure enough for most
-    applications.
-
-*   On Android KitKat (API level 19) without [Google Play
-    Services](https://developers.google.com/android/guides/overview), `AES-GCM`
-    does not work properly because KitKat uses Bouncy Castle 1.48 which doesn't
-    support updateAAD. If Google Play Services is present, `AES-GCM` should work
-    well. If you want to support all Android versions, without depending on
-    Google Play Services, please use `CHACHA20-POLY1305`, `AES-EAX`, or
-    `AES-CTR-HMAC-AEAD`.
-
-## Signature malleability
-
-*   ECDSA signatures are malleable. You probably can ignore this issue, unless
-    you're working on Bitcoin or cryptocurrencies and have to worry about
-    [transaction
-    malleability](https://en.bitcoin.it/wiki/Transaction_malleability). In that
-    case you want to use ED25519 signatures which are non-malleable.
-
-## Envelope encryption - Benign malleability
-
-*   Envelope encryption uses a third-party provider (e.g. GCP, AWS) to encrypt
-    the *data encryption key (DEK)*. It is possible to modify certain parts of
-    the *encrypted DEK* without detection when using *KmsEnvelopeAead* with
-    *AwsKmsAead* or *GcpKmsAead* as the remote provider. This is due to some
-    metadata being included (for instance version numbers) which is not
-    authenticated and modifications are not detected by the provider. Note that
-    this violates the CCA2 property for this interface, although the ciphertext
-    will still decrypt to the correct DEK. When using this interface one should
-    not rely on that for each DEK there only exists a single *encrypted DEK*.
-
-## Streaming AEAD - potential integer overflow issues
-
-*   Streaming AEAD implementations encrypt the plaintext in segments. Tink uses
-    a 4-byte segment counter. When encrypting a stream consisting of more than
-    2^32 segments, the segment counter might overflow and lead to leakage of key
-    material or plaintext. This problem was found in the Java and Go
-    implementations of the AES-GCM-HKDF-Streaming key type, and has been fixed
-    since 1.4.0.
+See https://developers.google.com/tink/known-issues.
diff --git a/docs/PRIMITIVES.md b/docs/PRIMITIVES.md
index 8fb0e23..841562d 100644
--- a/docs/PRIMITIVES.md
+++ b/docs/PRIMITIVES.md
@@ -27,45 +27,44 @@
 
 ### Primitives supported by language
 
-**Primitive**      | **Java** | **C++** | **Objective-C** | **Go** | **Python**
------------------- | :------: | :-----: | :-------------: | :----: | :--------:
-AEAD               | yes      | yes     | yes             | yes    | yes
-Streaming AEAD     | yes      | yes     | **no**          | yes    | yes
-Deterministic AEAD | yes      | yes     | yes             | yes    | yes
-MAC                | yes      | yes     | yes             | yes    | yes
-PRF                | yes      | yes     | **no**          | yes    | yes
-Digital signatures | yes      | yes     | yes             | yes    | yes
-Hybrid encryption  | yes      | yes     | yes             | yes    | yes
+**Primitive**      | **Java** | **C++** | **Objective-C** | **Go** | **Python** | **TypeScript**
+------------------ | :------: | :-----: | :-------------: | :----: | :--------: | :------------:
+AEAD               | yes      | yes     | yes             | yes    | yes        | yes
+Streaming AEAD     | yes      | yes     | **no**          | yes    | yes        | **no**
+Deterministic AEAD | yes      | yes     | yes             | yes    | yes        | **no**
+MAC                | yes      | yes     | yes             | yes    | yes        | **no**
+PRF                | yes      | yes     | **no**          | yes    | yes        | **no**
+Digital signatures | yes      | yes     | yes             | yes    | yes        | yes
+Hybrid encryption  | yes      | yes     | yes             | yes    | yes        | yes
 
-JavaScript is currently under development.
+TypeScript is still under development.
 
 ### Primitive implementations supported by language
 
-| **Primitive**      | **Implementation**                | **Java** | **C++ (BoringSSL)** | **C++ (OpenSSL)** | **Objective-C** | **Go**     | **Python** |
-| ------------------ | --------------------------------- | :------: | :-----------------: | :---------------: | :-------------: | :--------: | :--------: |
-| AEAD               | AES-GCM                           | yes      | yes                 | yes               | yes             | yes        | yes        |
-|                    | AES-GCM-SIV                       | yes      | yes                 | **no**            | **no**          | **no**     | **no**     |
-|                    | AES-CTR-HMAC                      | yes      | yes                 | yes               | yes             | yes        | yes        |
-|                    | AES-EAX                           | yes      | yes                 | yes               | yes             | **no**     | yes        |
-|                    | KMS Envelope                      | yes      | yes                 | yes               | **no**          | yes        | yes        |
-|                    | CHACHA20-POLY1305                 | yes      | **no**              | **no**            | **no**          | yes        | **no**     |
-|                    | XCHACHA20-POLY1305                | yes      | yes                 | **no**            | yes             | yes        | yes        |
-| Streaming AEAD     | AES-GCM-HKDF-STREAMING            | yes      | yes                 | yes               | **no**          | yes        | yes        |
-|                    | AES-CTR-HMAC-STREAMING            | yes      | yes                 | yes               | **no**          | yes        | yes        |
-| Deterministic AEAD | AES-SIV                           | yes      | yes                 | yes               | yes             | yes        | yes        |
-| MAC                | HMAC-SHA2                         | yes      | yes                 | yes               | yes             | yes        | yes        |
-|                    | AES-CMAC                          | yes      | yes                 | yes               | yes             | yes        | yes        |
-| PRF                | HKDF-SHA2                         | yes      | yes                 | yes               | **no**          | yes        | yes        |
-|                    | HMAC-SHA2                         | yes      | yes                 | yes               | **no**          | yes        | yes        |
-|                    | AES-CMAC                          | yes      | yes                 | yes               | **no**          | yes        | yes        |
-| Digital Signatures | ECDSA over NIST curves            | yes      | yes                 | yes \*            | yes             | yes        | yes        |
-|                    | Ed25519                           | yes      | yes                 | yes               | yes             | yes        | yes        |
-|                    | RSA-SSA-PKCS1                     | yes      | yes                 | yes               | yes             | **no**     | yes        |
-|                    | RSA-SSA-PSS                       | yes      | yes                 | yes               | yes             | **no**     | yes        |
-| Hybrid Encryption  | HPKE                              | yes      | yes                 | **no**            | **no**          | yes        | yes        |
-|                    | ECIES with AEAD                   | yes      | yes                 | yes               | yes             | yes        | yes        |
-|                    | ECIES with DeterministicAEAD      | yes      | yes                 | yes               | **no**          | yes        | yes        |
-|                    | HKDF                              | yes      | yes                 | yes               | yes             | yes        | yes        |
+| **Primitive**       | **Implementation**                    | **Java** | **C++ (BoringSSL)** | **C++ (OpenSSL)** | **Objective-C** | **Go** | **Python** | **TypeScript**
+| ------------------- | ------------------------------------- | :------: | :-----------------: | :---------------: | :-------------: | :----: | :--------: | :------------:
+| AEAD                | AES-GCM                               | yes      | yes                 | yes               | yes             | yes    | yes        | yes
+|                     | AES-GCM-SIV                           | yes      | yes                 | **no**            | **no**          | **no** | **no**     | **no**
+|                     | AES-CTR-HMAC                          | yes      | yes                 | yes               | yes             | yes    | yes        | yes
+|                     | AES-EAX                               | yes      | yes                 | yes               | yes             | **no** | yes        | **no**
+|                     | KMS Envelope                          | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
+|                     | CHACHA20-POLY1305                     | yes      | **no**              | **no**            | **no**          | yes    | **no**     | **no**
+|                     | XCHACHA20-POLY1305                    | yes      | yes                 | **no**            | yes             | yes    | yes        | **no**
+| Streaming AEAD      | AES-GCM-HKDF-STREAMING                | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
+|                     | AES-CTR-HMAC-STREAMING                | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
+| Deterministic AEAD  | AES-SIV                               | yes      | yes                 | yes               | yes             | yes    | yes        | **no**
+| MAC                 | HMAC-SHA2                             | yes      | yes                 | yes               | yes             | yes    | yes        | **no**
+|                     | AES-CMAC                              | yes      | yes                 | yes               | yes             | yes    | yes        | **no**
+| PRF                 | HKDF-SHA2                             | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
+|                     | HMAC-SHA2                             | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
+|                     | AES-CMAC                              | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
+| Digital Signatures  | ECDSA over NIST curves                | yes      | yes                 | yes \*            | yes             | yes    | yes        | yes
+|                     | Ed25519                               | yes      | yes                 | yes               | yes             | yes    | yes        | **no**
+|                     | RSA-SSA-PKCS1                         | yes      | yes                 | yes               | yes             | yes    | yes        | **no**
+|                     | RSA-SSA-PSS                           | yes      | yes                 | yes               | yes             | yes    | yes        | **no**
+| Hybrid Encryption   | HPKE                                  | yes      | yes                 | **no**            | **no**          | yes    | yes        | **no**
+|                     | ECIES with AEAD and HKDF              | yes      | yes                 | yes               | yes             | yes    | yes        | yes
+|                     | ECIES with DeterministicAEAD and HKDF | yes      | yes                 | yes               | **no**          | yes    | yes        | **no**
 
 \* EC key creation from seed (`DeriveKey`) is unsupported.
 
diff --git a/docs/TINKEY.md b/docs/TINKEY.md
index 9c4804f..1fc9f17 100644
--- a/docs/TINKEY.md
+++ b/docs/TINKEY.md
@@ -45,17 +45,20 @@
 
 Available commands:
 
+*   `help`: Prints a help message for all available commands.
 *   `add-key`: Generates and adds a new key to a keyset.
 *   `convert-keyset`: Changes format, encrypts, decrypts a keyset.
 *   `create-keyset`: Creates a new keyset.
 *   `create-public-keyset`: Creates a public keyset from a private keyset.
 *   `list-key-templates`: Lists all supported key templates.
 *   `delete-key`: Deletes a specified key in a keyset.
+*   `destroy-key`: Destroys the key material of a specified key in a keyset.
 *   `disable-key`: Disables a specified key in a keyset.
 *   `enable-key`: Enables a specified key in a keyset.
 *   `list-keyset`: Lists keys in a keyset.
 *   `promote-key`: Promotes a specified key to primary.
-*   `rotate-keyset`: Performs a key rotation in a keyset.
+*   `rotate-keyset`: *Deprecated.* Rotate keysets in two steps using the commands
+    `add-key` and later `promote-key`.
 
 To obtain info about arguments available/required for a command, run `tinkey
 <command>` without further arguments.
@@ -66,18 +69,11 @@
 tinkey create-keyset --key-template ECDSA_P256 --out private-keyset.cfg
 ```
 
--   Add a new key to a keyset
+-   Add a new key to a keyset. This does not change the primary key.
 
 ```shell
 tinkey add-key --key-template ECDSA_P512 --in private-keyset.cfg \
---out private-keyset.cfg
-```
-
--   Rotate a keyset by adding a primary key
-
-```shell
-tinkey rotate-keyset --key-template ED25519 --in private-keyset.cfg \
---out private-keyset.cfg
+  --out new-private-keyset.cfg
 ```
 
 -   List metadata of keys in a keyset:
@@ -86,12 +82,25 @@
 tinkey list-keyset --in private-keyset.cfg
 ```
 
+-   Make the key with key ID 1234567890 the primary key. (You can get the key ID
+    using the `list-keyset` command.)
+
+```shell
+tinkey promote-key --in private-keyset.cfg --key-id 1234567890 \
+  --out new-private-keyset.cfg
+```
+
 -   Create a public keyset from a private keyset
 
 ```shell
 tinkey create-public-keyset --in private-keyset.cfg --out public-keyset.cfg
 ```
 
+Note: tinkey does not allow you to overwrite the keyset to prevent accidental
+loss of existing keys. We recommend you always create a new keyset and
+only delete the old keyset when you are certain that the new keyset works.
+
+
 ## Work with Key Management System (KMS)
 
 Tinkey can encrypt or decrypt keysets with master keys residing in remote KMSes.
@@ -176,18 +185,20 @@
 --new-master-key-uri gcp-kms://projects/tink-examples/locations/global/keyRings/foo/cryptoKeys/bar
 ```
 
--   Add a new key to an encrypted keyset, using default credentials
+-   Add a new key to an encrypted keyset, using default credentials. This does
+    not change the primary key.
 
 ```shell
 tinkey add-key --key-template AES256_GCM --in encrypted-keyset.cfg \
---out encrypted-keyset.cfg \
+  --out new-encrypted-keyset.cfg \
 --master-key-uri gcp-kms://projects/tink-examples/locations/global/keyRings/foo/cryptoKeys/bar
 ```
 
--   Rotate an encrypted keyset by adding a primary key
+-   Make the key with key ID 1234567890 the primary key in an encrypted keyset.
+    (You can get the key ID using the `list-keyset` command.)
 
 ```shell
-tinkey rotate-keyset --key-template AES256_GCM --in encrypted-keyset.cfg \
---out encrypted-keyset.cfg \
+tinkey promote-key--in encrypted-keyset.cfg --key-id 1234567890 \
+  --out new-encrypted-keyset.cfg \
 --master-key-uri gcp-kms://projects/tink-examples/locations/global/keyRings/foo/cryptoKeys/bar
 ```
diff --git a/docs/TYPESCRIPT-HOWTO.md b/docs/TYPESCRIPT-HOWTO.md
new file mode 100644
index 0000000..1fbd9ad
--- /dev/null
+++ b/docs/TYPESCRIPT-HOWTO.md
@@ -0,0 +1,143 @@
+# Tink for TypeScript HOW-TO
+
+This document presents instructions and TypeScript code snippets for common
+tasks in [Tink](https://github.com/google/tink).
+
+Depending on the specifics of your build setup, you may need to alter these
+snippets to use a different import syntax. Both ES modules and UMD are
+supported.
+
+WARNING: Tink for TypeScript/JavaScript is still in an alpha state! Breaking
+changes are likely.
+
+## Setup instructions
+
+To add Tink to a TypeScript/JavaScript project, just run:
+
+```sh
+npm install tink-crypto
+```
+
+Or, if you're using Yarn:
+
+```sh
+yarn add tink-crypto
+```
+
+## Generating new keys and keysets
+
+To take advantage of key rotation and other key management features, you usually
+do not work with single keys, but with keysets, which you can use via a wrapper
+called `KeysetHandle`. Keysets are just sets of keys with some additional
+parameters and metadata. You can generate a new keyset and obtain its handle
+using a `KeyTemplate` (example in below code snippet).
+
+To avoid accidental leakage of sensitive key material, you should usually avoid
+mixing keyset generation and usage in code. To support the separation of these
+activities Tink provides a command-line tool, [Tinkey](TINKEY.md), which can be
+used for common key management tasks. Still, if there is a need to generate a
+`KeysetHandle` with fresh key material directly in TypeScript code, you can use
+`generateNewKeysetHandle`:
+
+```javascript
+import {aead, generateNewKeysetHandle} from 'tink-crypto';
+const {aes256GcmKeyTemplate} = aead;
+
+(async () => {
+  const keyTemplate = aes256GcmKeyTemplate()
+  const keysetHandle = await generateNewKeysetHandle(keyTemplate);
+  // use the keyset...
+})();
+```
+
+Currently, key templates are only available for AEAD encryption, digital
+signatures, and hybrid encryption.
+
+| Key       | Key Template                                                     |
+: Template  :                                                                  :
+: Type      :                                                                  :
+| --------- | ---------------------------------------------------------------- |
+| AEAD      | `aead.aes128GcmKeyTemplate()`                                    |
+| AEAD      | `aead.aes256GcmKeyTemplate()`                                    |
+| AEAD      | `aead.aes256GcmNoPrefixKeyTemplate()`                            |
+| Signature | `signature.ecdsaP256KeyTemplate()`                               |
+| Signature | `signature.ecdsaP256IeeeEncodingKeyTemplate()`                   |
+| Signature | `signature.ecdsaP384KeyTemplate()`                               |
+| Signature | `signature.ecdsaP384IeeeEncodingKeyTemplate()`                   |
+| Signature | `signature.ecdsaP521KeyTemplate()`                               |
+| Signature | `signature.ecdsaP521IeeeEncodingKeyTemplate()`                   |
+| Hybrid    | `hybrid.eciesP256HkdfHmacSha256Aes128GcmKeyTemplate()`           |
+| Hybrid    | `hybrid.eciesP256HkdfHmacSha256Aes128CtrHmacSha256KeyTemplate()` |
+
+### Storing and loading existing keysets
+
+After generating key material, you might want to persist it to LocalStorage or
+IndexedDB, or send it to a server to be stored there. The `binary` and
+`binaryInsecure` subpackages can be used to serialize and deserialize keysets to
+and from `UInt8Array`. `binary` handles only public keys; `binaryInsecure` can
+additionally handle private and symmetric keys. With these, you must be careful
+not to leak the raw key material.
+
+```javascript
+import {aead, binaryInsecure, generateNewKeysetHandle} from 'tink-crypto';
+
+const {Aead, register, aes256GcmKeyTemplate} = aead;
+const {deserializeKeyset, serializeKeyset} = binaryInsecure;
+
+register();
+
+(async () => {
+  const keysetHandle = await generateNewKeysetHandle(aes256GcmKeyTemplate());
+  // Serialize keyset to send/store
+  const serializedKeyset = serializeKeyset(keysetHandle);
+
+  const deserializedKeyset = deserializeKeyset(serializedKeyset)
+  const aead = await deserializedKeyset.getPrimitive(Aead);
+  // Use deserialization... (i.e. to decrypt a ciphertext)
+})();
+```
+
+## Obtaining and using primitives
+
+[*Primitives*](PRIMITIVES.md) represent cryptographic operations offered by
+Tink, hence they form the core of Tink API. A primitive is just an interface
+that specifies what operations are offered by the primitive. A primitive can
+have multiple implementations, and you choose a desired implementation by using
+a key of corresponding type (see the
+[this section](KEY-MANAGEMENT.md#key-keyset-and-keysethandle) for details).
+
+A list of primitives and their implementations currently supported by Tink in
+TypeScript/JavaScript can be found [here](PRIMITIVES.md#typescriptjavascript).
+Note that there are currently a few additional limitations:
+
+*   MAC is supported only via the subtle API, not the keyset API.
+*   It's not possible to generate a fresh new asymmetric keyset using the keyset
+    API and then use it without going through the subtle API. (The public key is
+    not directly accessible yet)
+
+### AEAD
+
+AEAD encryption assures the confidentiality and authenticity of the data. This
+primitive is CPA secure.
+
+```javascript
+// See live on StackBlitz: https://stackblitz.com/edit/tink-typescript?file=index.ts
+
+import {aead, generateNewKeysetHandle} from 'tink-crypto';
+
+const {Aead, register, aes256GcmKeyTemplate} = aead;
+
+register();
+
+(async () => {
+  const keysetHandle = await generateNewKeysetHandle(aes256GcmKeyTemplate());
+  const aead = await keysetHandle.getPrimitive(Aead);
+  const ciphertext = await aead.encrypt(
+      new TextEncoder().encode('this data needs to be encrypted'),
+      new TextEncoder().encode('associated data'));
+  const plaintext = new TextDecoder().decode(await aead.decrypt(
+      ciphertext, new TextEncoder().encode('associated data')));
+  console.log('Ciphertext:', ciphertext);
+  console.log('Plaintext:', plaintext);
+})();
+```
diff --git a/docs/WIRE-FORMAT.md b/docs/WIRE-FORMAT.md
index 5b171d1..6765d2c 100644
--- a/docs/WIRE-FORMAT.md
+++ b/docs/WIRE-FORMAT.md
@@ -1,10 +1,5 @@
 # Tink Wire Format
 
-<!--*
-# Document freshness: For more information, see go/fresh-source.
-freshness: { owner: 'tink-dev' reviewed: '2022-04-12' }
-*-->
-
 Please see the
 [developer documentation](https://developers.google.com/tink/wire-format)
 for details on Tink's wire format for keys and primitive output.
diff --git a/examples/.bazelignore b/examples/.bazelignore
deleted file mode 100644
index 70ff329..0000000
--- a/examples/.bazelignore
+++ /dev/null
@@ -1,2 +0,0 @@
-cc
-java_src
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 5da3b38..0000000
--- a/examples/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# tink-examples
-
-These examples show how to use [Tink](https://github.com/google/tink)
-to perform common crypto tasks. They also show how to add a dependency
-on Tink using Maven, Gradle or Bazel.
-
-Subscribe to our
-[mailing list](https://groups.google.com/forum/#!forum/tink-users)
-if you have any questions.
diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle
deleted file mode 100644
index 6cc4b8e..0000000
--- a/examples/android/helloworld/app/build.gradle
+++ /dev/null
@@ -1,45 +0,0 @@
-apply plugin: 'com.android.application'
-
-android {
-    compileSdkVersion 26
-    defaultConfig {
-        applicationId "com.helloworld"
-        minSdkVersion 19
-        targetSdkVersion 26
-        versionCode 1
-        versionName "1.0"
-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-    }
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
-        }
-    }
-   compileOptions {
-        sourceCompatibility 1.8
-        targetCompatibility 1.8
-   }
-}
-
-apply from: "maven_${mavenLocation}.gradle"
-
-dependencies {
-    implementation fileTree(dir: 'libs', include: ['*.jar'])
-    androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
-        exclude group: 'com.android.support', module: 'support-annotations'
-        // This is already included in protobuf-lite that Tink depends on.
-        exclude group: 'com.google.code.findbugs'
-    })
-    implementation 'com.android.support:appcompat-v7:26.+'
-    implementation 'com.android.support:design:26.+'
-    testImplementation 'junit:junit:4.12'
-
-    // Tink HEAD-SNAPSHOT for Android.
-    // In production apps, please use a named version, e.g., 1.4.0.
-    implementation 'com.google.crypto.tink:tink-android:HEAD-SNAPSHOT'
-
-    // An artificial dependency to test whether Tink can co-exist with other
-    // protobuf dependencies. Please remove from production apps.
-    implementation 'com.google.protobuf:protobuf-lite:3.0.1'
-}
diff --git a/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java b/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java
deleted file mode 100644
index daefd70..0000000
--- a/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.helloworld;
-
-import android.app.Application;
-import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.Config;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.config.TinkConfig;
-import com.google.crypto.tink.integration.android.AndroidKeysetManager;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-
-/** A custom application that initializes the Tink runtime at application startup. */
-public class TinkApplication extends Application {
-  private static final String TAG = TinkApplication.class.toString();
-  private static final String PREF_FILE_NAME = "hello_world_pref";
-  private static final String TINK_KEYSET_NAME = "hello_world_keyset";
-  private static final String MASTER_KEY_URI = "android-keystore://hello_world_master_key";
-  public Aead aead;
-
-  @Override
-  public final void onCreate() {
-    super.onCreate();
-    try {
-      Config.register(TinkConfig.TINK_1_0_0);
-      aead = getOrGenerateNewKeysetHandle().getPrimitive(Aead.class);
-    } catch (GeneralSecurityException | IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  private KeysetHandle getOrGenerateNewKeysetHandle() throws IOException, GeneralSecurityException {
-    return new AndroidKeysetManager.Builder()
-        .withSharedPref(getApplicationContext(), TINK_KEYSET_NAME, PREF_FILE_NAME)
-        .withKeyTemplate(AeadKeyTemplates.AES256_GCM)
-        .withMasterKeyUri(MASTER_KEY_URI)
-        .build()
-        .getKeysetHandle();
-  }
-}
diff --git a/examples/android/helloworld/build.gradle b/examples/android/helloworld/build.gradle
deleted file mode 100644
index 4959c1c..0000000
--- a/examples/android/helloworld/build.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
-    repositories {
-        google()
-        mavenCentral()
-    }
-    dependencies {
-        classpath 'com.android.tools.build:gradle:3.6.0'
-
-        // NOTE: Do not place your application dependencies here; they belong
-        // in the individual module build.gradle files
-    }
-}
-
-allprojects {
-    repositories {
-        google()
-        mavenCentral()
-    }
-}
-
-task clean(type: Delete) {
-    delete rootProject.buildDir
-}
diff --git a/examples/android/helloworld/gradle.properties b/examples/android/helloworld/gradle.properties
deleted file mode 100644
index f874b2f..0000000
--- a/examples/android/helloworld/gradle.properties
+++ /dev/null
@@ -1,23 +0,0 @@
-# Project-wide Gradle settings.
-
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
-
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
-
-# Preferred source for Maven packages:
-#   "local": Local Maven repository (i.e. for testing without publishing)
-#   "snapshot": Maven Central snapshot repository (i.e. for testing published
-#       snapshots)
-mavenLocation=snapshot
diff --git a/examples/android/helloworld/gradle/wrapper/gradle-wrapper.jar b/examples/android/helloworld/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 7a3265e..0000000
--- a/examples/android/helloworld/gradle/wrapper/gradle-wrapper.jar
+++ /dev/null
Binary files differ
diff --git a/examples/android/helloworld/gradle/wrapper/gradle-wrapper.properties b/examples/android/helloworld/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 8205549..0000000
--- a/examples/android/helloworld/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
diff --git a/examples/android/helloworld/gradlew b/examples/android/helloworld/gradlew
deleted file mode 100755
index cccdd3d..0000000
--- a/examples/android/helloworld/gradlew
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/usr/bin/env sh
-
-##############################################################################
-##
-##  Gradle start up script for UN*X
-##
-##############################################################################
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn () {
-    echo "$*"
-}
-
-die () {
-    echo
-    echo "$*"
-    echo
-    exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
-    else
-        JAVACMD="$JAVA_HOME/bin/java"
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
-        fi
-        i=$((i+1))
-    done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
-fi
-
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
-fi
-
-exec "$JAVACMD" "$@"
diff --git a/examples/android/helloworld/gradlew.bat b/examples/android/helloworld/gradlew.bat
deleted file mode 100644
index e95643d..0000000
--- a/examples/android/helloworld/gradlew.bat
+++ /dev/null
@@ -1,84 +0,0 @@
-@if "%DEBUG%" == "" @echo off

-@rem ##########################################################################

-@rem

-@rem  Gradle startup script for Windows

-@rem

-@rem ##########################################################################

-

-@rem Set local scope for the variables with windows NT shell

-if "%OS%"=="Windows_NT" setlocal

-

-set DIRNAME=%~dp0

-if "%DIRNAME%" == "" set DIRNAME=.

-set APP_BASE_NAME=%~n0

-set APP_HOME=%DIRNAME%

-

-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

-set DEFAULT_JVM_OPTS=

-

-@rem Find java.exe

-if defined JAVA_HOME goto findJavaFromJavaHome

-

-set JAVA_EXE=java.exe

-%JAVA_EXE% -version >NUL 2>&1

-if "%ERRORLEVEL%" == "0" goto init

-

-echo.

-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

-echo.

-echo Please set the JAVA_HOME variable in your environment to match the

-echo location of your Java installation.

-

-goto fail

-

-:findJavaFromJavaHome

-set JAVA_HOME=%JAVA_HOME:"=%

-set JAVA_EXE=%JAVA_HOME%/bin/java.exe

-

-if exist "%JAVA_EXE%" goto init

-

-echo.

-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

-echo.

-echo Please set the JAVA_HOME variable in your environment to match the

-echo location of your Java installation.

-

-goto fail

-

-:init

-@rem Get command-line arguments, handling Windows variants

-

-if not "%OS%" == "Windows_NT" goto win9xME_args

-

-:win9xME_args

-@rem Slurp the command line arguments.

-set CMD_LINE_ARGS=

-set _SKIP=2

-

-:win9xME_args_slurp

-if "x%~1" == "x" goto execute

-

-set CMD_LINE_ARGS=%*

-

-:execute

-@rem Setup the command line

-

-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

-

-@rem Execute Gradle

-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

-

-:end

-@rem End local scope for the variables with windows NT shell

-if "%ERRORLEVEL%"=="0" goto mainEnd

-

-:fail

-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

-rem the _cmd.exe /c_ return code!

-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

-exit /b 1

-

-:mainEnd

-if "%OS%"=="Windows_NT" endlocal

-

-:omega

diff --git a/examples/objc/helloworld/README.md b/examples/objc/helloworld/README.md
deleted file mode 100644
index 901ed51..0000000
--- a/examples/objc/helloworld/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Obj-C Hello World
-
-This is an example iOS application that can encrypt and decrypt text using [AEAD
-(Authenticated Encryption with Associated
-Data)](../../../docs/PRIMITIVES.md#authenticated-encryption-with-associated-data).
-
-It demonstrates the basic steps of using Tink, namely generating key material,
-obtaining a primitive, and using the primitive to do crypto.
-
-The example comes with a Podfile that demonstrates how to install Tink from Cocoapods.
-
-## Build and Run
-
-### Cocoapods
-
-```shell
-git clone https://github.com/google/tink
-cd tink/examples/objc/helloworld
-pod install
-open TinkExampleApp.xcworkspace
-```
-
diff --git a/go/.bazelversion b/go/.bazelversion
index ac14c3d..09b254e 100644
--- a/go/.bazelversion
+++ b/go/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/go/README.md b/go/README.md
new file mode 100644
index 0000000..5d8c259
--- /dev/null
+++ b/go/README.md
@@ -0,0 +1,7 @@
+# Tink Go
+
+This is the Go implementation of Tink v1.7.0.
+
+> **NOTE**: **This library has migrated to
+> https://github.com/tink-crypto/tink-go
+> ([godoc](https://pkg.go.dev/github.com/tink-crypto/tink-go/tink))**
diff --git a/go/WORKSPACE b/go/WORKSPACE
index 060119d..526c81c 100644
--- a/go/WORKSPACE
+++ b/go/WORKSPACE
@@ -10,6 +10,25 @@
 )
 
 # -------------------------------------------------------------------------
+# Bazel Skylib.
+# -------------------------------------------------------------------------
+# Release from 2023-02-09
+# Protobuf vX.21.9 imports a version of bazel-skylib [1] that is incompatible
+# with the one required by bazel-gazelle, so we make sure we have a newer
+# version [2].
+#
+# [1] https://github.com/protocolbuffers/protobuf/blob/90b73ac3f0b10320315c2ca0d03a5a9b095d2f66/protobuf_deps.bzl#L28
+# [2] https://github.com/bazelbuild/bazel-gazelle/issues/1290#issuecomment-1312809060
+http_archive(
+    name = "bazel_skylib",
+    sha256 = "b8a1527901774180afc798aeb28c4634bdccf19c4d98e7bdd1ce79d1fe9aaad7",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz",
+        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz",
+    ],
+)
+
+# -------------------------------------------------------------------------
 # Wycheproof.
 # -------------------------------------------------------------------------
 # Commit from 2019-12-17
@@ -29,12 +48,12 @@
 #   * @com_google_protobuf//:cc_toolchain
 #   * @com_google_protobuf//:java_toolchain
 # This statement defines the @com_google_protobuf repo.
-# Release from 2021-06-08.
+# Release X.21.9 from 2022-10-26.
 http_archive(
     name = "com_google_protobuf",
-    sha256 = "6b6bf5cd8d0cca442745c4c3c9f527c83ad6ef35a405f64db5215889ac779b42",
-    strip_prefix = "protobuf-3.19.3",
-    urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.3.zip"],
+    strip_prefix = "protobuf-21.9",
+    urls = ["https://github.com/protocolbuffers/protobuf/archive/refs/tags/v21.9.zip"],
+    sha256 = "5babb8571f1cceafe0c18e13ddb3be556e87e12ceea3463d6b0d0064e6cc1ac3",
 )
 
 load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
@@ -42,37 +61,32 @@
 protobuf_deps()
 
 # -------------------------------------------------------------------------
-# Remote Build Execution (RBE).
+# Bazel rules for Go.
 # -------------------------------------------------------------------------
-# Latest bazel_toolchains package on 2021-10-13.
+# Release from 2023-04-20
 http_archive(
-    name = "bazel_toolchains",
-    sha256 = "179ec02f809e86abf56356d8898c8bd74069f1bd7c56044050c2cd3d79d0e024",
-    strip_prefix = "bazel-toolchains-4.1.0",
+    name = "io_bazel_rules_go",
+    sha256 = "6dc2da7ab4cf5d7bfc7c949776b1b7c733f05e56edc4bcd9022bb249d2e2a996",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip",
     ],
 )
 
-load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
-
-rbe_autoconfig(name = "rbe_default")
-
 # -------------------------------------------------------------------------
 # Bazel Gazelle.
 # -------------------------------------------------------------------------
-# Release from 2021-10-11.
+# Release from 2023-01-14
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
+    sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
     ],
 )
 
-load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
 
 # -------------------------------------------------------------------------
 # Tink Go Deps.
@@ -82,34 +96,13 @@
 # gazelle:repository_macro deps.bzl%go_dependencies
 go_dependencies()
 
-# -------------------------------------------------------------------------
-# Bazel rules for Go.
-# -------------------------------------------------------------------------
-# Release from 2022-03-21
-http_archive(
-    name = "io_bazel_rules_go",
-    sha256 = "f2dcd210c7095febe54b804bb1cd3a58fe8435a909db2ec04e31542631cf715c",
-    urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.31.0/rules_go-v0.31.0.zip",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.31.0/rules_go-v0.31.0.zip",
-    ],
-)
-
 load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
 
-# TODO(b/213404399): Remove after Gazelle issue is fixed.
-go_repository(
-    name = "com_google_cloud_go_compute",
-    importpath = "cloud.google.com/go/compute",
-    sum = "h1:rSUBvAyVwNJ5uQCKNJFMwPtTvJkfN38b6Pvb9zZoqJ8=",
-    version = "v0.1.0",
-)
-
 go_rules_dependencies()
 
 go_register_toolchains(
     nogo = "@//:tink_nogo",
-    version = "1.17.6",
+    version = "1.19.9",
 )
 
 gazelle_dependencies()
diff --git a/go/aead/BUILD.bazel b/go/aead/BUILD.bazel
index 80e9e0b..33ef479 100644
--- a/go/aead/BUILD.bazel
+++ b/go/aead/BUILD.bazel
@@ -27,6 +27,7 @@
         "//core/registry",
         "//internal/internalregistry",
         "//internal/monitoringutil",
+        "//internal/tinkerror",
         "//keyset",
         "//mac/subtle",
         "//monitoring",
@@ -51,12 +52,15 @@
     name = "aead_test",
     srcs = [
         "aead_factory_test.go",
+        "aead_init_test.go",
         "aead_key_templates_test.go",
         "aead_test.go",
         "aes_ctr_hmac_aead_key_manager_test.go",
         "aes_gcm_key_manager_test.go",
         "aes_gcm_siv_key_manager_test.go",
         "chacha20poly1305_key_manager_test.go",
+        "kms_envelope_aead_example_test.go",
+        "kms_envelope_aead_key_manager_test.go",
         "kms_envelope_aead_test.go",
         "xchacha20poly1305_key_manager_test.go",
     ],
@@ -65,13 +69,18 @@
         "//aead/subtle",
         "//core/cryptofmt",
         "//core/registry",
+        "//insecurecleartextkeyset",
         "//internal/internalregistry",
+        "//internal/testing/stubkeymanager",
+        "//internal/tinkerror/tinkerrortest",
         "//keyset",
+        "//mac",
         "//monitoring",
         "//proto/aes_ctr_hmac_aead_go_proto",
         "//proto/aes_gcm_go_proto",
         "//proto/aes_gcm_siv_go_proto",
         "//proto/chacha20_poly1305_go_proto",
+        "//proto/kms_envelope_go_proto",
         "//proto/tink_go_proto",
         "//proto/xchacha20_poly1305_go_proto",
         "//signature",
diff --git a/go/aead/aead.go b/go/aead/aead.go
index 45cd93e..4c313a0 100644
--- a/go/aead/aead.go
+++ b/go/aead/aead.go
@@ -23,6 +23,7 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 )
 
 func init() {
@@ -33,6 +34,9 @@
 	if err := registry.RegisterKeyManager(new(aesGCMKeyManager)); err != nil {
 		panic(fmt.Sprintf("aead.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(aesGCMTypeURL); err != nil {
+		panic(fmt.Sprintf("aead.init() failed: %v", err))
+	}
 
 	if err := registry.RegisterKeyManager(new(chaCha20Poly1305KeyManager)); err != nil {
 		panic(fmt.Sprintf("aead.init() failed: %v", err))
@@ -41,6 +45,9 @@
 	if err := registry.RegisterKeyManager(new(xChaCha20Poly1305KeyManager)); err != nil {
 		panic(fmt.Sprintf("aead.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(xChaCha20Poly1305TypeURL); err != nil {
+		panic(fmt.Sprintf("aead.init() failed: %v", err))
+	}
 
 	if err := registry.RegisterKeyManager(new(kmsEnvelopeAEADKeyManager)); err != nil {
 		panic(fmt.Sprintf("aead.init() failed: %v", err))
diff --git a/go/aead/aead_factory.go b/go/aead/aead_factory.go
index 55bbdb1..bfe8879 100644
--- a/go/aead/aead_factory.go
+++ b/go/aead/aead_factory.go
@@ -21,7 +21,6 @@
 
 	"github.com/google/tink/go/core/cryptofmt"
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
@@ -30,19 +29,11 @@
 )
 
 // New returns an AEAD primitive from the given keyset handle.
-func New(h *keyset.Handle) (tink.AEAD, error) {
-	return NewWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewWithKeyManager returns an AEAD primitive from the given keyset handle and custom key manager.
-//
-// Deprecated: Use [New].
-func NewWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.AEAD, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func New(handle *keyset.Handle) (tink.AEAD, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("aead_factory: cannot obtain primitive set: %s", err)
 	}
-
 	return newWrappedAead(ps)
 }
 
@@ -66,32 +57,43 @@
 			}
 		}
 	}
-	wa := &wrappedAead{ps: ps}
-	client := internalregistry.GetMonitoringClient()
-	if client == nil {
-		return wa, nil
-	}
-	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	encLogger, decLogger, err := createLoggers(ps)
 	if err != nil {
 		return nil, err
 	}
-	wa.encLogger, err = client.NewLogger(&monitoring.Context{
+	return &wrappedAead{
+		ps:        ps,
+		encLogger: encLogger,
+		decLogger: decLogger,
+	}, nil
+}
+
+func createLoggers(ps *primitiveset.PrimitiveSet) (monitoring.Logger, monitoring.Logger, error) {
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, &monitoringutil.DoNothingLogger{}, nil
+	}
+	client := internalregistry.GetMonitoringClient()
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, nil, err
+	}
+	encLogger, err := client.NewLogger(&monitoring.Context{
 		Primitive:   "aead",
 		APIFunction: "encrypt",
 		KeysetInfo:  keysetInfo,
 	})
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	wa.decLogger, err = client.NewLogger(&monitoring.Context{
+	decLogger, err := client.NewLogger(&monitoring.Context{
 		Primitive:   "aead",
 		APIFunction: "decrypt",
 		KeysetInfo:  keysetInfo,
 	})
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	return wa, nil
+	return encLogger, decLogger, nil
 }
 
 // Encrypt encrypts the given plaintext with the given associatedData.
@@ -108,7 +110,13 @@
 		return nil, err
 	}
 	a.encLogger.Log(primary.KeyID, len(plaintext))
-	return append([]byte(primary.Prefix), ct...), nil
+	if len(primary.Prefix) == 0 {
+		return ct, nil
+	}
+	output := make([]byte, 0, len(primary.Prefix)+len(ct))
+	output = append(output, primary.Prefix...)
+	output = append(output, ct...)
+	return output, nil
 }
 
 // Decrypt decrypts the given ciphertext and authenticates it with the given
diff --git a/go/aead/aead_factory_test.go b/go/aead/aead_factory_test.go
index 9716142..7f8f8c3 100644
--- a/go/aead/aead_factory_test.go
+++ b/go/aead/aead_factory_test.go
@@ -18,6 +18,7 @@
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"strings"
 	"testing"
@@ -26,7 +27,10 @@
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/google/tink/go/aead"
 	"github.com/google/tink/go/core/cryptofmt"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/signature"
@@ -37,6 +41,7 @@
 	"github.com/google/tink/go/tink"
 
 	"github.com/google/tink/go/aead/subtle"
+	agpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
@@ -103,9 +108,7 @@
 	}
 }
 
-func validateAEADFactoryCipher(encryptCipher tink.AEAD,
-	decryptCipher tink.AEAD,
-	expectedPrefix string) error {
+func validateAEADFactoryCipher(encryptCipher, decryptCipher tink.AEAD, expectedPrefix string) error {
 	prefixSize := len(expectedPrefix)
 	// regular plaintext
 	pt := random.GetRandomBytes(20)
@@ -170,26 +173,37 @@
 	}
 }
 
-func TestFactoryWithMonitoringPrimitiveLogsEncryptionDecryptionWithPrefix(t *testing.T) {
+func TestPrimitiveFactoryWithMonitoringAnnotationsLogsEncryptionDecryptionWithPrefix(t *testing.T) {
 	defer internalregistry.ClearMonitoringClient()
 	client := fakemonitoring.NewClient("fake-client")
 	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
-		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
 	}
 	kh, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
 	if err != nil {
 		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
 	}
-	p, err := aead.New(kh)
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(mh)
 	if err != nil {
 		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
 	data := []byte("HELLO_WORLD")
-	ct, err := p.Encrypt(data, nil)
+	ad := []byte("_!")
+	ct, err := p.Encrypt(data, ad)
 	if err != nil {
 		t.Fatalf("p.Encrypt() err = %v, want nil", err)
 	}
-	if _, err := p.Decrypt(ct, nil); err != nil {
+	if _, err := p.Decrypt(ct, ad); err != nil {
 		t.Fatalf("p.Decrypt() err = %v, want nil", err)
 	}
 	failures := client.Failures()
@@ -198,46 +212,57 @@
 	}
 	got := client.Events()
 	wantKeysetInfo := monitoring.NewKeysetInfo(
-		make(map[string]string),
+		annotations,
 		kh.KeysetInfo().GetPrimaryKeyId(),
 		[]*monitoring.Entry{
 			{
-				KeyID:          kh.KeysetInfo().GetPrimaryKeyId(),
-				Status:         monitoring.Enabled,
-				FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.AesGcmKey",
+				KeyPrefix: "TINK",
 			},
 		},
 	)
 	want := []*fakemonitoring.LogEvent{
 		{
-			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			KeyID:    mh.KeysetInfo().GetPrimaryKeyId(),
 			NumBytes: len(data),
 			Context:  monitoring.NewContext("aead", "encrypt", wantKeysetInfo),
 		},
 		{
-			KeyID: kh.KeysetInfo().GetPrimaryKeyId(),
+			KeyID: mh.KeysetInfo().GetPrimaryKeyId(),
 			// ciphertext was encrypted with a key that has TINK ouput prefix. This adds a 5 bytes prefix
 			// to the ciphertext. This prefix is not included in `Log` call.
 			NumBytes: len(ct) - cryptofmt.NonRawPrefixSize,
 			Context:  monitoring.NewContext("aead", "decrypt", wantKeysetInfo),
 		},
 	}
-	if !cmp.Equal(got, want) {
-		t.Errorf("got = %v, want = %v", got, want)
+	if cmp.Diff(got, want) != "" {
+		t.Errorf("%v", cmp.Diff(got, want))
 	}
 }
 
-func TestFactoryWithMonitoringPrimitiveLogsEncryptionDecryptionWithoutPrefix(t *testing.T) {
+func TestPrimitiveFactoryWithMonitoringAnnotationsLogsEncryptionDecryptionWithoutPrefix(t *testing.T) {
 	defer internalregistry.ClearMonitoringClient()
 	client := fakemonitoring.NewClient("fake-client")
 	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
-		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
 	}
 	kh, err := keyset.NewHandle(aead.AES256GCMNoPrefixKeyTemplate())
 	if err != nil {
 		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
 	}
-	p, err := aead.New(kh)
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(mh)
 	if err != nil {
 		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
@@ -255,34 +280,35 @@
 	}
 	got := client.Events()
 	wantKeysetInfo := monitoring.NewKeysetInfo(
-		make(map[string]string),
+		annotations,
 		kh.KeysetInfo().GetPrimaryKeyId(),
 		[]*monitoring.Entry{
 			{
-				KeyID:          kh.KeysetInfo().GetPrimaryKeyId(),
-				Status:         monitoring.Enabled,
-				FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.AesGcmKey",
+				KeyPrefix: "RAW",
 			},
 		},
 	)
 	want := []*fakemonitoring.LogEvent{
 		{
-			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			KeyID:    mh.KeysetInfo().GetPrimaryKeyId(),
 			NumBytes: len(data),
 			Context:  monitoring.NewContext("aead", "encrypt", wantKeysetInfo),
 		},
 		{
-			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			KeyID:    mh.KeysetInfo().GetPrimaryKeyId(),
 			NumBytes: len(ct),
 			Context:  monitoring.NewContext("aead", "decrypt", wantKeysetInfo),
 		},
 	}
-	if !cmp.Equal(got, want) {
-		t.Errorf("got = %v, want = %v", got, want)
+	if cmp.Diff(got, want) != "" {
+		t.Errorf("%v", cmp.Diff(got, want))
 	}
 }
 
-func TestFactoryWithMonitoringPrimitiveWithMultipleKeysLogsEncryptionDecryption(t *testing.T) {
+func TestPrimitiveFactoryMonitoringWithAnnotatiosMultipleKeysLogsEncryptionDecryption(t *testing.T) {
 	defer internalregistry.ClearMonitoringClient()
 	client := fakemonitoring.NewClient("fake-client")
 	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
@@ -313,7 +339,17 @@
 	if err != nil {
 		t.Fatalf("manager.Handle() err = %v, want nil", err)
 	}
-	p, err := aead.New(kh)
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(mh)
 	if err != nil {
 		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
@@ -330,21 +366,24 @@
 		t.Fatalf("p.Decrypt() err = %v, want nil", err)
 	}
 	got := client.Events()
-	wantKeysetInfo := monitoring.NewKeysetInfo(make(map[string]string), keyIDs[1], []*monitoring.Entry{
+	wantKeysetInfo := monitoring.NewKeysetInfo(annotations, keyIDs[1], []*monitoring.Entry{
 		{
-			KeyID:          keyIDs[1],
-			Status:         monitoring.Enabled,
-			FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+			KeyID:     keyIDs[1],
+			Status:    monitoring.Enabled,
+			KeyType:   "tink.AesGcmKey",
+			KeyPrefix: "RAW",
 		},
 		{
-			KeyID:          keyIDs[2],
-			Status:         monitoring.Enabled,
-			FormatAsString: "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+			KeyID:     keyIDs[2],
+			Status:    monitoring.Enabled,
+			KeyType:   "tink.AesCtrHmacAeadKey",
+			KeyPrefix: "TINK",
 		},
 		{
-			KeyID:          keyIDs[3],
-			Status:         monitoring.Enabled,
-			FormatAsString: "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+			KeyID:     keyIDs[3],
+			Status:    monitoring.Enabled,
+			KeyType:   "tink.XChaCha20Poly1305Key",
+			KeyPrefix: "TINK",
 		},
 	})
 	want := []*fakemonitoring.LogEvent{
@@ -376,25 +415,47 @@
 	}
 }
 
-func TestFactoryWithMonitoringPrimitiveEncryptionFailureIsLogged(t *testing.T) {
+func TestPrimitiveFactoryWithMonitoringAnnotationsEncryptionFailureIsLogged(t *testing.T) {
 	defer internalregistry.ClearMonitoringClient()
 	client := &fakemonitoring.Client{Name: ""}
 	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
 		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
 	}
-	kh, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	typeURL := "TestFactoryWithMonitoringPrimitiveEncryptionFailureIsLogged"
+	template := &tinkpb.KeyTemplate{
+		TypeUrl:          typeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_LEGACY,
+	}
+	km := &stubkeymanager.StubKeyManager{
+		URL:  typeURL,
+		Key:  &agpb.AesGcmKey{},
+		Prim: &testutil.AlwaysFailingAead{Error: errors.New("failed")},
+		KeyData: &tinkpb.KeyData{
+			TypeUrl:         typeURL,
+			KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+			Value:           []byte("serialized_key"),
+		},
+	}
+	if err := registry.RegisterKeyManager(km); err != nil {
+		t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(template)
 	if err != nil {
 		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
 	}
-	km := testutil.NewTestKeyManager(
-		&testutil.AlwaysFailingAead{
-			Error: fmt.Errorf("failed"),
-		},
-		testutil.AESGCMTypeURL,
-	)
-	p, err := aead.NewWithKeyManager(kh, km)
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
 	if err != nil {
-		t.Fatalf("aead.NewWithKeyManager() err = %v, want nil", err)
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(mh)
+	if err != nil {
+		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
 	if _, err := p.Encrypt(nil, nil); err == nil {
 		t.Fatalf("Encrypt() err = nil, want error")
@@ -406,27 +467,28 @@
 				"aead",
 				"encrypt",
 				monitoring.NewKeysetInfo(
-					make(map[string]string),
+					annotations,
 					kh.KeysetInfo().GetPrimaryKeyId(),
 					[]*monitoring.Entry{
 						{
-							KeyID:          kh.KeysetInfo().GetPrimaryKeyId(),
-							Status:         monitoring.Enabled,
-							FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   typeURL,
+							KeyPrefix: "LEGACY",
 						},
 					},
 				),
 			),
 		},
 	}
-	if !cmp.Equal(got, want) {
-		t.Errorf("got = %v, want = %v", got, want)
+	if cmp.Diff(got, want) != "" {
+		t.Errorf("%v", cmp.Diff(got, want))
 	}
 }
 
-func TestFactoryWithMonitoringPrimitiveDecryptionFailureIsLogged(t *testing.T) {
+func TestPrimitiveFactoryWithMonitoringAnnotationsDecryptionFailureIsLogged(t *testing.T) {
 	defer internalregistry.ClearMonitoringClient()
-	client := &fakemonitoring.Client{Name: ""}
+	client := fakemonitoring.NewClient("fake-client")
 	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
 		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
 	}
@@ -434,7 +496,17 @@
 	if err != nil {
 		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
 	}
-	p, err := aead.New(kh)
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(mh)
 	if err != nil {
 		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
@@ -448,21 +520,22 @@
 				"aead",
 				"decrypt",
 				monitoring.NewKeysetInfo(
-					make(map[string]string),
+					annotations,
 					kh.KeysetInfo().GetPrimaryKeyId(),
 					[]*monitoring.Entry{
 						{
-							KeyID:          kh.KeysetInfo().GetPrimaryKeyId(),
-							Status:         monitoring.Enabled,
-							FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesGcmKey",
+							KeyPrefix: "TINK",
 						},
 					},
 				),
 			),
 		},
 	}
-	if !cmp.Equal(got, want) {
-		t.Errorf("got = %v, want = %v", got, want)
+	if cmp.Diff(got, want) != "" {
+		t.Errorf("%v", cmp.Diff(got, want))
 	}
 }
 
@@ -476,7 +549,17 @@
 	if err != nil {
 		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
 	}
-	p1, err := aead.New(kh1)
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh1, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh1, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p1, err := aead.New(mh1)
 	if err != nil {
 		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
@@ -484,7 +567,15 @@
 	if err != nil {
 		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
 	}
-	p2, err := aead.New(kh2)
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(kh2, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	mh2, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p2, err := aead.New(mh2)
 	if err != nil {
 		t.Fatalf("aead.New() err = %v, want nil", err)
 	}
@@ -505,13 +596,14 @@
 				"aead",
 				"encrypt",
 				monitoring.NewKeysetInfo(
-					make(map[string]string),
+					annotations,
 					kh1.KeysetInfo().GetPrimaryKeyId(),
 					[]*monitoring.Entry{
 						{
-							KeyID:          kh1.KeysetInfo().GetPrimaryKeyId(),
-							Status:         monitoring.Enabled,
-							FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+							KeyID:     kh1.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesGcmKey",
+							KeyPrefix: "TINK",
 						},
 					},
 				),
@@ -524,13 +616,14 @@
 				"aead",
 				"encrypt",
 				monitoring.NewKeysetInfo(
-					make(map[string]string),
+					annotations,
 					kh2.KeysetInfo().GetPrimaryKeyId(),
 					[]*monitoring.Entry{
 						{
-							KeyID:          kh2.KeysetInfo().GetPrimaryKeyId(),
-							Status:         monitoring.Enabled,
-							FormatAsString: "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+							KeyID:     kh2.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesCtrHmacAeadKey",
+							KeyPrefix: "TINK",
 						},
 					},
 				),
@@ -541,3 +634,35 @@
 		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
 	}
 }
+
+func TestPrimitiveFactoryEncryptDecryptWithoutAnnotationsDoesNothing(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	p, err := aead.New(kh)
+	if err != nil {
+		t.Fatalf("aead.New() err = %v, want nil", err)
+	}
+	data := []byte("YELLOW_ORANGE")
+	ct, err := p.Encrypt(data, nil)
+	if err != nil {
+		t.Fatalf("p.Encrypt() err = %v, want nil", err)
+	}
+	if _, err := p.Decrypt(ct, nil); err != nil {
+		t.Fatalf("p.Decrypt() err = %v, want nil", err)
+	}
+	got := client.Events()
+	if len(got) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(got))
+	}
+	failures := len(client.Failures())
+	if failures != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", failures)
+	}
+}
diff --git a/go/aead/aead_init_test.go b/go/aead/aead_init_test.go
new file mode 100644
index 0000000..664ef4b
--- /dev/null
+++ b/go/aead/aead_init_test.go
@@ -0,0 +1,44 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package aead_test
+
+import (
+	"testing"
+
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestAEADInit(t *testing.T) {
+	// Check that the AES-GCM key manager is in the global registry.
+	_, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+
+	// Check that the ChaCha20Poly1305 key manager is in the global registry.
+	_, err = registry.GetKeyManager(testutil.ChaCha20Poly1305TypeURL)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+
+	// Check that the XChaCha20Poly1305 key manager is in the global registry.
+	_, err = registry.GetKeyManager(testutil.XChaCha20Poly1305TypeURL)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+}
diff --git a/go/aead/aead_key_templates.go b/go/aead/aead_key_templates.go
index b536279..1cbb001 100644
--- a/go/aead/aead_key_templates.go
+++ b/go/aead/aead_key_templates.go
@@ -17,10 +17,14 @@
 package aead
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	ctrpb "github.com/google/tink/go/proto/aes_ctr_go_proto"
 	ctrhmacpb "github.com/google/tink/go/proto/aes_ctr_hmac_aead_go_proto"
 	gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
+	gcmsivpb "github.com/google/tink/go/proto/aes_gcm_siv_go_proto"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	hmacpb "github.com/google/tink/go/proto/hmac_go_proto"
 	kmsenvpb "github.com/google/tink/go/proto/kms_envelope_go_proto"
@@ -51,22 +55,43 @@
 	return createAESGCMKeyTemplate(32, tinkpb.OutputPrefixType_RAW)
 }
 
+// AES128GCMSIVKeyTemplate is a KeyTemplate that generates an AES-GCM-SIV key with the following parameters:
+//   - Key size: 16 bytes
+//   - Output prefix type: TINK
+func AES128GCMSIVKeyTemplate() *tinkpb.KeyTemplate {
+	return createAESGCMSIVKeyTemplate(16, tinkpb.OutputPrefixType_TINK)
+}
+
+// AES256GCMSIVKeyTemplate is a KeyTemplate that generates an AES-GCM-SIV key with the following parameters:
+//   - Key size: 32 bytes
+//   - Output prefix type: TINK
+func AES256GCMSIVKeyTemplate() *tinkpb.KeyTemplate {
+	return createAESGCMSIVKeyTemplate(32, tinkpb.OutputPrefixType_TINK)
+}
+
+// AES256GCMSIVNoPrefixKeyTemplate is a KeyTemplate that generates an AES-GCM key with the following parameters:
+//   - Key size: 32 bytes
+//   - Output prefix type: RAW
+func AES256GCMSIVNoPrefixKeyTemplate() *tinkpb.KeyTemplate {
+	return createAESGCMSIVKeyTemplate(32, tinkpb.OutputPrefixType_RAW)
+}
+
 // AES128CTRHMACSHA256KeyTemplate is a KeyTemplate that generates an AES-CTR-HMAC-AEAD key with the following parameters:
-//  - AES key size: 16 bytes
-//  - AES CTR IV size: 16 bytes
-//  - HMAC key size: 32 bytes
-//  - HMAC tag size: 16 bytes
-//  - HMAC hash function: SHA256
+//   - AES key size: 16 bytes
+//   - AES CTR IV size: 16 bytes
+//   - HMAC key size: 32 bytes
+//   - HMAC tag size: 16 bytes
+//   - HMAC hash function: SHA256
 func AES128CTRHMACSHA256KeyTemplate() *tinkpb.KeyTemplate {
 	return createAESCTRHMACAEADKeyTemplate(16, 16, 32, 16, commonpb.HashType_SHA256)
 }
 
 // AES256CTRHMACSHA256KeyTemplate is a KeyTemplate that generates an AES-CTR-HMAC-AEAD key with the following parameters:
-//  - AES key size: 32 bytes
-//  - AES CTR IV size: 16 bytes
-//  - HMAC key size: 32 bytes
-//  - HMAC tag size: 32 bytes
-//  - HMAC hash function: SHA256
+//   - AES key size: 32 bytes
+//   - AES CTR IV size: 16 bytes
+//   - HMAC key size: 32 bytes
+//   - HMAC tag size: 32 bytes
+//   - HMAC hash function: SHA256
 func AES256CTRHMACSHA256KeyTemplate() *tinkpb.KeyTemplate {
 	return createAESCTRHMACAEADKeyTemplate(32, 16, 32, 32, commonpb.HashType_SHA256)
 }
@@ -89,22 +114,65 @@
 	}
 }
 
-// KMSEnvelopeAEADKeyTemplate is a KeyTemplate that generates a KMSEnvelopeAEAD key for
-// a given KEK in remote KMS. Keys generated by this key template uses RAW output prefix
-// to make them compatible with the remote KMS' encrypt/decrypt operations.
-// Unlike other templates, when you generate new keys with this template, Tink does not
-// generate new key material, but only creates a reference to the remote KEK.
-func KMSEnvelopeAEADKeyTemplate(uri string, dekT *tinkpb.KeyTemplate) *tinkpb.KeyTemplate {
+// CreateKMSEnvelopeAEADKeyTemplate returns a key template that generates a
+// KMSEnvelopeAEAD key for a given key encryption key (KEK) in a remote key
+// management service (KMS).
+//
+// When performing encrypt operations, a data encryption key (DEK) is generated
+// for each ciphertext.  The DEK is wrapped by the remote KMS using the KEK and
+// stored alongside the ciphertext.
+//
+// dekTemplate must be a KeyTemplate for any of these Tink AEAD key types (any
+// other key template will be rejected):
+//   - AesCtrHmacAeadKey
+//   - AesGcmKey
+//   - ChaCha20Poly1305Key
+//   - XChaCha20Poly1305
+//   - AesGcmSivKey
+//
+// DEKs generated by this key template use the RAW output prefix to make them
+// compatible with remote KMS encrypt/decrypt operations.
+//
+// Unlike other templates, when you generate new keys with this template, Tink
+// does not generate new key material, but only creates a reference to the
+// remote KEK.
+//
+// If either uri or dekTemplate contain invalid input, an error is returned.
+func CreateKMSEnvelopeAEADKeyTemplate(uri string, dekTemplate *tinkpb.KeyTemplate) (*tinkpb.KeyTemplate, error) {
+	if !isSupporedKMSEnvelopeDEK(dekTemplate.GetTypeUrl()) {
+		return nil, fmt.Errorf("unsupported DEK key type %s. Only Tink AEAD key types are supported", dekTemplate.GetTypeUrl())
+	}
+
 	f := &kmsenvpb.KmsEnvelopeAeadKeyFormat{
 		KekUri:      uri,
-		DekTemplate: dekT,
+		DekTemplate: dekTemplate,
 	}
-	serializedFormat, _ := proto.Marshal(f)
+	serializedFormat, err := proto.Marshal(f)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal key format: %s", err)
+	}
 	return &tinkpb.KeyTemplate{
 		Value:            serializedFormat,
 		TypeUrl:          kmsEnvelopeAEADTypeURL,
 		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+	}, nil
+}
+
+// KMSEnvelopeAEADKeyTemplate returns a KeyTemplate that generates a
+// KMSEnvelopeAEAD key for a given key encryption key (KEK) in a remote key
+// management service (KMS).
+//
+// If either uri or dekTemplate contain invalid input, program execution will
+// be interrupted.
+//
+// Deprecated: Use [CreateKMSEnvelopeAEADKeyTemplate], which returns an error
+// value instead of interrupting the program.
+func KMSEnvelopeAEADKeyTemplate(uri string, dekTemplate *tinkpb.KeyTemplate) *tinkpb.KeyTemplate {
+	t, err := CreateKMSEnvelopeAEADKeyTemplate(uri, dekTemplate)
+	if err != nil {
+		tinkerror.Fail(err.Error())
 	}
+	return t
 }
 
 // createAESGCMKeyTemplate creates a new AES-GCM key template with the given key
@@ -113,7 +181,10 @@
 	format := &gcmpb.AesGcmKeyFormat{
 		KeySize: keySize,
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          aesGCMTypeURL,
 		Value:            serializedFormat,
@@ -121,6 +192,23 @@
 	}
 }
 
+// createAESGCMSIVKeyTemplate creates a new AES-GCM-SIV key template with the given key
+// size in bytes.
+func createAESGCMSIVKeyTemplate(keySize uint32, outputPrefixType tinkpb.OutputPrefixType) *tinkpb.KeyTemplate {
+	format := &gcmsivpb.AesGcmSivKeyFormat{
+		KeySize: keySize,
+	}
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
+	return &tinkpb.KeyTemplate{
+		TypeUrl:          aesGCMSIVTypeURL,
+		Value:            serializedFormat,
+		OutputPrefixType: outputPrefixType,
+	}
+}
+
 func createAESCTRHMACAEADKeyTemplate(aesKeySize, ivSize, hmacKeySize, tagSize uint32, hash commonpb.HashType) *tinkpb.KeyTemplate {
 	format := &ctrhmacpb.AesCtrHmacAeadKeyFormat{
 		AesCtrKeyFormat: &ctrpb.AesCtrKeyFormat{
@@ -132,7 +220,10 @@
 			KeySize: hmacKeySize,
 		},
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		Value:            serializedFormat,
 		TypeUrl:          aesCTRHMACAEADTypeURL,
diff --git a/go/aead/aead_key_templates_test.go b/go/aead/aead_key_templates_test.go
index 3b13523..cf6d6c8 100644
--- a/go/aead/aead_key_templates_test.go
+++ b/go/aead/aead_key_templates_test.go
@@ -23,7 +23,9 @@
 
 	"github.com/google/tink/go/aead"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/tinkerror/tinkerrortest"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/mac"
 	"github.com/google/tink/go/testing/fakekms"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -43,6 +45,15 @@
 			name:     "AES256_GCM_NO_PREFIX",
 			template: aead.AES256GCMNoPrefixKeyTemplate(),
 		}, {
+			name:     "AES128_GCM_SIV",
+			template: aead.AES128GCMSIVKeyTemplate(),
+		}, {
+			name:     "AES256_GCM_SIV",
+			template: aead.AES256GCMSIVKeyTemplate(),
+		}, {
+			name:     "AES256_GCM_SIV_NO_PREFIX",
+			template: aead.AES256GCMSIVNoPrefixKeyTemplate(),
+		}, {
 			name:     "AES128_CTR_HMAC_SHA256",
 			template: aead.AES128CTRHMACSHA256KeyTemplate(),
 		}, {
@@ -77,16 +88,25 @@
 	if err != nil {
 		t.Fatalf("fakekms.NewKeyURI() failed: %v", err)
 	}
+	fixedKeyTemplate, err := aead.CreateKMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("CreateKMSEnvelopeAEADKeyTemplate() err = %v", err)
+	}
+	newKeyTemplate, err := aead.CreateKMSEnvelopeAEADKeyTemplate(newKeyURI, aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("CreateKMSEnvelopeAEADKeyTemplate() err = %v", err)
+	}
+
 	var testCases = []struct {
 		name     string
 		template *tinkpb.KeyTemplate
 	}{
 		{
 			name:     "Fixed Fake KMS Envelope AEAD Key with AES128_GCM",
-			template: aead.KMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate()),
+			template: fixedKeyTemplate,
 		}, {
 			name:     "New Fake KMS Envelope AEAD Key with AES128_GCM",
-			template: aead.KMSEnvelopeAEADKeyTemplate(newKeyURI, aead.AES128GCMKeyTemplate()),
+			template: newKeyTemplate,
 		},
 	}
 	for _, tc := range testCases {
@@ -101,8 +121,8 @@
 	}
 }
 
-// Tests that two KMSEnvelopeAEAD keys that use the same KEK and DEK template should be able to
-// decrypt each  other's ciphertexts.
+// Tests that two KMSEnvelopeAEAD keys that use the same KEK and DEK template
+// should be able to decrypt each other's ciphertexts.
 func TestKMSEnvelopeAEADKeyTemplateMultipleKeysSameKEK(t *testing.T) {
 	fakeKmsClient, err := fakekms.NewClient("fake-kms://")
 	if err != nil {
@@ -111,7 +131,62 @@
 	registry.RegisterKMSClient(fakeKmsClient)
 
 	fixedKeyURI := "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
-	template1 := aead.KMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate())
+	template1, err := aead.CreateKMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("CreateKMSEnvelopeAEADKeyTemplate() err = %v", err)
+	}
+	template2, err := aead.CreateKMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("CreateKMSEnvelopeAEADKeyTemplate() err = %v", err)
+	}
+
+	handle1, err := keyset.NewHandle(template1)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(template1) failed: %v", err)
+	}
+	aead1, err := aead.New(handle1)
+	if err != nil {
+		t.Fatalf("aead.New(handle) failed: %v", err)
+	}
+
+	handle2, err := keyset.NewHandle(template2)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(template2) failed: %v", err)
+	}
+	aead2, err := aead.New(handle2)
+	if err != nil {
+		t.Fatalf("aead.New(handle) failed: %v", err)
+	}
+
+	plaintext := []byte("some data to encrypt")
+	aad := []byte("extra data to authenticate")
+
+	ciphertext, err := aead1.Encrypt(plaintext, aad)
+	if err != nil {
+		t.Fatalf("encryption failed, error: %v", err)
+	}
+	decrypted, err := aead2.Decrypt(ciphertext, aad)
+	if err != nil {
+		t.Fatalf("decryption failed, error: %v", err)
+	}
+	if !bytes.Equal(plaintext, decrypted) {
+		t.Fatalf("decrypted data doesn't match plaintext, got: %q, want: %q", decrypted, plaintext)
+	}
+}
+
+// Testing deprecated function, ignoring GoDeprecated.
+func TestCreateKMSEnvelopeAEADKeyTemplateCompatibleWithKMSEnevelopeAEADKeyTemplate(t *testing.T) {
+	fakeKmsClient, err := fakekms.NewClient("fake-kms://")
+	if err != nil {
+		t.Fatalf("fakekms.NewClient('fake-kms://') failed: %v", err)
+	}
+	registry.RegisterKMSClient(fakeKmsClient)
+
+	fixedKeyURI := "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+	template1, err := aead.CreateKMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("CreateKMSEnvelopeAEADKeyTemplate() err = %v", err)
+	}
 	template2 := aead.KMSEnvelopeAEADKeyTemplate(fixedKeyURI, aead.AES128GCMKeyTemplate())
 
 	handle1, err := keyset.NewHandle(template1)
@@ -148,6 +223,51 @@
 	}
 }
 
+// Testing deprecated function, ignoring GoDeprecated.
+func TestKMSEnvelopeAEADKeyTemplateFails(t *testing.T) {
+	keyURI, err := fakekms.NewKeyURI()
+	if err != nil {
+		t.Fatalf("fakekms.NewKeyURI() err = %v", err)
+	}
+	invalidTemplate := &tinkpb.KeyTemplate{
+		// String fields cannot contain invalid UTF-8 characters.
+		TypeUrl: "\xff",
+	}
+	var template *tinkpb.KeyTemplate
+	err = tinkerrortest.RecoverFromFail(func() {
+		template = aead.KMSEnvelopeAEADKeyTemplate(keyURI, invalidTemplate)
+	})
+	if err == nil {
+		t.Errorf("aead.KMSEnvelopAEADKeyTemplate() err = nil, want non-nil")
+	}
+	t.Logf("template: %+v", template)
+}
+
+func TestCreateKMSEnvelopeAEADKeyTemplateFails(t *testing.T) {
+	keyURI, err := fakekms.NewKeyURI()
+	if err != nil {
+		t.Fatalf("fakekms.NewKeyURI() err = %v", err)
+	}
+	invalidTemplate := &tinkpb.KeyTemplate{
+		// String fields cannot contain invalid UTF-8 characters.
+		TypeUrl: "\xff",
+	}
+	if _, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, invalidTemplate); err == nil {
+		t.Errorf("aead.CreateKMSEnvelopAEADKeyTemplate(keyURI, invalidTemplate) err = nil, want non-nil")
+	}
+}
+
+func TestCreateKMSEnvelopeAEADKeyTemplateWithUnsupportedTemplateFails(t *testing.T) {
+	keyURI, err := fakekms.NewKeyURI()
+	if err != nil {
+		t.Fatalf("fakekms.NewKeyURI() err = %v", err)
+	}
+	unsupportedTemplate := mac.HMACSHA256Tag128KeyTemplate()
+	if _, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, unsupportedTemplate); err == nil {
+		t.Errorf("aead.CreateKMSEnvelopAEADKeyTemplate(keyURI, unsupportedTemplate) err = nil, want non-nil")
+	}
+}
+
 func testEncryptDecrypt(template *tinkpb.KeyTemplate) error {
 	handle, err := keyset.NewHandle(template)
 	if err != nil {
diff --git a/go/aead/aead_test.go b/go/aead/aead_test.go
index 456991a..b0583cc 100644
--- a/go/aead/aead_test.go
+++ b/go/aead/aead_test.go
@@ -16,66 +16,76 @@
 
 package aead_test
 
+// [START aead-example]
+
 import (
-	"encoding/base64"
+	"bytes"
 	"fmt"
 	"log"
-	"testing"
 
 	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testutil"
 )
 
 func Example() {
-	kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
-	if err != nil {
-		log.Fatal(err)
-	}
+	// A keyset created with "tinkey create-keyset --key-template=AES256_GCM". Note
+	// that this keyset has the secret key information in cleartext.
+	jsonKeyset := `{
+			"key": [{
+					"keyData": {
+							"keyMaterialType":
+									"SYMMETRIC",
+							"typeUrl":
+									"type.googleapis.com/google.crypto.tink.AesGcmKey",
+							"value":
+									"GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+					},
+					"keyId": 294406504,
+					"outputPrefixType": "TINK",
+					"status": "ENABLED"
+			}],
+			"primaryKeyId": 294406504
+	}`
 
-	// TODO: save the keyset to a safe location. DO NOT hardcode it in source code.
+	// Create a keyset handle from the cleartext keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the exposure of accessing the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use a insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
 	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
 	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
-
-	a, err := aead.New(kh)
+	keysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(jsonKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	msg := []byte("this message needs to be encrypted")
-	aad := []byte("this data needs to be authenticated, but not encrypted")
-	ct, err := a.Encrypt(msg, aad)
+	// Retrieve the AEAD primitive we want to use from the keyset handle.
+	primitive, err := aead.New(keysetHandle)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	pt, err := a.Decrypt(ct, aad)
+	// Use the primitive to encrypt a message. In this case the primary key of the
+	// keyset will be used (which is also the only key in this example).
+	plaintext := []byte("message")
+	associatedData := []byte("associated data")
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	fmt.Printf("Ciphertext: %s\n", base64.StdEncoding.EncodeToString(ct))
-	fmt.Printf("Original  plaintext: %s\n", msg)
-	fmt.Printf("Decrypted Plaintext: %s\n", pt)
+	// Use the primitive to decrypt the message. Decrypt finds the correct key in
+	// the keyset and decrypts the ciphertext. If no key is found or decryption
+	// fails, it returns an error.
+	decrypted, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Println(string(decrypted))
+	// Output: message
 }
 
-func TestAEADInit(t *testing.T) {
-	// Check for AES-GCM key manager.
-	_, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-
-	// Check for ChaCha20Poly1305 key manager.
-	_, err = registry.GetKeyManager(testutil.ChaCha20Poly1305TypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-
-	// Check for XChaCha20Poly1305 key manager.
-	_, err = registry.GetKeyManager(testutil.XChaCha20Poly1305TypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-}
+// [END aead-example]
diff --git a/go/aead/aes_gcm_key_manager.go b/go/aead/aes_gcm_key_manager.go
index c74da12..f5142b9 100644
--- a/go/aead/aes_gcm_key_manager.go
+++ b/go/aead/aes_gcm_key_manager.go
@@ -18,6 +18,7 @@
 
 import (
 	"fmt"
+	"io"
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/aead/subtle"
@@ -97,7 +98,7 @@
 	return &tinkpb.KeyData{
 		TypeUrl:         aesGCMTypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
@@ -111,10 +112,41 @@
 	return aesGCMTypeURL
 }
 
+// KeyMaterialType returns the key material type of the key manager.
+func (km *aesGCMKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+func (km *aesGCMKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errInvalidAESGCMKeyFormat
+	}
+	keyFormat := new(gcmpb.AesGcmKeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidAESGCMKeyFormat
+	}
+	if err := km.validateKeyFormat(keyFormat); err != nil {
+		return nil, fmt.Errorf("aes_gcm_key_manager: invalid key format: %s", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), aesGCMKeyVersion); err != nil {
+		return nil, fmt.Errorf("aes_gcm_key_manager: invalid key version: %s", err)
+	}
+
+	keyValue := make([]byte, keyFormat.GetKeySize())
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("aes_gcm_key_manager: not enough pseudorandomness given")
+	}
+
+	return &gcmpb.AesGcmKey{
+		Version:  aesGCMKeyVersion,
+		KeyValue: keyValue,
+	}, nil
+}
+
 // validateKey validates the given AESGCMKey.
 func (km *aesGCMKeyManager) validateKey(key *gcmpb.AesGcmKey) error {
-	err := keyset.ValidateKeyVersion(key.Version, aesGCMKeyVersion)
-	if err != nil {
+	if err := keyset.ValidateKeyVersion(key.Version, aesGCMKeyVersion); err != nil {
 		return fmt.Errorf("aes_gcm_key_manager: %s", err)
 	}
 	keySize := uint32(len(key.KeyValue))
diff --git a/go/aead/aes_gcm_key_manager_test.go b/go/aead/aes_gcm_key_manager_test.go
index 6ea647e..156b98a 100644
--- a/go/aead/aes_gcm_key_manager_test.go
+++ b/go/aead/aes_gcm_key_manager_test.go
@@ -21,9 +21,11 @@
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/aead/subtle"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
 	gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
@@ -38,7 +40,7 @@
 		t.Errorf("cannot obtain AES-GCM key manager: %s", err)
 	}
 	for _, keySize := range keySizes {
-		key := testutil.NewAESGCMKey(testutil.AESGCMKeyVersion, uint32(keySize))
+		key := testutil.NewAESGCMKey(testutil.AESGCMKeyVersion, keySize)
 		serializedKey, _ := proto.Marshal(key)
 		p, err := keyManager.Primitive(serializedKey)
 		if err != nil {
@@ -102,7 +104,7 @@
 		t.Errorf("cannot obtain AES-GCM key manager: %s", err)
 	}
 	for _, keySize := range keySizes {
-		format := testutil.NewAESGCMKeyFormat(uint32(keySize))
+		format := testutil.NewAESGCMKeyFormat(keySize)
 		serializedFormat, _ := proto.Marshal(format)
 		m, err := keyManager.NewKey(serializedFormat)
 		if err != nil {
@@ -144,7 +146,7 @@
 		t.Errorf("cannot obtain AES-GCM key manager: %s", err)
 	}
 	for _, keySize := range keySizes {
-		format := testutil.NewAESGCMKeyFormat(uint32(keySize))
+		format := testutil.NewAESGCMKeyFormat(keySize)
 		serializedFormat, _ := proto.Marshal(format)
 		keyData, err := keyManager.NewKeyData(serializedFormat)
 		if err != nil {
@@ -211,6 +213,167 @@
 	}
 }
 
+func TestAESGCMKeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestAESGCMDeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	for _, test := range []struct {
+		name    string
+		keySize uint32
+	}{
+		{
+			name:    "AES-128-GCM",
+			keySize: 16,
+		},
+		{
+			name:    "AES-256-GCM",
+			keySize: 32,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat := testutil.NewAESGCMKeyFormat(test.keySize)
+			serializedKeyFormat, err := proto.Marshal(keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+			}
+
+			rand := random.GetRandomBytes(test.keySize)
+			buf := &bytes.Buffer{}
+			buf.Write(rand) // never returns a non-nil error
+
+			k, err := keyManager.DeriveKey(serializedKeyFormat, buf)
+			if err != nil {
+				t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+			}
+			key := k.(*gcmpb.AesGcmKey)
+			if got, want := len(key.GetKeyValue()), int(test.keySize); got != want {
+				t.Errorf("key length = %d, want %d", got, want)
+			}
+			if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+				t.Errorf("incorrect derived key: diff = %v", diff)
+			}
+		})
+	}
+}
+
+func TestAESGCMDeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	for _, test := range []struct {
+		name      string
+		keyFormat *gcmpb.AesGcmKeyFormat
+		randLen   uint32
+	}{
+		{
+			name:      "invalid key size",
+			keyFormat: &gcmpb.AesGcmKeyFormat{KeySize: 50, Version: 0},
+			randLen:   50,
+		},
+		{
+			name:      "not enough randomness",
+			keyFormat: &gcmpb.AesGcmKeyFormat{KeySize: 32, Version: 0},
+			randLen:   10,
+		},
+		{
+			name:      "invalid version",
+			keyFormat: &gcmpb.AesGcmKeyFormat{KeySize: 32, Version: 100000},
+			randLen:   32,
+		},
+		{
+			name:      "empty key format",
+			keyFormat: &gcmpb.AesGcmKeyFormat{},
+			randLen:   16,
+		},
+		{
+			name:    "nil key format",
+			randLen: 16,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			serializedKeyFormat, err := proto.Marshal(test.keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", test.keyFormat, err)
+			}
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.randLen))
+			if _, err := keyManager.DeriveKey(serializedKeyFormat, buf); err == nil {
+				t.Error("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestAESGCMDeriveKeyFailsWithMalformedSerializedKeyFormat(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	size := proto.Size(&gcmpb.AesGcmKeyFormat{KeySize: 16, Version: 0})
+	malformedSerializedKeyFormat := random.GetRandomBytes(uint32(size))
+	buf := bytes.NewBuffer(random.GetRandomBytes(32))
+	if _, err := keyManager.DeriveKey(malformedSerializedKeyFormat, buf); err == nil {
+		t.Error("keyManager.DeriveKey() err = nil, want non-nil")
+	}
+}
+
+func TestAESGCMDeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	var keySize uint32 = 16
+	keyFormat, err := proto.Marshal(testutil.NewAESGCMKeyFormat(keySize))
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(keySize))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(keySize - 1))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func genInvalidAESGCMKeys() []proto.Message {
 	return []proto.Message{
 		// not a AESGCMKey
diff --git a/go/aead/internal/BUILD.bazel b/go/aead/internal/BUILD.bazel
new file mode 100644
index 0000000..5440850
--- /dev/null
+++ b/go/aead/internal/BUILD.bazel
@@ -0,0 +1,3 @@
+package(default_visibility = ["//:__subpackages__"])  # keep
+
+licenses(["notice"])  # keep
diff --git a/go/aead/internal/testing/BUILD.bazel b/go/aead/internal/testing/BUILD.bazel
new file mode 100644
index 0000000..5440850
--- /dev/null
+++ b/go/aead/internal/testing/BUILD.bazel
@@ -0,0 +1,3 @@
+package(default_visibility = ["//:__subpackages__"])  # keep
+
+licenses(["notice"])  # keep
diff --git a/go/aead/internal/testing/kmsaead/BUILD.bazel b/go/aead/internal/testing/kmsaead/BUILD.bazel
new file mode 100644
index 0000000..b73b5cc
--- /dev/null
+++ b/go/aead/internal/testing/kmsaead/BUILD.bazel
@@ -0,0 +1,39 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+package(default_visibility = ["//:__subpackages__"])  # keep
+
+licenses(["notice"])  # keep
+
+go_library(
+    name = "kmsaead",
+    testonly = 1,
+    srcs = ["key_manager.go"],
+    importpath = "github.com/google/tink/go/aead/internal/testing/kmsaead",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//core/registry",
+        "//keyset",
+        "//proto/kms_aead_go_proto",
+        "//proto/tink_go_proto",
+        "@org_golang_google_protobuf//proto",
+    ],
+)
+
+go_test(
+    name = "kmsaead_test",
+    testonly = 1,
+    srcs = ["key_manager_test.go"],
+    deps = [
+        ":kmsaead",
+        "//aead",
+        "//core/registry",
+        "//keyset",
+        "//testing/fakekms",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":kmsaead",
+    visibility = ["//aead:__subpackages__"],
+)
diff --git a/go/aead/internal/testing/kmsaead/key_manager.go b/go/aead/internal/testing/kmsaead/key_manager.go
new file mode 100644
index 0000000..c141f08
--- /dev/null
+++ b/go/aead/internal/testing/kmsaead/key_manager.go
@@ -0,0 +1,111 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package kmsaead provides a keymanager for KmsAeadKey that may only be used in tests.
+//
+// Golang currently doesn't implement KmsAeadKey. This is an internal implementation
+// to be used by the cross-language tests.
+package kmsaead
+
+import (
+	"errors"
+	"fmt"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyset"
+	kmsaeadpb "github.com/google/tink/go/proto/kms_aead_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const kmsAEADTypeURL = "type.googleapis.com/google.crypto.tink.KmsAeadKey"
+
+type keyManager struct{}
+
+func (km *keyManager) Primitive(protoSerializedKey []byte) (interface{}, error) {
+	if len(protoSerializedKey) == 0 {
+		return nil, errors.New("kmsaead.keyManager: invalid key")
+	}
+	key := new(kmsaeadpb.KmsAeadKey)
+	if err := proto.Unmarshal(protoSerializedKey, key); err != nil {
+		return nil, errors.New("kmsaead.keyManager: invalid key")
+	}
+	err := keyset.ValidateKeyVersion(key.Version, 0)
+	if err != nil {
+		return nil, errors.New("kmsaead.keyManager: invalid version")
+	}
+	uri := key.GetParams().GetKeyUri()
+	kmsClient, err := registry.GetKMSClient(uri)
+	if err != nil {
+		return nil, err
+	}
+	return kmsClient.GetAEAD(uri)
+}
+
+func (km *keyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errors.New("kmsaead.keyManager: invalid key format")
+	}
+	keyFormat := new(kmsaeadpb.KmsAeadKeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errors.New("kmsaead.keyManager: invalid key format")
+	}
+	return &kmsaeadpb.KmsAeadKey{
+		Version: 0,
+		Params:  keyFormat,
+	}, nil
+}
+
+func (km *keyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	key, err := km.NewKey(serializedKeyFormat)
+	if err != nil {
+		return nil, err
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         kmsAEADTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: tinkpb.KeyData_REMOTE,
+	}, nil
+}
+
+func (km *keyManager) DoesSupport(typeURL string) bool {
+	return typeURL == kmsAEADTypeURL
+}
+
+func (km *keyManager) TypeURL() string {
+	return kmsAEADTypeURL
+}
+
+// NewKeyManager returns a new KeyManager for the KMS AEAD key type.
+func NewKeyManager() registry.KeyManager { return new(keyManager) }
+
+// CreateKeyTemplate creates a new KMS AEAD key template.
+func CreateKeyTemplate(uri string) (*tinkpb.KeyTemplate, error) {
+	f := &kmsaeadpb.KmsAeadKeyFormat{KeyUri: uri}
+	serializedFormat, err := proto.Marshal(f)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal key format: %s", err)
+	}
+	return &tinkpb.KeyTemplate{
+		Value:            serializedFormat,
+		TypeUrl:          kmsAEADTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+	}, nil
+}
diff --git a/go/aead/internal/testing/kmsaead/key_manager_test.go b/go/aead/internal/testing/kmsaead/key_manager_test.go
new file mode 100644
index 0000000..c4c1282
--- /dev/null
+++ b/go/aead/internal/testing/kmsaead/key_manager_test.go
@@ -0,0 +1,88 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package kmsaead_test
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/aead/internal/testing/kmsaead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/testing/fakekms"
+)
+
+// The fake KMS should only be used in tests. It is not secure.
+const keyURI = "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+
+func TestCreateEncryptDecrypt(t *testing.T) {
+	registry.RegisterKeyManager(kmsaead.NewKeyManager())
+
+	client, err := fakekms.NewClient(keyURI)
+	if err != nil {
+		t.Fatalf("fakekms.NewClient(keyURI) err = %q, want nil", err)
+	}
+	registry.RegisterKMSClient(client)
+
+	template, err := kmsaead.CreateKeyTemplate(keyURI)
+	if err != nil {
+		t.Fatalf("kmsaead.CreateKeyTemplate(keyURI) err = %q, want nil", err)
+	}
+	handle, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(template) err = %q, want nil", err)
+	}
+	primitive, err := aead.New(handle)
+	if err != nil {
+		t.Fatalf("aead.New(handle) err = %q, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Encrypt(plaintext, associatedData) err = %q, want nil", err)
+	}
+
+	gotPlaintext, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) err = %q, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext, plaintext) {
+		t.Fatalf("gotPlaintext = %q, want %q", gotPlaintext, plaintext)
+	}
+
+	_, err = primitive.Decrypt(ciphertext, []byte("invalidAssociatedData"))
+	if err == nil {
+		t.Fatalf("primitive.Decrypt(ciphertext, []byte(\"invalidAssociatedData\")) err = nil, want error")
+	}
+
+	// Verify that the AEAD primitive returned by client is also able to decrypt.
+	primitive2, err := client.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("client.GetAEAD(keyURI) err = %q, want nil", err)
+	}
+	gotPlaintext2, err := primitive2.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive2.Decrypt(ciphertext, associatedData) err = %q, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext2, plaintext) {
+		t.Fatalf("gotPlaintext2 = %q, want %q", gotPlaintext, plaintext)
+	}
+}
diff --git a/go/aead/kms_envelope_aead.go b/go/aead/kms_envelope_aead.go
index 05e1c2f..edeef70 100644
--- a/go/aead/kms_envelope_aead.go
+++ b/go/aead/kms_envelope_aead.go
@@ -17,55 +17,76 @@
 package aead
 
 import (
-	"bytes"
 	"encoding/binary"
 	"errors"
 	"fmt"
 
-	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/tink"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 const (
-	lenDEK = 4
+	lenDEK        = 4
+	maxUint32Size = 4294967295
 )
 
 // KMSEnvelopeAEAD represents an instance of Envelope AEAD.
 type KMSEnvelopeAEAD struct {
 	dekTemplate *tinkpb.KeyTemplate
 	remote      tink.AEAD
+	// if err != nil, then the primitive will always fail with this error.
+	// this is needed because NewKMSEnvelopeAEAD2 doesn't return an error.
+	err error
 }
 
-// NewKMSEnvelopeAEAD creates an new instance of KMSEnvelopeAEAD.
-//
-// Deprecated: Use [NewKMSEnvelopeAEAD2] which takes a pointer to a KeyTemplate proto rather than a value.
-func NewKMSEnvelopeAEAD(kt tinkpb.KeyTemplate, remote tink.AEAD) *KMSEnvelopeAEAD {
-	return &KMSEnvelopeAEAD{
-		remote:      remote,
-		dekTemplate: &kt,
-	}
+var tinkAEADKeyTypes map[string]bool = map[string]bool{
+	aesCTRHMACAEADTypeURL:    true,
+	aesGCMTypeURL:            true,
+	chaCha20Poly1305TypeURL:  true,
+	xChaCha20Poly1305TypeURL: true,
+	aesGCMSIVTypeURL:         true,
+}
+
+func isSupporedKMSEnvelopeDEK(dekKeyTypeURL string) bool {
+	_, found := tinkAEADKeyTypes[dekKeyTypeURL]
+	return found
 }
 
 // NewKMSEnvelopeAEAD2 creates an new instance of KMSEnvelopeAEAD.
-func NewKMSEnvelopeAEAD2(kt *tinkpb.KeyTemplate, remote tink.AEAD) *KMSEnvelopeAEAD {
+//
+// dekTemplate must be a KeyTemplate for any of these Tink AEAD key types (any
+// other key template will be rejected):
+//   - AesCtrHmacAeadKey
+//   - AesGcmKey
+//   - ChaCha20Poly1305Key
+//   - XChaCha20Poly1305
+//   - AesGcmSivKey
+func NewKMSEnvelopeAEAD2(dekTemplate *tinkpb.KeyTemplate, remote tink.AEAD) *KMSEnvelopeAEAD {
+	if !isSupporedKMSEnvelopeDEK(dekTemplate.GetTypeUrl()) {
+		return &KMSEnvelopeAEAD{
+			remote:      nil,
+			dekTemplate: nil,
+			err:         fmt.Errorf("unsupported DEK key type %s", dekTemplate.GetTypeUrl()),
+		}
+	}
 	return &KMSEnvelopeAEAD{
 		remote:      remote,
-		dekTemplate: kt,
+		dekTemplate: dekTemplate,
+		err:         nil,
 	}
 }
 
 // Encrypt implements the tink.AEAD interface for encryption.
 func (a *KMSEnvelopeAEAD) Encrypt(pt, aad []byte) ([]byte, error) {
-	dekM, err := registry.NewKey(a.dekTemplate)
+	if a.err != nil {
+		return nil, a.err
+	}
+	dekKeyData, err := registry.NewKeyData(a.dekTemplate)
 	if err != nil {
 		return nil, err
 	}
-	dek, err := proto.Marshal(dekM)
-	if err != nil {
-		return nil, err
-	}
+	dek := dekKeyData.GetValue()
 	encryptedDEK, err := a.remote.Encrypt(dek, []byte{})
 	if err != nil {
 		return nil, err
@@ -83,12 +104,21 @@
 	if err != nil {
 		return nil, err
 	}
-	return buildCipherText(encryptedDEK, payload)
-
+	if len(encryptedDEK) > maxUint32Size {
+		return nil, errors.New("kms_envelope_aead: encrypted dek too large")
+	}
+	res := make([]byte, 0, lenDEK+len(encryptedDEK)+len(payload))
+	res = binary.BigEndian.AppendUint32(res, uint32(len(encryptedDEK)))
+	res = append(res, encryptedDEK...)
+	res = append(res, payload...)
+	return res, nil
 }
 
 // Decrypt implements the tink.AEAD interface for decryption.
 func (a *KMSEnvelopeAEAD) Decrypt(ct, aad []byte) ([]byte, error) {
+	if a.err != nil {
+		return nil, a.err
+	}
 	// Verify we have enough bytes for the length of the encrypted DEK.
 	if len(ct) <= lenDEK {
 		return nil, errors.New("kms_envelope_aead: invalid ciphertext")
@@ -127,28 +157,3 @@
 	// Decrypt the payload.
 	return primitive.Decrypt(payload, aad)
 }
-
-// buildCipherText builds the cipher text by appending the length DEK, encrypted DEK
-// and the encrypted payload.
-func buildCipherText(encryptedDEK, payload []byte) ([]byte, error) {
-	var b bytes.Buffer
-
-	// Write the length of the encrypted DEK.
-	lenDEKbuf := make([]byte, lenDEK)
-	binary.BigEndian.PutUint32(lenDEKbuf, uint32(len(encryptedDEK)))
-	_, err := b.Write(lenDEKbuf)
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = b.Write(encryptedDEK)
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = b.Write(payload)
-	if err != nil {
-		return nil, err
-	}
-	return b.Bytes(), nil
-}
diff --git a/go/aead/kms_envelope_aead_example_test.go b/go/aead/kms_envelope_aead_example_test.go
new file mode 100644
index 0000000..92f78ba
--- /dev/null
+++ b/go/aead/kms_envelope_aead_example_test.go
@@ -0,0 +1,65 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package aead_test
+
+// [START kms-envelope-aead-example]
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/testing/fakekms"
+)
+
+// The fake KMS should only be used in tests. It is not secure.
+const keyURI = "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+
+func Example_kmsEnvelopeAEAD() {
+	// Get a KEK (key encryption key) AEAD. This is usually a remote AEAD to a KMS. In this example,
+	// we use a fake KMS to avoid making RPCs.
+	client, err := fakekms.NewClient(keyURI)
+	if err != nil {
+		log.Fatal(err)
+	}
+	kekAEAD, err := client.GetAEAD(keyURI)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Get the KMS envelope AEAD primitive.
+	primitive := aead.NewKMSEnvelopeAEAD2(aead.AES256GCMKeyTemplate(), kekAEAD)
+
+	// Use the primitive.
+	plaintext := []byte("message")
+	associatedData := []byte("example KMS envelope AEAD encryption")
+
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	decrypted, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(string(decrypted))
+	// Output: message
+}
+
+// [END kms-envelope-aead-example]
+
diff --git a/go/aead/kms_envelope_aead_key_manager.go b/go/aead/kms_envelope_aead_key_manager.go
index ae894ff..cd738db 100644
--- a/go/aead/kms_envelope_aead_key_manager.go
+++ b/go/aead/kms_envelope_aead_key_manager.go
@@ -70,6 +70,11 @@
 	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
 		return nil, errors.New("kms_envelope_aead_key_manager: invalid key format")
 	}
+	dekKeyType := keyFormat.GetDekTemplate().GetTypeUrl()
+	if !isSupporedKMSEnvelopeDEK(dekKeyType) {
+		return nil, fmt.Errorf("unsupported DEK key type %s. Only Tink AEAD key types are supported with KMSEnvelopeAEAD", dekKeyType)
+	}
+
 	return &kmsepb.KmsEnvelopeAeadKey{
 		Version: kmsEnvelopeAEADKeyVersion,
 		Params:  keyFormat,
@@ -111,5 +116,9 @@
 	if err != nil {
 		return fmt.Errorf("kms_envelope_aead_key_manager: %s", err)
 	}
+	dekKeyType := key.GetParams().GetDekTemplate().GetTypeUrl()
+	if !isSupporedKMSEnvelopeDEK(dekKeyType) {
+		return fmt.Errorf("unsupported DEK key type %s. Only Tink AEAD key types are supported with KMSEnvelopeAEAD", dekKeyType)
+	}
 	return nil
 }
diff --git a/go/aead/kms_envelope_aead_key_manager_test.go b/go/aead/kms_envelope_aead_key_manager_test.go
new file mode 100644
index 0000000..1df63b7
--- /dev/null
+++ b/go/aead/kms_envelope_aead_key_manager_test.go
@@ -0,0 +1,56 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package aead_test
+
+import (
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/testing/fakekms"
+	"github.com/google/tink/go/testutil"
+	kmsenvpb "github.com/google/tink/go/proto/kms_envelope_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func TestNewKMSEnvelopeAEADKeyWithInvalidDEK(t *testing.T) {
+	keyURI, err := fakekms.NewKeyURI()
+	if err != nil {
+		t.Fatalf("fakekms.NewKeyURI() err = %v", err)
+	}
+
+	// Create a KmsEnvelopeAeadKeyFormat with a DekTemplate that is not supported.
+	format := &kmsenvpb.KmsEnvelopeAeadKeyFormat{
+		KekUri:      keyURI,
+		DekTemplate: mac.HMACSHA256Tag128KeyTemplate(),
+	}
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		t.Fatalf("failed to marshal key format: %s", err)
+	}
+	keyTemplate := &tinkpb.KeyTemplate{
+		Value:            serializedFormat,
+		TypeUrl:          testutil.KMSEnvelopeAEADTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+	}
+
+	_, err = keyset.NewHandle(keyTemplate)
+	if err == nil {
+		t.Errorf("keyset.NewHandle(keyTemplate) err = nil, want error")
+	}
+}
diff --git a/go/aead/kms_envelope_aead_test.go b/go/aead/kms_envelope_aead_test.go
index 8399bb6..4d50dc8 100644
--- a/go/aead/kms_envelope_aead_test.go
+++ b/go/aead/kms_envelope_aead_test.go
@@ -17,56 +17,124 @@
 package aead_test
 
 import (
+	"bytes"
+	"log"
 	"testing"
 
 	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/tink"
+	"github.com/google/tink/go/testing/fakekms"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
-func createKMSEnvelopeAEAD(t *testing.T) tink.AEAD {
-	t.Helper()
-
-	kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
+func TestKMSEnvelopeWorksWithTinkKeyTemplatesAsDekTemplate(t *testing.T) {
+	keyURI := "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+	client, err := fakekms.NewClient(keyURI)
 	if err != nil {
-		t.Fatalf("failed to create new handle: %v", err)
+		log.Fatal(err)
 	}
-
-	parentAEAD, err := aead.New(kh)
+	kekAEAD, err := client.GetAEAD(keyURI)
 	if err != nil {
-		t.Fatalf("failed to create parent AEAD: %v", err)
+		log.Fatal(err)
 	}
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	invalidAssociatedData := []byte("invalidAssociatedData")
 
-	return aead.NewKMSEnvelopeAEAD2(aead.AES256GCMKeyTemplate(), parentAEAD)
+	var kmsEnvelopeAeadDekTestCases = []struct {
+		name        string
+		dekTemplate *tinkpb.KeyTemplate
+	}{
+		{
+			name:        "AES128_GCM",
+			dekTemplate: aead.AES128GCMKeyTemplate(),
+		}, {
+			name:        "AES256_GCM",
+			dekTemplate: aead.AES256GCMKeyTemplate(),
+		}, {
+			name:        "AES256_GCM_NO_PREFIX",
+			dekTemplate: aead.AES256GCMNoPrefixKeyTemplate(),
+		}, {
+			name:        "AES128_GCM_SIV",
+			dekTemplate: aead.AES128GCMSIVKeyTemplate(),
+		}, {
+			name:        "AES256_GCM_SIV",
+			dekTemplate: aead.AES256GCMSIVKeyTemplate(),
+		}, {
+			name:        "AES256_GCM_SIV_NO_PREFIX",
+			dekTemplate: aead.AES256GCMSIVNoPrefixKeyTemplate(),
+		}, {
+			name:        "AES128_CTR_HMAC_SHA256",
+			dekTemplate: aead.AES128CTRHMACSHA256KeyTemplate(),
+		}, {
+			name:        "AES256_CTR_HMAC_SHA256",
+			dekTemplate: aead.AES256CTRHMACSHA256KeyTemplate(),
+		}, {
+			name:        "CHACHA20_POLY1305",
+			dekTemplate: aead.ChaCha20Poly1305KeyTemplate(),
+		}, {
+			name:        "XCHACHA20_POLY1305",
+			dekTemplate: aead.XChaCha20Poly1305KeyTemplate(),
+		},
+	}
+	for _, tc := range kmsEnvelopeAeadDekTestCases {
+		t.Run(tc.name, func(t *testing.T) {
+			a := aead.NewKMSEnvelopeAEAD2(tc.dekTemplate, kekAEAD)
+			ciphertext, err := a.Encrypt(plaintext, associatedData)
+			if err != nil {
+				t.Fatalf("a.Encrypt(plaintext, associatedData) err = %q, want nil", err)
+			}
+			gotPlaintext, err := a.Decrypt(ciphertext, associatedData)
+			if err != nil {
+				t.Fatalf("a.Decrypt(ciphertext, associatedData) err = %q, want nil", err)
+			}
+			if !bytes.Equal(gotPlaintext, plaintext) {
+				t.Fatalf("got plaintext %q, want %q", gotPlaintext, plaintext)
+			}
+			if _, err = a.Decrypt(ciphertext, invalidAssociatedData); err == nil {
+				t.Error("a.Decrypt(ciphertext, invalidAssociatedData) err = nil, want error")
+			}
+		})
+	}
 }
 
-func TestKMSEnvelopeRoundtrip(t *testing.T) {
-	a := createKMSEnvelopeAEAD(t)
-
-	originalPlaintext := "hello world"
-
-	ciphertext, err := a.Encrypt([]byte(originalPlaintext), nil)
+func TestKMSEnvelopeWithKmsEnvelopeKeyTemplatesAsDekTemplate_fails(t *testing.T) {
+	keyURI := "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+	client, err := fakekms.NewClient(keyURI)
 	if err != nil {
-		t.Fatalf("failed to encrypt: %v", err)
+		t.Fatalf("fakekms.NewClient(keyURI) err = %q, want nil", err)
+	}
+	kekAEAD, err := client.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("client.GetAEAD(keyURI) err = %q, want nil", err)
+	}
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+
+	// Use a KmsEnvelopeAeadKeyTemplate as DEK template.
+	dekTemplate, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("aead.CreateKMSEnvelopAEADKeyTemplate() err = %q, want nil", err)
 	}
 
-	plaintextBytes, err := a.Decrypt(ciphertext, nil)
-	if err != nil {
-		t.Fatalf("failed to decrypt: %v", err)
-	}
-	plaintext := string(plaintextBytes)
-
-	if plaintext != originalPlaintext {
-		t.Errorf("Decrypt(Encrypt(%q)) = %q; want %q", originalPlaintext, plaintext, originalPlaintext)
+	a := aead.NewKMSEnvelopeAEAD2(dekTemplate, kekAEAD)
+	_, err = a.Encrypt(plaintext, associatedData)
+	if err == nil {
+		t.Error("a.Encrypt(plaintext, associatedData) err = nil, want error")
 	}
 }
 
 func TestKMSEnvelopeShortCiphertext(t *testing.T) {
-	a := createKMSEnvelopeAEAD(t)
-
-	_, err := a.Decrypt([]byte{1}, nil)
-	if err == nil {
-		t.Errorf("Decrypt({1}) worked, but should've errored out")
+	keyURI := "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+	client, err := fakekms.NewClient(keyURI)
+	if err != nil {
+		log.Fatal(err)
 	}
-
+	kekAEAD, err := client.GetAEAD(keyURI)
+	if err != nil {
+		log.Fatal(err)
+	}
+	a := aead.NewKMSEnvelopeAEAD2(aead.AES256GCMKeyTemplate(), kekAEAD)
+	if _, err = a.Decrypt([]byte{1}, nil); err == nil {
+		t.Error("a.Decrypt([]byte{1}, nil) err = nil, want error")
+	}
 }
diff --git a/go/aead/subtle/chacha20poly1305.go b/go/aead/subtle/chacha20poly1305.go
index c74f8cb..2ed87f7 100644
--- a/go/aead/subtle/chacha20poly1305.go
+++ b/go/aead/subtle/chacha20poly1305.go
@@ -17,10 +17,10 @@
 package subtle
 
 import (
+	"errors"
 	"fmt"
 
 	"golang.org/x/crypto/chacha20poly1305"
-	internalaead "github.com/google/tink/go/internal/aead"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/tink"
 )
@@ -31,8 +31,7 @@
 
 // ChaCha20Poly1305 is an implementation of AEAD interface.
 type ChaCha20Poly1305 struct {
-	Key                           []byte
-	chaCha20Poly1305InsecureNonce *internalaead.ChaCha20Poly1305InsecureNonce
+	Key []byte
 }
 
 // Assert that ChaCha20Poly1305 implements the AEAD interface.
@@ -41,30 +40,54 @@
 // NewChaCha20Poly1305 returns an ChaCha20Poly1305 instance.
 // The key argument should be a 32-bytes key.
 func NewChaCha20Poly1305(key []byte) (*ChaCha20Poly1305, error) {
-	chaCha20Poly1305InsecureNonce, err := internalaead.NewChaCha20Poly1305InsecureNonce(key)
-	return &ChaCha20Poly1305{
-		Key:                           key,
-		chaCha20Poly1305InsecureNonce: chaCha20Poly1305InsecureNonce,
-	}, err
+	if len(key) != chacha20poly1305.KeySize {
+		return nil, errors.New("chacha20poly1305: bad key length")
+	}
+
+	return &ChaCha20Poly1305{Key: key}, nil
 }
 
 // Encrypt encrypts plaintext with associatedData.
+//
 // The resulting ciphertext consists of two parts:
-// (1) the nonce used for encryption and (2) the actual ciphertext.
+//  1. the nonce used for encryption
+//  2. the actual ciphertext
 func (ca *ChaCha20Poly1305) Encrypt(plaintext []byte, associatedData []byte) ([]byte, error) {
-	nonce := random.GetRandomBytes(chacha20poly1305.NonceSize)
-	ct, err := ca.chaCha20Poly1305InsecureNonce.Encrypt(nonce, plaintext, associatedData)
+	if len(plaintext) > maxInt-chacha20poly1305.NonceSize-poly1305TagSize {
+		return nil, fmt.Errorf("chacha20poly1305: plaintext too long")
+	}
+	c, err := chacha20poly1305.New(ca.Key)
 	if err != nil {
 		return nil, err
 	}
-	return append(nonce, ct...), nil
+
+	nonce := random.GetRandomBytes(chacha20poly1305.NonceSize)
+	// Set dst's capacity to fit the nonce and ciphertext.
+	dst := make([]byte, 0, chacha20poly1305.NonceSize+len(plaintext)+c.Overhead())
+	dst = append(dst, nonce...)
+	// Seal appends the ciphertext to dst. So the final output is: nonce || ciphertext.
+	return c.Seal(dst, nonce, plaintext, associatedData), nil
 }
 
 // Decrypt decrypts ciphertext with associatedData.
+// 
+// ciphertext consists of two parts:
+//  1. the nonce used for encryption
+//  2. the actual ciphertext
 func (ca *ChaCha20Poly1305) Decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
 	if len(ciphertext) < chacha20poly1305.NonceSize+poly1305TagSize {
 		return nil, fmt.Errorf("chacha20poly1305: ciphertext too short")
 	}
+
+	c, err := chacha20poly1305.New(ca.Key)
+	if err != nil {
+		return nil, err
+	}
+
 	nonce := ciphertext[:chacha20poly1305.NonceSize]
-	return ca.chaCha20Poly1305InsecureNonce.Decrypt(nonce, ciphertext[chacha20poly1305.NonceSize:], associatedData)
+	pt, err := c.Open(nil, nonce, ciphertext[chacha20poly1305.NonceSize:] /*=ciphertext*/, associatedData)
+	if err != nil {
+		return nil, fmt.Errorf("ChaCha20Poly1305.Decrypt: %v", err)
+	}
+	return pt, nil
 }
diff --git a/go/aead/subtle/encrypt_then_authenticate.go b/go/aead/subtle/encrypt_then_authenticate.go
index c42469c..4efc71e 100644
--- a/go/aead/subtle/encrypt_then_authenticate.go
+++ b/go/aead/subtle/encrypt_then_authenticate.go
@@ -70,10 +70,12 @@
 		return nil, fmt.Errorf("encrypt_then_authenticate: %v", err)
 	}
 
-	toAuthData := append(associatedData, ciphertext...)
 	adSizeInBits := uint64(len(associatedData)) * 8
-	toAuthData = append(toAuthData, uint64ToByte(adSizeInBits)...)
-
+	adSizeInBitsEncoded := uint64ToByte(adSizeInBits)
+	toAuthData := make([]byte, 0, len(associatedData)+len(ciphertext)+len(adSizeInBitsEncoded))
+	toAuthData = append(toAuthData, associatedData...)
+	toAuthData = append(toAuthData, ciphertext...)
+	toAuthData = append(toAuthData, adSizeInBitsEncoded...)
 	tag, err := e.mac.ComputeMAC(toAuthData)
 	if err != nil {
 		return nil, fmt.Errorf("encrypt_then_authenticate: %v", err)
@@ -98,9 +100,12 @@
 
 	// Authenticate the following data:
 	// associatedData || payload || adSizeInBits
-	toAuthData := append(associatedData, payload...)
 	adSizeInBits := uint64(len(associatedData)) * 8
-	toAuthData = append(toAuthData, uint64ToByte(adSizeInBits)...)
+	adSizeInBitsEncoded := uint64ToByte(adSizeInBits)
+	toAuthData := make([]byte, 0, len(associatedData)+len(payload)+len(adSizeInBitsEncoded))
+	toAuthData = append(toAuthData, associatedData...)
+	toAuthData = append(toAuthData, payload...)
+	toAuthData = append(toAuthData, adSizeInBitsEncoded...)
 
 	err := e.mac.VerifyMAC(ciphertext[len(ciphertext)-e.tagSize:], toAuthData)
 	if err != nil {
diff --git a/go/aead/subtle/encrypt_then_authenticate_test.go b/go/aead/subtle/encrypt_then_authenticate_test.go
index 63678c7..f80b341 100644
--- a/go/aead/subtle/encrypt_then_authenticate_test.go
+++ b/go/aead/subtle/encrypt_then_authenticate_test.go
@@ -67,9 +67,10 @@
 
 // Copied from
 // https://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05.
+//
 // We use CTR but the RFC uses CBC mode, so it's not possible to compare
-// plaintexts. However, the tests are still valueable to ensure that we correcly
-// compute HMAC over ciphertext and associatedData.
+// plaintexts. However, the tests are still valueable to ensure that we
+// correcly compute HMAC over ciphertext and associatedData.
 var rfcTestVectors = []struct {
 	macKey         string
 	encryptionKey  string
@@ -79,9 +80,11 @@
 	ivSize         int
 	tagSize        int
 }{
-	{"000102030405060708090a0b0c0d0e0f",
-		"101112131415161718191a1b1c1d1e1f",
-		"1af38c2dc2b96ffdd86694092341bc04" +
+	{
+		macKey:        "000102030405060708090a0b0c0d0e0f",
+		encryptionKey: "101112131415161718191a1b1c1d1e1f",
+		ciphertext: "" +
+			"1af38c2dc2b96ffdd86694092341bc04" +
 			"c80edfa32ddf39d5ef00c0b468834279" +
 			"a2e46a1b8049f792f76bfe54b903a9c9" +
 			"a94ac9b47ad2655c5f10f9aef71427e2" +
@@ -92,13 +95,19 @@
 			"bd34d848b3d69550a67646344427ade5" +
 			"4b8851ffb598f7f80074b9473c82e2db" +
 			"652c3fa36b0a7c5b3219fab3a30bc1c4",
-		"546865207365636f6e64207072696e63" +
+		associatedData: "" +
+			"546865207365636f6e64207072696e63" +
 			"69706c65206f66204175677573746520" +
 			"4b6572636b686f666673",
-		"SHA256", 16, 16},
-	{"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
-		"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
-		"1af38c2dc2b96ffdd86694092341bc04" +
+		hashAlgo: "SHA256",
+		ivSize:   16,
+		tagSize:  16,
+	},
+	{
+		macKey:        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+		encryptionKey: "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
+		ciphertext: "" +
+			"1af38c2dc2b96ffdd86694092341bc04" +
 			"4affaaadb78c31c5da4b1b590d10ffbd" +
 			"3dd8d5d302423526912da037ecbcc7bd" +
 			"822c301dd67c373bccb584ad3e9279c2" +
@@ -110,26 +119,31 @@
 			"be2638d09dd7a4930930806d0703b1f6" +
 			"4dd3b4c088a7f45c216839645b2012bf" +
 			"2e6269a8c56a816dbc1b267761955bc5",
-		"546865207365636f6e64207072696e63" +
+		associatedData: "" +
+			"546865207365636f6e64207072696e63" +
 			"69706c65206f66204175677573746520" +
 			"4b6572636b686f666673",
-		"SHA512", 16, 32},
+		hashAlgo: "SHA512",
+		ivSize:   16,
+		tagSize:  32,
+	},
 }
 
-func hexDecodeOrDie(data string) []byte {
+func hexDecode(t *testing.T, data string) []byte {
+	t.Helper()
 	decoded, err := hex.DecodeString(data)
 	if err != nil {
-		panic(err)
+		t.Fatal(err)
 	}
 	return decoded
 }
 
 func TestETARFCTestVectors(t *testing.T) {
 	for _, v := range rfcTestVectors {
-		macKey := hexDecodeOrDie(v.macKey)
-		encryptionKey := hexDecodeOrDie(v.encryptionKey)
-		ciphertext := hexDecodeOrDie(v.ciphertext)
-		associatedData := hexDecodeOrDie(v.associatedData)
+		macKey := hexDecode(t, v.macKey)
+		encryptionKey := hexDecode(t, v.encryptionKey)
+		ciphertext := hexDecode(t, v.ciphertext)
+		associatedData := hexDecode(t, v.associatedData)
 
 		cipher, err := createAEADWithKeys(encryptionKey, v.ivSize, v.hashAlgo, macKey, v.tagSize)
 		if err != nil {
@@ -175,6 +189,31 @@
 	}
 }
 
+func TestETAWithAssociatedDataSlice(t *testing.T) {
+	const keySize = 16
+	const ivSize = 12
+	const macKeySize = 16
+	const tagSize = 16
+	cipher, err := createAEAD(keySize, ivSize, "SHA1", macKeySize, tagSize)
+	if err != nil {
+		t.Fatalf("got: %v, want: success", err)
+	}
+
+	message := []byte("message")
+	largeData := []byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
+	associatedData := largeData[:1]
+
+	_, err = cipher.Encrypt(message, associatedData)
+	if err != nil {
+		t.Fatalf("encryption failed, error: %v", err)
+	}
+
+	wantLargeData := []byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
+	if !bytes.Equal(largeData, wantLargeData) {
+		t.Errorf("largeData = %q, want: %q", largeData, wantLargeData)
+	}
+}
+
 func TestETAEncryptDecryptRandomMessage(t *testing.T) {
 	const keySize = 16
 	const ivSize = 12
diff --git a/go/aead/subtle/xchacha20poly1305.go b/go/aead/subtle/xchacha20poly1305.go
index ec97fd8..fb6a450 100644
--- a/go/aead/subtle/xchacha20poly1305.go
+++ b/go/aead/subtle/xchacha20poly1305.go
@@ -44,8 +44,10 @@
 }
 
 // Encrypt encrypts plaintext with associatedData.
+//
 // The resulting ciphertext consists of two parts:
-// (1) the nonce used for encryption and (2) the actual ciphertext.
+//  1. the nonce used for encryption
+//  2. the actual ciphertext
 func (x *XChaCha20Poly1305) Encrypt(plaintext []byte, associatedData []byte) ([]byte, error) {
 	if len(plaintext) > maxInt-chacha20poly1305.NonceSizeX-poly1305TagSize {
 		return nil, fmt.Errorf("xchacha20poly1305: plaintext too long")
@@ -55,12 +57,19 @@
 		return nil, err
 	}
 
-	n := x.newNonce()
-	ct := c.Seal(nil, n, plaintext, associatedData)
-	return append(n, ct...), nil
+	nounce := random.GetRandomBytes(chacha20poly1305.NonceSizeX)
+	// Make the capacity of dst large enough so that both the nounce and the ciphertext fit inside.
+	dst := make([]byte, 0, chacha20poly1305.NonceSizeX+len(plaintext)+c.Overhead())
+	dst = append(dst, nounce...)
+	// Seal appends the ciphertext to dst. So the final output is: nounce || ciphertext.
+	return c.Seal(dst, nounce, plaintext, associatedData), nil
 }
 
 // Decrypt decrypts ciphertext with associatedData.
+//
+// ciphertext consists of two parts:
+//  1. the nonce used for encryption
+//  2. the actual ciphertext
 func (x *XChaCha20Poly1305) Decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
 	if len(ciphertext) < chacha20poly1305.NonceSizeX+poly1305TagSize {
 		return nil, fmt.Errorf("xchacha20poly1305: ciphertext too short")
@@ -78,8 +87,3 @@
 	}
 	return pt, nil
 }
-
-// newNonce creates a new nonce for encryption.
-func (x *XChaCha20Poly1305) newNonce() []byte {
-	return random.GetRandomBytes(chacha20poly1305.NonceSizeX)
-}
diff --git a/go/aead/subtle/xchacha20poly1305_test.go b/go/aead/subtle/xchacha20poly1305_test.go
index 37f82eb..a382d4d 100644
--- a/go/aead/subtle/xchacha20poly1305_test.go
+++ b/go/aead/subtle/xchacha20poly1305_test.go
@@ -250,3 +250,25 @@
 		}
 	}
 }
+
+func TestPreallocatedCiphertextMemoryInXChaCha20Poly1305IsExact(t *testing.T) {
+	key := random.GetRandomBytes(chacha20poly1305.KeySize)
+	a, err := subtle.NewXChaCha20Poly1305(key)
+	if err != nil {
+		t.Fatalf("aead.NewAESGCMInsecureIV() err = %v, want nil", err)
+	}
+	plaintext := random.GetRandomBytes(13)
+	associatedData := random.GetRandomBytes(17)
+
+	ciphertext, err := a.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Encrypt() err = %v, want nil", err)
+	}
+	// Encrypt() uses cipher.Overhead() to pre-allocate the memory needed store the ciphertext.
+	// For ChaCha20Poly1305, the size of the allocated memory should always be exact. If this check
+	// fails, the pre-allocated memory was too large or too small. If it was too small, the system had
+	// to re-allocate more memory, which is expensive and should be avoided.
+	if len(ciphertext) != cap(ciphertext) {
+		t.Errorf("want len(ciphertext) == cap(ciphertext), got %d != %d", len(ciphertext), cap(ciphertext))
+	}
+}
diff --git a/go/aead/xchacha20poly1305_key_manager.go b/go/aead/xchacha20poly1305_key_manager.go
index 4a538b1..6badc96 100644
--- a/go/aead/xchacha20poly1305_key_manager.go
+++ b/go/aead/xchacha20poly1305_key_manager.go
@@ -18,6 +18,7 @@
 
 import (
 	"fmt"
+	"io"
 
 	"golang.org/x/crypto/chacha20poly1305"
 	"google.golang.org/protobuf/proto"
@@ -26,8 +27,8 @@
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/subtle/random"
 
-	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
-	xcppb "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
+	xpb "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto"
 )
 
 const (
@@ -35,22 +36,25 @@
 	xChaCha20Poly1305TypeURL    = "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key"
 )
 
-// Common errors.
-var errInvalidXChaCha20Poly1305Key = fmt.Errorf("xchacha20poly1305_key_manager: invalid key")
+var (
+	errInvalidXChaCha20Poly1305Key       = fmt.Errorf("xchacha20poly1305_key_manager: invalid key")
+	errInvalidXChaCha20Poly1305KeyFormat = fmt.Errorf("xchacha20poly1305_key_manager: invalid key format")
+)
 
-// xChaCha20Poly1305KeyManager is an implementation of KeyManager interface.
-// It generates new XChaCha20Poly1305Key keys and produces new instances of XChaCha20Poly1305 subtle.
+// xChaCha20Poly1305KeyManager generates XChaCha20Poly1305Key keys and produces
+// instances of XChaCha20Poly1305.
 type xChaCha20Poly1305KeyManager struct{}
 
 // Assert that xChaCha20Poly1305KeyManager implements the KeyManager interface.
 var _ registry.KeyManager = (*xChaCha20Poly1305KeyManager)(nil)
 
-// Primitive creates an XChaCha20Poly1305 subtle for the given serialized XChaCha20Poly1305Key proto.
+// Primitive constructs a XChaCha20Poly1305 for the given serialized
+// XChaCha20Poly1305Key.
 func (km *xChaCha20Poly1305KeyManager) Primitive(serializedKey []byte) (interface{}, error) {
 	if len(serializedKey) == 0 {
 		return nil, errInvalidXChaCha20Poly1305Key
 	}
-	key := new(xcppb.XChaCha20Poly1305Key)
+	key := new(xpb.XChaCha20Poly1305Key)
 	if err := proto.Unmarshal(serializedKey, key); err != nil {
 		return nil, errInvalidXChaCha20Poly1305Key
 	}
@@ -59,60 +63,85 @@
 	}
 	ret, err := subtle.NewXChaCha20Poly1305(key.KeyValue)
 	if err != nil {
-		return nil, fmt.Errorf("xchacha20poly1305_key_manager: cannot create new primitive: %s", err)
+		return nil, fmt.Errorf("xchacha20poly1305_key_manager: cannot create new primitive: %v", err)
 	}
 	return ret, nil
 }
 
-// NewKey creates a new key, ignoring the specification in the given serialized key format
+// NewKey generates a new XChaCha20Poly1305Key. It ignores serializedKeyFormat
 // because the key size and other params are fixed.
 func (km *xChaCha20Poly1305KeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
-	return km.newXChaCha20Poly1305Key(), nil
+	return &xpb.XChaCha20Poly1305Key{
+		Version:  xChaCha20Poly1305KeyVersion,
+		KeyValue: random.GetRandomBytes(chacha20poly1305.KeySize),
+	}, nil
 }
 
-// NewKeyData creates a new KeyData, ignoring the specification in the given serialized key format
-// because the key size and other params are fixed.
-// It should be used solely by the key management API.
-func (km *xChaCha20Poly1305KeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
-	key := km.newXChaCha20Poly1305Key()
+// NewKeyData generates a new KeyData. It ignores serializedKeyFormat because
+// the key size and other params are fixed. This should be used solely by the
+// key management API.
+func (km *xChaCha20Poly1305KeyManager) NewKeyData(serializedKeyFormat []byte) (*tpb.KeyData, error) {
+	key := &xpb.XChaCha20Poly1305Key{
+		Version:  xChaCha20Poly1305KeyVersion,
+		KeyValue: random.GetRandomBytes(chacha20poly1305.KeySize),
+	}
 	serializedKey, err := proto.Marshal(key)
 	if err != nil {
 		return nil, err
 	}
-	return &tinkpb.KeyData{
+	return &tpb.KeyData{
 		TypeUrl:         xChaCha20Poly1305TypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
-// DoesSupport indicates if this key manager supports the given key type.
+// DoesSupport checks whether this key manager supports the given key type.
 func (km *xChaCha20Poly1305KeyManager) DoesSupport(typeURL string) bool {
 	return typeURL == xChaCha20Poly1305TypeURL
 }
 
-// TypeURL returns the key type of keys managed by this key manager.
+// TypeURL returns the type URL of keys managed by this key manager.
 func (km *xChaCha20Poly1305KeyManager) TypeURL() string {
 	return xChaCha20Poly1305TypeURL
 }
 
-func (km *xChaCha20Poly1305KeyManager) newXChaCha20Poly1305Key() *xcppb.XChaCha20Poly1305Key {
-	keyValue := random.GetRandomBytes(chacha20poly1305.KeySize)
-	return &xcppb.XChaCha20Poly1305Key{
+// KeyMaterialType returns the key material type of this key manager.
+func (km *xChaCha20Poly1305KeyManager) KeyMaterialType() tpb.KeyData_KeyMaterialType {
+	return tpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+// Unlike NewKey, DeriveKey validates serializedKeyFormat's version.
+func (km *xChaCha20Poly1305KeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	keyFormat := new(xpb.XChaCha20Poly1305KeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, fmt.Errorf("xchacha20poly1305_key_manager: %v", err)
+	}
+	err := keyset.ValidateKeyVersion(keyFormat.Version, xChaCha20Poly1305KeyVersion)
+	if err != nil {
+		return nil, fmt.Errorf("xchacha20poly1305_key_manager: %v", err)
+	}
+
+	keyValue := make([]byte, chacha20poly1305.KeySize)
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("xchacha20poly1305_key_manager: not enough pseudorandomness given")
+	}
+	return &xpb.XChaCha20Poly1305Key{
 		Version:  xChaCha20Poly1305KeyVersion,
 		KeyValue: keyValue,
-	}
+	}, nil
 }
 
 // validateKey validates the given XChaCha20Poly1305Key.
-func (km *xChaCha20Poly1305KeyManager) validateKey(key *xcppb.XChaCha20Poly1305Key) error {
+func (km *xChaCha20Poly1305KeyManager) validateKey(key *xpb.XChaCha20Poly1305Key) error {
 	err := keyset.ValidateKeyVersion(key.Version, xChaCha20Poly1305KeyVersion)
 	if err != nil {
-		return fmt.Errorf("xchacha20poly1305_key_manager: %s", err)
+		return fmt.Errorf("xchacha20poly1305_key_manager: %v", err)
 	}
 	keySize := uint32(len(key.KeyValue))
 	if keySize != chacha20poly1305.KeySize {
-		return fmt.Errorf("xchacha20poly1305_key_manager: keySize != %d", chacha20poly1305.KeySize)
+		return fmt.Errorf("xchacha20poly1305_key_manager: key size != %d", chacha20poly1305.KeySize)
 	}
 	return nil
 }
diff --git a/go/aead/xchacha20poly1305_key_manager_test.go b/go/aead/xchacha20poly1305_key_manager_test.go
index 3450e82..ab4e843 100644
--- a/go/aead/xchacha20poly1305_key_manager_test.go
+++ b/go/aead/xchacha20poly1305_key_manager_test.go
@@ -18,18 +18,21 @@
 
 import (
 	"bytes"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"golang.org/x/crypto/chacha20poly1305"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
 
 	"github.com/google/tink/go/aead/subtle"
-	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
-	xcppb "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
+	xpb "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto"
 )
 
 func TestXChaCha20Poly1305GetPrimitive(t *testing.T) {
@@ -38,7 +41,7 @@
 		t.Errorf("cannot obtain XChaCha20Poly1305 key manager: %s", err)
 	}
 	m, _ := km.NewKey(nil)
-	key, _ := m.(*xcppb.XChaCha20Poly1305Key)
+	key, _ := m.(*xpb.XChaCha20Poly1305Key)
 	serializedKey, _ := proto.Marshal(key)
 	p, err := km.Primitive(serializedKey)
 	if err != nil {
@@ -54,7 +57,26 @@
 	if err != nil {
 		t.Errorf("cannot obtain XChaCha20Poly1305 key manager: %s", err)
 	}
-	invalidKeys := genInvalidXChaCha20Poly1305Keys()
+	invalidKeys := []*xpb.XChaCha20Poly1305Key{
+		// Bad key size.
+		&xpb.XChaCha20Poly1305Key{
+			Version:  testutil.XChaCha20Poly1305KeyVersion,
+			KeyValue: random.GetRandomBytes(17),
+		},
+		&xpb.XChaCha20Poly1305Key{
+			Version:  testutil.XChaCha20Poly1305KeyVersion,
+			KeyValue: random.GetRandomBytes(25),
+		},
+		&xpb.XChaCha20Poly1305Key{
+			Version:  testutil.XChaCha20Poly1305KeyVersion,
+			KeyValue: random.GetRandomBytes(33),
+		},
+		// Bad version.
+		&xpb.XChaCha20Poly1305Key{
+			Version:  testutil.XChaCha20Poly1305KeyVersion + 1,
+			KeyValue: random.GetRandomBytes(chacha20poly1305.KeySize),
+		},
+	}
 	for _, key := range invalidKeys {
 		serializedKey, _ := proto.Marshal(key)
 		if _, err := km.Primitive(serializedKey); err == nil {
@@ -72,7 +94,7 @@
 	if err != nil {
 		t.Errorf("km.NewKey(nil) = _, %v; want _, nil", err)
 	}
-	key, _ := m.(*xcppb.XChaCha20Poly1305Key)
+	key, _ := m.(*xpb.XChaCha20Poly1305Key)
 	if err := validateXChaCha20Poly1305Key(key); err != nil {
 		t.Errorf("validateXChaCha20Poly1305Key(%v) = %v; want nil", key, err)
 	}
@@ -90,10 +112,10 @@
 	if kd.TypeUrl != testutil.XChaCha20Poly1305TypeURL {
 		t.Errorf("TypeUrl: %v != %v", kd.TypeUrl, testutil.XChaCha20Poly1305TypeURL)
 	}
-	if kd.KeyMaterialType != tinkpb.KeyData_SYMMETRIC {
+	if kd.KeyMaterialType != tpb.KeyData_SYMMETRIC {
 		t.Errorf("KeyMaterialType: %v != SYMMETRIC", kd.KeyMaterialType)
 	}
-	key := new(xcppb.XChaCha20Poly1305Key)
+	key := new(xpb.XChaCha20Poly1305Key)
 	if err := proto.Unmarshal(kd.Value, key); err != nil {
 		t.Errorf("proto.Unmarshal(%v, key) = %v; want nil", kd.Value, err)
 	}
@@ -125,30 +147,140 @@
 	}
 }
 
-func genInvalidXChaCha20Poly1305Keys() []*xcppb.XChaCha20Poly1305Key {
-	return []*xcppb.XChaCha20Poly1305Key{
-		// Bad key size.
-		&xcppb.XChaCha20Poly1305Key{
-			Version:  testutil.XChaCha20Poly1305KeyVersion,
-			KeyValue: random.GetRandomBytes(17),
-		},
-		&xcppb.XChaCha20Poly1305Key{
-			Version:  testutil.XChaCha20Poly1305KeyVersion,
-			KeyValue: random.GetRandomBytes(25),
-		},
-		&xcppb.XChaCha20Poly1305Key{
-			Version:  testutil.XChaCha20Poly1305KeyVersion,
-			KeyValue: random.GetRandomBytes(33),
-		},
-		// Bad version.
-		&xcppb.XChaCha20Poly1305Key{
-			Version:  testutil.XChaCha20Poly1305KeyVersion + 1,
-			KeyValue: random.GetRandomBytes(chacha20poly1305.KeySize),
-		},
+func TestXChaCha20Poly1305KeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.XChaCha20Poly1305TypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.XChaCha20Poly1305TypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
 	}
 }
 
-func validateXChaCha20Poly1305Primitive(p interface{}, key *xcppb.XChaCha20Poly1305Key) error {
+func TestXChaCha20Poly1305DeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.XChaCha20Poly1305TypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.XChaCha20Poly1305TypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&xpb.XChaCha20Poly1305KeyFormat{Version: 0})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			// nil unmarshals to an empty proto, which implies version = 0.
+			name:      "nil",
+			keyFormat: nil,
+		},
+		{
+			// An empty proto implies version = 0.
+			name:      "empty",
+			keyFormat: []byte{},
+		},
+		{
+			name:      "specified",
+			keyFormat: keyFormat,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			rand := random.GetRandomBytes(chacha20poly1305.KeySize)
+			buf := &bytes.Buffer{}
+			buf.Write(rand) // never returns a non-nil error
+			k, err := keyManager.DeriveKey(test.keyFormat, buf)
+			if err != nil {
+				t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+			}
+			key := k.(*xpb.XChaCha20Poly1305Key)
+			if got, want := len(key.GetKeyValue()), chacha20poly1305.KeySize; got != want {
+				t.Errorf("key length = %d, want %d", got, want)
+			}
+			if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+				t.Errorf("incorrect derived key: diff = %v", diff)
+			}
+		})
+	}
+}
+
+func TestXChaCha20Poly1305DeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.XChaCha20Poly1305TypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.XChaCha20Poly1305TypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	invalidVersion, err := proto.Marshal(&xpb.XChaCha20Poly1305KeyFormat{Version: 10})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "invalid version",
+			keyFormat: invalidVersion,
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(chacha20poly1305.KeySize))
+			if _, err := keyManager.DeriveKey(test.keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestXChaCha20Poly1305DeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.XChaCha20Poly1305TypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.XChaCha20Poly1305TypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&xpb.XChaCha20Poly1305KeyFormat{Version: 0})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(chacha20poly1305.KeySize))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(chacha20poly1305.KeySize - 1))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
+func validateXChaCha20Poly1305Primitive(p interface{}, key *xpb.XChaCha20Poly1305Key) error {
 	cipher := p.(*subtle.XChaCha20Poly1305)
 	if !bytes.Equal(cipher.Key, key.KeyValue) {
 		return fmt.Errorf("key and primitive don't match")
@@ -171,7 +303,7 @@
 	return nil
 }
 
-func validateXChaCha20Poly1305Key(key *xcppb.XChaCha20Poly1305Key) error {
+func validateXChaCha20Poly1305Key(key *xpb.XChaCha20Poly1305Key) error {
 	if key.Version != testutil.XChaCha20Poly1305KeyVersion {
 		return fmt.Errorf("incorrect key version: keyVersion != %d", testutil.XChaCha20Poly1305KeyVersion)
 	}
diff --git a/go/core/primitiveset/primitiveset.go b/go/core/primitiveset/primitiveset.go
index 49b448f..3e342fa 100644
--- a/go/core/primitiveset/primitiveset.go
+++ b/go/core/primitiveset/primitiveset.go
@@ -40,10 +40,10 @@
 	TypeURL    string
 }
 
-func newEntry(keyID uint32, p interface{}, prefix string, prefixType tinkpb.OutputPrefixType, status tinkpb.KeyStatusType, typeURL string) *Entry {
+func newEntry(keyID uint32, primitive interface{}, prefix string, prefixType tinkpb.OutputPrefixType, status tinkpb.KeyStatusType, typeURL string) *Entry {
 	return &Entry{
 		KeyID:      keyID,
-		Primitive:  p,
+		Primitive:  primitive,
 		Prefix:     prefix,
 		Status:     status,
 		PrefixType: prefixType,
@@ -70,13 +70,19 @@
 	// primitives sharing the prefix). This allows quickly retrieving the
 	// primitives sharing some particular prefix.
 	Entries map[string][]*Entry
+	// Stores entries in the original keyset key order.
+	EntriesInKeysetOrder []*Entry
+
+	Annotations map[string]string
 }
 
 // New returns an empty instance of PrimitiveSet.
 func New() *PrimitiveSet {
 	return &PrimitiveSet{
-		Primary: nil,
-		Entries: make(map[string][]*Entry),
+		Primary:              nil,
+		Entries:              make(map[string][]*Entry),
+		EntriesInKeysetOrder: make([]*Entry, 0),
+		Annotations:          nil,
 	}
 }
 
@@ -95,8 +101,8 @@
 }
 
 // Add creates a new entry in the primitive set and returns the added entry.
-func (ps *PrimitiveSet) Add(p interface{}, key *tinkpb.Keyset_Key) (*Entry, error) {
-	if key == nil || p == nil {
+func (ps *PrimitiveSet) Add(primitive interface{}, key *tinkpb.Keyset_Key) (*Entry, error) {
+	if key == nil || primitive == nil {
 		return nil, fmt.Errorf("primitive_set: key and primitive must not be nil")
 	}
 	if key.GetKeyData() == nil {
@@ -111,12 +117,13 @@
 	}
 	e := newEntry(
 		key.GetKeyId(),
-		p,
+		primitive,
 		prefix,
 		key.GetOutputPrefixType(),
 		key.GetStatus(),
 		key.GetKeyData().GetTypeUrl(),
 	)
 	ps.Entries[prefix] = append(ps.Entries[prefix], e)
+	ps.EntriesInKeysetOrder = append(ps.EntriesInKeysetOrder, e)
 	return e, nil
 }
diff --git a/go/core/primitiveset/primitiveset_test.go b/go/core/primitiveset/primitiveset_test.go
index 5d31efa..8e4eed7 100644
--- a/go/core/primitiveset/primitiveset_test.go
+++ b/go/core/primitiveset/primitiveset_test.go
@@ -34,12 +34,12 @@
 
 func TestPrimitvesetNew(t *testing.T) {
 	ps := primitiveset.New()
-	if ps.Primary != nil || ps.Entries == nil {
+	if ps.Primary != nil || ps.Entries == nil || ps.EntriesInKeysetOrder == nil {
 		t.Errorf("expect primary to be nil and primitives is initialized")
 	}
 }
 
-func TestPrimitivesetAddEntries(t *testing.T) {
+func TestPrimitivesetAddAndEntriesInKeysetOrder(t *testing.T) {
 	keys := []*tinkpb.Keyset_Key{
 		makeTestKey(1234543, tinkpb.KeyStatusType_ENABLED, tinkpb.OutputPrefixType_TINK, "type.url.1"),
 		makeTestKey(7213743, tinkpb.KeyStatusType_ENABLED, tinkpb.OutputPrefixType_LEGACY, "type.url.2"),
@@ -87,6 +87,9 @@
 	if !cmp.Equal(got, want) {
 		t.Errorf("got = %v, want = %v", got, want)
 	}
+	if !cmp.Equal(ps.EntriesInKeysetOrder, want) {
+		t.Errorf("EntriesInKeysetOrder = %v, want = %v", ps.EntriesInKeysetOrder, want)
+	}
 }
 
 func TestPrimitivesetRawEntries(t *testing.T) {
diff --git a/go/core/registry/BUILD.bazel b/go/core/registry/BUILD.bazel
index d9f2d4a..db48f6d 100644
--- a/go/core/registry/BUILD.bazel
+++ b/go/core/registry/BUILD.bazel
@@ -23,19 +23,28 @@
 
 go_test(
     name = "registry_test",
-    srcs = ["registry_test.go"],
+    srcs = [
+        "custom_key_manager_test.go",
+        "registry_test.go",
+    ],
     deps = [
         ":registry",
         "//aead",
+        "//aead/subtle",
+        "//insecurecleartextkeyset",
+        "//internal/tinkerror",
+        "//keyset",
         "//mac",
         "//mac/subtle",
         "//proto/aes_gcm_go_proto",
         "//proto/common_go_proto",
         "//proto/hmac_go_proto",
         "//proto/tink_go_proto",
+        "//subtle/random",
         "//testing/fakekms",
         "//testutil",
         "@org_golang_google_protobuf//proto",
+        "@org_golang_google_protobuf//types/known/wrapperspb",
     ],
 )
 
diff --git a/go/core/registry/custom_key_manager_test.go b/go/core/registry/custom_key_manager_test.go
new file mode 100644
index 0000000..52328e3
--- /dev/null
+++ b/go/core/registry/custom_key_manager_test.go
@@ -0,0 +1,349 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package registry_test
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"testing"
+
+	wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/aead/subtle"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/tinkerror"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/subtle/random"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	customTypeURL = "type.googleapis.com/google.crypto.tink.CustomAesGcmKey"
+)
+
+// customKeyManager is a custom implementation of registry.KeyManager for AES GCM 128.
+type customKeyManager struct{}
+
+// Assert that customKeyManager implements the KeyManager interface.
+var _ registry.KeyManager = (*customKeyManager)(nil)
+
+func (km *customKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	key := new(wrapperspb.BytesValue)
+	if err := proto.Unmarshal(serializedKey, key); err != nil {
+		return nil, fmt.Errorf("invalid key")
+	}
+	if len(key.GetValue()) != 16 {
+		return nil, fmt.Errorf("invalid key")
+	}
+	return subtle.NewAESGCM(key.GetValue())
+}
+
+// NewKey is only used by registry.NewKey, and that function is only used by KMSEnvelopeAEAD.
+// So there is no need to implement it.
+func (km *customKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	return nil, errors.New("not implemented")
+}
+
+func (km *customKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	keyFormat := new(wrapperspb.StringValue)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, fmt.Errorf("invalid key format")
+	}
+	if keyFormat.GetValue() != "AEAD_AES_GCM_128" {
+		return nil, fmt.Errorf("invalid key format")
+	}
+	keyValue := random.GetRandomBytes(16)
+	key := &wrapperspb.BytesValue{
+		Value: keyValue,
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         customTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: km.KeyMaterialType(),
+	}, nil
+}
+
+func (km *customKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == customTypeURL
+}
+
+func (km *customKeyManager) TypeURL() string {
+	return customTypeURL
+}
+
+func (km *customKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_SYMMETRIC
+}
+
+// aesGCM128KeyTemplate creates a AES GCM 128 KeyTemplate for customKeyManager.
+func aesGCM128KeyTemplate() *tinkpb.KeyTemplate {
+	format := &wrapperspb.StringValue{
+		Value: "AEAD_AES_GCM_128",
+	}
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
+	return &tinkpb.KeyTemplate{
+		TypeUrl:          customTypeURL,
+		Value:            serializedFormat,
+		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+	}
+}
+
+// aesGCM128KeyToKeysetHandle creates a keyset.Handle with one custom AES GCM 128 key.
+func aesGCM128KeyToKeysetHandle(rawAESKey []byte, keyID uint32, prefixType tinkpb.OutputPrefixType) (*keyset.Handle, error) {
+	if len(rawAESKey) != 16 {
+		return nil, fmt.Errorf("invalid key length")
+	}
+	key := &wrapperspb.BytesValue{Value: rawAESKey}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, err
+	}
+	keyData := &tinkpb.KeyData{
+		TypeUrl:         customTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+	}
+	ks := &tinkpb.Keyset{
+		PrimaryKeyId: keyID,
+		Key: []*tinkpb.Keyset_Key{
+			&tinkpb.Keyset_Key{
+				KeyData:          keyData,
+				Status:           tinkpb.KeyStatusType_ENABLED,
+				KeyId:            keyID,
+				OutputPrefixType: prefixType,
+			},
+		},
+	}
+	serializedKeyset, err := proto.Marshal(ks)
+	if err != nil {
+		return nil, err
+	}
+	return insecurecleartextkeyset.Read(keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)))
+}
+
+func TestCreateEncryptDecrypt(t *testing.T) {
+	handle, err := keyset.NewHandle(aesGCM128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(aesGCM128KeyTemplate()) err = %v, want nil", err)
+	}
+	primitive, err := aead.New(handle)
+	if err != nil {
+		t.Fatalf("aead.New(handle) err = %v, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+	decrypted, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(plaintext, decrypted) {
+		t.Errorf("primitive.Decrypt(ciphertext, associatedData) = %q, want: %q", decrypted, plaintext)
+	}
+}
+
+func TestImportExistingKeyDecryptsExistingCiphertext(t *testing.T) {
+	rawAesKey := random.GetRandomBytes(16)
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+
+	// Create a AES GCM 128 ciphertext using rawAesKey.
+	aesGCMForRawAesKey, err := subtle.NewAESGCM(rawAesKey)
+	if err != nil {
+		t.Fatalf("subtle.NewAESGCM(rawAesKey) err = %v, want nil", err)
+	}
+	ciphertext, err := aesGCMForRawAesKey.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("aesGCMForRawAesKey.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+
+	// Import rawAesKey into a Tink keyset.Handle, and decrypt the ciphertext.
+	handle, err := aesGCM128KeyToKeysetHandle(rawAesKey, 123, tinkpb.OutputPrefixType_RAW)
+	if err != nil {
+		t.Fatalf("aesGCM128KeyToKeysetHandle() err = %v, want nil", err)
+	}
+	primitive, err := aead.New(handle)
+	if err != nil {
+		t.Fatalf("aead.New(handle) err = %v, want nil", err)
+	}
+	gotPlaintext, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(plaintext, gotPlaintext) {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) = %q, want: %q", gotPlaintext, plaintext)
+	}
+}
+
+func TestEncryptAndDecryptWithTinkPrefix(t *testing.T) {
+	// Create an AEAD for rawAesKey with output prefix type TINK.
+	rawAesKey := random.GetRandomBytes(16)
+	handle, err := aesGCM128KeyToKeysetHandle(rawAesKey, 0x11223344, tinkpb.OutputPrefixType_TINK)
+	if err != nil {
+		t.Fatalf("aesGCM128KeyToKeysetHandle() err = %v, want nil", err)
+	}
+	primitive, err := aead.New(handle)
+	if err != nil {
+		t.Fatalf("aead.New(handle) err = %v, want nil", err)
+	}
+
+	// Encrypt and decrypt.
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+	gotPlaintext, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(plaintext, gotPlaintext) {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) = %q, want: %q", gotPlaintext, plaintext)
+	}
+
+	// Check that ciphertext has the correct prefix.
+	gotPrefix := ciphertext[:5]
+	wantPrefix := []byte{0x01, 0x11, 0x22, 0x33, 0x44}
+	if !bytes.Equal(gotPrefix, wantPrefix) {
+		t.Fatalf("ciphertext[:5] = %q, want: %q", gotPrefix, wantPrefix)
+	}
+
+	// Check that subtle.NewAESGCM with rawAesKey can decrypt the ciphertext if the prefix is removed.
+	aesGCMForRawAesKey, err := subtle.NewAESGCM(rawAesKey)
+	if err != nil {
+		t.Fatalf("subtle.NewAESGCM(rawAesKey) err = %v, want nil", err)
+	}
+	gotPlaintext, err = aesGCMForRawAesKey.Decrypt(ciphertext[5:], associatedData)
+	if err != nil {
+		t.Fatalf("aesGCMForRawAesKey.Decrypt() err = %v, want nil", err)
+	}
+	if !bytes.Equal(plaintext, gotPlaintext) {
+		t.Fatalf("aesGCMForRawAesKey.Decrypt() = %q, want: %q", gotPlaintext, plaintext)
+	}
+}
+
+func TestMixedKeysetWorks(t *testing.T) {
+	rawAesKey := random.GetRandomBytes(16)
+
+	// Create a AES GCM 128 ciphertext using rawAesKey.
+	subtlePrimitive, err := subtle.NewAESGCM(rawAesKey)
+	if err != nil {
+		t.Fatalf("subtle.NewAESGCM(rawAesKey) err = %v, want nil", err)
+	}
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	ciphertext, err := subtlePrimitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("subtlePrimitive.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+
+	// Create handle2, which is a keyset.Handle that contains a customKeyManager key of rawAesKey and
+	// a new, non-customKeyManager key.
+	handle1, err := aesGCM128KeyToKeysetHandle(rawAesKey, 123, tinkpb.OutputPrefixType_RAW)
+	if err != nil {
+		t.Fatalf("aesGCM128KeyToKeysetHandle() err = %v, want nil", err)
+	}
+	manager := keyset.NewManagerFromHandle(handle1)
+	keyID, err := manager.Add(aead.AES128CTRHMACSHA256KeyTemplate())
+	if err != nil {
+		t.Fatalf("manager.Add(aead.AES128CTRHMACSHA256KeyTemplate()) err = %v, want nil", err)
+	}
+	err = manager.SetPrimary(keyID)
+	if err != nil {
+		t.Fatalf("manager.SetPrimary(keyID) = %v", err)
+	}
+	handle2, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle() err = %v", err)
+	}
+
+	primitive, err := aead.New(handle2)
+	if err != nil {
+		t.Fatalf("aead.New(handle2) err = %v", err)
+	}
+	gotPlaintext, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(plaintext, gotPlaintext) {
+		t.Errorf("primitive.Decrypt(ciphertext, associatedData) = %q, want: %q", gotPlaintext, plaintext)
+	}
+}
+
+func TestSerializeAndParseKeysetWorks(t *testing.T) {
+	handle, err := keyset.NewHandle(aesGCM128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(aesGCM128KeyTemplate()) err = %v, want nil", err)
+	}
+	primitive, err := aead.New(handle)
+	if err != nil {
+		t.Fatalf("aead.New(handle) err = %v, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+
+	// Serialize the keyset.
+	buff := &bytes.Buffer{}
+	err = insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)) = %v, want nil", err)
+	}
+	serializedKeyset := buff.Bytes()
+
+	// Parse the keyset.
+	parsedHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read(keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset))) = %v, want nil", err)
+	}
+
+	primitive2, err := aead.New(parsedHandle)
+	if err != nil {
+		t.Fatalf("aead.New(parsedHandle) err = %v, want nil", err)
+	}
+
+	gotPlaintext, err := primitive2.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("primitive2.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(plaintext, gotPlaintext) {
+		t.Errorf("primitive2.Decrypt(ciphertext, associatedData) = %q, want: %q", gotPlaintext, plaintext)
+	}
+}
+
+func init() { registry.RegisterKeyManager(&customKeyManager{}) }
diff --git a/go/core/registry/key_manager.go b/go/core/registry/key_manager.go
index 05ac6c7..b14e436 100644
--- a/go/core/registry/key_manager.go
+++ b/go/core/registry/key_manager.go
@@ -32,12 +32,16 @@
 
 	// NewKey generates a new key according to specification in serializedKeyFormat, which must be
 	// supported by this manager.
+	//
+	// Deprecated: Tink always used [NewKeyData] to create new keys. This function is
+	// unused (except in the unused and deprecated function [registry.NewKey]). It doesn't need to be
+	// implemented.
 	NewKey(serializedKeyFormat []byte) (proto.Message, error)
 
 	// DoesSupport returns true iff this KeyManager supports key type identified by typeURL.
 	DoesSupport(typeURL string) bool
 
-	// TypeURL returns the type URL that identifes the key type of keys managed by this key manager.
+	// TypeURL returns the type URL that identifies the key type of keys managed by this key manager.
 	TypeURL() string
 
 	// APIs for Key Management
diff --git a/go/core/registry/registry.go b/go/core/registry/registry.go
index c5516bf..e5e0d8c 100644
--- a/go/core/registry/registry.go
+++ b/go/core/registry/registry.go
@@ -47,14 +47,14 @@
 
 // RegisterKeyManager registers the given key manager.
 // Does not allow to overwrite existing key managers.
-func RegisterKeyManager(km KeyManager) error {
+func RegisterKeyManager(keyManager KeyManager) error {
 	keyManagersMu.Lock()
 	defer keyManagersMu.Unlock()
-	typeURL := km.TypeURL()
+	typeURL := keyManager.TypeURL()
 	if _, existed := keyManagers[typeURL]; existed {
 		return fmt.Errorf("registry.RegisterKeyManager: type %s already registered", typeURL)
 	}
-	keyManagers[typeURL] = km
+	keyManagers[typeURL] = keyManager
 	return nil
 }
 
@@ -62,76 +62,78 @@
 func GetKeyManager(typeURL string) (KeyManager, error) {
 	keyManagersMu.RLock()
 	defer keyManagersMu.RUnlock()
-	km, existed := keyManagers[typeURL]
+	keyManager, existed := keyManagers[typeURL]
 	if !existed {
 		return nil, fmt.Errorf("registry.GetKeyManager: unsupported key type: %s", typeURL)
 	}
-	return km, nil
+	return keyManager, nil
 }
 
 // NewKeyData generates a new KeyData for the given key template.
-func NewKeyData(kt *tinkpb.KeyTemplate) (*tinkpb.KeyData, error) {
-	if kt == nil {
+func NewKeyData(template *tinkpb.KeyTemplate) (*tinkpb.KeyData, error) {
+	if template == nil {
 		return nil, fmt.Errorf("registry.NewKeyData: invalid key template")
 	}
-	km, err := GetKeyManager(kt.TypeUrl)
+	keyManager, err := GetKeyManager(template.TypeUrl)
 	if err != nil {
 		return nil, err
 	}
-	return km.NewKeyData(kt.Value)
+	return keyManager.NewKeyData(template.Value)
 }
 
 // NewKey generates a new key for the given key template.
-func NewKey(kt *tinkpb.KeyTemplate) (proto.Message, error) {
-	if kt == nil {
+//
+// Deprecated: use [NewKeyData] instead.
+func NewKey(template *tinkpb.KeyTemplate) (proto.Message, error) {
+	if template == nil {
 		return nil, fmt.Errorf("registry.NewKey: invalid key template")
 	}
-	km, err := GetKeyManager(kt.TypeUrl)
+	keyManager, err := GetKeyManager(template.TypeUrl)
 	if err != nil {
 		return nil, err
 	}
-	return km.NewKey(kt.Value)
+	return keyManager.NewKey(template.Value)
 }
 
 // PrimitiveFromKeyData creates a new primitive for the key given in the given KeyData.
 // Note that the returned primitive does not add/remove the output prefix.
 // It is the caller's responsibility to handle this correctly, based on the key's output_prefix_type.
-func PrimitiveFromKeyData(kd *tinkpb.KeyData) (interface{}, error) {
-	if kd == nil {
+func PrimitiveFromKeyData(keyData *tinkpb.KeyData) (interface{}, error) {
+	if keyData == nil {
 		return nil, fmt.Errorf("registry.PrimitiveFromKeyData: invalid key data")
 	}
-	return Primitive(kd.TypeUrl, kd.Value)
+	return Primitive(keyData.TypeUrl, keyData.Value)
 }
 
 // Primitive creates a new primitive for the given serialized key using the KeyManager
 // identified by the given typeURL.
 // Note that the returned primitive does not add/remove the output prefix.
 // It is the caller's responsibility to handle this correctly, based on the key's output_prefix_type.
-func Primitive(typeURL string, sk []byte) (interface{}, error) {
-	if len(sk) == 0 {
+func Primitive(typeURL string, serializedKey []byte) (interface{}, error) {
+	if len(serializedKey) == 0 {
 		return nil, fmt.Errorf("registry.Primitive: invalid serialized key")
 	}
-	km, err := GetKeyManager(typeURL)
+	keyManager, err := GetKeyManager(typeURL)
 	if err != nil {
 		return nil, err
 	}
-	return km.Primitive(sk)
+	return keyManager.Primitive(serializedKey)
 }
 
 // RegisterKMSClient is used to register a new KMS client
-func RegisterKMSClient(k KMSClient) {
+func RegisterKMSClient(kmsClient KMSClient) {
 	kmsClientsMu.Lock()
 	defer kmsClientsMu.Unlock()
-	kmsClients = append(kmsClients, k)
+	kmsClients = append(kmsClients, kmsClient)
 }
 
 // GetKMSClient fetches a KMSClient by a given URI.
 func GetKMSClient(keyURI string) (KMSClient, error) {
 	kmsClientsMu.RLock()
 	defer kmsClientsMu.RUnlock()
-	for _, k := range kmsClients {
-		if k.Supported(keyURI) {
-			return k, nil
+	for _, kmsClient := range kmsClients {
+		if kmsClient.Supported(keyURI) {
+			return kmsClient, nil
 		}
 	}
 	return nil, fmt.Errorf("KMS client supporting %s not found", keyURI)
diff --git a/go/daead/BUILD.bazel b/go/daead/BUILD.bazel
index 715fd90..08c12b4 100644
--- a/go/daead/BUILD.bazel
+++ b/go/daead/BUILD.bazel
@@ -19,7 +19,11 @@
         "//core/primitiveset",
         "//core/registry",
         "//daead/subtle",
+        "//internal/internalregistry",
+        "//internal/monitoringutil",
+        "//internal/tinkerror",
         "//keyset",
+        "//monitoring",
         "//proto/aes_siv_go_proto",
         "//proto/tink_go_proto",
         "//subtle/random",
@@ -33,6 +37,7 @@
     srcs = [
         "aes_siv_key_manager_test.go",
         "daead_factory_test.go",
+        "daead_init_test.go",
         "daead_key_templates_test.go",
         "daead_test.go",
     ],
@@ -41,14 +46,21 @@
         "//core/cryptofmt",
         "//core/registry",
         "//daead/subtle",
+        "//insecurecleartextkeyset",
+        "//internal/internalregistry",
+        "//internal/testing/stubkeymanager",
         "//keyset",
+        "//monitoring",
         "//proto/aes_siv_go_proto",
         "//proto/tink_go_proto",
         "//signature",
         "//subtle/random",
+        "//testing/fakemonitoring",
         "//testkeyset",
         "//testutil",
         "//tink",
+        "@com_github_google_go_cmp//cmp",
+        "@com_github_google_go_cmp//cmp/cmpopts",
         "@org_golang_google_protobuf//proto",
     ],
 )
diff --git a/go/daead/aes_siv_key_manager.go b/go/daead/aes_siv_key_manager.go
index a168f5f..3bf3de7 100644
--- a/go/daead/aes_siv_key_manager.go
+++ b/go/daead/aes_siv_key_manager.go
@@ -17,15 +17,18 @@
 package daead
 
 import (
+	"errors"
 	"fmt"
+	"io"
 
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/daead/subtle"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/subtle/random"
 
 	aspb "github.com/google/tink/go/proto/aes_siv_go_proto"
-	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 const (
@@ -33,16 +36,22 @@
 	aesSIVTypeURL    = "type.googleapis.com/google.crypto.tink.AesSivKey"
 )
 
-// aesSIVKeyManager is an implementation of KeyManager interface.
-// It generates new AesSivKey keys and produces new instances of AESSIV subtle.
+var (
+	errInvalidAESSIVKeyFormat = errors.New("aes_siv_key_manager: invalid key format")
+	errInvalidAESSIVKeySize   = fmt.Errorf("aes_siv_key_manager: key size != %d", subtle.AESSIVKeySize)
+)
+
+// aesSIVKeyManager generates AES-SIV keys and produces instances of AES-SIV.
 type aesSIVKeyManager struct{}
 
-// Primitive creates an AESSIV subtle for the given serialized AesSivKey proto.
+// Assert that aesSIVKeyManager implements the KeyManager interface.
+var _ registry.KeyManager = (*aesSIVKeyManager)(nil)
+
+// Primitive constructs an AES-SIV for the given serialized AesSivKey.
 func (km *aesSIVKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
 	if len(serializedKey) == 0 {
-		return nil, fmt.Errorf("aes_siv_key_manager: invalid key")
+		return nil, errors.New("aes_siv_key_manager: invalid key")
 	}
-
 	key := new(aspb.AesSivKey)
 	if err := proto.Unmarshal(serializedKey, key); err != nil {
 		return nil, err
@@ -52,69 +61,97 @@
 	}
 	ret, err := subtle.NewAESSIV(key.KeyValue)
 	if err != nil {
-		return nil, fmt.Errorf("aes_siv_key_manager: cannot create new primitive: %s", err)
+		return nil, fmt.Errorf("aes_siv_key_manager: cannot create new primitive: %v", err)
 	}
 	return ret, nil
 }
 
-// NewKey creates a new key. serializedKeyFormat is not required, because there is only one
-// valid key format.
+// NewKey generates a new AesSivKey. serializedKeyFormat is optional because
+// there is only one valid key format.
 func (km *aesSIVKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	// A nil serializedKeyFormat is acceptable. If specified, validate.
 	if serializedKeyFormat != nil {
 		keyFormat := new(aspb.AesSivKeyFormat)
 		if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
-			return nil, fmt.Errorf("aes_siv_key_manager: invalid key format")
+			return nil, errInvalidAESSIVKeyFormat
 		}
 		if keyFormat.KeySize != subtle.AESSIVKeySize {
-			return nil, fmt.Errorf("aes_siv_key_manager: keyFormat.KeySize != %d", subtle.AESSIVKeySize)
+			return nil, errInvalidAESSIVKeySize
 		}
 	}
-	keyValue := random.GetRandomBytes(subtle.AESSIVKeySize)
-	key := &aspb.AesSivKey{
+	return &aspb.AesSivKey{
 		Version:  aesSIVKeyVersion,
-		KeyValue: keyValue,
-	}
-	return key, nil
+		KeyValue: random.GetRandomBytes(subtle.AESSIVKeySize),
+	}, nil
 }
 
-// NewKeyData creates a new KeyData. serializedKeyFormat is not required, because there is only one
-// valid key format.
-// It should be used solely by the key management API.
-func (km *aesSIVKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+// NewKeyData generates a new KeyData. serializedKeyFormat is optional because
+// there is only one valid key format. This should be used solely by the key
+// management API.
+func (km *aesSIVKeyManager) NewKeyData(serializedKeyFormat []byte) (*tpb.KeyData, error) {
 	key, err := km.NewKey(serializedKeyFormat)
 	if err != nil {
 		return nil, err
 	}
 	serializedKey, err := proto.Marshal(key)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("aes_siv_key_manager: %v", err)
 	}
-	return &tinkpb.KeyData{
+	return &tpb.KeyData{
 		TypeUrl:         aesSIVTypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
-// DoesSupport indicates if this key manager supports the given key type.
+// DoesSupport checks whether this key manager supports the given key type.
 func (km *aesSIVKeyManager) DoesSupport(typeURL string) bool {
 	return typeURL == aesSIVTypeURL
 }
 
-// TypeURL returns the key type of keys managed by this key manager.
+// TypeURL returns the type URL of keys managed by this key manager.
 func (km *aesSIVKeyManager) TypeURL() string {
 	return aesSIVTypeURL
 }
 
+// KeyMaterialType returns the key material type of this key manager.
+func (km *aesSIVKeyManager) KeyMaterialType() tpb.KeyData_KeyMaterialType {
+	return tpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+// Unlike NewKey, DeriveKey validates serializedKeyFormat.
+func (km *aesSIVKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	keyFormat := new(aspb.AesSivKeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidAESSIVKeyFormat
+	}
+	if keyFormat.GetKeySize() != subtle.AESSIVKeySize {
+		return nil, errInvalidAESSIVKeySize
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), aesSIVKeyVersion); err != nil {
+		return nil, fmt.Errorf("aes_siv_key_manager: invalid key version: %s", err)
+	}
+
+	keyValue := make([]byte, subtle.AESSIVKeySize)
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("aes_siv_key_manager: not enough pseudorandomness given")
+	}
+	return &aspb.AesSivKey{
+		Version:  aesSIVKeyVersion,
+		KeyValue: keyValue,
+	}, nil
+}
+
 // validateKey validates the given AesSivKey.
 func (km *aesSIVKeyManager) validateKey(key *aspb.AesSivKey) error {
 	err := keyset.ValidateKeyVersion(key.Version, aesSIVKeyVersion)
 	if err != nil {
-		return fmt.Errorf("aes_siv_key_manager: %s", err)
+		return fmt.Errorf("aes_siv_key_manager: %v", err)
 	}
 	keySize := uint32(len(key.KeyValue))
 	if keySize != subtle.AESSIVKeySize {
-		return fmt.Errorf("aes_siv_key_manager: keySize != %d", subtle.AESSIVKeySize)
+		return errInvalidAESSIVKeySize
 	}
 	return nil
 }
diff --git a/go/daead/aes_siv_key_manager_test.go b/go/daead/aes_siv_key_manager_test.go
index c07a0f1..2f3d570 100644
--- a/go/daead/aes_siv_key_manager_test.go
+++ b/go/daead/aes_siv_key_manager_test.go
@@ -18,17 +18,20 @@
 
 import (
 	"bytes"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
 
 	"github.com/google/tink/go/daead/subtle"
 	aspb "github.com/google/tink/go/proto/aes_siv_go_proto"
-	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 func TestAESSIVPrimitive(t *testing.T) {
@@ -56,7 +59,30 @@
 	if err != nil {
 		t.Errorf("cannot obtain AESSIV key manager: %s", err)
 	}
-	invalidKeys := genInvalidAESSIVKeys()
+	invalidKeys := []*aspb.AesSivKey{
+		// Bad key size.
+		&aspb.AesSivKey{
+			Version:  testutil.AESSIVKeyVersion,
+			KeyValue: random.GetRandomBytes(16),
+		},
+		&aspb.AesSivKey{
+			Version:  testutil.AESSIVKeyVersion,
+			KeyValue: random.GetRandomBytes(32),
+		},
+		&aspb.AesSivKey{
+			Version:  testutil.AESSIVKeyVersion,
+			KeyValue: random.GetRandomBytes(63),
+		},
+		&aspb.AesSivKey{
+			Version:  testutil.AESSIVKeyVersion,
+			KeyValue: random.GetRandomBytes(65),
+		},
+		// Bad version.
+		&aspb.AesSivKey{
+			Version:  testutil.AESSIVKeyVersion + 1,
+			KeyValue: random.GetRandomBytes(subtle.AESSIVKeySize),
+		},
+	}
 	for _, key := range invalidKeys {
 		serializedKey, _ := proto.Marshal(key)
 		if _, err := km.Primitive(serializedKey); err == nil {
@@ -92,7 +118,7 @@
 	if kd.TypeUrl != testutil.AESSIVTypeURL {
 		t.Errorf("TypeUrl: %v != %v", kd.TypeUrl, testutil.AESSIVTypeURL)
 	}
-	if kd.KeyMaterialType != tinkpb.KeyData_SYMMETRIC {
+	if kd.KeyMaterialType != tpb.KeyData_SYMMETRIC {
 		t.Errorf("KeyMaterialType: %v != SYMMETRIC", kd.KeyMaterialType)
 	}
 	key := new(aspb.AesSivKey)
@@ -107,18 +133,39 @@
 func TestAESSIVNewKeyInvalid(t *testing.T) {
 	km, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
 	if err != nil {
-		t.Errorf("cannot obtain AESSIV key manager: %s", err)
+		t.Errorf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESSIVTypeURL, err)
 	}
-	keyFormat := &aspb.AesSivKeyFormat{
+	invalidKeySize, err := proto.Marshal(&aspb.AesSivKeyFormat{
 		KeySize: subtle.AESSIVKeySize - 1,
-	}
-	serializedKeyFormat, err := proto.Marshal(keyFormat)
+		Version: testutil.AESSIVKeyVersion,
+	})
 	if err != nil {
-		t.Errorf("proto.Marshal(keyFormat) = %v; want nil", err)
+		t.Errorf("proto.Marshal() err = %v, want nil", err)
 	}
-	_, err = km.NewKey(serializedKeyFormat)
-	if err == nil {
-		t.Errorf("km.NewKey(serializedKeyFormat) = _, nil; want _, err")
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "invalid key size",
+			keyFormat: invalidKeySize,
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err = km.NewKey(test.keyFormat); err == nil {
+				t.Error("km.NewKey() err = nil, want non-nil")
+			}
+		})
 	}
 }
 
@@ -145,6 +192,166 @@
 	}
 }
 
+func TestAESSIVKeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESSIVTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestAESSIVDeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESSIVTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&aspb.AesSivKeyFormat{
+		KeySize: subtle.AESSIVKeySize,
+		Version: testutil.AESSIVKeyVersion,
+	})
+	if err != nil {
+		t.Errorf("proto.Marshal() = %v; want nil", err)
+	}
+	rand := random.GetRandomBytes(subtle.AESSIVKeySize)
+	buf := &bytes.Buffer{}
+	buf.Write(rand) // never returns a non-nil error
+	k, err := keyManager.DeriveKey(keyFormat, buf)
+	if err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+	key := k.(*aspb.AesSivKey)
+	if got, want := len(key.GetKeyValue()), subtle.AESSIVKeySize; got != want {
+		t.Errorf("key length = %d, want %d", got, want)
+	}
+	if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+		t.Errorf("incorrect derived key: diff = %v", diff)
+	}
+}
+
+func TestAESSIVDeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESSIVTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	for _, test := range []struct {
+		name    string
+		keySize uint32
+		version uint32
+	}{
+		{
+			name:    "invalid key size",
+			keySize: subtle.AESSIVKeySize - 1,
+			version: testutil.AESSIVKeyVersion,
+		},
+		{
+			name:    "invalid version",
+			keySize: subtle.AESSIVKeySize,
+			version: testutil.AESSIVKeyVersion + 1,
+		},
+		{
+			name:    "zero key size and version",
+			keySize: 0,
+			version: 0,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat := &aspb.AesSivKeyFormat{
+				KeySize: test.keySize,
+				Version: test.version,
+			}
+			serializedKeyFormat, err := proto.Marshal(keyFormat)
+			if err != nil {
+				t.Errorf("proto.Marshal() = %v; want nil", err)
+			}
+			buf := bytes.NewBuffer(random.GetRandomBytes(subtle.AESSIVKeySize))
+			if _, err := keyManager.DeriveKey(serializedKeyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestAESSIVDeriveKeyFailsWithMalformedKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESSIVTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "nil",
+			keyFormat: nil,
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(subtle.AESSIVKeySize))
+			if _, err := keyManager.DeriveKey(test.keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestAESSIVDeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESSIVTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&aspb.AesSivKeyFormat{
+		KeySize: subtle.AESSIVKeySize,
+		Version: testutil.AESSIVKeyVersion,
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(subtle.AESSIVKeySize))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(subtle.AESSIVKeySize - 1))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func validateAESSIVPrimitive(p interface{}, key *aspb.AesSivKey) error {
 	cipher := p.(*subtle.AESSIV)
 	// try to encrypt and decrypt
@@ -179,30 +386,3 @@
 	}
 	return validateAESSIVPrimitive(p, key)
 }
-
-func genInvalidAESSIVKeys() []*aspb.AesSivKey {
-	return []*aspb.AesSivKey{
-		// Bad key size.
-		&aspb.AesSivKey{
-			Version:  testutil.AESSIVKeyVersion,
-			KeyValue: random.GetRandomBytes(16),
-		},
-		&aspb.AesSivKey{
-			Version:  testutil.AESSIVKeyVersion,
-			KeyValue: random.GetRandomBytes(32),
-		},
-		&aspb.AesSivKey{
-			Version:  testutil.AESSIVKeyVersion,
-			KeyValue: random.GetRandomBytes(63),
-		},
-		&aspb.AesSivKey{
-			Version:  testutil.AESSIVKeyVersion,
-			KeyValue: random.GetRandomBytes(65),
-		},
-		// Bad version.
-		&aspb.AesSivKey{
-			Version:  testutil.AESSIVKeyVersion + 1,
-			KeyValue: random.GetRandomBytes(subtle.AESSIVKeySize),
-		},
-	}
-}
diff --git a/go/daead/daead.go b/go/daead/daead.go
index 2102f9a..2ddd475 100644
--- a/go/daead/daead.go
+++ b/go/daead/daead.go
@@ -24,10 +24,14 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 )
 
 func init() {
 	if err := registry.RegisterKeyManager(new(aesSIVKeyManager)); err != nil {
 		panic(fmt.Sprintf("daead.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(aesSIVTypeURL); err != nil {
+		panic(fmt.Sprintf("daead.init() failed: %v", err))
+	}
 }
diff --git a/go/daead/daead_factory.go b/go/daead/daead_factory.go
index 51a83df..20d6de5 100644
--- a/go/daead/daead_factory.go
+++ b/go/daead/daead_factory.go
@@ -21,25 +21,34 @@
 
 	"github.com/google/tink/go/core/cryptofmt"
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/tink"
 )
 
 // New returns a DeterministicAEAD primitive from the given keyset handle.
-func New(h *keyset.Handle) (tink.DeterministicAEAD, error) {
-	return NewWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewWithKeyManager returns a DeterministicAEAD primitive from the given keyset handle and custom key manager.
-//
-// Deprecated: Use [New].
-func NewWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.DeterministicAEAD, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func New(handle *keyset.Handle) (tink.DeterministicAEAD, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("daead_factory: cannot obtain primitive set: %s", err)
 	}
+	return newWrappedDeterministicAEAD(ps)
+}
 
+// wrappedDeterministicAEAD is a DeterministicAEAD implementation that uses an underlying primitive set
+// for deterministic encryption and decryption.
+type wrappedDeterministicAEAD struct {
+	ps        *primitiveset.PrimitiveSet
+	encLogger monitoring.Logger
+	decLogger monitoring.Logger
+}
+
+// Asserts that wrappedDeterministicAEAD implements the DeterministicAEAD interface.
+var _ tink.DeterministicAEAD = (*wrappedDeterministicAEAD)(nil)
+
+func newWrappedDeterministicAEAD(ps *primitiveset.PrimitiveSet) (*wrappedDeterministicAEAD, error) {
 	if _, ok := (ps.Primary.Primitive).(tink.DeterministicAEAD); !ok {
 		return nil, fmt.Errorf("daead_factory: not a DeterministicAEAD primitive")
 	}
@@ -51,21 +60,45 @@
 			}
 		}
 	}
-
-	ret := new(wrappedDeterministicAEAD)
-	ret.ps = ps
-	return tink.DeterministicAEAD(ret), nil
+	encLogger, decLogger, err := createLoggers(ps)
+	if err != nil {
+		return nil, err
+	}
+	return &wrappedDeterministicAEAD{
+		ps:        ps,
+		encLogger: encLogger,
+		decLogger: decLogger,
+	}, nil
 }
 
-// wrappedDeterministicAEAD is an DeterministicAEAD implementation that uses an underlying primitive set
-// for deterministic encryption and decryption.
-type wrappedDeterministicAEAD struct {
-	ps *primitiveset.PrimitiveSet
+func createLoggers(ps *primitiveset.PrimitiveSet) (monitoring.Logger, monitoring.Logger, error) {
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, &monitoringutil.DoNothingLogger{}, nil
+	}
+	client := internalregistry.GetMonitoringClient()
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, nil, err
+	}
+	encLogger, err := client.NewLogger(&monitoring.Context{
+		Primitive:   "daead",
+		APIFunction: "encrypt",
+		KeysetInfo:  keysetInfo,
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+	decLogger, err := client.NewLogger(&monitoring.Context{
+		Primitive:   "daead",
+		APIFunction: "decrypt",
+		KeysetInfo:  keysetInfo,
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+	return encLogger, decLogger, nil
 }
 
-// Asserts that wrappedDeterministicAEAD implements the DeterministicAEAD interface.
-var _ tink.DeterministicAEAD = (*wrappedDeterministicAEAD)(nil)
-
 // EncryptDeterministically deterministically encrypts plaintext with additionalData as additional authenticated data.
 // It returns the concatenation of the primary's identifier and the ciphertext.
 func (d *wrappedDeterministicAEAD) EncryptDeterministically(pt, aad []byte) ([]byte, error) {
@@ -77,9 +110,17 @@
 
 	ct, err := p.EncryptDeterministically(pt, aad)
 	if err != nil {
+		d.encLogger.LogFailure()
 		return nil, err
 	}
-	return append([]byte(primary.Prefix), ct...), nil
+	d.encLogger.Log(primary.KeyID, len(pt))
+	if len(primary.Prefix) == 0 {
+		return ct, nil
+	}
+	output := make([]byte, 0, len(primary.Prefix)+len(ct))
+	output = append(output, primary.Prefix...)
+	output = append(output, ct...)
+	return output, nil
 }
 
 // DecryptDeterministically deterministically decrypts ciphertext with additionalData as
@@ -101,6 +142,7 @@
 
 				pt, err := p.DecryptDeterministically(ctNoPrefix, aad)
 				if err == nil {
+					d.decLogger.Log(entries[i].KeyID, len(ctNoPrefix))
 					return pt, nil
 				}
 			}
@@ -118,11 +160,12 @@
 
 			pt, err := p.DecryptDeterministically(ct, aad)
 			if err == nil {
+				d.decLogger.Log(entries[i].KeyID, len(ct))
 				return pt, nil
 			}
 		}
 	}
-
 	// nothing worked
+	d.decLogger.LogFailure()
 	return nil, fmt.Errorf("daead_factory: decryption failed")
 }
diff --git a/go/daead/daead_factory_test.go b/go/daead/daead_factory_test.go
index 1c9e493..a53c8ba 100644
--- a/go/daead/daead_factory_test.go
+++ b/go/daead/daead_factory_test.go
@@ -22,11 +22,19 @@
 	"strings"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/google/tink/go/core/cryptofmt"
+	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/daead"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/signature"
 	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testing/fakemonitoring"
 	"github.com/google/tink/go/testkeyset"
 	"github.com/google/tink/go/testutil"
 	"github.com/google/tink/go/tink"
@@ -161,3 +169,483 @@
 		t.Fatalf("calling New() with good *keyset.Handle failed: %s", err)
 	}
 }
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsLogsEncryptionDecryptionWithPrefix(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(daead.AESSIVKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := daead.New(mh)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	pt := []byte("HELLO_WORLD")
+	ct, err := p.EncryptDeterministically(pt, nil)
+	if err != nil {
+		t.Fatalf("p.EncryptDeterministically() err = %v, want nil", err)
+	}
+	if _, err := p.DecryptDeterministically(ct, nil); err != nil {
+		t.Fatalf("p.DecryptDeterministically() err = %v, want nil", err)
+	}
+	got := client.Events()
+	wantKeysetInfo := monitoring.NewKeysetInfo(
+		annotations,
+		kh.KeysetInfo().GetPrimaryKeyId(),
+		[]*monitoring.Entry{
+			{
+				Status:    monitoring.Enabled,
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				KeyType:   "tink.AesSivKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	)
+	want := []*fakemonitoring.LogEvent{
+		{
+			KeyID:    mh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(pt),
+			Context:  monitoring.NewContext("daead", "encrypt", wantKeysetInfo),
+		},
+		{
+			KeyID: mh.KeysetInfo().GetPrimaryKeyId(),
+			// Ciphertext was encrypted with a key that has a TINK output prefix. This adds a 5-byte prefix
+			// to the ciphertext. This prefix is not included in `Log` call.
+			NumBytes: len(ct) - cryptofmt.NonRawPrefixSize,
+			Context:  monitoring.NewContext("daead", "decrypt", wantKeysetInfo),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsLogsEncryptionDecryptionWithoutPrefix(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	template := daead.AESSIVKeyTemplate()
+	// There's currently not a raw template in the public API, but
+	// we add a test by customizing the output prefix of an existing one.
+	template.OutputPrefixType = tinkpb.OutputPrefixType_RAW
+	kh, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := daead.New(mh)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	data := []byte("hello_world")
+	aad := []byte("_!")
+	ct, err := p.EncryptDeterministically(data, aad)
+	if err != nil {
+		t.Fatalf("p.EncryptDeterministically() err = %v, want nil", err)
+	}
+	if _, err := p.DecryptDeterministically(ct, aad); err != nil {
+		t.Fatalf("p.DecryptDeterministically() err = %v, want nil", err)
+	}
+	got := client.Events()
+	wantKeysetInfo := monitoring.NewKeysetInfo(
+		annotations,
+		kh.KeysetInfo().GetPrimaryKeyId(),
+		[]*monitoring.Entry{
+			{
+				Status:    monitoring.Enabled,
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				KeyType:   "tink.AesSivKey",
+				KeyPrefix: "RAW",
+			},
+		},
+	)
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("daead", "encrypt", wantKeysetInfo),
+			KeyID:    wantKeysetInfo.PrimaryKeyID,
+			NumBytes: len(data),
+		},
+		{
+			Context:  monitoring.NewContext("daead", "decrypt", wantKeysetInfo),
+			KeyID:    wantKeysetInfo.PrimaryKeyID,
+			NumBytes: len(ct),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestFactoryWithMonitoringPrimitiveWithMultipleKeysLogsEncryptionDecryption(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	manager := keyset.NewManager()
+	numKeys := 4
+	keyIDs := make([]uint32, numKeys, numKeys)
+	var err error
+	for i := 0; i < numKeys; i++ {
+		keyIDs[i], err = manager.Add(daead.AESSIVKeyTemplate())
+		if err != nil {
+			t.Fatalf("manager.Add() err = %v, want nil", err)
+		}
+	}
+	if err := manager.SetPrimary(keyIDs[1]); err != nil {
+		t.Fatalf("manager.SetPrimary(%d) err = %v, want nil", keyIDs[1], err)
+	}
+	if err := manager.Disable(keyIDs[0]); err != nil {
+		t.Fatalf("manager.Disable(%d) err = %v, want nil", keyIDs[0], err)
+	}
+	kh, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := daead.New(mh)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	data := []byte("YELLOW_ORANGE")
+	ct, err := p.EncryptDeterministically(data, nil)
+	if err != nil {
+		t.Fatalf("p.EncryptDeterministically() err = %v, want nil", err)
+	}
+	if _, err := p.DecryptDeterministically(ct, nil); err != nil {
+		t.Fatalf("p.DecryptDeterministically() err = %v, want nil", err)
+	}
+	failures := len(client.Failures())
+	if failures != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", failures)
+	}
+	got := client.Events()
+	wantKeysetInfo := monitoring.NewKeysetInfo(annotations, kh.KeysetInfo().GetPrimaryKeyId(), []*monitoring.Entry{
+		{
+			KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+			Status:    monitoring.Enabled,
+			KeyType:   "tink.AesSivKey",
+			KeyPrefix: "TINK",
+		},
+		{
+			KeyID:     keyIDs[2],
+			Status:    monitoring.Enabled,
+			KeyType:   "tink.AesSivKey",
+			KeyPrefix: "TINK",
+		},
+		{
+			KeyID:     keyIDs[3],
+			Status:    monitoring.Enabled,
+			KeyType:   "tink.AesSivKey",
+			KeyPrefix: "TINK",
+		},
+	})
+	want := []*fakemonitoring.LogEvent{
+		{
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+			Context: monitoring.NewContext(
+				"daead",
+				"encrypt",
+				wantKeysetInfo,
+			),
+		},
+		{
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(ct) - cryptofmt.NonRawPrefixSize,
+			Context: monitoring.NewContext(
+				"daead",
+				"decrypt",
+				wantKeysetInfo,
+			),
+		},
+	}
+	// sort by keyID to avoid non deterministic order.
+	entryLessFunc := func(a, b *monitoring.Entry) bool {
+		return a.KeyID < b.KeyID
+	}
+	if !cmp.Equal(got, want, cmpopts.SortSlices(entryLessFunc)) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsEncryptionFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := &fakemonitoring.Client{Name: ""}
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	typeURL := "TestFactoryWithMonitoringPrimitiveEncryptionFailureIsLogged"
+	km := &stubkeymanager.StubKeyManager{
+		URL:  typeURL,
+		Prim: &testutil.AlwaysFailingDeterministicAead{Error: fmt.Errorf("failed")},
+		KeyData: &tinkpb.KeyData{
+			TypeUrl:         typeURL,
+			Value:           []byte("serialized_key"),
+			KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		},
+	}
+	if err := registry.RegisterKeyManager(km); err != nil {
+		t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+	}
+	template := &tinkpb.KeyTemplate{
+		TypeUrl:          typeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_LEGACY,
+	}
+	kh, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := daead.New(mh)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	if _, err := p.EncryptDeterministically(nil, nil); err == nil {
+		t.Fatalf("EncryptDeterministically() err = nil, want error")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"daead",
+				"encrypt",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   typeURL,
+							KeyPrefix: "LEGACY",
+						},
+					},
+				),
+			),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsDecryptionFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := &fakemonitoring.Client{Name: ""}
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(daead.AESSIVKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := daead.New(mh)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	if _, err := p.DecryptDeterministically([]byte("invalid_data"), nil); err == nil {
+		t.Fatalf("DecryptDeterministically() err = nil, want error")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"daead",
+				"decrypt",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesSivKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestFactoryWithMonitoringMultiplePrimitivesLogOperations(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := &fakemonitoring.Client{Name: ""}
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh1, err := keyset.NewHandle(daead.AESSIVKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh1, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh1, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p1, err := daead.New(mh1)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	kh2, err := keyset.NewHandle(daead.AESSIVKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(kh2, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	mh2, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p2, err := daead.New(mh2)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	d1 := []byte("YELLOW_ORANGE")
+	if _, err := p1.EncryptDeterministically(d1, nil); err != nil {
+		t.Fatalf("p1.EncryptDeterministically() err = %v, want nil", err)
+	}
+	d2 := []byte("ORANGE_BLUE")
+	if _, err := p2.EncryptDeterministically(d2, nil); err != nil {
+		t.Fatalf("p2.EncryptDeterministically() err = %v, want nil", err)
+	}
+	got := client.Events()
+	want := []*fakemonitoring.LogEvent{
+		{
+			KeyID:    kh1.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(d1),
+			Context: monitoring.NewContext(
+				"daead",
+				"encrypt",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh1.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh1.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesSivKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+		{
+			KeyID:    kh2.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(d2),
+			Context: monitoring.NewContext(
+				"daead",
+				"encrypt",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh2.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh2.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesSivKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryEncryptDecryptWithoutAnnotationsDoesNotMonitor(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(daead.AESSIVKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	p, err := daead.New(kh)
+	if err != nil {
+		t.Fatalf("daead.New() err = %v, want nil", err)
+	}
+	data := []byte("hello_world")
+	ct, err := p.EncryptDeterministically(data, nil)
+	if err != nil {
+		t.Fatalf("p.EncryptDeterministically() err = %v, want nil", err)
+	}
+	if _, err := p.DecryptDeterministically(ct, nil); err != nil {
+		t.Fatalf("p.DecryptDeterministically() err = %v, want nil", err)
+	}
+	got := client.Events()
+	if len(got) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(got))
+	}
+}
diff --git a/go/daead/daead_init_test.go b/go/daead/daead_init_test.go
new file mode 100644
index 0000000..7284115
--- /dev/null
+++ b/go/daead/daead_init_test.go
@@ -0,0 +1,32 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package daead_test
+
+import (
+	"testing"
+
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestDeterministicAEADInit(t *testing.T) {
+	// Check that the AES-SIV key manager is in the global registry.
+	_, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+}
diff --git a/go/daead/daead_key_templates.go b/go/daead/daead_key_templates.go
index 381b652..a04c0a8 100644
--- a/go/daead/daead_key_templates.go
+++ b/go/daead/daead_key_templates.go
@@ -17,7 +17,10 @@
 package daead
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	aspb "github.com/google/tink/go/proto/aes_siv_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -27,7 +30,10 @@
 	format := &aspb.AesSivKeyFormat{
 		KeySize: 64,
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          aesSIVTypeURL,
 		OutputPrefixType: tinkpb.OutputPrefixType_TINK,
diff --git a/go/daead/daead_test.go b/go/daead/daead_test.go
index ad92646..c7c3e79 100644
--- a/go/daead/daead_test.go
+++ b/go/daead/daead_test.go
@@ -16,64 +16,79 @@
 
 package daead_test
 
+// [START deterministic-aead-example]
+
 import (
 	"bytes"
-	"encoding/base64"
 	"fmt"
 	"log"
-	"testing"
 
-	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/daead"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testutil"
 )
 
 func Example() {
-	kh, err := keyset.NewHandle(daead.AESSIVKeyTemplate())
-	if err != nil {
-		log.Fatal(err)
-	}
+	// A keyset created with "tinkey create-keyset --key-template=AES256_SIV". Note
+	// that this keyset has the secret key information in cleartext.
+	jsonKeyset := `{
+			"key": [{
+				"keyData": {
+						"keyMaterialType":
+								"SYMMETRIC",
+						"typeUrl":
+								"type.googleapis.com/google.crypto.tink.AesSivKey",
+						"value":
+								"EkAl9HCMmKTN1p3V186uhZpJQ+tivyc4IKyE+opg6SsEbWQ/WesWHzwCRrlgRuxdaggvgMzwWhjPnkk9gptBnGLK"
+				},
+				"keyId": 1919301694,
+				"outputPrefixType": "TINK",
+				"status": "ENABLED"
+		}],
+		"primaryKeyId": 1919301694
+	}`
 
-	// TODO: save the keyset to a safe location. DO NOT hardcode it in source code.
+	// Create a keyset handle from the cleartext keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the exposure of accessing the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use a insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
 	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
 	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
-
-	d, err := daead.New(kh)
+	keysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(jsonKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	msg := []byte("this data needs to be encrypted")
-	aad := []byte("this data needs to be authenticated, but not encrypted")
-	ct1, err := d.EncryptDeterministically(msg, aad)
+	// Retrieve the DAEAD primitive we want to use from the keyset handle.
+	primitive, err := daead.New(keysetHandle)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	pt, err := d.DecryptDeterministically(ct1, aad)
+	// Use the primitive to encrypt a message. In this case the primary key of the
+	// keyset will be used (which is also the only key in this example).
+	plaintext := []byte("message")
+	associatedData := []byte("associated data")
+	ciphertext, err := primitive.EncryptDeterministically(plaintext, associatedData)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	ct2, err := d.EncryptDeterministically(msg, aad)
+	// Use the primitive to decrypt the message. Decrypt finds the correct key in
+	// the keyset and decrypts the ciphertext. If no key is found or decryption
+	// fails, it returns an error.
+	decrypted, err := primitive.DecryptDeterministically(ciphertext, associatedData)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	if !bytes.Equal(ct1, ct2) {
-		log.Fatal("ct1 != ct2")
-	}
-
-	fmt.Printf("Ciphertext: %s\n", base64.StdEncoding.EncodeToString(ct1))
-	fmt.Printf("Original  plaintext: %s\n", msg)
-	fmt.Printf("Decrypted Plaintext: %s\n", pt)
+	fmt.Println(ciphertext)
+	fmt.Println(string(decrypted))
+	// Output:
+	// [1 114 102 56 62 150 98 146 84 99 211 36 127 214 229 231 157 56 143 192 250 132 32 153 124 244 238 112]
+	// message
 }
 
-func TestDeterministicAEADInit(t *testing.T) {
-	// Check for AES-SIV key manager.
-	_, err := registry.GetKeyManager(testutil.AESSIVTypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-}
+// [END deterministic-aead-example]
diff --git a/go/deps.bzl b/go/deps.bzl
index 1155a48..9ff1b70 100644
--- a/go/deps.bzl
+++ b/go/deps.bzl
@@ -11,51 +11,30 @@
     go_repository(
         name = "co_honnef_go_tools",
         importpath = "honnef.co/go/tools",
-        sum = "h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=",
-        version = "v0.0.1-2020.1.4",
+        sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=",
+        version = "v0.0.0-20190523083050-ea95bdfd59fc",
     )
-    go_repository(
-        name = "com_github_alecthomas_template",
-        importpath = "github.com/alecthomas/template",
-        sum = "h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=",
-        version = "v0.0.0-20190718012654-fb15b899a751",
-    )
-    go_repository(
-        name = "com_github_alecthomas_units",
-        importpath = "github.com/alecthomas/units",
-        sum = "h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=",
-        version = "v0.0.0-20190717042225-c3de453c63f4",
-    )
+
     go_repository(
         name = "com_github_antihax_optional",
         importpath = "github.com/antihax/optional",
         sum = "h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=",
         version = "v1.0.0",
     )
-    go_repository(
-        name = "com_github_armon_go_metrics",
-        importpath = "github.com/armon/go-metrics",
-        sum = "h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18=",
-        version = "v0.3.9",
-    )
+
     go_repository(
         name = "com_github_armon_go_radix",
         importpath = "github.com/armon/go-radix",
-        sum = "h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=",
-        version = "v1.0.0",
+        sum = "h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=",
+        version = "v0.0.0-20180808171621-7fddfc383310",
     )
     go_repository(
         name = "com_github_aws_aws_sdk_go",
         importpath = "github.com/aws/aws-sdk-go",
-        sum = "h1:k1S/29Bp2QD5ZopnGzIn0Sp63yyt3WH1JRE2OOU3Aig=",
-        version = "v1.43.9",
+        sum = "h1:Asrp6EMqqRxZvjK0NjzkWcrOk15RnWtupuUrUuZMabk=",
+        version = "v1.44.267",
     )
-    go_repository(
-        name = "com_github_beorn7_perks",
-        importpath = "github.com/beorn7/perks",
-        sum = "h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=",
-        version = "v1.0.1",
-    )
+
     go_repository(
         name = "com_github_bgentry_speakeasy",
         importpath = "github.com/bgentry/speakeasy",
@@ -68,12 +47,7 @@
         sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=",
         version = "v0.3.1",
     )
-    go_repository(
-        name = "com_github_burntsushi_xgb",
-        importpath = "github.com/BurntSushi/xgb",
-        sum = "h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=",
-        version = "v0.0.0-20160522181843-27f122750802",
-    )
+
     go_repository(
         name = "com_github_cenkalti_backoff_v3",
         importpath = "github.com/cenkalti/backoff/v3",
@@ -83,51 +57,17 @@
     go_repository(
         name = "com_github_census_instrumentation_opencensus_proto",
         importpath = "github.com/census-instrumentation/opencensus-proto",
-        sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=",
-        version = "v0.2.1",
+        sum = "h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=",
+        version = "v0.4.1",
     )
-    go_repository(
-        name = "com_github_cespare_xxhash",
-        importpath = "github.com/cespare/xxhash",
-        sum = "h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=",
-        version = "v1.1.0",
-    )
+
     go_repository(
         name = "com_github_cespare_xxhash_v2",
         importpath = "github.com/cespare/xxhash/v2",
-        sum = "h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=",
-        version = "v2.1.1",
+        sum = "h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=",
+        version = "v2.2.0",
     )
-    go_repository(
-        name = "com_github_chzyer_logex",
-        importpath = "github.com/chzyer/logex",
-        sum = "h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=",
-        version = "v1.1.10",
-    )
-    go_repository(
-        name = "com_github_chzyer_readline",
-        importpath = "github.com/chzyer/readline",
-        sum = "h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=",
-        version = "v0.0.0-20180603132655-2972be24d48e",
-    )
-    go_repository(
-        name = "com_github_chzyer_test",
-        importpath = "github.com/chzyer/test",
-        sum = "h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=",
-        version = "v0.0.0-20180213035817-a1ea475d72b1",
-    )
-    go_repository(
-        name = "com_github_circonus_labs_circonus_gometrics",
-        importpath = "github.com/circonus-labs/circonus-gometrics",
-        sum = "h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY=",
-        version = "v2.3.1+incompatible",
-    )
-    go_repository(
-        name = "com_github_circonus_labs_circonusllhist",
-        importpath = "github.com/circonus-labs/circonusllhist",
-        sum = "h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA=",
-        version = "v0.1.3",
-    )
+
     go_repository(
         name = "com_github_client9_misspell",
         importpath = "github.com/client9/misspell",
@@ -137,27 +77,16 @@
     go_repository(
         name = "com_github_cncf_udpa_go",
         importpath = "github.com/cncf/udpa/go",
-        sum = "h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=",
-        version = "v0.0.0-20210930031921-04548b0d99d4",
+        sum = "h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=",
+        version = "v0.0.0-20220112060539-c52dc94e7fbe",
     )
     go_repository(
         name = "com_github_cncf_xds_go",
         importpath = "github.com/cncf/xds/go",
-        sum = "h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=",
-        version = "v0.0.0-20211011173535-cb28da3451f1",
+        sum = "h1:ACGZRIr7HsgBKHsueQ1yM4WaVaXh21ynwqsF8M8tXhA=",
+        version = "v0.0.0-20230105202645-06c439db220b",
     )
-    go_repository(
-        name = "com_github_creack_pty",
-        importpath = "github.com/creack/pty",
-        sum = "h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=",
-        version = "v1.1.9",
-    )
-    go_repository(
-        name = "com_github_datadog_datadog_go",
-        importpath = "github.com/DataDog/datadog-go",
-        sum = "h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4=",
-        version = "v3.2.0+incompatible",
-    )
+
     go_repository(
         name = "com_github_davecgh_go_spew",
         importpath = "github.com/davecgh/go-spew",
@@ -167,104 +96,42 @@
     go_repository(
         name = "com_github_envoyproxy_go_control_plane",
         importpath = "github.com/envoyproxy/go-control-plane",
-        sum = "h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs=",
-        version = "v0.9.10-0.20210907150352-cf90f659a021",
+        sum = "h1:xdCVXxEe0Y3FQith+0cj2irwZudqGYvecuLB1HtdexY=",
+        version = "v0.10.3",
     )
     go_repository(
         name = "com_github_envoyproxy_protoc_gen_validate",
         importpath = "github.com/envoyproxy/protoc-gen-validate",
-        sum = "h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=",
-        version = "v0.1.0",
+        sum = "h1:PS7VIOgmSVhWUEeZwTe7z7zouA22Cr590PzXKbZHOVY=",
+        version = "v0.9.1",
     )
-    go_repository(
-        name = "com_github_evanphx_json_patch_v5",
-        importpath = "github.com/evanphx/json-patch/v5",
-        sum = "h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc=",
-        version = "v5.5.0",
-    )
+
     go_repository(
         name = "com_github_fatih_color",
         importpath = "github.com/fatih/color",
         sum = "h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=",
         version = "v1.7.0",
     )
-    go_repository(
-        name = "com_github_fatih_structs",
-        importpath = "github.com/fatih/structs",
-        sum = "h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=",
-        version = "v1.1.0",
-    )
-    go_repository(
-        name = "com_github_frankban_quicktest",
-        importpath = "github.com/frankban/quicktest",
-        sum = "h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=",
-        version = "v1.13.0",
-    )
+
     go_repository(
         name = "com_github_ghodss_yaml",
         importpath = "github.com/ghodss/yaml",
         sum = "h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=",
         version = "v1.0.0",
     )
-    go_repository(
-        name = "com_github_go_asn1_ber_asn1_ber",
-        importpath = "github.com/go-asn1-ber/asn1-ber",
-        sum = "h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=",
-        version = "v1.3.1",
-    )
-    go_repository(
-        name = "com_github_go_gl_glfw",
-        importpath = "github.com/go-gl/glfw",
-        sum = "h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=",
-        version = "v0.0.0-20190409004039-e6da0acd62b1",
-    )
-    go_repository(
-        name = "com_github_go_gl_glfw_v3_3_glfw",
-        importpath = "github.com/go-gl/glfw/v3.3/glfw",
-        sum = "h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=",
-        version = "v0.0.0-20200222043503-6f7a984d4dc4",
-    )
-    go_repository(
-        name = "com_github_go_kit_kit",
-        importpath = "github.com/go-kit/kit",
-        sum = "h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=",
-        version = "v0.9.0",
-    )
-    go_repository(
-        name = "com_github_go_ldap_ldap_v3",
-        importpath = "github.com/go-ldap/ldap/v3",
-        sum = "h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ=",
-        version = "v3.1.10",
-    )
-    go_repository(
-        name = "com_github_go_logfmt_logfmt",
-        importpath = "github.com/go-logfmt/logfmt",
-        sum = "h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=",
-        version = "v0.4.0",
-    )
-    go_repository(
-        name = "com_github_go_stack_stack",
-        importpath = "github.com/go-stack/stack",
-        sum = "h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=",
-        version = "v1.8.0",
-    )
+
     go_repository(
         name = "com_github_go_test_deep",
         importpath = "github.com/go-test/deep",
         sum = "h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=",
         version = "v1.0.2",
     )
-    go_repository(
-        name = "com_github_gogo_protobuf",
-        importpath = "github.com/gogo/protobuf",
-        sum = "h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=",
-        version = "v1.1.1",
-    )
+
     go_repository(
         name = "com_github_golang_glog",
         importpath = "github.com/golang/glog",
-        sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=",
-        version = "v0.0.0-20160126235308-23def4e6c14b",
+        sum = "h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=",
+        version = "v1.0.0",
     )
     go_repository(
         name = "com_github_golang_groupcache",
@@ -275,75 +142,49 @@
     go_repository(
         name = "com_github_golang_mock",
         importpath = "github.com/golang/mock",
-        sum = "h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=",
-        version = "v1.6.0",
+        sum = "h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=",
+        version = "v1.1.1",
     )
     go_repository(
         name = "com_github_golang_protobuf",
         importpath = "github.com/golang/protobuf",
-        sum = "h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=",
-        version = "v1.5.2",
+        sum = "h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=",
+        version = "v1.5.3",
     )
-    go_repository(
-        name = "com_github_golang_snappy",
-        importpath = "github.com/golang/snappy",
-        sum = "h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=",
-        version = "v0.0.4",
-    )
-    go_repository(
-        name = "com_github_google_btree",
-        importpath = "github.com/google/btree",
-        sum = "h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=",
-        version = "v1.0.0",
-    )
+
     go_repository(
         name = "com_github_google_go_cmp",
         importpath = "github.com/google/go-cmp",
-        sum = "h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=",
-        version = "v0.5.7",
+        sum = "h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=",
+        version = "v0.5.9",
     )
     go_repository(
-        name = "com_github_google_gofuzz",
-        importpath = "github.com/google/gofuzz",
-        sum = "h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=",
-        version = "v1.0.0",
+        name = "com_github_google_s2a_go",
+        importpath = "github.com/google/s2a-go",
+        sum = "h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=",
+        version = "v0.1.3",
     )
-    go_repository(
-        name = "com_github_google_martian",
-        importpath = "github.com/google/martian",
-        sum = "h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=",
-        version = "v2.1.0+incompatible",
-    )
-    go_repository(
-        name = "com_github_google_martian_v3",
-        importpath = "github.com/google/martian/v3",
-        sum = "h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=",
-        version = "v3.2.1",
-    )
-    go_repository(
-        name = "com_github_google_pprof",
-        importpath = "github.com/google/pprof",
-        sum = "h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=",
-        version = "v0.0.0-20210720184732-4bb14d4b1be1",
-    )
-    go_repository(
-        name = "com_github_google_renameio",
-        importpath = "github.com/google/renameio",
-        sum = "h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=",
-        version = "v0.1.0",
-    )
+
     go_repository(
         name = "com_github_google_uuid",
         importpath = "github.com/google/uuid",
-        sum = "h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=",
-        version = "v1.1.2",
+        sum = "h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=",
+        version = "v1.3.0",
     )
     go_repository(
+        name = "com_github_googleapis_enterprise_certificate_proxy",
+        importpath = "github.com/googleapis/enterprise-certificate-proxy",
+        sum = "h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=",
+        version = "v0.2.3",
+    )
+
+    go_repository(
         name = "com_github_googleapis_gax_go_v2",
         importpath = "github.com/googleapis/gax-go/v2",
-        sum = "h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=",
-        version = "v2.1.1",
+        sum = "h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=",
+        version = "v2.8.0",
     )
+
     go_repository(
         name = "com_github_grpc_ecosystem_grpc_gateway",
         importpath = "github.com/grpc-ecosystem/grpc-gateway",
@@ -368,30 +209,14 @@
         sum = "h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=",
         version = "v0.16.2",
     )
-    go_repository(
-        name = "com_github_hashicorp_go_immutable_radix",
-        importpath = "github.com/hashicorp/go-immutable-radix",
-        sum = "h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=",
-        version = "v1.3.1",
-    )
-    go_repository(
-        name = "com_github_hashicorp_go_kms_wrapping_entropy",
-        importpath = "github.com/hashicorp/go-kms-wrapping/entropy",
-        sum = "h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs=",
-        version = "v0.1.0",
-    )
+
     go_repository(
         name = "com_github_hashicorp_go_multierror",
         importpath = "github.com/hashicorp/go-multierror",
         sum = "h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=",
         version = "v1.1.1",
     )
-    go_repository(
-        name = "com_github_hashicorp_go_plugin",
-        importpath = "github.com/hashicorp/go-plugin",
-        sum = "h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=",
-        version = "v1.4.3",
-    )
+
     go_repository(
         name = "com_github_hashicorp_go_retryablehttp",
         importpath = "github.com/hashicorp/go-retryablehttp",
@@ -404,66 +229,28 @@
         sum = "h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=",
         version = "v1.0.2",
     )
-    go_repository(
-        name = "com_github_hashicorp_go_secure_stdlib_base62",
-        importpath = "github.com/hashicorp/go-secure-stdlib/base62",
-        sum = "h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ=",
-        version = "v0.1.1",
-    )
-    go_repository(
-        name = "com_github_hashicorp_go_secure_stdlib_mlock",
-        importpath = "github.com/hashicorp/go-secure-stdlib/mlock",
-        sum = "h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc=",
-        version = "v0.1.1",
-    )
+
     go_repository(
         name = "com_github_hashicorp_go_secure_stdlib_parseutil",
         importpath = "github.com/hashicorp/go-secure-stdlib/parseutil",
-        sum = "h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI=",
-        version = "v0.1.1",
+        sum = "h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=",
+        version = "v0.1.6",
     )
-    go_repository(
-        name = "com_github_hashicorp_go_secure_stdlib_password",
-        importpath = "github.com/hashicorp/go-secure-stdlib/password",
-        sum = "h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60=",
-        version = "v0.1.1",
-    )
+
     go_repository(
         name = "com_github_hashicorp_go_secure_stdlib_strutil",
         importpath = "github.com/hashicorp/go-secure-stdlib/strutil",
-        sum = "h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788=",
-        version = "v0.1.1",
+        sum = "h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=",
+        version = "v0.1.2",
     )
-    go_repository(
-        name = "com_github_hashicorp_go_secure_stdlib_tlsutil",
-        importpath = "github.com/hashicorp/go-secure-stdlib/tlsutil",
-        sum = "h1:Yc026VyMyIpq1UWRnakHRG01U8fJm+nEfEmjoAb00n8=",
-        version = "v0.1.1",
-    )
+
     go_repository(
         name = "com_github_hashicorp_go_sockaddr",
         importpath = "github.com/hashicorp/go-sockaddr",
         sum = "h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=",
         version = "v1.0.2",
     )
-    go_repository(
-        name = "com_github_hashicorp_go_uuid",
-        importpath = "github.com/hashicorp/go-uuid",
-        sum = "h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=",
-        version = "v1.0.2",
-    )
-    go_repository(
-        name = "com_github_hashicorp_go_version",
-        importpath = "github.com/hashicorp/go-version",
-        sum = "h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=",
-        version = "v1.2.0",
-    )
-    go_repository(
-        name = "com_github_hashicorp_golang_lru",
-        importpath = "github.com/hashicorp/golang-lru",
-        sum = "h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=",
-        version = "v0.5.4",
-    )
+
     go_repository(
         name = "com_github_hashicorp_hcl",
         importpath = "github.com/hashicorp/hcl",
@@ -473,39 +260,10 @@
     go_repository(
         name = "com_github_hashicorp_vault_api",
         importpath = "github.com/hashicorp/vault/api",
-        sum = "h1:mWLfPT0RhxBitjKr6swieCEP2v5pp/M//t70S3kMLRo=",
-        version = "v1.4.1",
+        sum = "h1:LtY/I16+5jVGU8rufyyAkwopgq/HpUnxFBg+QLOAV38=",
+        version = "v1.9.1",
     )
-    go_repository(
-        name = "com_github_hashicorp_vault_sdk",
-        importpath = "github.com/hashicorp/vault/sdk",
-        sum = "h1:3SaHOJY687jY1fnB61PtL0cOkKItphrbLmux7T92HBo=",
-        version = "v0.4.1",
-    )
-    go_repository(
-        name = "com_github_hashicorp_yamux",
-        importpath = "github.com/hashicorp/yamux",
-        sum = "h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=",
-        version = "v0.0.0-20180604194846-3520598351bb",
-    )
-    go_repository(
-        name = "com_github_ianlancetaylor_demangle",
-        importpath = "github.com/ianlancetaylor/demangle",
-        sum = "h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=",
-        version = "v0.0.0-20200824232613-28f6c0f3b639",
-    )
-    go_repository(
-        name = "com_github_jessevdk_go_flags",
-        importpath = "github.com/jessevdk/go-flags",
-        sum = "h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=",
-        version = "v1.4.0",
-    )
-    go_repository(
-        name = "com_github_jhump_protoreflect",
-        importpath = "github.com/jhump/protoreflect",
-        sum = "h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=",
-        version = "v1.6.0",
-    )
+
     go_repository(
         name = "com_github_jmespath_go_jmespath",
         importpath = "github.com/jmespath/go-jmespath",
@@ -518,60 +276,7 @@
         sum = "h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=",
         version = "v1.5.1",
     )
-    go_repository(
-        name = "com_github_json_iterator_go",
-        importpath = "github.com/json-iterator/go",
-        sum = "h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=",
-        version = "v1.1.9",
-    )
-    go_repository(
-        name = "com_github_jstemmer_go_junit_report",
-        importpath = "github.com/jstemmer/go-junit-report",
-        sum = "h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=",
-        version = "v0.9.1",
-    )
-    go_repository(
-        name = "com_github_julienschmidt_httprouter",
-        importpath = "github.com/julienschmidt/httprouter",
-        sum = "h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=",
-        version = "v1.2.0",
-    )
-    go_repository(
-        name = "com_github_kisielk_gotool",
-        importpath = "github.com/kisielk/gotool",
-        sum = "h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=",
-        version = "v1.0.0",
-    )
-    go_repository(
-        name = "com_github_konsorten_go_windows_terminal_sequences",
-        importpath = "github.com/konsorten/go-windows-terminal-sequences",
-        sum = "h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=",
-        version = "v1.0.1",
-    )
-    go_repository(
-        name = "com_github_kr_logfmt",
-        importpath = "github.com/kr/logfmt",
-        sum = "h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=",
-        version = "v0.0.0-20140226030751-b84e30acd515",
-    )
-    go_repository(
-        name = "com_github_kr_pretty",
-        importpath = "github.com/kr/pretty",
-        sum = "h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=",
-        version = "v0.2.1",
-    )
-    go_repository(
-        name = "com_github_kr_pty",
-        importpath = "github.com/kr/pty",
-        sum = "h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=",
-        version = "v1.1.1",
-    )
-    go_repository(
-        name = "com_github_kr_text",
-        importpath = "github.com/kr/text",
-        sum = "h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=",
-        version = "v0.2.0",
-    )
+
     go_repository(
         name = "com_github_mattn_go_colorable",
         importpath = "github.com/mattn/go-colorable",
@@ -584,36 +289,21 @@
         sum = "h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=",
         version = "v0.0.12",
     )
-    go_repository(
-        name = "com_github_matttproud_golang_protobuf_extensions",
-        importpath = "github.com/matttproud/golang_protobuf_extensions",
-        sum = "h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=",
-        version = "v1.0.1",
-    )
+
     go_repository(
         name = "com_github_mitchellh_cli",
         importpath = "github.com/mitchellh/cli",
         sum = "h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=",
         version = "v1.0.0",
     )
-    go_repository(
-        name = "com_github_mitchellh_copystructure",
-        importpath = "github.com/mitchellh/copystructure",
-        sum = "h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=",
-        version = "v1.0.0",
-    )
+
     go_repository(
         name = "com_github_mitchellh_go_homedir",
         importpath = "github.com/mitchellh/go-homedir",
         sum = "h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=",
         version = "v1.1.0",
     )
-    go_repository(
-        name = "com_github_mitchellh_go_testing_interface",
-        importpath = "github.com/mitchellh/go-testing-interface",
-        sum = "h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=",
-        version = "v1.0.0",
-    )
+
     go_repository(
         name = "com_github_mitchellh_go_wordwrap",
         importpath = "github.com/mitchellh/go-wordwrap",
@@ -623,57 +313,10 @@
     go_repository(
         name = "com_github_mitchellh_mapstructure",
         importpath = "github.com/mitchellh/mapstructure",
-        sum = "h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=",
-        version = "v1.4.2",
+        sum = "h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=",
+        version = "v1.5.0",
     )
-    go_repository(
-        name = "com_github_mitchellh_reflectwalk",
-        importpath = "github.com/mitchellh/reflectwalk",
-        sum = "h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=",
-        version = "v1.0.0",
-    )
-    go_repository(
-        name = "com_github_modern_go_concurrent",
-        importpath = "github.com/modern-go/concurrent",
-        sum = "h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=",
-        version = "v0.0.0-20180306012644-bacd9c7ef1dd",
-    )
-    go_repository(
-        name = "com_github_modern_go_reflect2",
-        importpath = "github.com/modern-go/reflect2",
-        sum = "h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=",
-        version = "v1.0.1",
-    )
-    go_repository(
-        name = "com_github_mwitkow_go_conntrack",
-        importpath = "github.com/mwitkow/go-conntrack",
-        sum = "h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=",
-        version = "v0.0.0-20161129095857-cc309e4a2223",
-    )
-    go_repository(
-        name = "com_github_oklog_run",
-        importpath = "github.com/oklog/run",
-        sum = "h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=",
-        version = "v1.0.0",
-    )
-    go_repository(
-        name = "com_github_oneofone_xxhash",
-        importpath = "github.com/OneOfOne/xxhash",
-        sum = "h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=",
-        version = "v1.2.2",
-    )
-    go_repository(
-        name = "com_github_pascaldekloe_goe",
-        importpath = "github.com/pascaldekloe/goe",
-        sum = "h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=",
-        version = "v0.1.0",
-    )
-    go_repository(
-        name = "com_github_pierrec_lz4",
-        importpath = "github.com/pierrec/lz4",
-        sum = "h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=",
-        version = "v2.5.2+incompatible",
-    )
+
     go_repository(
         name = "com_github_pkg_errors",
         importpath = "github.com/pkg/errors",
@@ -692,42 +335,21 @@
         sum = "h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=",
         version = "v1.1.1",
     )
-    go_repository(
-        name = "com_github_prometheus_client_golang",
-        importpath = "github.com/prometheus/client_golang",
-        sum = "h1:YVIb/fVcOTMSqtqZWSKnHpSLBxu8DKgxq8z6RuBZwqI=",
-        version = "v1.4.0",
-    )
+
     go_repository(
         name = "com_github_prometheus_client_model",
         importpath = "github.com/prometheus/client_model",
-        sum = "h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=",
-        version = "v0.2.0",
+        sum = "h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=",
+        version = "v0.0.0-20190812154241-14fe0d1b01d4",
     )
-    go_repository(
-        name = "com_github_prometheus_common",
-        importpath = "github.com/prometheus/common",
-        sum = "h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=",
-        version = "v0.9.1",
-    )
-    go_repository(
-        name = "com_github_prometheus_procfs",
-        importpath = "github.com/prometheus/procfs",
-        sum = "h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=",
-        version = "v0.0.8",
-    )
+
     go_repository(
         name = "com_github_rogpeppe_fastuuid",
         importpath = "github.com/rogpeppe/fastuuid",
         sum = "h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=",
         version = "v1.2.0",
     )
-    go_repository(
-        name = "com_github_rogpeppe_go_internal",
-        importpath = "github.com/rogpeppe/go-internal",
-        sum = "h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=",
-        version = "v1.3.0",
-    )
+
     go_repository(
         name = "com_github_ryanuber_columnize",
         importpath = "github.com/ryanuber/columnize",
@@ -740,96 +362,772 @@
         sum = "h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=",
         version = "v1.0.0",
     )
-    go_repository(
-        name = "com_github_sirupsen_logrus",
-        importpath = "github.com/sirupsen/logrus",
-        sum = "h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=",
-        version = "v1.4.2",
-    )
-    go_repository(
-        name = "com_github_spaolacci_murmur3",
-        importpath = "github.com/spaolacci/murmur3",
-        sum = "h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=",
-        version = "v0.0.0-20180118202830-f09979ecbc72",
-    )
+
     go_repository(
         name = "com_github_stretchr_objx",
         importpath = "github.com/stretchr/objx",
-        sum = "h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=",
-        version = "v0.1.1",
+        sum = "h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=",
+        version = "v0.5.0",
     )
     go_repository(
         name = "com_github_stretchr_testify",
         importpath = "github.com/stretchr/testify",
-        sum = "h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=",
-        version = "v1.7.0",
+        sum = "h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=",
+        version = "v1.8.1",
     )
-    go_repository(
-        name = "com_github_tv42_httpunix",
-        importpath = "github.com/tv42/httpunix",
-        sum = "h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8=",
-        version = "v0.0.0-20150427012821-b75d8614f926",
-    )
+
     go_repository(
         name = "com_github_yuin_goldmark",
         importpath = "github.com/yuin/goldmark",
-        sum = "h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=",
-        version = "v1.3.5",
+        sum = "h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=",
+        version = "v1.4.13",
     )
     go_repository(
         name = "com_google_cloud_go",
         importpath = "cloud.google.com/go",
-        sum = "h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=",
-        version = "v0.100.2",
+        sum = "h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=",
+        version = "v0.110.0",
     )
     go_repository(
-        name = "com_google_cloud_go_bigquery",
-        importpath = "cloud.google.com/go/bigquery",
-        sum = "h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=",
-        version = "v1.8.0",
+        name = "com_google_cloud_go_accessapproval",
+        importpath = "cloud.google.com/go/accessapproval",
+        sum = "h1:x0cEHro/JFPd7eS4BlEWNTMecIj2HdXjOVB5BtvwER0=",
+        version = "v1.6.0",
     )
     go_repository(
-        name = "com_google_cloud_go_datastore",
-        importpath = "cloud.google.com/go/datastore",
-        sum = "h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=",
-        version = "v1.1.0",
+        name = "com_google_cloud_go_accesscontextmanager",
+        importpath = "cloud.google.com/go/accesscontextmanager",
+        sum = "h1:MG60JgnEoawHJrbWw0jGdv6HLNSf6gQvYRiXpuzqgEA=",
+        version = "v1.7.0",
     )
     go_repository(
-        name = "com_google_cloud_go_pubsub",
-        importpath = "cloud.google.com/go/pubsub",
-        sum = "h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=",
-        version = "v1.3.1",
+        name = "com_google_cloud_go_aiplatform",
+        importpath = "cloud.google.com/go/aiplatform",
+        sum = "h1:zTw+suCVchgZyO+k847wjzdVjWmrAuehxdvcZvJwfGg=",
+        version = "v1.37.0",
     )
     go_repository(
-        name = "com_google_cloud_go_storage",
-        importpath = "cloud.google.com/go/storage",
-        sum = "h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=",
+        name = "com_google_cloud_go_analytics",
+        importpath = "cloud.google.com/go/analytics",
+        sum = "h1:LqAo3tAh2FU9+w/r7vc3hBjU23Kv7GhO/PDIW7kIYgM=",
+        version = "v0.19.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_apigateway",
+        importpath = "cloud.google.com/go/apigateway",
+        sum = "h1:ZI9mVO7x3E9RK/BURm2p1aw9YTBSCQe3klmyP1WxWEg=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_apigeeconnect",
+        importpath = "cloud.google.com/go/apigeeconnect",
+        sum = "h1:sWOmgDyAsi1AZ48XRHcATC0tsi9SkPT7DA/+VCfkaeA=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_apigeeregistry",
+        importpath = "cloud.google.com/go/apigeeregistry",
+        sum = "h1:E43RdhhCxdlV+I161gUY2rI4eOaMzHTA5kNkvRsFXvc=",
+        version = "v0.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_apikeys",
+        importpath = "cloud.google.com/go/apikeys",
+        sum = "h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ=",
+        version = "v0.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_appengine",
+        importpath = "cloud.google.com/go/appengine",
+        sum = "h1:aBGDKmRIaRRoWJ2tAoN0oVSHoWLhtO9aj/NvUyP4aYs=",
+        version = "v1.7.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_area120",
+        importpath = "cloud.google.com/go/area120",
+        sum = "h1:ugckkFh4XkHJMPhTIx0CyvdoBxmOpMe8rNs4Ok8GAag=",
+        version = "v0.7.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_artifactregistry",
+        importpath = "cloud.google.com/go/artifactregistry",
+        sum = "h1:o1Q80vqEB6Qp8WLEH3b8FBLNUCrGQ4k5RFj0sn/sgO8=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_asset",
+        importpath = "cloud.google.com/go/asset",
+        sum = "h1:YAsssO08BqZ6mncbb6FPlj9h6ACS7bJQUOlzciSfbNk=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_assuredworkloads",
+        importpath = "cloud.google.com/go/assuredworkloads",
+        sum = "h1:VLGnVFta+N4WM+ASHbhc14ZOItOabDLH1MSoDv+Xuag=",
         version = "v1.10.0",
     )
     go_repository(
-        name = "com_shuralyov_dmitri_gpu_mtl",
-        importpath = "dmitri.shuralyov.com/gpu/mtl",
-        sum = "h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=",
-        version = "v0.0.0-20190408044501-666a987793e9",
+        name = "com_google_cloud_go_automl",
+        importpath = "cloud.google.com/go/automl",
+        sum = "h1:50VugllC+U4IGl3tDNcZaWvApHBTrn/TvyHDJ0wM+Uw=",
+        version = "v1.12.0",
     )
     go_repository(
-        name = "in_gopkg_alecthomas_kingpin_v2",
-        importpath = "gopkg.in/alecthomas/kingpin.v2",
-        sum = "h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=",
-        version = "v2.2.6",
+        name = "com_google_cloud_go_baremetalsolution",
+        importpath = "cloud.google.com/go/baremetalsolution",
+        sum = "h1:2AipdYXL0VxMboelTTw8c1UJ7gYu35LZYUbuRv9Q28s=",
+        version = "v0.5.0",
     )
     go_repository(
+        name = "com_google_cloud_go_batch",
+        importpath = "cloud.google.com/go/batch",
+        sum = "h1:YbMt0E6BtqeD5FvSv1d56jbVsWEzlGm55lYte+M6Mzs=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_beyondcorp",
+        importpath = "cloud.google.com/go/beyondcorp",
+        sum = "h1:UkY2BTZkEUAVrgqnSdOJ4p3y9ZRBPEe1LkjgC8Bj/Pc=",
+        version = "v0.5.0",
+    )
+
+    go_repository(
+        name = "com_google_cloud_go_bigquery",
+        importpath = "cloud.google.com/go/bigquery",
+        sum = "h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ=",
+        version = "v1.50.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_billing",
+        importpath = "cloud.google.com/go/billing",
+        sum = "h1:JYj28UYF5w6VBAh0gQYlgHJ/OD1oA+JgW29YZQU+UHM=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_binaryauthorization",
+        importpath = "cloud.google.com/go/binaryauthorization",
+        sum = "h1:d3pMDBCCNivxt5a4eaV7FwL7cSH0H7RrEnFrTb1QKWs=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_certificatemanager",
+        importpath = "cloud.google.com/go/certificatemanager",
+        sum = "h1:5C5UWeSt8Jkgp7OWn2rCkLmYurar/vIWIoSQ2+LaTOc=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_channel",
+        importpath = "cloud.google.com/go/channel",
+        sum = "h1:GpcQY5UJKeOekYgsX3QXbzzAc/kRGtBq43fTmyKe6Uw=",
+        version = "v1.12.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_cloudbuild",
+        importpath = "cloud.google.com/go/cloudbuild",
+        sum = "h1:GHQCjV4WlPPVU/j3Rlpc8vNIDwThhd1U9qSY/NPZdko=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_clouddms",
+        importpath = "cloud.google.com/go/clouddms",
+        sum = "h1:E7v4TpDGUyEm1C/4KIrpVSOCTm0P6vWdHT0I4mostRA=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_cloudtasks",
+        importpath = "cloud.google.com/go/cloudtasks",
+        sum = "h1:uK5k6abf4yligFgYFnG0ni8msai/dSv6mDmiBulU0hU=",
+        version = "v1.10.0",
+    )
+
+    go_repository(
+        name = "com_google_cloud_go_compute",
+        importpath = "cloud.google.com/go/compute",
+        sum = "h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=",
+        version = "v1.19.0",
+    )
+
+    go_repository(
+        name = "com_google_cloud_go_compute_metadata",
+        importpath = "cloud.google.com/go/compute/metadata",
+        sum = "h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=",
+        version = "v0.2.3",
+    )
+    go_repository(
+        name = "com_google_cloud_go_contactcenterinsights",
+        importpath = "cloud.google.com/go/contactcenterinsights",
+        sum = "h1:jXIpfcH/VYSE1SYcPzO0n1VVb+sAamiLOgCw45JbOQk=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_container",
+        importpath = "cloud.google.com/go/container",
+        sum = "h1:NKlY/wCDapfVZlbVVaeuu2UZZED5Dy1z4Zx1KhEzm8c=",
+        version = "v1.15.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_containeranalysis",
+        importpath = "cloud.google.com/go/containeranalysis",
+        sum = "h1:EQ4FFxNaEAg8PqQCO7bVQfWz9NVwZCUKaM1b3ycfx3U=",
+        version = "v0.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_datacatalog",
+        importpath = "cloud.google.com/go/datacatalog",
+        sum = "h1:4H5IJiyUE0X6ShQBqgFFZvGGcrwGVndTwUSLP4c52gw=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dataflow",
+        importpath = "cloud.google.com/go/dataflow",
+        sum = "h1:eYyD9o/8Nm6EttsKZaEGD84xC17bNgSKCu0ZxwqUbpg=",
+        version = "v0.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dataform",
+        importpath = "cloud.google.com/go/dataform",
+        sum = "h1:Dyk+fufup1FR6cbHjFpMuP4SfPiF3LI3JtoIIALoq48=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_datafusion",
+        importpath = "cloud.google.com/go/datafusion",
+        sum = "h1:sZjRnS3TWkGsu1LjYPFD/fHeMLZNXDK6PDHi2s2s/bk=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_datalabeling",
+        importpath = "cloud.google.com/go/datalabeling",
+        sum = "h1:ch4qA2yvddGRUrlfwrNJCr79qLqhS9QBwofPHfFlDIk=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dataplex",
+        importpath = "cloud.google.com/go/dataplex",
+        sum = "h1:RvoZ5T7gySwm1CHzAw7yY1QwwqaGswunmqEssPxU/AM=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dataproc",
+        importpath = "cloud.google.com/go/dataproc",
+        sum = "h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU=",
+        version = "v1.12.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dataqna",
+        importpath = "cloud.google.com/go/dataqna",
+        sum = "h1:yFzi/YU4YAdjyo7pXkBE2FeHbgz5OQQBVDdbErEHmVQ=",
+        version = "v0.7.0",
+    )
+
+    go_repository(
+        name = "com_google_cloud_go_datastore",
+        importpath = "cloud.google.com/go/datastore",
+        sum = "h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774=",
+        version = "v1.11.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_datastream",
+        importpath = "cloud.google.com/go/datastream",
+        sum = "h1:BBCBTnWMDwwEzQQmipUXxATa7Cm7CA/gKjKcR2w35T0=",
+        version = "v1.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_deploy",
+        importpath = "cloud.google.com/go/deploy",
+        sum = "h1:otshdKEbmsi1ELYeCKNYppwV0UH5xD05drSdBm7ouTk=",
+        version = "v1.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dialogflow",
+        importpath = "cloud.google.com/go/dialogflow",
+        sum = "h1:uVlKKzp6G/VtSW0E7IH1Y5o0H48/UOCmqksG2riYCwQ=",
+        version = "v1.32.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_dlp",
+        importpath = "cloud.google.com/go/dlp",
+        sum = "h1:1JoJqezlgu6NWCroBxr4rOZnwNFILXr4cB9dMaSKO4A=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_documentai",
+        importpath = "cloud.google.com/go/documentai",
+        sum = "h1:KM3Xh0QQyyEdC8Gs2vhZfU+rt6OCPF0dwVwxKgLmWfI=",
+        version = "v1.18.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_domains",
+        importpath = "cloud.google.com/go/domains",
+        sum = "h1:2ti/o9tlWL4N+wIuWUNH+LbfgpwxPr8J1sv9RHA4bYQ=",
+        version = "v0.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_edgecontainer",
+        importpath = "cloud.google.com/go/edgecontainer",
+        sum = "h1:O0YVE5v+O0Q/ODXYsQHmHb+sYM8KNjGZw2pjX2Ws41c=",
+        version = "v1.0.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_errorreporting",
+        importpath = "cloud.google.com/go/errorreporting",
+        sum = "h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0=",
+        version = "v0.3.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_essentialcontacts",
+        importpath = "cloud.google.com/go/essentialcontacts",
+        sum = "h1:gIzEhCoOT7bi+6QZqZIzX1Erj4SswMPIteNvYVlu+pM=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_eventarc",
+        importpath = "cloud.google.com/go/eventarc",
+        sum = "h1:fsJmNeqvqtk74FsaVDU6cH79lyZNCYP8Rrv7EhaB/PU=",
+        version = "v1.11.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_filestore",
+        importpath = "cloud.google.com/go/filestore",
+        sum = "h1:ckTEXN5towyTMu4q0uQ1Mde/JwTHur0gXs8oaIZnKfw=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_firestore",
+        importpath = "cloud.google.com/go/firestore",
+        sum = "h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_functions",
+        importpath = "cloud.google.com/go/functions",
+        sum = "h1:pPDqtsXG2g9HeOQLoquLbmvmb82Y4Ezdo1GXuotFoWg=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_gaming",
+        importpath = "cloud.google.com/go/gaming",
+        sum = "h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_gkebackup",
+        importpath = "cloud.google.com/go/gkebackup",
+        sum = "h1:za3QZvw6ujR0uyqkhomKKKNoXDyqYGPJies3voUK8DA=",
+        version = "v0.4.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_gkeconnect",
+        importpath = "cloud.google.com/go/gkeconnect",
+        sum = "h1:gXYKciHS/Lgq0GJ5Kc9SzPA35NGc3yqu6SkjonpEr2Q=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_gkehub",
+        importpath = "cloud.google.com/go/gkehub",
+        sum = "h1:TqCSPsEBQ6oZSJgEYZ3XT8x2gUadbvfwI32YB0kuHCs=",
+        version = "v0.12.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_gkemulticloud",
+        importpath = "cloud.google.com/go/gkemulticloud",
+        sum = "h1:8I84Q4vl02rJRsFiinBxl7WCozfdLlUVBQuSrqr9Wtk=",
+        version = "v0.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_gsuiteaddons",
+        importpath = "cloud.google.com/go/gsuiteaddons",
+        sum = "h1:1mvhXqJzV0Vg5Fa95QwckljODJJfDFXV4pn+iL50zzA=",
+        version = "v1.5.0",
+    )
+
+    go_repository(
+        name = "com_google_cloud_go_iam",
+        importpath = "cloud.google.com/go/iam",
+        sum = "h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=",
+        version = "v0.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_iap",
+        importpath = "cloud.google.com/go/iap",
+        sum = "h1:PxVHFuMxmSZyfntKXHXhd8bo82WJ+LcATenq7HLdVnU=",
+        version = "v1.7.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_ids",
+        importpath = "cloud.google.com/go/ids",
+        sum = "h1:fodnCDtOXuMmS8LTC2y3h8t24U8F3eKWfhi+3LY6Qf0=",
+        version = "v1.3.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_iot",
+        importpath = "cloud.google.com/go/iot",
+        sum = "h1:39W5BFSarRNZfVG0eXI5LYux+OVQT8GkgpHCnrZL2vM=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_kms",
+        importpath = "cloud.google.com/go/kms",
+        sum = "h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g=",
+        version = "v1.10.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_language",
+        importpath = "cloud.google.com/go/language",
+        sum = "h1:7Ulo2mDk9huBoBi8zCE3ONOoBrL6UXfAI71CLQ9GEIM=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_lifesciences",
+        importpath = "cloud.google.com/go/lifesciences",
+        sum = "h1:uWrMjWTsGjLZpCTWEAzYvyXj+7fhiZST45u9AgasasI=",
+        version = "v0.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_logging",
+        importpath = "cloud.google.com/go/logging",
+        sum = "h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=",
+        version = "v1.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_longrunning",
+        importpath = "cloud.google.com/go/longrunning",
+        sum = "h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=",
+        version = "v0.4.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_managedidentities",
+        importpath = "cloud.google.com/go/managedidentities",
+        sum = "h1:ZRQ4k21/jAhrHBVKl/AY7SjgzeJwG1iZa+mJ82P+VNg=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_maps",
+        importpath = "cloud.google.com/go/maps",
+        sum = "h1:mv9YaczD4oZBZkM5XJl6fXQ984IkJNHPwkc8MUsdkBo=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_mediatranslation",
+        importpath = "cloud.google.com/go/mediatranslation",
+        sum = "h1:anPxH+/WWt8Yc3EdoEJhPMBRF7EhIdz426A+tuoA0OU=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_memcache",
+        importpath = "cloud.google.com/go/memcache",
+        sum = "h1:8/VEmWCpnETCrBwS3z4MhT+tIdKgR1Z4Tr2tvYH32rg=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_metastore",
+        importpath = "cloud.google.com/go/metastore",
+        sum = "h1:QCFhZVe2289KDBQ7WxaHV2rAmPrmRAdLC6gbjUd3HPo=",
+        version = "v1.10.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_monitoring",
+        importpath = "cloud.google.com/go/monitoring",
+        sum = "h1:2qsrgXGVoRXpP7otZ14eE1I568zAa92sJSDPyOJvwjM=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_networkconnectivity",
+        importpath = "cloud.google.com/go/networkconnectivity",
+        sum = "h1:ZD6b4Pk1jEtp/cx9nx0ZYcL3BKqDa+KixNDZ6Bjs1B8=",
+        version = "v1.11.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_networkmanagement",
+        importpath = "cloud.google.com/go/networkmanagement",
+        sum = "h1:8KWEUNGcpSX9WwZXq7FtciuNGPdPdPN/ruDm769yAEM=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_networksecurity",
+        importpath = "cloud.google.com/go/networksecurity",
+        sum = "h1:sOc42Ig1K2LiKlzG71GUVloeSJ0J3mffEBYmvu+P0eo=",
+        version = "v0.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_notebooks",
+        importpath = "cloud.google.com/go/notebooks",
+        sum = "h1:Kg2K3K7CbSXYJHZ1aGQpf1xi5x2GUvQWf2sFVuiZh8M=",
+        version = "v1.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_optimization",
+        importpath = "cloud.google.com/go/optimization",
+        sum = "h1:dj8O4VOJRB4CUwZXdmwNViH1OtI0WtWL867/lnYH248=",
+        version = "v1.3.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_orchestration",
+        importpath = "cloud.google.com/go/orchestration",
+        sum = "h1:Vw+CEXo8M/FZ1rb4EjcLv0gJqqw89b7+g+C/EmniTb8=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_orgpolicy",
+        importpath = "cloud.google.com/go/orgpolicy",
+        sum = "h1:XDriMWug7sd0kYT1QKofRpRHzjad0bK8Q8uA9q+XrU4=",
+        version = "v1.10.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_osconfig",
+        importpath = "cloud.google.com/go/osconfig",
+        sum = "h1:PkSQx4OHit5xz2bNyr11KGcaFccL5oqglFPdTboyqwQ=",
+        version = "v1.11.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_oslogin",
+        importpath = "cloud.google.com/go/oslogin",
+        sum = "h1:whP7vhpmc+ufZa90eVpkfbgzJRK/Xomjz+XCD4aGwWw=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_phishingprotection",
+        importpath = "cloud.google.com/go/phishingprotection",
+        sum = "h1:l6tDkT7qAEV49MNEJkEJTB6vOO/onbSOcNtAT09HPuA=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_policytroubleshooter",
+        importpath = "cloud.google.com/go/policytroubleshooter",
+        sum = "h1:yKAGC4p9O61ttZUswaq9GAn1SZnEzTd0vUYXD7ZBT7Y=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_privatecatalog",
+        importpath = "cloud.google.com/go/privatecatalog",
+        sum = "h1:EPEJ1DpEGXLDnmc7mnCAqFmkwUJbIsaLAiLHVOkkwtc=",
+        version = "v0.8.0",
+    )
+
+    go_repository(
+        name = "com_google_cloud_go_pubsub",
+        importpath = "cloud.google.com/go/pubsub",
+        sum = "h1:vCge8m7aUKBJYOgrZp7EsNDf6QMd2CAlXZqWTn3yq6s=",
+        version = "v1.30.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_pubsublite",
+        importpath = "cloud.google.com/go/pubsublite",
+        sum = "h1:cb9fsrtpINtETHiJ3ECeaVzrfIVhcGjhhJEjybHXHao=",
+        version = "v1.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_recaptchaenterprise_v2",
+        importpath = "cloud.google.com/go/recaptchaenterprise/v2",
+        sum = "h1:6iOCujSNJ0YS7oNymI64hXsjGq60T4FK1zdLugxbzvU=",
+        version = "v2.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_recommendationengine",
+        importpath = "cloud.google.com/go/recommendationengine",
+        sum = "h1:VibRFCwWXrFebEWKHfZAt2kta6pS7Tlimsnms0fjv7k=",
+        version = "v0.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_recommender",
+        importpath = "cloud.google.com/go/recommender",
+        sum = "h1:ZnFRY5R6zOVk2IDS1Jbv5Bw+DExCI5rFumsTnMXiu/A=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_redis",
+        importpath = "cloud.google.com/go/redis",
+        sum = "h1:JoAd3SkeDt3rLFAAxEvw6wV4t+8y4ZzfZcZmddqphQ8=",
+        version = "v1.11.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_resourcemanager",
+        importpath = "cloud.google.com/go/resourcemanager",
+        sum = "h1:NRM0p+RJkaQF9Ee9JMnUV9BQ2QBIOq/v8M+Pbv/wmCs=",
+        version = "v1.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_resourcesettings",
+        importpath = "cloud.google.com/go/resourcesettings",
+        sum = "h1:8Dua37kQt27CCWHm4h/Q1XqCF6ByD7Ouu49xg95qJzI=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_retail",
+        importpath = "cloud.google.com/go/retail",
+        sum = "h1:1Dda2OpFNzIb4qWgFZjYlpP7sxX3aLeypKG6A3H4Yys=",
+        version = "v1.12.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_run",
+        importpath = "cloud.google.com/go/run",
+        sum = "h1:ydJQo+k+MShYnBfhaRHSZYeD/SQKZzZLAROyfpeD9zw=",
+        version = "v0.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_scheduler",
+        importpath = "cloud.google.com/go/scheduler",
+        sum = "h1:NpQAHtx3sulByTLe2dMwWmah8PWgeoieFPpJpArwFV0=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_secretmanager",
+        importpath = "cloud.google.com/go/secretmanager",
+        sum = "h1:pu03bha7ukxF8otyPKTFdDz+rr9sE3YauS5PliDXK60=",
+        version = "v1.10.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_security",
+        importpath = "cloud.google.com/go/security",
+        sum = "h1:PYvDxopRQBfYAXKAuDpFCKBvDOWPWzp9k/H5nB3ud3o=",
+        version = "v1.13.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_securitycenter",
+        importpath = "cloud.google.com/go/securitycenter",
+        sum = "h1:AF3c2s3awNTMoBtMX3oCUoOMmGlYxGOeuXSYHNBkf14=",
+        version = "v1.19.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_servicecontrol",
+        importpath = "cloud.google.com/go/servicecontrol",
+        sum = "h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE=",
+        version = "v1.11.1",
+    )
+    go_repository(
+        name = "com_google_cloud_go_servicedirectory",
+        importpath = "cloud.google.com/go/servicedirectory",
+        sum = "h1:SJwk0XX2e26o25ObYUORXx6torSFiYgsGkWSkZgkoSU=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_servicemanagement",
+        importpath = "cloud.google.com/go/servicemanagement",
+        sum = "h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY=",
+        version = "v1.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_serviceusage",
+        importpath = "cloud.google.com/go/serviceusage",
+        sum = "h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_shell",
+        importpath = "cloud.google.com/go/shell",
+        sum = "h1:wT0Uw7ib7+AgZST9eCDygwTJn4+bHMDtZo5fh7kGWDU=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_spanner",
+        importpath = "cloud.google.com/go/spanner",
+        sum = "h1:7VdjZ8zj4sHbDw55atp5dfY6kn1j9sam9DRNpPQhqR4=",
+        version = "v1.45.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_speech",
+        importpath = "cloud.google.com/go/speech",
+        sum = "h1:JEVoWGNnTF128kNty7T4aG4eqv2z86yiMJPT9Zjp+iw=",
+        version = "v1.15.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_storagetransfer",
+        importpath = "cloud.google.com/go/storagetransfer",
+        sum = "h1:5T+PM+3ECU3EY2y9Brv0Sf3oka8pKmsCfpQ07+91G9o=",
+        version = "v1.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_talent",
+        importpath = "cloud.google.com/go/talent",
+        sum = "h1:nI9sVZPjMKiO2q3Uu0KhTDVov3Xrlpt63fghP9XjyEM=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_texttospeech",
+        importpath = "cloud.google.com/go/texttospeech",
+        sum = "h1:H4g1ULStsbVtalbZGktyzXzw6jP26RjVGYx9RaYjBzc=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_tpu",
+        importpath = "cloud.google.com/go/tpu",
+        sum = "h1:/34T6CbSi+kTv5E19Q9zbU/ix8IviInZpzwz3rsFE+A=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_trace",
+        importpath = "cloud.google.com/go/trace",
+        sum = "h1:olxC0QHC59zgJVALtgqfD9tGk0lfeCP5/AGXL3Px/no=",
+        version = "v1.9.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_translate",
+        importpath = "cloud.google.com/go/translate",
+        sum = "h1:GvLP4oQ4uPdChBmBaUSa/SaZxCdyWELtlAaKzpHsXdA=",
+        version = "v1.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_video",
+        importpath = "cloud.google.com/go/video",
+        sum = "h1:upIbnGI0ZgACm58HPjAeBMleW3sl5cT84AbYQ8PWOgM=",
+        version = "v1.15.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_videointelligence",
+        importpath = "cloud.google.com/go/videointelligence",
+        sum = "h1:Uh5BdoET8XXqXX2uXIahGb+wTKbLkGH7s4GXR58RrG8=",
+        version = "v1.10.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_vision_v2",
+        importpath = "cloud.google.com/go/vision/v2",
+        sum = "h1:8C8RXUJoflCI4yVdqhTy9tRyygSHmp60aP363z23HKg=",
+        version = "v2.7.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_vmmigration",
+        importpath = "cloud.google.com/go/vmmigration",
+        sum = "h1:Azs5WKtfOC8pxvkyrDvt7J0/4DYBch0cVbuFfCCFt5k=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_vmwareengine",
+        importpath = "cloud.google.com/go/vmwareengine",
+        sum = "h1:b0NBu7S294l0gmtrT0nOJneMYgZapr5x9tVWvgDoVEM=",
+        version = "v0.3.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_vpcaccess",
+        importpath = "cloud.google.com/go/vpcaccess",
+        sum = "h1:FOe6CuiQD3BhHJWt7E8QlbBcaIzVRddupwJlp7eqmn4=",
+        version = "v1.6.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_webrisk",
+        importpath = "cloud.google.com/go/webrisk",
+        sum = "h1:IY+L2+UwxcVm2zayMAtBhZleecdIFLiC+QJMzgb0kT0=",
+        version = "v1.8.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_websecurityscanner",
+        importpath = "cloud.google.com/go/websecurityscanner",
+        sum = "h1:AHC1xmaNMOZtNqxI9Rmm87IJEyPaRkOxeI0gpAacXGk=",
+        version = "v1.5.0",
+    )
+    go_repository(
+        name = "com_google_cloud_go_workflows",
+        importpath = "cloud.google.com/go/workflows",
+        sum = "h1:FfGp9w0cYnaKZJhUOMqCOJCYT/WlvYBfTQhFWV3sRKI=",
+        version = "v1.10.0",
+    )
+
+    go_repository(
         name = "in_gopkg_check_v1",
         importpath = "gopkg.in/check.v1",
-        sum = "h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=",
-        version = "v1.0.0-20190902080502-41f04d3bba15",
+        sum = "h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=",
+        version = "v0.0.0-20161208181325-20d25e280405",
     )
-    go_repository(
-        name = "in_gopkg_errgo_v2",
-        importpath = "gopkg.in/errgo.v2",
-        sum = "h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=",
-        version = "v2.1.0",
-    )
+
     go_repository(
         name = "in_gopkg_square_go_jose_v2",
         importpath = "gopkg.in/square/go-jose.v2",
@@ -845,14 +1143,14 @@
     go_repository(
         name = "in_gopkg_yaml_v3",
         importpath = "gopkg.in/yaml.v3",
-        sum = "h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=",
-        version = "v3.0.0-20200313102051-9f266ea9e77c",
+        sum = "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=",
+        version = "v3.0.1",
     )
     go_repository(
         name = "io_opencensus_go",
         importpath = "go.opencensus.io",
-        sum = "h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=",
-        version = "v0.23.0",
+        sum = "h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=",
+        version = "v0.24.0",
     )
     go_repository(
         name = "io_opentelemetry_go_proto_otlp",
@@ -860,29 +1158,12 @@
         sum = "h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8=",
         version = "v0.7.0",
     )
-    go_repository(
-        name = "io_rsc_binaryregexp",
-        importpath = "rsc.io/binaryregexp",
-        sum = "h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=",
-        version = "v0.2.0",
-    )
-    go_repository(
-        name = "io_rsc_quote_v3",
-        importpath = "rsc.io/quote/v3",
-        sum = "h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=",
-        version = "v3.1.0",
-    )
-    go_repository(
-        name = "io_rsc_sampler",
-        importpath = "rsc.io/sampler",
-        sum = "h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=",
-        version = "v1.3.0",
-    )
+
     go_repository(
         name = "org_golang_google_api",
         importpath = "google.golang.org/api",
-        sum = "h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw=",
-        version = "v0.70.0",
+        sum = "h1:yHVU//vA+qkOhm4reEC9LtzHVUCN/IqqNRl1iQ9xE20=",
+        version = "v0.123.0",
     )
     go_repository(
         name = "org_golang_google_appengine",
@@ -893,99 +1174,84 @@
     go_repository(
         name = "org_golang_google_genproto",
         importpath = "google.golang.org/genproto",
-        sum = "h1:TU4rFa5APdKTq0s6B7WTsH6Xmx0Knj86s6Biz56mErE=",
-        version = "v0.0.0-20220218161850-94dd64e39d7c",
+        sum = "h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=",
+        version = "v0.0.0-20230410155749-daa745c078e1",
     )
     go_repository(
         name = "org_golang_google_grpc",
         build_file_proto_mode = "disable",
         importpath = "google.golang.org/grpc",
-        sum = "h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=",
-        version = "v1.44.0",
+        sum = "h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=",
+        version = "v1.54.0",
     )
-    go_repository(
-        name = "org_golang_google_grpc_cmd_protoc_gen_go_grpc",
-        importpath = "google.golang.org/grpc/cmd/protoc-gen-go-grpc",
-        sum = "h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=",
-        version = "v1.1.0",
-    )
+
     go_repository(
         name = "org_golang_google_protobuf",
         importpath = "google.golang.org/protobuf",
-        sum = "h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=",
-        version = "v1.27.1",
+        sum = "h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=",
+        version = "v1.30.0",
     )
     go_repository(
         name = "org_golang_x_crypto",
         importpath = "golang.org/x/crypto",
-        sum = "h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=",
-        version = "v0.0.0-20220214200702-86341886e292",
+        sum = "h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=",
+        version = "v0.9.0",
     )
     go_repository(
         name = "org_golang_x_exp",
         importpath = "golang.org/x/exp",
-        sum = "h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=",
-        version = "v0.0.0-20200224162631-6cc2880d07d6",
+        sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=",
+        version = "v0.0.0-20190121172915-509febef88a4",
     )
-    go_repository(
-        name = "org_golang_x_image",
-        importpath = "golang.org/x/image",
-        sum = "h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=",
-        version = "v0.0.0-20190802002840-cff245a6509b",
-    )
+
     go_repository(
         name = "org_golang_x_lint",
         importpath = "golang.org/x/lint",
-        sum = "h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=",
-        version = "v0.0.0-20210508222113-6edffad5e616",
+        sum = "h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=",
+        version = "v0.0.0-20190313153728-d0100b6bd8b3",
     )
-    go_repository(
-        name = "org_golang_x_mobile",
-        importpath = "golang.org/x/mobile",
-        sum = "h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=",
-        version = "v0.0.0-20190719004257-d2bd2a29d028",
-    )
+
     go_repository(
         name = "org_golang_x_mod",
         importpath = "golang.org/x/mod",
-        sum = "h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=",
-        version = "v0.4.2",
+        sum = "h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=",
+        version = "v0.8.0",
     )
     go_repository(
         name = "org_golang_x_net",
         importpath = "golang.org/x/net",
-        sum = "h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=",
-        version = "v0.0.0-20220127200216-cd36cc0744dd",
+        sum = "h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=",
+        version = "v0.10.0",
     )
     go_repository(
         name = "org_golang_x_oauth2",
         importpath = "golang.org/x/oauth2",
-        sum = "h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=",
-        version = "v0.0.0-20220223155221-ee480838109b",
+        sum = "h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=",
+        version = "v0.7.0",
     )
     go_repository(
         name = "org_golang_x_sync",
         importpath = "golang.org/x/sync",
-        sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=",
-        version = "v0.0.0-20210220032951-036812b2e83c",
+        sum = "h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=",
+        version = "v0.1.0",
     )
     go_repository(
         name = "org_golang_x_sys",
         importpath = "golang.org/x/sys",
-        sum = "h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=",
-        version = "v0.0.0-20220209214540-3681064d5158",
+        sum = "h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=",
+        version = "v0.8.0",
     )
     go_repository(
         name = "org_golang_x_term",
         importpath = "golang.org/x/term",
-        sum = "h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=",
-        version = "v0.0.0-20210927222741-03fcf44c2211",
+        sum = "h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=",
+        version = "v0.8.0",
     )
     go_repository(
         name = "org_golang_x_text",
         importpath = "golang.org/x/text",
-        sum = "h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=",
-        version = "v0.3.7",
+        sum = "h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=",
+        version = "v0.9.0",
     )
     go_repository(
         name = "org_golang_x_time",
@@ -996,8 +1262,8 @@
     go_repository(
         name = "org_golang_x_tools",
         importpath = "golang.org/x/tools",
-        sum = "h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=",
-        version = "v0.1.5",
+        sum = "h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=",
+        version = "v0.6.0",
     )
     go_repository(
         name = "org_golang_x_xerrors",
@@ -1005,9 +1271,3 @@
         sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=",
         version = "v0.0.0-20200804184101-5ec99f83aff1",
     )
-    go_repository(
-        name = "org_uber_go_atomic",
-        importpath = "go.uber.org/atomic",
-        sum = "h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=",
-        version = "v1.9.0",
-    )
diff --git a/go/go.mod b/go/go.mod
index 9384596..d4f5d24 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -1,65 +1,47 @@
 module github.com/google/tink/go
 
-go 1.17
+go 1.19
 
 require (
-	github.com/aws/aws-sdk-go v1.43.9
-	github.com/google/go-cmp v0.5.7
-	github.com/hashicorp/vault/api v1.4.1
-	golang.org/x/crypto v0.0.0-20220214200702-86341886e292
-	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
-	google.golang.org/api v0.70.0
-	google.golang.org/protobuf v1.27.1
+	github.com/aws/aws-sdk-go v1.44.267
+	github.com/google/go-cmp v0.5.9
+	github.com/hashicorp/vault/api v1.9.1
+	golang.org/x/crypto v0.9.0
+	google.golang.org/api v0.123.0
+	google.golang.org/protobuf v1.30.0
 )
 
 require (
-	cloud.google.com/go/compute v1.3.0 // indirect
-	github.com/armon/go-metrics v0.3.9 // indirect
-	github.com/armon/go-radix v1.0.0 // indirect
+	cloud.google.com/go/compute v1.19.0 // indirect
+	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	github.com/cenkalti/backoff/v3 v3.0.0 // indirect
-	github.com/fatih/color v1.7.0 // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/golang/snappy v0.0.4 // indirect
-	github.com/googleapis/gax-go/v2 v2.1.1 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/google/s2a-go v0.1.3 // indirect
+	github.com/google/uuid v1.3.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
+	github.com/googleapis/gax-go/v2 v2.8.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-	github.com/hashicorp/go-hclog v0.16.2 // indirect
-	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/hashicorp/go-plugin v1.4.3 // indirect
 	github.com/hashicorp/go-retryablehttp v0.6.6 // indirect
 	github.com/hashicorp/go-rootcerts v1.0.2 // indirect
-	github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect
-	github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect
-	github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect
+	github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
+	github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
 	github.com/hashicorp/go-sockaddr v1.0.2 // indirect
-	github.com/hashicorp/go-uuid v1.0.2 // indirect
-	github.com/hashicorp/go-version v1.2.0 // indirect
-	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/hashicorp/vault/sdk v0.4.1 // indirect
-	github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
-	github.com/mattn/go-colorable v0.1.6 // indirect
-	github.com/mattn/go-isatty v0.0.12 // indirect
-	github.com/mitchellh/copystructure v1.0.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
-	github.com/mitchellh/go-testing-interface v1.0.0 // indirect
-	github.com/mitchellh/mapstructure v1.4.2 // indirect
-	github.com/mitchellh/reflectwalk v1.0.0 // indirect
-	github.com/oklog/run v1.0.0 // indirect
-	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/ryanuber/go-glob v1.0.0 // indirect
-	go.opencensus.io v0.23.0 // indirect
-	go.uber.org/atomic v1.9.0 // indirect
-	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
-	golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	golang.org/x/net v0.10.0 // indirect
+	golang.org/x/oauth2 v0.7.0 // indirect
+	golang.org/x/sys v0.8.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
-	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect
-	google.golang.org/grpc v1.44.0 // indirect
+	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+	google.golang.org/grpc v1.54.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
 )
diff --git a/go/go.sum b/go/go.sum
index 766fe50..cb046e7 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -1,148 +1,49 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
-cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
-cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
-cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
-cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
-cloud.google.com/go/compute v1.3.0 h1:mPL/MzDDYHsh5tHRS9mhmhWlcgClCrCa6ApQCU6wnHI=
-cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
+cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
+cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18=
-github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
-github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aws/aws-sdk-go v1.43.9 h1:k1S/29Bp2QD5ZopnGzIn0Sp63yyt3WH1JRE2OOU3Aig=
-github.com/aws/aws-sdk-go v1.43.9/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/aws/aws-sdk-go v1.44.267 h1:Asrp6EMqqRxZvjK0NjzkWcrOk15RnWtupuUrUuZMabk=
+github.com/aws/aws-sdk-go v1.44.267/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
 github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
-github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
-github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
-github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
-github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
-github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
-github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -152,605 +53,189 @@
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
+github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
-github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
+github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
-github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
-github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
-github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
-github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
-github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
 github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
 github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
-github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
-github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc=
-github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
-github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI=
-github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
-github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo=
-github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
 github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
-github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
 github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
 github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
-github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
-github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/vault/api v1.4.1 h1:mWLfPT0RhxBitjKr6swieCEP2v5pp/M//t70S3kMLRo=
-github.com/hashicorp/vault/api v1.4.1/go.mod h1:LkMdrZnWNrFaQyYYazWVn7KshilfDidgVBq6YiTq/bM=
-github.com/hashicorp/vault/sdk v0.4.1 h1:3SaHOJY687jY1fnB61PtL0cOkKItphrbLmux7T92HBo=
-github.com/hashicorp/vault/sdk v0.4.1/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
-github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
-github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
-github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
+github.com/hashicorp/vault/api v1.9.1 h1:LtY/I16+5jVGU8rufyyAkwopgq/HpUnxFBg+QLOAV38=
+github.com/hashicorp/vault/api v1.9.1/go.mod h1:78kktNcQYbBGSrOjQfHjXN32OhhxXnbYl3zxpd2uPUs=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
-github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
-github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
-github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
-github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
-github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
-github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
-github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
-github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
-go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
-golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
-golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
+golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
 golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
-google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
-google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
-google.golang.org/api v0.70.0 h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw=
-google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
+google.golang.org/api v0.123.0 h1:yHVU//vA+qkOhm4reEC9LtzHVUCN/IqqNRl1iQ9xE20=
+google.golang.org/api v0.123.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c h1:TU4rFa5APdKTq0s6B7WTsH6Xmx0Knj86s6Biz56mErE=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
-google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=
-google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -759,35 +244,20 @@
 google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
 gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/go/hybrid/BUILD.bazel b/go/hybrid/BUILD.bazel
index 3babea4..7bca0d0 100644
--- a/go/hybrid/BUILD.bazel
+++ b/go/hybrid/BUILD.bazel
@@ -22,7 +22,11 @@
         "//core/registry",
         "//hybrid/internal/hpke",
         "//hybrid/subtle",
+        "//internal/internalregistry",
+        "//internal/monitoringutil",
+        "//internal/tinkerror",
         "//keyset",
+        "//monitoring",
         "//proto/aes_ctr_hmac_aead_go_proto",
         "//proto/aes_gcm_go_proto",
         "//proto/aes_siv_go_proto",
@@ -44,6 +48,7 @@
         "ecies_aead_hkdf_hybrid_encrypt_test.go",
         "hpke_private_key_manager_test.go",
         "hpke_public_key_manager_test.go",
+        "hybrid_b243759652_test.go",
         "hybrid_factory_test.go",
         "hybrid_key_templates_test.go",
         "hybrid_test.go",
@@ -51,22 +56,29 @@
     embed = [":hybrid"],
     deps = [
         "//aead",
+        "//core/cryptofmt",
         "//core/registry",
         "//daead",
         "//hybrid/internal/hpke",
         "//hybrid/subtle",
+        "//insecurecleartextkeyset",
+        "//internal/internalregistry",
+        "//internal/testing/stubkeymanager",
         "//keyset",
         "//mac",
+        "//monitoring",
         "//proto/common_go_proto",
         "//proto/hpke_go_proto",
         "//proto/tink_go_proto",
         "//signature",
         "//subtle",
         "//subtle/random",
+        "//testing/fakemonitoring",
         "//testkeyset",
         "//testutil",
         "//tink",
         "@com_github_google_go_cmp//cmp",
+        "@com_github_google_go_cmp//cmp/cmpopts",
         "@org_golang_google_protobuf//proto",
         "@org_golang_google_protobuf//testing/protocmp",
     ],
diff --git a/go/hybrid/hybrid_b243759652_test.go b/go/hybrid/hybrid_b243759652_test.go
new file mode 100644
index 0000000..a2aa98e
--- /dev/null
+++ b/go/hybrid/hybrid_b243759652_test.go
@@ -0,0 +1,78 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package hybrid_test
+
+import (
+	"testing"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/hybrid"
+	"github.com/google/tink/go/keyset"
+)
+
+/* This test demonstrates buggy behavior of Tink. Please do not rely on it. */
+func TestBHybridDecrypt(t *testing.T) {
+	manager := keyset.NewManager()
+	id, err := manager.Add(aead.AES256GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("manager.Add gives err = '%v', want nil", err)
+	}
+	err = manager.SetPrimary(id)
+	if err != nil {
+		t.Fatalf("manager.SetPrimary gives err = '%v', want nil", err)
+	}
+	_, err = manager.Add(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template())
+	if err != nil {
+		t.Fatalf("manager.Add gives err = '%v', want nil", err)
+	}
+	handle, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle gives err = '%v', want nil", err)
+	}
+
+	// TODO(b/243759652): This should fail.
+	_, err = hybrid.NewHybridDecrypt(handle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridDecrypt gives err = '%v', you fixed b/243759652, please change the test.", err)
+	}
+}
+
+/* This test demonstrates buggy behavior of Tink. Please do not rely on it. */
+func TestBHybridEncrypt(t *testing.T) {
+	handle, err := keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle gives err = '%v', want nil", err)
+	}
+	pub, err := handle.Public()
+	if err != nil {
+		t.Fatalf("handle.Public gives err = '%v', want nil", err)
+	}
+	manager := keyset.NewManagerFromHandle(pub)
+	_, err = manager.Add(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("manager.Add gives err = '%v', want nil", err)
+	}
+	mixedHandle, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle gives err = '%v', want nil", err)
+	}
+	// TODO(b/243759652): We now have a HybridEncrypt and a Aead key, so this should fail
+	_, err = hybrid.NewHybridEncrypt(mixedHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridEncrypt gives err = '%v', you fixed b/243759652, please change the test.", err)
+	}
+}
diff --git a/go/hybrid/hybrid_decrypt_factory.go b/go/hybrid/hybrid_decrypt_factory.go
index 64b7e72..614da17 100644
--- a/go/hybrid/hybrid_decrypt_factory.go
+++ b/go/hybrid/hybrid_decrypt_factory.go
@@ -21,35 +21,32 @@
 
 	"github.com/google/tink/go/core/cryptofmt"
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/tink"
 )
 
 // NewHybridDecrypt returns an HybridDecrypt primitive from the given keyset handle.
-func NewHybridDecrypt(h *keyset.Handle) (tink.HybridDecrypt, error) {
-	return NewHybridDecryptWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewHybridDecryptWithKeyManager returns an HybridDecrypt primitive from the given keyset handle
-// and custom key manager.
-//
-// Deprecated: Use [NewHybridDecrypt].
-func NewHybridDecryptWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.HybridDecrypt, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func NewHybridDecrypt(handle *keyset.Handle) (tink.HybridDecrypt, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("hybrid_factory: cannot obtain primitive set: %s", err)
 	}
-
 	return newWrappedHybridDecrypt(ps)
 }
 
 // wrappedHybridDecrypt is an HybridDecrypt implementation that uses the underlying primitive set
 // for decryption.
 type wrappedHybridDecrypt struct {
-	ps *primitiveset.PrimitiveSet
+	ps     *primitiveset.PrimitiveSet
+	logger monitoring.Logger
 }
 
+// compile time assertion that wrappedHybridDecrypt implements the HybridDecrypt interface.
+var _ tink.HybridDecrypt = (*wrappedHybridDecrypt)(nil)
+
 func newWrappedHybridDecrypt(ps *primitiveset.PrimitiveSet) (*wrappedHybridDecrypt, error) {
 	if _, ok := (ps.Primary.Primitive).(tink.HybridDecrypt); !ok {
 		return nil, fmt.Errorf("hybrid_factory: not a HybridDecrypt primitive")
@@ -62,11 +59,29 @@
 			}
 		}
 	}
+	logger, err := createDecryptLogger(ps)
+	if err != nil {
+		return nil, err
+	}
+	return &wrappedHybridDecrypt{
+		ps:     ps,
+		logger: logger,
+	}, nil
+}
 
-	ret := new(wrappedHybridDecrypt)
-	ret.ps = ps
-
-	return ret, nil
+func createDecryptLogger(ps *primitiveset.PrimitiveSet) (monitoring.Logger, error) {
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, nil
+	}
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, err
+	}
+	return internalregistry.GetMonitoringClient().NewLogger(&monitoring.Context{
+		KeysetInfo:  keysetInfo,
+		Primitive:   "hybrid_decrypt",
+		APIFunction: "decrypt",
+	})
 }
 
 // Decrypt decrypts the given ciphertext, verifying the integrity of contextInfo.
@@ -87,6 +102,7 @@
 
 				pt, err := p.Decrypt(ctNoPrefix, contextInfo)
 				if err == nil {
+					a.logger.Log(entries[i].KeyID, len(ctNoPrefix))
 					return pt, nil
 				}
 			}
@@ -104,11 +120,13 @@
 
 			pt, err := p.Decrypt(ciphertext, contextInfo)
 			if err == nil {
+				a.logger.Log(entries[i].KeyID, len(ciphertext))
 				return pt, nil
 			}
 		}
 	}
 
 	// nothing worked
+	a.logger.LogFailure()
 	return nil, fmt.Errorf("hybrid_factory: decryption failed")
 }
diff --git a/go/hybrid/hybrid_encrypt_factory.go b/go/hybrid/hybrid_encrypt_factory.go
index 35dc268..04db9c2 100644
--- a/go/hybrid/hybrid_encrypt_factory.go
+++ b/go/hybrid/hybrid_encrypt_factory.go
@@ -20,33 +20,31 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/tink"
 )
 
 // NewHybridEncrypt returns an HybridEncrypt primitive from the given keyset handle.
-func NewHybridEncrypt(h *keyset.Handle) (tink.HybridEncrypt, error) {
-	return NewHybridEncryptWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewHybridEncryptWithKeyManager returns an HybridEncrypt primitive from the given keyset handle and custom key manager.
-//
-// Deprecated: Use [NewHybridEncrypt].
-func NewHybridEncryptWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.HybridEncrypt, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func NewHybridEncrypt(handle *keyset.Handle) (tink.HybridEncrypt, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("hybrid_factory: cannot obtain primitive set: %s", err)
 	}
-
 	return newEncryptPrimitiveSet(ps)
 }
 
 // encryptPrimitiveSet is an HybridEncrypt implementation that uses the underlying primitive set for encryption.
 type wrappedHybridEncrypt struct {
-	ps *primitiveset.PrimitiveSet
+	ps     *primitiveset.PrimitiveSet
+	logger monitoring.Logger
 }
 
+// compile time assertion that wrappedHybridEncrypt implements the HybridEncrypt interface.
+var _ tink.HybridEncrypt = (*wrappedHybridEncrypt)(nil)
+
 func newEncryptPrimitiveSet(ps *primitiveset.PrimitiveSet) (*wrappedHybridEncrypt, error) {
 	if _, ok := (ps.Primary.Primitive).(tink.HybridEncrypt); !ok {
 		return nil, fmt.Errorf("hybrid_factory: not a HybridEncrypt primitive")
@@ -59,11 +57,29 @@
 			}
 		}
 	}
+	logger, err := createEncryptLogger(ps)
+	if err != nil {
+		return nil, err
+	}
+	return &wrappedHybridEncrypt{
+		ps:     ps,
+		logger: logger,
+	}, nil
+}
 
-	ret := new(wrappedHybridEncrypt)
-	ret.ps = ps
-
-	return ret, nil
+func createEncryptLogger(ps *primitiveset.PrimitiveSet) (monitoring.Logger, error) {
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, nil
+	}
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, err
+	}
+	return internalregistry.GetMonitoringClient().NewLogger(&monitoring.Context{
+		KeysetInfo:  keysetInfo,
+		Primitive:   "hybrid_encrypt",
+		APIFunction: "encrypt",
+	})
 }
 
 // Encrypt encrypts the given plaintext binding contextInfo to the resulting ciphertext.
@@ -77,7 +93,15 @@
 
 	ct, err := p.Encrypt(plaintext, contextInfo)
 	if err != nil {
+		a.logger.LogFailure()
 		return nil, err
 	}
-	return append([]byte(primary.Prefix), ct...), nil
+	a.logger.Log(primary.KeyID, len(plaintext))
+	if len(primary.Prefix) == 0 {
+		return ct, nil
+	}
+	output := make([]byte, 0, len(primary.Prefix)+len(ct))
+	output = append(output, primary.Prefix...)
+	output = append(output, ct...)
+	return output, nil
 }
diff --git a/go/hybrid/hybrid_factory_test.go b/go/hybrid/hybrid_factory_test.go
index d0d06cc..1d6b3c3 100644
--- a/go/hybrid/hybrid_factory_test.go
+++ b/go/hybrid/hybrid_factory_test.go
@@ -18,14 +18,24 @@
 
 import (
 	"bytes"
+	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/cryptofmt"
+	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/hybrid"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/signature"
 	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testing/fakemonitoring"
 	"github.com/google/tink/go/testkeyset"
 	"github.com/google/tink/go/testutil"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
@@ -182,3 +192,517 @@
 		t.Errorf("NewHybridDecrypt(privHandleWithoutPrimary) err = nil, want not nil")
 	}
 }
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsLogsEncryptAndDecryptWithPrefix(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(pubHandle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	pubHandle, err = insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	e, err := hybrid.NewHybridEncrypt(pubHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridEncrypt() err = %v, want nil", err)
+	}
+	d, err := hybrid.NewHybridDecrypt(privHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridDecrypt() err = %v, want nil", err)
+	}
+	data := []byte("some_secret_piece_of_data")
+	aad := []byte("some_non_secret_piece_of_data")
+	ct, err := e.Encrypt(data, aad)
+	if err != nil {
+		t.Fatalf("e.Encrypt() err = %v, want nil", err)
+	}
+	if _, err := d.Decrypt(ct, aad); err != nil {
+		t.Fatalf("d.Decrypt() err = %v, want nil", err)
+	}
+	got := client.Events()
+	wantEncryptKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     pubHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePublicKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	wantDecryptKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     privHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePrivateKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("hybrid_encrypt", "encrypt", wantEncryptKeysetInfo),
+			KeyID:    privHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+		{
+			Context: monitoring.NewContext("hybrid_decrypt", "decrypt", wantDecryptKeysetInfo),
+			KeyID:   privHandle.KeysetInfo().GetPrimaryKeyId(),
+			// ciphertext was encrypted with a key that has a TINK output prefix. This adds a
+			// 5-byte prefix to the ciphertext. This prefix is not included in the `Log` call.
+			NumBytes: len(ct) - cryptofmt.NonRawPrefixSize,
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsLogsEncryptAndDecryptWithoutPrefix(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Raw_Key_Template())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(pubHandle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	pubHandle, err = insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	e, err := hybrid.NewHybridEncrypt(pubHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridEncrypt() err = %v, want nil", err)
+	}
+	d, err := hybrid.NewHybridDecrypt(privHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridDecrypt() err = %v, want nil", err)
+	}
+	data := []byte("some_secret_piece_of_data")
+	aad := []byte("some_non_secret_piece_of_data")
+	ct, err := e.Encrypt(data, aad)
+	if err != nil {
+		t.Fatalf("e.Encrypt() err = %v, want nil", err)
+	}
+	if _, err := d.Decrypt(ct, aad); err != nil {
+		t.Fatalf("d.Decrypt() err = %v, want nil", err)
+	}
+	got := client.Events()
+	wantEncryptKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     pubHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePublicKey",
+				KeyPrefix: "RAW",
+			},
+		},
+	}
+	wantDecryptKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     privHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePrivateKey",
+				KeyPrefix: "RAW",
+			},
+		},
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("hybrid_encrypt", "encrypt", wantEncryptKeysetInfo),
+			KeyID:    pubHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+		{
+			Context:  monitoring.NewContext("hybrid_decrypt", "decrypt", wantDecryptKeysetInfo),
+			KeyID:    privHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(ct),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryWithMonitoringWithMultipleKeysLogsEncryptionDecryption(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	manager := keyset.NewManager()
+	templates := []*tinkpb.KeyTemplate{
+		hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template(),
+		hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_Raw_Key_Template(),
+		hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Key_Template(),
+		hybrid.ECIESHKDFAES128GCMKeyTemplate(),
+	}
+	keyIDs := make([]uint32, 4, 4)
+	var err error
+	for i, tm := range templates {
+		keyIDs[i], err = manager.Add(tm)
+		if err != nil {
+			t.Fatalf("manager.Add() err = %v, want nil", err)
+		}
+	}
+	if err := manager.SetPrimary(keyIDs[1]); err != nil {
+		t.Fatalf("manager.SetPrimary(%d) err = %v, want nil", keyIDs[1], err)
+	}
+	if err := manager.Disable(keyIDs[0]); err != nil {
+		t.Fatalf("manager.Disable(%d) err = %v, want nil", keyIDs[0], err)
+	}
+	handle, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(pubHandle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	pubHandle, err = insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	e, err := hybrid.NewHybridEncrypt(pubHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridEncrypt() err = %v, want nil", err)
+	}
+	d, err := hybrid.NewHybridDecrypt(privHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridDecrypt() err = %v, want nil", err)
+	}
+	data := []byte("some_secret_piece_of_data")
+	aad := []byte("some_non_secret_piece_of_data")
+	ct, err := e.Encrypt(data, aad)
+	if err != nil {
+		t.Fatalf("e.Encrypt() err = %v, want nil", err)
+	}
+	if _, err := d.Decrypt(ct, aad); err != nil {
+		t.Fatalf("d.Decrypt() err = %v, want nil", err)
+	}
+	failures := len(client.Failures())
+	if failures != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", failures)
+	}
+	got := client.Events()
+	wantEncryptKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     pubHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePublicKey",
+				KeyPrefix: "RAW",
+			},
+			{
+				KeyID:     keyIDs[2],
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePublicKey",
+				KeyPrefix: "TINK",
+			},
+			{
+				KeyID:     keyIDs[3],
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.EciesAeadHkdfPublicKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	wantDecryptKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     privHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePrivateKey",
+				KeyPrefix: "RAW",
+			},
+			{
+				KeyID:     keyIDs[2],
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HpkePrivateKey",
+				KeyPrefix: "TINK",
+			},
+			{
+				KeyID:     keyIDs[3],
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.EciesAeadHkdfPrivateKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("hybrid_encrypt", "encrypt", wantEncryptKeysetInfo),
+			KeyID:    privHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+		{
+			Context:  monitoring.NewContext("hybrid_decrypt", "decrypt", wantDecryptKeysetInfo),
+			KeyID:    privHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(ct),
+		},
+	}
+	// sort by keyID to avoid non deterministic order.
+	entryLessFunc := func(a, b *monitoring.Entry) bool {
+		return a.KeyID < b.KeyID
+	}
+	if diff := cmp.Diff(want, got, cmpopts.SortSlices(entryLessFunc)); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsEncryptFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	// Since this key type will be registered in the registry,
+	// we create a very unique typeURL to avoid colliding with other tests.
+	privKeyTypeURL := "TestPrimitiveFactoryWithMonitoringEncryptionFailureIsLogged" + "PrivateKeyManager"
+	pubKeyTypeURL := "TestPrimitiveFactoryWithMonitoringEncryptionFailureIsLogged" + "PublicKeyManager"
+	template := &tinkpb.KeyTemplate{
+		TypeUrl:          privKeyTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_LEGACY,
+	}
+	privKeyManager := &stubkeymanager.StubPrivateKeyManager{
+		StubKeyManager: stubkeymanager.StubKeyManager{
+			URL: privKeyTypeURL,
+			KeyData: &tinkpb.KeyData{
+				TypeUrl:         template.TypeUrl,
+				Value:           []byte("some_data"),
+				KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE,
+			},
+		},
+		PubKeyData: &tinkpb.KeyData{
+			TypeUrl:         pubKeyTypeURL,
+			Value:           []byte("some_data"),
+			KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
+		},
+	}
+	pubKeyManager := &stubkeymanager.StubKeyManager{
+		URL: pubKeyTypeURL,
+		// TODO(b/243759652): implement an always failing HybridEncrypt
+		Prim: &testutil.AlwaysFailingAead{Error: fmt.Errorf("panic at the kernel")},
+	}
+	if err := registry.RegisterKeyManager(privKeyManager); err != nil {
+		t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+	}
+	if err := registry.RegisterKeyManager(pubKeyManager); err != nil {
+		t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(pubHandle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	pubHandle, err = insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	e, err := hybrid.NewHybridEncrypt(pubHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridEncrypt() err = %v, want nil", err)
+	}
+	if _, err := e.Encrypt(nil, nil); err == nil {
+		t.Fatalf("e.Encrypt() err = nil, want non-nil error")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"hybrid_encrypt",
+				"encrypt",
+				monitoring.NewKeysetInfo(
+					annotations,
+					pubHandle.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     pubHandle.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   pubKeyTypeURL,
+							KeyPrefix: "LEGACY",
+						},
+					},
+				),
+			),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsDecryptFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	e, err := hybrid.NewHybridDecrypt(privHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridDecrypt() err = %v, want nil", err)
+	}
+	if _, err := e.Decrypt([]byte("invalid_data"), nil); err == nil {
+		t.Fatalf("e.Decrypt() err = nil, want non-nil error")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"hybrid_decrypt",
+				"decrypt",
+				monitoring.NewKeysetInfo(
+					annotations,
+					privHandle.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     privHandle.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.HpkePrivateKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryEncryptDecryptWithoutAnnotationsDoesNotMonitor(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	privHandle, err := keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	e, err := hybrid.NewHybridEncrypt(pubHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridEncrypt() err = %v, want nil", err)
+	}
+	d, err := hybrid.NewHybridDecrypt(privHandle)
+	if err != nil {
+		t.Fatalf("hybrid.NewHybridDecrypt() err = %v, want nil", err)
+	}
+	data := []byte("some_secret_piece_of_data")
+	aad := []byte("some_non_secret_piece_of_data")
+	ct, err := e.Encrypt(data, aad)
+	if err != nil {
+		t.Fatalf("e.Encrypt() err = %v, want nil", err)
+	}
+	if _, err := d.Decrypt(ct, aad); err != nil {
+		t.Fatalf("d.Decrypt() err = %v, want nil", err)
+	}
+	if len(client.Events()) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(client.Events()))
+	}
+	if len(client.Failures()) != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", len(client.Failures()))
+	}
+}
diff --git a/go/hybrid/hybrid_key_templates.go b/go/hybrid/hybrid_key_templates.go
index ce47ec6..01c369e 100644
--- a/go/hybrid/hybrid_key_templates.go
+++ b/go/hybrid/hybrid_key_templates.go
@@ -17,22 +17,26 @@
 package hybrid
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/internal/tinkerror"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	eciespb "github.com/google/tink/go/proto/ecies_aead_hkdf_go_proto"
 	hpkepb "github.com/google/tink/go/proto/hpke_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
-// This file contains pre-generated KeyTemplates for HybridEncrypt keys. One can use these templates
-// to generate new Keysets.
+// This file contains pre-generated KeyTemplates for HybridEncrypt keys. One
+// can use these templates to generate new Keysets.
 
 // DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template creates a HPKE
-// key template with
-//  - KEM: DHKEM_X25519_HKDF_SHA256,
-//  - KDF: HKDF_SHA256, and
-//  - AEAD: AES_128_GCM.
+// key template with:
+//   - KEM: DHKEM_X25519_HKDF_SHA256,
+//   - KDF: HKDF_SHA256, and
+//   - AEAD: AES_128_GCM.
+//
 // It adds the 5-byte Tink prefix to ciphertexts.
 func DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template() *tinkpb.KeyTemplate {
 	return createHPKEKeyTemplate(
@@ -44,10 +48,11 @@
 }
 
 // DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Raw_Key_Template creates a
-// HPKE key template with
-//  - KEM: DHKEM_X25519_HKDF_SHA256,
-//  - KDF: HKDF_SHA256, and
-//  - AEAD: AES_128_GCM.
+// HPKE key template with:
+//   - KEM: DHKEM_X25519_HKDF_SHA256,
+//   - KDF: HKDF_SHA256, and
+//   - AEAD: AES_128_GCM.
+//
 // It does not add a prefix to ciphertexts.
 func DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Raw_Key_Template() *tinkpb.KeyTemplate {
 	return createHPKEKeyTemplate(
@@ -59,10 +64,11 @@
 }
 
 // DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_Key_Template creates a HPKE
-// key template with
-//  - KEM: DHKEM_X25519_HKDF_SHA256,
-//  - KDF: HKDF_SHA256, and
-//  - AEAD: AES_256_GCM.
+// key template with:
+//   - KEM: DHKEM_X25519_HKDF_SHA256,
+//   - KDF: HKDF_SHA256, and
+//   - AEAD: AES_256_GCM.
+//
 // It adds the 5-byte Tink prefix to ciphertexts.
 func DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_Key_Template() *tinkpb.KeyTemplate {
 	return createHPKEKeyTemplate(
@@ -74,10 +80,11 @@
 }
 
 // DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_Raw_Key_Template creates a
-// HPKE key template with
-//  - KEM: DHKEM_X25519_HKDF_SHA256,
-//  - KDF: HKDF_SHA256, and
-//  - AEAD: AES_256_GCM.
+// HPKE key template with:
+//   - KEM: DHKEM_X25519_HKDF_SHA256,
+//   - KDF: HKDF_SHA256, and
+//   - AEAD: AES_256_GCM.
+//
 // It does not add a prefix to ciphertexts.
 func DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_Raw_Key_Template() *tinkpb.KeyTemplate {
 	return createHPKEKeyTemplate(
@@ -88,11 +95,12 @@
 	)
 }
 
-// DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Key_Template creates a HPKE
-// key template with
-//  - KEM: DHKEM_X25519_HKDF_SHA256,
-//  - KDF: HKDF_SHA256, and
-//  - AEAD: CHACHA20_POLY1305.
+// DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Key_Template creates
+// a HPKE key template with:
+//   - KEM: DHKEM_X25519_HKDF_SHA256,
+//   - KDF: HKDF_SHA256, and
+//   - AEAD: CHACHA20_POLY1305.
+//
 // It adds the 5-byte Tink prefix to ciphertexts.
 func DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Key_Template() *tinkpb.KeyTemplate {
 	return createHPKEKeyTemplate(
@@ -103,11 +111,12 @@
 	)
 }
 
-// DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Raw_Key_Template creates a
-// HPKE key template with
-//  - KEM: DHKEM_X25519_HKDF_SHA256,
-//  - KDF: HKDF_SHA256, and
-//  - AEAD: CHACHA20_POLY1305.
+// DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Raw_Key_Template creates
+// a HPKE key template with:
+//   - KEM: DHKEM_X25519_HKDF_SHA256,
+//   - KDF: HKDF_SHA256, and
+//   - AEAD: CHACHA20_POLY1305.
+//
 // It does not add a prefix to ciphertexts.
 func DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Raw_Key_Template() *tinkpb.KeyTemplate {
 	return createHPKEKeyTemplate(
@@ -118,7 +127,8 @@
 	)
 }
 
-// createHPKEKeyTemplate creates a new HPKE key template with the given parameters.
+// createHPKEKeyTemplate creates a new HPKE key template with the given
+// parameters.
 func createHPKEKeyTemplate(kem hpkepb.HpkeKem, kdf hpkepb.HpkeKdf, aead hpkepb.HpkeAead, outputPrefixType tinkpb.OutputPrefixType) *tinkpb.KeyTemplate {
 	format := &hpkepb.HpkeKeyFormat{
 		Params: &hpkepb.HpkeParams{
@@ -127,7 +137,10 @@
 			Aead: aead,
 		},
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          hpkePrivateKeyTypeURL,
 		Value:            serializedFormat,
@@ -135,32 +148,33 @@
 	}
 }
 
-// ECIESHKDFAES128GCMKeyTemplate is a KeyTemplate that generates an ECDH P-256 and decapsulation key
-// AES128-GCM key with the following parameters:
-//  - KEM: ECDH over NIST P-256
-//  - DEM: AES128-GCM
-//  - KDF: HKDF-HMAC-SHA256 with an empty salt
+// ECIESHKDFAES128GCMKeyTemplate creates an ECIES-AEAD-HKDF key template with:
+//   - KEM: ECDH over NIST P-256
+//   - DEM: AES128-GCM
+//   - KDF: HKDF-HMAC-SHA256 with an empty salt
 func ECIESHKDFAES128GCMKeyTemplate() *tinkpb.KeyTemplate {
-	empty := []byte{}
-	return createECIESAEADHKDFKeyTemplate(commonpb.EllipticCurveType_NIST_P256, commonpb.HashType_SHA256, commonpb.EcPointFormat_UNCOMPRESSED, aead.AES128GCMKeyTemplate(), empty)
+	salt := []byte{}
+	return createECIESAEADHKDFKeyTemplate(commonpb.EllipticCurveType_NIST_P256, commonpb.HashType_SHA256, commonpb.EcPointFormat_UNCOMPRESSED, aead.AES128GCMKeyTemplate(), salt)
 }
 
-// ECIESHKDFAES128CTRHMACSHA256KeyTemplate is a KeyTemplate that generates an ECDH P-256 and
-// decapsulation key AES128-CTR-HMAC-SHA256 with the following parameters:
-//  - KEM: ECDH over NIST P-256
-//  - DEM: AES128-CTR-HMAC-SHA256 with the following parameters
-//      - AES key size: 16 bytes
-//      - AES CTR IV size: 16 bytes
-//      - HMAC key size: 32 bytes
-//      - HMAC tag size: 16 bytes
-//  - KDF: HKDF-HMAC-SHA256 with an empty salt
+// ECIESHKDFAES128CTRHMACSHA256KeyTemplate creates an ECIES-AEAD-HKDF key
+// template with:
+//   - KEM: ECDH over NIST P-256
+//   - DEM: AES128-CTR-HMAC-SHA256
+//   - KDF: HKDF-HMAC-SHA256 with an empty salt
+//
+// The DEM parameters are:
+//   - AES key size: 16 bytes
+//   - AES CTR IV size: 16 bytes
+//   - HMAC key size: 32 bytes
+//   - HMAC tag size: 16 bytes
 func ECIESHKDFAES128CTRHMACSHA256KeyTemplate() *tinkpb.KeyTemplate {
-	empty := []byte{}
-	return createECIESAEADHKDFKeyTemplate(commonpb.EllipticCurveType_NIST_P256, commonpb.HashType_SHA256, commonpb.EcPointFormat_UNCOMPRESSED, aead.AES128CTRHMACSHA256KeyTemplate(), empty)
+	salt := []byte{}
+	return createECIESAEADHKDFKeyTemplate(commonpb.EllipticCurveType_NIST_P256, commonpb.HashType_SHA256, commonpb.EcPointFormat_UNCOMPRESSED, aead.AES128CTRHMACSHA256KeyTemplate(), salt)
 }
 
-// createEciesAEADHKDFKeyTemplate creates a new ECIES-AEAD-HKDF key template with the given key size
-// in bytes.
+// createEciesAEADHKDFKeyTemplate creates a new ECIES-AEAD-HKDF key template
+// with the given parameters.
 func createECIESAEADHKDFKeyTemplate(c commonpb.EllipticCurveType, ht commonpb.HashType, ptfmt commonpb.EcPointFormat, dekT *tinkpb.KeyTemplate, salt []byte) *tinkpb.KeyTemplate {
 	format := &eciespb.EciesAeadHkdfKeyFormat{
 		Params: &eciespb.EciesAeadHkdfParams{
@@ -175,7 +189,10 @@
 			EcPointFormat: ptfmt,
 		},
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          eciesAEADHKDFPrivateKeyTypeURL,
 		Value:            serializedFormat,
diff --git a/go/hybrid/hybrid_test.go b/go/hybrid/hybrid_test.go
index 0239d48..4b91b27 100644
--- a/go/hybrid/hybrid_test.go
+++ b/go/hybrid/hybrid_test.go
@@ -16,55 +16,104 @@
 
 package hybrid_test
 
+// [START hybrid-example]
+
 import (
-	"encoding/base64"
+	"bytes"
 	"fmt"
 	"log"
 
 	"github.com/google/tink/go/hybrid"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
 )
 
 func Example() {
-	khPriv, err := keyset.NewHandle(hybrid.ECIESHKDFAES128CTRHMACSHA256KeyTemplate())
+	// A private keyset created with
+	// "tinkey create-keyset --key-template=DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM --out private_keyset.cfg".
+	// Note that this keyset has the secret key information in cleartext.
+	privateJSONKeyset := `{
+		"key": [{
+				"keyData": {
+						"keyMaterialType":
+								"ASYMMETRIC_PRIVATE",
+						"typeUrl":
+								"type.googleapis.com/google.crypto.tink.HpkePrivateKey",
+						"value":
+								"EioSBggBEAEYAhogVWQpmQoz74jcAp5WOD36KiBQ71MVCpn2iWfOzWLtKV4aINfn8qlMbyijNJcCzrafjsgJ493ZZGN256KTfKw0WN+p"
+				},
+				"keyId": 958452012,
+				"outputPrefixType": "TINK",
+				"status": "ENABLED"
+		}],
+		"primaryKeyId": 958452012
+  }`
+
+	// The corresponding public keyset created with
+	// "tinkey create-public-keyset --in private_keyset.cfg".
+	publicJSONKeyset := `{
+		"key": [{
+				"keyData": {
+						"keyMaterialType":
+								"ASYMMETRIC_PUBLIC",
+						"typeUrl":
+								"type.googleapis.com/google.crypto.tink.HpkePublicKey",
+						"value":
+								"EgYIARABGAIaIFVkKZkKM++I3AKeVjg9+iogUO9TFQqZ9olnzs1i7Sle"
+				},
+				"keyId": 958452012,
+				"outputPrefixType": "TINK",
+				"status": "ENABLED"
+		}],
+		"primaryKeyId": 958452012
+  }`
+
+	// Create a keyset handle from the keyset containing the public key. Because the
+	// public keyset does not contain any secrets, we can use [keyset.ReadWithNoSecrets].
+	publicKeysetHandle, err := keyset.ReadWithNoSecrets(
+		keyset.NewJSONReader(bytes.NewBufferString(publicJSONKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// TODO: save the private keyset to a safe location. DO NOT hardcode it in source code.
+	// Retrieve the HybridEncrypt primitive from publicKeysetHandle.
+	encPrimitive, err := hybrid.NewHybridEncrypt(publicKeysetHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	plaintext := []byte("message")
+	encryptionContext := []byte("encryption context")
+	ciphertext, err := encPrimitive.Encrypt(plaintext, encryptionContext)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Create a keyset handle from the cleartext private keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the access of the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use a insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
 	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
 	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
-
-	khPub, err := khPriv.Public()
+	privateKeysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(privateJSONKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// TODO: share the public keyset with the sender.
-
-	enc, err := hybrid.NewHybridEncrypt(khPub)
+	// Retrieve the HybridDecrypt primitive from privateKeysetHandle.
+	decPrimitive, err := hybrid.NewHybridDecrypt(privateKeysetHandle)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	msg := []byte("this data needs to be encrypted")
-	encryptionContext := []byte("encryption context")
-	ct, err := enc.Encrypt(msg, encryptionContext)
+	decrypted, err := decPrimitive.Decrypt(ciphertext, encryptionContext)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	dec, err := hybrid.NewHybridDecrypt(khPriv)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	pt, err := dec.Decrypt(ct, encryptionContext)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	fmt.Printf("Ciphertext: %s\n", base64.StdEncoding.EncodeToString(ct))
-	fmt.Printf("Original  plaintext: %s\n", msg)
-	fmt.Printf("Decrypted Plaintext: %s\n", pt)
+	fmt.Println(string(decrypted))
+	// Output: message
 }
+// [END hybrid-example]
diff --git a/go/hybrid/internal/hpke/hpke.go b/go/hybrid/internal/hpke/hpke.go
index b4c3ad9..f646458 100644
--- a/go/hybrid/internal/hpke/hpke.go
+++ b/go/hybrid/internal/hpke/hpke.go
@@ -52,24 +52,26 @@
 // kemSuiteID generates the KEM suite ID from kemID according to
 // https://www.rfc-editor.org/rfc/rfc9180.html#section-4.1-5.
 func kemSuiteID(kemID uint16) []byte {
-	return appendBigEndianUint16([]byte("KEM"), kemID)
+	return binary.BigEndian.AppendUint16([]byte("KEM"), kemID)
 }
 
 // hpkeSuiteID generates the HPKE suite ID according to
 // https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1-8.
 func hpkeSuiteID(kemID, kdfID, aeadID uint16) []byte {
-	var res []byte
+	// Allocate memory for the return value with the exact amount of bytes needed.
+	res := make([]byte, 0, 4+2+2+2)
 	res = append(res, "HPKE"...)
-	res = appendBigEndianUint16(res, kemID)
-	res = appendBigEndianUint16(res, kdfID)
-	res = appendBigEndianUint16(res, aeadID)
+	res = binary.BigEndian.AppendUint16(res, kemID)
+	res = binary.BigEndian.AppendUint16(res, kdfID)
+	res = binary.BigEndian.AppendUint16(res, aeadID)
 	return res
 }
 
 // keyScheduleContext creates the key_schedule_context defined at
 // https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1-10.
 func keyScheduleContext(mode uint8, pskIDHash, infoHash []byte) []byte {
-	var res []byte
+	// Allocate memory for the return value with the exact amount of bytes needed.
+	res := make([]byte, 0, 1+len(pskIDHash)+len(infoHash))
 	res = append(res, mode)
 	res = append(res, pskIDHash...)
 	res = append(res, infoHash...)
@@ -79,7 +81,8 @@
 // labelIKM returns a labeled IKM according to LabeledExtract() defined at
 // https://www.rfc-editor.org/rfc/rfc9180.html#section-4.
 func labelIKM(label string, ikm, suiteID []byte) []byte {
-	var res []byte
+	// Allocate memory for the return value with the exact amount of bytes needed.
+	res := make([]byte, 0, len(hpkeV1)+len(suiteID)+len(label)+len(ikm))
 	res = append(res, hpkeV1...)
 	res = append(res, suiteID...)
 	res = append(res, label...)
@@ -95,18 +98,12 @@
 		return nil, fmt.Errorf("length %d must be a valid uint16 value", length)
 	}
 
-	var res []byte
-	res = appendBigEndianUint16(res, length16)
+	// Allocate memory for the return value with the exact amount of bytes needed.
+	res := make([]byte, 0, 2+len(hpkeV1)+len(suiteID)+len(label)+len(info))
+	res = binary.BigEndian.AppendUint16(res, length16)
 	res = append(res, hpkeV1...)
 	res = append(res, suiteID...)
 	res = append(res, label...)
 	res = append(res, info...)
 	return res, nil
 }
-
-// appendBigEndianUint16 appends a uint16 v to the end of a byte array out.
-func appendBigEndianUint16(out []byte, v uint16) []byte {
-	b := make([]byte, 2)
-	binary.BigEndian.PutUint16(b, v)
-	return append(out, b...)
-}
diff --git a/go/hybrid/internal/hpke/hpke_test.go b/go/hybrid/internal/hpke/hpke_test.go
index 304d859..f9c3701 100644
--- a/go/hybrid/internal/hpke/hpke_test.go
+++ b/go/hybrid/internal/hpke/hpke_test.go
@@ -417,3 +417,34 @@
 
 	return m
 }
+
+func TestHpkeSuiteIDMemoryAllocatedIsExact(t *testing.T) {
+	suiteID := hpkeSuiteID(1, 2, 3)
+	if len(suiteID) != cap(suiteID) {
+		t.Errorf("want len(suiteID) == cap(suiteID), got %d != %d", len(suiteID), cap(suiteID))
+	}
+}
+
+func TestKeyScheduleContextMemoryAllocatedIsExact(t *testing.T) {
+	context := keyScheduleContext(1, []byte{1, 2, 3}, []byte{1, 2, 3, 4, 5})
+	if len(context) != cap(context) {
+		t.Errorf("want len(context) == cap(context), got %d != %d", len(context), cap(context))
+	}
+}
+
+func TestLabelIKMMemoryAllocatedIsExact(t *testing.T) {
+	ikm := labelIKM("abcde", []byte{1, 2, 3}, []byte{1, 2, 3, 4, 5})
+	if len(ikm) != cap(ikm) {
+		t.Errorf("want len(ikm) == cap(ikm), got %d != %d", len(ikm), cap(ikm))
+	}
+}
+
+func TestLabelInfoMemoryAllocatedIsExact(t *testing.T) {
+	info, err := labelInfo("abcde", []byte{1, 2, 3}, []byte{1, 2, 3, 4, 5}, 42)
+	if err != nil {
+		t.Errorf("labelInfo() err = %v, want nil", err)
+	}
+	if len(info) != cap(info) {
+		t.Errorf("want len(info) == cap(info), got %d != %d", len(info), cap(info))
+	}
+}
diff --git a/go/hybrid/internal/hpke/x25519_kem.go b/go/hybrid/internal/hpke/x25519_kem.go
index 807ffbc..98d050d 100644
--- a/go/hybrid/internal/hpke/x25519_kem.go
+++ b/go/hybrid/internal/hpke/x25519_kem.go
@@ -87,8 +87,8 @@
 
 // deriveKEMSharedSecret returns a pseudorandom key obtained via HKDF SHA256.
 func (x *x25519KEM) deriveKEMSharedSecret(dh, senderPubKey, recipientPubKey []byte) ([]byte, error) {
-	ctx := make([]byte, len(senderPubKey))
-	copy(ctx, senderPubKey)
+	ctx := make([]byte, 0, len(senderPubKey)+len(recipientPubKey))
+	ctx = append(ctx, senderPubKey...)
 	ctx = append(ctx, recipientPubKey...)
 
 	suiteID := kemSuiteID(x25519HKDFSHA256)
@@ -101,7 +101,7 @@
 		return nil, err
 	}
 	return hkdfKDF.extractAndExpand(
-		/*salt=*/ nil,
+		nil, /*=salt*/
 		dh,
 		"eae_prk",
 		ctx,
diff --git a/go/hybrid/subtle/ecies_hkdf_recipient_kem.go b/go/hybrid/subtle/ecies_hkdf_recipient_kem.go
index 8295692..1214dde 100644
--- a/go/hybrid/subtle/ecies_hkdf_recipient_kem.go
+++ b/go/hybrid/subtle/ecies_hkdf_recipient_kem.go
@@ -34,7 +34,8 @@
 	if err != nil {
 		return nil, err
 	}
-	i := append(kem, secret...)
-
+	i := make([]byte, 0, len(kem)+len(secret))
+	i = append(i, kem...)
+	i = append(i, secret...)
 	return subtle.ComputeHKDF(hashAlg, i, salt, info, keySize)
 }
diff --git a/go/hybrid/subtle/ecies_hkdf_sender_kem.go b/go/hybrid/subtle/ecies_hkdf_sender_kem.go
index 0f6b2db..00c441d 100644
--- a/go/hybrid/subtle/ecies_hkdf_sender_kem.go
+++ b/go/hybrid/subtle/ecies_hkdf_sender_kem.go
@@ -24,12 +24,12 @@
 }
 
 // ECIESHKDFSenderKem represents HKDF-based ECIES-KEM (key encapsulation mechanism)
-//for ECIES sender.
+// for ECIES sender.
 type ECIESHKDFSenderKem struct {
 	recipientPublicKey *ECPublicKey
 }
 
-// GenerateKey a HDKF based KEM.
+// encapsulate generates an HKDF-based KEMKey.
 func (s *ECIESHKDFSenderKem) encapsulate(hashAlg string, salt []byte, info []byte, keySize uint32, pointFormat string) (*KEMKey, error) {
 
 	pvt, err := GenerateECDHKeyPair(s.recipientPublicKey.Curve)
@@ -46,7 +46,9 @@
 	if err != nil {
 		return nil, err
 	}
-	i := append(sdata, secret...)
+	i := make([]byte, 0, len(sdata)+len(secret))
+	i = append(i, sdata...)
+	i = append(i, secret...)
 
 	sKey, err := subtle.ComputeHKDF(hashAlg, i, salt, info, keySize)
 	if err != nil {
diff --git a/go/insecurecleartextkeyset/BUILD.bazel b/go/insecurecleartextkeyset/BUILD.bazel
index 5476c7e..b768581 100644
--- a/go/insecurecleartextkeyset/BUILD.bazel
+++ b/go/insecurecleartextkeyset/BUILD.bazel
@@ -18,13 +18,22 @@
 
 go_test(
     name = "insecurecleartextkeyset_test",
-    srcs = ["insecurecleartextkeyset_test.go"],
+    srcs = [
+        "example_test.go",
+        "insecurecleartextkeyset_test.go",
+    ],
     deps = [
         ":insecurecleartextkeyset",
+        "//aead",
+        "//hybrid",
+        "//internal/internalregistry",
         "//keyset",
+        "//mac",
         "//proto/tink_go_proto",
-        "//testutil",
+        "//testing/fakemonitoring",
+        "@com_github_google_go_cmp//cmp",
         "@org_golang_google_protobuf//proto",
+        "@org_golang_google_protobuf//testing/protocmp",
     ],
 )
 
diff --git a/go/insecurecleartextkeyset/example_test.go b/go/insecurecleartextkeyset/example_test.go
new file mode 100644
index 0000000..a27442d
--- /dev/null
+++ b/go/insecurecleartextkeyset/example_test.go
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package insecurecleartextkeyset_test
+
+// [START cleartext-keyset-example]
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/keyset"
+)
+
+func Example_cleartextKeysetInBinary() {
+	// Generate a new keyset handle for the primitive we want to use.
+	handle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Serialize the keyset.
+	buff := &bytes.Buffer{}
+	err = insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff))
+	if err != nil {
+		log.Fatal(err)
+	}
+	serializedKeyset := buff.Bytes()
+
+	// serializedKeyset can now be stored at a secure location.
+	// WARNING: Storing the keyset in cleartext to disk is not recommended!
+
+	// Parse the keyset.
+	parsedHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Get the primitive.
+	primitive, err := aead.New(parsedHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Use the primitive.
+	plaintext := []byte("message")
+	associatedData := []byte("example encryption")
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	decrypted, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(string(decrypted))
+	// Output: message
+}
+
+// [END cleartext-keyset-example]
+
+func Example_cleartextKeysetInJSON() {
+	// Generate a new keyset handle for the primitive we want to use.
+	handle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Serialize the keyset.
+	buff := &bytes.Buffer{}
+	err = insecurecleartextkeyset.Write(handle, keyset.NewJSONWriter(buff))
+	if err != nil {
+		log.Fatal(err)
+	}
+	serializedKeyset := buff.Bytes()
+
+	// serializedKeyset can now be stored at a secure location.
+	// WARNING: Storing the keyset in cleartext to disk is not recommended!
+
+	// Parse the keyset.
+	parsedHandle, err := insecurecleartextkeyset.Read(keyset.NewJSONReader(bytes.NewBuffer(serializedKeyset)))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Get the primitive.
+	primitive, err := aead.New(parsedHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Use the primitive.
+	plaintext := []byte("message")
+	associatedData := []byte("example encryption")
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	decrypted, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(string(decrypted))
+	// Output: message
+}
diff --git a/go/insecurecleartextkeyset/insecurecleartextkeyset.go b/go/insecurecleartextkeyset/insecurecleartextkeyset.go
index a0bd250..aba58e7 100644
--- a/go/insecurecleartextkeyset/insecurecleartextkeyset.go
+++ b/go/insecurecleartextkeyset/insecurecleartextkeyset.go
@@ -30,10 +30,8 @@
 )
 
 var (
-	// KeysetHandle creates a keyset.Handle from cleartext key material.
-	KeysetHandle = internal.KeysetHandle.(func(*tinkpb.Keyset) *keyset.Handle)
-	// KeysetMaterial returns the key material contained in a keyset.Handle.
-	KeysetMaterial = internal.KeysetMaterial.(func(*keyset.Handle) *tinkpb.Keyset)
+	keysetHandle   = internal.KeysetHandle.(func(*tinkpb.Keyset, ...keyset.Option) (*keyset.Handle, error))
+	keysetMaterial = internal.KeysetMaterial.(func(*keyset.Handle) *tinkpb.Keyset)
 
 	errInvalidKeyset = errors.New("insecurecleartextkeyset: invalid keyset")
 	errInvalidHandle = errors.New("insecurecleartextkeyset: invalid handle")
@@ -42,7 +40,7 @@
 )
 
 // Read creates a keyset.Handle from a cleartext keyset obtained via r.
-func Read(r keyset.Reader) (*keyset.Handle, error) {
+func Read(r keyset.Reader, opts ...keyset.Option) (*keyset.Handle, error) {
 	if r == nil {
 		return nil, errInvalidReader
 	}
@@ -50,19 +48,49 @@
 	if err != nil || ks == nil || len(ks.Key) == 0 {
 		return nil, errInvalidKeyset
 	}
-	return KeysetHandle(ks), nil
+	return keysetHandle(ks, opts...)
 }
 
-// Write exports the keyset from h to the given writer w without encrypting it.
+// Write exports the keyset from handle to the given writer w without encrypting it.
 //
 // Storing secret key material in an unencrypted fashion is dangerous. If
-// feasible, you should use func keyset.Handle.Write() instead.
-func Write(h *keyset.Handle, w keyset.Writer) error {
-	if h == nil {
+// feasible, you should use [keyset.Handle.Write] instead.
+func Write(handle *keyset.Handle, w keyset.Writer) error {
+	if handle == nil {
 		return errInvalidHandle
 	}
 	if w == nil {
 		return errInvalidWriter
 	}
-	return w.Write(KeysetMaterial(h))
+	return w.Write(KeysetMaterial(handle))
+}
+
+// KeysetMaterial returns the key material contained in a keyset.Handle.
+func KeysetMaterial(handle *keyset.Handle) *tinkpb.Keyset {
+	return keysetMaterial(handle)
+}
+
+// KeysetHandle creates a keyset.Handle from cleartext key material.
+//
+// Callers should verify that the returned *keyset.Handle isn't nil.
+//
+// Deprecated: Use [Read] instead with a serialized keyset.
+//
+//	sks, err := proto.Marshal(ks)
+//	if err != nil {
+//		return err
+//	}
+//	h, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(bytes.NewBuffer(sks)))
+//	if err != nil {
+//		return err
+//	}
+func KeysetHandle(ks *tinkpb.Keyset) *keyset.Handle {
+	kh, err := keysetHandle(ks)
+	if err != nil {
+		// this *keyset.Handle can only return errors when *keyset.Option arguments
+		// are provided. To maintain backwards compatibility and avoid panic, it returns
+		// a nil value if an error happens.
+		return nil
+	}
+	return kh
 }
diff --git a/go/insecurecleartextkeyset/insecurecleartextkeyset_test.go b/go/insecurecleartextkeyset/insecurecleartextkeyset_test.go
index e1f0458..2ad7ffa 100644
--- a/go/insecurecleartextkeyset/insecurecleartextkeyset_test.go
+++ b/go/insecurecleartextkeyset/insecurecleartextkeyset_test.go
@@ -17,59 +17,214 @@
 package insecurecleartextkeyset_test
 
 import (
+	"bytes"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/hybrid"
 	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testutil"
+	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/testing/fakemonitoring"
 
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
-func TestInvalidInput(t *testing.T) {
-	if _, err := insecurecleartextkeyset.Read(nil); err == nil {
-		t.Error("insecurecleartextkeyset.Read should not accept nil as keyset")
-	}
-	if err := insecurecleartextkeyset.Write(nil, &keyset.MemReaderWriter{}); err == nil {
-		t.Error("insecurecleartextkeyset.Write should not accept nil as keyset")
-	}
-	if err := insecurecleartextkeyset.Write(&keyset.Handle{}, nil); err == nil {
-		t.Error("insecurecleartextkeyset.Write should not accept nil as writer")
+// A KeysetReader that always returns nil.
+type NilKeysetReader struct {
+}
+
+func (m *NilKeysetReader) Read() (*tinkpb.Keyset, error) {
+	return nil, nil
+}
+
+func (m *NilKeysetReader) ReadEncrypted() (*tinkpb.EncryptedKeyset, error) {
+	return nil, nil
+}
+
+func TestReadWithNilKeysetFails(t *testing.T) {
+	if _, err := insecurecleartextkeyset.Read(&NilKeysetReader{}); err == nil {
+		t.Error("insecurecleartextkeyset.Read(&NilKeysetReader{}) err = nil, want error")
 	}
 }
 
-func TestHandleFromReader(t *testing.T) {
-	// Create a keyset that contains a single HmacKey.
-	manager := testutil.NewHMACKeysetManager()
-	handle, err := manager.Handle()
-	if handle == nil || err != nil {
-		t.Fatalf("cannot get keyset handle: %v", err)
+func TestReadWithNilReaderFails(t *testing.T) {
+	if _, err := insecurecleartextkeyset.Read(nil); err == nil {
+		t.Error("insecurecleartextkeyset.Read(nil) err = nil, want error")
+	}
+}
+
+func TestWriteWithNilHandleFails(t *testing.T) {
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(nil, keyset.NewBinaryWriter(buff)); err == nil {
+		t.Error("insecurecleartextkeyset.Write(nil, _) err = nil, want error")
+	}
+}
+
+func TestWriteWithNilWriterFails(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(aead.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
+	}
+	if err := insecurecleartextkeyset.Write(handle, nil); err == nil {
+		t.Error("insecurecleartextkeyset.Write(_, nil) err = nil, want error")
+	}
+}
+
+func TestWriteAndReadInBinary(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
+	}
+
+	buff := &bytes.Buffer{}
+	err = insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	serialized := buff.Bytes()
+
+	parsedHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(bytes.NewBuffer(serialized)))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+
+	want := insecurecleartextkeyset.KeysetMaterial(handle)
+	got := insecurecleartextkeyset.KeysetMaterial(parsedHandle)
+	if !proto.Equal(got, want) {
+		t.Errorf("KeysetMaterial(Read()) = %q, want %q", got, want)
+	}
+}
+
+func TestWriteAndReadInJson(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
+	}
+
+	buff := &bytes.Buffer{}
+	err = insecurecleartextkeyset.Write(handle, keyset.NewJSONWriter(buff))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	serialized := buff.Bytes()
+
+	parsedHandle, err := insecurecleartextkeyset.Read(keyset.NewJSONReader(bytes.NewBuffer(serialized)))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+
+	want := insecurecleartextkeyset.KeysetMaterial(handle)
+	got := insecurecleartextkeyset.KeysetMaterial(parsedHandle)
+	if !proto.Equal(got, want) {
+		t.Errorf("KeysetMaterial(Read()) = %q, want %q", got, want)
+	}
+}
+
+func TestLegacyKeysetHandle(t *testing.T) {
+	handle, err := keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template())
+	if err != nil {
+		t.Fatalf(" keyset.NewHandle(hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template()) err = %v, want nil", err)
 	}
 	ks := insecurecleartextkeyset.KeysetMaterial(handle)
-	parsedHandle, err := insecurecleartextkeyset.Read(&keyset.MemReaderWriter{Keyset: ks})
-	if err != nil {
-		t.Fatalf("unexpected error reading keyset: %v", err)
+	gotHandle1 := insecurecleartextkeyset.KeysetHandle(ks)
+	if !cmp.Equal(gotHandle1.KeysetInfo(), handle.KeysetInfo(), protocmp.Transform()) {
+		t.Errorf("gotHandle1.KeysetInfo() = %v, want %v", gotHandle1.KeysetInfo(), handle.KeysetInfo())
 	}
-	parsedKs := insecurecleartextkeyset.KeysetMaterial(parsedHandle)
-	if !proto.Equal(ks, parsedKs) {
-		t.Errorf("parsed keyset (%s) doesn't match original keyset (%s)", parsedKs, ks)
+	serializedKeyset, err := proto.Marshal(ks)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	gotHandle2, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	if !cmp.Equal(gotHandle2.KeysetInfo(), handle.KeysetInfo(), protocmp.Transform()) {
+		t.Errorf("gotHandle2.KeysetInfo() = %v, want %v", gotHandle2.KeysetInfo(), handle.KeysetInfo())
 	}
 }
 
-func TestWrite(t *testing.T) {
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_SYMMETRIC)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	ks := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	h, err := insecurecleartextkeyset.Read(&keyset.MemReaderWriter{Keyset: ks})
+func TestHandleFromReaderWithAnnotationsGetsMonitored(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := &fakemonitoring.Client{}
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
 	if err != nil {
-		t.Fatalf("unexpected error creating new KeysetHandle: %v", err)
+		t.Fatalf(" keyset.NewHandle(aead.AES256GCMKeyTemplate()) err = %v, want nil", err)
 	}
-	exported := &keyset.MemReaderWriter{}
-	if err := insecurecleartextkeyset.Write(h, exported); err != nil {
-		t.Fatalf("unexpected error writing keyset: %v", err)
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
 	}
-	if !proto.Equal(exported.Keyset, ks) {
-		t.Errorf("exported keyset (%s) doesn't match original keyset (%s)", exported.Keyset, ks)
+	wantAnnotations := map[string]string{"foo": "bar"}
+	annotatedHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(wantAnnotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(annotatedHandle)
+	if err != nil {
+		t.Fatalf("aead.New() err = %v, want nil", err)
+	}
+	if _, err := p.Encrypt([]byte("some_data"), nil); err != nil {
+		t.Fatalf("Encrypt() err = %v, want nil", err)
+	}
+	events := client.Events()
+	gotAnnotations := events[0].Context.KeysetInfo.Annotations
+	if !cmp.Equal(gotAnnotations, wantAnnotations) {
+		t.Errorf("Annotations = %v, want %v", gotAnnotations, wantAnnotations)
+	}
+}
+
+func TestHandleFromReaderWithAnnotationsTwiceFails(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf(" keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	if _, err := insecurecleartextkeyset.Read(
+		keyset.NewBinaryReader(buff),
+		keyset.WithAnnotations(annotations),
+		keyset.WithAnnotations(annotations)); err == nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = nil, want error")
+	}
+}
+
+func TestHandleFromReaderWithoutAnnotationsDoesNotGetMonitored(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := &fakemonitoring.Client{}
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf(" keyset.NewHandle(aead.AES256GCMKeyTemplate()) err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	unannotatedHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := aead.New(unannotatedHandle)
+	if err != nil {
+		t.Fatalf("aead.New() err = %v, want nil", err)
+	}
+	if _, err := p.Encrypt([]byte("some_data"), nil); err != nil {
+		t.Fatalf("Encrypt() err = %v, want nil", err)
+	}
+	if len(client.Events()) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(client.Events()))
 	}
 }
diff --git a/go/integration/awskms/BUILD.bazel b/go/integration/awskms/BUILD.bazel
index 1505cae..479dd77 100644
--- a/go/integration/awskms/BUILD.bazel
+++ b/go/integration/awskms/BUILD.bazel
@@ -26,25 +26,28 @@
 go_test(
     name = "awskms_test",
     srcs = [
-        "aws_kms_aead_test.go",
         "aws_kms_client_test.go",
+        "aws_kms_integration_test.go",
     ],
     data = [
         "//testdata/aws:bad_credentials",
         "//testdata/aws:credentials",
         "@google_root_pem//file",  #keep
     ],
+    embed = [":awskms"],
     tags = [
         "manual",
         "no_rbe",
     ],
     deps = [
-        ":awskms",
         "//aead",
         "//core/registry",
+        "//integration/awskms/internal/fakeawskms",
         "//keyset",
         "//subtle/random",
         "//tink",
+        "@com_github_aws_aws_sdk_go//aws",
+        "@com_github_aws_aws_sdk_go//service/kms",
     ],
 )
 
diff --git a/go/integration/awskms/aws_kms_aead.go b/go/integration/awskms/aws_kms_aead.go
index e89dc28..5dcb5e4 100644
--- a/go/integration/awskms/aws_kms_aead.go
+++ b/go/integration/awskms/aws_kms_aead.go
@@ -14,91 +14,71 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-// Package awskms provides integration with the AWS Cloud KMS.
+// Package awskms provides integration with the AWS Key Management Service.
 package awskms
 
 import (
 	"encoding/hex"
-	"errors"
-	"strings"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/kms"
 	"github.com/aws/aws-sdk-go/service/kms/kmsiface"
 )
 
-// AWSAEAD represents a AWS KMS service to a particular URI.
+// AWSAEAD is an implementation of the AEAD interface which performs
+// cryptographic operations remotely via the AWS KMS service using a specific
+// key URI.
 type AWSAEAD struct {
-	keyURI string
-	kms    kmsiface.KMSAPI
+	keyURI                string
+	kms                   kmsiface.KMSAPI
+	encryptionContextName EncryptionContextName
 }
 
-// newAWSAEAD returns a new AWS KMS service.
-// keyURI must have the following format: 'arn:<partition>:kms:<region>:[:path]'.
+// newAWSAEAD returns a new AWSAEAD instance.
+//
+// keyURI must have the following format:
+//
+//	aws-kms://arn:<partition>:kms:<region>:[<path>]
+//
 // See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html.
-func newAWSAEAD(keyURI string, kms kmsiface.KMSAPI) *AWSAEAD {
+func newAWSAEAD(keyURI string, kms kmsiface.KMSAPI, name EncryptionContextName) *AWSAEAD {
 	return &AWSAEAD{
-		keyURI: keyURI,
-		kms:    kms,
+		keyURI:                keyURI,
+		kms:                   kms,
+		encryptionContextName: name,
 	}
 }
 
 // Encrypt encrypts the plaintext with associatedData.
 func (a *AWSAEAD) Encrypt(plaintext, associatedData []byte) ([]byte, error) {
-	ad := hex.EncodeToString(associatedData)
 	req := &kms.EncryptInput{
-		KeyId:             aws.String(a.keyURI),
-		Plaintext:         plaintext,
-		EncryptionContext: map[string]*string{"additionalData": &ad},
+		KeyId:     aws.String(a.keyURI),
+		Plaintext: plaintext,
 	}
-	if ad == "" {
-		req = &kms.EncryptInput{
-			KeyId:     aws.String(a.keyURI),
-			Plaintext: plaintext,
-		}
+	if len(associatedData) > 0 {
+		ad := hex.EncodeToString(associatedData)
+		req.EncryptionContext = map[string]*string{a.encryptionContextName.String(): &ad}
 	}
 	resp, err := a.kms.Encrypt(req)
 	if err != nil {
 		return nil, err
 	}
-
 	return resp.CiphertextBlob, nil
 }
 
-// Decrypt AEAD decrypts the data and verified the associated data.
-//
-// Returns an error if the KeyId field in the response does not match the KeyURI
-// provided when creating the client. If we don't do this, the possibility exists
-// for the ciphertext to be replaced by one under a key we don't control/expect,
-// but do have decrypt permissions on.
-//
-// This check is disabled if AWSAEAD.keyURI is not in key ARN format.
-//
-// See https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id.
+// Decrypt decrypts the ciphertext and verifies the associated data.
 func (a *AWSAEAD) Decrypt(ciphertext, associatedData []byte) ([]byte, error) {
-	ad := hex.EncodeToString(associatedData)
 	req := &kms.DecryptInput{
-		KeyId:             aws.String(a.keyURI),
-		CiphertextBlob:    ciphertext,
-		EncryptionContext: map[string]*string{"additionalData": &ad},
+		KeyId:          aws.String(a.keyURI),
+		CiphertextBlob: ciphertext,
 	}
-	if ad == "" {
-		req = &kms.DecryptInput{
-			CiphertextBlob: ciphertext,
-		}
+	if len(associatedData) > 0 {
+		ad := hex.EncodeToString(associatedData)
+		req.EncryptionContext = map[string]*string{a.encryptionContextName.String(): &ad}
 	}
 	resp, err := a.kms.Decrypt(req)
 	if err != nil {
 		return nil, err
 	}
-	if isKeyArnFormat(a.keyURI) && strings.Compare(*resp.KeyId, a.keyURI) != 0 {
-		return nil, errors.New("decryption failed: wrong key id")
-	}
 	return resp.Plaintext, nil
 }
-
-// isKeyArnFormat returns true if the keyURI is the KMS Key ARN format; false otherwise.
-func isKeyArnFormat(keyURI string) bool {
-	tokens := strings.Split(keyURI, ":")
-	return len(tokens) == 6 && strings.HasPrefix(tokens[5], "key/")
-}
diff --git a/go/integration/awskms/aws_kms_aead_test.go b/go/integration/awskms/aws_kms_aead_test.go
deleted file mode 100644
index 32fedd6..0000000
--- a/go/integration/awskms/aws_kms_aead_test.go
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package awskms_test
-
-import (
-	"bytes"
-	"errors"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"flag"
-	// context is used to cancel outstanding requests
-	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/subtle/random"
-	"github.com/google/tink/go/tink"
-
-	"github.com/google/tink/go/integration/awskms"
-)
-
-const (
-	keyAliasURI = "aws-kms://arn:aws:kms:us-east-2:235739564943:alias/unit-and-integration-testing"
-	keyURI      = "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-	profile     = "tink-user1"
-)
-
-var (
-	credFile    = "tink_go/testdata/aws/credentials.csv"
-	credINIFile = "tink_go/testdata/aws/credentials.ini"
-)
-
-func init() {
-	certPath := filepath.Join(os.Getenv("TEST_SRCDIR"), "tink_base/roots.pem")
-	flag.Set("cacerts", certPath)
-	os.Setenv("SSL_CERT_FILE", certPath)
-}
-
-func setupKMS(t *testing.T, cf string) {
-	t.Helper()
-	setupKMSWithURI(t, cf, keyURI)
-}
-
-func setupKMSWithURI(t *testing.T, cf string, uri string) {
-	t.Helper()
-	g, err := awskms.NewClientWithCredentials(uri, cf)
-	if err != nil {
-		t.Fatalf("error setting up aws client: %v", err)
-	}
-	// The registry will return the first KMS client that claims support for
-	// the keyURI.  The tests re-use the same keyURI, so clear any clients
-	// registered by earlier tests before registering the new client.
-	registry.ClearKMSClients()
-	registry.RegisterKMSClient(g)
-}
-
-func basicAEADTest(t *testing.T, a tink.AEAD) error {
-	t.Helper()
-	return basicAEADTestWithOptions(t, a, 100 /*loopCount*/, true /*withAdditionalData*/)
-}
-
-func basicAEADTestWithOptions(t *testing.T, a tink.AEAD, loopCount int, withAdditionalData bool) error {
-	t.Helper()
-	for i := 0; i < loopCount; i++ {
-		pt := random.GetRandomBytes(20)
-		var ad []byte = nil
-		if withAdditionalData {
-			ad = random.GetRandomBytes(20)
-		}
-		ct, err := a.Encrypt(pt, ad)
-		if err != nil {
-			return err
-		}
-		dt, err := a.Decrypt(ct, ad)
-		if err != nil {
-			return err
-		}
-		if !bytes.Equal(dt, pt) {
-			return errors.New("decrypt not inverse of encrypt")
-		}
-	}
-	return nil
-}
-
-func TestBasicAead(t *testing.T) {
-	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
-	if !ok {
-		t.Skip("TEST_SRCDIR not set")
-	}
-
-	for _, file := range []string{credFile, credINIFile} {
-		setupKMS(t, filepath.Join(srcDir, file))
-		dek := aead.AES128CTRHMACSHA256KeyTemplate()
-		kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(keyURI, dek))
-		if err != nil {
-			t.Fatalf("error getting a new keyset handle: %v", err)
-		}
-		a, err := aead.New(kh)
-		if err != nil {
-			t.Fatalf("error getting the primitive: %v", err)
-		}
-		if err := basicAEADTest(t, a); err != nil {
-			t.Errorf("error in basic aead tests: %v", err)
-		}
-	}
-}
-
-func TestBasicAeadWithoutAdditionalData(t *testing.T) {
-	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
-	if !ok {
-		t.Skip("TEST_SRCDIR not set")
-	}
-
-	for _, uri := range []string{keyURI, keyAliasURI} {
-		for _, file := range []string{credFile, credINIFile} {
-			setupKMSWithURI(t, filepath.Join(srcDir, file), uri)
-			dek := aead.AES128CTRHMACSHA256KeyTemplate()
-			kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(uri, dek))
-			if err != nil {
-				t.Fatalf("error getting a new keyset handle: %v", err)
-			}
-			a, err := aead.New(kh)
-			if err != nil {
-				t.Fatalf("error getting the primitive: %v", err)
-			}
-			// Only test 10 times (instead of 100) because each test makes HTTP requests to AWS.
-			if err := basicAEADTestWithOptions(t, a, 10 /*loopCount*/, false /*withAdditionalData*/); err != nil {
-				t.Errorf("error in basic aead tests without additinal data: %v", err)
-			}
-		}
-	}
-}
diff --git a/go/integration/awskms/aws_kms_client.go b/go/integration/awskms/aws_kms_client.go
index ea73073..a85b0a3 100644
--- a/go/integration/awskms/aws_kms_client.go
+++ b/go/integration/awskms/aws_kms_client.go
@@ -22,6 +22,7 @@
 	"fmt"
 	"os"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -40,37 +41,260 @@
 var (
 	errCred    = errors.New("invalid credential path")
 	errBadFile = errors.New("cannot open credential path")
-	errCredCSV = errors.New("malformed credential csv file")
+	errCredCSV = errors.New("malformed credential CSV file")
 )
 
-// awsClient represents a client that connects to the AWS KMS backend.
+// awsClient is a wrapper around an AWS SDK provided KMS client that can
+// instantiate Tink primitives.
 type awsClient struct {
-	keyURIPrefix string
-	kms          kmsiface.KMSAPI
+	keyURIPrefix          string
+	kms                   kmsiface.KMSAPI
+	encryptionContextName EncryptionContextName
 }
 
-// NewClient returns a new AWS KMS client which will use default
-// credentials to handle keys with uriPrefix prefix.
-// uriPrefix must have the following format: 'aws-kms://arn:<partition>:kms:<region>:[:path]'.
-// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html.
+// ClientOption is an interface for defining options that are passed to
+// [NewClientWithOptions].
+type ClientOption interface{ set(*awsClient) error }
+
+type option func(*awsClient) error
+
+func (o option) set(a *awsClient) error { return o(a) }
+
+// WithCredentialPath instantiates the underlying AWS KMS client using the
+// credentials located at credentialPath.
+//
+// credentialPath can specify a file in CSV format as provided in the IAM
+// console or an INI-style credentials file.
+//
+// See https://docs.aws.amazon.com/cli/latest/userguide/cli-authentication-user.html#cli-authentication-user-configure-csv
+// and https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format.
+func WithCredentialPath(credentialPath string) ClientOption {
+	return option(func(a *awsClient) error {
+		if a.kms != nil {
+			return errors.New("WithCredentialPath option cannot be used, KMS client already set")
+		}
+
+		k, err := getKMSFromCredentialPath(a.keyURIPrefix, credentialPath)
+		if err != nil {
+			return err
+		}
+
+		a.kms = k
+		return nil
+	})
+}
+
+// WithKMS sets the underlying AWS KMS client to kms, a preexisting AWS KMS
+// client instance.
+//
+// It's the callers responsibility to ensure that the configured region of kms
+// aligns with the region in key URIs passed to this client. Otherwise, API
+// requests will fail.
+func WithKMS(kms kmsiface.KMSAPI) ClientOption {
+	return option(func(a *awsClient) error {
+		if a.kms != nil {
+			return errors.New("WithKMS option cannot be used, KMS client already set")
+		}
+		a.kms = kms
+		return nil
+	})
+}
+
+// EncryptionContextName specifies the name used in the EncryptionContext field
+// of EncryptInput and DecryptInput requests. See [WithEncryptionContextName]
+// for further details.
+type EncryptionContextName uint
+
+const (
+	// AssociatedData will set the EncryptionContext name to "associatedData".
+	AssociatedData EncryptionContextName = 1 + iota
+	// LegacyAdditionalData will set the EncryptionContext name to "additionalData".
+	LegacyAdditionalData
+)
+
+var encryptionContextNames = map[EncryptionContextName]string{
+	AssociatedData: "associatedData",
+	LegacyAdditionalData: "additionalData",
+}
+
+func (n EncryptionContextName) valid() bool {
+	_, ok := encryptionContextNames[n]
+	return ok
+}
+
+func (n EncryptionContextName) String() string {
+	if !n.valid() {
+		return "unrecognized value " + strconv.Itoa(int(n))
+	}
+	return encryptionContextNames[n]
+}
+
+// WithEncryptionContextName sets the name which maps to the base64 encoded
+// associated data within the EncryptionContext field of EncrypInput and
+// DecryptInput requests.
+//
+// The default is [AssociatedData], which is compatible with the Tink AWS KMS
+// extensions in other languages. In older versions of this packge, before this
+// option was present, "additionalData" was hardcoded.
+//
+// This option is provided to facilitate compatibility with older ciphertexts.
+func WithEncryptionContextName(name EncryptionContextName) ClientOption {
+	return option(func(a *awsClient) error {
+		if !name.valid() {
+			return fmt.Errorf("invalid EncryptionContextName: %v", name)
+		}
+		if a.encryptionContextName != 0 {
+			return errors.New("encryptionContextName already set")
+		}
+		a.encryptionContextName = name
+		return nil
+	})
+}
+
+// NewClientWithOptions returns a [registry.KMSClient] which wraps an AWS KMS
+// client and will handle keys whose URIs start with uriPrefix.
+//
+// By default, the client will use default credentials.
+//
+// AEAD primitives produced by this client will use [AssociatedData] when
+// serializing associated data.
+func NewClientWithOptions(uriPrefix string, opts ...ClientOption) (registry.KMSClient, error) {
+	if !strings.HasPrefix(strings.ToLower(uriPrefix), awsPrefix) {
+		return nil, fmt.Errorf("uriPrefix must start with %q, but got %q", awsPrefix, uriPrefix)
+	}
+
+	a := &awsClient{
+		keyURIPrefix: uriPrefix,
+	}
+
+	// Process options, if any.
+	for _, opt := range opts {
+		if err := opt.set(a); err != nil {
+			return nil, fmt.Errorf("failed setting option: %v", err)
+		}
+	}
+
+	// Populate values not defined via options.
+	if a.kms == nil {
+		k, err := getKMS(uriPrefix)
+		if err != nil {
+			return nil, err
+		}
+		a.kms = k
+	}
+	if a.encryptionContextName == 0 {
+		a.encryptionContextName = AssociatedData
+	}
+
+	return a, nil
+}
+
+// NewClient returns a KMSClient backed by AWS KMS using default credentials to
+// handle keys whose URIs start with uriPrefix.
+//
+// uriPrefix must have the following format:
+//
+//	aws-kms://arn:<partition>:kms:<region>:[<path>]
+//
+// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
+//
+// AEAD primitives produced by this client will use [LegacyAdditionalData] when
+// serializing associated data.
+//
+// Deprecated: Instead, use [NewClientWithOptions].
+//
+//	awskms.NewClientWithOptions(uriPrefix)
 func NewClient(uriPrefix string) (registry.KMSClient, error) {
+	return NewClientWithOptions(uriPrefix, WithEncryptionContextName(LegacyAdditionalData))
+}
+
+// NewClientWithCredentials returns a KMSClient backed by AWS KMS using the given
+// credentials to handle keys whose URIs start with uriPrefix.
+//
+// uriPrefix must have the following format:
+//
+//	aws-kms://arn:<partition>:kms:<region>:[<path>]
+//
+// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
+//
+// credentialPath can specify a file in CSV format as provided in the IAM
+// console or an INI-style credentials file.
+//
+// See https://docs.aws.amazon.com/cli/latest/userguide/cli-authentication-user.html#cli-authentication-user-configure-csv
+// and https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format.
+//
+// AEAD primitives produced by this client will use [LegacyAdditionalData] when
+// serializing associated data.
+//
+// Deprecated: Instead use [NewClientWithOptions] and [WithCredentialPath].
+//
+//	awskms.NewClientWithOptions(uriPrefix, awskms.WithCredentialPath(credentialPath))
+func NewClientWithCredentials(uriPrefix string, credentialPath string) (registry.KMSClient, error) {
+	return NewClientWithOptions(uriPrefix, WithCredentialPath(credentialPath), WithEncryptionContextName(LegacyAdditionalData))
+}
+
+// NewClientWithKMS returns a KMSClient backed by AWS KMS using the provided
+// instance of the AWS SDK KMS client.
+//
+// The caller is responsible for ensuring that the region specified in the KMS
+// client is consitent with the region specified within uriPrefix.
+//
+// uriPrefix must have the following format:
+//
+//	aws-kms://arn:<partition>:kms:<region>:[<path>]
+//
+// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
+//
+// AEAD primitives produced by this client will use [LegacyAdditionalData] when
+// serializing associated data.
+//
+// Deprecated: Instead use [NewClientWithOptions] and [WithKMS].
+//
+//	awskms.NewClientWithOptions(uriPrefix, awskms.WithKMS(kms))
+func NewClientWithKMS(uriPrefix string, kms kmsiface.KMSAPI) (registry.KMSClient, error) {
+	return NewClientWithOptions(uriPrefix, WithKMS(kms), WithEncryptionContextName(LegacyAdditionalData))
+}
+
+// Supported returns true if keyURI starts with the URI prefix provided when
+// creating the client.
+func (c *awsClient) Supported(keyURI string) bool {
+	return strings.HasPrefix(keyURI, c.keyURIPrefix)
+}
+
+// GetAEAD returns an implementation of the AEAD interface which performs
+// cryptographic operations remotely via AWS KMS using keyURI.
+//
+// keyUri must be supported by this client and must have the following format:
+//
+//	aws-kms://arn:<partition>:kms:<region>:<path>
+//
+// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
+func (c *awsClient) GetAEAD(keyURI string) (tink.AEAD, error) {
+	if !c.Supported(keyURI) {
+		return nil, fmt.Errorf("keyURI must start with prefix %s, but got %s", c.keyURIPrefix, keyURI)
+	}
+
+	uri := strings.TrimPrefix(keyURI, awsPrefix)
+	return newAWSAEAD(uri, c.kms, c.encryptionContextName), nil
+}
+
+func getKMS(uriPrefix string) (*kms.KMS, error) {
 	r, err := getRegion(uriPrefix)
 	if err != nil {
 		return nil, err
 	}
 
-	session := session.Must(session.NewSession(&aws.Config{
+	session, err := session.NewSession(&aws.Config{
 		Region: aws.String(r),
-	}))
+	})
+	if err != nil {
+		return nil, err
+	}
 
-	return NewClientWithKMS(uriPrefix, kms.New(session))
+	return kms.New(session), nil
 }
 
-// NewClientWithCredentials returns a new AWS KMS client which will use given
-// credentials to handle keys with uriPrefix prefix.
-// uriPrefix must have the following format: 'aws-kms://arn:<partition>:kms:<region>:[:path]'.
-// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html.
-func NewClientWithCredentials(uriPrefix string, credentialPath string) (registry.KMSClient, error) {
+func getKMSFromCredentialPath(uriPrefix string, credentialPath string) (*kms.KMS, error) {
 	r, err := getRegion(uriPrefix)
 	if err != nil {
 		return nil, err
@@ -87,49 +311,31 @@
 	case errBadFile, errCredCSV:
 		return nil, err
 	default:
-		// fallback to load the credential path as .ini shared credentials.
+		// Fallback to load the credential path as .ini shared credentials.
 		creds = credentials.NewSharedCredentials(credentialPath, "default")
 	}
-	session := session.Must(session.NewSession(&aws.Config{
+
+	session, err := session.NewSession(&aws.Config{
 		Credentials: creds,
 		Region:      aws.String(r),
-	}))
-
-	return NewClientWithKMS(uriPrefix, kms.New(session))
-}
-
-// NewClientWithKMS returns a new AWS KMS client with user created KMS client.
-// Client is responsible for keeping the region consistency between key URI and KMS client.
-// uriPrefix must have the following format: 'aws-kms://arn:<partition>:kms:<region>:[:path]'.
-// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html.
-func NewClientWithKMS(uriPrefix string, kms kmsiface.KMSAPI) (registry.KMSClient, error) {
-	if !strings.HasPrefix(strings.ToLower(uriPrefix), awsPrefix) {
-		return nil, fmt.Errorf("uriPrefix must start with %s, but got %s", awsPrefix, uriPrefix)
+	})
+	if err != nil {
+		return nil, err
 	}
 
-	return &awsClient{
-		keyURIPrefix: uriPrefix,
-		kms:          kms,
-	}, nil
+	return kms.New(session), nil
 }
 
-// Supported true if this client does support keyURI
-func (c *awsClient) Supported(keyURI string) bool {
-	return strings.HasPrefix(keyURI, c.keyURIPrefix)
-}
-
-// GetAEAD gets an AEAD backend by keyURI.
-// keyURI must have the following format: 'aws-kms://arn:<partition>:kms:<region>:[:path]'.
-// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html.
-func (c *awsClient) GetAEAD(keyURI string) (tink.AEAD, error) {
-	if !c.Supported(keyURI) {
-		return nil, fmt.Errorf("keyURI must start with prefix %s, but got %s", c.keyURIPrefix, keyURI)
-	}
-
-	uri := strings.TrimPrefix(keyURI, awsPrefix)
-	return newAWSAEAD(uri, c.kms), nil
-}
-
+// extractCredsCSV extracts credentials from a CSV file.
+//
+// A CSV formatted credentials file can be obtained when an AWS IAM user is
+// created through the IAM console.
+//
+// Properties of a properly formatted CSV file:
+//
+//  1. The first line consists of the headers:
+//     "User name,Password,Access key ID,Secret access key,Console login link"
+//  2. The second line contains 5 comma separated values.
 func extractCredsCSV(file string) (*credentials.Value, error) {
 	f, err := os.Open(file)
 	if err != nil {
@@ -148,12 +354,6 @@
 		return nil, errors.New("not a valid CSV credential file")
 	}
 
-	// credentials.csv can be obtained when a AWS IAM user is created through IAM console.
-	// The first line of the csv file is "User name,Password,Access key ID,Secret access key,Console login link"
-	// The 2nd line of it contains 5 comma separated values.
-	// Parse the file with a strict format assumption as follows:
-	// 1. There must be at least 4 columns and 2 rows.
-	// 2. The access key id and the secret access key must be on (0-based) column 2 and 3.
 	if len(lines) < 2 {
 		return nil, errCredCSV
 	}
@@ -168,9 +368,8 @@
 	}, nil
 }
 
+// getRegion extracts the region from keyURI.
 func getRegion(keyURI string) (string, error) {
-	// keyURI must have the following format: 'aws-kms://arn:<partition>:kms:<region>:[:path]'.
-	// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html.
 	re1, err := regexp.Compile(`aws-kms://arn:(aws[a-zA-Z0-9-_]*):kms:([a-z0-9-]+):`)
 	if err != nil {
 		return "", err
diff --git a/go/integration/awskms/aws_kms_client_test.go b/go/integration/awskms/aws_kms_client_test.go
index 213f8a1..6760d53 100644
--- a/go/integration/awskms/aws_kms_client_test.go
+++ b/go/integration/awskms/aws_kms_client_test.go
@@ -14,91 +14,171 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-package awskms_test
+package awskms
 
 import (
+	"bytes"
+	"encoding/hex"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
-	"github.com/google/tink/go/integration/awskms"
+	"github.com/google/tink/go/integration/awskms/internal/fakeawskms"
+	"github.com/google/tink/go/core/registry"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/kms"
 )
 
-func TestNewClientGoodUriPrefixWithAwsPartition(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-	_, err := awskms.NewClient(uriPrefix)
+func TestNewClientWithOptions_URIPrefix(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+
+	// Necessary for testing deprecated factory functions.
+	credFile := filepath.Join(srcDir, "tink_go/testdata/aws/credentials.csv")
+	keyARN := "arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	fakekms, err := fakeawskms.New([]string{keyARN})
 	if err != nil {
-		t.Fatalf("error getting new client with good URI prefix: %v", err)
+		t.Fatalf("fakeawskms.New() failed: %v", err)
+	}
+
+	tests := []struct {
+		name      string
+		uriPrefix string
+		valid     bool
+	}{
+		{
+			name:      "AWS partition",
+			uriPrefix: "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f",
+			valid:     true,
+		},
+		{
+			name:      "AWS US government partition",
+			uriPrefix: "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f",
+			valid:     true,
+		},
+		{
+			name:      "AWS CN partition",
+			uriPrefix: "aws-kms://arn:aws-cn:kms:cn-north-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f",
+			valid:     true,
+		},
+		{
+			name:      "invalid",
+			uriPrefix: "bad-prefix://arn:aws-cn:kms:cn-north-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f",
+			valid:     false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			_, err := NewClientWithOptions(test.uriPrefix)
+			if test.valid && err != nil {
+				t.Errorf("NewClientWithOptions(%q) err = %v, want nil", test.uriPrefix, err)
+			}
+			if !test.valid && err == nil {
+				t.Errorf("NewClientWithOptions(%q) err = nil, want error", test.uriPrefix)
+			}
+
+			// Test deprecated factory functions.
+			_, err = NewClient(test.uriPrefix)
+			if test.valid && err != nil {
+				t.Errorf("NewClient(%q) err = %v, want nil", test.uriPrefix, err)
+			}
+			if !test.valid && err == nil {
+				t.Errorf("NewClient(%q) err = nil, want error", test.uriPrefix)
+			}
+
+			_, err = NewClientWithCredentials(test.uriPrefix, credFile)
+			if test.valid && err != nil {
+				t.Errorf("NewClientWithCredentialPath(%q, _) err = %v, want nil", test.uriPrefix, err)
+			}
+			if !test.valid && err == nil {
+				t.Errorf("NewClientWithCredentialPath(%q, _) err = nil, want error", test.uriPrefix)
+			}
+
+			_, err = NewClientWithKMS(test.uriPrefix, fakekms)
+			if test.valid && err != nil {
+				t.Errorf("NewClientWithKMS(%q, _) err = %v, want nil", test.uriPrefix, err)
+			}
+			if !test.valid && err == nil {
+				t.Errorf("NewClientWithKMS(%q, _) err = nil, want error", test.uriPrefix)
+			}
+		})
 	}
 }
 
-func TestNewClientGoodUriPrefixWithAwsUsGovPartition(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-	_, err := awskms.NewClient(uriPrefix)
-	if err != nil {
-		t.Fatalf("error getting new client with good URI prefix: %v", err)
+func TestNewClientWithOptions_WithCredentialPath(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+
+	uriPrefix := "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/"
+
+	tests := []struct {
+		name     string
+		credFile string
+		valid    bool
+	}{
+		{
+			name:     "valid CSV credentials file",
+			credFile: filepath.Join(srcDir, "tink_go/testdata/aws/credentials.csv"),
+			valid:    true,
+		},
+		{
+			name:     "valid INI credentials file",
+			credFile: filepath.Join(srcDir, "tink_go/testdata/aws/credentials.cred"),
+			valid:    true,
+		},
+		{
+			name:     "invalid credentials file",
+			credFile: filepath.Join(srcDir, "tink_go/testdata/aws/access_keys_bad.csv"),
+			valid:    false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			_, err := NewClientWithOptions(uriPrefix, WithCredentialPath(test.credFile))
+			if test.valid && err != nil {
+				t.Errorf("NewClientWithOptions(uriPrefix, WithCredentialPath(%q)) err = %v, want nil", test.credFile, err)
+			}
+			if !test.valid && err == nil {
+				t.Errorf("NewClientWithOptions(uriPrefix, WithCredentialPath(%q)) err = nil, want error", test.credFile)
+			}
+
+			// Test deprecated factory function.
+			_, err = NewClientWithCredentials(uriPrefix, test.credFile)
+			if test.valid && err != nil {
+				t.Errorf("NewClientWithCredentials(uriPrefix, %q) err = %v, want nil", test.credFile, err)
+			}
+			if !test.valid && err == nil {
+				t.Errorf("NewClientWithCredentials(uriPrefix, %q) err = nil, want error", test.credFile)
+			}
+
+		})
 	}
 }
 
-func TestNewClientGoodUriPrefixWithAwsCnPartition(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws-cn:kms:cn-north-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-	_, err := awskms.NewClient(uriPrefix)
+func TestNewClientWithOptions_RepeatedWithKMSFails(t *testing.T) {
+	keyARN := "arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	fakekms, err := fakeawskms.New([]string{keyARN})
 	if err != nil {
-		t.Fatalf("error getting new client with good URI prefix: %v", err)
+		t.Fatalf("fakekms.New() failed: %v", err)
 	}
-}
 
-func TestNewClientBadUriPrefix(t *testing.T) {
-	uriPrefix := "bad-prefix://arn:aws-cn:kms:cn-north-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-
-	_, err := awskms.NewClient(uriPrefix)
+	_, err = NewClientWithOptions("aws-kms://", WithKMS(fakekms), WithKMS(fakekms))
 	if err == nil {
-		t.Fatalf("does not reject bad URI prefix: %s", uriPrefix)
+		t.Fatalf("NewClientWithOptions(_, WithKMS(_), WithKMS(_)) err = nil, want error")
 	}
 }
 
-func TestNewClientWithCredentialsWithGoodCredentialsCsv(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-
-	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
-	if !ok {
-		t.Skip("TEST_SRCDIR not set")
-	}
-	goodCsvCredFile := filepath.Join(srcDir, "tink_go/testdata/aws/credentials.csv")
-
-	_, err := awskms.NewClientWithCredentials(uriPrefix, goodCsvCredFile)
-	if err != nil {
-		t.Fatalf("reject good CSV cred file: %s", goodCsvCredFile)
-	}
-}
-
-func TestNewClientWithCredentialsWithGoodCredentialsIni(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-
-	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
-	if !ok {
-		t.Skip("TEST_SRCDIR not set")
-	}
-	credINIFile := filepath.Join(srcDir, "tink_go/testdata/aws/credentials.cred")
-
-	_, err := awskms.NewClientWithCredentials(uriPrefix, credINIFile)
-	if err != nil {
-		t.Fatalf("reject good CSV cred file: %s", credINIFile)
-	}
-}
-
-func TestNewClientWithCredentialsWithBadCredentials(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-
-	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
-	if !ok {
-		t.Skip("TEST_SRCDIR not set")
-	}
-	badCredFile := filepath.Join(srcDir, "tink_go/testdata/aws/access_keys_bad.csv")
-
-	_, err := awskms.NewClientWithCredentials(uriPrefix, badCredFile)
+func TestNewClientWithOptions_RepeatedWithEncryptionContextNameFails(t *testing.T) {
+	_, err := NewClientWithOptions("aws-kms://", WithEncryptionContextName(LegacyAdditionalData), WithEncryptionContextName(AssociatedData))
 	if err == nil {
-		t.Fatalf("awskms.NewClientWithCredentials(uriPrefix, badCredFile) err = nil, want error")
+		t.Fatalf("NewClientWithOptions(_, WithEncryptionContextName(_), WithEncryptionContextName(_)) err = nil, want error")
 	}
 }
 
@@ -107,46 +187,257 @@
 	supportedKeyURI := "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
 	nonSupportedKeyURI := "aws-kms://arn:aws-us-gov:kms:us-gov-east-DOES-NOT-EXIST:key/"
 
-	client, err := awskms.NewClient(uriPrefix)
+	client, err := NewClientWithOptions(uriPrefix)
 	if err != nil {
-		t.Fatal(err)
+		t.Fatalf("NewClientWithOptions() failed: %v", err)
 	}
 
 	if !client.Supported(supportedKeyURI) {
-		t.Fatalf("client with URI prefix %s should support key URI %s", uriPrefix, supportedKeyURI)
+		t.Errorf("client with URI prefix %q should support key URI %q", uriPrefix, supportedKeyURI)
 	}
 
 	if client.Supported(nonSupportedKeyURI) {
-		t.Fatalf("client with URI prefix %s should NOT support key URI %s", uriPrefix, nonSupportedKeyURI)
+		t.Errorf("client with URI prefix %q should NOT support key URI %q", uriPrefix, nonSupportedKeyURI)
 	}
 }
 
-func TestGetAeadSupportedURI(t *testing.T) {
+func TestGetAEADSupportedURI(t *testing.T) {
 	uriPrefix := "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/"
 	supportedKeyURI := "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
 
-	client, err := awskms.NewClient(uriPrefix)
+	client, err := NewClientWithOptions(uriPrefix)
 	if err != nil {
-		t.Fatal(err)
+		t.Fatalf("NewClientWithOptions() failed: %v", err)
 	}
 
 	_, err = client.GetAEAD(supportedKeyURI)
 	if err != nil {
-		t.Fatalf("client with URI prefix %s should support key URI %s", uriPrefix, supportedKeyURI)
+		t.Errorf("client with URI prefix %q should support key URI %q", uriPrefix, supportedKeyURI)
 	}
 }
 
-func TestGetAeadNonSupportedURI(t *testing.T) {
-	uriPrefix := "aws-kms://arn:aws-us-gov:kms:us-gov-east-1:235739564943:key/"
-	nonSupportedKeyURI := "aws-kms://arn:aws-us-gov:kms:us-gov-east-DOES-NOT-EXIST:key/"
-
-	client, err := awskms.NewClient(uriPrefix)
+func TestGetAEADEncryptDecrypt(t *testing.T) {
+	keyARN := "arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	keyURI := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	fakekms, err := fakeawskms.New([]string{keyARN})
 	if err != nil {
-		t.Fatal(err)
+		t.Fatalf("fakekms.New() failed: %v", err)
 	}
 
-	_, err = client.GetAEAD(nonSupportedKeyURI)
+	client, err := NewClientWithOptions("aws-kms://", WithKMS(fakekms))
+	if err != nil {
+		t.Fatalf("NewClientWithOptions() failed: %v", err)
+	}
+
+	a, err := client.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("client.GetAEAD(keyURI) err = %v, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	ciphertext, err := a.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+	decrypted, err := a.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(decrypted, plaintext) {
+		t.Errorf("decrypted = %q, want %q", decrypted, plaintext)
+	}
+
+	_, err = a.Decrypt(ciphertext, []byte("invalidAssociatedData"))
 	if err == nil {
-		t.Fatalf("client with URI prefix %s should NOT support key URI %s", uriPrefix, nonSupportedKeyURI)
+		t.Error("a.Decrypt(ciphertext, []byte(\"invalidAssociatedData\")) err = nil, want error")
+	}
+
+	_, err = a.Decrypt([]byte("invalidCiphertext"), associatedData)
+	if err == nil {
+		t.Error("a.Decrypt([]byte(\"invalidCiphertext\"), associatedData) err = nil, want error")
+	}
+}
+
+func TestUsesAdditionalDataAsContextName(t *testing.T) {
+	keyARN := "arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	keyURI := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	fakekms, err := fakeawskms.New([]string{keyARN})
+	if err != nil {
+		t.Fatalf("fakeawskms.New() failed: %v", err)
+	}
+
+	client, err := NewClientWithKMS("aws-kms://", fakekms)
+	if err != nil {
+		t.Fatalf("NewClientWithKMS() failed: %v", err)
+	}
+
+	a, err := client.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("client.GetAEAD(keyURI) failed: %s", err)
+	}
+
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	ciphertext, err := a.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+
+	hexAD := hex.EncodeToString(associatedData)
+	context := map[string]*string{"additionalData": &hexAD}
+	decRequest := &kms.DecryptInput{
+		KeyId:             aws.String(keyARN),
+		CiphertextBlob:    ciphertext,
+		EncryptionContext: context,
+	}
+	decResponse, err := fakekms.Decrypt(decRequest)
+	if err != nil {
+		t.Fatalf("fakeKMS.Decrypt(decRequest) err = %s, want nil", err)
+	}
+	if !bytes.Equal(decResponse.Plaintext, plaintext) {
+		t.Errorf("decResponse.Plaintext = %q, want %q", decResponse.Plaintext, plaintext)
+	}
+	if strings.Compare(*decResponse.KeyId, keyARN) != 0 {
+		t.Errorf("decResponse.KeyId = %q, want %q", *decResponse.KeyId, keyARN)
+	}
+}
+
+func TestEncryptionContextName(t *testing.T) {
+	keyARN := "arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	keyURI := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	fakekms, err := fakeawskms.New([]string{keyARN})
+	if err != nil {
+		t.Fatalf("fakeawskms.New() failed: %v", err)
+	}
+
+	tests := []struct {
+		contextName     EncryptionContextName
+		wantContextName string
+	}{
+		{
+			contextName:     LegacyAdditionalData,
+			wantContextName: "additionalData",
+		},
+		{
+			contextName:     AssociatedData,
+			wantContextName: "associatedData",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.wantContextName, func(t *testing.T) {
+			client, err := NewClientWithOptions("aws-kms://", WithKMS(fakekms), WithEncryptionContextName(test.contextName))
+			if err != nil {
+				t.Fatalf("NewClientWithOptions() failed: %v", err)
+			}
+
+			a, err := client.GetAEAD(keyURI)
+			if err != nil {
+				t.Fatalf("client.GetAEAD(keyURI) failed: %s", err)
+			}
+
+			plaintext := []byte("plaintext")
+			associatedData := []byte("associatedData")
+			ciphertext, err := a.Encrypt(plaintext, associatedData)
+			if err != nil {
+				t.Fatalf("a.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+			}
+
+			hexAD := hex.EncodeToString(associatedData)
+			context := map[string]*string{test.wantContextName: &hexAD}
+			decRequest := &kms.DecryptInput{
+				KeyId:             aws.String(keyARN),
+				CiphertextBlob:    ciphertext,
+				EncryptionContext: context,
+			}
+			decResponse, err := fakekms.Decrypt(decRequest)
+			if err != nil {
+				t.Fatalf("fakeKMS.Decrypt(decRequest) err = %s, want nil", err)
+			}
+			if !bytes.Equal(decResponse.Plaintext, plaintext) {
+				t.Errorf("decResponse.Plaintext = %q, want %q", decResponse.Plaintext, plaintext)
+			}
+			if strings.Compare(*decResponse.KeyId, keyARN) != 0 {
+				t.Errorf("decResponse.KeyId = %q, want %q", *decResponse.KeyId, keyARN)
+			}
+		})
+	}
+}
+
+func TestEncryptionContextName_defaultEncryptionContextName(t *testing.T) {
+	keyARN := "arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	keyURI := "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	fakekms, err := fakeawskms.New([]string{keyARN})
+	if err != nil {
+		t.Fatalf("fakeawskms.New() failed: %v", err)
+	}
+
+	tests := []struct {
+		name string
+		client		func(t *testing.T) registry.KMSClient
+		wantContextName string
+	}{
+		{
+			name: "NewClientWithOptions",
+			client: func(t *testing.T) registry.KMSClient{
+				t.Helper()
+				c, err := NewClientWithOptions(keyURI, WithKMS(fakekms))
+				if err != nil {
+					t.Fatalf("NewClientWithOptions() failed: %v", err)
+				}
+				return c
+
+			},
+			wantContextName: "associatedData",
+		},
+		// Test deprecated factory function.
+		{
+			name: "NewClientWithKMS",
+			client: func(t *testing.T) registry.KMSClient{
+				t.Helper()
+				c, err := NewClientWithKMS(keyURI, fakekms)
+				if err != nil {
+					t.Fatalf("NewClientWithKMS() failed: %v", err)
+				}
+				return c
+			},
+			wantContextName: "additionalData",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			client := test.client(t)
+			a, err := client.GetAEAD(keyURI)
+			if err != nil {
+				t.Fatalf("client.GetAEAD(keyURI) failed: %s", err)
+			}
+
+			plaintext := []byte("plaintext")
+			associatedData := []byte("associatedData")
+			ciphertext, err := a.Encrypt(plaintext, associatedData)
+			if err != nil {
+				t.Fatalf("a.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+			}
+
+			hexAD := hex.EncodeToString(associatedData)
+			context := map[string]*string{test.wantContextName: &hexAD}
+			decRequest := &kms.DecryptInput{
+				KeyId:             aws.String(keyARN),
+				CiphertextBlob:    ciphertext,
+				EncryptionContext: context,
+			}
+			decResponse, err := fakekms.Decrypt(decRequest)
+			if err != nil {
+				t.Fatalf("fakeKMS.Decrypt(decRequest) err = %s, want nil", err)
+			}
+			if !bytes.Equal(decResponse.Plaintext, plaintext) {
+				t.Errorf("decResponse.Plaintext = %q, want %q", decResponse.Plaintext, plaintext)
+			}
+			if strings.Compare(*decResponse.KeyId, keyARN) != 0 {
+				t.Errorf("decResponse.KeyId = %q, want %q", *decResponse.KeyId, keyARN)
+			}
+		})
 	}
 }
diff --git a/go/integration/awskms/aws_kms_integration_test.go b/go/integration/awskms/aws_kms_integration_test.go
new file mode 100644
index 0000000..7478dd0
--- /dev/null
+++ b/go/integration/awskms/aws_kms_integration_test.go
@@ -0,0 +1,217 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package awskms_test
+
+import (
+	"bytes"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"flag"
+	// context is used to cancel outstanding requests
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/tink"
+
+	"github.com/google/tink/go/integration/awskms"
+)
+
+const (
+	keyPrefix   = "aws-kms://arn:aws:kms:us-east-2:235739564943:"
+	keyAliasURI = "aws-kms://arn:aws:kms:us-east-2:235739564943:alias/unit-and-integration-testing"
+	keyURI      = "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
+	keyURI2     = "aws-kms://arn:aws:kms:us-east-2:235739564943:key/b3ca2efd-a8fb-47f2-b541-7e20f8c5cd11"
+)
+
+var (
+	credCSVFile = "tink_go/testdata/aws/credentials.csv"
+	credINIFile = "tink_go/testdata/aws/credentials.ini"
+)
+
+func init() {
+	certPath := filepath.Join(os.Getenv("TEST_SRCDIR"), "tink_base/roots.pem")
+	flag.Set("cacerts", certPath)
+	os.Setenv("SSL_CERT_FILE", certPath)
+}
+
+func TestNewClientWithCredentialsGetAEADEncryptDecrypt(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+	client, err := awskms.NewClientWithOptions(keyURI, awskms.WithCredentialPath(filepath.Join(srcDir, credCSVFile)))
+	if err != nil {
+		t.Fatalf("error setting up AWS client: %v", err)
+	}
+	a, err := client.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("client.GetAEAD(keyURI) err = %v, want nil", err)
+	}
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associatedData")
+	ciphertext, err := a.Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+	gotPlaintext, err := a.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext, plaintext) {
+		t.Errorf("a.Decrypt() = %q, want %q", gotPlaintext, plaintext)
+	}
+
+	invalidAssociatedData := []byte("invalidAssociatedData")
+	_, err = a.Decrypt(ciphertext, invalidAssociatedData)
+	if err == nil {
+		t.Error("a.Decrypt(ciphertext, invalidAssociatedData) err = nil, want error")
+	}
+}
+
+func TestEmptyAssociatedDataEncryptDecrypt(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+	client, err := awskms.NewClientWithOptions(keyURI, awskms.WithCredentialPath(filepath.Join(srcDir, credCSVFile)))
+	if err != nil {
+		t.Fatalf("error setting up AWS client: %v", err)
+	}
+	a, err := client.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("client.GetAEAD(keyURI) err = %v, want nil", err)
+	}
+	plaintext := []byte("plaintext")
+	emptyAssociatedData := []byte{}
+	ciphertext, err := a.Encrypt(plaintext, emptyAssociatedData)
+	if err != nil {
+		t.Fatalf("a.Encrypt(plaintext, emptyAssociatedData) err = %v, want nil", err)
+	}
+	gotPlaintext, err := a.Decrypt(ciphertext, emptyAssociatedData)
+	if err != nil {
+		t.Fatalf("a.Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext, plaintext) {
+		t.Errorf("a.Decrypt() = %q, want %q", gotPlaintext, plaintext)
+	}
+
+	gotPlaintext2, err := a.Decrypt(ciphertext, nil)
+	if err != nil {
+		t.Fatalf("a.Decrypt(ciphertext, nil) err = %v, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext2, plaintext) {
+		t.Errorf("a.Decrypt() = %q, want %q", gotPlaintext, plaintext)
+	}
+}
+
+func TestKeyCommitment(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+
+	client, err := awskms.NewClientWithOptions(keyPrefix, awskms.WithCredentialPath(filepath.Join(srcDir, credCSVFile)))
+	if err != nil {
+		t.Fatalf("error setting up AWS client: %v", err)
+	}
+
+	// Create AEAD primitives for two keys.
+	keys := []string{keyURI, keyURI2}
+	aeads := make([]tink.AEAD, 0, len(keys))
+	for _, k := range keys {
+		a, err := client.GetAEAD(k)
+		if err != nil {
+			t.Fatalf("client.GetAEAD(keyURI) err = %v, want nil", err)
+		}
+		aeads = append(aeads, a)
+	}
+
+	// Create a ciphertext using the first primitive.
+	plaintext := []byte("plaintext")
+	associatedData := []byte("associated data")
+	ciphertext, err := aeads[0].Encrypt(plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("aeads[0].Encrypt(plaintext, associatedData) err = %v, want nil", err)
+	}
+	gotPlaintext, err := aeads[0].Decrypt(ciphertext, associatedData)
+	if err != nil {
+		t.Fatalf("aeads[0].Decrypt(ciphertext, associatedData) err = %v, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext, plaintext) {
+		t.Errorf("aeads[0].Decrypt() = %q, want %q", gotPlaintext, plaintext)
+	}
+
+	// Attempt to decrypt using the other primitive.
+	_, err = aeads[1].Decrypt(ciphertext, associatedData)
+	if err == nil {
+		t.Fatalf("aeads[1].Decrypt(ciphertext, associatedData) err = nil, want non-nil")
+	}
+}
+
+func setupKMS(t *testing.T, credPath string, uri string) {
+	t.Helper()
+	client, err := awskms.NewClientWithOptions(keyURI, awskms.WithCredentialPath(credPath))
+	if err != nil {
+		t.Fatalf("error setting up AWS client: %v", err)
+	}
+	// The registry will return the first KMS client that claims support for
+	// the keyURI.  The tests re-use the same keyURI, so clear any clients
+	// registered by earlier tests before registering the new client.
+	registry.ClearKMSClients()
+	registry.RegisterKMSClient(client)
+}
+
+func TestKMSEnvelopeAEADEncryptAndDecrypt(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+
+	for _, credFile := range []string{credCSVFile, credINIFile} {
+		setupKMS(t, filepath.Join(srcDir, credFile), keyURI)
+		dek := aead.AES128CTRHMACSHA256KeyTemplate()
+		template, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, dek)
+		if err != nil {
+			t.Fatalf("aead.CreateKMSEnvelopeAEADKeyTemplate() err = %v, want nil", err)
+		}
+		handle, err := keyset.NewHandle(template)
+		if err != nil {
+			t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+		}
+		a, err := aead.New(handle)
+		if err != nil {
+			t.Fatalf("aead.New() err = %v, want nil", err)
+		}
+		for _, ad := range [][]byte{nil, random.GetRandomBytes(20)} {
+			pt := random.GetRandomBytes(20)
+			ct, err := a.Encrypt(pt, ad)
+			if err != nil {
+				t.Fatalf("a.Encrypt(pt, ad) err = %v, want nil", err)
+			}
+			dt, err := a.Decrypt(ct, ad)
+			if err != nil {
+				t.Fatalf("a.Decrypt(ct, ad) err = %v, want nil", err)
+			}
+			if !bytes.Equal(dt, pt) {
+				t.Errorf("a.Decrypt() = %q, want %q", dt, pt)
+			}
+		}
+	}
+}
diff --git a/go/integration/awskms/internal/fakeawskms/BUILD.bazel b/go/integration/awskms/internal/fakeawskms/BUILD.bazel
new file mode 100644
index 0000000..ef45141
--- /dev/null
+++ b/go/integration/awskms/internal/fakeawskms/BUILD.bazel
@@ -0,0 +1,34 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])  # keep
+
+go_library(
+    name = "fakeawskms",
+    srcs = ["fakeawskms.go"],
+    importpath = "github.com/google/tink/go/integration/awskms/internal/fakeawskms",
+    deps = [
+        "//aead",
+        "//keyset",
+        "//tink",
+        "@com_github_aws_aws_sdk_go//service/kms",
+        "@com_github_aws_aws_sdk_go//service/kms/kmsiface",
+    ],
+)
+
+go_test(
+    name = "fakeawskms_test",
+    srcs = ["fakeawskms_test.go"],
+    embed = [":fakeawskms"],
+    deps = [
+        "@com_github_aws_aws_sdk_go//aws",
+        "@com_github_aws_aws_sdk_go//service/kms",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":fakeawskms",
+    visibility = ["//integration/awskms:__subpackages__"],
+)
diff --git a/go/integration/awskms/internal/fakeawskms/fakeawskms.go b/go/integration/awskms/internal/fakeawskms/fakeawskms.go
new file mode 100644
index 0000000..c634a3d
--- /dev/null
+++ b/go/integration/awskms/internal/fakeawskms/fakeawskms.go
@@ -0,0 +1,121 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package fakeawskms provides a partial fake implementation of kmsiface.KMSAPI.
+package fakeawskms
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"sort"
+
+	"github.com/aws/aws-sdk-go/service/kms"
+	"github.com/aws/aws-sdk-go/service/kms/kmsiface"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/tink"
+)
+
+type fakeAWSKMS struct {
+	kmsiface.KMSAPI
+	aeads  map[string]tink.AEAD
+	keyIDs []string
+}
+
+// serializeContext serializes the context map in a canonical way into a byte array.
+func serializeContext(context map[string]*string) []byte {
+	names := make([]string, 0, len(context))
+	for name := range context {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	b := new(bytes.Buffer)
+	b.WriteString("{")
+	for i, name := range names {
+		if i > 0 {
+			b.WriteString(",")
+		}
+		fmt.Fprintf(b, "%q:%q", name, *context[name])
+	}
+	b.WriteString("}")
+	return b.Bytes()
+}
+
+// New returns a new fake AWS KMS API.
+func New(validKeyIDs []string) (kmsiface.KMSAPI, error) {
+	aeads := make(map[string]tink.AEAD)
+	for _, keyID := range validKeyIDs {
+		handle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
+		if err != nil {
+			return nil, err
+		}
+		a, err := aead.New(handle)
+		if err != nil {
+			return nil, err
+		}
+		aeads[keyID] = a
+	}
+	return &fakeAWSKMS{
+		aeads:  aeads,
+		keyIDs: validKeyIDs,
+	}, nil
+}
+
+func (f *fakeAWSKMS) Encrypt(request *kms.EncryptInput) (*kms.EncryptOutput, error) {
+	a, ok := f.aeads[*request.KeyId]
+	if !ok {
+		return nil, fmt.Errorf("Unknown keyID: %q not in %q", *request.KeyId, f.keyIDs)
+	}
+	serializedContext := serializeContext(request.EncryptionContext)
+	ciphertext, err := a.Encrypt(request.Plaintext, serializedContext)
+	if err != nil {
+		return nil, err
+	}
+	return &kms.EncryptOutput{
+		CiphertextBlob: ciphertext,
+		KeyId:          request.KeyId,
+	}, nil
+}
+
+func (f *fakeAWSKMS) Decrypt(request *kms.DecryptInput) (*kms.DecryptOutput, error) {
+	serializedContext := serializeContext(request.EncryptionContext)
+	if request.KeyId != nil {
+		a, ok := f.aeads[*request.KeyId]
+		if !ok {
+			return nil, fmt.Errorf("Unknown keyID: %q not in %q", *request.KeyId, f.keyIDs)
+		}
+		plaintext, err := a.Decrypt(request.CiphertextBlob, serializedContext)
+		if err != nil {
+			return nil, fmt.Errorf("Decryption with keyID %q failed", *request.KeyId)
+		}
+		return &kms.DecryptOutput{
+			Plaintext: plaintext,
+			KeyId:     request.KeyId,
+		}, nil
+	}
+	// When KeyId is not set, try out all AEADs.
+	for keyID, a := range f.aeads {
+		plaintext, err := a.Decrypt(request.CiphertextBlob, serializedContext)
+		if err == nil {
+			return &kms.DecryptOutput{
+				Plaintext: plaintext,
+				KeyId:     &keyID,
+			}, nil
+		}
+	}
+	return nil, errors.New("unable to decrypt message")
+}
diff --git a/go/integration/awskms/internal/fakeawskms/fakeawskms_test.go b/go/integration/awskms/internal/fakeawskms/fakeawskms_test.go
new file mode 100644
index 0000000..1624235
--- /dev/null
+++ b/go/integration/awskms/internal/fakeawskms/fakeawskms_test.go
@@ -0,0 +1,273 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package fakeawskms
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/kms"
+)
+
+const validKeyID = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
+const validKeyID2 = "arn:aws:kms:us-west-2:123:key/different"
+
+func TestEncyptDecryptWithValidKeyId(t *testing.T) {
+	fakeKMS, err := New([]string{validKeyID})
+	if err != nil {
+		t.Fatalf("New() err = %s, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	contextValue := "contextValue"
+	context := map[string]*string{"contextName": &contextValue}
+
+	encRequest := &kms.EncryptInput{
+		KeyId:             aws.String(validKeyID),
+		Plaintext:         plaintext,
+		EncryptionContext: context,
+	}
+
+	encResponse, err := fakeKMS.Encrypt(encRequest)
+	if err != nil {
+		t.Fatalf("fakeKMS.Encrypt(encRequest) err = %s, want nil", err)
+	}
+
+	ciphertext := encResponse.CiphertextBlob
+
+	decRequest := &kms.DecryptInput{
+		KeyId:             aws.String(validKeyID),
+		CiphertextBlob:    ciphertext,
+		EncryptionContext: context,
+	}
+	decResponse, err := fakeKMS.Decrypt(decRequest)
+	if err != nil {
+		t.Fatalf("fakeKMS.Decrypt(decRequest) err = %s, want nil", err)
+	}
+	if !bytes.Equal(decResponse.Plaintext, plaintext) {
+		t.Fatalf("decResponse.Plaintext = %q, want %q", decResponse.Plaintext, plaintext)
+	}
+	if strings.Compare(*decResponse.KeyId, validKeyID) != 0 {
+		t.Fatalf("decResponse.KeyId = %q, want %q", *decResponse.KeyId, validKeyID)
+	}
+
+	// decrypt with a different context should fail
+	otherContextValue := "otherContextValue"
+	otherContext := map[string]*string{"contextName": &otherContextValue}
+	otherDecRequest := &kms.DecryptInput{
+		KeyId:             aws.String(validKeyID),
+		CiphertextBlob:    ciphertext,
+		EncryptionContext: otherContext,
+	}
+	if _, err := fakeKMS.Decrypt(otherDecRequest); err == nil {
+		t.Fatal("fakeKMS.Decrypt(otherDecRequest) err = nil, want not nil")
+	}
+}
+
+func TestEncyptWithUnknownKeyID(t *testing.T) {
+	fakeKMS, err := New([]string{validKeyID})
+	if err != nil {
+		t.Fatalf("New() err = %s, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	contextValue := "contextValue"
+	context := map[string]*string{"contextName": &contextValue}
+
+	encRequestWithUnknownKeyID := &kms.EncryptInput{
+		KeyId:             aws.String(validKeyID2),
+		Plaintext:         plaintext,
+		EncryptionContext: context,
+	}
+
+	if _, err := fakeKMS.Encrypt(encRequestWithUnknownKeyID); err == nil {
+		t.Fatal("fakeKMS.Encrypt(encRequestWithvalidKeyID2) err = nil, want not nil")
+	}
+}
+
+func TestDecryptWithInvalidCiphertext(t *testing.T) {
+	fakeKMS, err := New([]string{validKeyID})
+	if err != nil {
+		t.Fatalf("New() err = %s, want nil", err)
+	}
+
+	invalidCiphertext := []byte("plaintext")
+	contextValue := "contextValue"
+	context := map[string]*string{"contextName": &contextValue}
+
+	decRequest := &kms.DecryptInput{
+		CiphertextBlob:    invalidCiphertext,
+		EncryptionContext: context,
+	}
+
+	if _, err := fakeKMS.Decrypt(decRequest); err == nil {
+		t.Fatal("fakeKMS.Decrypt(decRequest) err = nil, want not nil")
+	}
+}
+
+func TestDecryptWithUnknownKeyId(t *testing.T) {
+	fakeKMS, err := New([]string{validKeyID})
+	if err != nil {
+		t.Fatalf("New() err = %s, want nil", err)
+	}
+
+	ciphertext := []byte("invalidCiphertext")
+	contextValue := "contextValue"
+	context := map[string]*string{"contextName": &contextValue}
+
+	decRequest := &kms.DecryptInput{
+		KeyId:             aws.String(validKeyID2),
+		CiphertextBlob:    ciphertext,
+		EncryptionContext: context,
+	}
+
+	if _, err := fakeKMS.Decrypt(decRequest); err == nil {
+		t.Fatal("fakeKMS.Decrypt(decRequest) err = nil, want not nil")
+	}
+}
+
+func TestDecryptWithWrongKeyId(t *testing.T) {
+	fakeKMS, err := New([]string{validKeyID, validKeyID2})
+	if err != nil {
+		t.Fatalf("New() err = %s, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	contextValue := "contextValue"
+	context := map[string]*string{"contextName": &contextValue}
+
+	encRequest := &kms.EncryptInput{
+		KeyId:             aws.String(validKeyID),
+		Plaintext:         plaintext,
+		EncryptionContext: context,
+	}
+
+	encResponse, err := fakeKMS.Encrypt(encRequest)
+	if err != nil {
+		t.Fatalf("fakeKMS.Encrypt(encRequest) err = %s, want nil", err)
+	}
+
+	ciphertext := encResponse.CiphertextBlob
+
+	decRequest := &kms.DecryptInput{
+		KeyId:             aws.String(validKeyID2), // wrong key id
+		CiphertextBlob:    ciphertext,
+		EncryptionContext: context,
+	}
+	if _, err := fakeKMS.Decrypt(decRequest); err == nil {
+		t.Fatal("fakeKMS.Decrypt(decRequest) err = nil, want not nil")
+	}
+}
+
+func TestDecryptWithoutKeyId(t *testing.T) {
+	// setting the keyId in DecryptInput is not required, see
+	// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/kms#DecryptInput
+
+	fakeKMS, err := New([]string{validKeyID, validKeyID2})
+	if err != nil {
+		t.Fatalf("New() err = %s, want nil", err)
+	}
+
+	plaintext := []byte("plaintext")
+	plaintext2 := []byte("plaintext2")
+	contextValue := "contextValue"
+	context := map[string]*string{"contextName": &contextValue}
+
+	encRequest := &kms.EncryptInput{
+		KeyId:             aws.String(validKeyID),
+		Plaintext:         plaintext,
+		EncryptionContext: context,
+	}
+	encResponse, err := fakeKMS.Encrypt(encRequest)
+	if err != nil {
+		t.Fatalf("fakeKMS.Encrypt(encRequest) err = %s, want nil", err)
+	}
+	if strings.Compare(*encResponse.KeyId, validKeyID) != 0 {
+		t.Fatalf("encResponse.KeyId = %q, want %q", *encResponse.KeyId, validKeyID)
+	}
+
+	encRequest2 := &kms.EncryptInput{
+		KeyId:             aws.String(validKeyID2),
+		Plaintext:         plaintext2,
+		EncryptionContext: context,
+	}
+	encResponse2, err := fakeKMS.Encrypt(encRequest2)
+	if err != nil {
+		t.Fatalf("fakeKMS.Encrypt(encRequest2) err = %s, want nil", err)
+	}
+	if strings.Compare(*encResponse2.KeyId, validKeyID2) != 0 {
+		t.Fatalf("encResponse2.KeyId = %q, want %q", *encResponse2.KeyId, validKeyID2)
+	}
+
+	decRequest := &kms.DecryptInput{
+		// KeyId is not set
+		CiphertextBlob:    encResponse.CiphertextBlob,
+		EncryptionContext: context,
+	}
+	decResponse, err := fakeKMS.Decrypt(decRequest)
+	if err != nil {
+		t.Fatalf("fakeKMS.Decrypt(decRequest) err = %s, want nil", err)
+	}
+	if !bytes.Equal(decResponse.Plaintext, plaintext) {
+		t.Fatalf("decResponse.Plaintext = %q, want %q", decResponse.Plaintext, plaintext)
+	}
+	if strings.Compare(*decResponse.KeyId, validKeyID) != 0 {
+		t.Fatalf("decResponse.KeyId = %q, want %q", *decResponse.KeyId, validKeyID)
+	}
+
+	decRequest2 := &kms.DecryptInput{
+		// KeyId is not set
+		CiphertextBlob:    encResponse2.CiphertextBlob,
+		EncryptionContext: context,
+	}
+	decResponse2, err := fakeKMS.Decrypt(decRequest2)
+	if err != nil {
+		t.Fatalf("fakeKMS.Decrypt(decRequest2) err = %s, want nil", err)
+	}
+	if !bytes.Equal(decResponse2.Plaintext, plaintext2) {
+		t.Fatalf("decResponse.Plaintext = %q, want %q", decResponse.Plaintext, plaintext2)
+	}
+	if strings.Compare(*decResponse2.KeyId, validKeyID2) != 0 {
+		t.Fatalf("decResponse2.KeyId = %q, want %q", *decResponse2.KeyId, validKeyID2)
+	}
+}
+
+func TestSerializeContext(t *testing.T) {
+	uvw := "uvw"
+	xyz := "xyz"
+	rst := "rst"
+	context := map[string]*string{"def": &uvw, "abc": &xyz, "ghi": &rst}
+
+	got := string(serializeContext(context))
+	want := "{\"abc\":\"xyz\",\"def\":\"uvw\",\"ghi\":\"rst\"}"
+	if got != want {
+		t.Fatalf("SerializeContext(context) = %s, want %s", got, want)
+	}
+
+	gotEscaped := string(serializeContext(map[string]*string{"a\"b": &xyz}))
+	wantEscaped := "{\"a\\\"b\":\"xyz\"}"
+	if gotEscaped != wantEscaped {
+		t.Fatalf("SerializeContext(context) = %s, want %s", gotEscaped, wantEscaped)
+	}
+
+	gotEmpty := string(serializeContext(map[string]*string{}))
+	if gotEmpty != "{}" {
+		t.Fatalf("SerializeContext(context) = %s, want %s", gotEmpty, "{}")
+	}
+}
diff --git a/go/integration/gcpkms/BUILD.bazel b/go/integration/gcpkms/BUILD.bazel
index 6a66834..df2f6aa 100644
--- a/go/integration/gcpkms/BUILD.bazel
+++ b/go/integration/gcpkms/BUILD.bazel
@@ -17,16 +17,14 @@
         "//tink",
         "@org_golang_google_api//cloudkms/v1:cloudkms",
         "@org_golang_google_api//option",
-        "@org_golang_x_oauth2//:oauth2",
-        "@org_golang_x_oauth2//google",
     ],
 )
 
 go_test(
     name = "gcpkms_test",
     srcs = [
-        "gcp_kms_aead_test.go",
         "gcp_kms_client_test.go",
+        "gcp_kms_integration_test.go",
     ],
     data = [
         # Credentials can be injected into the test files included in these
@@ -44,6 +42,7 @@
         "//keyset",
         "//subtle/random",
         "//tink",
+        "@org_golang_google_api//option",
     ],
 )
 
diff --git a/go/integration/gcpkms/gcp_kms_aead_test.go b/go/integration/gcpkms/gcp_kms_aead_test.go
deleted file mode 100644
index cc26db7..0000000
--- a/go/integration/gcpkms/gcp_kms_aead_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package gcpkms_test
-
-import (
-	"bytes"
-	"errors"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"flag"
-	// context is used to cancel outstanding requests
-	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/integration/gcpkms"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/subtle/random"
-	"github.com/google/tink/go/tink"
-)
-
-const (
-	keyURI = "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key"
-)
-
-var (
-	credFile = "tink_go/testdata/gcp/credential.json"
-)
-
-func init() {
-	certPath := filepath.Join(os.Getenv("TEST_SRCDIR"), "tink_base/roots.pem")
-	flag.Set("cacerts", certPath)
-	os.Setenv("SSL_CERT_FILE", certPath)
-}
-
-func setupKMS(t *testing.T) {
-	t.Helper()
-
-	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
-	if !ok {
-		t.Skip("TEST_SRCDIR not set")
-	}
-
-	g, err := gcpkms.NewClientWithCredentials(keyURI, filepath.Join(srcDir, credFile))
-	if err != nil {
-		t.Errorf("error setting up gcp client: %v", err)
-	}
-	registry.RegisterKMSClient(g)
-}
-
-func basicAEADTest(t *testing.T, a tink.AEAD) error {
-	t.Helper()
-	for i := 0; i < 100; i++ {
-		pt := random.GetRandomBytes(20)
-		ad := random.GetRandomBytes(20)
-		ct, err := a.Encrypt(pt, ad)
-		if err != nil {
-			return err
-		}
-		dt, err := a.Decrypt(ct, ad)
-		if err != nil {
-			return err
-		}
-		if !bytes.Equal(dt, pt) {
-			return errors.New("decrypt not inverse of encrypt")
-		}
-	}
-	return nil
-}
-
-func TestBasicAead(t *testing.T) {
-	setupKMS(t)
-	dek := aead.AES128CTRHMACSHA256KeyTemplate()
-	kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(keyURI, dek))
-	if err != nil {
-		t.Errorf("error getting a new keyset handle: %v", err)
-	}
-	a, err := aead.New(kh)
-	if err != nil {
-		t.Errorf("error getting the primitive: %v", err)
-	}
-	if err := basicAEADTest(t, a); err != nil {
-		t.Errorf("error in basic aead tests: %v", err)
-	}
-}
diff --git a/go/integration/gcpkms/gcp_kms_client.go b/go/integration/gcpkms/gcp_kms_client.go
index 535c4d8..7db9e51 100644
--- a/go/integration/gcpkms/gcp_kms_client.go
+++ b/go/integration/gcpkms/gcp_kms_client.go
@@ -22,15 +22,11 @@
 	"context"
 	"errors"
 	"fmt"
-	"io/ioutil"
-	"net/http"
 	"runtime"
 	"strings"
 
 	"google.golang.org/api/cloudkms/v1"
 	"google.golang.org/api/option"
-	"golang.org/x/oauth2/google"
-	"golang.org/x/oauth2"
 	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/tink"
 )
@@ -72,97 +68,6 @@
 	}, nil
 }
 
-// ClientConfig defines the configuration that can be provided to configure
-// a GCP KMS client.
-//
-// Deprecated: Use NewClientWithOptions instead to provide client options.
-type ClientConfig struct {
-	// HTTP transport for use with the GCP KMS client.
-	// If it is nil, default config will be used.
-	HTTPTransport *http.Transport
-}
-
-// NewClientWithConfig returns a new GCP KMS client
-// using the provided ClientConfig.
-// It will use default credentials to handle keys with uriPrefix prefix.
-// uriPrefix must have the following format: 'gcp-kms://[:path]'.
-//
-// Deprecated: Use NewClientWithOptions instead.
-// To provide a custom HTTP client, use option.WithHTTPClient.
-func NewClientWithConfig(uriPrefix string, config *ClientConfig) (registry.KMSClient, error) {
-	if !strings.HasPrefix(strings.ToLower(uriPrefix), gcpPrefix) {
-		return nil, fmt.Errorf("uriPrefix must start with %s", gcpPrefix)
-	}
-
-	ctx := context.Background()
-	client, err := google.DefaultClient(ctx, cloudkms.CloudPlatformScope)
-	if err != nil {
-		return nil, err
-	}
-	if config != nil && config.HTTPTransport != nil {
-		t, ok := client.Transport.(*oauth2.Transport)
-		if !ok {
-			return nil, fmt.Errorf("unable to type assert HTTP client.Transport to *oauth2.Transport, got %T", client.Transport)
-		}
-		t.Base = config.HTTPTransport
-	}
-
-	kmsService, err := cloudkms.New(client)
-	if err != nil {
-		return nil, err
-	}
-
-	return &gcpClient{
-		keyURIPrefix: uriPrefix,
-		kms:          kmsService,
-	}, nil
-}
-
-// NewClient returns a new GCP KMS client which will use default
-// credentials to handle keys with uriPrefix prefix.
-// uriPrefix must have the following format: 'gcp-kms://[:path]'.
-//
-// Deprecated: Use NewClientWithOptions instead.
-func NewClient(uriPrefix string) (registry.KMSClient, error) {
-	return NewClientWithConfig(uriPrefix, nil)
-}
-
-// NewClientWithCredentials returns a new GCP KMS client which will use given
-// credentials to handle keys with uriPrefix prefix.
-// uriPrefix must have the following format: 'gcp-kms://[:path]'.
-//
-// Deprecated: Use NewClientWithOptions instead.
-// To provide a credential file, use option.WithCredentialsFile.
-func NewClientWithCredentials(uriPrefix string, credentialPath string) (registry.KMSClient, error) {
-	if !strings.HasPrefix(strings.ToLower(uriPrefix), gcpPrefix) {
-		return nil, fmt.Errorf("uriPrefix must start with %s", gcpPrefix)
-	}
-
-	ctx := context.Background()
-	if len(credentialPath) <= 0 {
-		return nil, errCred
-	}
-	data, err := ioutil.ReadFile(credentialPath)
-	if err != nil {
-		return nil, errCred
-	}
-	creds, err := google.CredentialsFromJSON(ctx, data, "https://www.googleapis.com/auth/cloudkms")
-	if err != nil {
-		return nil, errCred
-	}
-	client := oauth2.NewClient(ctx, creds.TokenSource)
-	kmsService, err := cloudkms.New(client)
-	kmsService.UserAgent = tinkUserAgent
-	if err != nil {
-		return nil, err
-	}
-
-	return &gcpClient{
-		keyURIPrefix: uriPrefix,
-		kms:          kmsService,
-	}, nil
-}
-
 // Supported true if this client does support keyURI
 func (c *gcpClient) Supported(keyURI string) bool {
 	return strings.HasPrefix(keyURI, c.keyURIPrefix)
diff --git a/go/integration/gcpkms/gcp_kms_client_test.go b/go/integration/gcpkms/gcp_kms_client_test.go
index 8d9e6df..c6b1ccd 100644
--- a/go/integration/gcpkms/gcp_kms_client_test.go
+++ b/go/integration/gcpkms/gcp_kms_client_test.go
@@ -17,8 +17,10 @@
 package gcpkms_test
 
 import (
+	"context"
 	"log"
 
+	"google.golang.org/api/option"
 	"github.com/google/tink/go/aead"
 	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/integration/gcpkms"
@@ -27,19 +29,23 @@
 
 func Example() {
 	const keyURI = "gcp-kms://......"
-
-	gcpclient, err := gcpkms.NewClientWithCredentials(keyURI, "/mysecurestorage/credentials.json")
+	ctx := context.Background()
+	gcpclient, err := gcpkms.NewClientWithOptions(ctx, keyURI, option.WithCredentialsFile("/mysecurestorage/credentials.json"))
 	if err != nil {
 		log.Fatal(err)
 	}
 	registry.RegisterKMSClient(gcpclient)
 
 	dek := aead.AES128CTRHMACSHA256KeyTemplate()
-	kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(keyURI, dek))
+	template, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, dek)
 	if err != nil {
 		log.Fatal(err)
 	}
-	a, err := aead.New(kh)
+	handle, err := keyset.NewHandle(template)
+	if err != nil {
+		log.Fatal(err)
+	}
+	a, err := aead.New(handle)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/go/integration/gcpkms/gcp_kms_integration_test.go b/go/integration/gcpkms/gcp_kms_integration_test.go
new file mode 100644
index 0000000..26d98ae
--- /dev/null
+++ b/go/integration/gcpkms/gcp_kms_integration_test.go
@@ -0,0 +1,105 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package gcpkms_test
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"flag"
+	// context is used to cancel outstanding requests
+	"google.golang.org/api/option"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/integration/gcpkms"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/tink"
+)
+
+const (
+	keyURI = "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key"
+)
+
+var (
+	credFile = "tink_go/testdata/gcp/credential.json"
+)
+
+func init() {
+	certPath := filepath.Join(os.Getenv("TEST_SRCDIR"), "tink_base/roots.pem")
+	flag.Set("cacerts", certPath)
+	os.Setenv("SSL_CERT_FILE", certPath)
+}
+
+func setupKMS(t *testing.T) {
+	t.Helper()
+
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+	ctx := context.Background()
+	g, err := gcpkms.NewClientWithOptions(ctx, keyURI, option.WithCredentialsFile(filepath.Join(srcDir, credFile)))
+	if err != nil {
+		t.Fatalf("error setting up GCP client: %v", err)
+	}
+	registry.RegisterKMSClient(g)
+}
+
+func basicAEADTest(t *testing.T, a tink.AEAD) error {
+	t.Helper()
+	for i := 0; i < 100; i++ {
+		pt := random.GetRandomBytes(20)
+		ad := random.GetRandomBytes(20)
+		ct, err := a.Encrypt(pt, ad)
+		if err != nil {
+			return err
+		}
+		dt, err := a.Decrypt(ct, ad)
+		if err != nil {
+			return err
+		}
+		if !bytes.Equal(dt, pt) {
+			return errors.New("decrypt not inverse of encrypt")
+		}
+	}
+	return nil
+}
+
+func TestBasicAead(t *testing.T) {
+	setupKMS(t)
+	dek := aead.AES128CTRHMACSHA256KeyTemplate()
+	template, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, dek)
+	if err != nil {
+		t.Fatalf("error creating key template: %v", err)
+	}
+	handle, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("error getting a new keyset handle: %v", err)
+	}
+	a, err := aead.New(handle)
+	if err != nil {
+		t.Fatalf("error getting the primitive: %v", err)
+	}
+	if err := basicAEADTest(t, a); err != nil {
+		t.Errorf("error in basic aead tests: %v", err)
+	}
+}
diff --git a/go/integration/hcvault/BUILD.bazel b/go/integration/hcvault/BUILD.bazel
index 087390a..f941fef 100644
--- a/go/integration/hcvault/BUILD.bazel
+++ b/go/integration/hcvault/BUILD.bazel
@@ -21,12 +21,13 @@
 go_test(
     name = "hcvault_test",
     srcs = [
+        "hcvault_aead_internal_test.go",
         "hcvault_aead_test.go",
         "hcvault_client_test.go",
     ],
     data = ["//integration/hcvault/testdata:server_tls_files"],
+    embed = [":hcvault"],
     deps = [
-        ":hcvault",
         "//aead",
         "//core/registry",
         "//keyset",
diff --git a/go/integration/hcvault/hcvault_aead.go b/go/integration/hcvault/hcvault_aead.go
index fffe4da..94bb910 100644
--- a/go/integration/hcvault/hcvault_aead.go
+++ b/go/integration/hcvault/hcvault_aead.go
@@ -19,45 +19,51 @@
 import (
 	"encoding/base64"
 	"errors"
-	"fmt"
-	"regexp"
+	"net/url"
 	"strings"
 
-	"github.com/hashicorp/vault/api"
 	"github.com/google/tink/go/tink"
+	"github.com/hashicorp/vault/api"
 )
 
 // vaultAEAD represents a HashiCorp Vault service to a particular URI.
 type vaultAEAD struct {
-	keyURI string
-	client *api.Logical
+	encKeyPath string
+	decKeyPath string
+	client     *api.Logical
 }
 
 var _ tink.AEAD = (*vaultAEAD)(nil)
 
+const (
+	encryptSegment = "encrypt"
+	decryptSegment = "decrypt"
+)
+
 // newHCVaultAEAD returns a new HashiCorp Vault service.
-func newHCVaultAEAD(keyURI string, client *api.Logical) tink.AEAD {
-	return &vaultAEAD{
-		keyURI: keyURI,
-		client: client,
+func newHCVaultAEAD(keyURI string, client *api.Logical) (tink.AEAD, error) {
+	encKeyPath, decKeyPath, err := getEndpointPaths(keyURI)
+	if err != nil {
+		return nil, err
 	}
+	return &vaultAEAD{
+		encKeyPath: encKeyPath,
+		decKeyPath: decKeyPath,
+		client:     client,
+	}, nil
 }
 
 // Encrypt encrypts the plaintext data using a key stored in HashiCorp Vault.
 // associatedData parameter is used as a context for key derivation, more
 // information available https://www.vaultproject.io/docs/secrets/transit/index.html.
 func (a *vaultAEAD) Encrypt(plaintext, associatedData []byte) ([]byte, error) {
-	encryptionPath, err := a.getEncryptionPath(a.keyURI)
-	if err != nil {
-		return nil, err
-	}
 	// Create an encryption request map according to Vault REST API:
 	// https://www.vaultproject.io/api/secret/transit/index.html#encrypt-data.
 	req := map[string]interface{}{
 		"plaintext": base64.StdEncoding.EncodeToString(plaintext),
 		"context":   base64.StdEncoding.EncodeToString(associatedData),
 	}
-	secret, err := a.client.Write(encryptionPath, req)
+	secret, err := a.client.Write(a.encKeyPath, req)
 	if err != nil {
 		return nil, err
 	}
@@ -69,17 +75,13 @@
 // associatedData parameter is used as a context for key derivation, more
 // information available https://www.vaultproject.io/docs/secrets/transit/index.html.
 func (a *vaultAEAD) Decrypt(ciphertext, associatedData []byte) ([]byte, error) {
-	decryptionPath, err := a.getDecryptionPath(a.keyURI)
-	if err != nil {
-		return nil, err
-	}
 	// Create a decryption request map according to Vault REST API:
 	// https://www.vaultproject.io/api/secret/transit/index.html#decrypt-data.
 	req := map[string]interface{}{
 		"ciphertext": string(ciphertext),
 		"context":    base64.StdEncoding.EncodeToString(associatedData),
 	}
-	secret, err := a.client.Write(decryptionPath, req)
+	secret, err := a.client.Write(a.decKeyPath, req)
 	if err != nil {
 		return nil, err
 	}
@@ -91,36 +93,30 @@
 	return plaintext, nil
 }
 
-// getEncryptionPath transforms keyURL to a Vault encryption path.
-// For example a keyURL "transit/keys/key-foo" will be transformed to "transit/encrypt/key-foo".
-func (a *vaultAEAD) getEncryptionPath(keyURL string) (string, error) {
-	key, err := a.extractKey(keyURL)
-	if err != nil {
-		return "", err
+// getEndpointPaths transforms keyURL into the Vault transit encrypt and decrypt
+// paths. The keyURL is expected to end in "/{mount}/keys/{keyName}". For
+// example, the keyURL "hcvault:///transit/keys/key-foo" will be transformed to
+// "transit/encrypt/key-foo" and "transit/decrypt/key-foo", and
+// "hcvault://my-vault.example.com/teams/billing/service/cipher/keys/key-bar"
+// will be transformed into
+// "hcvault://my-vault.example.com/teams/billing/service/cipher/encrypt/key-bar"
+// and
+// "hcvault://my-vault.example.com/teams/billing/service/cipher/decrypt/key-bar".
+func getEndpointPaths(keyURL string) (encryptPath, decryptPath string, err error) {
+	u, err := url.Parse(keyURL)
+	if err != nil || u.Scheme != "hcvault" {
+		return "", "", errors.New("malformed keyURL")
 	}
-	parts := strings.Split(key, "/")
-	parts[len(parts)-2] = "encrypt"
-	return strings.Join(parts, "/"), nil
-}
 
-// getDecryptionPath transforms keyURL to a Vault decryption path.
-// For example a keyURL "transit/keys/key-foo" will be transformed to "transit/decrypt/key-foo".
-func (a *vaultAEAD) getDecryptionPath(keyURL string) (string, error) {
-	key, err := a.extractKey(keyURL)
-	if err != nil {
-		return "", err
+	parts := strings.Split(u.EscapedPath(), "/")
+	length := len(parts)
+	if length < 4 || parts[length-2] != "keys" {
+		return "", "", errors.New("malformed keyURL")
 	}
-	parts := strings.Split(key, "/")
-	parts[len(parts)-2] = "decrypt"
-	return strings.Join(parts, "/"), nil
-}
 
-var vaultKeyRegex = regexp.MustCompile(fmt.Sprintf("^%s/*([a-zA-Z0-9.:]+)/(.*)$", vaultPrefix))
-
-func (a *vaultAEAD) extractKey(keyURL string) (string, error) {
-	m := vaultKeyRegex.FindAllStringSubmatch(keyURL, -1)
-	if m == nil {
-		return "", errors.New("malformed keyURL")
-	}
-	return m[0][2], nil
+	parts[length-2] = encryptSegment
+	encryptPath = strings.Join(parts[1:], "/")
+	parts[length-2] = decryptSegment
+	decryptPath = strings.Join(parts[1:], "/")
+	return encryptPath, decryptPath, nil
 }
diff --git a/go/integration/hcvault/hcvault_aead_internal_test.go b/go/integration/hcvault/hcvault_aead_internal_test.go
new file mode 100644
index 0000000..428c06f
--- /dev/null
+++ b/go/integration/hcvault/hcvault_aead_internal_test.go
@@ -0,0 +1,119 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package hcvault
+
+import (
+	"testing"
+)
+
+func TestGetEndpointPaths(t *testing.T) {
+	for _, tc := range []struct {
+		desc string
+		uri  string
+		enc  string
+		dec  string
+		err  string
+	}{
+		{
+			desc: "simple",
+			uri:  "hcvault://vault.example.com/transit/keys/foo",
+			enc:  "transit/encrypt/foo",
+			dec:  "transit/decrypt/foo",
+		},
+		{
+			desc: "escaped",
+			uri:  "hcvault://vault.example.com/transit/keys/this%2Band+that",
+			enc:  "transit/encrypt/this%2Band+that",
+			dec:  "transit/decrypt/this%2Band+that",
+		},
+		{
+			desc: "sub-path",
+			uri:  "hcvault://vault.example.com/teams/billing/something/transit/keys/pci-key",
+			enc:  "teams/billing/something/transit/encrypt/pci-key",
+			dec:  "teams/billing/something/transit/decrypt/pci-key",
+		},
+		{
+			desc: "transit-twice",
+			uri:  "hcvault://vault.example.com/transit/keys/something/transit/keys/my-key",
+			enc:  "transit/keys/something/transit/encrypt/my-key",
+			dec:  "transit/keys/something/transit/decrypt/my-key",
+		},
+		{
+			desc: "hyphen-host",
+			uri:  "hcvault://vault-prd.example.com/transit/keys/hi",
+			enc:  "transit/encrypt/hi",
+			dec:  "transit/decrypt/hi",
+		},
+		{
+			desc: "no-host",
+			uri:  "hcvault:///transit/keys/hi",
+			enc:  "transit/encrypt/hi",
+			dec:  "transit/decrypt/hi",
+		},
+		{
+			desc: "mount-not-named-transit",
+			uri:  "hcvault:///cipher/keys/hi",
+			enc:  "cipher/encrypt/hi",
+			dec:  "cipher/decrypt/hi",
+		},
+		{
+			desc: "http",
+			uri:  "http://vault.com/hi",
+			err:  "malformed keyURL",
+		},
+		{
+			desc: "no-path",
+			uri:  "hcvault://vault.com",
+			err:  "malformed keyURL",
+		},
+		{
+			desc: "slash-only",
+			uri:  "hcvault://vault.com/",
+			err:  "malformed keyURL",
+		},
+		{
+			desc: "not-transit",
+			uri:  "hcvault://vault.example.com/foo/bar/baz",
+			err:  "malformed keyURL",
+		},
+		{
+			desc: "not-end-of-path",
+			uri:  "hcvault://vault.example.com/transit/keys/bar/baz",
+			err:  "malformed keyURL",
+		},
+	} {
+		t.Run(tc.desc, func(t *testing.T) {
+			encPath, decPath, err := getEndpointPaths(tc.uri)
+			if err == nil {
+				if tc.err != "" {
+					t.Errorf("getEndpointPaths(%q) err is nil, want %q", tc.uri, tc.err)
+				}
+			} else {
+				if tc.err != err.Error() {
+					t.Errorf("getEndpointPaths(%q) err = %v; want %q", tc.uri, err, tc.err)
+				}
+			}
+
+			if encPath != tc.enc {
+				t.Errorf("getEndpointPaths(%q) encryptPath = %q, want %q", tc.uri, encPath, tc.enc)
+			}
+			if decPath != tc.dec {
+				t.Errorf("getEndpointPaths(%q) decryptPath = %q, want %q", tc.uri, decPath, tc.dec)
+			}
+		})
+	}
+}
diff --git a/go/integration/hcvault/hcvault_aead_test.go b/go/integration/hcvault/hcvault_aead_test.go
index af79790..1d87b8f 100644
--- a/go/integration/hcvault/hcvault_aead_test.go
+++ b/go/integration/hcvault/hcvault_aead_test.go
@@ -4,7 +4,7 @@
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
 //
-//      http://www.apache.org/licenses/LICENSE-2.0
+//	http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //
-////////////////////////////////////////////////////////////////////////////////
+// //////////////////////////////////////////////////////////////////////////////
 package hcvault_test
 
 import (
@@ -48,7 +48,8 @@
 
 	client, err := hcvault.NewClient(
 		fmt.Sprintf("hcvault://localhost:%d/", port),
-		&tls.Config{InsecureSkipVerify: true},
+		// Using InsecureSkipVerify is fine here, since this is just a test running locally.
+		&tls.Config{InsecureSkipVerify: true}, // NOLINT
 		token,
 	)
 	if err != nil {
@@ -78,7 +79,8 @@
 
 	client, err := hcvault.NewClient(
 		fmt.Sprintf("hcvault://localhost:%d/", port),
-		&tls.Config{InsecureSkipVerify: true},
+		// Using InsecureSkipVerify is fine here, since this is just a test running locally.
+		&tls.Config{InsecureSkipVerify: true}, // NOLINT
 		token,
 	)
 	if err != nil {
@@ -102,6 +104,49 @@
 	}
 }
 
+func TestGetAEADFailWithBadKeyURI(t *testing.T) {
+	port, stopFunc := newServer(t)
+	defer stopFunc()
+
+	client, err := hcvault.NewClient(
+		fmt.Sprintf("hcvault://localhost:%d/", port),
+		// Using InsecureSkipVerify is fine here, since this is just a test running locally.
+		&tls.Config{InsecureSkipVerify: true}, // NOLINT
+		token,
+	)
+	if err != nil {
+		t.Fatalf("hcvault.NewClient() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name   string
+		keyURI string
+	}{
+		{
+			name:   "empty",
+			keyURI: fmt.Sprintf("hcvault://localhost:%d/", port),
+		},
+		{
+			name:   "without slash",
+			keyURI: fmt.Sprintf("hcvault://localhost:%d/badKeyUri", port),
+		},
+		{
+			name:   "with one slash",
+			keyURI: fmt.Sprintf("hcvault://localhost:%d/bad/KeyUri", port),
+		},
+		{
+			name:   "with three slash",
+			keyURI: fmt.Sprintf("hcvault://localhost:%d/one/two/three/four", port),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := client.GetAEAD(test.keyURI); err == nil {
+				t.Errorf("client.GetAEAD(%q) err = nil, want error", test.keyURI)
+			}
+		})
+	}
+}
+
 type closeFunc func() error
 
 func newServer(t *testing.T) (int, closeFunc) {
diff --git a/go/integration/hcvault/hcvault_client.go b/go/integration/hcvault/hcvault_client.go
index 5e5d31d..536ac29 100644
--- a/go/integration/hcvault/hcvault_client.go
+++ b/go/integration/hcvault/hcvault_client.go
@@ -14,7 +14,9 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-// Package hcvault provides integration with the HashiCorp Vault (https://www.vaultproject.io/).
+// Package hcvault provides integration with the [HashiCorp Vault].
+//
+//	[HashiCorp Vault]: https://www.vaultproject.io/.
 package hcvault
 
 import (
@@ -25,9 +27,9 @@
 	"net/url"
 	"strings"
 
-	"github.com/hashicorp/vault/api"
 	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/tink"
+	"github.com/hashicorp/vault/api"
 )
 
 const (
@@ -97,5 +99,5 @@
 		return nil, errors.New("unsupported keyURI")
 	}
 
-	return newHCVaultAEAD(keyURI, c.client), nil
+	return newHCVaultAEAD(keyURI, c.client)
 }
diff --git a/go/integration/hcvault/hcvault_client_test.go b/go/integration/hcvault/hcvault_client_test.go
index d3fc7dc..e9e8376 100644
--- a/go/integration/hcvault/hcvault_client_test.go
+++ b/go/integration/hcvault/hcvault_client_test.go
@@ -36,11 +36,15 @@
 	registry.RegisterKMSClient(vaultClient)
 
 	dek := aead.AES128CTRHMACSHA256KeyTemplate()
-	kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(keyURI, dek))
+	template, err := aead.CreateKMSEnvelopeAEADKeyTemplate(keyURI, dek)
 	if err != nil {
 		log.Fatal(err)
 	}
-	a, err := aead.New(kh)
+	handle, err := keyset.NewHandle(template)
+	if err != nil {
+		log.Fatal(err)
+	}
+	a, err := aead.New(handle)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/go/internal/BUILD.bazel b/go/internal/BUILD.bazel
index 6c86976..d7fa787 100644
--- a/go/internal/BUILD.bazel
+++ b/go/internal/BUILD.bazel
@@ -10,6 +10,7 @@
     importpath = "github.com/google/tink/go/internal",
     visibility = [
         "//insecurecleartextkeyset:__pkg__",
+        "//keyderivation:__pkg__",
         "//keyset:__pkg__",
         "//testkeyset:__pkg__",
     ],
diff --git a/go/internal/aead/aead_test.go b/go/internal/aead/aead_test.go
index 4ed0880..9d993e6 100644
--- a/go/internal/aead/aead_test.go
+++ b/go/internal/aead/aead_test.go
@@ -30,7 +30,7 @@
 
 type AEADGroup struct {
 	testutil.WycheproofGroup
-	IvSize  uint32      `json:"ivSize"`
+	IVSize  uint32      `json:"ivSize"`
 	KeySize uint32      `json:"keySize"`
 	TagSize uint32      `json:"tagSize"`
 	Type    string      `json:"type"`
@@ -39,12 +39,12 @@
 
 type AEADCase struct {
 	testutil.WycheproofCase
-	Aad testutil.HexBytes `json:"aad"`
-	Ct  testutil.HexBytes `json:"ct"`
-	Iv  testutil.HexBytes `json:"iv"`
-	Key testutil.HexBytes `json:"key"`
-	Msg testutil.HexBytes `json:"msg"`
-	Tag testutil.HexBytes `json:"tag"`
+	AD      testutil.HexBytes `json:"aad"`
+	CT      testutil.HexBytes `json:"ct"`
+	IV      testutil.HexBytes `json:"iv"`
+	Key     testutil.HexBytes `json:"key"`
+	Message testutil.HexBytes `json:"msg"`
+	Tag     testutil.HexBytes `json:"tag"`
 }
 
 func TestValidateAESKeySize(t *testing.T) {
diff --git a/go/internal/aead/aes_gcm_insecure_iv.go b/go/internal/aead/aes_gcm_insecure_iv.go
index b5ee40f..a9241b6 100644
--- a/go/internal/aead/aes_gcm_insecure_iv.go
+++ b/go/internal/aead/aes_gcm_insecure_iv.go
@@ -88,12 +88,14 @@
 	if err != nil {
 		return nil, err
 	}
-	ciphertext := cipher.Seal(nil, iv, plaintext, associatedData)
-
-	if i.prependIV {
-		return append(iv, ciphertext...), nil
+	if !i.prependIV {
+		return cipher.Seal(nil, iv, plaintext, associatedData), nil
 	}
-	return ciphertext, nil
+	// Make the capacity of dst large enough so that both the IV and the ciphertext fit inside.
+	dst := make([]byte, 0, AESGCMIVSize+len(plaintext)+cipher.Overhead())
+	dst = append(dst, iv...)
+	// Seal appends the ciphertext to dst. So the final output is: iv || ciphertext.
+	return cipher.Seal(dst, iv, plaintext, associatedData), nil
 }
 
 // Decrypt decrypts ciphertext with iv as the initialization vector and
@@ -101,10 +103,12 @@
 //
 // If prependIV is true, the iv argument and the first AESGCMIVSize bytes of
 // ciphertext must be equal. The ciphertext argument is as follows:
-//     | iv | actual ciphertext | tag |
+//
+//	| iv | actual ciphertext | tag |
 //
 // If false, the ciphertext argument is as follows:
-//     | actual ciphertext | tag |
+//
+//	| actual ciphertext | tag |
 func (i *AESGCMInsecureIV) Decrypt(iv, ciphertext, associatedData []byte) ([]byte, error) {
 	if len(iv) != AESGCMIVSize {
 		return nil, fmt.Errorf("unexpected IV size: got %d, want %d", len(iv), AESGCMIVSize)
diff --git a/go/internal/aead/aes_gcm_insecure_iv_test.go b/go/internal/aead/aes_gcm_insecure_iv_test.go
index a4989b3..1f935f2 100644
--- a/go/internal/aead/aes_gcm_insecure_iv_test.go
+++ b/go/internal/aead/aes_gcm_insecure_iv_test.go
@@ -232,34 +232,10 @@
 	}
 }
 
-type aeadSuite struct {
-	testutil.WycheproofSuite
-	TestGroups []*aeadGroup `json:"testGroups"`
-}
-
-type aeadGroup struct {
-	testutil.WycheproofGroup
-	IVSize  uint32      `json:"ivSize"`
-	KeySize uint32      `json:"keySize"`
-	TagSize uint32      `json:"tagSize"`
-	Type    string      `json:"type"`
-	Tests   []*aeadCase `json:"tests"`
-}
-
-type aeadCase struct {
-	testutil.WycheproofCase
-	AD      testutil.HexBytes `json:"aad"`
-	CT      testutil.HexBytes `json:"ct"`
-	IV      testutil.HexBytes `json:"iv"`
-	Key     testutil.HexBytes `json:"key"`
-	Message testutil.HexBytes `json:"msg"`
-	Tag     testutil.HexBytes `json:"tag"`
-}
-
 func TestAESGCMInsecureIVWycheproofVectors(t *testing.T) {
 	testutil.SkipTestIfTestSrcDirIsNotSet(t)
 
-	suite := new(aeadSuite)
+	suite := new(AEADSuite)
 	if err := testutil.PopulateSuite(suite, "aes_gcm_test.json"); err != nil {
 		t.Fatalf("failed to populate suite: %s", err)
 	}
@@ -271,7 +247,7 @@
 			continue
 		}
 		for _, tc := range group.Tests {
-			name := fmt.Sprintf("%s-%s(%d,%d)/Case-%d", suite.Algorithm, group.Type, group.KeySize, group.TagSize, tc.CaseID)
+			name := fmt.Sprintf("%s-%s(%d,%d):Case-%d", suite.Algorithm, group.Type, group.KeySize, group.TagSize, tc.CaseID)
 			t.Run(name, func(t *testing.T) {
 				a, err := aead.NewAESGCMInsecureIV(tc.Key, false /*=prependIV*/)
 				if err != nil {
@@ -299,3 +275,26 @@
 		}
 	}
 }
+
+func TestPreallocatedCiphertextMemoryIsExact(t *testing.T) {
+	key := random.GetRandomBytes(16)
+	a, err := aead.NewAESGCMInsecureIV(key, true /*=prependIV*/)
+	if err != nil {
+		t.Fatalf("aead.NewAESGCMInsecureIV() err = %v, want nil", err)
+	}
+	iv := random.GetRandomBytes(aead.AESGCMIVSize)
+	plaintext := random.GetRandomBytes(13)
+	associatedData := random.GetRandomBytes(17)
+
+	ciphertext, err := a.Encrypt(iv, plaintext, associatedData)
+	if err != nil {
+		t.Fatalf("a.Encrypt() err = %v, want nil", err)
+	}
+  // Encrypt() uses cipher.Overhead() to pre-allocate the memory needed store the ciphertext.
+	// For AES GCM, the size of the allocated memory should always be exact. If this check fails, the
+	// pre-allocated memory was too large or too small. If it was too small, the system had to
+	// re-allocate more memory, which is expensive and should be avoided.
+	if len(ciphertext) != cap(ciphertext) {
+		t.Errorf("want len(ciphertext) == cap(ciphertext), got %d != %d", len(ciphertext), cap(ciphertext))
+	}
+}
diff --git a/go/internal/aead/chacha20poly1305_insecure_nonce_test.go b/go/internal/aead/chacha20poly1305_insecure_nonce_test.go
index 163a151..9540bff 100644
--- a/go/internal/aead/chacha20poly1305_insecure_nonce_test.go
+++ b/go/internal/aead/chacha20poly1305_insecure_nonce_test.go
@@ -197,7 +197,7 @@
 		if group.KeySize/8 != chacha20poly1305.KeySize {
 			continue
 		}
-		if group.IvSize/8 != chacha20poly1305.NonceSize {
+		if group.IVSize/8 != chacha20poly1305.NonceSize {
 			continue
 		}
 
@@ -215,13 +215,13 @@
 	}
 
 	nonce := random.GetRandomBytes(chacha20poly1305.NonceSize)
-	_, err = ca.Encrypt(nonce, tc.Msg, tc.Aad)
+	_, err = ca.Encrypt(nonce, tc.Message, tc.AD)
 	if err != nil {
 		t.Fatalf("unexpected encryption error: %s", err)
 	}
 
-	ct := append(tc.Ct, tc.Tag...)
-	decrypted, err := ca.Decrypt(tc.Iv, ct, tc.Aad)
+	ct := append(tc.CT, tc.Tag...)
+	decrypted, err := ca.Decrypt(tc.IV, ct, tc.AD)
 	if err != nil {
 		if tc.Result == "valid" {
 			t.Errorf("unexpected error: %s", err)
@@ -230,7 +230,7 @@
 		if tc.Result == "invalid" {
 			t.Error("decrypted invalid")
 		}
-		if !bytes.Equal(decrypted, tc.Msg) {
+		if !bytes.Equal(decrypted, tc.Message) {
 			t.Error("incorrect decryption")
 		}
 	}
diff --git a/go/internal/internalregistry/BUILD.bazel b/go/internal/internalregistry/BUILD.bazel
index 0396edf..f461b39 100644
--- a/go/internal/internalregistry/BUILD.bazel
+++ b/go/internal/internalregistry/BUILD.bazel
@@ -6,17 +6,36 @@
 
 go_library(
     name = "internalregistry",
-    srcs = ["internal_registry.go"],
+    srcs = [
+        "derivable_key_manager.go",
+        "internal_registry.go",
+        "key_derivation.go",
+    ],
     importpath = "github.com/google/tink/go/internal/internalregistry",
-    deps = ["//monitoring"],
+    deps = [
+        "//core/registry",
+        "//monitoring",
+        "//proto/tink_go_proto",
+        "@org_golang_google_protobuf//proto",
+    ],
 )
 
 go_test(
     name = "internalregistry_test",
-    srcs = ["internal_registry_test.go"],
+    srcs = [
+        "internal_registry_test.go",
+        "key_derivation_test.go",
+    ],
     deps = [
         ":internalregistry",
+        "//aead",
+        "//core/registry",
+        "//internal/testing/stubkeymanager",
+        "//proto/aes_gcm_go_proto",
+        "//proto/tink_go_proto",
+        "//subtle/random",
         "//testing/fakemonitoring",
+        "@org_golang_google_protobuf//proto",
     ],
 )
 
diff --git a/go/internal/internalregistry/derivable_key_manager.go b/go/internal/internalregistry/derivable_key_manager.go
new file mode 100644
index 0000000..f069884
--- /dev/null
+++ b/go/internal/internalregistry/derivable_key_manager.go
@@ -0,0 +1,40 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package internalregistry
+
+import (
+	"io"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// DerivableKeyManager is a special type of KeyManager that can derive new keys.
+type DerivableKeyManager interface {
+	registry.KeyManager
+
+	// KeyMaterialType returns the key material type of the key manager.
+	KeyMaterialType() tinkpb.KeyData_KeyMaterialType
+
+	// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+	//
+	// Note: The given parameter pseudorandomness may only produce a finite amount
+	// of randomness. Implementions must obtain the pseudorandom bytes needed
+	// before producing the key.
+	DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error)
+}
diff --git a/go/internal/internalregistry/key_derivation.go b/go/internal/internalregistry/key_derivation.go
new file mode 100644
index 0000000..6fe509b
--- /dev/null
+++ b/go/internal/internalregistry/key_derivation.go
@@ -0,0 +1,95 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package internalregistry
+
+import (
+	"fmt"
+	"io"
+	"sync"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+var (
+	derivableKeyManagersMu sync.RWMutex
+
+	// derivableKeyManagers is the set of all key managers allowed to derive keys.
+	// It is keyed by the key manager's type URL, i.e. typeURL -> true. All type
+	// URLs in this map correspond to key managers that are
+	//   - in the registry and
+	//   - implement key derivation.
+	//
+	// This exists because of Golang's weak type system and the desire to keep key
+	// derivation non-public. If we do not explicitly restrict derivable key
+	// managers, users would be able to register any custom key manager that
+	// implements DeriveKey() and be able to derive keys with it, even without
+	// access to this library, internalregistry.
+	derivableKeyManagers = make(map[string]bool)
+)
+
+// AllowKeyDerivation adds the type URL to derivableKeyManagers if the
+// corresponding key manager is in the registry and implements key derivation.
+func AllowKeyDerivation(typeURL string) error {
+	km, err := registry.GetKeyManager(typeURL)
+	if err != nil {
+		return err
+	}
+	if _, ok := km.(DerivableKeyManager); !ok {
+		return fmt.Errorf("key manager for type %s does not implement key derivation", typeURL)
+	}
+	derivableKeyManagersMu.Lock()
+	derivableKeyManagers[typeURL] = true
+	derivableKeyManagersMu.Unlock()
+	return nil
+}
+
+// CanDeriveKeys returns true if typeURL is in derivableKeyManagers.
+func CanDeriveKeys(typeURL string) bool {
+	derivableKeyManagersMu.Lock()
+	defer derivableKeyManagersMu.Unlock()
+	return derivableKeyManagers[typeURL]
+}
+
+// DeriveKey derives a new key from template and pseudorandomness.
+func DeriveKey(keyTemplate *tinkpb.KeyTemplate, pseudorandomness io.Reader) (*tinkpb.KeyData, error) {
+	if !CanDeriveKeys(keyTemplate.GetTypeUrl()) {
+		return nil, fmt.Errorf("key manager for type %s is not allowed to derive keys", keyTemplate.GetTypeUrl())
+	}
+	km, err := registry.GetKeyManager(keyTemplate.GetTypeUrl())
+	if err != nil {
+		return nil, err
+	}
+	keyManager, ok := km.(DerivableKeyManager)
+	if !ok {
+		return nil, fmt.Errorf("key manager for type %s does not implement key derivation", keyTemplate.GetTypeUrl())
+	}
+	key, err := keyManager.DeriveKey(keyTemplate.GetValue(), pseudorandomness)
+	if err != nil {
+		return nil, err
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, fmt.Errorf("failed to serialize derived key: %v", err)
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         keyTemplate.GetTypeUrl(),
+		Value:           serializedKey,
+		KeyMaterialType: keyManager.KeyMaterialType(),
+	}, nil
+}
diff --git a/go/internal/internalregistry/key_derivation_test.go b/go/internal/internalregistry/key_derivation_test.go
new file mode 100644
index 0000000..ae0fb9e
--- /dev/null
+++ b/go/internal/internalregistry/key_derivation_test.go
@@ -0,0 +1,199 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package internalregistry_test
+
+import (
+	"bytes"
+	"errors"
+	"sync"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
+	"github.com/google/tink/go/subtle/random"
+	gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	typeURLRoot           = "TestDeriveKeyFails"
+	unregisteredKMTypeURL = typeURLRoot + "UnregisteredKeyManager"
+	notDerivableKMTypeURL = typeURLRoot + "NotDerivableKeyManager"
+	failingKMTypeURL      = typeURLRoot + "FailingKeyManager"
+)
+
+var once sync.Once
+
+func mustRegisterBadKeyManagers(t *testing.T) {
+	t.Helper()
+	// The registry does not allow a key manager to be registered more than once.
+	once.Do(func() {
+		notDerivableKM := &stubkeymanager.StubKeyManager{URL: notDerivableKMTypeURL}
+		if err := registry.RegisterKeyManager(notDerivableKM); err != nil {
+			t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+		}
+
+		failingKM := &stubkeymanager.StubDerivableKeyManager{
+			StubKeyManager: stubkeymanager.StubKeyManager{
+				URL: failingKMTypeURL,
+			},
+			DerErr: errors.New("failing"),
+		}
+		if err := registry.RegisterKeyManager(failingKM); err != nil {
+			t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+		}
+	})
+}
+
+func TestDerivableKeyManagers(t *testing.T) {
+	mustRegisterBadKeyManagers(t)
+	for _, typeURL := range []string{
+		aead.AES128GCMKeyTemplate().GetTypeUrl(),
+		aead.AES256GCMKeyTemplate().GetTypeUrl(),
+		failingKMTypeURL,
+	} {
+		t.Run(typeURL, func(t *testing.T) {
+			if err := internalregistry.AllowKeyDerivation(typeURL); err != nil {
+				t.Fatalf("internalregistry.AllowKeyDerivation() err = %v, want nil", err)
+			}
+			if !internalregistry.CanDeriveKeys(typeURL) {
+				t.Errorf("internalregistry.CanDeriveKeys() = false, want true")
+			}
+		})
+	}
+}
+
+func TestDerivableKeyManagersRejectsInvalidInputs(t *testing.T) {
+	mustRegisterBadKeyManagers(t)
+	for _, typeURL := range []string{
+		"",
+		unregisteredKMTypeURL,
+		notDerivableKMTypeURL,
+	} {
+		t.Run(typeURL, func(t *testing.T) {
+			if err := internalregistry.AllowKeyDerivation(typeURL); err == nil {
+				t.Error("internalregistry.AllowKeyDerivation() err = nil, want non-nil")
+			}
+			if internalregistry.CanDeriveKeys(typeURL) {
+				t.Errorf("internalregistry.CanDeriveKeys() = true, want false")
+			}
+		})
+	}
+}
+
+func TestDeriveKey(t *testing.T) {
+	for _, test := range []struct {
+		name            string
+		keyTemplate     *tinkpb.KeyTemplate
+		keySize         uint32
+		keyMaterialType tinkpb.KeyData_KeyMaterialType
+	}{
+		{
+			name:            "AES-128-GCM",
+			keyTemplate:     aead.AES128GCMKeyTemplate(),
+			keySize:         16,
+			keyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		},
+		{
+			name:            "AES-256-GCM",
+			keyTemplate:     aead.AES256GCMKeyTemplate(),
+			keySize:         32,
+			keyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.keySize))
+			keyData, err := internalregistry.DeriveKey(test.keyTemplate, buf)
+			if err != nil {
+				t.Fatalf("internalregistry.DeriveKey() err = %v, want nil", err)
+			}
+			if got, want := keyData.GetTypeUrl(), test.keyTemplate.GetTypeUrl(); got != want {
+				t.Errorf("TypeUrl = %s, want %s", got, want)
+			}
+			key := &gcmpb.AesGcmKey{}
+			if err := proto.Unmarshal(keyData.GetValue(), key); err != nil {
+				t.Errorf("proto.Unmarshal() err = %v, want nil", err)
+			}
+			if got, want := len(key.GetKeyValue()), int(test.keySize); got != want {
+				t.Errorf("len(KeyValue) = %d, want %d", got, want)
+			}
+			if got, want := keyData.GetKeyMaterialType(), test.keyMaterialType; got != want {
+				t.Errorf("KeyMaterialType = %s, want %s", got, want)
+			}
+		})
+	}
+}
+
+func TestDeriveKeyFails(t *testing.T) {
+	mustRegisterBadKeyManagers(t)
+	rand := random.GetRandomBytes(32)
+	for _, test := range []struct {
+		name        string
+		keyTemplate *tinkpb.KeyTemplate
+		randLen     uint32
+	}{
+		{
+			name:        "not enough randomness",
+			keyTemplate: aead.AES128GCMKeyTemplate(),
+			randLen:     15},
+		{
+			name:    "nil key template",
+			randLen: 32},
+		{
+			name:        "derivation-disallowed but registered key manager",
+			keyTemplate: aead.AES128CTRHMACSHA256KeyTemplate(),
+			randLen:     32,
+		},
+		{
+			name: "derivation-allowed but unregistered key manager",
+			keyTemplate: &tinkpb.KeyTemplate{
+				TypeUrl:          unregisteredKMTypeURL,
+				Value:            rand,
+				OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+			},
+			randLen: 32,
+		},
+		{
+			"does not implement DerivableKeyManager",
+			&tinkpb.KeyTemplate{
+				TypeUrl:          notDerivableKMTypeURL,
+				Value:            rand,
+				OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+			},
+			32,
+		},
+		{
+			"key manager with failing DeriveKey()",
+			&tinkpb.KeyTemplate{
+				TypeUrl:          failingKMTypeURL,
+				Value:            rand,
+				OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+			},
+			32,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.randLen))
+			if _, err := internalregistry.DeriveKey(test.keyTemplate, buf); err == nil {
+				t.Error("internalregistry.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
diff --git a/go/internal/monitoringutil/monitoring_util.go b/go/internal/monitoringutil/monitoring_util.go
index 35390ce..ff147f7 100644
--- a/go/internal/monitoringutil/monitoring_util.go
+++ b/go/internal/monitoringutil/monitoring_util.go
@@ -19,12 +19,26 @@
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/google/tink/go/core/primitiveset"
 	"github.com/google/tink/go/monitoring"
 	tpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
+const keytypeURLPrefix = "type.googleapis.com/google.crypto."
+
+// DoNothingLogger is a Logger that does nothing when invoked.
+type DoNothingLogger struct{}
+
+var _ monitoring.Logger = (*DoNothingLogger)(nil)
+
+// Log drops a log call.
+func (l *DoNothingLogger) Log(uint32, int) {}
+
+// LogFailure drops a failure call.
+func (l *DoNothingLogger) LogFailure() {}
+
 func keyStatusFromProto(status tpb.KeyStatusType) (monitoring.KeyStatus, error) {
 	var keyStatus monitoring.KeyStatus = 55
 	switch status {
@@ -41,6 +55,10 @@
 
 }
 
+func parseKeyTypeURL(ktu string) string {
+	return strings.TrimPrefix(ktu, keytypeURLPrefix)
+}
+
 // KeysetInfoFromPrimitiveSet creates a `KeysetInfo` from a `PrimitiveSet`.
 // This function doesn't guarantee to preserve the ordering of the keys in the keyset.
 func KeysetInfoFromPrimitiveSet(ps *primitiveset.PrimitiveSet) (*monitoring.KeysetInfo, error) {
@@ -61,15 +79,16 @@
 				return nil, err
 			}
 			e := &monitoring.Entry{
-				KeyID:          pe.KeyID,
-				Status:         keyStatus,
-				FormatAsString: pe.TypeURL, // TODO(b/225071831): populate FormatAsString with key format when available.
+				KeyID:     pe.KeyID,
+				Status:    keyStatus,
+				KeyType:   parseKeyTypeURL(pe.TypeURL),
+				KeyPrefix: pe.PrefixType.String(),
 			}
 			entries = append(entries, e)
 		}
 	}
 	return &monitoring.KeysetInfo{
-		Annotations:  make(map[string]string), // TODO(b/225071831): propagate annotations to monitoring set.
+		Annotations:  ps.Annotations,
 		PrimaryKeyID: ps.Primary.KeyID,
 		Entries:      entries,
 	}, nil
diff --git a/go/internal/monitoringutil/monitoring_util_test.go b/go/internal/monitoringutil/monitoring_util_test.go
index 89e9359..adf4616 100644
--- a/go/internal/monitoringutil/monitoring_util_test.go
+++ b/go/internal/monitoringutil/monitoring_util_test.go
@@ -86,45 +86,58 @@
 		Primary: &primitiveset.Entry{
 			KeyID: 1,
 		},
+		Annotations: map[string]string{
+			"foo": "bar",
+			"zoo": "far",
+		},
 		Entries: map[string][]*primitiveset.Entry{
 			// Adding all entries under the same prefix to get deterministic output.
 			"one": []*primitiveset.Entry{
 				&primitiveset.Entry{
-					KeyID:   1,
-					Status:  tpb.KeyStatusType_ENABLED,
-					TypeURL: "type.googleapis.com/google.crypto.tink.AesSivKey",
+					KeyID:      1,
+					Status:     tpb.KeyStatusType_ENABLED,
+					TypeURL:    "type.googleapis.com/google.crypto.tink.AesSivKey",
+					PrefixType: tpb.OutputPrefixType_TINK,
 				},
 				&primitiveset.Entry{
-					KeyID:   2,
-					Status:  tpb.KeyStatusType_DISABLED,
-					TypeURL: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+					KeyID:      2,
+					Status:     tpb.KeyStatusType_DISABLED,
+					TypeURL:    "type.googleapis.com/google.crypto.tink.AesGcmKey",
+					PrefixType: tpb.OutputPrefixType_TINK,
 				},
 				&primitiveset.Entry{
-					KeyID:   3,
-					Status:  tpb.KeyStatusType_DESTROYED,
-					TypeURL: "type.googleapis.com/google.crypto.tink.AesCtrHmacKey",
+					KeyID:      3,
+					Status:     tpb.KeyStatusType_DESTROYED,
+					TypeURL:    "type.googleapis.com/google.crypto.tink.AesCtrHmacKey",
+					PrefixType: tpb.OutputPrefixType_TINK,
 				},
 			},
 		},
 	}
 	want := &monitoring.KeysetInfo{
 		PrimaryKeyID: 1,
-		Annotations:  make(map[string]string),
+		Annotations: map[string]string{
+			"foo": "bar",
+			"zoo": "far",
+		},
 		Entries: []*monitoring.Entry{
 			{
-				KeyID:          1,
-				Status:         monitoring.Enabled,
-				FormatAsString: "type.googleapis.com/google.crypto.tink.AesSivKey",
+				KeyID:     1,
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.AesSivKey",
+				KeyPrefix: "TINK",
 			},
 			{
-				KeyID:          2,
-				Status:         monitoring.Disabled,
-				FormatAsString: "type.googleapis.com/google.crypto.tink.AesGcmKey",
+				KeyID:     2,
+				Status:    monitoring.Disabled,
+				KeyType:   "tink.AesGcmKey",
+				KeyPrefix: "TINK",
 			},
 			{
-				KeyID:          3,
-				Status:         monitoring.Destroyed,
-				FormatAsString: "type.googleapis.com/google.crypto.tink.AesCtrHmacKey",
+				KeyID:     3,
+				Status:    monitoring.Destroyed,
+				KeyType:   "tink.AesCtrHmacKey",
+				KeyPrefix: "TINK",
 			},
 		},
 	}
diff --git a/go/internal/signature/BUILD.bazel b/go/internal/signature/BUILD.bazel
new file mode 100644
index 0000000..209bc4a
--- /dev/null
+++ b/go/internal/signature/BUILD.bazel
@@ -0,0 +1,43 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "signature",
+    srcs = [
+        "rsa.go",
+        "rsassapkcs1_signer.go",
+        "rsassapkcs1_verifier.go",
+        "rsassapss_signer.go",
+        "rsassapss_verifier.go",
+        "signature.go",
+    ],
+    importpath = "github.com/google/tink/go/internal/signature",
+    visibility = ["//:__subpackages__"],
+    deps = [
+        "//subtle",
+        "//tink",
+    ],
+)
+
+go_test(
+    name = "signature_test",
+    srcs = [
+        "rsa_test.go",
+        "rsassapkcs1_signer_verifier_test.go",
+        "rsassapss_signer_verifier_test.go",
+    ],
+    data = [
+        "@wycheproof//testvectors:all",
+    ],
+    deps = [
+        ":signature",
+        "//subtle",
+        "//subtle/random",
+        "//testutil",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":signature",
+    visibility = ["//:__subpackages__"],
+)
diff --git a/go/internal/signature/rsa.go b/go/internal/signature/rsa.go
new file mode 100644
index 0000000..6ac9cbd
--- /dev/null
+++ b/go/internal/signature/rsa.go
@@ -0,0 +1,143 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto"
+	"crypto/rsa"
+	"fmt"
+	"hash"
+
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/tink"
+)
+
+const (
+	rsaMinModulusSizeInBits  = 2048
+	rsaDefaultPublicExponent = 65537
+)
+
+// RSAValidModulusSizeInBits the size in bits for an RSA key.
+func RSAValidModulusSizeInBits(m int) error {
+	if m < rsaMinModulusSizeInBits {
+		return fmt.Errorf("modulus size too small, must be >= %d", rsaMinModulusSizeInBits)
+	}
+	return nil
+}
+
+// RSAValidPublicExponent validates a public RSA exponent.
+func RSAValidPublicExponent(e int) error {
+	// crypto/rsa uses the following hardcoded public exponent value.
+	if e != rsaDefaultPublicExponent {
+		return fmt.Errorf("invalid public exponent")
+	}
+	return nil
+}
+
+// HashSafeForSignature checks whether a hash function is safe to use with digital signatures
+// that require collision resistance.
+func HashSafeForSignature(hashAlg string) error {
+	switch hashAlg {
+	case "SHA256", "SHA384", "SHA512":
+		return nil
+	default:
+		return fmt.Errorf("hash function not safe for digital signatures: %q", hashAlg)
+	}
+}
+
+const (
+	testMsg          = "Tink and Wycheproof."
+	signVerifyErrMsg = "signing with private key followed by verifying with public key failed, the key may be corrupted"
+)
+
+// Validate_RSA_SSA_PKCS1 validates that the corresponding private key is valid by signing and verifying a message.
+func Validate_RSA_SSA_PKCS1(hashAlg string, privKey *rsa.PrivateKey) error {
+	signer, err := New_RSA_SSA_PKCS1_Signer(hashAlg, privKey)
+	if err != nil {
+		return err
+	}
+	verifier, err := New_RSA_SSA_PKCS1_Verifier(hashAlg, &privKey.PublicKey)
+	if err != nil {
+		return err
+	}
+	if err := validateSignerVerifier(signer, verifier); err != nil {
+		return fmt.Errorf("RSA-SSA-PKCS1: %q", signVerifyErrMsg)
+	}
+	return nil
+}
+
+// Validate_RSA_SSA_PSS validates that the corresponding private key is valid by signing and verifying a message.
+func Validate_RSA_SSA_PSS(hashAlg string, saltLen int, privKey *rsa.PrivateKey) error {
+	signer, err := New_RSA_SSA_PSS_Signer(hashAlg, saltLen, privKey)
+	if err != nil {
+		return err
+	}
+	verifier, err := New_RSA_SSA_PSS_Verifier(hashAlg, saltLen, &privKey.PublicKey)
+	if err != nil {
+		return err
+	}
+	if err := validateSignerVerifier(signer, verifier); err != nil {
+		return fmt.Errorf("RSA-SSA-PSS: %q", signVerifyErrMsg)
+	}
+	return nil
+}
+
+func validateSignerVerifier(signer tink.Signer, verifier tink.Verifier) error {
+	signature, err := signer.Sign([]byte(testMsg))
+	if err != nil {
+		return err
+	}
+	if err := verifier.Verify([]byte(signature), []byte(testMsg)); err != nil {
+		return err
+	}
+	return nil
+}
+
+func validRSAPublicKey(publicKey *rsa.PublicKey) error {
+	if err := RSAValidModulusSizeInBits(publicKey.N.BitLen()); err != nil {
+		return err
+	}
+	return RSAValidPublicExponent(publicKey.E)
+}
+
+func hashID(hashAlg string) (crypto.Hash, error) {
+	switch hashAlg {
+	case "SHA256":
+		return crypto.SHA256, nil
+	case "SHA384":
+		return crypto.SHA384, nil
+	case "SHA512":
+		return crypto.SHA512, nil
+	default:
+		return 0, fmt.Errorf("invalid hash function: %q", hashAlg)
+	}
+}
+
+func rsaHashFunc(hashAlg string) (func() hash.Hash, crypto.Hash, error) {
+	if err := HashSafeForSignature(hashAlg); err != nil {
+		return nil, 0, err
+	}
+	hashFunc := subtle.GetHashFunc(hashAlg)
+	if hashFunc == nil {
+		return nil, 0, fmt.Errorf("invalid hash function: %q", hashAlg)
+	}
+	hashID, err := hashID(hashAlg)
+	if err != nil {
+		return nil, 0, err
+	}
+	return hashFunc, hashID, nil
+}
diff --git a/go/internal/signature/rsa_test.go b/go/internal/signature/rsa_test.go
new file mode 100644
index 0000000..1de0e77
--- /dev/null
+++ b/go/internal/signature/rsa_test.go
@@ -0,0 +1,165 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"math/big"
+	"testing"
+
+	internal "github.com/google/tink/go/internal/signature"
+)
+
+func TestValidatePublicExponent(t *testing.T) {
+	if err := internal.RSAValidPublicExponent(65537); err != nil {
+		t.Errorf("ValidPublicExponent(65537) err = %v, want nil", err)
+	}
+}
+
+func TestValidateInvalidPublicExponentFails(t *testing.T) {
+	if err := internal.RSAValidPublicExponent(3); err == nil {
+		t.Errorf("ValidPublicExponent(3) err = nil, want error")
+	}
+}
+
+func TestValidateModulusSizeInBits(t *testing.T) {
+	if err := internal.RSAValidModulusSizeInBits(2048); err != nil {
+		t.Errorf("ValidModulusSizeInBits(2048) err = %v, want nil", err)
+	}
+}
+
+func TestValidateInvalidModulusSizeInBitsFails(t *testing.T) {
+	if err := internal.RSAValidModulusSizeInBits(1024); err == nil {
+		t.Errorf("ValidModulusSizeInBits(1024) err = nil, want error")
+	}
+}
+
+func TestHashSafeForSignature(t *testing.T) {
+	for _, h := range []string{
+		"SHA256",
+		"SHA384",
+		"SHA512",
+	} {
+		t.Run(h, func(t *testing.T) {
+			if err := internal.HashSafeForSignature(h); err != nil {
+				t.Errorf("HashSafeForSignature(%q)  err = %v, want nil", h, err)
+			}
+		})
+	}
+}
+
+func TestHashNotSafeForSignatureFails(t *testing.T) {
+	for _, h := range []string{
+		"SHA1",
+		"SHA224",
+		"MD5",
+	} {
+		t.Run(h, func(t *testing.T) {
+			if err := internal.HashSafeForSignature(h); err == nil {
+				t.Errorf("HashSafeForSignature(%q)  err = nil, want error", h)
+			}
+		})
+	}
+}
+
+func TestRSAKeySelfTestWithCorruptedKeysFails(t *testing.T) {
+	validPrivKey, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 3072) err = %v, want nil", err)
+	}
+	if err := internal.Validate_RSA_SSA_PKCS1("SHA256", validPrivKey); err != nil {
+		t.Errorf("internal.Validate_RSA_SSA_PKCS1('SHA256', validPrivKey) err = %v, want nil", err)
+	}
+	saltLen := 0
+	if err := internal.Validate_RSA_SSA_PSS("SHA256", saltLen, validPrivKey); err != nil {
+		t.Errorf("internal.Validate_RSA_SSA_PSS('SHA256', saltLen, validPrivKey) err = %v, want nil", err)
+	}
+	type testCase struct {
+		tag  string
+		key  *rsa.PrivateKey
+		hash string
+	}
+	for _, tc := range []testCase{
+		{
+			tag: "modify public modulus",
+			key: &rsa.PrivateKey{
+				D:           validPrivKey.D,
+				Primes:      validPrivKey.Primes,
+				Precomputed: validPrivKey.Precomputed,
+				PublicKey: rsa.PublicKey{
+					N: validPrivKey.N.Add(validPrivKey.N, big.NewInt(500)),
+					E: validPrivKey.E,
+				},
+			},
+		},
+		{
+			tag: "modify public exponent",
+			key: &rsa.PrivateKey{
+				D:           validPrivKey.D,
+				Primes:      validPrivKey.Primes,
+				Precomputed: validPrivKey.Precomputed,
+				PublicKey: rsa.PublicKey{
+					N: validPrivKey.N,
+					E: validPrivKey.E + 5,
+				},
+			},
+		},
+		{
+			tag: "one byte shift in Q",
+			key: &rsa.PrivateKey{
+				PublicKey:   validPrivKey.PublicKey,
+				D:           validPrivKey.D,
+				Precomputed: validPrivKey.Precomputed,
+				Primes: []*big.Int{
+					func() *big.Int {
+						p := validPrivKey.Primes[0].Bytes()
+						p[4] = byte(uint8(p[4] + 1))
+						return new(big.Int).SetBytes(p)
+					}(),
+					validPrivKey.Primes[1],
+				},
+			},
+			hash: "SHA256",
+		},
+		{
+			tag: "removing one byte from P",
+			key: &rsa.PrivateKey{
+				PublicKey:   validPrivKey.PublicKey,
+				D:           validPrivKey.D,
+				Precomputed: validPrivKey.Precomputed,
+				Primes: []*big.Int{
+					validPrivKey.Primes[0],
+					func() *big.Int {
+						p := validPrivKey.Primes[1].Bytes()
+						return new(big.Int).SetBytes(p[:len(p)-2])
+					}(),
+				},
+			},
+			hash: "SHA256",
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			if err := internal.Validate_RSA_SSA_PKCS1(tc.hash, tc.key); err == nil {
+				t.Errorf("internal.Validate_RSA_SSA_PKCS1(hash = %q, key) err = nil, want error", tc.hash)
+			}
+			if err := internal.Validate_RSA_SSA_PSS(tc.hash, saltLen, tc.key); err == nil {
+				t.Errorf("internal.Validate_RSA_SSA_PSS(hash = %d saltLen = %q, key) err = nil, want error", saltLen, tc.hash)
+			}
+		})
+	}
+}
diff --git a/go/internal/signature/rsassapkcs1_signer.go b/go/internal/signature/rsassapkcs1_signer.go
new file mode 100644
index 0000000..cdba314
--- /dev/null
+++ b/go/internal/signature/rsassapkcs1_signer.go
@@ -0,0 +1,61 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"hash"
+
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/tink"
+)
+
+// RSA_SSA_PKCS1_Signer is an implementation of Signer for RSA-SSA-PKCS1.
+type RSA_SSA_PKCS1_Signer struct {
+	privateKey *rsa.PrivateKey
+	hashFunc   func() hash.Hash
+	hashID     crypto.Hash
+}
+
+var _ (tink.Signer) = (*RSA_SSA_PKCS1_Signer)(nil)
+
+// New_RSA_SSA_PKCS1_Signer creates a new intance of RSA_SSA_PKCS1_Signer.
+func New_RSA_SSA_PKCS1_Signer(hashAlg string, privKey *rsa.PrivateKey) (*RSA_SSA_PKCS1_Signer, error) {
+	if err := validRSAPublicKey(privKey.Public().(*rsa.PublicKey)); err != nil {
+		return nil, err
+	}
+	hashFunc, hashID, err := rsaHashFunc(hashAlg)
+	if err != nil {
+		return nil, err
+	}
+	return &RSA_SSA_PKCS1_Signer{
+		privateKey: privKey,
+		hashFunc:   hashFunc,
+		hashID:     hashID,
+	}, nil
+}
+
+// Sign computes a signature for the given data.
+func (s *RSA_SSA_PKCS1_Signer) Sign(data []byte) ([]byte, error) {
+	digest, err := subtle.ComputeHash(s.hashFunc, data)
+	if err != nil {
+		return nil, err
+	}
+	return rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashID, digest)
+}
diff --git a/go/internal/signature/rsassapkcs1_signer_verifier_test.go b/go/internal/signature/rsassapkcs1_signer_verifier_test.go
new file mode 100644
index 0000000..a07cbe6
--- /dev/null
+++ b/go/internal/signature/rsassapkcs1_signer_verifier_test.go
@@ -0,0 +1,218 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+	"math/big"
+	"testing"
+
+	internal "github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestRSASSAPKCS1SignVerify(t *testing.T) {
+	data := random.GetRandomBytes(20)
+	hash := "SHA256"
+	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 2048) err = %v, want nil", err)
+	}
+	signer, err := internal.New_RSA_SSA_PKCS1_Signer(hash, privKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PKCS1_Signer() err = %v, want nil", err)
+	}
+	verifier, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, &privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PKCS1_Verifier() err = %v, want nil", err)
+	}
+	signature, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("Sign() err = %v, want nil", err)
+	}
+	if err := verifier.Verify(signature, data); err != nil {
+		t.Fatalf("Verify() err = %v, want nil", err)
+	}
+}
+
+func TestRSASSAPKCS1ModifySignatureFails(t *testing.T) {
+	data := random.GetRandomBytes(20)
+	hash := "SHA256"
+	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 2048) err = %v, want nil", err)
+	}
+	signer, err := internal.New_RSA_SSA_PKCS1_Signer(hash, privKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PKCS1_Signer() err = %v, want nil", err)
+	}
+	signature, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("Sign() err = %v, want nil", err)
+	}
+	verifier, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, &privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PKCS1_Verifier() err = %v, want nil", err)
+	}
+	appendSig := append(signature, 0x01)
+	if err := verifier.Verify(appendSig, data); err == nil {
+		t.Fatalf("Verify() err = nil, want error")
+	}
+	truncSig := signature[:len(signature)-2]
+	if err := verifier.Verify(truncSig, data); err == nil {
+		t.Fatalf("Verify() err = nil, want error")
+	}
+	signature[0] = byte(uint8(signature[0] + 1))
+	if err := verifier.Verify(truncSig, data); err == nil {
+		t.Fatalf("Verify() err = nil, want error")
+	}
+}
+
+func TestNewRSASSAPKCS1SignerVerifierInvalidInput(t *testing.T) {
+	validPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 2048) err = %v, want nil", err)
+	}
+	rsaShortModulusKey, err := rsa.GenerateKey(rand.Reader, 1024)
+	if err != nil {
+		t.Fatalf("decoding rsa short modulus: %v", err)
+	}
+	testCases := []struct {
+		name    string
+		hash    string
+		privKey *rsa.PrivateKey
+	}{
+		{
+			name:    "weak signature hash algorithm",
+			hash:    "SHA1",
+			privKey: validPrivKey,
+		},
+		{
+			name: "invalid public key exponent",
+			hash: "SHA256",
+			privKey: &rsa.PrivateKey{
+				D:           validPrivKey.D,
+				Primes:      validPrivKey.Primes,
+				Precomputed: validPrivKey.Precomputed,
+				PublicKey: rsa.PublicKey{
+					N: validPrivKey.PublicKey.N,
+					E: 3,
+				},
+			},
+		},
+		{
+			name:    "small modulus size",
+			hash:    "SHA256",
+			privKey: rsaShortModulusKey,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			if _, err := internal.New_RSA_SSA_PKCS1_Signer(tc.hash, tc.privKey); err == nil {
+				t.Errorf("New_RSA_SSA_PKCS1_Signer() err = nil, want error")
+			}
+			if _, err := internal.New_RSA_SSA_PKCS1_Verifier(tc.hash, &tc.privKey.PublicKey); err == nil {
+				t.Errorf("New_RSA_SSA_PKCS1_Verifier() err = nil, want error")
+			}
+		})
+	}
+}
+
+type rsaSSAPKCS1Suite struct {
+	testutil.WycheproofSuite
+	TestGroups []*rsaSSAPKCS1Group `json:"testGroups"`
+}
+
+type rsaSSAPKCS1Group struct {
+	testutil.WycheproofGroup
+	SHA   string             `json:"sha"`
+	E     testutil.HexBytes  `json:"e"`
+	N     testutil.HexBytes  `json:"n"`
+	Type  string             `json:"type"`
+	Tests []*rsaSSAPKCS1Case `json:"tests"`
+}
+
+type rsaSSAPKCS1Case struct {
+	testutil.WycheproofCase
+	Message   testutil.HexBytes `json:"msg"`
+	Signature testutil.HexBytes `json:"sig"`
+}
+
+func TestRSASSAPKCS1WycheproofCases(t *testing.T) {
+	testutil.SkipTestIfTestSrcDirIsNotSet(t)
+
+	testsRan := 0
+	for _, v := range []string{
+		"rsa_signature_2048_sha256_test.json",
+		"rsa_signature_3072_sha512_test.json",
+		"rsa_signature_4096_sha512_test.json",
+	} {
+		suite := &rsaSSAPKCS1Suite{}
+		if err := testutil.PopulateSuite(suite, v); err != nil {
+			t.Fatalf("testutil.PopulateSuite() err = %v, want nil", err)
+		}
+		for _, group := range suite.TestGroups {
+			hash := subtle.ConvertHashName(group.SHA)
+			if hash == "" {
+				t.Fatalf("invalid hash name")
+			}
+			publicKey := &rsa.PublicKey{
+				E: int(new(big.Int).SetBytes(group.E).Uint64()),
+				N: new(big.Int).SetBytes(group.N),
+			}
+			if publicKey.E != 65537 {
+				// golang "crypto/rsa" only supports 65537 as an exponent.
+				if _, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, publicKey); err == nil {
+					t.Errorf("NewRSASSAPKCS1Verifier() err = nil, want error")
+				}
+				continue
+			}
+			verifier, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, publicKey)
+			if err != nil {
+				t.Fatalf("NewRSASSAPKCS1Verifier() err = %v, want nil", err)
+			}
+			for _, test := range group.Tests {
+				caseName := fmt.Sprintf("%s: %s-%s:Case-%d", v, group.Type, group.SHA, test.CaseID)
+				t.Run(caseName, func(t *testing.T) {
+					testsRan++
+					err := verifier.Verify(test.Signature, test.Message)
+					switch test.Result {
+					case "valid":
+						if err != nil {
+							t.Errorf("Verify() err = %v, want nil", err)
+						}
+					case "invalid":
+						if err == nil {
+							t.Errorf("Verify() err = nil, want error")
+						}
+					case "acceptable":
+						// TODO(b/230489047): Inspect flags to appropriately handle acceptable test cases.
+					default:
+						t.Errorf("unsupported test result: %q", test.Result)
+					}
+				})
+			}
+		}
+	}
+	if testsRan != 716 {
+		t.Errorf("testsRan = %d, want = %d", testsRan, 716)
+	}
+}
diff --git a/go/internal/signature/rsassapkcs1_verifier.go b/go/internal/signature/rsassapkcs1_verifier.go
new file mode 100644
index 0000000..3cf783e
--- /dev/null
+++ b/go/internal/signature/rsassapkcs1_verifier.go
@@ -0,0 +1,61 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto"
+	"crypto/rsa"
+	"hash"
+
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/tink"
+)
+
+// RSA_SSA_PKCS1_Verifier is an implementation of Verifier for RSA-SSA-PKCS1.
+type RSA_SSA_PKCS1_Verifier struct {
+	publicKey *rsa.PublicKey
+	hashFunc  func() hash.Hash
+	hashID    crypto.Hash
+}
+
+var _ tink.Verifier = (*RSA_SSA_PKCS1_Verifier)(nil)
+
+// New_RSA_SSA_PKCS1_Verifier creates a new intance of RSASSAPKCS1Verifier.
+func New_RSA_SSA_PKCS1_Verifier(hashAlg string, pubKey *rsa.PublicKey) (*RSA_SSA_PKCS1_Verifier, error) {
+	if err := validRSAPublicKey(pubKey); err != nil {
+		return nil, err
+	}
+	hashFunc, hashID, err := rsaHashFunc(hashAlg)
+	if err != nil {
+		return nil, err
+	}
+	return &RSA_SSA_PKCS1_Verifier{
+		publicKey: pubKey,
+		hashFunc:  hashFunc,
+		hashID:    hashID,
+	}, nil
+}
+
+// Verify verifies whether the given signaure is valid for the given data.
+// It returns an error if the signature is not valid; nil otherwise.
+func (v *RSA_SSA_PKCS1_Verifier) Verify(signature, data []byte) error {
+	hashed, err := subtle.ComputeHash(v.hashFunc, data)
+	if err != nil {
+		return err
+	}
+	return rsa.VerifyPKCS1v15(v.publicKey, v.hashID, hashed, signature)
+}
diff --git a/go/internal/signature/rsassapss_signer.go b/go/internal/signature/rsassapss_signer.go
new file mode 100644
index 0000000..fe7b6a6
--- /dev/null
+++ b/go/internal/signature/rsassapss_signer.go
@@ -0,0 +1,68 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+	"hash"
+
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/tink"
+)
+
+// RSA_SSA_PSS_Signer is an implementation of Signer for RSA-SSA-PSS.
+type RSA_SSA_PSS_Signer struct {
+	privateKey *rsa.PrivateKey
+	hashFunc   func() hash.Hash
+	hashID     crypto.Hash
+	saltLength int
+}
+
+var _ tink.Signer = (*RSA_SSA_PSS_Signer)(nil)
+
+// New_RSA_SSA_PSS_Signer creates a new instance of RSA_SSA_PSS_Signer.
+func New_RSA_SSA_PSS_Signer(hashAlg string, saltLength int, privKey *rsa.PrivateKey) (*RSA_SSA_PSS_Signer, error) {
+	if err := validRSAPublicKey(&privKey.PublicKey); err != nil {
+		return nil, err
+	}
+	hashFunc, hashID, err := rsaHashFunc(hashAlg)
+	if err != nil {
+		return nil, err
+	}
+	if saltLength < 0 {
+		return nil, fmt.Errorf("invalid salt length")
+	}
+	return &RSA_SSA_PSS_Signer{
+		privateKey: privKey,
+		hashFunc:   hashFunc,
+		hashID:     hashID,
+		saltLength: saltLength,
+	}, nil
+}
+
+// Sign computes a signature for the given data.
+func (s *RSA_SSA_PSS_Signer) Sign(data []byte) ([]byte, error) {
+	digest, err := subtle.ComputeHash(s.hashFunc, data)
+	if err != nil {
+		return nil, err
+	}
+	return rsa.SignPSS(rand.Reader, s.privateKey, s.hashID, digest, &rsa.PSSOptions{SaltLength: s.saltLength})
+
+}
diff --git a/go/internal/signature/rsassapss_signer_verifier_test.go b/go/internal/signature/rsassapss_signer_verifier_test.go
new file mode 100644
index 0000000..f194df7
--- /dev/null
+++ b/go/internal/signature/rsassapss_signer_verifier_test.go
@@ -0,0 +1,254 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+	"math/big"
+	"testing"
+
+	"github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestRSASSAPSSSignVerify(t *testing.T) {
+	data := random.GetRandomBytes(20)
+	sigHash := "SHA256"
+	saltLength := 10
+	privKey, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 3072) err = %v, want nil", err)
+	}
+	signer, err := signature.New_RSA_SSA_PSS_Signer(sigHash, saltLength, privKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PSS_Signer() error = %v, want nil", err)
+	}
+	verifier, err := signature.New_RSA_SSA_PSS_Verifier(sigHash, saltLength, &privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PSS_Verifier() error = %v, want nil", err)
+	}
+	s, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("Sign() err = %v, want nil", err)
+	}
+	if err = verifier.Verify(s, data); err != nil {
+		t.Fatalf("Verify() err = %v, want nil", err)
+	}
+}
+
+func TestRSASSAPSSSignVerifyInvalidFails(t *testing.T) {
+	data := random.GetRandomBytes(20)
+	sigHash := "SHA256"
+	saltLength := 10
+	privKey, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 3072) err = %v, want nil", err)
+	}
+	signer, err := signature.New_RSA_SSA_PSS_Signer(sigHash, saltLength, privKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PSS_Signer() error = %v, want nil", err)
+	}
+	verifier, err := signature.New_RSA_SSA_PSS_Verifier(sigHash, saltLength, &privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PSS_Verifier() error = %v, want nil", err)
+	}
+	s, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("Sign() err = %v, want nil", err)
+	}
+	if err = verifier.Verify(s, data); err != nil {
+		t.Fatalf("Verify() err = %v, want nil", err)
+	}
+
+	modifiedSig := s[:]
+	// modify first byte in signature
+	modifiedSig[0] = byte(uint8(modifiedSig[0]) + 1)
+	if err := verifier.Verify(modifiedSig, data); err == nil {
+		t.Errorf("Verify(modifiedSig, data) err = nil, want error")
+	}
+	if err := verifier.Verify(s, []byte("invalid_data")); err == nil {
+		t.Errorf("Verify(s, invalid_data) err = nil, want error")
+	}
+	if err := verifier.Verify([]byte("invalid_signature"), data); err == nil {
+		t.Errorf("Verify(invalid_signature, data) err = nil, want error")
+	}
+
+	diffPrivKey, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 3072) err = %v, want nil", err)
+	}
+	diffVerifier, err := signature.New_RSA_SSA_PSS_Verifier(sigHash, saltLength, &diffPrivKey.PublicKey)
+	if err != nil {
+		t.Fatalf("New_RSA_SSA_PSS_Verifier() error = %v, want nil", err)
+	}
+	if err := diffVerifier.Verify(s, data); err == nil {
+		t.Errorf("Verify() err = nil, want error")
+	}
+}
+
+func TestNewRSASSAPSSSignerVerifierFailWithInvalidInputs(t *testing.T) {
+	type testCase struct {
+		name    string
+		hash    string
+		salt    int
+		privKey *rsa.PrivateKey
+	}
+	validPrivKey, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey(rand.Reader, 3072) err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:    "invalid hash function",
+			hash:    "SHA1",
+			privKey: validPrivKey,
+			salt:    0,
+		},
+		{
+			name: "invalid exponent",
+			hash: "SHA256",
+			salt: 0,
+			privKey: &rsa.PrivateKey{
+				D: validPrivKey.D,
+				PublicKey: rsa.PublicKey{
+					N: validPrivKey.N,
+					E: 8,
+				},
+				Primes:      validPrivKey.Primes,
+				Precomputed: validPrivKey.Precomputed,
+			},
+		},
+		{
+			name: "invalid modulus",
+			hash: "SHA256",
+			salt: 0,
+			privKey: &rsa.PrivateKey{
+				D: validPrivKey.D,
+				PublicKey: rsa.PublicKey{
+					N: big.NewInt(5),
+					E: validPrivKey.E,
+				},
+				Primes:      validPrivKey.Primes,
+				Precomputed: validPrivKey.Precomputed,
+			},
+		},
+		{
+			name:    "invalid salt",
+			hash:    "SHA256",
+			salt:    -1,
+			privKey: validPrivKey,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			if _, err := signature.New_RSA_SSA_PSS_Signer(tc.hash, tc.salt, tc.privKey); err == nil {
+				t.Errorf("New_RSA_SSA_PSS_Signer() err = nil, want error")
+			}
+			if _, err := signature.New_RSA_SSA_PSS_Verifier(tc.hash, tc.salt, &tc.privKey.PublicKey); err == nil {
+				t.Errorf("New_RSA_SSA_PSS_Verifier() err = nil, want error")
+			}
+		})
+	}
+}
+
+type rsaSSAPSSSuite struct {
+	testutil.WycheproofSuite
+	TestGroups []*rsaSSAPSSGroup `json:"testGroups"`
+}
+
+type rsaSSAPSSGroup struct {
+	testutil.WycheproofGroup
+	SHA        string            `json:"sha"`
+	MGFSHA     string            `json:"mgfSha"`
+	SaltLength int               `json:"sLen"`
+	E          testutil.HexBytes `json:"e"`
+	N          testutil.HexBytes `json:"N"`
+	Tests      []*rsaSSAPSSCase  `json:"tests"`
+}
+
+type rsaSSAPSSCase struct {
+	testutil.WycheproofCase
+	Message   testutil.HexBytes `json:"msg"`
+	Signature testutil.HexBytes `json:"sig"`
+}
+
+func TestRSASSAPSSWycheproofCases(t *testing.T) {
+	testutil.SkipTestIfTestSrcDirIsNotSet(t)
+	ranTestCount := 0
+	vectorsFiles := []string{
+		"rsa_pss_2048_sha512_256_mgf1_28_test.json",
+		"rsa_pss_2048_sha512_256_mgf1_32_test.json",
+		"rsa_pss_2048_sha256_mgf1_0_test.json",
+		"rsa_pss_2048_sha256_mgf1_32_test.json",
+		"rsa_pss_3072_sha256_mgf1_32_test.json",
+		"rsa_pss_4096_sha256_mgf1_32_test.json",
+		"rsa_pss_4096_sha512_mgf1_32_test.json",
+	}
+	for _, v := range vectorsFiles {
+		suite := &rsaSSAPSSSuite{}
+		if err := testutil.PopulateSuite(suite, v); err != nil {
+			t.Fatalf("failed populating suite: %s", err)
+		}
+		for _, group := range suite.TestGroups {
+			sigHash := subtle.ConvertHashName(group.SHA)
+			if sigHash == "" {
+				continue
+			}
+			pubKey := &rsa.PublicKey{
+				E: int(new(big.Int).SetBytes(group.E).Uint64()),
+				N: new(big.Int).SetBytes(group.N),
+			}
+			verifier, err := signature.New_RSA_SSA_PSS_Verifier(sigHash, group.SaltLength, pubKey)
+			if err != nil {
+				t.Fatalf("New_RSA_SSA_PSS_Verifier() err = %v, want nil", err)
+			}
+			for _, test := range group.Tests {
+				if (test.CaseID == 67 || test.CaseID == 68) && v == "rsa_pss_2048_sha256_mgf1_0_test.json" {
+					// crypto/rsa will interpret zero length salt and parse the salt length from signature.
+					// Since this test cases use a zero salt length as a parameter, even if a different parameter
+					// is provided, Golang will interpret it and parse the salt directly from the signature.
+					continue
+				}
+				ranTestCount++
+				caseName := fmt.Sprintf("%s: %s-%s-%s-%d:Case-%d", v, group.Type, group.SHA, group.MGFSHA, group.SaltLength, test.CaseID)
+				t.Run(caseName, func(t *testing.T) {
+					err := verifier.Verify(test.Signature, test.Message)
+					switch test.Result {
+					case "valid":
+						if err != nil {
+							t.Errorf("Verify() err = %, want nil", err)
+						}
+					case "invalid":
+						if err == nil {
+							t.Errorf("Verify() err = nil, want error")
+						}
+					case "acceptable":
+						// TODO(b/230489047): Inspect flags to appropriately handle acceptable test cases.
+					default:
+						t.Errorf("unsupported test result: %q", test.Result)
+					}
+				})
+			}
+		}
+	}
+	if ranTestCount < 578 {
+		t.Errorf("ranTestCount > %d, want > %d", ranTestCount, 578)
+	}
+}
diff --git a/go/internal/signature/rsassapss_verifier.go b/go/internal/signature/rsassapss_verifier.go
new file mode 100644
index 0000000..b7652c0
--- /dev/null
+++ b/go/internal/signature/rsassapss_verifier.go
@@ -0,0 +1,67 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto"
+	"crypto/rsa"
+	"fmt"
+	"hash"
+
+	"github.com/google/tink/go/subtle"
+	"github.com/google/tink/go/tink"
+)
+
+// RSA_SSA_PSS_Verifier is an implementation of Verifier for RSA-SSA-PSS.
+type RSA_SSA_PSS_Verifier struct {
+	publicKey  *rsa.PublicKey
+	hashFunc   func() hash.Hash
+	hashID     crypto.Hash
+	saltLength int
+}
+
+var _ tink.Verifier = (*RSA_SSA_PSS_Verifier)(nil)
+
+// New_RSA_SSA_PSS_Verifier creates a new instance of RSA_SSA_PSS_Verifier.
+func New_RSA_SSA_PSS_Verifier(hashAlg string, saltLength int, pubKey *rsa.PublicKey) (*RSA_SSA_PSS_Verifier, error) {
+	if err := validRSAPublicKey(pubKey); err != nil {
+		return nil, err
+	}
+	hashFunc, hashID, err := rsaHashFunc(hashAlg)
+	if err != nil {
+		return nil, err
+	}
+	if saltLength < 0 {
+		return nil, fmt.Errorf("invalid salt length")
+	}
+	return &RSA_SSA_PSS_Verifier{
+		publicKey:  pubKey,
+		hashFunc:   hashFunc,
+		hashID:     hashID,
+		saltLength: saltLength,
+	}, nil
+}
+
+// Verify verifies whether the given signature is valid for the given data.
+// It returns an error if the signature is not valid; nil otherwise.
+func (v *RSA_SSA_PSS_Verifier) Verify(signature, data []byte) error {
+	digest, err := subtle.ComputeHash(v.hashFunc, data)
+	if err != nil {
+		return err
+	}
+	return rsa.VerifyPSS(v.publicKey, v.hashID, digest, signature, &rsa.PSSOptions{SaltLength: v.saltLength})
+}
diff --git a/go/internal/signature/signature.go b/go/internal/signature/signature.go
new file mode 100644
index 0000000..7639fad
--- /dev/null
+++ b/go/internal/signature/signature.go
@@ -0,0 +1,18 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package signature provides internal implementations of the Signer and Verifier primitives.
+package signature
diff --git a/go/internal/testing/stubkeymanager/BUILD.bazel b/go/internal/testing/stubkeymanager/BUILD.bazel
new file mode 100644
index 0000000..1c5a9c5
--- /dev/null
+++ b/go/internal/testing/stubkeymanager/BUILD.bazel
@@ -0,0 +1,33 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "stubkeymanager",
+    testonly = 1,
+    srcs = ["stubkeymanager.go"],
+    importpath = "github.com/google/tink/go/internal/testing/stubkeymanager",
+    visibility = ["//:__subpackages__"],
+    deps = [
+        "//core/registry",
+        "//internal/internalregistry",
+        "//proto/tink_go_proto",
+        "@org_golang_google_protobuf//proto",
+    ],
+)
+
+go_test(
+    name = "stubkeymanager_test",
+    srcs = ["stubkeymanager_test.go"],
+    deps = [
+        ":stubkeymanager",
+        "//proto/aes_gcm_go_proto",
+        "//proto/tink_go_proto",
+        "@com_github_google_go_cmp//cmp",
+        "@org_golang_google_protobuf//testing/protocmp",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":stubkeymanager",
+    visibility = ["//:__subpackages__"],
+)
diff --git a/go/internal/testing/stubkeymanager/stubkeymanager.go b/go/internal/testing/stubkeymanager/stubkeymanager.go
new file mode 100644
index 0000000..9c359f5
--- /dev/null
+++ b/go/internal/testing/stubkeymanager/stubkeymanager.go
@@ -0,0 +1,95 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package stubkeymanager defines key managers for testing primitives.
+package stubkeymanager
+
+import (
+	"io"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// StubKeyManager is a key manager for testing.
+type StubKeyManager struct {
+	URL     string
+	Prim    interface{}
+	Key     proto.Message
+	KeyData *tinkpb.KeyData
+}
+
+var _ (registry.KeyManager) = (*StubKeyManager)(nil)
+
+// Primitive returns the stub primitive.
+func (km *StubKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	return km.Prim, nil
+}
+
+// NewKey returns the stub Key.
+func (km *StubKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	return km.Key, nil
+}
+
+// NewKeyData returns the stub KeyData.
+func (km *StubKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	return km.KeyData, nil
+}
+
+// DoesSupport returns true if this KeyManager supports key type identified by typeURL.
+func (km *StubKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == km.URL
+}
+
+// TypeURL returns the stub type url.
+func (km *StubKeyManager) TypeURL() string {
+	return km.URL
+}
+
+// StubPrivateKeyManager is a private key manager for testing.
+type StubPrivateKeyManager struct {
+	StubKeyManager
+	PubKeyData *tinkpb.KeyData
+}
+
+var _ (registry.PrivateKeyManager) = (*StubPrivateKeyManager)(nil)
+
+// PublicKeyData returns the stub public key data.
+func (skm *StubPrivateKeyManager) PublicKeyData(serializedKey []byte) (*tinkpb.KeyData, error) {
+	return skm.PubKeyData, nil
+}
+
+// StubDerivableKeyManager is a derivable key manager for testing.
+type StubDerivableKeyManager struct {
+	StubKeyManager
+	KeyMatType tinkpb.KeyData_KeyMaterialType
+	DerKey     proto.Message
+	DerErr     error
+}
+
+var _ (internalregistry.DerivableKeyManager) = (*StubDerivableKeyManager)(nil)
+
+// KeyMaterialType returns the stub key material type.
+func (dkm *StubDerivableKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return dkm.KeyMatType
+}
+
+// DeriveKey returns the stub derived key and error.
+func (dkm *StubDerivableKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	return dkm.DerKey, dkm.DerErr
+}
diff --git a/go/internal/testing/stubkeymanager/stubkeymanager_test.go b/go/internal/testing/stubkeymanager/stubkeymanager_test.go
new file mode 100644
index 0000000..b597980
--- /dev/null
+++ b/go/internal/testing/stubkeymanager/stubkeymanager_test.go
@@ -0,0 +1,112 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package stubkeymanager_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/testing/protocmp"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
+	agpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+type fakePrimitive struct {
+	Name string
+}
+
+func TestStubKeyManager(t *testing.T) {
+	keyType := "some.key.type"
+	km := stubkeymanager.StubKeyManager{
+		URL:  keyType,
+		Key:  &agpb.AesGcmKey{Version: 1, KeyValue: []byte("key_value")},
+		Prim: &fakePrimitive{Name: "fake-primitive-name"},
+		KeyData: &tpb.KeyData{
+			TypeUrl:         keyType,
+			Value:           []byte("key_value"),
+			KeyMaterialType: tpb.KeyData_SYMMETRIC,
+		},
+	}
+	if !km.DoesSupport(keyType) {
+		t.Errorf("DoesSupport(%q) = false , want true", keyType)
+	}
+	if km.DoesSupport("some.other.key.type") {
+		t.Errorf("DoesSupport(%q) = true , want false", keyType)
+	}
+	if km.TypeURL() != km.URL {
+		t.Errorf("TypeURL() = %q, want %q", km.TypeURL(), keyType)
+	}
+	key, err := km.NewKey(nil)
+	if err != nil {
+		t.Errorf("NewKey() err = %v, want nil", err)
+	}
+	if !cmp.Equal(key, km.Key, protocmp.Transform()) {
+		t.Errorf("NewKey() = %v, want %v", key, km.Key)
+	}
+	keyData, err := km.NewKeyData(nil)
+	if err != nil {
+		t.Errorf("NewKeyData() err = %v, want nil", err)
+	}
+	if !cmp.Equal(keyData, km.KeyData, protocmp.Transform()) {
+		t.Errorf("NewKeyData() = %v, want %v", keyData, km.KeyData)
+	}
+	p, err := km.Primitive(nil)
+	if err != nil {
+		t.Errorf("Primitive() err = %v, want nil", err)
+	}
+	if !cmp.Equal(p, km.Prim) {
+		t.Errorf("Primitive() = %v, want %v", p, km.Prim)
+	}
+}
+
+func TestStubPrivateKeyManager(t *testing.T) {
+	km := stubkeymanager.StubPrivateKeyManager{
+		PubKeyData: &tpb.KeyData{
+			TypeUrl:         "some.key.type",
+			Value:           []byte("key_value"),
+			KeyMaterialType: tpb.KeyData_ASYMMETRIC_PUBLIC,
+		},
+	}
+	pubKeyData, err := km.PublicKeyData(nil)
+	if err != nil {
+		t.Errorf("PublicKeyData() err = %v, want nil", err)
+	}
+	if !cmp.Equal(pubKeyData, km.PubKeyData, protocmp.Transform()) {
+		t.Errorf("PublicKeyData() = %v, want %v", pubKeyData, km.PubKeyData)
+	}
+}
+
+func TestStubDerivableKeyManager(t *testing.T) {
+	km := stubkeymanager.StubDerivableKeyManager{
+		KeyMatType: tpb.KeyData_SYMMETRIC,
+		DerKey:     &agpb.AesGcmKey{Version: 0, KeyValue: []byte("derived_key_value")},
+		DerErr:     errors.New("hiya"),
+	}
+	if km.KeyMaterialType() != km.KeyMatType {
+		t.Errorf("KeyMaterialType() = %d, want %d", km.KeyMaterialType(), tpb.KeyData_SYMMETRIC)
+	}
+	derivedKey, err := km.DeriveKey([]byte{}, &bytes.Buffer{})
+	if !cmp.Equal(derivedKey, km.DerKey, protocmp.Transform()) {
+		t.Errorf("DeriveKey() = %v, want %v", derivedKey, km.DerKey)
+	}
+	if err != km.DerErr {
+		t.Errorf("DeriveKey() err = %v, want %v", err, km.DerErr)
+	}
+}
diff --git a/go/internal/tinkerror/BUILD.bazel b/go/internal/tinkerror/BUILD.bazel
new file mode 100644
index 0000000..d8c4fa9
--- /dev/null
+++ b/go/internal/tinkerror/BUILD.bazel
@@ -0,0 +1,29 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+go_library(
+    name = "tinkerror",
+    srcs = [
+        "doc.go",
+        "tinkerror.go",
+    ],
+    importpath = "github.com/google/tink/go/internal/tinkerror",
+)
+
+go_test(
+    name = "tinkerror_test",
+    srcs = ["tinkerror_test.go"],
+    deps = [
+        ":tinkerror",
+        "//internal/tinkerror/tinkerrortest",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":tinkerror",
+    visibility = ["//:__subpackages__"],
+)
diff --git a/go/internal/tinkerror/doc.go b/go/internal/tinkerror/doc.go
new file mode 100644
index 0000000..dfd6b9b
--- /dev/null
+++ b/go/internal/tinkerror/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package tinkerror provides error handling functionality which helps maintain
+// API backwards compatibility.
+package tinkerror
diff --git a/go/internal/tinkerror/tinkerror.go b/go/internal/tinkerror/tinkerror.go
new file mode 100644
index 0000000..54609f7
--- /dev/null
+++ b/go/internal/tinkerror/tinkerror.go
@@ -0,0 +1,28 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package tinkerror
+
+// Fail should be called by functions which do not return an error value but
+// need to handle an error condition.
+//
+// Such functions are strong candidates for replacement by an equivalent
+// function which returns an explicit error value. Once a replacement has been
+// introduced, the original should be deprecated and subsequently removed when
+// performing a major release.
+func Fail(message string) {
+	panic(message)
+}
diff --git a/go/internal/tinkerror/tinkerror_test.go b/go/internal/tinkerror/tinkerror_test.go
new file mode 100644
index 0000000..fcc4d23
--- /dev/null
+++ b/go/internal/tinkerror/tinkerror_test.go
@@ -0,0 +1,33 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package tinkerror_test
+
+import (
+	"testing"
+
+	"github.com/google/tink/go/internal/tinkerror"
+	"github.com/google/tink/go/internal/tinkerror/tinkerrortest"
+)
+
+func TestFail(t *testing.T) {
+	err := tinkerrortest.RecoverFromFail(func() {
+		tinkerror.Fail("error message")
+	})
+	if err == nil {
+		t.Errorf("tinkerror.Fail() did not fail")
+	}
+}
diff --git a/go/internal/tinkerror/tinkerrortest/BUILD.bazel b/go/internal/tinkerror/tinkerrortest/BUILD.bazel
new file mode 100644
index 0000000..eb52275
--- /dev/null
+++ b/go/internal/tinkerror/tinkerrortest/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+go_library(
+    name = "tinkerrortest",
+    testonly = 1,
+    srcs = [
+        "doc.go",
+        "tinkerrortest.go",
+    ],
+    importpath = "github.com/google/tink/go/internal/tinkerror/tinkerrortest",
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":tinkerrortest",
+    visibility = ["//:__subpackages__"],
+)
diff --git a/go/internal/tinkerror/tinkerrortest/doc.go b/go/internal/tinkerror/tinkerrortest/doc.go
new file mode 100644
index 0000000..231403b
--- /dev/null
+++ b/go/internal/tinkerror/tinkerrortest/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package tinkerrortest provides test functionality for package tinkerror.
+package tinkerrortest
diff --git a/go/internal/tinkerror/tinkerrortest/tinkerrortest.go b/go/internal/tinkerror/tinkerrortest/tinkerrortest.go
new file mode 100644
index 0000000..a81c123
--- /dev/null
+++ b/go/internal/tinkerror/tinkerrortest/tinkerrortest.go
@@ -0,0 +1,32 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package tinkerrortest
+
+import "fmt"
+
+// RecoverFromFail facilitates testing a function f in which [tinkerror.Fail]
+// is called.
+func RecoverFromFail(f func()) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	f()
+	return
+}
diff --git a/go/jwt/BUILD.bazel b/go/jwt/BUILD.bazel
index 454424c..fe4fb36 100644
--- a/go/jwt/BUILD.bazel
+++ b/go/jwt/BUILD.bazel
@@ -17,6 +17,10 @@
         "jwt_mac.go",
         "jwt_mac_factory.go",
         "jwt_mac_kid.go",
+        "jwt_rsa_ssa_pkcs1_signer_key_manager.go",
+        "jwt_rsa_ssa_pkcs1_verifier_key_manager.go",
+        "jwt_rsa_ssa_pss_signer_key_manager.go",
+        "jwt_rsa_ssa_pss_verify_key_manager.go",
         "jwt_signer.go",
         "jwt_signer_factory.go",
         "jwt_signer_kid.go",
@@ -32,10 +36,14 @@
     deps = [
         "//core/primitiveset",
         "//core/registry",
+        "//internal/signature",
+        "//internal/tinkerror",
         "//keyset",
         "//mac/subtle",
         "//proto/jwt_ecdsa_go_proto",
         "//proto/jwt_hmac_go_proto",
+        "//proto/jwt_rsa_ssa_pkcs1_go_proto",
+        "//proto/jwt_rsa_ssa_pss_go_proto",
         "//proto/tink_go_proto",
         "//signature/subtle",
         "//subtle",
@@ -57,6 +65,10 @@
         "jwt_key_templates_test.go",
         "jwt_mac_factory_test.go",
         "jwt_mac_kid_test.go",
+        "jwt_rsa_ssa_pkcs1_signer_key_manager_test.go",
+        "jwt_rsa_ssa_pkcs1_verifier_key_manager_test.go",
+        "jwt_rsa_ssa_pss_signer_key_manager_test.go",
+        "jwt_rsa_ssa_pss_verify_key_manager_test.go",
         "jwt_signer_verifier_factory_test.go",
         "jwt_signer_verifier_kid_test.go",
         "jwt_test.go",
@@ -67,10 +79,13 @@
     embed = [":jwt"],
     deps = [
         "//core/registry",
+        "//insecurecleartextkeyset",
         "//keyset",
         "//mac/subtle",
         "//proto/jwt_ecdsa_go_proto",
         "//proto/jwt_hmac_go_proto",
+        "//proto/jwt_rsa_ssa_pkcs1_go_proto",
+        "//proto/jwt_rsa_ssa_pss_go_proto",
         "//proto/tink_go_proto",
         "//signature",
         "//signature/subtle",
diff --git a/go/jwt/jwk_converter.go b/go/jwt/jwk_converter.go
index d4dc73d..e2cc275 100644
--- a/go/jwt/jwk_converter.go
+++ b/go/jwt/jwk_converter.go
@@ -25,11 +25,15 @@
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/keyset"
 	jepb "github.com/google/tink/go/proto/jwt_ecdsa_go_proto"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+	jrpsspb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 const (
 	jwtECDSAPublicKeyType = "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey"
+	jwtRSPublicKeyType    = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey"
+	jwtPSPublicKeyType    = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey"
 )
 
 func keysetHasID(ks *tinkpb.Keyset, keyID uint32) bool {
@@ -104,12 +108,12 @@
 	return nil
 }
 
-func decodePoint(s *spb.Struct, name string) ([]byte, error) {
-	point, err := stringItem(s, name)
+func decodeItem(s *spb.Struct, name string) ([]byte, error) {
+	e, err := stringItem(s, name)
 	if err != nil {
 		return nil, err
 	}
-	return base64Decode(point)
+	return base64Decode(e)
 }
 
 func validateKeyOPSIsVerify(s *spb.Struct) error {
@@ -140,18 +144,144 @@
 	return expectStringItem(s, "use", "sig")
 }
 
-func algorithmPrefixMatch(s *spb.Struct, prefix string) error {
+func algorithmPrefix(s *spb.Struct) (string, error) {
 	alg, err := stringItem(s, "alg")
 	if err != nil {
-		return err
+		return "", err
 	}
-	if len(alg) < len(prefix) {
-		return fmt.Errorf("invalid algorithm")
+	if len(alg) < 2 {
+		return "", fmt.Errorf("invalid algorithm")
 	}
-	if alg[0:len(prefix)] != prefix {
-		return fmt.Errorf("invalid algorithm")
+	return alg[0:2], nil
+}
+
+var psNameToAlg = map[string]jrpsspb.JwtRsaSsaPssAlgorithm{
+	"PS256": jrpsspb.JwtRsaSsaPssAlgorithm_PS256,
+	"PS384": jrpsspb.JwtRsaSsaPssAlgorithm_PS384,
+	"PS512": jrpsspb.JwtRsaSsaPssAlgorithm_PS512,
+}
+
+func psPublicKeyDataFromStruct(keyStruct *spb.Struct) (*tinkpb.KeyData, error) {
+	alg, err := stringItem(keyStruct, "alg")
+	if err != nil {
+		return nil, err
 	}
-	return nil
+	algorithm, ok := psNameToAlg[alg]
+	if !ok {
+		return nil, fmt.Errorf("invalid alg header: %q", alg)
+	}
+	rsaPubKey, err := rsaPubKeyFromStruct(keyStruct)
+	if err != nil {
+		return nil, err
+	}
+	jwtPubKey := &jrpsspb.JwtRsaSsaPssPublicKey{
+		Version:   jwtECDSASignerKeyVersion,
+		Algorithm: algorithm,
+		E:         rsaPubKey.exponent,
+		N:         rsaPubKey.modulus,
+	}
+	if rsaPubKey.customKID != nil {
+		jwtPubKey.CustomKid = &jrpsspb.JwtRsaSsaPssPublicKey_CustomKid{
+			Value: *rsaPubKey.customKID,
+		}
+	}
+	serializedPubKey, err := proto.Marshal(jwtPubKey)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         jwtPSPublicKeyType,
+		Value:           serializedPubKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
+	}, nil
+}
+
+var rsNameToAlg = map[string]jrsppb.JwtRsaSsaPkcs1Algorithm{
+	"RS256": jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+	"RS384": jrsppb.JwtRsaSsaPkcs1Algorithm_RS384,
+	"RS512": jrsppb.JwtRsaSsaPkcs1Algorithm_RS512,
+}
+
+func rsPublicKeyDataFromStruct(keyStruct *spb.Struct) (*tinkpb.KeyData, error) {
+	alg, err := stringItem(keyStruct, "alg")
+	if err != nil {
+		return nil, err
+	}
+	algorithm, ok := rsNameToAlg[alg]
+	if !ok {
+		return nil, fmt.Errorf("invalid alg header: %q", alg)
+	}
+	rsaPubKey, err := rsaPubKeyFromStruct(keyStruct)
+	if err != nil {
+		return nil, err
+	}
+	jwtPubKey := &jrsppb.JwtRsaSsaPkcs1PublicKey{
+		Version:   0,
+		Algorithm: algorithm,
+		E:         rsaPubKey.exponent,
+		N:         rsaPubKey.modulus,
+	}
+	if rsaPubKey.customKID != nil {
+		jwtPubKey.CustomKid = &jrsppb.JwtRsaSsaPkcs1PublicKey_CustomKid{
+			Value: *rsaPubKey.customKID,
+		}
+	}
+	serializedPubKey, err := proto.Marshal(jwtPubKey)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         jwtRSPublicKeyType,
+		Value:           serializedPubKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
+	}, nil
+}
+
+type rsaPubKey struct {
+	exponent  []byte
+	modulus   []byte
+	customKID *string
+}
+
+func rsaPubKeyFromStruct(keyStruct *spb.Struct) (*rsaPubKey, error) {
+	if hasItem(keyStruct, "p") ||
+		hasItem(keyStruct, "q") ||
+		hasItem(keyStruct, "dq") ||
+		hasItem(keyStruct, "dp") ||
+		hasItem(keyStruct, "d") ||
+		hasItem(keyStruct, "qi") {
+		return nil, fmt.Errorf("private key can't be converted")
+	}
+	if err := expectStringItem(keyStruct, "kty", "RSA"); err != nil {
+		return nil, err
+	}
+	if err := validateUseIsSig(keyStruct); err != nil {
+		return nil, err
+	}
+	if err := validateKeyOPSIsVerify(keyStruct); err != nil {
+		return nil, err
+	}
+	e, err := decodeItem(keyStruct, "e")
+	if err != nil {
+		return nil, err
+	}
+	n, err := decodeItem(keyStruct, "n")
+	if err != nil {
+		return nil, err
+	}
+	var customKID *string = nil
+	if hasItem(keyStruct, "kid") {
+		kid, err := stringItem(keyStruct, "kid")
+		if err != nil {
+			return nil, err
+		}
+		customKID = &kid
+	}
+	return &rsaPubKey{
+		exponent:  e,
+		modulus:   n,
+		customKID: customKID,
+	}, nil
 }
 
 func esPublicKeyDataFromStruct(keyStruct *spb.Struct) (*tinkpb.KeyData, error) {
@@ -188,11 +318,11 @@
 	if err := validateKeyOPSIsVerify(keyStruct); err != nil {
 		return nil, err
 	}
-	x, err := decodePoint(keyStruct, "x")
+	x, err := decodeItem(keyStruct, "x")
 	if err != nil {
 		return nil, fmt.Errorf("failed to decode x: %v", err)
 	}
-	y, err := decodePoint(keyStruct, "y")
+	y, err := decodeItem(keyStruct, "y")
 	if err != nil {
 		return nil, fmt.Errorf("failed to decode y: %v", err)
 	}
@@ -228,10 +358,21 @@
 	if keyStruct == nil {
 		return nil, fmt.Errorf("key is not a JSON object")
 	}
-	if err := algorithmPrefixMatch(keyStruct, "ES"); err != nil {
+	algPrefix, err := algorithmPrefix(keyStruct)
+	if err != nil {
 		return nil, err
 	}
-	keyData, err := esPublicKeyDataFromStruct(keyStruct)
+	var keyData *tinkpb.KeyData
+	switch algPrefix {
+	case "ES":
+		keyData, err = esPublicKeyDataFromStruct(keyStruct)
+	case "RS":
+		keyData, err = rsPublicKeyDataFromStruct(keyStruct)
+	case "PS":
+		keyData, err = psPublicKeyDataFromStruct(keyStruct)
+	default:
+		return nil, fmt.Errorf("unsupported algorithm prefix: %v", algPrefix)
+	}
 	if err != nil {
 		return nil, err
 	}
@@ -245,7 +386,7 @@
 
 // JWKSetToPublicKeysetHandle converts a Json Web Key (JWK) set into a Tink KeysetHandle.
 // It requires that all keys in the set have the "alg" field set. Currently, only
-// public keys for algorithms ES256, ES384 and ES512 are supported.
+// public keys for algorithms ES256, ES384, ES512, RS256, RS384, and RS512 are supported.
 // JWK is defined in https://www.rfc-editor.org/rfc/rfc7517.txt.
 func JWKSetToPublicKeysetHandle(jwkSet []byte) (*keyset.Handle, error) {
 	jwk := &spb.Struct{}
@@ -277,6 +418,77 @@
 	s.GetFields()[key] = spb.NewStringValue(val)
 }
 
+var psAlgToStr map[jrpsspb.JwtRsaSsaPssAlgorithm]string = map[jrpsspb.JwtRsaSsaPssAlgorithm]string{
+	jrpsspb.JwtRsaSsaPssAlgorithm_PS256: "PS256",
+	jrpsspb.JwtRsaSsaPssAlgorithm_PS384: "PS384",
+	jrpsspb.JwtRsaSsaPssAlgorithm_PS512: "PS512",
+}
+
+func psPublicKeyToStruct(key *tinkpb.Keyset_Key) (*spb.Struct, error) {
+	pubKey := &jrpsspb.JwtRsaSsaPssPublicKey{}
+	if err := proto.Unmarshal(key.GetKeyData().GetValue(), pubKey); err != nil {
+		return nil, err
+	}
+	alg, ok := psAlgToStr[pubKey.GetAlgorithm()]
+	if !ok {
+		return nil, fmt.Errorf("invalid algorithm")
+	}
+	outKey := &spb.Struct{
+		Fields: map[string]*spb.Value{},
+	}
+	addStringEntry(outKey, "alg", alg)
+	addStringEntry(outKey, "kty", "RSA")
+	addStringEntry(outKey, "e", base64Encode(pubKey.GetE()))
+	addStringEntry(outKey, "n", base64Encode(pubKey.GetN()))
+	addStringEntry(outKey, "use", "sig")
+	addKeyOPSVerify(outKey)
+	var customKID *string = nil
+	if pubKey.GetCustomKid() != nil {
+		ck := pubKey.GetCustomKid().GetValue()
+		customKID = &ck
+	}
+	if err := setKeyID(outKey, key, customKID); err != nil {
+		return nil, err
+	}
+	return outKey, nil
+}
+
+var rsAlgToStr map[jrsppb.JwtRsaSsaPkcs1Algorithm]string = map[jrsppb.JwtRsaSsaPkcs1Algorithm]string{
+	jrsppb.JwtRsaSsaPkcs1Algorithm_RS256: "RS256",
+	jrsppb.JwtRsaSsaPkcs1Algorithm_RS384: "RS384",
+	jrsppb.JwtRsaSsaPkcs1Algorithm_RS512: "RS512",
+}
+
+func rsPublicKeyToStruct(key *tinkpb.Keyset_Key) (*spb.Struct, error) {
+	pubKey := &jrsppb.JwtRsaSsaPkcs1PublicKey{}
+	if err := proto.Unmarshal(key.GetKeyData().GetValue(), pubKey); err != nil {
+		return nil, err
+	}
+	alg, ok := rsAlgToStr[pubKey.GetAlgorithm()]
+	if !ok {
+		return nil, fmt.Errorf("invalid algorithm")
+	}
+	outKey := &spb.Struct{
+		Fields: map[string]*spb.Value{},
+	}
+	addStringEntry(outKey, "alg", alg)
+	addStringEntry(outKey, "kty", "RSA")
+	addStringEntry(outKey, "e", base64Encode(pubKey.GetE()))
+	addStringEntry(outKey, "n", base64Encode(pubKey.GetN()))
+	addStringEntry(outKey, "use", "sig")
+	addKeyOPSVerify(outKey)
+
+	var customKID *string = nil
+	if pubKey.GetCustomKid() != nil {
+		ck := pubKey.GetCustomKid().GetValue()
+		customKID = &ck
+	}
+	if err := setKeyID(outKey, key, customKID); err != nil {
+		return nil, err
+	}
+	return outKey, nil
+}
+
 func esPublicKeyToStruct(key *tinkpb.Keyset_Key) (*spb.Struct, error) {
 	pubKey := &jepb.JwtEcdsaPublicKey{}
 	if err := proto.Unmarshal(key.GetKeyData().GetValue(), pubKey); err != nil {
@@ -304,20 +516,35 @@
 	addStringEntry(outKey, "use", "sig")
 	addKeyOPSVerify(outKey)
 
-	if key.GetOutputPrefixType() == tinkpb.OutputPrefixType_TINK {
-		kid := keyID(key.KeyId, key.GetOutputPrefixType())
-		if kid == nil {
-			return nil, fmt.Errorf("tink KID shouldn't be nil")
-		}
-		addStringEntry(outKey, "kid", *kid)
-	} else if pubKey.GetCustomKid() != nil {
-		addStringEntry(outKey, "kid", pubKey.GetCustomKid().GetValue())
+	var customKID *string = nil
+	if pubKey.GetCustomKid() != nil {
+		ck := pubKey.GetCustomKid().GetValue()
+		customKID = &ck
+	}
+	if err := setKeyID(outKey, key, customKID); err != nil {
+		return nil, err
 	}
 	return outKey, nil
 }
 
+func setKeyID(outKey *spb.Struct, key *tinkpb.Keyset_Key, customKID *string) error {
+	if key.GetOutputPrefixType() == tinkpb.OutputPrefixType_TINK {
+		if customKID != nil {
+			return fmt.Errorf("TINK keys shouldn't have custom KID")
+		}
+		kid := keyID(key.KeyId, key.GetOutputPrefixType())
+		if kid == nil {
+			return fmt.Errorf("tink KID shouldn't be nil")
+		}
+		addStringEntry(outKey, "kid", *kid)
+	} else if customKID != nil {
+		addStringEntry(outKey, "kid", *customKID)
+	}
+	return nil
+}
+
 // JWKSetFromPublicKeysetHandle converts a Tink KeysetHandle with JWT keys into a Json Web Key (JWK) set.
-// Currently only public keys for algorithms ES256, ES384 and ES512 are supported.
+// Currently only public keys for algorithms ES256, ES384, ES512, RS256, RS384, and RS512 are supported.
 // JWK is defined in https://www.rfc-editor.org/rfc/rfc7517.html.
 func JWKSetFromPublicKeysetHandle(kh *keyset.Handle) ([]byte, error) {
 	b := &bytes.Buffer{}
@@ -344,10 +571,18 @@
 		if keyData.GetKeyMaterialType() != tinkpb.KeyData_ASYMMETRIC_PUBLIC {
 			return nil, fmt.Errorf("only asymmetric public keys are supported")
 		}
-		if keyData.GetTypeUrl() != jwtECDSAPublicKeyType {
+		keyStruct := &spb.Struct{}
+		var err error
+		switch keyData.GetTypeUrl() {
+		case jwtECDSAPublicKeyType:
+			keyStruct, err = esPublicKeyToStruct(k)
+		case jwtRSPublicKeyType:
+			keyStruct, err = rsPublicKeyToStruct(k)
+		case jwtPSPublicKeyType:
+			keyStruct, err = psPublicKeyToStruct(k)
+		default:
 			return nil, fmt.Errorf("unsupported key type url")
 		}
-		keyStruct, err := esPublicKeyToStruct(k)
 		if err != nil {
 			return nil, err
 		}
diff --git a/go/jwt/jwk_converter_test.go b/go/jwt/jwk_converter_test.go
index 2e56836..b5b96a1 100644
--- a/go/jwt/jwk_converter_test.go
+++ b/go/jwt/jwk_converter_test.go
@@ -29,6 +29,8 @@
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/testkeyset"
 	jepb "github.com/google/tink/go/proto/jwt_ecdsa_go_proto"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+	jrpsspb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
@@ -190,6 +192,232 @@
 			]
 		}`,
 	},
+	{
+		tag: "RS256",
+		jwkSet: `{
+			"keys":[{
+				"kty":"RSA",
+				"n": "vmUOa62TYrxj7N8rZVAzoEdSnmsRQaNWBMAdB8adGa8n4ycGiYWoGv0uZWc8vH2jn6l3Pa_72bb2IHf3-KD2UaTwLk1x3yShXybEoS5ZF9bemzrn2ohNixGoN7Ofj7wPb61Z-F1Nv53nq308z-RI1WeyIH-9HjuIcuUxaWY0VevsXzCehMJP5g7kVzyl55bYcRi28didkVazrzVgNG35yNNMEL32oW1Vfvvp7hfQHtxSwkFOPzJgzIPHbJFbxALGrrgXHsoq7UtDQdS9vvoEp4_JzQhCtnCEKahgkTwOWyT96OlRGYiPJSFHWTujy1Qnd6OKc8LGEspAX4oD6Zl-YQ",
+				"e":"AQAB",
+				"use":"sig",
+				"alg":"RS256",
+				"key_ops":["verify"],
+				"kid":"TCGiGw"
+			}]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId":1277272603,
+			"key":[{
+				"keyData":{
+					"typeUrl":
+							"type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey",
+					"value":"QoABP3S5U0JiFQcqcMFT0Ysqk7FK2NunBCY9o+EAE+svaQi6zWQq2ODFoxB2NU9nqa3ZbhRiCdKNLz6o+jOTIpemKx8Gh/7GufRGLFAjjMchZYs3ripiTNSMaqXgm6ECt8DqrAZbMQ7D3Ha1vArcZG97pbE9t3m4M87zhLs3wPYd/kQ6gAEFPE2GLD5ai8VYd/Q0ePZR0ttLgkJ/2yIig5T8YyJaoZEPjK+v3zVFQuGguJApnl2tC0S7OqOtqsDZ5Dux0H3Cx85FLeyB2STHlXtq9GUGI2VrC/TP3OASc6ap75WMKZRpowEVaip8wWehAOL+VIgTajiFf0yXdSodc4ZjJKreiTKAAd6ahHQiVJapNKY6XANgA+JmluAWq/Fk1LmEnTybWVelcODbppwIvhJ6Xuz6kjuEhhxsUtkPO4vuZJfEF8DWAH5L/FHjJpgP3NnDoNVzGOL5w8SdgIfgCS0UqBLSv2/KhlIEijuL9NYaqydN1cPcjdeadSMcDSIwKjNASRVaPZDJKoABx1/CfOqCbE8eh450YvGwYvII+ro8tR+uusnt2QuQZux3wvl9eto9Dr+5Iq/0bKqpMMgvYHIT+mlkgK6SYLcynZx+SYMAtbixa0nH1lJnnBodOJS6zdMRTcFkpI4g/CbCvzTp5gF5EkfBSbVToVLqICydokKnTvNK6chX3MEUjskigAH0eGwQwn174yJzJTUWH4cRxDredI6LkjADm/ikza76AHT8qRJHJkmwSXL88p3M2bYFN+g9Z/FTL21Ylc0mxn/iII3vabfZWZTWK9QGR7YjAicFyLDeu/ZccCkCXgTFzqqlZ7w4Sv05hWz57xxm81JyxftzapeflfAmjRircFXG2RqAAgub/Z28+SFSf6zSPFMKiYVWx//DI0ubbiuuu65tUse9xYq9JtHEobgYk0dJXNuY9RzPkGblZ8/SD06yRf9l8DMRAbivDfgXY5QZ2PBDk1jn6A2y0S+i80h9MILJ+/sfkljiyvtBFDQwiI9tPOOnxbWmg6bl5xYUdvjbhxBoVB1fgOtAid6gGuLstbf8ycV+DkaWg3mo4054ge9BBT4eWKGC/LHctSaQ/OBs5cbGW+UqZxIjSN9YeOTkbvNKO4l4jGTg0BUBPB3GH8KQPtE4sbBhUDyjYYgAZZcSaRq7AfhLUkiDSfIVcKAIoEOaTS63vf2BQlbW8/HuNlWNUX0M+hkSigIiAwEAARqAAr5lDmutk2K8Y+zfK2VQM6BHUp5rEUGjVgTAHQfGnRmvJ+MnBomFqBr9LmVnPLx9o5+pdz2v+9m29iB39/ig9lGk8C5Ncd8koV8mxKEuWRfW3ps659qITYsRqDezn4+8D2+tWfhdTb+d56t9PM/kSNVnsiB/vR47iHLlMWlmNFXr7F8wnoTCT+YO5Fc8peeW2HEYtvHYnZFWs681YDRt+cjTTBC99qFtVX776e4X0B7cUsJBTj8yYMyDx2yRW8QCxq64Fx7KKu1LQ0HUvb76BKePyc0IQrZwhCmoYJE8Dlsk/ejpURmIjyUhR1k7o8tUJ3ejinPCxhLKQF+KA+mZfmEQAQ==",
+					"keyMaterialType":"ASYMMETRIC_PRIVATE"
+				},
+				"status":"ENABLED",
+				"keyId":1277272603,
+				"outputPrefixType":"TINK"
+			}]
+		}`,
+	},
+	{
+		tag: "RS384",
+		jwkSet: `{
+			"keys":[{
+				"kty":"RSA",
+				"n":"AI83_8Uy0v4xS6kDZKqcqzSbeyksy2C67ajtI41J2KMDtO9jUaEAQ9uDhMubjZzPYh1wf_gtJgAC5PSiI3fOLUG0AHCbi_yXVfH3_1U_Yl4b_e8yx_NPyuIvwHwXwE5a32hiss9PuY2-qEivH5LK4AXxPiTiUc9x4gh1OwZaSTYWT7SRO-0ROwYwCwpg4Uf0IMLtmHou_NmNw0uOlOgKfx-EFmMzV-5pspEnwsHq_ijFSxmHNAdy5S0n4u1LIKKmgXJIyUu3AKfAJMydn6nTKzrOcpX0yMnxPq9yP8xKuK_mXysFyNvmS0Sq5c-grOETFeMFScweoUpWVnYOCCSyZ93yAhsTUWnDjZd7iuji9Y7zUo4PWlKXyRRz_aSpxrsn70LOZNLLUjILVeyfCRs2JXptfxCNg3wg6FVAH0xTORmPGICgWDmwOFgP1Y6tW-p0cnK8LwVkuRclyKAMvTtYm9xZZHUSjw86rHEnB2VfsPTIn0_WAVnJ2OAKhuVMtwjB7Q",
+				"e":"AQAB",
+				"use":"sig",
+				"alg":"RS384",
+				"key_ops":["verify"],
+				"kid":"FVLRIg"
+			}]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId":357749026,
+			"key":[{
+				"keyData":{
+					"typeUrl":
+							"type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey",
+					"value":"EosDEAIagQMAjzf/xTLS/jFLqQNkqpyrNJt7KSzLYLrtqO0jjUnYowO072NRoQBD24OEy5uNnM9iHXB/+C0mAALk9KIjd84tQbQAcJuL/JdV8ff/VT9iXhv97zLH80/K4i/AfBfATlrfaGKyz0+5jb6oSK8fksrgBfE+JOJRz3HiCHU7BlpJNhZPtJE77RE7BjALCmDhR/Qgwu2Yei782Y3DS46U6Ap/H4QWYzNX7mmykSfCwer+KMVLGYc0B3LlLSfi7UsgoqaBckjJS7cAp8AkzJ2fqdMrOs5ylfTIyfE+r3I/zEq4r+ZfKwXI2+ZLRKrlz6Cs4RMV4wVJzB6hSlZWdg4IJLJn3fICGxNRacONl3uK6OL1jvNSjg9aUpfJFHP9pKnGuyfvQs5k0stSMgtV7J8JGzYlem1/EI2DfCDoVUAfTFM5GY8YgKBYObA4WA/Vjq1b6nRycrwvBWS5FyXIoAy9O1ib3FlkdRKPDzqscScHZV+w9MifT9YBWcnY4AqG5Uy3CMHtIgMBAAEagAMPPYhMNdJaFmjUvXWy6iUV3g3HHesuifXMah/EYz1Ya4aPiuQe2+Zcr6wr9oulSjRIqbYUdMl8atJubeqUTy5ltX/ue77zzC7rJtbW/X28QgJNt/urGqyeUTKMggKG1Ai+FPKuOO+n88f4pBoaBti8CSXxytul1ZqWB9OWI3ly9gDZWDMmURUU3XvvSMvwWjw6Qgpdxi5GAF3t5mhWIPfSJL41JDuRNVI5PB/vftA5CnWpa8fPmxxkJ8BwO/RnGoy3GgFGFwRuvuLiQgQiHwjCq6czgCKaw1FPq5HaTyJBdCfv+gDG8EjvnsDLzWQqQkq7obvY7uSCxw2lomlXp4cPOo5dbwTSJg2OessyL4rQQJZjw78etOMjOx1M+Q4sVlKbPdd/qxZpnlZ2EG1oUjRM7ZMuJEVfxSy7KRt9GG0P4pqMX7uhMmjF5B/H/re/GujrhISA92cPc8MQX+IA1z9ZHUrHgSMLoSVIf7pwB335sQe9R6pR8xjgoPorFzQ7/kEiwQEAwBOLB9g8uS68ypaUHMixM0RSqaHwaCmhJ8YvJ3z3y7qDYrJWSNmL1zzAjPdbHtvO+u9yssevvuj0/RdjI4U0lAYbT/RsSWMG9M+ojRr/CpK3tcOGE+fJg6EAjOnJXKxkGhxdftM8Nr9ErlyQcei5iNoNbzC5yrytPOM3QvczwiIpbWiygI/+IouMM1gnY7f0OfUFHEyMMOl4hEc8rBKiijJSSbLnuTERLLHDkVlvoCz59D1VUuG+aCUaaRb3vF6ZKsEBAL7h5x4M7K+tBi0prtJy/PCxL70RgGWscummSF7gO8Rw0W0SmA4g8Q9SrEBOI3I/51KFTdxgp6CWQfO2S/L5tbhtDeye7CGqnO8oVeDuB2kD7k4yUkhgyeUFxc7DB/aU1lo8Bc3kClsmecWtwDbJ1pMrCwF7yXifBK6TuVY6iZ/46+HfLnZQ+fWcvvbPAtSbKVZ1YMVYMVipBbvIWf1slWOaHfXIi1YtZlkM+wJHX+a9zheP4HW2TBt4qoTSlm92dTLAAXPv1+2mQhDs+xu1hDVTllIBnXuyua/F4PZnE7NcJR4duIxsZNSYK2aBzx/HdoLL3sVsnuj2y0gKyUWzRi38i14FyZqbSHmLgnlmlrCFaQhywty95kJBmEsRdYmY2+hKTinMkUqqKiBJlyU/zhhThxnptE43NQ4AkPi9lW+gUueNQ0A8//HF+HnVjYy4Wx4/vPT2xlzsf3pOkmYVsbOTk/SipzTA/km0Kk+2BPvI5i3iuAUKuGPMyueF7ckdCe/zkTrAAWDkpP/hCagnSTJVrVNQYUsAdj4gCzAROIeYC7Z1VoFhzzzxqlPJrvPbQGqn/2A4RgDif+J1AcIHY9UFXUoqLW8/lEjfZvez9lOEAwvZZ9OL1kTFUHVDBFkH9B//aiRl6uUFAOFBd2xLfJa2mxJ0pEIyIDURk/Rxq9u+St8VedTFc19Fff07H5boiRsZe9NWK8aicIvcN7hMnAd1LRDyNGbJzZl8whXtl71uVGAUwP6MrHfTZdn6vmlXeB9SEmDkHULAASQW/j0wELpL4tHoIM1q+MpU6x/JB4e+H3oAZv081V9ADroMaweBurQtfa6wH+w/imenWNh+ipFZQe7R9UKsno9fhU2uBZG6gsOLmb2MMpuBMWJNqJMZAQ7jfsubtpyTeL44nkRT8cOxIIGwmjU9jt6CA/CrfKrgH5s5UYcfhIiLqJI+jLIVHn+ygbG0aLoUVoy55mtdW3aCkpdb1GIR8G9ahguwIDzvWKIy8GQpyKA9Rt2tpzMFm7gWK4cz3qrXHg==",
+					"keyMaterialType":"ASYMMETRIC_PRIVATE"
+				},
+				"status":"ENABLED",
+				"keyId":357749026,
+				"outputPrefixType":"TINK"
+			}]
+		}`,
+	},
+	{
+		tag: "RS512",
+		jwkSet: `{
+			"keys":[{
+				"kty":"RSA",
+				"n":"AKZtuHAGYy-1Mc78sdp1gOV3jMCJtO7NmhyLSproWcBnqSN1g9mB2EdB22-WLWhB_U_JlZRCdHT6CxPHSid0c9JJc-2CmiV9zU2sVTJUkCytOVS0hrcPEz5JK6a6VVy-Skc_1-I0D2YurXd0aRByDALC8heHMok6VQXW8qwHgRyc0Jr1RcbY-CF_SMlRXn88g4e3bnk1AJiPcmHsJOcwkanwlWxq46DxPv5ff0ruXN4gPDYU-6_J6yZJreYjwrl-LhkqzOkz6e-LE4sdI5WFJQR9cGGRMf4ktgF3kqFtcFNFkGtdOvw5MdLe0eaENDzZ8TZyQDgiHYl878x8uPPpmoeif5af_ZUAsrv_bV-h3RpSoTdTP4SlQMmP-3y2R2LxvUs_CiUahoVFwTt_bRHO0Qy-QwpTvAdJX8CzrK2auqycFawYm8xYjj_epTFSwBCJuZjamxpZSa29zTDqP4AXwt2-9LO-70j5muzDQL35czpBgaXSAEJkrM9du91OjkJ2vtYFVLjWougN5uVpEBx1Isk_KgreOgl3lF1vs2EjTuihaxJhM-17alJLmDL06ZEDsht2Uhu_ZExEfPwTKaR_-kfjlamuoLUvTtVhzNZuOHD_XAOrGafMjM9WVq_D5XjqF7WFnb_t4YIOQNmGeOeIFLb4LlR5nHB1HIHUpAWazrvl",
+				"e":"AQAB",
+				"use":"sig",
+				"alg":"RS512",
+				"key_ops":["verify"],
+				"kid":"fVf-Qw"
+			}]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId":2102918723,
+			"key":[{
+				"keyData":{
+					"typeUrl":
+							"type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey",
+					"value":"EosEEAMagQQApm24cAZjL7Uxzvyx2nWA5XeMwIm07s2aHItKmuhZwGepI3WD2YHYR0Hbb5YtaEH9T8mVlEJ0dPoLE8dKJ3Rz0klz7YKaJX3NTaxVMlSQLK05VLSGtw8TPkkrprpVXL5KRz/X4jQPZi6td3RpEHIMAsLyF4cyiTpVBdbyrAeBHJzQmvVFxtj4IX9IyVFefzyDh7dueTUAmI9yYewk5zCRqfCVbGrjoPE+/l9/Su5c3iA8NhT7r8nrJkmt5iPCuX4uGSrM6TPp74sTix0jlYUlBH1wYZEx/iS2AXeSoW1wU0WQa106/Dkx0t7R5oQ0PNnxNnJAOCIdiXzvzHy48+mah6J/lp/9lQCyu/9tX6HdGlKhN1M/hKVAyY/7fLZHYvG9Sz8KJRqGhUXBO39tEc7RDL5DClO8B0lfwLOsrZq6rJwVrBibzFiOP96lMVLAEIm5mNqbGllJrb3NMOo/gBfC3b70s77vSPma7MNAvflzOkGBpdIAQmSsz1273U6OQna+1gVUuNai6A3m5WkQHHUiyT8qCt46CXeUXW+zYSNO6KFrEmEz7XtqUkuYMvTpkQOyG3ZSG79kTER8/BMppH/6R+OVqa6gtS9O1WHM1m44cP9cA6sZp8yMz1ZWr8PleOoXtYWdv+3hgg5A2YZ454gUtvguVHmccHUcgdSkBZrOu+UiAwEAARqABE1h5sfvsF6WWTpstCVnTS9kjsVXQhFm96kd+upb7p9Pk40xLsULYox/SpBvu10mkalviWUOISfiuxPPLeN6ef/kt0pP12xnOfZLkrF8MC0Vvfpslda347KqQuma6eXddJv8S1yZ6C8StQU90zwaSwtdqULXUeAMh0vXza2/L4EmSLhEItV6PKUWkblJZC607FNGLs+cnVJSIFT3f5EfPBtQCaoHaR+EDE4qCP3GJtgBFP3wc7YgpH2A9KJ1Li0hRj3dcLldsf/3InckbU8wQS39RSuYXy5T02yLNFpqkDenuKazCqIL1ea+Q8py3fcNPuKZ7NIsyp8KwFTMCRMgIwD5dq6l0lsNZ7UMx2/5ex5LEGlTmNdQZCZivav2hQF8/zeEWzq4dH+hDrNWSwIyMF1t70mxChMAQ0RAzH6iteCQQFnLIFFqVTiXIo2FCwwlyg2uQ6ASJvnW4M6ftXw8ktpLlPeP9uDpN2idBW3kO8dLUfQbCjIIr4cQozQvYenVkMBAbXjqORFK0YRp7xtUNeV5i/y0Dd8tKTmVx8QwGaI48RLVZUC6xelFugbP7UKCkVTPw204JbQGj0Bc1o+KM+ekEWd6Z1oyQQEE/tx2pMsQwrC5FrOv6LtVCLTyQrfHmrENpFI3MRyHJsBFSO0UrDFu9CSCsLSvGjM4eAlI+1xhIoECAP9WTkzedYf0VvNI3oMuENt4nG1CLycY9ZoUmebVvaR6jcFFHr8AxT0JGt/ZdnSt5iDK+VC52Z4kjVfiyJaj9O8PKifKiGho9IpXbd57k0lhDVwEZ6jLJ55y3KJRBcXaTtqodO3KsP8Nix2mcInQvKT9y6ZY7w8PT9WOrJuXtClc3CvgK5LyFQLRQ8dsCWclcb2MWD7IKBam1yvdd5mtCylsF0mnSoLfYPFcPAZ/O0zKCQOtyCm1duEfuBlef0mGwYAJsvKvj4N8U10Yk5TNr4oZM4olP2WY4Jf4fucnKscMxwkkbSVOOjms/r8NEBUH6XUpGewUQyaV47LPcFsvw48qgQIAptxTtmGV5XcQqYJJ3bvPAjm03+wr0A32cr4Z0cnByBz/dfNFxacEm6cWKflsu4CB931hDiI0CLveTgElNR0TKdNG5tpM6/17WOowACANRhLjEMH+p5A7zpzAwJrWHEh5qrSpgPm08fJhrUfyWoRZ7kxXm7SoVHWlKvAw4QR1PNPYxcg3Tm1zgZ40/gYn3JSdnDf1KN25XRfxrHgSVbKl3XRL4+6TgzTyu7olONlYEXjpxuuX+UMyTX5oozyxNAC3UUHNXlRPMWhKLy5vbhLDsk5LFwM4j5PL0Edj6pdfuegclsZYqxwWXLdHWu98EKUdZaucFVFoHc77h9OgmSv/SzKAAjhOW+3vkJNuek4j342l9umu6y/czHEeu+pCaL3SnINM0z2vdFxCWzxeaaK7XbfVMU5B9ECs+yQ4g0LCK+GsPjMJcQ5dRz9fBa4MIZpSPeSMllmYTxOV2SLDyYuxukgrIABv7XkSnX1hCzB6p458jV0E6ofATNdRVRWO5Nla1svYQmUahgFdiOyaIQw08s3gH/jgngUaNlzoZcKyj9E/q5pyz5/aWEAL6mDPKh10qSsB0oMRK3anIZP7XqmZgRBBuyH1AZUqyccA/5Ej/kduJCub6xWnqRdKYxygG7v1kyVZ1/pYIgl7+rMFRxfyVX2NxRmk+qZowXYcz516yRgSrFk6gQIAlvfbabTrKTzLv4IZENwelHXfl4WXslsfsnsa4zt273aFD5O2efj961KGdB2u6gqADIrM6Du79nb70Hmqz15p+zqj+LRkSlQCaNUh7ssRF2h5Nq0+mR6fbfVXVCwDMn3ETtW8UuwacZmKFHx24rzCnR9HWKJgdmImuS2uG7ir1ggaJgBbQcM3cXvRmE+7exCfdTsPvhS15GuIhjHw7MaA2VeiXix6HIkoYP8vNDs5Oj26zfZUfvr0JTcMtzxvW4yWT5eIlyMSr7IbBIsv2Fhz5Px/ZefNIeJn0h71YMfqnUpLq4LzsITuGp7cmYL6Lhkl+toEkykfWXDvFNo9gLhU90KBAgDytWdZp7okr10lBmVx+V5mMkmYv7Pa6H2Xp+Ntgr5JxGac771oZs/46EQ4Kl7F6+OSDqyL0d0JVgOYOT3toNnEdYEe+Pv0xfl7PKG2OV2v7+Ud0Ko4PITt9tYUrBHI/LuDJl1D9MsEDwEToQIFhNjgfNlwHsvqWpOWUo1Km2h108cubdC8wv7pkMCJJagOb8XsfnYscT+FCQHOGv+PRIzKTxU1DtZe07i3ZTkvRyYh2e5PLvMRFBNM0RudybikzECPboeWd8EpKY2RUaesNZoXmpPeFh/LsRZQfgnOt9trxQGtKmVUT0b63Jt0sRe3ydYuYldp0PvO0CsClFihj4tv",
+					"keyMaterialType":"ASYMMETRIC_PRIVATE"
+				},
+				"status":"ENABLED",
+				"keyId":2102918723,
+				"outputPrefixType":"TINK"
+			}]
+		}`,
+	},
+	{
+		tag: "RS256_NO_KID",
+		jwkSet: `{
+			"keys":[{
+				"kty":"RSA",
+				"n":"AImrUP3PDttint7alBxKexY-Oe4nCj0TOZ06yuKgq7UQu-3Gc8KJyQHO5SzPlMBy6FjcWqOzz-kkNm9sej3AsdGhTJCcOCYDoLgArYCaMQoMLOOjMQJTVbHeiPpyVgHzvpG9Xw_IVNPbRJhsT4mzqHuyopUEEexVQcFo6F3U8zE1kppxzoMvIiz5-Zm6dFX8EozolMD2TLDh4NZFAb-6uJs8TYzS8Od6V0BVh1CfHL1CuIpvIirkgki2RGXNE1r57bhJfMZUWtqAUXb5SM2IFhLUcgGLV-PfxP2cxcJ7HHhk5-lFf5794CmqcFa4mliR2tJRnhUR2vmlgxqUjzwK3HE",
+				"e":"AQAB",
+				"use":"sig",
+				"alg":"RS256",
+				"key_ops":["verify"]
+			}]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId":234505441,
+			"key":[{
+				"keyData":{
+					"typeUrl":
+							"type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey",
+					"value":"EosCEAEagQIAiatQ/c8O22Ke3tqUHEp7Fj457icKPRM5nTrK4qCrtRC77cZzwonJAc7lLM+UwHLoWNxao7PP6SQ2b2x6PcCx0aFMkJw4JgOguACtgJoxCgws46MxAlNVsd6I+nJWAfO+kb1fD8hU09tEmGxPibOoe7KilQQR7FVBwWjoXdTzMTWSmnHOgy8iLPn5mbp0VfwSjOiUwPZMsOHg1kUBv7q4mzxNjNLw53pXQFWHUJ8cvUK4im8iKuSCSLZEZc0TWvntuEl8xlRa2oBRdvlIzYgWEtRyAYtX49/E/ZzFwnsceGTn6UV/nv3gKapwVriaWJHa0lGeFRHa+aWDGpSPPArccSIDAQABGoACOgE5vcbpLpxt7d3Qu97R37xWMja2xKb+BnZIF5a04jRryjJsgdIGJEHlI61Osot3xEEL25+egU/ls6rUEoLHKVk55lA8BCBRLlXyxJWzBdW9cChJNP6hw7DMrCFShb4KVGOi0waIXz8qtsIj/RP6cCwC/qBZYOdHLlOiXC6mTNv0blQ2Cb9yfZZ1Lz855DH0l2/GMdZYXwb6JElM+u/vR7lxTp4Wc6kq/31PULDH7G+Ps+QpXxHMIqghgSWyRsJ9+SHv5yo7JxA58eTQEUXkI6RCJJQ3pSXjdveBzzPyN6ZCmjz91Np3oPh36dZtknW0UspZ6Jnpc5GLphkvG8GblSKBAQC/vcua6r6FGW0VO2yD93nWgX1qepmULYGw7lv+mfOvodPUr+8EqDZXaRzUqCHynhVfb1BDEsoxP9aLoPVFZoJbL1MqBnUx6X0FXoKu2FzqsEJYw2qnl4VLhFn7xebnR+vwv+MMYf+yvnIdcMfmrZhWmCS4hTFQlJDfxji2SPSdByqBAQC3znfJnB2xC7eDUCTSH49h/xW1YWaS6nTqXvk3LJeq4tX2WGBWxfCLh6xpNpzF31xCDdYlt+yGcy6UUBKr4TteePrWf6jY9TWJZO7FvAqIIIxaQv3a/0A4/sgzYcrr2ansWzhNtfCESxOaPFVfLE1wh/PpJBzbcltRbG/mEY3UxzKBAQCfvXhN5Pm6m1c0lCAwxVE88v5QYjlmqI7en4YG062gCbsX+0au45D6O7joNfaqUSdPLcZ5SsMmSp/sDbmpCuDZJNEtNtoWLgaZHYbUMa8fWp67onpNiz9ija4Fwnc/Ab1AAi0fGNnUyTL68gWoWcGLiw80pspR7qPPui1vN9KKqzqAASl2qg8Q6KHHwt4cdjHwbKfuozcHgdwih71XL2EC7jPed+XaieEJRfoz4PDbIQKCII3GEUjw9Kpf0WIjrhKX/IyTPgKlSbGnnywfWL3CbZ3HueGiuyFr81DoKMFujhgmQe7PpSPipx8w0Hs6oQeXNuDryloNi3T1lyQHEjcUPqqBQoABcIm6r6QyTlBactKBKEqyhkXF1tCvw7YR9herJoubM/xklWzU5J8bgSQ1h4dutlANutXFqeOInUufyPChP3inQhcirp3CccJFaMP9uevRMMhUxyOyQkpOfxnAe7hvCjRsDDZZqh5bi5siNzeIEnU1s7sq/0XvzZA7G5fGZgb+dZs=",
+					"keyMaterialType":"ASYMMETRIC_PRIVATE"
+				},
+				"status":"ENABLED",
+				"keyId":234505441,
+				"outputPrefixType":"RAW"
+			}]
+		}`,
+	},
+	{
+		tag: "PS256",
+		jwkSet: `{
+		"keys":[{
+			"kty":"RSA",
+			"n":"0JqDlgy_KaDpCWhaB95cKdLsyBGCbh865tHHK3LM1Iv5qlt4eqO9n2Bn5R5_ZHrMEGvVoBmwpkfnWmaMxqZg-69k8id0dN4PKeBuIYeO5C2IE3D0uO1UWzsPi4XHtXf3CYmwYOUHJ5DT8q_jgMXYCefys4OvYkRcfSpWVvFtF1PzBSijQaxDQUx0rdJvi0JZTQOXHl4MwgzrFoERTdZswAXh21MK1Uav68Aa_Z8TZU3R_qY-TX78qhBCv8T_1wrooprF_xaJqpywXktUnQxVgu-aG6-yooqrICvobc_LHdF_8R-Qp2pYfsHSmPDSKu-5JqyyIIoxfXpLdUsrDl4HDw",
+			"e":"AQAB",
+			"use":"sig",
+			"alg":"PS256",
+			"key_ops":["verify"],
+			"kid":"a4D_hA"
+			}]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId": 1803616132,
+			"key": [
+				{
+					"keyData": {
+						"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey",
+						"value": "QoABPzsxHq7K5f91YucwaXUDk7ERgE8pqLSc8w34gEnc/wo5vk0BamvQaWRVQQdzEfK+eqVbrHmWi5mhY9QXpOv0dhuhyvo8ZS0ya60cT6DYSu2LBLDHFa68Wp6SWbIwFN4X5uGC8DYvWpJU9PCYg6XUu67T37FhGFekGHTSXDLf9Ko6gAFm7TJOM/v8MbHkCpY5NTtda7fb09XBXFDSC2XFGKvOkfQrGEKdEAvOCffpTBHsyvZAEJag/p2OZ+4W2D3upPNFkrmtS9MSGU39o0kn2fd6Cw90w5S1gjfxgWDbZpzs4AvbpU436Zy2wZYjJSIG6xbjDuYwizrflPX/sq5GUpuCuTKAAW+ovScT/DR/doxZm+xykUTTfEr2W4pd5PpLQiI1gUA2UTnY6p0svW+IbbSaj6vTE8s6+STsTGYAteUgdFBo7Ao501XbAJpJQX4ONI6o66BUvvzy0S6VLs+YQ6MWpArvNnnzRo5NbznO6IESyumWNm+8HQMaJ12sAqpWOoH4bz1xKoAB02eSVf5ZSDiYa4uF85NvvAVvEVPOPAd2gOqXzOWH+AXtTHJ8n/gcvUMnFR3W7cdZdyY2HslV0qphvkL7mCwsoOUBH5dA+F10Ebmk4hU9XEkeQvgFVgffzyqKjG521WOnAXQXudhOkJgXqGoTB/fESyRvSqA7ZKwPL1dvZnpJRv8igAH8m64q3qJFFcHWsnUb3hS58BXm8aTuk8Reju8XDXjBa9DPy5UySS0P/Chyh8HF5PAIwWSXTYDtFvdve3UN28oxTzhZ1xsz86BOeF2lFHpZ1y8/uNzwLRTIYWCXhbAS+bGpQOUR4JJDjSyivJCBqrkMCDUWAXQSqIZzHnyD+wbP8RqAAkukY+fCuoTpXOd06ASnbIsb+ZF4y++LsoulcQ//wmemVEOihJcQDgAfcL0j6HTylFG2EJJMDoLVWv6sZgrYpR1O1g97IB8KsLvyLm1JHxb9rbTDBnKSWL72NSZWPfs/Q5y5SXRxSD1gJoL/pcL5uuOosJjIvQ2olVMryYAgbnsA5UHZP7N8YpX0njZxBl9/PFNrTkWBMr15+A0VqOGh0TGnE/D4iAAduMJn1f4a3ZYVC4FgxKVxLxkB3oOLZz+QXKvs61slwRjotY3BXoKeImedOFmZoOJCA9qD+9rT01mQ113Fi9ylkBD1VGqtvIoB1CZa4tZZkRyoAeIMU7vMUpESigIiAwEAARqAAtCag5YMvymg6QloWgfeXCnS7MgRgm4fOubRxytyzNSL+apbeHqjvZ9gZ+Uef2R6zBBr1aAZsKZH51pmjMamYPuvZPIndHTeDyngbiGHjuQtiBNw9LjtVFs7D4uFx7V39wmJsGDlByeQ0/Kv44DF2Ann8rODr2JEXH0qVlbxbRdT8wUoo0GsQ0FMdK3Sb4tCWU0Dlx5eDMIM6xaBEU3WbMAF4dtTCtVGr+vAGv2fE2VN0f6mPk1+/KoQQr/E/9cK6KKaxf8WiaqcsF5LVJ0MVYLvmhuvsqKKqyAr6G3Pyx3Rf/EfkKdqWH7B0pjw0irvuSassiCKMX16S3VLKw5eBw8QAQ==",
+						"keyMaterialType": "ASYMMETRIC_PRIVATE"
+					},
+					"status": "ENABLED",
+					"keyId": 1803616132,
+					"outputPrefixType": "TINK"
+				}
+			]
+		}`,
+	},
+	{
+		tag: "PS384",
+		jwkSet: `{
+			 "keys":[{
+				 "kty":"RSA",
+				 "n":"rMnTRrTk3zWf0ZqukmshN9GH9UsCcD0a2WlmO-0q7x_k31JIe2wtqhlQRwszfuOJmL5M4cpsvkDBT8th5yDqzzHMJRAs61Jq6ACNepj3_0hK8GszxiyxFQL3msxmu8e3F14M-V35n9aLr0meRHk9tzm968-wvp7I_IXlv1hbzHejh_gD14gy-GjdiJYGwg1oWINL6YzSv5DISxIAv9HLu5fmBLtoVyvU9iZLHfUJdq3Rlj5iCBUEFMJVb68PfWiB_xoA7nj3vpgAfGjDzQ62bVrVaOHOg2I4X2OxJBWJ8uFw6RRocpAfD_lEZBet-w6FaMHXh_iVwxPWNuNTbVHlerfdUHTMHO2jCR1JKKkI5px7aVM7fQUVtYSBk754LINhShkMCO9o--k7sZOFL_VohaCHtE9fRxIM5MYOKPyvPTf38EyCrAqreFd4ol0FCPea8n89BwV371GrXgP5C_9BdoG2uY6rxRwTzMNiLxzxWpkvlprNRxAsdRSZPEzKOI_t",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS384",
+				 "key_ops":["verify"],
+				 "kid":"LFa3bw"
+			 }]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId": 743880559,
+			"key": [
+				{
+					"keyData": {
+						"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey",
+						"value": "QsABGgBRiQlPP7T0l7qjag22t5qPSbLa/PkaEnEatcxTqtJ18qo9ncTqNa7Ts851twenilUdELx+HLARFmRtmYcJuanBNMIrJ4ua/I0+rWY2rU/NwxB39x3SglT2T/wvwOx0fZAhXPNqgF6m2aGLMsppgDN6TKrvWZdC8YTC1e+ZoDlq4miBuU+NEOOsrS6Zv0SW/ZI5OxUqvGqQwaaiiy0VCsIzpJjO8GkaPntCYnA6Z1MNZTREOUcncMg2MsPEmYslOsABxBOOesQfs2aiCmt50XjN9bUvTlu/Z9z6/k327kwLpWloxZuzZWz1LMpbchnmrNvl8uj+N8qSvaIk6/Gq2y6w5TtDEdELZuuNcCkUqnnOaUJyVqkZ10PwILL0Ig+tt4GSIGlEFmdt1cL9tdU8Za/IQTkYzDQG11iG6h6llYPmj4aJxeDc/wnon5dT1pMuW93uFygwXpSkYMIvzDBQys6sUGtRbPVjNRsndRVl9w8oiF5wvEeLMtAMpAUgFxmXdC5JMsABHN4zpQrc8qsuYZa57/5gCmi4qGhECQNdsJlu7YjqjScBcRQZEK5F4pUZl2lY4zGQlClRnXUgx/g6F9FGW/ENnHebfYQ63eg2wL/EqvWBujDdYjYvs1oUBXcMFSG66VAkOYkkS8a8JnQpOfEPCkvo4/Hmz32YXjExEZWe450v8KhE4JYsaEolyoH/EoDAfG++NoIfUR6A+slyXqeQlnWK8+GMitoLKaN6EMdc31YeVsioEhn/rFfzd7p5FlLbjqBJKsABzisQA/QhytqNUWQhnhYFSs9QF+Z10ZCuUxwaSZKmD8SV4JTiHcMy7LK7RGt3Btlf76HmTNVOtTTsjXbBftVv4HDNamPmtzg1ggZi05cjPYi3STFZu3lUVAv2tJP5gdjuMe7slW+MqECUfPyz7OkJRBVAPQl0fbH/FSeSb529H6R+/1uXQ9nmXmikUFEt5PvY77li7Qyb6p67B1krBQusW0Lk2SL1Fs8Y8bj/lkjJar86sxGIGl2JNfSwajyK/waJIsAB1o1XIXWE82dw1r/TmkhY+bF4vvApYMYSz7lhsK5shZcY6VeQMXNUY/SCMTTndHzUNmbwdi4NCbnNt/vEOvmZnvQ2Q3YNphd6BLfeZxEmBcPzUMDTKXNaZBLbe8j1HUtaOHoaCfVuLhxxDT8knntNZNIJNuGhAK8YweR96qKQSDyL1zZRXBqnPZlGNnVCDVx0ijMmAmAY43IC5/XCR5h03TwbiJTQ5tG3FImoSXqA7RmwTSr1ynR4EKmRWt34uiVFGoADF4o9gu4FGlXDarpwmxkGQwUESUpJUEI65LDD0Vk71q0ZMMWUg2AXDov5UFx5zQkxx0Hx1ncN/pNy4qyaL3NgGg82OTxtajflwarFm5S4gKp4Ly3jtVWEYJDxa8D6JA4O5xuUl+qSJhEEIcLdUYXU/x/aPISklyupxSF2ze07QG1yNYV3/IadLxOWTtPlos1R0HE+x9g8JAYVC4kt2fQ6ldmZaD6h9fJORqSr6i5mdikzGw1vrJs0XaGmIxuN+C9jAS031tkD15BgK9vd6wrlT9d5C/KDJT7zJShYnNTJ2E9vRXBby7AaiOGjeRx/E67oPzdWH/8qwsLNfkS4eYLT9nbwmIMQ7pWVxcatnWKzuQuYLpCR/O2iJlaSoO76Xuy8RklES38lB2+FNzHuHtN2xAPms74WAUX+dLrIlcA7ceWwUqeF8iyXL9vmCuMmd5kHZGxUJbzVpLOkRUdcDNtc1qXm8qufzWABOUtzVnkn1CuejH/Xv9IpbuCHhQEv8o4REooDIgMBAAEagAOsydNGtOTfNZ/Rmq6SayE30Yf1SwJwPRrZaWY77SrvH+TfUkh7bC2qGVBHCzN+44mYvkzhymy+QMFPy2HnIOrPMcwlECzrUmroAI16mPf/SErwazPGLLEVAveazGa7x7cXXgz5Xfmf1ouvSZ5EeT23Ob3rz7C+nsj8heW/WFvMd6OH+APXiDL4aN2IlgbCDWhYg0vpjNK/kMhLEgC/0cu7l+YEu2hXK9T2Jksd9Ql2rdGWPmIIFQQUwlVvrw99aIH/GgDuePe+mAB8aMPNDrZtWtVo4c6DYjhfY7EkFYny4XDpFGhykB8P+URkF637DoVowdeH+JXDE9Y241NtUeV6t91QdMwc7aMJHUkoqQjmnHtpUzt9BRW1hIGTvngsg2FKGQwI72j76Tuxk4Uv9WiFoIe0T19HEgzkxg4o/K89N/fwTIKsCqt4V3iiXQUI95ryfz0HBXfvUateA/kL/0F2gba5jqvFHBPMw2IvHPFamS+Wms1HECx1FJk8TMo4j+0QAg==",
+						"keyMaterialType": "ASYMMETRIC_PRIVATE"
+					},
+					"status": "ENABLED",
+					"keyId": 743880559,
+					"outputPrefixType": "TINK"
+				}
+			]
+		}`,
+	},
+	{
+		tag: "PS512",
+		jwkSet: `{
+			"keys":[{
+				"kty":"RSA",
+				"n":"ubM3lgyGn8IyKO-56q18hvuJkkxPrDXgalRWNmnA3QEseglU_9tp598dlq04eF1G4Xkrmk9OVyVSCuRdvMoko6wP4Jum-3cn42_Gsk8PdTwm3WD-yEBg_Usa_omLGiTfktyqqoZhh1TeOOBtNpD1U_p1wQxP3-bLl4__uR75CqlK9FYdBrIuqLP3nqa3_OAFuPBX77BuD1kcr5pUxPZkXBNAWpnvsW56swyIMZF2GRhfv2n2bZJgT4iybQcmEnvt1wfY3ecO5ZMSX2QNKpnRRejlIEqR9uAQa4wIJMViL8jDbAV-ZvUjMM1G0aAyMHPQzb2Hfkr9OtEi-_xyUCwqF2IUZfUb0-mCjOutpbBlSfkYULOrwd9RQTaLeNe3GhRjYWTJ-gLDS8DUWz8AcpCI7xoQSfuZLmBwxslqsObMYolxQJXej1IDmGX-Rjr4ro80EpMkv67gxYQwjP8p7FMHfK7FSDZMtT-h4mO7AD68vwHd99c9ALDJfPO7tAMG53opzD7YEZU-ySKRcMBIFRe5Kxj-m1fbN9q2ictzoQOvKh8TBlCsPLRbF5WVheUtE9anKiIik5zQInihoZidH5YJksdipMVWLeRs1Qk5J8ddv7n2dlbW7zoC60sh3ubLQ_MDm-eHlXoeKGioCMjDABRdokqal4wugvQUZyQcBBtfWT0",
+				"e":"AQAB",
+				"use":"sig",
+				"alg":"PS512",
+				"key_ops":["verify"],
+				"kid":"L-LcIw"
+			 }]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId": 803396643,
+			"key": [
+				{
+					"keyData": {
+						"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey",
+						"value": "QoACUsRKIPRhEtXtTxcFVM0/KMBMyzrafB6NNwb8cHSM0N9XGZEbeUh6EF5JGsbI0PndFyMk2wdhkCdtpdH7Bc/n7hLlW7yVR9fvPQMDoG6mITa0E2XXDkW/iJZ1cZhkiR4ptWMgNKm2xLlxOUTGcVr8+jKQ0Tb1TMsvojs3GeBLJ5jDtzq3HE6kcNY611L/hzft1aOb3+zJGRZpLcN3CuXVbhluTyrccl4V3jWN1KSejvj32zn5l0hRMYES9Ek2h686a+gqK4RYkbeP4QL7ZnT2tkG0rxfi5HlklmLn620YTzrlYpGd9x3ID7NnMjDfTz0mR/910p6JzVBloCbJ6Ai/JTqAApRrAvbP7oGaPN25FupqEWCrTZfpmOZuH2NT4h6KiB6/RxyrbRQWSh6bpRXsS/C8aHlnSj83nFT+G1j7qLINDbqHlrYD8aycRRuiLm5WWNtO6wQzpXmWmrSYutln9Yj6QWtIOIA0Pn4b1u1Aj7DudBpKhd8feihkZa9AHqmsolOi9FKILQ2FwAfmEGDXHtRjP6KrB6bMbg1XuLXrJT6xEBLyfSswsk/UnlHG3+q++jDp5tLPJnmqDgPcZ017PY71JoHE8QyNu2d4+Ng8+wOZxyYWPOvfgC12ZFaGso8do3+vG8C+HEIiHM9+brv4SyWaVZxFt3jn/aezXDlXbIsG4nMygAIJ7xT/Qz6vOVZSAvqRSVMXS20Awi1TnsgxHbUzImi6KMBRrlyFud0ltpQcZw98jlo5qB11d34HFnXTK1TOvNiB61Z2olr2+4Nt2MFPRu26r3uR3mhpacHW+TfkHw5whudHpybXkFc2asiL8auAToS2i2pr1hSOqKUDI0B6qy+qjDjWUCDziJE+IcpWjTEY74UpE5rREBIer5Xci8FPCP4FFjfomAtZZSGgS3DHwnCh9NfqyLZTGdDVJe+MEMlAFFmFUcCAk708H16bqJ8UuJMdGoFqvxU9bJrLGDkAg/CttX0BI6OCs5DR4Rqy+XKHYIkIvy6DVFja3mmhIhAVXXQHKoACvAckkJ1ayoNwbcV11yOBd0qNmPl0+NWdGlkc7+Aft6rLAR25t2tpfEjsFFYEaNCQIlzJNLAXa41Ac7cGdOLx+nRAJI3d/ExRLXhJrbAD95YM6WSM8cXf0dsR+q3hoTE0522T1XwSXICXb1Z2hzfmghL5WigezMdsEolqF/pRpQUcnZug/mpa0P40evFEIsoiPpMJYwS67iETxKeeEJv55z1W5GkT5reEeRwkQIuJm3kZB2r95p2sU82PFyXMVjgnqcqUAKWudi+oRp3jhzd0IUMQg6gcm62kpF7XgQmobMPYloc2c5VIEM1NS52s4arADR7dFxU6R28paLea8LsCByKAAvzUpants7GpQz2rJ7Gl9x0uQjr48yetqeTyzxInjezcKGgO7s85c2GzO3MkeaYcT+68NXHtdUVXrXJYerAiH+PAA2CdouEg8ra/ZOl0t3x9402kkFYcwbzmI1O0TLV4kv6NONapFj7U2WYfj0IdVILYoJWS4PSvvMWrDzP2SlZ7alSZ0zqCUGYa47Mz9d9A7d2teQ6z3UdzrUw3EBWz83szslYXQg6QDtsF+PYUhNx0tBuAdUtF4kVFXPSZoaOzaKdYwxb9TApmRheVsmOVAqb7xtwo9WmqUuJgDADjlfxwA9cam+uggvogd7Ta3i48SbJG6RXboaydht1F0AYeKZsagAQVZNwC5x8yE/nFakDyvtlO5SHR/1qvzhE0ZCepOIEmCmGTubQs5JwMllGJWhwxucVVv/5Rq9CsYjn+fpV8uj6DC2qqMiSIag+SuKjymACBktQuGGOiByYQExwMC8/ry326ehPAy588K9SM8ZuDeCswvp/cWs0aUDOlGsuXtJrKgKXdr8zDnbmZvrTIzA+nDC7R7Kv6NaBTF613XwIPIw0oPSDij0OPHy72+9BLraTRJVQP8GbvSWLb0YraMW2lyYNQN7Djd8rpO2AYKfsJAmmax/HFyPGMuKm2SjlnSxo8bmvH69DGjyK7wkU7bLJQ5Lbp98DpauhGY3EdXispU2fnJkoa9DaDmEzArRGa+T05YCyuzezuYE4eBUlxXJj2QY5ABDH5VkxcnWPSftKUUG5TSRwnIKZQ2Ab2ONNOQDafSOsg2KYDBKmLw4ZxUp2I2izXPeICfCJ2sBW2IOwSK5tRcvno8QoMvkz+9Ci8QNRpNLYTiCgbxXaoW/eLayvKt3qhkj+rKMded7yzWjq2dNv3HfvPUIwtSlAHGSqEhGkuzSijHhp2s2LN5OB6mfQt6d4pzvlh5w+pxaK3sH/wsLoVsdvUg4OBaH+KBFVYRZ9eAQMU8a6fmoFreMpSiNS6B0jY7XPsCL3mgSAuzkojCx2YBh79VB9SjcKrGGdRYLot/RKKBCIDAQABGoAEubM3lgyGn8IyKO+56q18hvuJkkxPrDXgalRWNmnA3QEseglU/9tp598dlq04eF1G4Xkrmk9OVyVSCuRdvMoko6wP4Jum+3cn42/Gsk8PdTwm3WD+yEBg/Usa/omLGiTfktyqqoZhh1TeOOBtNpD1U/p1wQxP3+bLl4//uR75CqlK9FYdBrIuqLP3nqa3/OAFuPBX77BuD1kcr5pUxPZkXBNAWpnvsW56swyIMZF2GRhfv2n2bZJgT4iybQcmEnvt1wfY3ecO5ZMSX2QNKpnRRejlIEqR9uAQa4wIJMViL8jDbAV+ZvUjMM1G0aAyMHPQzb2Hfkr9OtEi+/xyUCwqF2IUZfUb0+mCjOutpbBlSfkYULOrwd9RQTaLeNe3GhRjYWTJ+gLDS8DUWz8AcpCI7xoQSfuZLmBwxslqsObMYolxQJXej1IDmGX+Rjr4ro80EpMkv67gxYQwjP8p7FMHfK7FSDZMtT+h4mO7AD68vwHd99c9ALDJfPO7tAMG53opzD7YEZU+ySKRcMBIFRe5Kxj+m1fbN9q2ictzoQOvKh8TBlCsPLRbF5WVheUtE9anKiIik5zQInihoZidH5YJksdipMVWLeRs1Qk5J8ddv7n2dlbW7zoC60sh3ubLQ/MDm+eHlXoeKGioCMjDABRdokqal4wugvQUZyQcBBtfWT0QAw==",
+						"keyMaterialType": "ASYMMETRIC_PRIVATE"
+					},
+					"status": "ENABLED",
+					"keyId": 803396643,
+					"outputPrefixType": "TINK"
+				}
+			]
+		}`,
+	},
+	{
+		tag: "PS256_NO_KID",
+		jwkSet: `{
+		"keys":[{
+			"kty":"RSA",
+			"n":"rzu_DRFtzFpMUy-tXC98YxtyASy-3hVtM1X9KiwAoahSfd7VfzIlIXcbn3VewkZBtKGC98sGQJSQWA-EagOjMDua4rAGVCZ9Cj011Mxy1e2j6w7qRCudtWaMormfMpP6n2ht61HkZkQDZIlbdRvr20Glf2KWgd8KgSoEZKS7AjIHvoGbJCU7A7ajbONyKuicrYq1XYs4b1dYSqQ4VIZaei5NQM7_tddYJl-lSKN3mLEPhdWKHWf1rVfDbJNobAbqN7C70rUKJS3DZkwo-q3-QOoZleJXKTXurdRAhT66nfa-1f7idmIO37LwReX8zrgDWmMZPZ2mpfA86dIlkkk89Q",
+			"e":"AQAB",
+			"use":"sig",
+			"alg":"PS256",
+			"key_ops":["verify"]
+			}]
+		}`,
+		privateKeyset: `{
+			"primaryKeyId": 1629784556,
+			"key": [
+				{
+					"keyData": {
+						"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey",
+						"value": "QoABP9TTJpZ3lfj28Zh9hqHMNydjyJGup+Q8xjYubqsE+E3AlnSIvRDp9r0VVHZzsHBEdKtQQgCW4FT0I7Cy4z4W3ecKskuJWFYYn0PYOXLZoFo2MF3yZ0wI04aWhRS2+Zwl3BSr1eu84jiCm9rTsODyZ0MQORvpeBVaX9Y2IOPclvQ6gAGBpXDhI/1yKJq6vlymUBwKS2FG9Tf3as3YkH2B0b7wtv1Ir+WEa78ub52BwxnOKsf3V57WLnuQppLiw/bvHFxKVDNuWGiGTzEVhJW2qK3RgryXtqzkACm6cjL1FT22B9VmVx/GqWOOOLX4He1pq+UYkboWgXVkAdP0OaPv2hWIMTKAASnEMbcFq+ZbOJIJBwZXsSmrdSnfg8A2kwuatK2U2Of7/YCE5i11CUjWUvi99plk8g/mAinYu0Gfw6YSRgbWsAvK4GsIJ4322WT1yy4g6XuncL8MKC2rCYIkhFWpI1qcsS/PxU3zWMYodV6GjK31HXvqczlJfBYNEBo9HxeYDtchKoAB0vRt2QsYTMSVYw1gIDeKdHnhMDaakaIazjc4o+DCQSk+dU0EStSn8GHON0nIrEA8A5UHqF8/yh1mW+M0mkSaSiBp+7CLAowEu72wgdrymK/e6eIELH+joEDDgWpcF/WMEWSvls2a0q1atiYvC2ERLuSxSFjoJ8IRKVfVmjPi53EigAHUpqb3E/I863RAT2ocS5CnT7A8PBgttZqIyR1H8iC2bocre8H+8z8fVf4SeYsLhqvuBcTPXxZSUT+ZVf+LeELfmcd54savTU/yTQJ27s8WIkuLeTj+80FWCVtengLwP+Bte7nyzqbuXSWHUTUSVTCMK5PiBdWrOElVYlp3JxvTxRqAAgNrTEVGQYjy+xnFbKHHmGr7olwVAi1lqCGQDDZKMQH2fZOQqURH13MhdpPEL8LlKYuLejl5B+hzLaTWOqxx4TmD9Df3nMwAC0ELpDUAfz4e2quvuRD28+cR9u0G560ON53sJPbqPGVlbtaDmpn8nzvCOmczpoGmtzcBeZ/4GeEHThzq1sRE+tBJ6B4oS8R4LUtldg+FBUnZgqJvSC1gYYHO7oySCPC5V0R3EhpWDcVbYf7PyMC7oaxIPmCAu5Wc4DFirh13BAZI2FKW+Np/heZAjYUKa4Gtb0dMxvLwz3OcPPa/AQKSjko6aMRAQvjgd/UgQ+Sr496td45I4JGandESigIiAwEAARqAAq87vw0RbcxaTFMvrVwvfGMbcgEsvt4VbTNV/SosAKGoUn3e1X8yJSF3G591XsJGQbShgvfLBkCUkFgPhGoDozA7muKwBlQmfQo9NdTMctXto+sO6kQrnbVmjKK5nzKT+p9obetR5GZEA2SJW3Ub69tBpX9iloHfCoEqBGSkuwIyB76BmyQlOwO2o2zjcironK2KtV2LOG9XWEqkOFSGWnouTUDO/7XXWCZfpUijd5ixD4XVih1n9a1Xw2yTaGwG6jewu9K1CiUtw2ZMKPqt/kDqGZXiVyk17q3UQIU+up32vtX+4nZiDt+y8EXl/M64A1pjGT2dpqXwPOnSJZJJPPUQAQ==",
+						"keyMaterialType": "ASYMMETRIC_PRIVATE"
+					},
+					"status": "ENABLED",
+					"keyId": 1629784556,
+					"outputPrefixType": "RAW"
+				}
+			]
+		}`,
+	},
 }
 
 func TestToPublicKeysetHandle(t *testing.T) {
@@ -197,19 +425,19 @@
 		t.Run(tc.tag, func(t *testing.T) {
 			ks, err := jwt.JWKSetToPublicKeysetHandle([]byte(tc.jwkSet))
 			if err != nil {
-				t.Errorf("jwt.JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+				t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = %v, want nil", err)
 			}
 			jwkSet, err := jwt.JWKSetFromPublicKeysetHandle(ks)
 			if err != nil {
-				t.Errorf("jwt.JWKSetFromPublicKeysetHandle() err = %v, want nil", err)
+				t.Fatalf("jwt.JWKSetFromPublicKeysetHandle() err = %v, want nil", err)
 			}
 			want := &spb.Struct{}
 			if err := want.UnmarshalJSON([]byte(tc.jwkSet)); err != nil {
-				t.Errorf("want.UnmarshalJSON() err = %v, want nil", err)
+				t.Fatalf("want.UnmarshalJSON() err = %v, want nil", err)
 			}
 			got := &spb.Struct{}
 			if err := got.UnmarshalJSON(jwkSet); err != nil {
-				t.Errorf("got.UnmarshalJSON() err = %v, want nil", err)
+				t.Fatalf("got.UnmarshalJSON() err = %v, want nil", err)
 			}
 			if !cmp.Equal(want, got, protocmp.Transform()) {
 				t.Errorf("mismatch in jwk sets: diff (-want,+got): %v", cmp.Diff(want, got, protocmp.Transform()))
@@ -270,7 +498,385 @@
 	}
 }
 
-func TestJWKSetToPublicKeysetES256WithSmallXFails(t *testing.T) {
+func TestJWKSetToPublicKeysetPrimitivePS256SmallModulusFails(t *testing.T) {
+	jwk := `{"keys":[
+		{"kty":"RSA",
+		 "n":"AQAB",
+		 "e":"AQAB",
+		 "use":"sig",
+		 "alg":"PS256",
+		 "key_ops":["verify"],
+		 "kid":"DfpE4Q"
+		}]
+	}`
+	// Keys in the keyset are validated when the primitive is generated.
+	// JWKSetToPublicKeysetHandle doesn't fail, but NewVerifier will fail.
+	pubHandle, err := jwt.JWKSetToPublicKeysetHandle([]byte(jwk))
+	if err != nil {
+		t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+	}
+	if _, err := jwt.NewVerifier(pubHandle); err == nil {
+		t.Errorf("jwt.NewVerifier() err = nil, want error")
+	}
+}
+
+func TestJWKSetToPublicKeysetPS256CorrectlySetsKID(t *testing.T) {
+	jwkSet := `{"keys":[
+      {"kty":"RSA",
+       "n":"AQAB",
+       "e":"AQAB",
+       "use":"sig",
+       "alg":"PS256",
+       "key_ops":["verify"],
+       "kid":"DfpE4Q"
+      }]}`
+	kh, err := jwt.JWKSetToPublicKeysetHandle([]byte(jwkSet))
+	if err != nil {
+		t.Fatalf("JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+	}
+	ks := testkeyset.KeysetMaterial(kh)
+	key := ks.GetKey()[0]
+	if key.GetOutputPrefixType() != tinkpb.OutputPrefixType_RAW {
+		t.Errorf("key.GetOutputPrefixType() got %q, want %q", key.GetOutputPrefixType(), tinkpb.OutputPrefixType_RAW)
+	}
+	if key.GetKeyData() == nil {
+		t.Fatalf("GetKeyData() got nil, want *tinkpb.KeyData")
+	}
+	pubKey := &jrpsspb.JwtRsaSsaPssPublicKey{}
+	if err := proto.Unmarshal(key.GetKeyData().GetValue(), pubKey); err != nil {
+		t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+	}
+	if pubKey.GetCustomKid().GetValue() != "DfpE4Q" {
+		t.Errorf("pubKey.GetCustomKid().GetValue() = %q, want %q", pubKey.GetCustomKid().GetValue(), "DfpE4Q")
+	}
+}
+
+func TestJWKSetToPublicKeysetPS256WithoutOptionalFieldsSucceeds(t *testing.T) {
+	jwkSet := `{"keys":[
+      {"kty":"RSA",
+       "n":"AQAB",
+       "e":"AQAB",
+       "alg":"PS256"
+      }]}`
+	if _, err := jwt.JWKSetToPublicKeysetHandle([]byte(jwkSet)); err != nil {
+		t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+	}
+}
+
+func TestJWKSetToPublicKeysetInvalidPS256JWKSet(t *testing.T) {
+	for _, tc := range []jwkSetTestCase{
+		{
+			tag: "PS256 without kty",
+			jwkSet: `{"keys":[
+				{"n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PS256 without alg",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PS256 invalid kty",
+			jwkSet: `{"keys":[
+				{"kty":"EC",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PS256 invalid key ops",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS256",
+				 "key_ops":["verify "],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PS invalid alg",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS257",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PS256 invalid key ops type",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS256",
+				 "key_ops":"verify",
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PS256 invalid use",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"zag",
+				 "alg":"PS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}	`,
+		},
+		{
+			tag: "PS256 without modulus",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"PS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "PSS256 without exponent",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "use":"sig",
+				 "alg":"PS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			if _, err := jwt.JWKSetToPublicKeysetHandle([]byte(tc.jwkSet)); err == nil {
+				t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWKSetToPublicKeysetPrimitiveRS256SmallModulusFails(t *testing.T) {
+	jwk := `{"keys":[
+		{"kty":"RSA",
+		 "n":"AQAB",
+		 "e":"AQAB",
+		 "use":"sig",
+		 "alg":"RS256",
+		 "key_ops":["verify"],
+		 "kid":"DfpE4Q"
+		}]
+	}`
+	// Keys in the keyset are validated when the primitive is generated.
+	// JWKSetToPublicKeysetHandle but NewVerifier will fail.
+	pubHandle, err := jwt.JWKSetToPublicKeysetHandle([]byte(jwk))
+	if err != nil {
+		t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+	}
+	if _, err := jwt.NewVerifier(pubHandle); err == nil {
+		t.Errorf("jwt.NewVerifier() err = nil, want error")
+	}
+}
+
+func TestJWKSetToPublicKeysetRS256CorrectlySetsKID(t *testing.T) {
+	jwkSet := `{"keys":[
+      {"kty":"RSA",
+       "n":"AQAB",
+       "e":"AQAB",
+       "use":"sig",
+       "alg":"RS256",
+       "key_ops":["verify"],
+       "kid":"DfpE4Q"
+      }]}`
+	kh, err := jwt.JWKSetToPublicKeysetHandle([]byte(jwkSet))
+	if err != nil {
+		t.Fatalf("JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+	}
+	ks := testkeyset.KeysetMaterial(kh)
+	key := ks.GetKey()[0]
+	if key.GetOutputPrefixType() != tinkpb.OutputPrefixType_RAW {
+		t.Errorf("key.GetOutputPrefixType() got %q, want %q", key.GetOutputPrefixType(), tinkpb.OutputPrefixType_RAW)
+	}
+	if key.GetKeyData() == nil {
+		t.Fatalf("GetKeyData() got nil, want *tinkpb.KeyData")
+	}
+	pubKey := &jrsppb.JwtRsaSsaPkcs1PublicKey{}
+	if err := proto.Unmarshal(key.GetKeyData().GetValue(), pubKey); err != nil {
+		t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+	}
+	if pubKey.GetCustomKid().GetValue() != "DfpE4Q" {
+		t.Errorf("pubKey.GetCustomKid().GetValue() = %q, want %q", pubKey.GetCustomKid().GetValue(), "DfpE4Q")
+	}
+}
+
+func TestJWKSetToPublicKeysetRS256WithoutOptionalFieldsSucceeds(t *testing.T) {
+	jwkSet := `{"keys":[
+      {"kty":"RSA",
+       "n":"AQAB",
+       "e":"AQAB",
+       "alg":"RS256"
+      }]}`
+	if _, err := jwt.JWKSetToPublicKeysetHandle([]byte(jwkSet)); err != nil {
+		t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = %v, want nil", err)
+	}
+}
+
+func TestJWKSetToPublicKeysetInvalidRS256JWKSet(t *testing.T) {
+	for _, tc := range []jwkSetTestCase{
+		{
+			tag: "RS256 without kty",
+			jwkSet: `{"keys":[
+				{"n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"RS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RS256 without alg",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RS256 invalid kty",
+			jwkSet: `{"keys":[
+				{"kty":"EC",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"RS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RS256 invalid key ops",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"RS256",
+				 "key_ops":["verify "],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RS invalid alg",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"RS257",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RS256 invalid key ops type",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"RS256",
+				 "key_ops":"verify",
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RS256 invalid use",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "e":"AQAB",
+				 "use":"zag",
+				 "alg":"RS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}	`,
+		},
+		{
+			tag: "RS256 without modulus",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "e":"AQAB",
+				 "use":"sig",
+				 "alg":"RS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+		{
+			tag: "RSS256 without exponent",
+			jwkSet: `{"keys":[
+				{"kty":"RSA",
+				 "n":"AQAB",
+				 "use":"sig",
+				 "alg":"RS256",
+				 "key_ops":["verify"],
+				 "kid":"DfpE4Q"
+				}]
+			}`,
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			if _, err := jwt.JWKSetToPublicKeysetHandle([]byte(tc.jwkSet)); err == nil {
+				t.Fatalf("jwt.JWKSetToPublicKeysetHandle() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWKSetToPublicKeysetES256WithSmallXPrimitiveFails(t *testing.T) {
 	jwk := `{
     "keys":[{
     "kty":"EC",
@@ -623,52 +1229,88 @@
 	}
 }
 
-func TestJWKSetFromPublicKeysetHandleECDSAWithTinkOutputPrefixHasKID(t *testing.T) {
-	key := `{
-      "primaryKeyId": 303799737,
-      "key": [
-          {
-              "keyId": 303799737,
-              "status": "ENABLED",
-              "outputPrefixType": "TINK",
-              "keyData": {
-                  "typeUrl": "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
-                  "keyMaterialType": "ASYMMETRIC_PUBLIC",
-                  "value": "IiDuhGJiGeaQ/qeqt1daC2xZRarm4VEsmSHJUWJY9EHbvxogwO6uIxh8SkKOO8VjZXNRTteRcwCPE4/4JElKyaa0fcQQAQ=="
-              }
-          }
-      ]
-  }`
-	jwkES256PublicKey := `{
-		"keys":[{
-		"kty":"EC",
-		"crv":"P-256",
-		"x":"wO6uIxh8SkKOO8VjZXNRTteRcwCPE4_4JElKyaa0fcQ",
-		"y":"7oRiYhnmkP6nqrdXWgtsWUWq5uFRLJkhyVFiWPRB278",
-		"use":"sig",
-		"alg":"ES256",
-		"key_ops":["verify"],
-		"kid":"EhuduQ"}]
-	}`
-	handle, err := createKeysetHandle(key)
-	if err != nil {
-		t.Fatalf("createKeysetHandle() err = %v, want nil", err)
-	}
-	jwkSet, err := jwt.JWKSetFromPublicKeysetHandle(handle)
-	if err != nil {
-		t.Fatalf("jwt.JWKSetFromPublicKeysetHandle() err = %v, want nil", err)
-	}
-
-	got := &spb.Struct{}
-	if err := got.UnmarshalJSON(jwkSet); err != nil {
-		t.Fatalf("got.UnmarshalJSON() err = %v, want nil", err)
-	}
-	want := &spb.Struct{}
-	if err := want.UnmarshalJSON([]byte(jwkES256PublicKey)); err != nil {
-		t.Fatalf("want.UnmarshalJSON() err = %v, want nil", err)
-	}
-	if !cmp.Equal(want, got, protocmp.Transform()) {
-		t.Errorf("mismatch in jwk sets: diff (-want,+got): %v", cmp.Diff(want, got, protocmp.Transform()))
+func TestJWKSetFromPublicKeysetHandleTinkOutputPrefixHasKID(t *testing.T) {
+	for _, tc := range []jwkSetTestCase{
+		{
+			tag: "JwtEcdsaPublicKey",
+			publicKeyset: `{
+					"primaryKeyId": 303799737,
+					"key": [
+							{
+									"keyId": 303799737,
+									"status": "ENABLED",
+									"outputPrefixType": "TINK",
+									"keyData": {
+											"typeUrl": "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+											"keyMaterialType": "ASYMMETRIC_PUBLIC",
+											"value": "IiDuhGJiGeaQ/qeqt1daC2xZRarm4VEsmSHJUWJY9EHbvxogwO6uIxh8SkKOO8VjZXNRTteRcwCPE4/4JElKyaa0fcQQAQ=="
+									}
+							}
+					]
+			}`,
+			jwkSet: `{
+				"keys":[{
+				"kty":"EC",
+				"crv":"P-256",
+				"x":"wO6uIxh8SkKOO8VjZXNRTteRcwCPE4_4JElKyaa0fcQ",
+				"y":"7oRiYhnmkP6nqrdXWgtsWUWq5uFRLJkhyVFiWPRB278",
+				"use":"sig",
+				"alg":"ES256",
+				"key_ops":["verify"],
+				"kid":"EhuduQ"}]
+			}`,
+		},
+		{
+			tag: "JwtRsaSsaPkcs1PublicKey",
+			publicKeyset: `{
+				"primaryKeyId": 1277272603,
+				"key": [
+					{
+						"keyData": {
+							"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey",
+							"value": "IgMBAAEagAK+ZQ5rrZNivGPs3ytlUDOgR1KeaxFBo1YEwB0Hxp0ZryfjJwaJhaga/S5lZzy8faOfqXc9r/vZtvYgd/f4oPZRpPAuTXHfJKFfJsShLlkX1t6bOufaiE2LEag3s5+PvA9vrVn4XU2/neerfTzP5EjVZ7Igf70eO4hy5TFpZjRV6+xfMJ6Ewk/mDuRXPKXnlthxGLbx2J2RVrOvNWA0bfnI00wQvfahbVV+++nuF9Ae3FLCQU4/MmDMg8dskVvEAsauuBceyirtS0NB1L2++gSnj8nNCEK2cIQpqGCRPA5bJP3o6VEZiI8lIUdZO6PLVCd3o4pzwsYSykBfigPpmX5hEAE=",
+							"keyMaterialType": "ASYMMETRIC_PUBLIC"
+						},
+						"status": "ENABLED",
+						"keyId": 1277272603,
+						"outputPrefixType": "TINK"
+					}
+				]
+			}`,
+			jwkSet: `{
+				"keys":[{
+					"kty":"RSA",
+					"n": "vmUOa62TYrxj7N8rZVAzoEdSnmsRQaNWBMAdB8adGa8n4ycGiYWoGv0uZWc8vH2jn6l3Pa_72bb2IHf3-KD2UaTwLk1x3yShXybEoS5ZF9bemzrn2ohNixGoN7Ofj7wPb61Z-F1Nv53nq308z-RI1WeyIH-9HjuIcuUxaWY0VevsXzCehMJP5g7kVzyl55bYcRi28didkVazrzVgNG35yNNMEL32oW1Vfvvp7hfQHtxSwkFOPzJgzIPHbJFbxALGrrgXHsoq7UtDQdS9vvoEp4_JzQhCtnCEKahgkTwOWyT96OlRGYiPJSFHWTujy1Qnd6OKc8LGEspAX4oD6Zl-YQ",
+					"e":"AQAB",
+					"use":"sig",
+					"alg":"RS256",
+					"key_ops":["verify"],
+					"kid":"TCGiGw"
+				}]
+			}`,
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			handle, err := createKeysetHandle(tc.publicKeyset)
+			if err != nil {
+				t.Fatalf("createKeysetHandle() err = %v, want nil", err)
+			}
+			js, err := jwt.JWKSetFromPublicKeysetHandle(handle)
+			if err != nil {
+				t.Fatalf("jwt.JWKSetFromPublicKeysetHandle() err = %v, want nil", err)
+			}
+			got := &spb.Struct{}
+			if err := got.UnmarshalJSON(js); err != nil {
+				t.Fatalf("got.UnmarshalJSON() err = %v, want nil", err)
+			}
+			want := &spb.Struct{}
+			if err := want.UnmarshalJSON([]byte(tc.jwkSet)); err != nil {
+				t.Fatalf("want.UnmarshalJSON() err = %v, want nil", err)
+			}
+			if !cmp.Equal(want, got, protocmp.Transform()) {
+				t.Errorf("mismatch in jwk sets: diff (-want,+got): %v", cmp.Diff(want, got, protocmp.Transform()))
+			}
+		})
 	}
 }
 
@@ -693,7 +1335,7 @@
   	}`,
 		},
 		{
-			tag: "unknown algorithm", // The algorithm is set in the base64 encoded value of the key data.
+			tag: "JwtEcdsaPublicKey unknown algorithm", // The algorithm is set in the base64 encoded value of the key data.
 			publicKeyset: `{
 			"primaryKeyId": 303799737,
 			"key": [
@@ -711,7 +1353,7 @@
 		}`,
 		},
 		{
-			tag: "private keyset",
+			tag: "private ecdsa keyset",
 			publicKeyset: `{
       "primaryKeyId": 303799737,
       "key": [
@@ -746,6 +1388,24 @@
       ]
   }`,
 		},
+		{
+			tag: "JwtRsaSsaPkcs1 unknown algorithm", // The algorithm is set in the base64 encoded value of the key data.
+			publicKeyset: `{
+				"primaryKeyId": 1277272603,
+				"key": [
+					{
+						"keyData": {
+							"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey",
+							"value": "IgMBAAEagAK+ZQ5rrZNivGPs3ytlUDOgR1KeaxFBo1YEwB0Hxp0ZryfjJwaJhaga/S5lZzy8faOfqXc9r/vZtvYgd/f4oPZRpPAuTXHfJKFfJsShLlkX1t6bOufaiE2LEag3s5+PvA9vrVn4XU2/neerfTzP5EjVZ7Igf70eO4hy5TFpZjRV6+xfMJ6Ewk/mDuRXPKXnlthxGLbx2J2RVrOvNWA0bfnI00wQvfahbVV+++nuF9Ae3FLCQU4/MmDMg8dskVvEAsauuBceyirtS0NB1L2++gSnj8nNCEK2cIQpqGCRPA5bJP3o6VEZiI8lIUdZO6PLVCd3o4pzwsYSykBfigPpmX5h",
+							"keyMaterialType": "ASYMMETRIC_PUBLIC"
+						},
+						"status": "ENABLED",
+						"keyId": 1277272603,
+						"outputPrefixType": "TINK"
+					}
+				]
+			}`,
+		},
 	} {
 		t.Run(tc.tag, func(t *testing.T) {
 			handle, err := createKeysetHandle(tc.publicKeyset)
diff --git a/go/jwt/jwt.go b/go/jwt/jwt.go
index 69f3325..d604f13 100644
--- a/go/jwt/jwt.go
+++ b/go/jwt/jwt.go
@@ -18,19 +18,46 @@
 package jwt
 
 import (
+	"errors"
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
 )
 
+// A generic error returned when something went wrong before validation
+var errJwtVerification = errors.New("verification failed")
+var errJwtExpired = errors.New("token has expired")
+
+// IsExpirationErr returns true if err was returned by a JWT verification for a token
+// with a valid signature that is expired.
+//
+// Note that if the corresponding verification key has been removed from the keyset,
+// verification will not return an expiration error even if the token is expired, because
+// the expiration is only verified if the signature is valid.
+func IsExpirationErr(err error) bool {
+	return err == errJwtExpired
+}
+
 func init() {
 	if err := registry.RegisterKeyManager(new(jwtHMACKeyManager)); err != nil {
-		panic(fmt.Sprintf("jwt.init() failed registering JWT HMAC key manger: %v", err))
+		panic(fmt.Sprintf("jwt.init() failed registering JWT HMAC key manager: %v", err))
 	}
 	if err := registry.RegisterKeyManager(new(jwtECDSAVerifierKeyManager)); err != nil {
-		panic(fmt.Sprintf("jwt.init() failed registering JWT ECDSA verifier key manger: %v", err))
+		panic(fmt.Sprintf("jwt.init() failed registering JWT ECDSA verifier key manager: %v", err))
 	}
 	if err := registry.RegisterKeyManager(new(jwtECDSASignerKeyManager)); err != nil {
-		panic(fmt.Sprintf("jwt.init() failed registering JWT ECDSA signer key manger: %v", err))
+		panic(fmt.Sprintf("jwt.init() failed registering JWT ECDSA signer key manager: %v", err))
+	}
+	if err := registry.RegisterKeyManager(new(jwtRSSignerKeyManager)); err != nil {
+		panic(fmt.Sprintf("jwt.init() failed registering JWT RSA SSA PKCS1 signer key manager: %v", err))
+	}
+	if err := registry.RegisterKeyManager(new(jwtRSVerifierKeyManager)); err != nil {
+		panic(fmt.Sprintf("jwt.init() failed registering JWT RSA SSA PKCS1 verifier key manager: %v", err))
+	}
+	if err := registry.RegisterKeyManager(new(jwtPSSignerKeyManager)); err != nil {
+		panic(fmt.Sprintf("jwt.init() failed registering JWT RSA SSA PSS signer key manager: %v", err))
+	}
+	if err := registry.RegisterKeyManager(new(jwtPSVerifierKeyManager)); err != nil {
+		panic(fmt.Sprintf("jwt.init() failed registering JWT RSA SSA PSS verifier key manager: %v", err))
 	}
 }
diff --git a/go/jwt/jwt_key_templates.go b/go/jwt/jwt_key_templates.go
index 16f8f70..825bb64 100644
--- a/go/jwt/jwt_key_templates.go
+++ b/go/jwt/jwt_key_templates.go
@@ -17,9 +17,14 @@
 package jwt
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	jepb "github.com/google/tink/go/proto/jwt_ecdsa_go_proto"
 	jwtmacpb "github.com/google/tink/go/proto/jwt_hmac_go_proto"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+	jrpsspb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
@@ -29,7 +34,10 @@
 		Version:   jwtHMACKeyVersion,
 		Algorithm: algorithm,
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          jwtHMACTypeURL,
 		Value:            serializedFormat,
@@ -42,7 +50,10 @@
 		Version:   jwtECDSASignerKeyVersion,
 		Algorithm: algorithm,
 	}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          jwtECDSASignerTypeURL,
 		Value:            serializedFormat,
@@ -50,6 +61,42 @@
 	}
 }
 
+func createJWTRSKeyTemplate(algorithm jrsppb.JwtRsaSsaPkcs1Algorithm, modulusSizeInBits uint32, outputPrefixType tinkpb.OutputPrefixType) *tinkpb.KeyTemplate {
+	format := &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+		Version:           jwtRSSignerKeyVersion,
+		Algorithm:         algorithm,
+		ModulusSizeInBits: modulusSizeInBits,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
+	return &tinkpb.KeyTemplate{
+		TypeUrl:          jwtRSSignerTypeURL,
+		Value:            serializedFormat,
+		OutputPrefixType: outputPrefixType,
+	}
+}
+
+func createJWTPSKeyTemplate(algorithm jrpsspb.JwtRsaSsaPssAlgorithm, modulusSizeInBits uint32, outputPrefixType tinkpb.OutputPrefixType) *tinkpb.KeyTemplate {
+	format := &jrpsspb.JwtRsaSsaPssKeyFormat{
+		Version:           jwtPSSignerKeyVersion,
+		Algorithm:         algorithm,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+		ModulusSizeInBits: modulusSizeInBits,
+	}
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
+	return &tinkpb.KeyTemplate{
+		TypeUrl:          jwtPSSignerTypeURL,
+		Value:            serializedFormat,
+		OutputPrefixType: outputPrefixType,
+	}
+}
+
 // HS256Template creates a JWT key template for JWA algorithm "HS256", which is a
 // HMAC-SHA256 with a 32 byte key. It will set a key ID header "kid" in the token.
 func HS256Template() *tinkpb.KeyTemplate {
@@ -121,3 +168,99 @@
 func RawES512Template() *tinkpb.KeyTemplate {
 	return createJWTECDSAKeyTemplate(jepb.JwtEcdsaAlgorithm_ES512, tinkpb.OutputPrefixType_RAW)
 }
+
+// RS256_2048_F4_Key_Template creates a JWT key template for JWA algorithm "RS256", which is digital
+// signature with RSA-SSA-PKCS1 and SHA256. It will set a key ID header "kid" in the token.
+func RS256_2048_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS256, 2048, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawRS256_2048_F4_Key_Template creates a JWT key template for JWA algorithm "RS256", which is digital
+// signature with RSA-SSA-PKCS1 and SHA256. It will not set a key ID header "kid" in the token.
+func RawRS256_2048_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS256, 2048, tinkpb.OutputPrefixType_RAW)
+}
+
+// RS256_3072_F4_Key_Template creates a JWT key template for JWA algorithm "RS256", which is digital
+// signature with RSA-SSA-PKCS1 and SHA256. It will set a key ID header "kid" in the token.
+func RS256_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS256, 3072, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawRS256_3072_F4_Key_Template creates a JWT key template for JWA algorithm "RS256", which is digital
+// signature with RSA-SSA-PKCS1 and SHA256. It will not set a key ID header "kid" in the token.
+func RawRS256_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS256, 3072, tinkpb.OutputPrefixType_RAW)
+}
+
+// RS384_3072_F4_Key_Template creates a JWT key template for JWA algorithm "RS384", which is digital
+// signature with RSA-SSA-PKCS1 and SHA384. It will set a key ID header "kid" in the token.
+func RS384_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS384, 3072, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawRS384_3072_F4_Key_Template creates a JWT key template for JWA algorithm "RS384", which is digital
+// signature with RSA-SSA-PKCS1 and SHA384. It will not set a key ID header "kid" in the token.
+func RawRS384_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS384, 3072, tinkpb.OutputPrefixType_RAW)
+}
+
+// RS512_4096_F4_Key_Template creates a JWT key template for JWA algorithm "RS512", which is digital
+// signature with RSA-SSA-PKCS1 and SHA512. It will set a key ID header "kid" in the token.
+func RS512_4096_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS512, 4096, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawRS512_4096_F4_Key_Template creates a JWT key template for JWA algorithm "RS512", which is digital
+// signature with RSA-SSA-PKCS1 and SHA512. It will not set a key ID header "kid" in the token.
+func RawRS512_4096_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTRSKeyTemplate(jrsppb.JwtRsaSsaPkcs1Algorithm_RS512, 4096, tinkpb.OutputPrefixType_RAW)
+}
+
+// PS256_2048_F4_Key_Template creates a JWT key template for JWA algorithm "PS256", which is digital
+// signature with RSA-SSA-PSS, a 2048 bit modulus, and SHA256. It will set a key ID header "kid" in the token.
+func PS256_2048_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS256, 2048, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawPS256_2048_F4_Key_Template creates a JWT key template for JWA algorithm "PS256", which is digital
+// signature with RSA-SSA-PSS, a 2048 bit modulus, and SHA256. It will not set a key ID header "kid" in the token.
+func RawPS256_2048_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS256, 2048, tinkpb.OutputPrefixType_RAW)
+}
+
+// PS256_3072_F4_Key_Template creates a JWT key template for JWA algorithm "PS256", which is digital
+// signature with RSA-SSA-PSS, a 3072 bit modulus, and SHA256. It will set a key ID header "kid" in the token.
+func PS256_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS256, 3072, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawPS256_3072_F4_Key_Template creates a JWT key template for JWA algorithm "PS256", which is digital
+// signature with RSA-SSA-PSS, a 3072 bit modulus, and SHA256. It will not set a key ID header "kid" in the token.
+func RawPS256_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS256, 3072, tinkpb.OutputPrefixType_RAW)
+}
+
+// PS384_3072_F4_Key_Template creates a JWT key template for JWA algorithm "PS384", which is digital
+// signature with RSA-SSA-PSS, a 3072 bit modulus, and SHA384. It will set a key ID header "kid" in the token.
+func PS384_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS384, 3072, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawPS384_3072_F4_Key_Template creates a JWT key template for JWA algorithm "PS384", which is digital
+// signature with RSA-SSA-PSS, a 3072 bit modulus, and SHA384. It will not set a key ID header "kid" in the token.
+func RawPS384_3072_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS384, 3072, tinkpb.OutputPrefixType_RAW)
+}
+
+// PS512_4096_F4_Key_Template creates a JWT key template for JWA algorithm "PS512", which is digital
+// signature with RSA-SSA-PSS, a 4096 bit modulus, and SHA512. It will set a key ID header "kid" in the token.
+func PS512_4096_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS512, 4096, tinkpb.OutputPrefixType_TINK)
+}
+
+// RawPS512_4096_F4_Key_Template creates a JWT key template for JWA algorithm "PS512", which is digital
+// signature with RSA-SSA-PSS, a 4096 bit modulus, and SHA512. It will not set a key ID header "kid" in the token.
+func RawPS512_4096_F4_Key_Template() *tinkpb.KeyTemplate {
+	return createJWTPSKeyTemplate(jrpsspb.JwtRsaSsaPssAlgorithm_PS512, 4096, tinkpb.OutputPrefixType_RAW)
+}
diff --git a/go/jwt/jwt_key_templates_test.go b/go/jwt/jwt_key_templates_test.go
index 39f72fd..97bc1ac 100644
--- a/go/jwt/jwt_key_templates_test.go
+++ b/go/jwt/jwt_key_templates_test.go
@@ -18,6 +18,7 @@
 
 import (
 	"testing"
+	"time"
 
 	"github.com/google/tink/go/jwt"
 	"github.com/google/tink/go/keyset"
@@ -31,7 +32,10 @@
 }
 
 func TestJWTComputeVerifyMAC(t *testing.T) {
-	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{WithoutExpiration: true})
+	expiresAt := time.Now().Add(time.Hour)
+	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{
+		ExpiresAt: &expiresAt,
+	})
 	if err != nil {
 		t.Errorf("NewRawJWT() err = %v, want nil", err)
 	}
@@ -56,19 +60,36 @@
 			if err != nil {
 				t.Errorf("m.ComputeMACAndEncode() err = %v, want nil", err)
 			}
-			verifier, err := jwt.NewValidator(&jwt.ValidatorOpts{AllowMissingExpiration: true})
+			validator, err := jwt.NewValidator(&jwt.ValidatorOpts{})
 			if err != nil {
 				t.Errorf("jwt.NewValidator() err = %v, want nil", err)
 			}
-			if _, err := m.VerifyMACAndDecode(compact, verifier); err != nil {
+			if _, err := m.VerifyMACAndDecode(compact, validator); err != nil {
 				t.Errorf("m.VerifyMACAndDecode() err = %v, want nil", err)
 			}
+
+			// In two hours, VerifyMACAndDecode should fail with an expiration error.
+			inTwoHours := time.Now().Add(time.Hour * 2)
+			futureValidator, err := jwt.NewValidator(&jwt.ValidatorOpts{FixedNow: inTwoHours})
+			if err != nil {
+				t.Errorf("jwt.NewValidator() err = %v, want nil", err)
+			}
+			_, futureError := m.VerifyMACAndDecode(compact, futureValidator)
+			if futureError == nil {
+				t.Errorf("m.VerifyMACAndDecode(compact, futureValidator) err = nil, want error")
+			}
+			if !jwt.IsExpirationErr(futureError) {
+				t.Errorf("jwt.IsExpirationErr(futureError) = false, want true")
+			}
 		})
 	}
 }
 
-func TestJWTSignVerifyECDSA(t *testing.T) {
-	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{WithoutExpiration: true})
+func TestJWTSignVerify(t *testing.T) {
+	expiresAt := time.Now().Add(time.Hour)
+	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{
+		ExpiresAt: &expiresAt,
+	})
 	if err != nil {
 		t.Errorf("jwt.NewRawJWT() err = %v, want nil", err)
 	}
@@ -79,6 +100,22 @@
 		{tag: "JWT_ES256_RAW", template: jwt.RawES256Template()},
 		{tag: "JWT_ES384_RAW", template: jwt.RawES384Template()},
 		{tag: "JWT_ES512_RAW", template: jwt.RawES512Template()},
+		{tag: "JWT_RS256_2048_R4", template: jwt.RS256_2048_F4_Key_Template()},
+		{tag: "JWT_RS256_2048_R4_RAW", template: jwt.RawRS256_2048_F4_Key_Template()},
+		{tag: "JWT_RS256_3072_R4", template: jwt.RS256_3072_F4_Key_Template()},
+		{tag: "JWT_RS256_3072_R4_RAW", template: jwt.RawRS256_3072_F4_Key_Template()},
+		{tag: "JWT_RS384_3072_R4", template: jwt.RS384_3072_F4_Key_Template()},
+		{tag: "JWT_RS384_3072_R4_RAW", template: jwt.RawRS384_3072_F4_Key_Template()},
+		{tag: "JWT_RS512_4096_R4", template: jwt.RS512_4096_F4_Key_Template()},
+		{tag: "JWT_RS512_4096_R4_RAW", template: jwt.RawRS384_3072_F4_Key_Template()},
+		{tag: "JWT_PS256_2048_R4", template: jwt.PS256_2048_F4_Key_Template()},
+		{tag: "JWT_PS256_2048_R4_RAW", template: jwt.RawPS256_2048_F4_Key_Template()},
+		{tag: "JWT_PS256_3072_R4", template: jwt.PS256_3072_F4_Key_Template()},
+		{tag: "JWT_PS256_3072_R4_RAW", template: jwt.RawPS256_3072_F4_Key_Template()},
+		{tag: "JWT_PS384_3072_R4", template: jwt.PS384_3072_F4_Key_Template()},
+		{tag: "JWT_PS384_3072_R4_RAW", template: jwt.RawPS384_3072_F4_Key_Template()},
+		{tag: "JWT_PS512_4096_R4", template: jwt.PS512_4096_F4_Key_Template()},
+		{tag: "JWT_PS512_4096_R4_RAW", template: jwt.RawPS384_3072_F4_Key_Template()},
 	} {
 		t.Run(tc.tag, func(t *testing.T) {
 			kh, err := keyset.NewHandle(tc.template)
@@ -101,13 +138,27 @@
 			if err != nil {
 				t.Fatalf("jwt.NewVerifier() err = %v, want nil", err)
 			}
-			validator, err := jwt.NewValidator(&jwt.ValidatorOpts{AllowMissingExpiration: true})
+			validator, err := jwt.NewValidator(&jwt.ValidatorOpts{})
 			if err != nil {
 				t.Fatalf("jwt.NewValidator() err = %v, want nil", err)
 			}
 			if _, err := verifier.VerifyAndDecode(compact, validator); err != nil {
 				t.Errorf("verifier.VerifyAndDecode() err = %v, want nil", err)
 			}
+
+			// In two hours, VerifyAndDecode should fail with an expiration error.
+			inTwoHours := time.Now().Add(time.Hour * 2)
+			futureValidator, err := jwt.NewValidator(&jwt.ValidatorOpts{FixedNow: inTwoHours})
+			if err != nil {
+				t.Errorf("jwt.NewValidator() err = %v, want nil", err)
+			}
+			_, futureError := verifier.VerifyAndDecode(compact, futureValidator)
+			if futureError == nil {
+				t.Errorf("verifier.VerifyAndDecode(compact, futureValidator) err = nil, want error")
+			}
+			if !jwt.IsExpirationErr(futureError) {
+				t.Errorf("jwt.IsExpirationErr(expirationError) = false, want true")
+			}
 		})
 	}
 }
diff --git a/go/jwt/jwt_mac_factory.go b/go/jwt/jwt_mac_factory.go
index 732bd3b..ba28d0c 100644
--- a/go/jwt/jwt_mac_factory.go
+++ b/go/jwt/jwt_mac_factory.go
@@ -24,11 +24,11 @@
 )
 
 // NewMAC generates a new instance of the JWT MAC primitive.
-func NewMAC(h *keyset.Handle) (MAC, error) {
-	if h == nil {
+func NewMAC(handle *keyset.Handle) (MAC, error) {
+	if handle == nil {
 		return nil, fmt.Errorf("keyset handle can't be nil")
 	}
-	ps, err := h.PrimitivesWithKeyManager(nil)
+	ps, err := handle.PrimitivesWithKeyManager(nil)
 	if err != nil {
 		return nil, fmt.Errorf("jwt_mac_factory: cannot obtain primitive set: %v", err)
 	}
@@ -66,16 +66,25 @@
 }
 
 func (w *wrappedJWTMAC) VerifyMACAndDecode(compact string, validator *Validator) (*VerifiedJWT, error) {
+	var interestingErr error
 	for _, s := range w.ps.Entries {
 		for _, e := range s {
 			p, ok := e.Primitive.(*macWithKID)
 			if !ok {
 				return nil, fmt.Errorf("jwt_mac_factory: not a JWT MAC primitive")
 			}
-			if verifiedJWT, err := p.VerifyMACAndDecodeWithKID(compact, validator, keyID(e.KeyID, e.PrefixType)); err == nil {
+			verifiedJWT, err := p.VerifyMACAndDecodeWithKID(compact, validator, keyID(e.KeyID, e.PrefixType))
+			if err == nil {
 				return verifiedJWT, nil
 			}
+			if err != errJwtVerification {
+				// any error that is not the generic errJwtVerification is considered interesting
+				interestingErr = err
+			}
 		}
 	}
-	return nil, fmt.Errorf("verification failed")
+	if interestingErr != nil {
+		return nil, interestingErr
+	}
+	return nil, errJwtVerification
 }
diff --git a/go/jwt/jwt_mac_factory_test.go b/go/jwt/jwt_mac_factory_test.go
index 44933e0..4064845 100644
--- a/go/jwt/jwt_mac_factory_test.go
+++ b/go/jwt/jwt_mac_factory_test.go
@@ -239,3 +239,40 @@
 		t.Fatal("calling NewMAC() err = nil, want error")
 	}
 }
+
+func TestVerifyMACAndDecodeReturnsValidationError(t *testing.T) {
+	keyData, err := newKeyData(newJWTHMACKey(jwtmacpb.JwtHmacAlgorithm_HS256, nil))
+	if err != nil {
+		t.Fatalf("creating NewKeyData: %v", err)
+	}
+	p, err := createJWTMAC(keyData, tinkpb.OutputPrefixType_TINK)
+	if err != nil {
+		t.Fatalf("creating New JWT MAC: %v", err)
+	}
+
+	audience := "audience"
+	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{Audience: &audience, WithoutExpiration: true})
+	if err != nil {
+		t.Fatalf("jwt.NewRawJWT() err = %v, want nil", err)
+	}
+	token, err := p.ComputeMACAndEncode(rawJWT)
+	if err != nil {
+		t.Errorf("p.ComputeMACAndEncode() err = %v, want nil", err)
+	}
+
+	otherAudience := "otherAudience"
+	validator, err := jwt.NewValidator(
+		&jwt.ValidatorOpts{ExpectedAudience: &otherAudience, AllowMissingExpiration: true})
+	if err != nil {
+		t.Fatalf("jwt.NewValidator() err = %v, want nil", err)
+	}
+
+	_, err = p.VerifyMACAndDecode(token, validator)
+	wantErr := "validating audience claim: otherAudience not found"
+	if err == nil {
+		t.Errorf("p.VerifyMACAndDecode() err = nil, want %q", wantErr)
+	}
+	if err.Error() != wantErr {
+		t.Errorf("p.VerifyMACAndDecode() err = %q, want %q", err.Error(), wantErr)
+	}
+}
diff --git a/go/jwt/jwt_mac_kid.go b/go/jwt/jwt_mac_kid.go
index 96b941a..874cfb0 100644
--- a/go/jwt/jwt_mac_kid.go
+++ b/go/jwt/jwt_mac_kid.go
@@ -58,14 +58,14 @@
 func (jm *macWithKID) VerifyMACAndDecodeWithKID(compact string, verifier *Validator, kid *string) (*VerifiedJWT, error) {
 	tag, content, err := splitSignedCompact(compact)
 	if err != nil {
-		return nil, err
+		return nil, errJwtVerification
 	}
 	if err := jm.tm.VerifyMAC(tag, []byte(content)); err != nil {
-		return nil, err
+		return nil, errJwtVerification
 	}
 	rawJWT, err := decodeUnsignedTokenAndValidateHeader(content, jm.algorithm, kid, jm.customKID)
 	if err != nil {
-		return nil, err
+		return nil, errJwtVerification
 	}
 	if err := verifier.Validate(rawJWT); err != nil {
 		return nil, err
diff --git a/go/jwt/jwt_mac_kid_test.go b/go/jwt/jwt_mac_kid_test.go
index 901e096..c6a0a11 100644
--- a/go/jwt/jwt_mac_kid_test.go
+++ b/go/jwt/jwt_mac_kid_test.go
@@ -205,9 +205,13 @@
 	if err != nil {
 		t.Fatalf("creating JWTValidator: %v", err)
 	}
-	if _, err := m.VerifyMACAndDecodeWithKID(compact, presentValidator, nil); err == nil {
+	_, err = m.VerifyMACAndDecodeWithKID(compact, presentValidator, nil)
+	if err == nil {
 		t.Fatalf("m.VerifyMACAndDecodeWithKID() with expired token err = nil, want error")
 	}
+	if !IsExpirationErr(err) {
+		t.Fatalf("IsExpirationErr(err) for err = %q is false, want true", err)
+	}
 
 	// tampered token verification fails
 	tamperedCompact := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXx"
diff --git a/go/jwt/jwt_rsa_ssa_pkcs1_signer_key_manager.go b/go/jwt/jwt_rsa_ssa_pkcs1_signer_key_manager.go
new file mode 100644
index 0000000..17baf1d
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pkcs1_signer_key_manager.go
@@ -0,0 +1,201 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"errors"
+	"fmt"
+	"math/big"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	internal "github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/keyset"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	jwtRSSignerKeyVersion = 0
+	jwtRSSignerTypeURL    = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+)
+
+var (
+	errRSInvalidPrivateKey = errors.New("invalid JwtRsaSsaPkcs1PrivateKey")
+	errRSInvalidKeyFormat  = errors.New("invalid RSA SSA PKCS1 key format")
+)
+
+// jwtRSSignerKeyManager implements the KeyManager interface
+// for JWT Signing using the 'RS256', 'RS384', and 'RS512' JWA algorithm.
+type jwtRSSignerKeyManager struct{}
+
+var _ registry.PrivateKeyManager = (*jwtRSSignerKeyManager)(nil)
+
+func bytesToBigInt(v []byte) *big.Int {
+	return new(big.Int).SetBytes(v)
+}
+
+func (km *jwtRSSignerKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if serializedKey == nil {
+		return nil, fmt.Errorf("invalid JwtRsaSsaPkcs1PrivateKey")
+	}
+	privKey := &jrsppb.JwtRsaSsaPkcs1PrivateKey{}
+	if err := proto.Unmarshal(serializedKey, privKey); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal RsaSsaPkcs1PrivateKey: %v", err)
+	}
+	if err := validateRSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	rsaPrivKey := &rsa.PrivateKey{
+		PublicKey: rsa.PublicKey{
+			N: bytesToBigInt(privKey.GetPublicKey().GetN()),
+			E: int(bytesToBigInt(privKey.GetPublicKey().GetE()).Int64()),
+		},
+		D: bytesToBigInt(privKey.GetD()),
+		Primes: []*big.Int{
+			bytesToBigInt(privKey.GetP()),
+			bytesToBigInt(privKey.GetQ()),
+		},
+		Precomputed: rsa.PrecomputedValues{
+			Dp: bytesToBigInt(privKey.GetDp()),
+			Dq: bytesToBigInt(privKey.GetDq()),
+			// in crypto/rsa `GetCrt()` returns the "Chinese Remainder Theorem
+			// coefficient q^(-1) mod p. Which is `Qinv` in the tink proto and not
+			// the `CRTValues`.
+			Qinv: bytesToBigInt(privKey.GetCrt()),
+		},
+	}
+	alg := privKey.GetPublicKey().GetAlgorithm()
+	if err := internal.Validate_RSA_SSA_PKCS1(validRSAlgToHash[alg], rsaPrivKey); err != nil {
+		return nil, err
+	}
+	signer, err := internal.New_RSA_SSA_PKCS1_Signer(validRSAlgToHash[alg], rsaPrivKey)
+	if err != nil {
+		return nil, err
+	}
+	return newSignerWithKID(signer, alg.String(), rsCustomKID(privKey.GetPublicKey()))
+}
+
+func validateRSPrivateKey(privKey *jrsppb.JwtRsaSsaPkcs1PrivateKey) error {
+	if err := keyset.ValidateKeyVersion(privKey.Version, jwtRSSignerKeyVersion); err != nil {
+		return err
+	}
+	if privKey.GetD() == nil ||
+		len(privKey.GetPublicKey().GetN()) == 0 ||
+		len(privKey.GetPublicKey().GetE()) == 0 ||
+		privKey.GetP() == nil ||
+		privKey.GetQ() == nil ||
+		privKey.GetDp() == nil ||
+		privKey.GetDq() == nil ||
+		privKey.GetCrt() == nil {
+		return fmt.Errorf("invalid private key")
+	}
+	if err := validateRSPublicKey(privKey.GetPublicKey()); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (km *jwtRSSignerKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errRSInvalidKeyFormat
+	}
+	keyFormat := &jrsppb.JwtRsaSsaPkcs1KeyFormat{}
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal JwtRsaSsaPkcs1KeyFormat: %v", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), jwtRSSignerKeyVersion); err != nil {
+		return nil, err
+	}
+	if keyFormat.GetVersion() != jwtRSSignerKeyVersion {
+		return nil, fmt.Errorf("invalid key format version: %d", keyFormat.GetVersion())
+	}
+	rsaKey, err := rsa.GenerateKey(rand.Reader, int(keyFormat.GetModulusSizeInBits()))
+	if err != nil {
+		return nil, err
+	}
+	privKey := &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+		Version: jwtRSSignerKeyVersion,
+		PublicKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+			Version:   jwtRSSignerKeyVersion,
+			Algorithm: keyFormat.GetAlgorithm(),
+			N:         rsaKey.PublicKey.N.Bytes(),
+			E:         keyFormat.GetPublicExponent(),
+		},
+		D:  rsaKey.D.Bytes(),
+		P:  rsaKey.Primes[0].Bytes(),
+		Q:  rsaKey.Primes[1].Bytes(),
+		Dp: rsaKey.Precomputed.Dp.Bytes(),
+		Dq: rsaKey.Precomputed.Dq.Bytes(),
+		// in crypto/rsa `GetCrt()` returns the "Chinese Remainder Theorem
+		// coefficient q^(-1) mod p. Which is `Qinv` in the tink proto and not
+		// the `CRTValues`.
+		Crt: rsaKey.Precomputed.Qinv.Bytes(),
+	}
+	if err := validateRSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	return privKey, nil
+}
+
+func (km *jwtRSSignerKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	key, err := km.NewKey(serializedKeyFormat)
+	if err != nil {
+		return nil, err
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         jwtRSSignerTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE,
+	}, nil
+}
+
+func (km *jwtRSSignerKeyManager) PublicKeyData(serializedPrivKey []byte) (*tinkpb.KeyData, error) {
+	if serializedPrivKey == nil {
+		return nil, errRSInvalidKeyFormat
+	}
+	privKey := &jrsppb.JwtRsaSsaPkcs1PrivateKey{}
+	if err := proto.Unmarshal(serializedPrivKey, privKey); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal JwtRsaSsaPkcs1PrivateKey: %v", err)
+	}
+	if err := validateRSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	serializedPubKey, err := proto.Marshal(privKey.GetPublicKey())
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         jwtRSVerifierTypeURL,
+		Value:           serializedPubKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
+	}, nil
+}
+
+func (km *jwtRSSignerKeyManager) DoesSupport(typeURL string) bool {
+	return jwtRSSignerTypeURL == typeURL
+}
+
+func (km *jwtRSSignerKeyManager) TypeURL() string {
+	return jwtRSSignerTypeURL
+}
diff --git a/go/jwt/jwt_rsa_ssa_pkcs1_signer_key_manager_test.go b/go/jwt/jwt_rsa_ssa_pkcs1_signer_key_manager_test.go
new file mode 100644
index 0000000..30c3803
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pkcs1_signer_key_manager_test.go
@@ -0,0 +1,785 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"math/big"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+	"github.com/google/tink/go/core/registry"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const testJWTRSSignerKeyType = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+
+func makeValidJWTRSPrivateKey() (*jrsppb.JwtRsaSsaPkcs1PrivateKey, error) {
+	// key taken from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
+	pubKey, err := makeValidRSPublicKey()
+	if err != nil {
+		return nil, err
+	}
+	d, err := base64Decode(
+		"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I" +
+			"jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0" +
+			"BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn" +
+			"439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT" +
+			"CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh" +
+			"BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ")
+	if err != nil {
+		return nil, err
+	}
+	p, err := base64Decode(
+		"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi" +
+			"YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG" +
+			"BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc")
+	if err != nil {
+		return nil, err
+	}
+	q, err := base64Decode(
+		"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa" +
+			"ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA" +
+			"-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc")
+	if err != nil {
+		return nil, err
+	}
+	dp, err := base64Decode(
+		"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q" +
+			"CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb" +
+			"34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0")
+	if err != nil {
+		return nil, err
+	}
+	dq, err := base64Decode(
+		"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa" +
+			"7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky" +
+			"NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU")
+	if err != nil {
+		return nil, err
+	}
+	qi, err := base64Decode(
+		"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o" +
+			"y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU" +
+			"W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U")
+	if err != nil {
+		return nil, err
+	}
+	return &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+		PublicKey: pubKey,
+		Version:   0,
+		D:         d,
+		P:         p,
+		Q:         q,
+		Dp:        dp,
+		Dq:        dq,
+		Crt:       qi,
+	}, nil
+}
+
+func TestJWTRSSignerKeyManagerDoesSupport(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	if !km.DoesSupport(testJWTRSSignerKeyType) {
+		t.Errorf("DoesSupport(%q) = false, want true", testJWTRSSignerKeyType)
+	}
+	if km.DoesSupport("invalid.key.type") {
+		t.Errorf("DoesSupport(%q) = true, want false", "invalid.key.type")
+	}
+}
+
+func TestJWTRSSignerKeyManagerTypeURL(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	if km.TypeURL() != testJWTRSSignerKeyType {
+		t.Errorf("TypeURL() = %v, want = %v", km.TypeURL(), testJWTRSSignerKeyType)
+	}
+}
+
+func TestJWTRSSignerKeyManagerPrimitiveSignAndVerify(t *testing.T) {
+	skm, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	privKey, err := makeValidJWTRSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTRSPrivateKey() err = %v, want nil", err)
+	}
+	privKey.PublicKey.CustomKid = nil
+	serializedPrivKey, err := proto.Marshal(privKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	sp, err := skm.Primitive(serializedPrivKey)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	signer, ok := sp.(*signerWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT signer")
+	}
+	unsigned, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true})
+	if err != nil {
+		t.Fatalf("NewRawJWT() err = %v, want nil", err)
+	}
+	signed, err := signer.SignAndEncodeWithKID(unsigned, nil)
+	if err != nil {
+		t.Fatalf("SignAndEncodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	vkm, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	serializedPubKey, err := proto.Marshal(privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	vp, err := vkm.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := vp.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	validator, err := NewValidator(&ValidatorOpts{AllowMissingExpiration: true})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, nil); err != nil {
+		t.Errorf("VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	// Shouldn't contain KID header at all.
+	if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, refString("")); err == nil {
+		t.Errorf("VerifyAndDecodeWithKID(kid = '123') err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerPrimitiveWithInvalidSerializedKeyFails(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	if _, err := km.Primitive([]byte("invalid_serialization")); err == nil {
+		t.Fatalf("Primitive() err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerPrimitiveSignAndVerifyWithTinkKID(t *testing.T) {
+	skm, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	privKey, err := makeValidJWTRSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTRSPrivateKey() err = %v, want nil", err)
+	}
+	privKey.PublicKey.CustomKid = nil
+	serializedPrivKey, err := proto.Marshal(privKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	sp, err := skm.Primitive(serializedPrivKey)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	signer, ok := sp.(*signerWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT signer")
+	}
+	unsigned, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true})
+	if err != nil {
+		t.Fatalf("NewRawJWT() err = %v, want nil", err)
+	}
+	signedWithKID, err := signer.SignAndEncodeWithKID(unsigned, refString("555"))
+	if err != nil {
+		t.Fatalf("SignAndEncodeWithKID(kid = '555') err = %v, want nil", err)
+	}
+	vkm, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	serializedPubKey, err := proto.Marshal(privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	vp, err := vkm.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := vp.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	validator, err := NewValidator(&ValidatorOpts{AllowMissingExpiration: true})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	if _, err := verifier.VerifyAndDecodeWithKID(signedWithKID, validator, refString("555")); err != nil {
+		t.Fatalf("VerifyAndDecodeWithKID(kid = '555') err = %v, want nil", err)
+	}
+	if _, err := verifier.VerifyAndDecodeWithKID(signedWithKID, validator, refString("0")); err == nil {
+		t.Fatalf("VerifyAndDecodeWithKID(kid = '0') err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerPrimitiveSignAndVerifyWithCustomKID(t *testing.T) {
+	skm, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	privKey, err := makeValidJWTRSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTRSPrivateKey() err = %v, want nil", err)
+	}
+	privKey.PublicKey.CustomKid = &jrsppb.JwtRsaSsaPkcs1PublicKey_CustomKid{
+		Value: "7843",
+	}
+	serializedPrivKey, err := proto.Marshal(privKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	sp, err := skm.Primitive(serializedPrivKey)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	signer, ok := sp.(*signerWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT signer")
+	}
+	unsigned, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true})
+	if err != nil {
+		t.Fatalf("NewRawJWT() err = %v, want nil", err)
+	}
+	signed, err := signer.SignAndEncodeWithKID(unsigned, nil)
+	if err != nil {
+		t.Fatalf("SignAndEncodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	if _, err := signer.SignAndEncodeWithKID(unsigned, refString("555")); err == nil {
+		t.Fatalf("SignAndEncodeWithKID(kid = '555') err = nil, want error")
+	}
+	vkm, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	serializedPubKey, err := proto.Marshal(privKey.PublicKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	vp, err := vkm.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := vp.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	validator, err := NewValidator(&ValidatorOpts{AllowMissingExpiration: true})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, nil); err != nil {
+		t.Fatalf("VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, refString("7843")); err == nil {
+		t.Fatalf("VerifyAndDecodeWithKID(kid = '7843') err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerPrimitiveFailsWithInvalidKey(t *testing.T) {
+	type testCase struct {
+		name    string
+		privKey *jrsppb.JwtRsaSsaPkcs1PrivateKey
+	}
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	validPrivKey, err := makeValidJWTRSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTRSPrivateKey() err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:    "nil private key",
+			privKey: nil,
+		},
+		{
+			name:    "empty private key",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{},
+		},
+		{
+			name: "invalid private key version",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion() + 1,
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid D private key value",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         nil,
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid P private key value",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         nil,
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid Q private key value",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         nil,
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid pre computed Dp key value",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        nil,
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid pre computed Dq key value",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        nil,
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid pre computed Dq key value",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       nil,
+			},
+		},
+		{
+			name: "nil public key",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: nil,
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "empty public key",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{},
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid private key version",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.Version + 1,
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid public key version",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion() + 1,
+					Algorithm: validPrivKey.GetPublicKey().GetAlgorithm(),
+					N:         validPrivKey.GetPublicKey().GetN(),
+					E:         validPrivKey.GetPublicKey().GetE(),
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid algorithm",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion(),
+					Algorithm: jrsppb.JwtRsaSsaPkcs1Algorithm_RS_UNKNOWN,
+					N:         validPrivKey.GetPublicKey().GetN(),
+					E:         validPrivKey.GetPublicKey().GetE(),
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid modulus",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion(),
+					Algorithm: validPrivKey.GetPublicKey().GetAlgorithm(),
+					N:         []byte{0x00, 0x01},
+					E:         validPrivKey.GetPublicKey().GetE(),
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid exponent",
+			privKey: &jrsppb.JwtRsaSsaPkcs1PrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion(),
+					Algorithm: validPrivKey.GetPublicKey().GetAlgorithm(),
+					N:         validPrivKey.GetPublicKey().GetN(),
+					E:         []byte{0x07},
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedPrivKey, err := proto.Marshal(tc.privKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := km.Primitive(serializedPrivKey); err == nil {
+				t.Fatalf("Primitive() err = nil, want error")
+			}
+			if _, err := km.(registry.PrivateKeyManager).PublicKeyData(serializedPrivKey); err == nil {
+				t.Fatalf("PublicKeyData() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWTRSSignerKeyManagerPrimitiveFailsWithCorruptedKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	validPrivKey, err := makeValidJWTRSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTRSPrivateKey() err = %v, want nil", err)
+	}
+	corruptedPrivKey := validPrivKey
+	corruptedPrivKey.P[100] = byte(uint8(corruptedPrivKey.P[100] + 1))
+	corruptedPrivKey.P[5] = byte(uint8(corruptedPrivKey.P[5] + 1))
+	serializedPrivKey, err := proto.Marshal(corruptedPrivKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := km.Primitive(serializedPrivKey); err == nil {
+		t.Fatalf("Primitive() err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerPublicKeyData(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	privKey, err := makeValidJWTRSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTRSPrivateKey() err = %v, want nil", err)
+	}
+	serializedPrivKey, err := proto.Marshal(privKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	pubKeyData, err := km.(registry.PrivateKeyManager).PublicKeyData(serializedPrivKey)
+	if err != nil {
+		t.Fatalf("PublicKeyData() err = %v, want nil", err)
+	}
+	if pubKeyData.GetKeyMaterialType() != tpb.KeyData_ASYMMETRIC_PUBLIC {
+		t.Errorf("GetKeyMaterialType() = %v, want %v", pubKeyData.GetKeyMaterialType(), tpb.KeyData_ASYMMETRIC_PUBLIC)
+	}
+	if pubKeyData.GetTypeUrl() != testJWTRSVerifierKeyType {
+		t.Errorf("TypeURL() = %v, want %v", pubKeyData.GetTypeUrl(), testJWTRSVerifierKeyType)
+	}
+	gotPubKey := &jrsppb.JwtRsaSsaPkcs1PublicKey{}
+	if err := proto.Unmarshal(pubKeyData.GetValue(), gotPubKey); err != nil {
+		t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+	}
+	if !cmp.Equal(gotPubKey, privKey.GetPublicKey(), protocmp.Transform()) {
+		t.Errorf("got = %v, want = %v", gotPubKey, privKey.GetPublicKey())
+	}
+}
+
+func TestJWTRSSignerKeyManagerPublicKeyDataWithNilKeyFails(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	if _, err := km.(registry.PrivateKeyManager).PublicKeyData(nil); err == nil {
+		t.Fatalf("PublicKeyData(nil) err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerNewKeyData(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	type testCase struct {
+		name      string
+		keyFormat *jrsppb.JwtRsaSsaPkcs1KeyFormat
+	}
+	for _, tc := range []testCase{
+		{
+			name: "RS256 with 3072 modulus",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Version:           0,
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+				ModulusSizeInBits: 3072,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+			},
+		},
+		{
+			name: "RS384 with 3072 modulus",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Version:           0,
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS384,
+				ModulusSizeInBits: 3072,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+			},
+		},
+		{
+			name: "RS512 with 4096 modulus",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Version:           0,
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS512,
+				ModulusSizeInBits: 4096,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedKeyFormat, err := proto.Marshal(tc.keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			keyData, err := km.NewKeyData(serializedKeyFormat)
+			if err != nil {
+				t.Fatalf("NewKeyData() err = %v, want nil", err)
+			}
+			if keyData.GetTypeUrl() != testJWTRSSignerKeyType {
+				t.Errorf("GetTypeURL() = %v, want %v", keyData.GetTypeUrl(), testJWTRSSignerKeyType)
+			}
+			if keyData.GetKeyMaterialType() != tpb.KeyData_ASYMMETRIC_PRIVATE {
+				t.Errorf("GetKeyMaterialType() = %v, want %v", keyData.GetKeyMaterialType(), tpb.KeyData_ASYMMETRIC_PRIVATE)
+			}
+			key := &jrsppb.JwtRsaSsaPkcs1PrivateKey{}
+			if err := proto.Unmarshal(keyData.GetValue(), key); err != nil {
+				t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+			}
+			pubKey := key.GetPublicKey()
+			got, want := pubKey.GetAlgorithm(), tc.keyFormat.GetAlgorithm()
+			if got != want {
+				t.Errorf("GetAlgorithm() = %v, want %v", got, want)
+			}
+			gotE, wantE := pubKey.GetE(), tc.keyFormat.GetPublicExponent()
+			if !cmp.Equal(gotE, wantE) {
+				t.Errorf("Exponent = %v, want %v", gotE, wantE)
+			}
+			gotMod := new(big.Int).SetBytes(pubKey.GetN()).BitLen()
+			wantMod := int(tc.keyFormat.GetModulusSizeInBits())
+			if gotMod != wantMod {
+				t.Errorf("Modulus Size in Bits = %d, want %d", gotMod, wantMod)
+			}
+		})
+	}
+}
+
+func TestJWTRSSignerKeyManagerNewKeyDataFailsWithInvalidFormat(t *testing.T) {
+	type testCase struct {
+		name      string
+		keyFormat *jrsppb.JwtRsaSsaPkcs1KeyFormat
+	}
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:      "nil key format",
+			keyFormat: nil,
+		},
+		{
+			name:      "empty key format",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{},
+		},
+		{
+			name: "invalid version",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+				Version:           1,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+				ModulusSizeInBits: 3072,
+			},
+		},
+		{
+			name: "invalid algorithm",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS_UNKNOWN,
+				Version:           0,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+				ModulusSizeInBits: 3072,
+			},
+		},
+		{
+			name: "invalid public exponent",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+				Version:           0,
+				PublicExponent:    []byte{0x01},
+				ModulusSizeInBits: 3072,
+			},
+		},
+		{
+			name: "invalid modulus size",
+			keyFormat: &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+				Version:           0,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+				ModulusSizeInBits: 1024,
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedKeyFormat, err := proto.Marshal(tc.keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := km.NewKeyData(serializedKeyFormat); err == nil {
+				t.Fatalf("NewKeyData() err = nil, want error")
+			}
+			if _, err := km.NewKey(serializedKeyFormat); err == nil {
+				t.Fatalf("NewKey() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWTRSSignerKeyManagerNewKeyDataFailsWithNilKeyFormat(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	if _, err := km.NewKeyData(nil); err == nil {
+		t.Fatalf("NewKeyData() err = nil, want error")
+	}
+	if _, err := km.NewKey(nil); err == nil {
+		t.Fatalf("NewKey() err = nil, want error")
+	}
+}
+
+func TestJWTRSSignerKeyManagerNewKeyDataFailsWithInvalidSerializedKeyFormat(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSSignerKeyType, err)
+	}
+	if _, err := km.NewKeyData([]byte("invalid_data")); err == nil {
+		t.Fatalf("NewKeyData() err = nil, want error")
+	}
+	if _, err := km.NewKey([]byte("invalid_data")); err == nil {
+		t.Fatalf("NewKey() err = nil, want error")
+	}
+}
diff --git a/go/jwt/jwt_rsa_ssa_pkcs1_verifier_key_manager.go b/go/jwt/jwt_rsa_ssa_pkcs1_verifier_key_manager.go
new file mode 100644
index 0000000..fbb9044
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pkcs1_verifier_key_manager.go
@@ -0,0 +1,125 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"crypto/rsa"
+	"errors"
+	"fmt"
+	"math/big"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/keyset"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	jwtRSVerifierKeyVersion = 0
+	jwtRSVerifierTypeURL    = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey"
+)
+
+var (
+	errJWTRSVerifierNotImplemented = errors.New("not supported on verifier key manager")
+)
+
+// jwtRSVerifierKeyManager implements the KeyManager interface
+// for JWT Verifier using the 'RS256', 'RS384', and 'RS512' JSON Web Algorithms (JWA).
+type jwtRSVerifierKeyManager struct{}
+
+var _ registry.KeyManager = (*jwtRSVerifierKeyManager)(nil)
+
+// adding to this map will automatically add to the list of
+// "accepted" algorithms that will construct valid primitives.
+var validRSAlgToHash = map[jrsppb.JwtRsaSsaPkcs1Algorithm]string{
+	jrsppb.JwtRsaSsaPkcs1Algorithm_RS256: "SHA256",
+	jrsppb.JwtRsaSsaPkcs1Algorithm_RS384: "SHA384",
+	jrsppb.JwtRsaSsaPkcs1Algorithm_RS512: "SHA512",
+}
+
+func (km *jwtRSVerifierKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if serializedKey == nil || len(serializedKey) == 0 {
+		return nil, fmt.Errorf("invalid key")
+	}
+	pubKey := &jrsppb.JwtRsaSsaPkcs1PublicKey{}
+	if err := proto.Unmarshal(serializedKey, pubKey); err != nil {
+		return nil, err
+	}
+	if err := validateRSPublicKey(pubKey); err != nil {
+		return nil, err
+	}
+	e := new(big.Int).SetBytes(pubKey.GetE())
+	if !e.IsInt64() {
+		return nil, fmt.Errorf("public exponent can't fit in a 64 bit integer")
+	}
+	rsaPubKey := &rsa.PublicKey{
+		N: new(big.Int).SetBytes(pubKey.GetN()),
+		E: int(e.Int64()),
+	}
+	v, err := signature.New_RSA_SSA_PKCS1_Verifier(validRSAlgToHash[pubKey.GetAlgorithm()], rsaPubKey)
+	if err != nil {
+		return nil, err
+	}
+	return newVerifierWithKID(v, pubKey.GetAlgorithm().String(), rsCustomKID(pubKey))
+}
+
+func (km *jwtRSVerifierKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	return nil, errJWTRSVerifierNotImplemented
+}
+
+func (km *jwtRSVerifierKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	return nil, errJWTRSVerifierNotImplemented
+}
+
+func (km *jwtRSVerifierKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == jwtRSVerifierTypeURL
+}
+
+func (km *jwtRSVerifierKeyManager) TypeURL() string {
+	return jwtRSVerifierTypeURL
+}
+
+func validateRSPublicKey(pubKey *jrsppb.JwtRsaSsaPkcs1PublicKey) error {
+	if pubKey == nil {
+		return fmt.Errorf("nil public key")
+	}
+	if err := keyset.ValidateKeyVersion(pubKey.Version, jwtRSVerifierKeyVersion); err != nil {
+		return err
+	}
+	if _, ok := validRSAlgToHash[pubKey.GetAlgorithm()]; !ok {
+		return fmt.Errorf("invalid algorithm")
+	}
+	e := new(big.Int).SetBytes(pubKey.GetE())
+	if !e.IsInt64() {
+		return fmt.Errorf("public exponent can't fit in a 64 bit integer")
+	}
+	if err := signature.RSAValidPublicExponent(int(e.Int64())); err != nil {
+		return err
+	}
+	return signature.RSAValidModulusSizeInBits(new(big.Int).SetBytes(pubKey.GetN()).BitLen())
+}
+
+func rsCustomKID(pk *jrsppb.JwtRsaSsaPkcs1PublicKey) *string {
+	// nil is an acceptable value for a custom kid.
+	if pk.GetCustomKid() == nil {
+		return nil
+	}
+	k := pk.GetCustomKid().GetValue()
+	return &k
+}
diff --git a/go/jwt/jwt_rsa_ssa_pkcs1_verifier_key_manager_test.go b/go/jwt/jwt_rsa_ssa_pkcs1_verifier_key_manager_test.go
new file mode 100644
index 0000000..0e0880a
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pkcs1_verifier_key_manager_test.go
@@ -0,0 +1,371 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/subtle/random"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto"
+)
+
+const testJWTRSVerifierKeyType = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey"
+
+func makeValidRSPublicKey() (*jrsppb.JwtRsaSsaPkcs1PublicKey, error) {
+	// Public key taken from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
+	n, err := base64Decode(
+		"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx" +
+			"HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs" +
+			"D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH" +
+			"SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV" +
+			"MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8" +
+			"NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ")
+	if err != nil {
+		return nil, fmt.Errorf("base64 decoding modulus: %v", err)
+	}
+	e, err := base64Decode("AQAB")
+	if err != nil {
+		return nil, fmt.Errorf("base64 decoding public exponent: %v", err)
+	}
+	return &jrsppb.JwtRsaSsaPkcs1PublicKey{
+		Algorithm: jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+		Version:   0,
+		N:         n,
+		E:         e,
+		CustomKid: nil,
+	}, nil
+}
+
+func TestJWTRSVerifierNotImplemented(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	keyFormat := &jrsppb.JwtRsaSsaPkcs1KeyFormat{
+		Version:           0,
+		Algorithm:         jrsppb.JwtRsaSsaPkcs1Algorithm_RS256,
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01}, // 65537 aka F4
+	}
+	serializedKeyFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := km.NewKey(serializedKeyFormat); err == nil {
+		t.Fatalf("km.NewKey() err = nil, want error")
+	}
+	if _, err := km.NewKeyData(serializedKeyFormat); err == nil {
+		t.Fatalf("km.NewKeyData() err = nil, want error")
+	}
+}
+
+func TestJWTRSVerifierDoesSupport(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	if !km.DoesSupport(testJWTRSVerifierKeyType) {
+		t.Errorf("DoesSupport(%q) = false, want true", testJWTRSVerifierKeyType)
+	}
+	if km.DoesSupport("not.the.actual.key.type") {
+		t.Errorf("km.DoesSupport('not.the.actual.key.type') = true, want false")
+	}
+}
+
+func TestJWTRSVerifierTypeURL(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	if km.TypeURL() != testJWTRSVerifierKeyType {
+		t.Errorf("km.TypeURL() = %q, want %q", km.TypeURL(), testJWTRSVerifierKeyType)
+	}
+}
+
+func TestJWTRSVerifierPrimitiveWithInvalidKey(t *testing.T) {
+	type testCase struct {
+		name   string
+		pubKey *jrsppb.JwtRsaSsaPkcs1PublicKey
+	}
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	validPubKey, err := makeValidRSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidRSAPSSPKCS1PrivKey() err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:   "nil public key",
+			pubKey: nil,
+		},
+		{
+			name:   "empty public key",
+			pubKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{},
+		},
+		{
+			name: "invalid version",
+			pubKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+				Version:   validPubKey.Version + 1,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         validPubKey.GetN(),
+				E:         validPubKey.GetE(),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "invalid algorithm",
+			pubKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+				Algorithm: jrsppb.JwtRsaSsaPkcs1Algorithm_RS_UNKNOWN,
+				Version:   validPubKey.Version,
+				N:         validPubKey.GetN(),
+				E:         validPubKey.GetE(),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "invalid modulus",
+			pubKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+				Version:   validPubKey.Version,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         []byte{0x00},
+				E:         validPubKey.GetE(),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "invalid exponent",
+			pubKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+				Version:   validPubKey.Version,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         validPubKey.GetN(),
+				E:         []byte{0x05, 0x04, 0x03},
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "exponent larger than 64 bits",
+			pubKey: &jrsppb.JwtRsaSsaPkcs1PublicKey{
+				Version:   validPubKey.Version,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         validPubKey.GetN(),
+				E:         random.GetRandomBytes(65),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedPubKey, err := proto.Marshal(tc.pubKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := km.Primitive(serializedPubKey); err == nil {
+				t.Errorf("Primitive() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWTRSVerifierPrimitiveWithInvalidSerializedKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	if _, err := km.Primitive([]byte("invalid_serialization")); err == nil {
+		t.Errorf("Primitive() err = nil, want error")
+	}
+}
+
+func TestJWTRSVerifierPrimitiveVerifyFixedToken(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	pubKey, err := makeValidRSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidRSPublicKey() err = %v, want nil", err)
+	}
+	serializedPubKey, err := proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err := km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	// compact from https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
+	compact := "eyJhbGciOiJSUzI1NiJ9" +
+		"." +
+		"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt" +
+		"cGxlLmNvbS9pc19yb290Ijp0cnVlfQ" +
+		"." +
+		"cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7" +
+		"AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4" +
+		"BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K" +
+		"0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqv" +
+		"hJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrB" +
+		"p0igcN_IoypGlUPQGe77Rw"
+	issuer := "joe"
+	validator, err := NewValidator(&ValidatorOpts{
+		ExpectedIssuer: &issuer,
+		FixedNow:       time.Unix(123, 0),
+	})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	// verification succeeds because token was valid on January 1, 1970 UTC.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, nil); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	// verification with KID fails because token contains no KID.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("1234")); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '1234') err = nil, want error")
+	}
+}
+
+func TestJWTRSVerifierPrimitiveWithCustomKID(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	pubKey, err := makeValidRSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidRSPublicKey() err = %v, want nil", err)
+	}
+	pubKey.CustomKid = &jrsppb.JwtRsaSsaPkcs1PublicKey_CustomKid{
+		Value: "8542",
+	}
+	serializedPubKey, err := proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err := km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	// similar to https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2 but with KID 8542
+	compact := "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg1NDIifQ" +
+		"." +
+		"eyJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290I" +
+		"jp0cnVlLCJpc3MiOiJqb2UifQ" +
+		"." +
+		"aoQ4f8U_gpIymZM20rbAG2kjw5H5EKruPqPWf_wsEDeKPSjCXzkW016s5UqTz" +
+		"dJ72ZEP05PPZHs4VtZslUXQajLlZNgbK3UJ86QYBrqENq0Pwnhh43TVPi9lrF" +
+		"xOLjSQHAqKXYCy4aflqRdZqP9QqpLqaKtB1mAcDNM25Qx01Ix9FV_ngqI5OLD" +
+		"OYyDp5HoxgMAV-jNR9yq-r31_EBQmmDFHC8K8NJ5XLa4SybbhNlUWi6b1p7sQ" +
+		"NIOcb6RtSGSL73m-FYOo_dOMZ1ZNd7a_JiJe7QZ3-v1Dnw9GBSxvLdtKye2Fu" +
+		"ZHietYMJJczj14KeDbBK6TwmbUM8AacLt-JGg"
+	verifier, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	issuer := "joe"
+	validator, err := NewValidator(&ValidatorOpts{
+		ExpectedIssuer: &issuer,
+		FixedNow:       time.Unix(123, 0),
+	})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	// verification succeeds because token was valid on January 1, 1970 UTC.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, nil); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	// verification with custom KID and Tink KID fails, there can only be one KID set.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("8542")); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '8542') err = nil, want error")
+	}
+	pubKey.CustomKid = &jrsppb.JwtRsaSsaPkcs1PublicKey_CustomKid{
+		Value: "1234",
+	}
+	serializedPubKey, err = proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err = km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	verifierWrongKID, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	// primitive contains invalid Custom KID which fails verification.
+	if _, err := verifierWrongKID.VerifyAndDecodeWithKID(compact, validator, nil); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = nil) err = nil, want error")
+	}
+}
+
+func TestJWTRSVerifierPrimitiveWithTinkKID(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTRSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTRSVerifierKeyType, err)
+	}
+	pubKey, err := makeValidRSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidRSPublicKey() err = %v, want nil", err)
+	}
+	pubKey.CustomKid = nil
+	serializedPubKey, err := proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err := km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	// similar to https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2 but with KID 8542
+	compact := "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg1NDIifQ" +
+		"." +
+		"eyJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290I" +
+		"jp0cnVlLCJpc3MiOiJqb2UifQ" +
+		"." +
+		"aoQ4f8U_gpIymZM20rbAG2kjw5H5EKruPqPWf_wsEDeKPSjCXzkW016s5UqTz" +
+		"dJ72ZEP05PPZHs4VtZslUXQajLlZNgbK3UJ86QYBrqENq0Pwnhh43TVPi9lrF" +
+		"xOLjSQHAqKXYCy4aflqRdZqP9QqpLqaKtB1mAcDNM25Qx01Ix9FV_ngqI5OLD" +
+		"OYyDp5HoxgMAV-jNR9yq-r31_EBQmmDFHC8K8NJ5XLa4SybbhNlUWi6b1p7sQ" +
+		"NIOcb6RtSGSL73m-FYOo_dOMZ1ZNd7a_JiJe7QZ3-v1Dnw9GBSxvLdtKye2Fu" +
+		"ZHietYMJJczj14KeDbBK6TwmbUM8AacLt-JGg"
+	issuer := "joe"
+	validator, err := NewValidator(&ValidatorOpts{
+		ExpectedIssuer: &issuer,
+		FixedNow:       time.Unix(123, 0),
+	})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("8542")); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '8542') err = %v, want nil", err)
+	}
+	// verification fails with invalid KID
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("2333")); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '2333') err = nil, want error")
+	}
+}
diff --git a/go/jwt/jwt_rsa_ssa_pss_signer_key_manager.go b/go/jwt/jwt_rsa_ssa_pss_signer_key_manager.go
new file mode 100644
index 0000000..0835f8c
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pss_signer_key_manager.go
@@ -0,0 +1,194 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"errors"
+	"fmt"
+	"math/big"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/keyset"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	jwtPSSignerKeyVersion = 0
+	jwtPSSignerTypeURL    = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+)
+
+var (
+	errPSInvalidPrivateKey = errors.New("invalid JwtRsaSsaPssPrivateKey")
+	errPSInvalidKeyFormat  = errors.New("invalid RSA SSA PSS key format")
+)
+
+// jwtPSSignerKeyManager implements the KeyManager interface
+// for JWT Signing using the 'PS256', 'PS384', and 'PS512' JWA algorithm.
+type jwtPSSignerKeyManager struct{}
+
+var _ registry.PrivateKeyManager = (*jwtPSSignerKeyManager)(nil)
+
+func (km *jwtPSSignerKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if serializedKey == nil {
+		return nil, fmt.Errorf("invalid JwtRsaSsaPSSPrivateKey")
+	}
+	privKey := &jrsppb.JwtRsaSsaPssPrivateKey{}
+	if err := proto.Unmarshal(serializedKey, privKey); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal JwtRsaSsaPssPrivateKey: %v", err)
+	}
+	if err := validatePSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	rsaPrivKey := &rsa.PrivateKey{
+		PublicKey: rsa.PublicKey{
+			N: bytesToBigInt(privKey.GetPublicKey().GetN()),
+			E: int(bytesToBigInt(privKey.GetPublicKey().GetE()).Int64()),
+		},
+		D: bytesToBigInt(privKey.GetD()),
+		Primes: []*big.Int{
+			bytesToBigInt(privKey.GetP()),
+			bytesToBigInt(privKey.GetQ()),
+		},
+		Precomputed: rsa.PrecomputedValues{
+			Dp: bytesToBigInt(privKey.GetDp()),
+			Dq: bytesToBigInt(privKey.GetDq()),
+			// in crypto/rsa `Qinv` is the "Chinese Remainder Theorem
+			// coefficient q^(-1) mod p. Which is `GetCrt` in the tink proto and not
+			// the `CRTValues`.
+			Qinv: bytesToBigInt(privKey.GetCrt()),
+		},
+	}
+	algorithm := privKey.GetPublicKey().GetAlgorithm()
+	if err := signature.Validate_RSA_SSA_PSS(validPSAlgToHash[algorithm], psAlgToSaltLen[algorithm], rsaPrivKey); err != nil {
+		return nil, err
+	}
+	signer, err := signature.New_RSA_SSA_PSS_Signer(validPSAlgToHash[algorithm], psAlgToSaltLen[algorithm], rsaPrivKey)
+	if err != nil {
+		return nil, err
+	}
+	return newSignerWithKID(signer, algorithm.String(), psCustomKID(privKey.GetPublicKey()))
+}
+
+func validatePSPrivateKey(privKey *jrsppb.JwtRsaSsaPssPrivateKey) error {
+	if err := keyset.ValidateKeyVersion(privKey.Version, jwtPSSignerKeyVersion); err != nil {
+		return err
+	}
+	if privKey.GetD() == nil ||
+		len(privKey.GetPublicKey().GetN()) == 0 ||
+		len(privKey.GetPublicKey().GetE()) == 0 ||
+		privKey.GetP() == nil ||
+		privKey.GetQ() == nil ||
+		privKey.GetDp() == nil ||
+		privKey.GetDq() == nil ||
+		privKey.GetCrt() == nil {
+		return fmt.Errorf("invalid private key")
+	}
+	if err := validatePSPublicKey(privKey.GetPublicKey()); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (km *jwtPSSignerKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errPSInvalidKeyFormat
+	}
+	keyFormat := &jrsppb.JwtRsaSsaPssKeyFormat{}
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal JwtRsaSsaPssKeyFormat: %v", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), jwtPSSignerKeyVersion); err != nil {
+		return nil, err
+	}
+	rsaKey, err := rsa.GenerateKey(rand.Reader, int(keyFormat.GetModulusSizeInBits()))
+	if err != nil {
+		return nil, err
+	}
+	privKey := &jrsppb.JwtRsaSsaPssPrivateKey{
+		Version: jwtPSSignerKeyVersion,
+		PublicKey: &jrsppb.JwtRsaSsaPssPublicKey{
+			Version:   jwtPSSignerKeyVersion,
+			Algorithm: keyFormat.GetAlgorithm(),
+			N:         rsaKey.PublicKey.N.Bytes(),
+			E:         keyFormat.GetPublicExponent(),
+		},
+		D:  rsaKey.D.Bytes(),
+		P:  rsaKey.Primes[0].Bytes(),
+		Q:  rsaKey.Primes[1].Bytes(),
+		Dp: rsaKey.Precomputed.Dp.Bytes(),
+		Dq: rsaKey.Precomputed.Dq.Bytes(),
+		// in crypto/rsa `Qinv` is the "Chinese Remainder Theorem
+		// coefficient q^(-1) mod p. Which is `Crt` in the tink proto and not
+		// the `CRTValues`.
+		Crt: rsaKey.Precomputed.Qinv.Bytes(),
+	}
+	if err := validatePSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	return privKey, nil
+}
+
+func (km *jwtPSSignerKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	key, err := km.NewKey(serializedKeyFormat)
+	if err != nil {
+		return nil, err
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         jwtPSSignerTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE,
+	}, nil
+}
+
+func (km *jwtPSSignerKeyManager) PublicKeyData(serializedPrivKey []byte) (*tinkpb.KeyData, error) {
+	if serializedPrivKey == nil {
+		return nil, errPSInvalidKeyFormat
+	}
+	privKey := &jrsppb.JwtRsaSsaPssPrivateKey{}
+	if err := proto.Unmarshal(serializedPrivKey, privKey); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal JwtRsaSsaPssPrivateKey: %v", err)
+	}
+	if err := validatePSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	serializedPubKey, err := proto.Marshal(privKey.GetPublicKey())
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         jwtPSVerifierTypeURL,
+		Value:           serializedPubKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
+	}, nil
+}
+
+func (km *jwtPSSignerKeyManager) DoesSupport(typeURL string) bool {
+	return jwtPSSignerTypeURL == typeURL
+}
+
+func (km *jwtPSSignerKeyManager) TypeURL() string {
+	return jwtPSSignerTypeURL
+}
diff --git a/go/jwt/jwt_rsa_ssa_pss_signer_key_manager_test.go b/go/jwt/jwt_rsa_ssa_pss_signer_key_manager_test.go
new file mode 100644
index 0000000..178e90e
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pss_signer_key_manager_test.go
@@ -0,0 +1,670 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"math/big"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+	"github.com/google/tink/go/core/registry"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const testJWTPSSignerKeyType = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+
+func makeValidJWTPSPrivateKey() (*jrsppb.JwtRsaSsaPssPrivateKey, error) {
+	// key taken from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
+	pubKey, err := makeValidPSPublicKey()
+	if err != nil {
+		return nil, err
+	}
+	d, err := base64Decode(
+		"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I" +
+			"jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0" +
+			"BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn" +
+			"439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT" +
+			"CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh" +
+			"BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ")
+	if err != nil {
+		return nil, err
+	}
+	p, err := base64Decode(
+		"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi" +
+			"YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG" +
+			"BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc")
+	if err != nil {
+		return nil, err
+	}
+	q, err := base64Decode(
+		"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa" +
+			"ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA" +
+			"-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc")
+	if err != nil {
+		return nil, err
+	}
+	dp, err := base64Decode(
+		"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q" +
+			"CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb" +
+			"34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0")
+	if err != nil {
+		return nil, err
+	}
+	dq, err := base64Decode(
+		"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa" +
+			"7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky" +
+			"NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU")
+	if err != nil {
+		return nil, err
+	}
+	qi, err := base64Decode(
+		"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o" +
+			"y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU" +
+			"W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U")
+	if err != nil {
+		return nil, err
+	}
+	return &jrsppb.JwtRsaSsaPssPrivateKey{
+		PublicKey: pubKey,
+		Version:   0,
+		D:         d,
+		P:         p,
+		Q:         q,
+		Dp:        dp,
+		Dq:        dq,
+		Crt:       qi,
+	}, nil
+}
+
+func TestJWTPSSignerKeyManagerDoesSupport(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	if !km.DoesSupport(testJWTPSSignerKeyType) {
+		t.Errorf("DoesSupport(%q) = false, want true", testJWTPSSignerKeyType)
+	}
+	if km.DoesSupport("invalid.key.type") {
+		t.Errorf("DoesSupport(%q) = true, want false", "invalid.key.type")
+	}
+}
+
+func TestJWTPSSignerKeyManagerTypeURL(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	if km.TypeURL() != testJWTPSSignerKeyType {
+		t.Errorf("TypeURL() = %v, want = %v", km.TypeURL(), testJWTPSSignerKeyType)
+	}
+}
+
+func TestJWTPSSignerKeyManagerPritimiveSignVerify(t *testing.T) {
+	skm, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	vkm, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	type testCase struct {
+		tag       string
+		customKID *jrsppb.JwtRsaSsaPssPublicKey_CustomKid
+		tinkKID   *string
+	}
+	for _, tc := range []testCase{
+		{
+			tag:       "no tink and no custom kid",
+			customKID: nil,
+			tinkKID:   nil,
+		},
+		{
+			tag:       "with tink kid and no custom kid",
+			customKID: nil,
+			tinkKID:   refString("1234"),
+		},
+		{
+			tag: "no tink kid and with custom kid",
+			customKID: &jrsppb.JwtRsaSsaPssPublicKey_CustomKid{
+				Value: "",
+			},
+			tinkKID: nil,
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			privKey, err := makeValidJWTPSPrivateKey()
+			if err != nil {
+				t.Fatalf("makeValidJWTPSPrivateKey() err = %v, want nil", err)
+			}
+			privKey.PublicKey.CustomKid = tc.customKID
+			serializedPrivKey, err := proto.Marshal(privKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			sp, err := skm.Primitive(serializedPrivKey)
+			if err != nil {
+				t.Fatalf("Primitive() err = %v, want nil", err)
+			}
+			signer, ok := sp.(*signerWithKID)
+			if !ok {
+				t.Fatalf("primitive isn't a JWT signer")
+			}
+			unsigned, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true})
+			if err != nil {
+				t.Fatalf("NewRawJWT() err = %v, want nil", err)
+			}
+			signed, err := signer.SignAndEncodeWithKID(unsigned, tc.tinkKID)
+			if err != nil {
+				t.Fatalf("SignAndEncodeWithKID(kid = nil) err = %v, want nil", err)
+			}
+			serializedPubKey, err := proto.Marshal(privKey.PublicKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			vp, err := vkm.Primitive(serializedPubKey)
+			if err != nil {
+				t.Fatalf("Primitive() err = %v, want nil", err)
+			}
+			verifier, ok := vp.(*verifierWithKID)
+			if !ok {
+				t.Fatalf("primitive isn't a JWT Verifier")
+			}
+			validator, err := NewValidator(&ValidatorOpts{AllowMissingExpiration: true})
+			if err != nil {
+				t.Fatalf("NewValidator() err = %v, want nil", err)
+			}
+			if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, tc.tinkKID); err != nil {
+				t.Errorf("VerifyAndDecodeWithKID(kid = %v) err = %v, want nil", tc.tinkKID, err)
+			}
+			// wrong KID header should fail.
+			if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, refString("wrong_kid")); err == nil {
+				t.Errorf("VerifyAndDecodeWithKID(kid = 'wrong_kid') err = nil, want error")
+			}
+			// signer/verifier can only have either Tink KID or Custom KID set at a time but not both.
+			if tc.customKID != nil {
+				if _, err := signer.SignAndEncodeWithKID(unsigned, refString("123")); err == nil {
+					t.Fatalf("SignAndEncodeWithKID(kid = '123') err = nil, want error")
+				}
+				if _, err := verifier.VerifyAndDecodeWithKID(signed, validator, refString("123")); err == nil {
+					t.Errorf("VerifyAndDecodeWithKID(kid = '123') err = nil, want error")
+				}
+			}
+			if tc.tinkKID != nil {
+				// The tc.tinkKID value was written into the JWT header in SignAndEncodeWithKID.
+				// It is now ignored if the Tink KID is not set.
+				if _, err := verifier.VerifyAndDecodeWithKID(signed, validator /*=tinkKID*/, nil); err != nil {
+					t.Errorf("VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+				}
+			}
+		})
+	}
+}
+
+func TestJWTPSSignerKeyManagerPrimitiveFailsWithCorruptedKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	validPrivKey, err := makeValidJWTPSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTPSPrivateKey() err = %v, want nil", err)
+	}
+	invalidQ := validPrivKey.GetQ()
+	invalidQ[50] = byte(uint8(invalidQ[50] + 1))
+	corruptedPrivKey := &jrsppb.JwtRsaSsaPssPrivateKey{
+		Version:   validPrivKey.GetVersion(),
+		PublicKey: validPrivKey.GetPublicKey(),
+		D:         validPrivKey.GetD(),
+		P:         validPrivKey.GetP(),
+		Q:         invalidQ,
+		Dp:        validPrivKey.GetDp(),
+		Dq:        validPrivKey.GetDq(),
+		Crt:       validPrivKey.GetCrt(),
+	}
+	serializedPrivKey, err := proto.Marshal(corruptedPrivKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := km.Primitive(serializedPrivKey); err == nil {
+		t.Fatalf("Primitive() err = nil, want error")
+	}
+}
+
+func TestJWTPSSignerKeyManagerPrimitiveFailsWithInvalidKey(t *testing.T) {
+	type testCase struct {
+		name    string
+		privKey *jrsppb.JwtRsaSsaPssPrivateKey
+	}
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	validPrivKey, err := makeValidJWTPSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTPSPrivateKey() err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:    "nil private key",
+			privKey: nil,
+		},
+		{
+			name:    "empty private key",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{},
+		},
+		{
+			name: "invalid private key version",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion() + 1,
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid private key D value",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         nil,
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid private key P value",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         nil,
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid private key Q value",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         nil,
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid pre computed key Dp value",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        nil,
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid pre computed key Dq value",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        nil,
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid pre computed key Crt value",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: validPrivKey.GetPublicKey(),
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       nil,
+			},
+		},
+		{
+			name: "nil public key",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: nil,
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "empty public key",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPssPublicKey{},
+				Version:   validPrivKey.GetVersion(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid public key version",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPssPublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion() + 1,
+					Algorithm: validPrivKey.GetPublicKey().GetAlgorithm(),
+					N:         validPrivKey.GetPublicKey().GetN(),
+					E:         validPrivKey.GetPublicKey().GetE(),
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid algorithm",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPssPublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion(),
+					Algorithm: jrsppb.JwtRsaSsaPssAlgorithm_PS_UNKNOWN,
+					N:         validPrivKey.GetPublicKey().GetN(),
+					E:         validPrivKey.GetPublicKey().GetE(),
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid modulus",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPssPublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion(),
+					Algorithm: validPrivKey.GetPublicKey().GetAlgorithm(),
+					N:         []byte{0x00, 0x01},
+					E:         validPrivKey.GetPublicKey().GetE(),
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid exponent",
+			privKey: &jrsppb.JwtRsaSsaPssPrivateKey{
+				PublicKey: &jrsppb.JwtRsaSsaPssPublicKey{
+					Version:   validPrivKey.GetPublicKey().GetVersion(),
+					Algorithm: validPrivKey.GetPublicKey().GetAlgorithm(),
+					N:         validPrivKey.GetPublicKey().GetN(),
+					E:         []byte{0x07},
+					CustomKid: validPrivKey.GetPublicKey().GetCustomKid(),
+				},
+				Version: validPrivKey.GetVersion(),
+				D:       validPrivKey.GetD(),
+				P:       validPrivKey.GetP(),
+				Q:       validPrivKey.GetQ(),
+				Dp:      validPrivKey.GetDp(),
+				Dq:      validPrivKey.GetDq(),
+				Crt:     validPrivKey.GetCrt(),
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedPrivKey, err := proto.Marshal(tc.privKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := km.Primitive(serializedPrivKey); err == nil {
+				t.Fatalf("Primitive() err = nil, want error")
+			}
+			if _, err := km.(registry.PrivateKeyManager).PublicKeyData(serializedPrivKey); err == nil {
+				t.Fatalf("PublicKeyData() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWTPSSignerKeyManagerPrimitiveFailsWithNilSerializedKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	if _, err := km.(registry.PrivateKeyManager).PublicKeyData(nil); err == nil {
+		t.Fatalf("PublicKeyData() err = nil, want error")
+	}
+}
+
+func TestJWTPSSignerKeyManagerPrimitiveFailsWithInvalidSerializedKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	if _, err := km.(registry.PrivateKeyManager).PublicKeyData([]byte("invalid_serialization")); err == nil {
+		t.Fatalf("PublicKeyData() err = nil, want error")
+	}
+}
+
+func TestJWTPSSignerKeyManagerPublicKeyData(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	privKey, err := makeValidJWTPSPrivateKey()
+	if err != nil {
+		t.Fatalf("makeValidJWTPSPrivateKey() err = %v, want nil", err)
+	}
+	serializedPrivKey, err := proto.Marshal(privKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	pubKeyData, err := km.(registry.PrivateKeyManager).PublicKeyData(serializedPrivKey)
+	if err != nil {
+		t.Fatalf("PublicKeyData() err = %v, want nil", err)
+	}
+	if pubKeyData.GetKeyMaterialType() != tpb.KeyData_ASYMMETRIC_PUBLIC {
+		t.Errorf("GetKeyMaterialType() = %v, want %v", pubKeyData.GetKeyMaterialType(), tpb.KeyData_ASYMMETRIC_PUBLIC)
+	}
+	if pubKeyData.GetTypeUrl() != testJWTPSVerifierKeyType {
+		t.Errorf("TypeURL() = %v, want %v", pubKeyData.GetTypeUrl(), testJWTPSVerifierKeyType)
+	}
+	gotPubKey := &jrsppb.JwtRsaSsaPssPublicKey{}
+	if err := proto.Unmarshal(pubKeyData.GetValue(), gotPubKey); err != nil {
+		t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+	}
+	if !cmp.Equal(gotPubKey, privKey.GetPublicKey(), protocmp.Transform()) {
+		t.Errorf("got = %v, want = %v", gotPubKey, privKey.GetPublicKey())
+	}
+}
+
+func TestJWTPSSignerKeyManagerNewKeyData(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	keyFormat := &jrsppb.JwtRsaSsaPssKeyFormat{
+		Version:           0,
+		Algorithm:         jrsppb.JwtRsaSsaPssAlgorithm_PS256,
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedKeyFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	keyData, err := km.NewKeyData(serializedKeyFormat)
+	if err != nil {
+		t.Fatalf("NewKeyData() err = %v, want nil", err)
+	}
+	if keyData.GetTypeUrl() != testJWTPSSignerKeyType {
+		t.Errorf("GetTypeURL() = %v, want %v", keyData.GetTypeUrl(), testJWTPSSignerKeyType)
+	}
+	if keyData.GetKeyMaterialType() != tpb.KeyData_ASYMMETRIC_PRIVATE {
+		t.Errorf("GetKeyMaterialType() = %v, want %v", keyData.GetKeyMaterialType(), tpb.KeyData_ASYMMETRIC_PRIVATE)
+	}
+	key := &jrsppb.JwtRsaSsaPssPrivateKey{}
+	if err := proto.Unmarshal(keyData.GetValue(), key); err != nil {
+		t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+	}
+	pubKey := key.GetPublicKey()
+	if pubKey.GetAlgorithm() != keyFormat.GetAlgorithm() {
+		t.Errorf("GetAlgorithm() = %v, want %v", pubKey.GetAlgorithm(), keyFormat.GetAlgorithm())
+	}
+	if !cmp.Equal(pubKey.GetE(), keyFormat.GetPublicExponent()) {
+		t.Errorf("Exponent = %v, want %v", pubKey.GetE(), keyFormat.GetPublicExponent())
+	}
+	modSize := new(big.Int).SetBytes(pubKey.GetN()).BitLen()
+	if modSize != int(keyFormat.GetModulusSizeInBits()) {
+		t.Errorf("Modulus Size in Bits = %d, want %d", modSize, keyFormat.GetModulusSizeInBits())
+	}
+}
+
+func TestJWTPSSignerKeyManagerNewKeyDataFailsWithInvalidFormat(t *testing.T) {
+	type testCase struct {
+		name      string
+		keyFormat *jrsppb.JwtRsaSsaPssKeyFormat
+	}
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:      "nil key format",
+			keyFormat: nil,
+		},
+		{
+			name:      "empty key format",
+			keyFormat: &jrsppb.JwtRsaSsaPssKeyFormat{},
+		},
+		{
+			name: "invalid version",
+			keyFormat: &jrsppb.JwtRsaSsaPssKeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPssAlgorithm_PS256,
+				Version:           1,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+				ModulusSizeInBits: 3072,
+			},
+		},
+		{
+			name: "invalid algorithm",
+			keyFormat: &jrsppb.JwtRsaSsaPssKeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPssAlgorithm_PS_UNKNOWN,
+				Version:           0,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+				ModulusSizeInBits: 3072,
+			},
+		},
+		{
+			name: "invalid public exponent",
+			keyFormat: &jrsppb.JwtRsaSsaPssKeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPssAlgorithm_PS256,
+				Version:           0,
+				PublicExponent:    []byte{0x01},
+				ModulusSizeInBits: 3072,
+			},
+		},
+		{
+			name: "invalid modulus size",
+			keyFormat: &jrsppb.JwtRsaSsaPssKeyFormat{
+				Algorithm:         jrsppb.JwtRsaSsaPssAlgorithm_PS256,
+				Version:           0,
+				PublicExponent:    []byte{0x01, 0x00, 0x01},
+				ModulusSizeInBits: 1024,
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedKeyFormat, err := proto.Marshal(tc.keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := km.NewKeyData(serializedKeyFormat); err == nil {
+				t.Fatalf("NewKeyData() err = nil, want error")
+			}
+			if _, err := km.NewKey(serializedKeyFormat); err == nil {
+				t.Fatalf("NewKey() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWTPSSignerKeyManagerNewKeyDataFailsWithNilKeyFormat(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	if _, err := km.NewKeyData(nil); err == nil {
+		t.Fatalf("NewKeyData() err = nil, want error")
+	}
+	if _, err := km.NewKey(nil); err == nil {
+		t.Fatalf("NewKey() err = nil, want error")
+	}
+}
+
+func TestJWTPSSignerKeyManagerNewKeyDataFailsWithInvalidSerializedKeyFormat(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSSignerKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSSignerKeyType, err)
+	}
+	if _, err := km.NewKeyData([]byte("invalid_serialization")); err == nil {
+		t.Fatalf("NewKeyData() err = nil, want error")
+	}
+	if _, err := km.NewKey([]byte("invalid_serialization")); err == nil {
+		t.Fatalf("NewKey() err = nil, want error")
+	}
+}
diff --git a/go/jwt/jwt_rsa_ssa_pss_verify_key_manager.go b/go/jwt/jwt_rsa_ssa_pss_verify_key_manager.go
new file mode 100644
index 0000000..ae6b42d
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pss_verify_key_manager.go
@@ -0,0 +1,130 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"crypto/rsa"
+	"errors"
+	"fmt"
+	"math/big"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/keyset"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	jwtPSVerifierKeyVersion = 0
+	jwtPSVerifierTypeURL    = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey"
+)
+
+var errJWTPSVerifierNotImplemented = errors.New("not supported on verifier key manager")
+
+// jwtPSVerifierKeyManager implements the KeyManager interface
+// for JWT Verifier using the 'PS256', 'PS384', and 'PS512' JSON Web Algorithms (JWA).
+type jwtPSVerifierKeyManager struct{}
+
+var _ registry.KeyManager = (*jwtPSVerifierKeyManager)(nil)
+
+// adding to this map will automatically add to the list of
+// "accepted" algorithms that will construct valid primitives.
+var validPSAlgToHash = map[jrsppb.JwtRsaSsaPssAlgorithm]string{
+	jrsppb.JwtRsaSsaPssAlgorithm_PS256: "SHA256",
+	jrsppb.JwtRsaSsaPssAlgorithm_PS384: "SHA384",
+	jrsppb.JwtRsaSsaPssAlgorithm_PS512: "SHA512",
+}
+
+var psAlgToSaltLen = map[jrsppb.JwtRsaSsaPssAlgorithm]int{
+	jrsppb.JwtRsaSsaPssAlgorithm_PS256: 32,
+	jrsppb.JwtRsaSsaPssAlgorithm_PS384: 48,
+	jrsppb.JwtRsaSsaPssAlgorithm_PS512: 64,
+}
+
+func (km *jwtPSVerifierKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if serializedKey == nil || len(serializedKey) == 0 {
+		return nil, fmt.Errorf("invalid key")
+	}
+	pubKey := &jrsppb.JwtRsaSsaPssPublicKey{}
+	if err := proto.Unmarshal(serializedKey, pubKey); err != nil {
+		return nil, err
+	}
+	if err := validatePSPublicKey(pubKey); err != nil {
+		return nil, err
+	}
+	e := new(big.Int).SetBytes(pubKey.GetE())
+	if !e.IsInt64() {
+		return nil, fmt.Errorf("public exponent can't fit in a 64 bit integer")
+	}
+	rsaPubKey := &rsa.PublicKey{
+		N: new(big.Int).SetBytes(pubKey.GetN()),
+		E: int(e.Int64()),
+	}
+	algorithm := pubKey.GetAlgorithm()
+	v, err := signature.New_RSA_SSA_PSS_Verifier(validPSAlgToHash[algorithm], psAlgToSaltLen[algorithm], rsaPubKey)
+	if err != nil {
+		return nil, err
+	}
+	return newVerifierWithKID(v, algorithm.String(), psCustomKID(pubKey))
+}
+
+func (km *jwtPSVerifierKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	return nil, errJWTPSVerifierNotImplemented
+}
+
+func (km *jwtPSVerifierKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	return nil, errJWTPSVerifierNotImplemented
+}
+
+func (km *jwtPSVerifierKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == jwtPSVerifierTypeURL
+}
+
+func (km *jwtPSVerifierKeyManager) TypeURL() string {
+	return jwtPSVerifierTypeURL
+}
+
+func validatePSPublicKey(pubKey *jrsppb.JwtRsaSsaPssPublicKey) error {
+	if pubKey == nil {
+		return fmt.Errorf("nil public key")
+	}
+	if err := keyset.ValidateKeyVersion(pubKey.Version, jwtPSVerifierKeyVersion); err != nil {
+		return err
+	}
+	if _, ok := validPSAlgToHash[pubKey.GetAlgorithm()]; !ok {
+		return fmt.Errorf("invalid algorithm")
+	}
+	e := new(big.Int).SetBytes(pubKey.GetE())
+	if !e.IsInt64() {
+		return fmt.Errorf("public exponent can't fit in a 64 bit integer")
+	}
+	if err := signature.RSAValidPublicExponent(int(e.Int64())); err != nil {
+		return err
+	}
+	return signature.RSAValidModulusSizeInBits(new(big.Int).SetBytes(pubKey.GetN()).BitLen())
+}
+
+func psCustomKID(pk *jrsppb.JwtRsaSsaPssPublicKey) *string {
+	// nil is an acceptable value for a custom kid.
+	if pk.GetCustomKid() == nil {
+		return nil
+	}
+	k := pk.GetCustomKid().GetValue()
+	return &k
+}
diff --git a/go/jwt/jwt_rsa_ssa_pss_verify_key_manager_test.go b/go/jwt/jwt_rsa_ssa_pss_verify_key_manager_test.go
new file mode 100644
index 0000000..9038475
--- /dev/null
+++ b/go/jwt/jwt_rsa_ssa_pss_verify_key_manager_test.go
@@ -0,0 +1,338 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package jwt
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/subtle/random"
+	jrsppb "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto"
+)
+
+const testJWTPSVerifierKeyType = "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey"
+
+func makeValidPSPublicKey() (*jrsppb.JwtRsaSsaPssPublicKey, error) {
+	// Public key taken from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
+	n, err := base64Decode(
+		"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx" +
+			"HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs" +
+			"D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH" +
+			"SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV" +
+			"MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8" +
+			"NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ")
+	if err != nil {
+		return nil, fmt.Errorf("base64 decoding modulus: %v", err)
+	}
+	e, err := base64Decode("AQAB")
+	if err != nil {
+		return nil, fmt.Errorf("base64 decoding public exponent: %v", err)
+	}
+	return &jrsppb.JwtRsaSsaPssPublicKey{
+		Algorithm: jrsppb.JwtRsaSsaPssAlgorithm_PS256,
+		Version:   0,
+		N:         n,
+		E:         e,
+		CustomKid: nil,
+	}, nil
+}
+
+func TestJWTPSVerifierNotImplemented(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	keyFormat := &jrsppb.JwtRsaSsaPssKeyFormat{
+		Version:           0,
+		Algorithm:         jrsppb.JwtRsaSsaPssAlgorithm_PS256,
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01}, // 65537 aka F4
+	}
+	serializedKeyFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := km.NewKey(serializedKeyFormat); err == nil {
+		t.Fatalf("km.NewKey() err = nil, want error")
+	}
+	if _, err := km.NewKeyData(serializedKeyFormat); err == nil {
+		t.Fatalf("km.NewKeyData() err = nil, want error")
+	}
+}
+
+func TestJWTPSVerifierDoesSupport(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	if !km.DoesSupport(testJWTPSVerifierKeyType) {
+		t.Errorf("DoesSupport(%q) = false, want true", testJWTPSVerifierKeyType)
+	}
+	if km.DoesSupport("not.the.actual.key.type") {
+		t.Errorf("km.DoesSupport('not.the.actual.key.type') = true, want false")
+	}
+}
+
+func TestJWTPSVerifierTypeURL(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	if km.TypeURL() != testJWTPSVerifierKeyType {
+		t.Errorf("km.TypeURL() = %q, want %q", km.TypeURL(), testJWTPSVerifierKeyType)
+	}
+}
+
+func TestJWTPSVerifierPrimitiveWithInvalidKey(t *testing.T) {
+	type testCase struct {
+		name   string
+		pubKey *jrsppb.JwtRsaSsaPssPublicKey
+	}
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	validPubKey, err := makeValidPSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidPSPublicKey() err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			name:   "nil public key",
+			pubKey: nil,
+		},
+		{
+			name:   "empty public key",
+			pubKey: &jrsppb.JwtRsaSsaPssPublicKey{},
+		},
+		{
+			name: "invalid version",
+			pubKey: &jrsppb.JwtRsaSsaPssPublicKey{
+				Version:   validPubKey.Version + 1,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         validPubKey.GetN(),
+				E:         validPubKey.GetE(),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "invalid algorithm",
+			pubKey: &jrsppb.JwtRsaSsaPssPublicKey{
+				Algorithm: jrsppb.JwtRsaSsaPssAlgorithm_PS_UNKNOWN,
+				Version:   validPubKey.Version,
+				N:         validPubKey.GetN(),
+				E:         validPubKey.GetE(),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "invalid modulus",
+			pubKey: &jrsppb.JwtRsaSsaPssPublicKey{
+				Version:   validPubKey.Version,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         []byte{0x00},
+				E:         validPubKey.GetE(),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "invalid exponent",
+			pubKey: &jrsppb.JwtRsaSsaPssPublicKey{
+				Version:   validPubKey.Version,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         validPubKey.GetN(),
+				E:         []byte{0x05, 0x04, 0x03},
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+		{
+			name: "exponent larger than 64 bits",
+			pubKey: &jrsppb.JwtRsaSsaPssPublicKey{
+				Version:   validPubKey.Version,
+				Algorithm: validPubKey.GetAlgorithm(),
+				N:         validPubKey.GetN(),
+				E:         random.GetRandomBytes(65),
+				CustomKid: validPubKey.GetCustomKid(),
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			serializedPubKey, err := proto.Marshal(tc.pubKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := km.Primitive(serializedPubKey); err == nil {
+				t.Errorf("Primitive() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestJWTPSVerifierPrimitiveVerifyFixedToken(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	pubKey, err := makeValidPSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidPSPublicKey() err = %v, want nil", err)
+	}
+	serializedPubKey, err := proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err := km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	// // similar to https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2
+	compact := "eyJhbGciOiJQUzI1NiJ9" +
+		"." +
+		"eyJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJqb2UifQ" +
+		"." +
+		"PpLcmEZ2zlsOmYygy8SU9Zxwab9deDuibgCg8dCZ8Po1N51kyMU9Mty7wj9fTCOONNqu3QxLe_2Wu_BkVhz41W" +
+		"bxXrP3cci7deSnQmgN2ZkA23egSFfMoDd56CFvY3-eaG22NRxPsDWypECdDgXJXoSPnlRxgtaJDxUUD3Ej9DZ4" +
+		"gmdVcG4ZqmLSxoIAXtmjGi-Da_fqf48DOKaL5AI1uE2SW_byXPXdtaD_oIvNoeL0J5wuU2cSJQutu-UCyfO1rl" +
+		"R3DTOzR_XRx7dEzziqfzP7_YlSxdidkph1Jrh1DIapxsWrnaShYFofS35Vg17SdciALeRMnQHwhClJJqgChg"
+	issuer := "joe"
+	validator, err := NewValidator(&ValidatorOpts{
+		ExpectedIssuer: &issuer,
+		FixedNow:       time.Unix(123, 0),
+	})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	// verification succeeds because token was valid on January 1, 1970 UTC.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, nil); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	// verification with KID fails because token contains no KID.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("")); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '1234') err = nil, want error")
+	}
+}
+
+func TestJWTPSVerifierPrimitiveVerifyFixedTokenWithCustomKID(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	pubKey, err := makeValidPSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidPSPublicKey() err = %v, want nil", err)
+	}
+	pubKey.CustomKid = &jrsppb.JwtRsaSsaPssPublicKey_CustomKid{
+		Value: "oneoh",
+	}
+	serializedPubKey, err := proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err := km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	// // similar to https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2 but with KID "oneoh"
+	compact := "eyJhbGciOiJQUzI1NiIsImtpZCI6Im9uZW9oIn0" +
+		"." +
+		"eyJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJqb2UifQ" +
+		"." +
+		"hrkeS71m1bg9tDBfEI3P-E6CkLZuNOguG0LlY5Yb-HzjFan9_LmvmemMCYYTsifNJkJkiSZRwkv7BQ0Svd6Rn_" +
+		"TzckQdpr37pez_2mywfpAbYWxi40n35q9Q3W8IWgbpZFIRIru0n1R7v4XIpkVbd90IwahgZG3Yhvlwt3-EWCwz" +
+		"7tb3_EbcFFHsSK0PH-b9mPwrUzb_l-jJR5T2zATc3lTriyGsOhyubBAwcxKuAEg5Ru7_vgLI352jEzjFsz05Fu" +
+		"QVMEtdBGqiLn2iIu8yDQtKMPm-FBhBO_uomHcxjLY4nBziAkba3WPUGkB4HvbIGQz9ZedUjd2ivCQ52GT2uw"
+	issuer := "joe"
+	validator, err := NewValidator(&ValidatorOpts{
+		ExpectedIssuer: &issuer,
+		FixedNow:       time.Unix(123, 0),
+	})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	// verification succeeds because token was valid on January 1, 1970 UTC.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, nil); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = 'oneoh') err = %v, want nil", err)
+	}
+	// verification fails with Custom KID and Tink KID, only one can be present.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("oneoh")); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '1234') err = nil, want error")
+	}
+}
+
+func TestJWTPSVerifierPrimitiveVerifyFixedTokenWithTinkKID(t *testing.T) {
+	km, err := registry.GetKeyManager(testJWTPSVerifierKeyType)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testJWTPSVerifierKeyType, err)
+	}
+	pubKey, err := makeValidPSPublicKey()
+	if err != nil {
+		t.Fatalf("makeValidPSPublicKey() err = %v, want nil", err)
+	}
+	pubKey.CustomKid = nil
+	serializedPubKey, err := proto.Marshal(pubKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	v, err := km.Primitive(serializedPubKey)
+	if err != nil {
+		t.Fatalf("km.Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := v.(*verifierWithKID)
+	if !ok {
+		t.Fatalf("primitive isn't a JWT Verifier")
+	}
+	// // similar to https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2 but with KID "oneoh"
+	compact := "eyJhbGciOiJQUzI1NiIsImtpZCI6Im9uZW9oIn0" +
+		"." +
+		"eyJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJqb2UifQ" +
+		"." +
+		"hrkeS71m1bg9tDBfEI3P-E6CkLZuNOguG0LlY5Yb-HzjFan9_LmvmemMCYYTsifNJkJkiSZRwkv7BQ0Svd6Rn_" +
+		"TzckQdpr37pez_2mywfpAbYWxi40n35q9Q3W8IWgbpZFIRIru0n1R7v4XIpkVbd90IwahgZG3Yhvlwt3-EWCwz" +
+		"7tb3_EbcFFHsSK0PH-b9mPwrUzb_l-jJR5T2zATc3lTriyGsOhyubBAwcxKuAEg5Ru7_vgLI352jEzjFsz05Fu" +
+		"QVMEtdBGqiLn2iIu8yDQtKMPm-FBhBO_uomHcxjLY4nBziAkba3WPUGkB4HvbIGQz9ZedUjd2ivCQ52GT2uw"
+	issuer := "joe"
+	validator, err := NewValidator(&ValidatorOpts{
+		ExpectedIssuer: &issuer,
+		FixedNow:       time.Unix(123, 0),
+	})
+	if err != nil {
+		t.Fatalf("NewValidator() err = %v, want nil", err)
+	}
+	// verification succeeds because token was valid on January 1, 1970 UTC.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("oneoh")); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = 'oneoh') err = %v, want nil", err)
+	}
+	// verification without Tink KID ignores KID header.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, nil); err != nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = nil) err = %v, want nil", err)
+	}
+	// verification with incorrect KID fails because token contains KID header.
+	if _, err := verifier.VerifyAndDecodeWithKID(compact, validator, refString("1234")); err == nil {
+		t.Errorf("verifier.VerifyAndDecodeWithKID(kid = '1234') err = nil, want error")
+	}
+}
diff --git a/go/jwt/jwt_signer_factory.go b/go/jwt/jwt_signer_factory.go
index 2563c8a..0ee893e 100644
--- a/go/jwt/jwt_signer_factory.go
+++ b/go/jwt/jwt_signer_factory.go
@@ -24,11 +24,11 @@
 )
 
 // NewSigner generates a new instance of the JWT Signer primitive.
-func NewSigner(h *keyset.Handle) (Signer, error) {
-	if h == nil {
+func NewSigner(handle *keyset.Handle) (Signer, error) {
+	if handle == nil {
 		return nil, fmt.Errorf("keyset handle can't be nil")
 	}
-	ps, err := h.PrimitivesWithKeyManager(nil)
+	ps, err := handle.PrimitivesWithKeyManager(nil)
 	if err != nil {
 		return nil, fmt.Errorf("jwt_signer_factory: cannot obtain primitive set: %v", err)
 	}
diff --git a/go/jwt/jwt_signer_verifier_factory_test.go b/go/jwt/jwt_signer_verifier_factory_test.go
index 1f20420..d5ec415 100644
--- a/go/jwt/jwt_signer_verifier_factory_test.go
+++ b/go/jwt/jwt_signer_verifier_factory_test.go
@@ -258,6 +258,44 @@
 	}
 }
 
+func TestVerifyAndDecodeReturnsValidationError(t *testing.T) {
+	_, privateHandle, publicHandle := createKeyAndKeyHandles(t, nil /*=kid*/, tinkpb.OutputPrefixType_TINK)
+	signer, err := jwt.NewSigner(privateHandle)
+	if err != nil {
+		t.Fatalf("jwt.NewSigner() err = %v, want nil", err)
+	}
+	verifier, err := jwt.NewVerifier(publicHandle)
+	if err != nil {
+		t.Fatalf("jwt.NewVerifier() err = %v, want nil", err)
+	}
+
+	audience := "audience"
+	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{Audience: &audience, WithoutExpiration: true})
+	if err != nil {
+		t.Fatalf("jwt.NewRawJWT() err = %v, want nil", err)
+	}
+
+	compact, err := signer.SignAndEncode(rawJWT)
+	if err != nil {
+		t.Errorf("signer.SignAndEncode() err = %v, want nil", err)
+	}
+
+	otherAudience := "otherAudience"
+	validator, err := jwt.NewValidator(
+		&jwt.ValidatorOpts{ExpectedAudience: &otherAudience, AllowMissingExpiration: true})
+	if err != nil {
+		t.Fatalf("jwt.NewValidator() err = %v, want nil", err)
+	}
+	_, err = verifier.VerifyAndDecode(compact, validator)
+	wantErr := "validating audience claim: otherAudience not found"
+	if err == nil {
+		t.Errorf("verifier.VerifyAndDecode() err = nil, want %q", wantErr)
+	}
+	if err.Error() != wantErr {
+		t.Errorf("verifier.VerifyAndDecode() err = %q, want %q", err.Error(), wantErr)
+	}
+}
+
 func TestFactorySignVerifyWithKIDSuccess(t *testing.T) {
 	rawJWT, err := jwt.NewRawJWT(&jwt.RawJWTOptions{WithoutExpiration: true})
 	if err != nil {
diff --git a/go/jwt/jwt_test.go b/go/jwt/jwt_test.go
index 50359cf..1d31df2 100644
--- a/go/jwt/jwt_test.go
+++ b/go/jwt/jwt_test.go
@@ -17,32 +17,75 @@
 package jwt_test
 
 import (
+	"bytes"
 	"fmt"
 	"log"
 	"time"
 
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/jwt"
 	"github.com/google/tink/go/keyset"
 )
 
 func Example_signAndVerify() {
-	// Generate a private keyset handle.
-	handlePriv, err := keyset.NewHandle(jwt.ES256Template())
+	// A private keyset created with
+	// "tinkey create-keyset --key-template=JWT_RS256_2048_F4 --out private_keyset.cfg".
+	// Note that this keyset has the secret key information in cleartext.
+	privateJSONKeyset := `{
+		"primaryKeyId": 185188009,
+		"key": [
+			{
+				"keyData": {
+					"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey",
+					"value": "EosCEAEagQIAs9iifvWObNLbP+x7zupVIYTdHKba4VFgJEnnGtIII21R+KGddTdvNGAokd4GPrFk1GDPitHrAAoW1+NWrafsEUi2J9Sy3uwEyarsKDggewoBCNg2fcWAiZXplPjUyTlhrLvTuyrcL/mGPy+ib7bdmov+D2EP+rKUH6/ydtQGiyHRR3uurTUWfrMD1/6WaBVfngpy5Pxs2nuHXRmBHQKWmPfvErgr4abdjhKDaWIuxzSise1CSAbiWTNcxpIuFYZgPjgQzpqeh93LUXIX9YJds/bhHtXqRdxk6yTisloHOZETItK/rHCCE25dLkkaJ2Li7AtnJdBc6tEUNiuFj2JCjSIDAQABGoACT2lWxwySaQbp/N3lBUZ/dJ+AKsiaWWdfNmbTfwpCwbHhwhFKv5lMpynWgCIzS7d0uDpPKhLq20eZMpaVjXRaTn92vzuyB7DbpFiukkvGO839CvS9iueMjDP/weHlwzxtHqKJKVoRg7WAS6Iy7XUngLhT5GKNdbsooJ1GSKXyhbgWyMcspKSQe4lZXUntVMK5z4iLNmcQwsBp8yM55mZra13TXowob/E/wd+tGiABCn6CDt8G1gXzWDaoF2tt6WhSGZbXUVGagmoea/BWeAuJyKSSi5h+uPpc5SPhGvyKfSEVaCs2QeM7/UIXhzAcx2j/VqySb6y9EbSiJfy8vr49QSKBAQD+AbFCGHd9kZ5LIQrfe9caOxS9pQPdFkBJESw0C3x2uBIg8awiQsuVXMeEgyGLyWBZoi2x98OMSR9OzCuSLtb7Nv0Wqn0LUj4WPRdmg//uLeD3O2rcVRIR4db/B8WvXnK2uQsqwGDyh4BepGvprXQPYMX2uwnBGL2ccS2De53HJSqBAQC1QfOi4egjmlmXqJLpISUSN1NixkIi8EJHaZZ0YrbaRrEyiJczthcazNHFt6gzgOcosFaKaZeqps4Tet+5NgS7eh7RzLQ2+cfT4ewpT2ExJ4NsOy8XDqD6GRjliLxjGAoUf24s3B+3LLACPiQjeeZGJP0ivh384WabyXXxRgHFSTKBAQChl7gKIYCbHPHEQAAnzyQ4Js/6GinMFCTPlyI09f23lUDLPpRQs4fKvNydO8Myp+ko/NjvOH1qGPbW7WLmu+++n+wA6HNmqWqgQTtK170Q7JULE/zWsTQutitN0cb82yxFfJFTIFJM2NFc5GNWpSeJxPoMDk+VTcUK6qGW3SSyFTqBAQCeaPFA3SZAV1kNjio2zNzVOr0JijOqzUdfmgv/03Xy9e1POMjMTMuMhIygu42o1XMwwEwh037Vicp4g96aw3cHUgc1XC30DgByUPRQdit/BgV5xY+2GvbdHKoBkKrz/8Jvf58OXaLqN4frrdtvlc2GaDVC89zJcUR3ym3lW0WY4UKBAQD6MCruwXaxXJMxjtlH1YT5ow4R5neeiswNfGj4Ta/WbWyiVA60zpdNbGqH+etmiHY8+aBb/H4O9+JhOcBtlMLN4UlK1jg8wPSemZjsIPiUZXHkeIUa2RTUSz90wgz7aOqC0lYsLLFaJNWs54fC9LpZ0JzoqYDI8iDPnlE7xaag9g==",
+					"keyMaterialType": "ASYMMETRIC_PRIVATE"
+				},
+				"status": "ENABLED",
+				"keyId": 185188009,
+				"outputPrefixType": "TINK"
+			}
+		]
+	}`
+
+	// The corresponding public keyset created with
+	// "tinkey create-public-keyset --in private_keyset.cfg"
+	publicJSONKeyset := `{
+		"primaryKeyId": 185188009,
+		"key": [
+			{
+				"keyData": {
+					"typeUrl": "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey",
+					"value": "EAEagQIAs9iifvWObNLbP+x7zupVIYTdHKba4VFgJEnnGtIII21R+KGddTdvNGAokd4GPrFk1GDPitHrAAoW1+NWrafsEUi2J9Sy3uwEyarsKDggewoBCNg2fcWAiZXplPjUyTlhrLvTuyrcL/mGPy+ib7bdmov+D2EP+rKUH6/ydtQGiyHRR3uurTUWfrMD1/6WaBVfngpy5Pxs2nuHXRmBHQKWmPfvErgr4abdjhKDaWIuxzSise1CSAbiWTNcxpIuFYZgPjgQzpqeh93LUXIX9YJds/bhHtXqRdxk6yTisloHOZETItK/rHCCE25dLkkaJ2Li7AtnJdBc6tEUNiuFj2JCjSIDAQAB",
+					"keyMaterialType": "ASYMMETRIC_PUBLIC"
+				},
+				"status": "ENABLED",
+				"keyId": 185188009,
+				"outputPrefixType": "TINK"
+			}
+		]
+	}`
+
+	// Create a keyset handle from the cleartext private keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the access of the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use a insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
+	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
+	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
+	privateKeysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(privateJSONKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// TODO: Save the private keyset to a safe location. DO NOT hardcode it in
-	// source code.  Consider encrypting it with a remote key in a KMS.
-	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets
-
-	// Get a public keyset handle from the private keyset handle.
-	handlePub, err := handlePriv.Public()
+	// Retrieve the JWT Signer primitive from privateKeysetHandle.
+	signer, err := jwt.NewSigner(privateKeysetHandle)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// Create and sign a token.
+	// Use the primitive to create and sign a token. In this case, the primary key of the
+	// keyset will be used (which is also the only key in this example).
 	expiresAt := time.Now().Add(time.Hour)
 	audience := "example audience"
 	subject := "example subject"
@@ -54,20 +97,26 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	signer, err := jwt.NewSigner(handlePriv)
-	if err != nil {
-		log.Fatal(err)
-	}
 	token, err := signer.SignAndEncode(rawJWT)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// Verify the signed token.
-	verifier, err := jwt.NewVerifier(handlePub)
+	// Create a keyset handle from the keyset containing the public key. Because the
+	// public keyset does not contain any secrets, we can use [keyset.ReadWithNoSecrets].
+	publicKeysetHandle, err := keyset.ReadWithNoSecrets(
+		keyset.NewJSONReader(bytes.NewBufferString(publicJSONKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
+
+	// Retrieve the Verifier primitive from publicKeysetHandle.
+	verifier, err := jwt.NewVerifier(publicKeysetHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Verify the signed token.
 	validator, err := jwt.NewValidator(&jwt.ValidatorOpts{ExpectedAudience: &audience})
 	if err != nil {
 		log.Fatal(err)
@@ -89,6 +138,65 @@
 	// Output: example subject
 }
 
+func Example_verifyWithJWKS() {
+	// A signed token with the subject 'example subject', audience 'example audience'.
+	// and expiration on 2023-03-23.
+	token := `eyJhbGciOiJSUzI1NiIsImtpZCI6IkN3bS1xUSJ9.eyJhdWQiOiJleGFtcGxlIGF1ZGllbmNlIiwiZXhwIjoxNjc5NTcyODQzLCJzdWIiOiJleGFtcGxlIHN1YmplY3QifQ.dUPhvdmEnGuyESLBQn5OC3QmnRcJlcMfxDPsZ2wfqBK9poQag94xLxBnkzSZnhPP2gQcIt2aOCFeftL1MK3boI3g887J2hZ6hJmeABVi82YGK16P6LIgZuALdjiUcyexus5sxcEo2iuELzUy0hOzS2dDQWOoWCznltGFuavNQGW8A2365JScCsQeoDLAa-IX89vJww0uQVRZ8AxYigLJ5DhILtu-Lssq5sSpT28XASAMzafuYvAI60Cw8nvxTaheRA8AkTI9DWERV4Z-0UQNV2O61U6_24hkjIYCGpuz8_5vBB-W3jijIdWf8J1BNyBfjNeh9eXgSZh8J3wBCEb98Q`
+
+	// A public keyset in the JWK set format.
+	publicJWKset := `{
+		"keys":[
+			{
+				"alg":"RS256",
+				"e":"AQAB",
+				"key_ops":["verify"],
+				"kid":"Cwm-qQ",
+				"kty":"RSA",
+				"n":"ALPYon71jmzS2z_se87qVSGE3Rym2uFRYCRJ5xrSCCNtUfihnXU3bzRgKJHeBj6xZNRgz4rR6wAKFtfjVq2n7BFItifUst7sBMmq7Cg4IHsKAQjYNn3FgImV6ZT41Mk5Yay707sq3C_5hj8vom-23ZqL_g9hD_qylB-v8nbUBosh0Ud7rq01Fn6zA9f-lmgVX54KcuT8bNp7h10ZgR0Clpj37xK4K-Gm3Y4Sg2liLsc0orHtQkgG4lkzXMaSLhWGYD44EM6anofdy1FyF_WCXbP24R7V6kXcZOsk4rJaBzmREyLSv6xwghNuXS5JGidi4uwLZyXQXOrRFDYrhY9iQo0",
+				"use":"sig"
+			}
+		]
+	}`
+
+	// Create a keyset handle from publicJWKset.
+	publicKeysetHandle, err := jwt.JWKSetToPublicKeysetHandle([]byte(publicJWKset))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Retrieve the Verifier primitive from publicKeysetHandle.
+	verifier, err := jwt.NewVerifier(publicKeysetHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Verify the signed token. For this example, we use a fixed date. Usually, you would
+	// either not set FixedNow, or set it to the current time.
+	audience := "example audience"
+	validator, err := jwt.NewValidator(&jwt.ValidatorOpts{
+		ExpectedAudience: &audience,
+		FixedNow:         time.Date(2023, 3, 23, 0, 0, 0, 0, time.UTC),
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+	verifiedJWT, err := verifier.VerifyAndDecode(token, validator)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Extract subject claim from the token.
+	if !verifiedJWT.HasSubject() {
+		log.Fatal(err)
+	}
+	extractedSubject, err := verifiedJWT.Subject()
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(extractedSubject)
+	// Output: example subject
+}
+
 func Example_computeMACAndVerify() {
 	// Generate a keyset handle.
 	handle, err := keyset.NewHandle(jwt.HS256Template())
diff --git a/go/jwt/jwt_validator.go b/go/jwt/jwt_validator.go
index 84461b2..6f6de73 100644
--- a/go/jwt/jwt_validator.go
+++ b/go/jwt/jwt_validator.go
@@ -114,7 +114,7 @@
 			return err
 		}
 		if !exp.After(now.Add(-v.opts.ClockSkew)) {
-			return fmt.Errorf("token has expired")
+			return errJwtExpired
 		}
 	}
 	if rawJWT.HasNotBefore() {
@@ -169,7 +169,7 @@
 		return err
 	}
 	if issuer != *v.opts.ExpectedIssuer {
-		return fmt.Errorf("wrong issuer")
+		return fmt.Errorf("got %s, want %s", issuer, *v.opts.ExpectedIssuer)
 	}
 	return nil
 }
@@ -191,7 +191,7 @@
 			break
 		}
 		if i == len(audiences)-1 {
-			return fmt.Errorf("audience not found")
+			return fmt.Errorf("%s not found", *v.opts.ExpectedAudience)
 		}
 	}
 	return nil
diff --git a/go/jwt/jwt_validator_test.go b/go/jwt/jwt_validator_test.go
index bb1787e..dab19a4 100644
--- a/go/jwt/jwt_validator_test.go
+++ b/go/jwt/jwt_validator_test.go
@@ -276,6 +276,58 @@
 	}
 }
 
+func TestExpiredTokenValidationReturnsExpiredErr(t *testing.T) {
+	tokenOpts := &jwt.RawJWTOptions{
+		ExpiresAt: refTime(100),
+	}
+	expiredToken, err := jwt.NewRawJWT(tokenOpts)
+	if err != nil {
+		t.Fatalf("jwt.NewRawJWT(tokenOpts) err = %v, want nil", err)
+	}
+	validatorOpts := &jwt.ValidatorOpts{
+		FixedNow: time.Unix(500, 0),
+	}
+	validator, err := jwt.NewValidator(validatorOpts)
+	if err != nil {
+		t.Fatalf("jwt.NewValidator(validatorOpts) err = %v, want nil", err)
+	}
+
+	validationErr := validator.Validate(expiredToken)
+	if validationErr == nil {
+		t.Errorf("validator.Validate(expiredToken) err = nil, want error")
+	}
+	if !jwt.IsExpirationErr(validationErr) {
+		t.Errorf("jwt.IsExpirationErr(validationErr) = false, want true")
+	}
+}
+
+func TestExpirationGetsValidatedFirst(t *testing.T) {
+	tokenOpts := &jwt.RawJWTOptions{
+		ExpiresAt: refTime(100),
+		Audience:  refString("invalidAudience"),
+	}
+	expiredTokenWithInvalidAudience, err := jwt.NewRawJWT(tokenOpts)
+	if err != nil {
+		t.Fatalf("jwt.NewRawJWT(tokenOpts) err = %v, want nil", err)
+	}
+	validatorOpts := &jwt.ValidatorOpts{
+		ExpectedAudiences: refString("audience"),
+		FixedNow:          time.Unix(500, 0),
+	}
+	validator, err := jwt.NewValidator(validatorOpts)
+	if err != nil {
+		t.Fatalf("jwt.NewValidator(validatorOpts) err = %v, want nil", err)
+	}
+
+	validationErr := validator.Validate(expiredTokenWithInvalidAudience)
+	if validationErr == nil {
+		t.Errorf("validator.Validate(expiredTokenWithInvalidAudience) err = nil, want error")
+	}
+	if !jwt.IsExpirationErr(validationErr) {
+		t.Errorf("jwt.IsExpirationErr(validationErr) = false, want true")
+	}
+}
+
 func TestValidationSuccess(t *testing.T) {
 	for _, tc := range []validationTestCase{
 		{
diff --git a/go/jwt/jwt_verifier_factory.go b/go/jwt/jwt_verifier_factory.go
index b3cc522..c3ed420 100644
--- a/go/jwt/jwt_verifier_factory.go
+++ b/go/jwt/jwt_verifier_factory.go
@@ -24,11 +24,11 @@
 )
 
 // NewVerifier generates a new instance of the JWT Verifier primitive.
-func NewVerifier(h *keyset.Handle) (Verifier, error) {
-	if h == nil {
+func NewVerifier(handle *keyset.Handle) (Verifier, error) {
+	if handle == nil {
 		return nil, fmt.Errorf("keyset handle can't be nil")
 	}
-	ps, err := h.PrimitivesWithKeyManager(nil)
+	ps, err := handle.PrimitivesWithKeyManager(nil)
 	if err != nil {
 		return nil, fmt.Errorf("jwt_verifier_factory: cannot obtain primitive set: %v", err)
 	}
@@ -57,16 +57,25 @@
 }
 
 func (w *wrappedVerifier) VerifyAndDecode(compact string, validator *Validator) (*VerifiedJWT, error) {
+	var interestingErr error
 	for _, s := range w.ps.Entries {
 		for _, e := range s {
 			p, ok := e.Primitive.(*verifierWithKID)
 			if !ok {
 				return nil, fmt.Errorf("jwt_verifier_factory: not a JWT Verifier primitive")
 			}
-			if verifiedJWT, err := p.VerifyAndDecodeWithKID(compact, validator, keyID(e.KeyID, e.PrefixType)); err == nil {
+			verifiedJWT, err := p.VerifyAndDecodeWithKID(compact, validator, keyID(e.KeyID, e.PrefixType))
+			if err == nil {
 				return verifiedJWT, nil
 			}
+			if err != errJwtVerification {
+				// any error that is not the generic errJwtVerification is considered interesting
+				interestingErr = err
+			}
 		}
 	}
-	return nil, fmt.Errorf("verification failed")
+	if interestingErr != nil {
+		return nil, interestingErr
+	}
+	return nil, errJwtVerification
 }
diff --git a/go/jwt/jwt_verifier_kid.go b/go/jwt/jwt_verifier_kid.go
index 9a880eb..99c1356 100644
--- a/go/jwt/jwt_verifier_kid.go
+++ b/go/jwt/jwt_verifier_kid.go
@@ -44,14 +44,14 @@
 func (v *verifierWithKID) VerifyAndDecodeWithKID(compact string, validator *Validator, kid *string) (*VerifiedJWT, error) {
 	sig, content, err := splitSignedCompact(compact)
 	if err != nil {
-		return nil, err
+		return nil, errJwtVerification
 	}
 	if err := v.tv.Verify(sig, []byte(content)); err != nil {
-		return nil, err
+		return nil, errJwtVerification
 	}
 	rawJWT, err := decodeUnsignedTokenAndValidateHeader(content, v.algorithm, kid, v.customKID)
 	if err != nil {
-		return nil, err
+		return nil, errJwtVerification
 	}
 	if err := validator.Validate(rawJWT); err != nil {
 		return nil, err
diff --git a/go/jwt/raw_jwt.go b/go/jwt/raw_jwt.go
index 7025910..9510015 100644
--- a/go/jwt/raw_jwt.go
+++ b/go/jwt/raw_jwt.go
@@ -134,7 +134,7 @@
 	if val, isString := aud.GetKind().(*spb.Value_StringValue); isString {
 		return []string{val.StringValue}, nil
 	}
-	s := []string{}
+	s := make([]string, 0, len(aud.GetListValue().GetValues()))
 	for _, a := range aud.GetListValue().GetValues() {
 		s = append(s, a.GetStringValue())
 	}
@@ -516,7 +516,7 @@
 		return
 	}
 	audList := &spb.ListValue{
-		Values: []*spb.Value{},
+		Values: make([]*spb.Value, 0, len(vals)),
 	}
 	for _, aud := range vals {
 		audList.Values = append(audList.Values, spb.NewStringValue(aud))
diff --git a/go/jwt/raw_jwt_test.go b/go/jwt/raw_jwt_test.go
index ff6fd78..4787ccb 100644
--- a/go/jwt/raw_jwt_test.go
+++ b/go/jwt/raw_jwt_test.go
@@ -617,6 +617,16 @@
 			json: `{"exp":78324}`,
 		},
 		{
+			tag: "integer",
+			opts: &jwt.RawJWTOptions{
+				WithoutExpiration: true,
+				CustomClaims: map[string]interface{}{
+					"num": 1,
+				},
+			},
+			json: `{"num":1}`,
+		},
+		{
 			tag: "custom-claim",
 			opts: &jwt.RawJWTOptions{
 				WithoutExpiration: true,
diff --git a/go/keyderivation/BUILD.bazel b/go/keyderivation/BUILD.bazel
new file mode 100644
index 0000000..d11bffc
--- /dev/null
+++ b/go/keyderivation/BUILD.bazel
@@ -0,0 +1,72 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+package(default_visibility = ["//:__subpackages__"])  # keep
+
+licenses(["notice"])  # keep
+
+go_library(
+    name = "keyderivation",
+    srcs = [
+        "keyderivation.go",
+        "keyderivation_key_templates.go",
+        "keyset_deriver.go",
+        "keyset_deriver_factory.go",
+        "prf_based_deriver.go",
+        "prf_based_deriver_key_manager.go",
+    ],
+    importpath = "github.com/google/tink/go/keyderivation",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//core/primitiveset",
+        "//core/registry",
+        "//insecurecleartextkeyset",
+        "//internal",
+        "//internal/internalregistry",
+        "//keyderivation/internal/streamingprf",
+        "//keyset",
+        "//proto/prf_based_deriver_go_proto",
+        "//proto/tink_go_proto",
+        "@org_golang_google_protobuf//proto",
+    ],
+)
+
+go_test(
+    name = "keyderivation_test",
+    srcs = [
+        "keyderivation_key_templates_test.go",
+        "keyderivation_test.go",
+        "keyset_deriver_factory_test.go",
+        "keyset_deriver_factory_x_test.go",
+        "prf_based_deriver_key_manager_test.go",
+        "prf_based_deriver_test.go",
+    ],
+    embed = [":keyderivation"],
+    deps = [
+        "//aead",
+        "//core/cryptofmt",
+        "//core/primitiveset",
+        "//core/registry",
+        "//daead",
+        "//insecurecleartextkeyset",
+        "//keyset",
+        "//mac",
+        "//prf",
+        "//proto/aes_gcm_go_proto",
+        "//proto/common_go_proto",
+        "//proto/hkdf_prf_go_proto",
+        "//proto/prf_based_deriver_go_proto",
+        "//proto/tink_go_proto",
+        "//signature",
+        "//streamingaead",
+        "//subtle/random",
+        "@com_github_google_go_cmp//cmp",
+        "@org_golang_google_protobuf//proto",
+        "@org_golang_google_protobuf//testing/protocmp",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":keyderivation",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/keyderivation/internal/streamingprf/BUILD.bazel b/go/keyderivation/internal/streamingprf/BUILD.bazel
new file mode 100644
index 0000000..3e953ca
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/BUILD.bazel
@@ -0,0 +1,60 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+package(default_visibility = ["//:__subpackages__"])  # keep
+
+licenses(["notice"])  # keep
+
+go_library(
+    name = "streamingprf",
+    srcs = [
+        "hkdf_streaming_prf.go",
+        "hkdf_streaming_prf_key_manager.go",
+        "streaming_prf.go",
+        "streaming_prf_factory.go",
+    ],
+    importpath = "github.com/google/tink/go/keyderivation/internal/streamingprf",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//core/primitiveset",
+        "//core/registry",
+        "//keyset",
+        "//proto/common_go_proto",
+        "//proto/hkdf_prf_go_proto",
+        "//proto/tink_go_proto",
+        "//subtle",
+        "@org_golang_google_protobuf//proto",
+        "@org_golang_x_crypto//hkdf",
+    ],
+)
+
+go_test(
+    name = "streamingprf_test",
+    srcs = [
+        "hkdf_streaming_prf_key_manager_test.go",
+        "hkdf_streaming_prf_test.go",
+        "streaming_prf_factory_test.go",
+        "streaming_prf_test.go",
+    ],
+    data = ["@wycheproof//testvectors:all"],
+    embed = [":streamingprf"],
+    deps = [
+        "//aead",
+        "//core/registry",
+        "//keyset",
+        "//prf",
+        "//proto/aes_gcm_go_proto",
+        "//proto/common_go_proto",
+        "//proto/hkdf_prf_go_proto",
+        "//proto/tink_go_proto",
+        "//subtle/random",
+        "//testkeyset",
+        "//testutil",
+        "@org_golang_google_protobuf//proto",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":streamingprf",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/keyderivation/internal/streamingprf/hkdf_streaming_prf.go b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf.go
new file mode 100644
index 0000000..1be086b
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf.go
@@ -0,0 +1,67 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf
+
+import (
+	"fmt"
+	"hash"
+	"io"
+
+	"golang.org/x/crypto/hkdf"
+	"github.com/google/tink/go/subtle"
+)
+
+// minHKDFStreamingPRFKeySize is the minimum allowed key size in bytes.
+const minHKDFStreamingPRFKeySize = 32
+
+// hkdfStreamingPRF is a HKDF Streaming PRF that implements StreamingPRF.
+type hkdfStreamingPRF struct {
+	h    func() hash.Hash
+	key  []byte
+	salt []byte
+}
+
+// Asserts that hkdfStreamingPRF implements the StreamingPRF interface.
+var _ StreamingPRF = (*hkdfStreamingPRF)(nil)
+
+// newHKDFStreamingPRF constructs a new hkdfStreamingPRF using hashName, key,
+// and salt. Salt can be nil.
+func newHKDFStreamingPRF(hashName string, key, salt []byte) (*hkdfStreamingPRF, error) {
+	if err := validateHKDFStreamingPRFParams(hashName, len(key)); err != nil {
+		return nil, err
+	}
+	return &hkdfStreamingPRF{
+		h:    subtle.GetHashFunc(hashName),
+		key:  key,
+		salt: salt,
+	}, nil
+}
+
+// Compute computes and returns the HKDF as a Reader.
+func (h *hkdfStreamingPRF) Compute(data []byte) (io.Reader, error) {
+	return hkdf.New(h.h, h.key, h.salt, data), nil
+}
+
+func validateHKDFStreamingPRFParams(hash string, keySize int) error {
+	if hash != "SHA256" && hash != "SHA512" {
+		return fmt.Errorf("only SHA-256, SHA-512 allowed for HKDF")
+	}
+	if keySize < minHKDFStreamingPRFKeySize {
+		return fmt.Errorf("key too short, require %d-bytes: %d", minHKDFStreamingPRFKeySize, keySize)
+	}
+	return nil
+}
diff --git a/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_key_manager.go b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_key_manager.go
new file mode 100644
index 0000000..8f0379b
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_key_manager.go
@@ -0,0 +1,94 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf
+
+import (
+	"errors"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyset"
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
+	hkdfpb "github.com/google/tink/go/proto/hkdf_prf_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// TODO(b/260619626): HKDF PRF and HKDF Streaming PRF currently share the same
+// type URL. This is fine as HKDFStreamingPRFKeyManager is not in the global
+// registry. HKDF PRF and HKDF Streaming PRF will eventually share the same key
+// manager, rendering this one obsolete.
+
+const (
+	hkdfStreamingPRFKeyVersion = 0
+	hkdfPRFTypeURL             = "type.googleapis.com/google.crypto.tink.HkdfPrfKey"
+)
+
+var (
+	errInvalidHKDFStreamingPRFKey       = errors.New("hkdf_streaming_prf_key_manager: invalid key")
+	errInvalidHKDFStreamingPRFKeyFormat = errors.New("hkdf_streaming_prf_key_manager: invalid key format")
+	errHKDFStreamingPRFNotImplemented   = errors.New("hkdf_streaming_prf_key_manager: not implemented")
+)
+
+// HKDFStreamingPRFKeyManager is a KeyManager for HKDF Streaming PRF keys. It is
+// exported for use in keyderivation.prfBasedDeriver. This is not part of the
+// public API as this is in internal/.
+type HKDFStreamingPRFKeyManager struct{}
+
+var _ registry.KeyManager = (*HKDFStreamingPRFKeyManager)(nil)
+
+// Primitive constructs a primitive instance for the key given in serializedKey.
+func (km *HKDFStreamingPRFKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if len(serializedKey) == 0 {
+		return nil, errInvalidHKDFStreamingPRFKey
+	}
+	key := &hkdfpb.HkdfPrfKey{}
+	if err := proto.Unmarshal(serializedKey, key); err != nil {
+		return nil, errInvalidHKDFStreamingPRFKey
+	}
+	if keyset.ValidateKeyVersion(key.GetVersion(), hkdfStreamingPRFKeyVersion) != nil {
+		return nil, errInvalidHKDFStreamingPRFKey
+	}
+	hashName := commonpb.HashType_name[int32(key.GetParams().GetHash())]
+	return newHKDFStreamingPRF(hashName, key.GetKeyValue(), key.GetParams().GetSalt())
+}
+
+// NewKey generates a new key according to specification in serializedKeyFormat.
+// It is not implemented for this KeyManager to prevent the generation of keys
+// of this key type.
+func (km *HKDFStreamingPRFKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	return nil, errHKDFStreamingPRFNotImplemented
+}
+
+// NewKeyData generates a new KeyData according to specification in
+// serializedkeyFormat. This should be used solely by the key management API.
+// It is not implemented for this KeyManager to prevent the generation of keys
+// of this key type.
+func (km *HKDFStreamingPRFKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	return nil, errHKDFStreamingPRFNotImplemented
+}
+
+// DoesSupport returns true iff this KeyManager supports key type identified by
+// typeURL.
+func (km *HKDFStreamingPRFKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == hkdfPRFTypeURL
+}
+
+// TypeURL returns the type URL that identifes the key type of keys managed by
+// this KeyManager.
+func (km *HKDFStreamingPRFKeyManager) TypeURL() string {
+	return hkdfPRFTypeURL
+}
diff --git a/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_key_manager_test.go b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_key_manager_test.go
new file mode 100644
index 0000000..d28d8d9
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_key_manager_test.go
@@ -0,0 +1,227 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf_test
+
+import (
+	"strings"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/keyderivation/internal/streamingprf"
+	"github.com/google/tink/go/subtle/random"
+	aesgcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
+	hkdfpb "github.com/google/tink/go/proto/hkdf_prf_go_proto"
+)
+
+func TestHKDFStreamingPRFKeyManagerPrimitive(t *testing.T) {
+	km := streamingprf.HKDFStreamingPRFKeyManager{}
+	for _, test := range []struct {
+		name string
+		hash commonpb.HashType
+		salt []byte
+	}{
+		{
+			name: "SHA256_nil_salt",
+			hash: commonpb.HashType_SHA256,
+		},
+		{
+			name: "SHA256_random_salt",
+			hash: commonpb.HashType_SHA256,
+			salt: random.GetRandomBytes(16),
+		},
+		{
+			name: "SHA512_nil_salt",
+			hash: commonpb.HashType_SHA512,
+		},
+		{
+			name: "SHA512_random_salt",
+			hash: commonpb.HashType_SHA512,
+			salt: random.GetRandomBytes(16),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			key := &hkdfpb.HkdfPrfKey{
+				Version: 0,
+				Params: &hkdfpb.HkdfPrfParams{
+					Hash: test.hash,
+					Salt: test.salt,
+				},
+				KeyValue: random.GetRandomBytes(32),
+			}
+			serializedKey, err := proto.Marshal(key)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", key, err)
+			}
+			p, err := km.Primitive(serializedKey)
+			if err != nil {
+				t.Fatalf("Primitive() err = %v, want nil", err)
+			}
+			prf, ok := p.(streamingprf.StreamingPRF)
+			if !ok {
+				t.Fatal("primitive is not StreamingPRF")
+			}
+			r, err := prf.Compute(random.GetRandomBytes(32))
+			if err != nil {
+				t.Fatalf("Compute() err = %v, want nil", err)
+			}
+			limit := limitFromHash(t, test.hash)
+			out := make([]byte, limit)
+			n, err := r.Read(out)
+			if n != limit || err != nil {
+				t.Errorf("Read() not enough bytes: %d, %v", n, err)
+			}
+		})
+	}
+}
+
+func TestHKDFStreamingPRFKeyManagerPrimitiveRejectsIncorrectKeys(t *testing.T) {
+	km := streamingprf.HKDFStreamingPRFKeyManager{}
+	missingParamsKey := &hkdfpb.HkdfPrfKey{
+		Version:  0,
+		KeyValue: random.GetRandomBytes(32),
+	}
+	serializedMissingParamsKey, err := proto.Marshal(missingParamsKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", serializedMissingParamsKey, err)
+	}
+	aesGCMKey := &aesgcmpb.AesGcmKey{Version: 0}
+	serializedAESGCMKey, err := proto.Marshal(aesGCMKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", aesGCMKey, err)
+	}
+	for _, test := range []struct {
+		name          string
+		serializedKey []byte
+	}{
+		{
+			name: "nil key",
+		},
+		{
+			name:          "zero-length key",
+			serializedKey: []byte{},
+		},
+		{
+			name:          "missing params",
+			serializedKey: serializedMissingParamsKey,
+		},
+		{
+			name:          "wrong key type",
+			serializedKey: serializedAESGCMKey,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := km.Primitive(test.serializedKey); err == nil {
+				t.Error("Primitive() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHKDFStreamingPRFKeyManagerPrimitiveRejectsInvalidKeys(t *testing.T) {
+	km := streamingprf.HKDFStreamingPRFKeyManager{}
+
+	validKey := &hkdfpb.HkdfPrfKey{
+		Version: 0,
+		Params: &hkdfpb.HkdfPrfParams{
+			Hash: commonpb.HashType_SHA256,
+			Salt: random.GetRandomBytes(16),
+		},
+		KeyValue: random.GetRandomBytes(32),
+	}
+	serializedValidKey, err := proto.Marshal(validKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKey, err)
+	}
+	if _, err := km.Primitive(serializedValidKey); err != nil {
+		t.Errorf("Primitive() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name     string
+		version  uint32
+		hash     commonpb.HashType
+		keyValue []byte
+	}{
+		{
+			"invalid version",
+			100,
+			validKey.GetParams().GetHash(),
+			validKey.GetKeyValue(),
+		},
+		{
+			"invalid hash",
+			validKey.GetVersion(),
+			commonpb.HashType_SHA1,
+			validKey.GetKeyValue(),
+		},
+		{
+			"invalid key size",
+			validKey.GetVersion(),
+			validKey.GetParams().GetHash(),
+			random.GetRandomBytes(12),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			key := &hkdfpb.HkdfPrfKey{
+				Version: test.version,
+				Params: &hkdfpb.HkdfPrfParams{
+					Hash: test.hash,
+					// There is no concept of an invalid salt, as it can either be nil or
+					// have a value.
+					Salt: validKey.GetParams().GetSalt(),
+				},
+				KeyValue: test.keyValue,
+			}
+			serializedKey, err := proto.Marshal(key)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", key, err)
+			}
+			if _, err := km.Primitive(serializedKey); err == nil {
+				t.Error("Primitive() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHKDFStreamingPRFKeyManagerNewKeyAndNewKeyData(t *testing.T) {
+	km := streamingprf.HKDFStreamingPRFKeyManager{}
+	notImplemented := "not implemented"
+	if _, err := km.NewKey(random.GetRandomBytes(16)); !strings.Contains(err.Error(), notImplemented) {
+		t.Errorf("NewKey() err = %v, want containing %q", err, notImplemented)
+	}
+	if _, err := km.NewKeyData(random.GetRandomBytes(16)); !strings.Contains(err.Error(), notImplemented) {
+		t.Errorf("NewKey() err = %v, want containing %q", err, notImplemented)
+	}
+}
+
+func TestHKDFStreamingPRFKeyManagerDoesSupport(t *testing.T) {
+	km := streamingprf.HKDFStreamingPRFKeyManager{}
+	if !km.DoesSupport(hkdfPRFTypeURL) {
+		t.Errorf("DoesSupport(%q) = false, want true", hkdfPRFTypeURL)
+	}
+	if unsupported := "unsupported.key.type"; km.DoesSupport(unsupported) {
+		t.Errorf("DoesSupport(%q) = true, want false", unsupported)
+	}
+}
+
+func TestHKDFStreamingPRFKeyManagerTypeURL(t *testing.T) {
+	km := streamingprf.HKDFStreamingPRFKeyManager{}
+	if km.TypeURL() != hkdfPRFTypeURL {
+		t.Errorf("TypeURL() = %q, want %q", km.TypeURL(), hkdfPRFTypeURL)
+	}
+}
diff --git a/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_test.go b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_test.go
new file mode 100644
index 0000000..c9b90d1
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/hkdf_streaming_prf_test.go
@@ -0,0 +1,231 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf
+
+import (
+	"bytes"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"strings"
+	"testing"
+
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestNewHKDFStreamingPRF(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		hash string
+		salt []byte
+	}{
+		{
+			name: "SHA256_nil_salt",
+			hash: "SHA256",
+		},
+		{
+			name: "SHA256_random_salt",
+			hash: "SHA256",
+			salt: random.GetRandomBytes(16),
+		},
+		{
+			name: "SHA512_nil_salt",
+			hash: "SHA512",
+		},
+		{
+			name: "SHA512_random_salt",
+			hash: "SHA512",
+			salt: random.GetRandomBytes(16),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			key := random.GetRandomBytes(32)
+			h, err := newHKDFStreamingPRF(test.hash, key, test.salt)
+			if err != nil {
+				t.Fatalf("newHKDFStreamingPRF() err = %v, want nil", err)
+			}
+			if !bytes.Equal(h.key, key) {
+				t.Errorf("key = %v, want %v", h.key, key)
+			}
+			if !bytes.Equal(h.salt, test.salt) {
+				t.Errorf("salt = %v, want %v", h.salt, test.salt)
+			}
+		})
+	}
+}
+
+func TestNewHKDFStreamingPRFFails(t *testing.T) {
+	for _, test := range []struct {
+		hash    string
+		keySize uint32
+	}{
+		{
+			hash:    "SHA256",
+			keySize: 16,
+		},
+		{
+			hash:    "SHA512",
+			keySize: 16},
+		{
+			hash:    "SHA1",
+			keySize: 20,
+		},
+	} {
+		t.Run(test.hash, func(t *testing.T) {
+			if _, err := newHKDFStreamingPRF(test.hash, random.GetRandomBytes(test.keySize), nil); err == nil {
+				t.Error("newHKDFStreamingPRF() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHKDFStreamingPRFWithRFCVector(t *testing.T) {
+	// This is the only vector that uses an accepted hash function and has key
+	// size >= minHKDFStreamingPRFKeySize.
+	// https://www.rfc-editor.org/rfc/rfc5869#appendix-A.2
+	vec := struct {
+		hash   string
+		key    string
+		salt   string
+		info   string
+		outLen int
+		okm    string
+	}{
+		hash:   "SHA256",
+		key:    "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f",
+		salt:   "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+		info:   "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+		outLen: 82,
+		okm:    "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87",
+	}
+	key, err := hex.DecodeString(vec.key)
+	if err != nil {
+		t.Fatalf("hex.DecodeString err = %v, want nil", err)
+	}
+	salt, err := hex.DecodeString(vec.salt)
+	if err != nil {
+		t.Fatalf("hex.DecodeString err = %v, want nil", err)
+	}
+	info, err := hex.DecodeString(vec.info)
+	if err != nil {
+		t.Fatalf("hex.DecodeString err = %v, want nil", err)
+	}
+
+	h, err := newHKDFStreamingPRF(vec.hash, key, salt)
+	if err != nil {
+		t.Fatalf("newHKDFStreamingPRF() err = %v, want nil", err)
+	}
+	r, err := h.Compute(info)
+	if err != nil {
+		t.Fatalf("Compute() err = %v, want nil", err)
+	}
+	out := make([]byte, vec.outLen)
+	if _, err := io.ReadAtLeast(r, out, len(out)); err != nil {
+		t.Fatalf("io.ReadAtLeast err = %v, want nil", err)
+	}
+	if hex.EncodeToString(out) != vec.okm {
+		t.Errorf("Compute() = %v, want %v", hex.EncodeToString(out), vec.okm)
+	}
+}
+
+func TestHKDFStreamingPRFWithWycheproof(t *testing.T) {
+	testutil.SkipTestIfTestSrcDirIsNotSet(t)
+
+	type hkdfCase struct {
+		testutil.WycheproofCase
+		IKM  testutil.HexBytes `json:"ikm"`
+		Salt testutil.HexBytes `json:"salt"`
+		Info testutil.HexBytes `json:"info"`
+		Size uint32            `json:"size"`
+		OKM  testutil.HexBytes `json:"okm"`
+	}
+	type hkdfGroup struct {
+		testutil.WycheproofGroup
+		KeySize uint32      `json:"keySize"`
+		Type    string      `json:"type"`
+		Tests   []*hkdfCase `json:"tests"`
+	}
+	type hkdfSuite struct {
+		testutil.WycheproofSuite
+		TestGroups []*hkdfGroup `json:"testGroups"`
+	}
+
+	count := 0
+	for _, hash := range []string{"SHA256", "SHA512"} {
+		filename := fmt.Sprintf("hkdf_%s_test.json", strings.ToLower(hash))
+		suite := new(hkdfSuite)
+		if err := testutil.PopulateSuite(suite, filename); err != nil {
+			t.Fatalf("testutil.PopulateSuite(%v, %s): %v", suite, filename, err)
+		}
+		for _, group := range suite.TestGroups {
+			for _, test := range group.Tests {
+				caseName := fmt.Sprintf("%s(%d):Case-%d", hash, group.KeySize, test.CaseID)
+				t.Run(caseName, func(t *testing.T) {
+					if got, want := len(test.IKM), int(group.KeySize/8); got != want {
+						t.Fatalf("invalid key length = %d, want %d", got, want)
+					}
+					count++
+
+					h, err := newHKDFStreamingPRF(hash, test.IKM, test.Salt)
+					switch test.Result {
+					case "valid":
+						if len(test.IKM) < minHKDFStreamingPRFKeySize {
+							if err == nil {
+								t.Error("newHKDFStreamingPRF err = nil, want non-nil")
+							}
+							return
+						}
+						if err != nil {
+							t.Fatalf("newHKDFStreamingPRF err = %v, want nil", err)
+						}
+						r, err := h.Compute(test.Info)
+						if err != nil {
+							t.Fatalf("Compute() err = %v, want nil", err)
+						}
+						out := make([]byte, test.Size)
+						if _, err := io.ReadAtLeast(r, out, len(out)); err != nil {
+							t.Fatalf("io.ReadAtLeast err = %v, want nil", err)
+						}
+						if !bytes.Equal(out, test.OKM) {
+							t.Errorf("Compute() = %v, want %v", out, test.OKM)
+						}
+
+					case "invalid":
+						if err != nil {
+							return
+						}
+						r, err := h.Compute(test.Info)
+						if err != nil {
+							t.Fatalf("Compute() err = %v, want nil", err)
+						}
+						out := make([]byte, test.Size)
+						if _, err := io.ReadAtLeast(r, out, len(out)); err == nil {
+							t.Error("io.ReadAtLeast err = nil, want non-nil")
+						}
+
+					default:
+						t.Errorf("unsupported test result: %s", test.Result)
+					}
+				})
+			}
+		}
+	}
+	if count < 200 {
+		t.Errorf("number of test cases = %d, want > 200", count)
+	}
+}
diff --git a/go/keyderivation/internal/streamingprf/streaming_prf.go b/go/keyderivation/internal/streamingprf/streaming_prf.go
new file mode 100644
index 0000000..8086f60
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/streaming_prf.go
@@ -0,0 +1,33 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package streamingprf provides implementations of streaming pseudorandom
+// function families.
+package streamingprf
+
+import (
+	"io"
+)
+
+// StreamingPRF is the interface used to represent a streaming pseudorandom
+// function family for a specified key.
+//
+// It has the same properties as the PRF primitive.
+type StreamingPRF interface {
+	// Compute computes the PRF selected by the specified key on input and returns
+	// the result via a reader.
+	Compute(input []byte) (io.Reader, error)
+}
diff --git a/go/keyderivation/internal/streamingprf/streaming_prf_factory.go b/go/keyderivation/internal/streamingprf/streaming_prf_factory.go
new file mode 100644
index 0000000..8362b07
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/streaming_prf_factory.go
@@ -0,0 +1,76 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf
+
+import (
+	"errors"
+	"fmt"
+	"io"
+
+	"github.com/google/tink/go/core/primitiveset"
+	"github.com/google/tink/go/keyset"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// New generates a new instance of the Streaming PRF primitive.
+func New(h *keyset.Handle) (StreamingPRF, error) {
+	if h == nil {
+		return nil, errors.New("keyset handle can't be nil")
+	}
+	ps, err := h.PrimitivesWithKeyManager(new(HKDFStreamingPRFKeyManager))
+	if err != nil {
+		return nil, fmt.Errorf("streaming_prf_factory: cannot obtain primitive set: %v", err)
+	}
+	return newWrappedStreamingPRF(ps)
+}
+
+// wrappedStreamingPRF is a Streaming PRF implementation that uses the underlying primitive set for Streaming PRF.
+type wrappedStreamingPRF struct {
+	ps *primitiveset.PrimitiveSet
+}
+
+// Asserts that wrappedStreamingPRF implements the StreamingPRF interface.
+var _ StreamingPRF = (*wrappedStreamingPRF)(nil)
+
+func newWrappedStreamingPRF(ps *primitiveset.PrimitiveSet) (*wrappedStreamingPRF, error) {
+	if rawEntries, err := ps.RawEntries(); err != nil || len(rawEntries) != 1 {
+		return nil, errors.New("streaming_prf_factory: only accepts keysets with 1 RAW key")
+	}
+	// ps.Entries is a map of prefix type -> []*Entry.
+	if len(ps.Entries) != 1 {
+		return nil, errors.New("streaming_prf_factory: only accepts keys with prefix type RAW")
+	}
+	if _, ok := (ps.Primary.Primitive).(StreamingPRF); !ok {
+		return nil, errors.New("streaming_prf_factory: not a Streaming PRF primitive")
+	}
+	if ps.Primary.PrefixType != tinkpb.OutputPrefixType_RAW {
+		return nil, errors.New("streaming_prf_factory: primary key prefix type is not RAW")
+	}
+	if ps.Primary.Status != tinkpb.KeyStatusType_ENABLED {
+		return nil, errors.New("streaming_prf_factory: primary key is not ENABLED")
+	}
+	return &wrappedStreamingPRF{ps: ps}, nil
+}
+
+func (w *wrappedStreamingPRF) Compute(input []byte) (io.Reader, error) {
+	primary := w.ps.Primary
+	p, ok := (primary.Primitive).(StreamingPRF)
+	if !ok {
+		return nil, errors.New("streaming_prf_factory: not a Streaming PRF primitive")
+	}
+	return p.Compute(input)
+}
diff --git a/go/keyderivation/internal/streamingprf/streaming_prf_factory_test.go b/go/keyderivation/internal/streamingprf/streaming_prf_factory_test.go
new file mode 100644
index 0000000..20b932f
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/streaming_prf_factory_test.go
@@ -0,0 +1,299 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf_test
+
+import (
+	"bytes"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyderivation/internal/streamingprf"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/prf"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testkeyset"
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
+	hkdfpb "github.com/google/tink/go/proto/hkdf_prf_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func TestNew(t *testing.T) {
+	keyData, err := registry.NewKeyData(prf.HKDFSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v", err)
+	}
+	ks := &tinkpb.Keyset{
+		PrimaryKeyId: 119,
+		Key: []*tinkpb.Keyset_Key{
+			&tinkpb.Keyset_Key{
+				KeyData:          keyData,
+				Status:           tinkpb.KeyStatusType_ENABLED,
+				KeyId:            119,
+				OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+			},
+		},
+	}
+	handle, err := testkeyset.NewHandle(ks)
+	if err != nil {
+		t.Fatalf("testkeyset.NewHandle(ks) err = %v, want nil", err)
+	}
+	prf, err := streamingprf.New(handle)
+	if err != nil {
+		t.Fatalf("streamingprf.New() err = %v, want nil", err)
+	}
+	r, err := prf.Compute(random.GetRandomBytes(32))
+	if err != nil {
+		t.Fatalf("prf.Compute() err = %v, want nil", err)
+	}
+	limit := limitFromHash(t, commonpb.HashType_SHA256)
+	out := make([]byte, limit)
+	n, err := r.Read(out)
+	if n != limit || err != nil {
+		t.Errorf("Read() bytes = %d, want %d: %v", n, limit, err)
+	}
+}
+
+func TestNewEqualToStreamingPRFPrimitive(t *testing.T) {
+	streamingPRFKM := streamingprf.HKDFStreamingPRFKeyManager{}
+	prfKM, err := registry.GetKeyManager(hkdfPRFTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%s) err = %v, want nil", hkdfPRFTypeURL, err)
+	}
+	for _, test := range []struct {
+		name string
+		hash commonpb.HashType
+		salt []byte
+	}{
+		{
+			name: "SHA256_nil_salt",
+			hash: commonpb.HashType_SHA256,
+		},
+		{
+			name: "SHA256_random_salt",
+			hash: commonpb.HashType_SHA256,
+			salt: random.GetRandomBytes(16),
+		},
+		{
+			name: "SHA512_nil_salt",
+			hash: commonpb.HashType_SHA512,
+		},
+		{
+			name: "SHA512_random_salt",
+			hash: commonpb.HashType_SHA512,
+			salt: random.GetRandomBytes(16),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			// Construct shared key data.
+			keyFormat := &hkdfpb.HkdfPrfKeyFormat{
+				Params: &hkdfpb.HkdfPrfParams{
+					Hash: test.hash,
+					Salt: test.salt,
+				},
+				KeySize: 32,
+				Version: 0,
+			}
+			serializedKeyFormat, err := proto.Marshal(keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+			}
+			sharedKeyData, err := prfKM.NewKeyData(serializedKeyFormat)
+			if err != nil {
+				t.Fatalf("NewKeyData() err = %v, want nil", err)
+			}
+
+			// Use shared key data to create StreamingPRF using New().
+			var primaryKeyID uint32 = 12
+			handle, err := testkeyset.NewHandle(
+				&tinkpb.Keyset{
+					PrimaryKeyId: primaryKeyID,
+					Key: []*tinkpb.Keyset_Key{
+						&tinkpb.Keyset_Key{
+							KeyData:          sharedKeyData,
+							Status:           tinkpb.KeyStatusType_ENABLED,
+							KeyId:            primaryKeyID,
+							OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+						},
+					},
+				})
+			if err != nil {
+				t.Fatalf("testkeyset.NewHandle() err = %v, want nil", err)
+			}
+			gotPRF, err := streamingprf.New(handle)
+			if err != nil {
+				t.Fatalf("streamingprf.New() err = %v, want nil", err)
+			}
+
+			// Use shared key data to create StreamingPRF using Primitive().
+			p, err := streamingPRFKM.Primitive(sharedKeyData.GetValue())
+			if err != nil {
+				t.Fatalf("Primitive() err = %v, want nil", err)
+			}
+			wantPRF, ok := p.(streamingprf.StreamingPRF)
+			if !ok {
+				t.Fatal("primitive is not StreamingPRF")
+			}
+
+			// Verify both PRFs return the same results.
+			limit := limitFromHash(t, test.hash)
+			got, want := make([]byte, limit), make([]byte, limit)
+			data := random.GetRandomBytes(32)
+			{
+				r, err := gotPRF.Compute(data)
+				if err != nil {
+					t.Fatalf("Compute() err = %v, want nil", err)
+				}
+				n, err := r.Read(got)
+				if n != limit || err != nil {
+					t.Fatalf("Read() bytes = %d, want %d: %v", n, limit, err)
+				}
+			}
+			{
+				r, err := wantPRF.Compute(data)
+				if err != nil {
+					t.Fatalf("Compute() err = %v, want nil", err)
+				}
+				n, err := r.Read(want)
+				if n != limit || err != nil {
+					t.Fatalf("Read() bytes = %d, want %d: %v", n, limit, err)
+				}
+			}
+			if !bytes.Equal(got, want) {
+				t.Errorf("Read() = %v, want %v", got, want)
+			}
+		})
+	}
+}
+
+func TestNewRejectsIncorrectKeysetHandle(t *testing.T) {
+	if _, err := streamingprf.New(nil); err == nil {
+		t.Error("streamingprf.New() err = nil, want non-nil")
+	}
+
+	aeadHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	if _, err := streamingprf.New(aeadHandle); err == nil {
+		t.Error("streamingprf.New() err = nil, want non-nil")
+	}
+}
+
+func TestNewRejectsInvalidKeysetHandle(t *testing.T) {
+	keyData, err := registry.NewKeyData(prf.HKDFSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v", err)
+	}
+	for _, test := range []struct {
+		name   string
+		keyset *tinkpb.Keyset
+	}{
+		{
+			"multiple raw keys",
+			&tinkpb.Keyset{
+				PrimaryKeyId: 119,
+				Key: []*tinkpb.Keyset_Key{
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_ENABLED,
+						KeyId:            119,
+						OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+					},
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_ENABLED,
+						KeyId:            200,
+						OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+					},
+				},
+			},
+		},
+		{
+			"various output prefix keys",
+			&tinkpb.Keyset{
+				PrimaryKeyId: 119,
+				Key: []*tinkpb.Keyset_Key{
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_ENABLED,
+						KeyId:            119,
+						OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+					},
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_ENABLED,
+						KeyId:            200,
+						OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+					},
+				},
+			},
+		},
+		{
+			"invalid prefix type",
+			&tinkpb.Keyset{
+				PrimaryKeyId: 119,
+				Key: []*tinkpb.Keyset_Key{
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_ENABLED,
+						KeyId:            119,
+						OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+					},
+				},
+			}},
+		{
+			"invalid status",
+			&tinkpb.Keyset{
+				PrimaryKeyId: 119,
+				Key: []*tinkpb.Keyset_Key{
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_UNKNOWN_STATUS,
+						KeyId:            119,
+						OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+					},
+				},
+			},
+		},
+		{
+			"no primary key",
+			&tinkpb.Keyset{
+				PrimaryKeyId: 200,
+				Key: []*tinkpb.Keyset_Key{
+					&tinkpb.Keyset_Key{
+						KeyData:          keyData,
+						Status:           tinkpb.KeyStatusType_UNKNOWN_STATUS,
+						KeyId:            119,
+						OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+					},
+				},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			handle, err := testkeyset.NewHandle(test.keyset)
+			if err != nil {
+				t.Fatalf("testkeyset.NewHandle(test.keyset) err = %v, want nil", err)
+			}
+			if _, err := streamingprf.New(handle); err == nil {
+				t.Error("streamingprf.New() err = nil, want non-nil")
+			}
+		})
+	}
+}
diff --git a/go/keyderivation/internal/streamingprf/streaming_prf_test.go b/go/keyderivation/internal/streamingprf/streaming_prf_test.go
new file mode 100644
index 0000000..6b2dfff
--- /dev/null
+++ b/go/keyderivation/internal/streamingprf/streaming_prf_test.go
@@ -0,0 +1,41 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package streamingprf_test
+
+import (
+	"crypto/sha256"
+	"crypto/sha512"
+	"testing"
+
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
+)
+
+const hkdfPRFTypeURL = "type.googleapis.com/google.crypto.tink.HkdfPrfKey"
+
+// limitFromHash returns the maximum output bytes from a HKDF using hash.
+func limitFromHash(t *testing.T, hash commonpb.HashType) (limit int) {
+	t.Helper()
+	switch hash {
+	case commonpb.HashType_SHA256:
+		limit = sha256.Size * 255
+	case commonpb.HashType_SHA512:
+		limit = sha512.Size * 255
+	default:
+		t.Fatalf("unsupported hash type: %s", hash.String())
+	}
+	return
+}
diff --git a/go/keyderivation/keyderivation.go b/go/keyderivation/keyderivation.go
new file mode 100644
index 0000000..318fb39
--- /dev/null
+++ b/go/keyderivation/keyderivation.go
@@ -0,0 +1,38 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package keyderivation provides implementations of the keyset deriver
+// primitive.
+package keyderivation
+
+import (
+	"fmt"
+
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal"
+	"github.com/google/tink/go/keyset"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+var (
+	keysetHandle = internal.KeysetHandle.(func(*tinkpb.Keyset, ...keyset.Option) (*keyset.Handle, error))
+)
+
+func init() {
+	if err := registry.RegisterKeyManager(new(prfBasedDeriverKeyManager)); err != nil {
+		panic(fmt.Sprintf("keyderivation.init() failed: %v", err))
+	}
+}
diff --git a/go/keyderivation/keyderivation_key_templates.go b/go/keyderivation/keyderivation_key_templates.go
new file mode 100644
index 0000000..2c1b9de
--- /dev/null
+++ b/go/keyderivation/keyderivation_key_templates.go
@@ -0,0 +1,52 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"fmt"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/keyset"
+	prfderpb "github.com/google/tink/go/proto/prf_based_deriver_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// CreatePRFBasedKeyTemplate creates a PRF-Based Deriver key template with the
+// specified PRF and derived key templates. If either the PRF or derived key
+// templates are not supported by the registry, an error is returned.
+func CreatePRFBasedKeyTemplate(prfKeyTemplate, derivedKeyTemplate *tinkpb.KeyTemplate) (*tinkpb.KeyTemplate, error) {
+	keyFormat := &prfderpb.PrfBasedDeriverKeyFormat{
+		PrfKeyTemplate: prfKeyTemplate,
+		Params: &prfderpb.PrfBasedDeriverParams{
+			DerivedKeyTemplate: derivedKeyTemplate,
+		},
+	}
+	serializedFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal key format: %s", err)
+	}
+	template := &tinkpb.KeyTemplate{
+		TypeUrl:          prfBasedDeriverTypeURL,
+		OutputPrefixType: derivedKeyTemplate.GetOutputPrefixType(),
+		Value:            serializedFormat,
+	}
+	// Verify `template` is derivable.
+	if _, err := keyset.NewHandle(template); err != nil {
+		return nil, err
+	}
+	return template, nil
+}
diff --git a/go/keyderivation/keyderivation_key_templates_test.go b/go/keyderivation/keyderivation_key_templates_test.go
new file mode 100644
index 0000000..d88f316
--- /dev/null
+++ b/go/keyderivation/keyderivation_key_templates_test.go
@@ -0,0 +1,152 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation_test
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/keyderivation"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/prf"
+	"github.com/google/tink/go/subtle/random"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func TestPRFBasedKeyTemplateDerivesAESGCMKeyset(t *testing.T) {
+	plaintext := random.GetRandomBytes(16)
+	associatedData := random.GetRandomBytes(8)
+	prfs := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "HKDF-SHA256",
+			template: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+	}
+	derivations := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "AES128GCM",
+			template: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCM",
+			template: aead.AES256GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCMNoPrefix",
+			template: aead.AES256GCMNoPrefixKeyTemplate(),
+		},
+	}
+	for _, prf := range prfs {
+		for _, der := range derivations {
+			for _, salt := range [][]byte{nil, []byte("salt")} {
+				name := fmt.Sprintf("%s_%s", prf.name, der.name)
+				if salt != nil {
+					name += "_with_salt"
+				}
+				t.Run(name, func(t *testing.T) {
+					template, err := keyderivation.CreatePRFBasedKeyTemplate(prf.template, der.template)
+					if err != nil {
+						t.Fatalf("CreatePRFBasedKeyTemplate() err = %v, want nil", err)
+					}
+					handle, err := keyset.NewHandle(template)
+					if err != nil {
+						t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+					}
+					d, err := keyderivation.New(handle)
+					if err != nil {
+						t.Fatalf("keyderivation.New() err = %v, want nil", err)
+					}
+					derivedHandle, err := d.DeriveKeyset(salt)
+					if err != nil {
+						t.Fatalf("DeriveKeyset() err = %v, want nil", err)
+					}
+					a, err := aead.New(derivedHandle)
+					if err != nil {
+						t.Fatalf("aead.New() err = %v, want nil", err)
+					}
+					ciphertext, err := a.Encrypt(plaintext, associatedData)
+					if err != nil {
+						t.Fatalf("Encrypt() err = %v, want nil", err)
+					}
+					gotPlaintext, err := a.Decrypt(ciphertext, associatedData)
+					if err != nil {
+						t.Fatalf("Decrypt() err = %v, want nil", err)
+					}
+					if !bytes.Equal(gotPlaintext, plaintext) {
+						t.Errorf("Decrypt() = %v, want %v", gotPlaintext, plaintext)
+					}
+				})
+			}
+		}
+	}
+}
+
+func TestInvalidPRFBasedDeriverKeyTemplates(t *testing.T) {
+	for _, test := range []struct {
+		name               string
+		prfKeyTemplate     *tinkpb.KeyTemplate
+		derivedKeyTemplate *tinkpb.KeyTemplate
+	}{
+		{
+			name: "nil templates",
+		},
+		{
+			name:               "nil PRF key template",
+			derivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:           "nil derived key template",
+			prfKeyTemplate: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+		{
+			name:               "malformed PRF key template",
+			prfKeyTemplate:     &tinkpb.KeyTemplate{TypeUrl: "\xff"},
+			derivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+		// AES128CTRHMACSHA256KeyTemplate() is an unsupported derived key template
+		// because DeriveKey() is not implemented in the AES-CTR-HMAC key manager.
+		// TODO(b/227682336): Add mock key manager that doesn't derive keys.
+		{
+			name:               "unsupported templates",
+			prfKeyTemplate:     aead.AES128GCMKeyTemplate(),
+			derivedKeyTemplate: aead.AES128CTRHMACSHA256KeyTemplate()},
+		{
+			name:               "unsupported PRF key template",
+			prfKeyTemplate:     aead.AES128GCMKeyTemplate(),
+			derivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:               "unsupported derived key template",
+			prfKeyTemplate:     prf.HKDFSHA256PRFKeyTemplate(),
+			derivedKeyTemplate: aead.AES128CTRHMACSHA256KeyTemplate(),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := keyderivation.CreatePRFBasedKeyTemplate(test.prfKeyTemplate, test.derivedKeyTemplate); err == nil {
+				t.Error("CreatePRFBasedKeyTemplate() err = nil, want non-nil")
+			}
+		})
+	}
+}
diff --git a/go/keyderivation/keyderivation_test.go b/go/keyderivation/keyderivation_test.go
new file mode 100644
index 0000000..bec797b
--- /dev/null
+++ b/go/keyderivation/keyderivation_test.go
@@ -0,0 +1,68 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation_test
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/keyderivation"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/prf"
+)
+
+func Example() {
+	template, err := keyderivation.CreatePRFBasedKeyTemplate(prf.HKDFSHA256PRFKeyTemplate(), aead.AES128GCMKeyTemplate())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	handle, err := keyset.NewHandle(template)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	deriver, err := keyderivation.New(handle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	derivedHandle, err := deriver.DeriveKeyset([]byte("salt"))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Use the derived keyset.
+	a, err := aead.New(derivedHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	ciphertext, err := a.Encrypt([]byte("a secret message"), nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	plaintext, err := a.Decrypt(ciphertext, nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Println(string(plaintext))
+	// Output: a secret message
+}
diff --git a/go/keyderivation/keyset_deriver.go b/go/keyderivation/keyset_deriver.go
new file mode 100644
index 0000000..9d6dced
--- /dev/null
+++ b/go/keyderivation/keyset_deriver.go
@@ -0,0 +1,31 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"github.com/google/tink/go/keyset"
+)
+
+// KeysetDeriver is the interface used to derive new keysets based on an
+// additional input, the salt.
+//
+// The salt is used to create the keyset using a pseudorandom function.
+// Implementations must be indistinguishable from ideal KeysetDerivers, which,
+// for every salt, generates a new random keyset and caches it.
+type KeysetDeriver interface {
+	DeriveKeyset(salt []byte) (*keyset.Handle, error)
+}
diff --git a/go/keyderivation/keyset_deriver_factory.go b/go/keyderivation/keyset_deriver_factory.go
new file mode 100644
index 0000000..a693e75
--- /dev/null
+++ b/go/keyderivation/keyset_deriver_factory.go
@@ -0,0 +1,95 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/google/tink/go/core/primitiveset"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/keyset"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+var errNotKeysetDeriverPrimitive = errors.New("keyset_deriver_factory: not a Keyset Deriver primitive")
+
+// New generates a new instance of the Keyset Deriver primitive.
+func New(handle *keyset.Handle) (KeysetDeriver, error) {
+	if handle == nil {
+		return nil, errors.New("keyset_deriver_factory: keyset handle can't be nil")
+	}
+	ps, err := handle.PrimitivesWithKeyManager(nil)
+	if err != nil {
+		return nil, fmt.Errorf("keyset_deriver_factory: cannot obtain primitive set: %v", err)
+	}
+	return newWrappedKeysetDeriver(ps)
+}
+
+// wrappedKeysetDeriver is a Keyset Deriver implementation that uses the underlying primitive set to derive keysets.
+type wrappedKeysetDeriver struct {
+	ps *primitiveset.PrimitiveSet
+}
+
+// Asserts that wrappedKeysetDeriver implements the KeysetDeriver interface.
+var _ KeysetDeriver = (*wrappedKeysetDeriver)(nil)
+
+func newWrappedKeysetDeriver(ps *primitiveset.PrimitiveSet) (*wrappedKeysetDeriver, error) {
+	if _, ok := (ps.Primary.Primitive).(KeysetDeriver); !ok {
+		return nil, errNotKeysetDeriverPrimitive
+	}
+	for _, p := range ps.EntriesInKeysetOrder {
+		if _, ok := (p.Primitive).(KeysetDeriver); !ok {
+			return nil, errNotKeysetDeriverPrimitive
+		}
+	}
+	return &wrappedKeysetDeriver{ps: ps}, nil
+}
+
+func (w *wrappedKeysetDeriver) DeriveKeyset(salt []byte) (*keyset.Handle, error) {
+	keys := make([]*tinkpb.Keyset_Key, 0, len(w.ps.EntriesInKeysetOrder))
+	for _, e := range w.ps.EntriesInKeysetOrder {
+		p, ok := (e.Primitive).(KeysetDeriver)
+		if !ok {
+			return nil, errNotKeysetDeriverPrimitive
+		}
+		handle, err := p.DeriveKeyset(salt)
+		if err != nil {
+			return nil, errors.New("keyset_deriver_factory: keyset derivation failed")
+		}
+		if len(handle.KeysetInfo().GetKeyInfo()) != 1 {
+			return nil, errors.New("keyset_deriver_factory: primitive must derive keyset handle with exactly one key")
+		}
+		ks := insecurecleartextkeyset.KeysetMaterial(handle)
+		if len(ks.GetKey()) != 1 {
+			return nil, errors.New("keyset_deriver_factory: primitive must derive keyset handle with exactly one key")
+		}
+		// Set all fields, except for KeyData, to match the Entry's in the keyset.
+		key := &tinkpb.Keyset_Key{
+			KeyData:          ks.GetKey()[0].GetKeyData(),
+			Status:           e.Status,
+			KeyId:            e.KeyID,
+			OutputPrefixType: e.PrefixType,
+		}
+		keys = append(keys, key)
+	}
+	ks := &tinkpb.Keyset{
+		PrimaryKeyId: w.ps.Primary.KeyID,
+		Key:          keys,
+	}
+	return keysetHandle(ks)
+}
diff --git a/go/keyderivation/keyset_deriver_factory_test.go b/go/keyderivation/keyset_deriver_factory_test.go
new file mode 100644
index 0000000..b833ef0
--- /dev/null
+++ b/go/keyderivation/keyset_deriver_factory_test.go
@@ -0,0 +1,88 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/cryptofmt"
+	"github.com/google/tink/go/core/primitiveset"
+	"github.com/google/tink/go/keyset"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// invalidDeriver returns two keys, but wrappedKeysetDeriver accepts only one.
+type invalidDeriver struct{}
+
+var _ KeysetDeriver = (*invalidDeriver)(nil)
+
+func (i *invalidDeriver) DeriveKeyset(salt []byte) (*keyset.Handle, error) {
+	manager := keyset.NewManager()
+	keyID, err := manager.Add(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		return nil, err
+	}
+	manager.SetPrimary(keyID)
+	if _, err = manager.Add(aead.AES256GCMKeyTemplate()); err != nil {
+		return nil, err
+	}
+	return manager.Handle()
+}
+
+func TestDeriveKeysetWithInvalidPrimitiveImplementationFails(t *testing.T) {
+	entry := &primitiveset.Entry{
+		KeyID:     119,
+		Primitive: &invalidDeriver{},
+		Prefix:    cryptofmt.RawPrefix,
+		Status:    tinkpb.KeyStatusType_ENABLED,
+		TypeURL:   "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey",
+	}
+	ps := &primitiveset.PrimitiveSet{
+		Primary: entry,
+		Entries: map[string][]*primitiveset.Entry{
+			cryptofmt.RawPrefix: []*primitiveset.Entry{entry},
+		},
+		EntriesInKeysetOrder: []*primitiveset.Entry{entry},
+	}
+	wrappedDeriver, err := newWrappedKeysetDeriver(ps)
+	if err != nil {
+		t.Fatalf("newWrappedKeysetDeriver() err = %v, want nil", err)
+	}
+	_, err = wrappedDeriver.DeriveKeyset([]byte("salt"))
+	if err == nil {
+		t.Fatal("DeriveKeyset() err = nil, want non-nil")
+	}
+	if !strings.Contains(err.Error(), "exactly one key") {
+		t.Errorf("DeriveKeyset() err = %q, doesn't contain %q", err, "exactly one key")
+	}
+}
+
+func TestNewWrappedKeysetDeriverWrongPrimitiveFails(t *testing.T) {
+	handle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	ps, err := handle.Primitives()
+	if err != nil {
+		t.Fatalf("handle.Primitives() err = %v, want nil", err)
+	}
+	if _, err := newWrappedKeysetDeriver(ps); err == nil {
+		t.Errorf("newWrappedKeysetDeriver() err = nil, want non-nil")
+	}
+}
diff --git a/go/keyderivation/keyset_deriver_factory_x_test.go b/go/keyderivation/keyset_deriver_factory_x_test.go
new file mode 100644
index 0000000..c403a33
--- /dev/null
+++ b/go/keyderivation/keyset_deriver_factory_x_test.go
@@ -0,0 +1,209 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation_test
+
+import (
+	"bytes"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/keyderivation"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/prf"
+	"github.com/google/tink/go/subtle/random"
+	prfderpb "github.com/google/tink/go/proto/prf_based_deriver_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func TestWrappedKeysetDeriver(t *testing.T) {
+	// Construct deriving keyset handle containing one key.
+	aes128GCMKeyFormat, err := proto.Marshal(&prfderpb.PrfBasedDeriverKeyFormat{
+		PrfKeyTemplate: prf.HKDFSHA256PRFKeyTemplate(),
+		Params: &prfderpb.PrfBasedDeriverParams{
+			DerivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal(aes128GCMKeyFormat) err = %v, want nil", err)
+	}
+	singleKeyHandle, err := keyset.NewHandle(&tinkpb.KeyTemplate{
+		TypeUrl:          prfBasedDeriverTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+		Value:            aes128GCMKeyFormat,
+	})
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+
+	// Construct deriving keyset handle containing three keys.
+	xChaChaKeyFormat, err := proto.Marshal(&prfderpb.PrfBasedDeriverKeyFormat{
+		PrfKeyTemplate: prf.HKDFSHA256PRFKeyTemplate(),
+		Params: &prfderpb.PrfBasedDeriverParams{
+			DerivedKeyTemplate: aead.XChaCha20Poly1305KeyTemplate(),
+		},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal(xChaChaKeyFormat) err = %v, want nil", err)
+	}
+	aes256GCMKeyFormat, err := proto.Marshal(&prfderpb.PrfBasedDeriverKeyFormat{
+		PrfKeyTemplate: prf.HKDFSHA256PRFKeyTemplate(),
+		Params: &prfderpb.PrfBasedDeriverParams{
+			DerivedKeyTemplate: aead.AES256GCMKeyTemplate(),
+		},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal(aes256GCMKeyFormat) err = %v, want nil", err)
+	}
+	manager := keyset.NewManager()
+	aes128GCMKeyID, err := manager.Add(&tinkpb.KeyTemplate{
+		TypeUrl:          prfBasedDeriverTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+		Value:            aes128GCMKeyFormat,
+	})
+	if err != nil {
+		t.Fatalf("manager.Add(aes128GCMTemplate) err = %v, want nil", err)
+	}
+	if err := manager.SetPrimary(aes128GCMKeyID); err != nil {
+		t.Fatalf("manager.SetPrimary() err = %v, want nil", err)
+	}
+	if _, err := manager.Add(&tinkpb.KeyTemplate{
+		TypeUrl:          prfBasedDeriverTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+		Value:            xChaChaKeyFormat,
+	}); err != nil {
+		t.Fatalf("manager.Add(xChaChaTemplate) err = %v, want nil", err)
+	}
+	if _, err := manager.Add(&tinkpb.KeyTemplate{
+		TypeUrl:          prfBasedDeriverTypeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_CRUNCHY,
+		Value:            aes256GCMKeyFormat,
+	}); err != nil {
+		t.Fatalf("manager.Add(aes256GCMTemplate) err = %v, want nil", err)
+	}
+	multipleKeysHandle, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle() err = %v, want nil", err)
+	}
+	if got, want := len(multipleKeysHandle.KeysetInfo().GetKeyInfo()), 3; got != want {
+		t.Fatalf("len(multipleKeysHandle) = %d, want %d", got, want)
+	}
+
+	for _, test := range []struct {
+		name         string
+		handle       *keyset.Handle
+		wantTypeURLs []string
+	}{
+		{
+			name:   "single key",
+			handle: singleKeyHandle,
+			wantTypeURLs: []string{
+				"type.googleapis.com/google.crypto.tink.AesGcmKey",
+			},
+		},
+		{
+			name:   "multiple keys",
+			handle: multipleKeysHandle,
+			wantTypeURLs: []string{
+				"type.googleapis.com/google.crypto.tink.AesGcmKey",
+				"type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+				"type.googleapis.com/google.crypto.tink.AesGcmKey",
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			// Derive keyset handle.
+			kd, err := keyderivation.New(test.handle)
+			if err != nil {
+				t.Fatalf("keyderivation.New() err = %v, want nil", err)
+			}
+			derivedHandle, err := kd.DeriveKeyset([]byte("salt"))
+			if err != nil {
+				t.Fatalf("DeriveKeyset() err = %v, want nil", err)
+			}
+
+			// Verify number of derived keys = number of deriving keys.
+			derivedKeyInfo := derivedHandle.KeysetInfo().GetKeyInfo()
+			keyInfo := test.handle.KeysetInfo().GetKeyInfo()
+			if len(derivedKeyInfo) != len(keyInfo) {
+				t.Errorf("number of derived keys = %d, want %d", len(derivedKeyInfo), len(keyInfo))
+			}
+			if len(derivedKeyInfo) != len(test.wantTypeURLs) {
+				t.Errorf("number of derived keys = %d, want %d", len(derivedKeyInfo), len(keyInfo))
+			}
+
+			// Verify derived keys.
+			hasPrimaryKey := false
+			for i, derivedKey := range derivedKeyInfo {
+				derivingKey := keyInfo[i]
+				if got, want := derivedKey.GetOutputPrefixType(), derivingKey.GetOutputPrefixType(); got != want {
+					t.Errorf("GetOutputPrefixType() = %s, want %s", got, want)
+				}
+				if got, want := derivedKey.GetKeyId(), derivingKey.GetKeyId(); got != want {
+					t.Errorf("GetKeyId() = %d, want %d", got, want)
+				}
+				if got, want := derivedKey.GetTypeUrl(), test.wantTypeURLs[i]; got != want {
+					t.Errorf("GetTypeUrl() = %q, want %q", got, want)
+				}
+				if got, want := derivedKey.GetStatus(), derivingKey.GetStatus(); got != want {
+					t.Errorf("GetStatus() = %s, want %s", got, want)
+				}
+				if derivedKey.GetKeyId() == derivedHandle.KeysetInfo().GetPrimaryKeyId() {
+					hasPrimaryKey = true
+				}
+			}
+			if !hasPrimaryKey {
+				t.Errorf("derived keyset has no primary key")
+			}
+
+			// Verify derived keyset handle works for AEAD.
+			pt := random.GetRandomBytes(16)
+			ad := random.GetRandomBytes(4)
+			a, err := aead.New(derivedHandle)
+			if err != nil {
+				t.Fatalf("aead.New() err = %v, want nil", err)
+			}
+			ct, err := a.Encrypt(pt, ad)
+			if err != nil {
+				t.Fatalf("Encrypt() err = %v, want nil", err)
+			}
+			gotPT, err := a.Decrypt(ct, ad)
+			if err != nil {
+				t.Fatalf("Decrypt() err = %v, want nil", err)
+			}
+			if !bytes.Equal(gotPT, pt) {
+				t.Errorf("Decrypt() = %v, want %v", gotPT, pt)
+			}
+		})
+	}
+}
+
+func TestNewRejectsNilKeysetHandle(t *testing.T) {
+	if _, err := keyderivation.New(nil); err == nil {
+		t.Error("keyderivation.New() err = nil, want non-nil")
+	}
+}
+
+func TestNewRejectsIncorrectKey(t *testing.T) {
+	kh, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	if _, err := keyderivation.New(kh); err == nil {
+		t.Error("keyderivation.New() err = nil, want non-nil")
+	}
+}
diff --git a/go/keyderivation/prf_based_deriver.go b/go/keyderivation/prf_based_deriver.go
new file mode 100644
index 0000000..cefc139
--- /dev/null
+++ b/go/keyderivation/prf_based_deriver.go
@@ -0,0 +1,103 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/keyderivation/internal/streamingprf"
+	"github.com/google/tink/go/keyset"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const hkdfPRFTypeURL = "type.googleapis.com/google.crypto.tink.HkdfPrfKey"
+
+// prfBasedDeriver uses prf and the Tink registry to derive a keyset handle as
+// described by derivedKeyTemplate.
+type prfBasedDeriver struct {
+	prf                streamingprf.StreamingPRF
+	derivedKeyTemplate *tinkpb.KeyTemplate
+}
+
+// Asserts that prfBasedDeriver implements the KeysetDeriver interface.
+var _ KeysetDeriver = (*prfBasedDeriver)(nil)
+
+func newPRFBasedDeriver(prfKeyData *tinkpb.KeyData, derivedKeyTemplate *tinkpb.KeyTemplate) (*prfBasedDeriver, error) {
+	// Obtain Streaming PRF from PRF key data.
+	if prfKeyData == nil {
+		return nil, errors.New("PRF key data is nil")
+	}
+	if prfKeyData.GetTypeUrl() != hkdfPRFTypeURL {
+		return nil, fmt.Errorf("PRF key data with type URL %q is not supported", prfKeyData.GetTypeUrl())
+	}
+	// For HKDF PRF keys, create a local instance of the HKDF Streaming PRF key
+	// manager and obtain the Streaming PRF interface through it, instead of
+	// obtaining it through the registry. This allows us to keep the HKDF
+	// Streaming PRF key manager out of the registry for smoother deprecation.
+	//
+	// TODO(b/260619626): Remove this once PRF and Streaming PRF share the same
+	// type URL and registry.Primitive() can return multiple interfaces per
+	// primitive.
+	hkdfStreamingPRFKeyManager := streamingprf.HKDFStreamingPRFKeyManager{}
+	p, err := hkdfStreamingPRFKeyManager.Primitive(prfKeyData.GetValue())
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve StreamingPRF primitive from key manager: %v", err)
+	}
+	prf, ok := p.(streamingprf.StreamingPRF)
+	if !ok {
+		return nil, errors.New("primitive is not StreamingPRF")
+	}
+
+	// Validate derived key template.
+	if !internalregistry.CanDeriveKeys(derivedKeyTemplate.GetTypeUrl()) {
+		return nil, errors.New("derived key template is not a derivable key type")
+	}
+
+	return &prfBasedDeriver{
+		prf:                prf,
+		derivedKeyTemplate: derivedKeyTemplate,
+	}, nil
+}
+
+func (p *prfBasedDeriver) DeriveKeyset(salt []byte) (*keyset.Handle, error) {
+	randomness, err := p.prf.Compute(salt)
+	if err != nil {
+		return nil, fmt.Errorf("compute randomness from PRF failed: %v", err)
+	}
+	keyData, err := internalregistry.DeriveKey(p.derivedKeyTemplate, randomness)
+	if err != nil {
+		return nil, fmt.Errorf("derive key failed: %v", err)
+	}
+	// Fill in placeholder values for key ID, status, and output prefix type.
+	// These will be populated with the correct values in the keyset deriver
+	// factory. This is acceptable because the keyset as-is will never leave Tink,
+	// and the user only interacts via the keyset deriver factory.
+	var primaryKeyID uint32 = 0
+	return keysetHandle(&tinkpb.Keyset{
+		PrimaryKeyId: primaryKeyID,
+		Key: []*tinkpb.Keyset_Key{
+			&tinkpb.Keyset_Key{
+				KeyData:          keyData,
+				Status:           tinkpb.KeyStatusType_UNKNOWN_STATUS,
+				KeyId:            primaryKeyID,
+				OutputPrefixType: tinkpb.OutputPrefixType_UNKNOWN_PREFIX,
+			},
+		},
+	})
+}
diff --git a/go/keyderivation/prf_based_deriver_key_manager.go b/go/keyderivation/prf_based_deriver_key_manager.go
new file mode 100644
index 0000000..c78f72c
--- /dev/null
+++ b/go/keyderivation/prf_based_deriver_key_manager.go
@@ -0,0 +1,106 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"errors"
+	"fmt"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyset"
+	prfderpb "github.com/google/tink/go/proto/prf_based_deriver_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	prfBasedDeriverKeyVersion = 0
+	prfBasedDeriverTypeURL    = "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"
+)
+
+var (
+	errInvalidPRFBasedDeriverKey       = errors.New("prf_based_deriver_key_manager: invalid key")
+	errInvalidPRFBasedDeriverKeyFormat = errors.New("prf_based_deriver_key_manager: invalid key format")
+)
+
+type prfBasedDeriverKeyManager struct{}
+
+var _ registry.KeyManager = (*prfBasedDeriverKeyManager)(nil)
+
+func (km *prfBasedDeriverKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if len(serializedKey) == 0 {
+		return nil, errInvalidPRFBasedDeriverKey
+	}
+	key := &prfderpb.PrfBasedDeriverKey{}
+	if err := proto.Unmarshal(serializedKey, key); err != nil {
+		return nil, errInvalidPRFBasedDeriverKey
+	}
+	if keyset.ValidateKeyVersion(key.GetVersion(), prfBasedDeriverKeyVersion) != nil {
+		return nil, errInvalidPRFBasedDeriverKey
+	}
+	return newPRFBasedDeriver(key.GetPrfKey(), key.GetParams().GetDerivedKeyTemplate())
+}
+
+func (km *prfBasedDeriverKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errInvalidPRFBasedDeriverKeyFormat
+	}
+	keyFormat := &prfderpb.PrfBasedDeriverKeyFormat{}
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidPRFBasedDeriverKeyFormat
+	}
+	if keyFormat.GetParams() == nil {
+		return nil, errors.New("prf_based_deriver_key_manager: nil PRF-Based Deriver params")
+	}
+	prfKey, err := registry.NewKeyData(keyFormat.GetPrfKeyTemplate())
+	if err != nil {
+		return nil, errors.New("prf_based_deriver_key_manager: failed to generate key from PRF key template")
+	}
+	// Validate PRF key data and derived key template.
+	if _, err := newPRFBasedDeriver(prfKey, keyFormat.GetParams().GetDerivedKeyTemplate()); err != nil {
+		return nil, fmt.Errorf("prf_based_deriver_key_manager: %v", err)
+	}
+	return &prfderpb.PrfBasedDeriverKey{
+		Version: prfBasedDeriverKeyVersion,
+		PrfKey:  prfKey,
+		Params:  keyFormat.GetParams(),
+	}, nil
+}
+
+func (km *prfBasedDeriverKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	key, err := km.NewKey(serializedKeyFormat)
+	if err != nil {
+		return nil, err
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, errInvalidPRFBasedDeriverKeyFormat
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         prfBasedDeriverTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+	}, nil
+}
+
+func (km *prfBasedDeriverKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == prfBasedDeriverTypeURL
+}
+
+func (km *prfBasedDeriverKeyManager) TypeURL() string {
+	return prfBasedDeriverTypeURL
+}
diff --git a/go/keyderivation/prf_based_deriver_key_manager_test.go b/go/keyderivation/prf_based_deriver_key_manager_test.go
new file mode 100644
index 0000000..ad44c2a
--- /dev/null
+++ b/go/keyderivation/prf_based_deriver_key_manager_test.go
@@ -0,0 +1,523 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/keyderivation"
+	"github.com/google/tink/go/prf"
+	"github.com/google/tink/go/subtle/random"
+	aesgcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
+	prfderpb "github.com/google/tink/go/proto/prf_based_deriver_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	prfBasedDeriverKeyVersion = 0
+	prfBasedDeriverTypeURL    = "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"
+)
+
+func TestPRFBasedDeriverKeyManagerPrimitive(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	prfs := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "HKDF-SHA256",
+			template: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+	}
+	derivations := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "AES128GCM",
+			template: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCM",
+			template: aead.AES256GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCMNoPrefix",
+			template: aead.AES256GCMNoPrefixKeyTemplate(),
+		},
+	}
+	for _, prf := range prfs {
+		for _, der := range derivations {
+			for _, salt := range [][]byte{nil, []byte("salt")} {
+				name := fmt.Sprintf("%s_%s", prf.name, der.name)
+				if salt != nil {
+					name += "_with_salt"
+				}
+				t.Run(name, func(t *testing.T) {
+					prfKey, err := registry.NewKeyData(prf.template)
+					if err != nil {
+						t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+					}
+					key := &prfderpb.PrfBasedDeriverKey{
+						Version: 0,
+						PrfKey:  prfKey,
+						Params: &prfderpb.PrfBasedDeriverParams{
+							DerivedKeyTemplate: der.template,
+						},
+					}
+					serializedKey, err := proto.Marshal(key)
+					if err != nil {
+						t.Fatalf("proto.Marshal(%v) err = %v, want nil", key, err)
+					}
+					p, err := km.Primitive(serializedKey)
+					if err != nil {
+						t.Fatalf("Primitive() err = %v, want nil", err)
+					}
+					d, ok := p.(keyderivation.KeysetDeriver)
+					if !ok {
+						t.Fatal("primitive is not KeysetDeriver")
+					}
+					if _, err := d.DeriveKeyset(salt); err != nil {
+						t.Fatalf("DeriveKeyset() err = %v, want nil", err)
+					}
+					// We cannot test the derived keyset handle because, at this point, it
+					// is filled with placeholder values for the key ID, status, and
+					// output prefix type fields.
+				})
+			}
+		}
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerPrimitiveRejectsIncorrectKeys(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	prfKey, err := registry.NewKeyData(prf.HKDFSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+	}
+	missingParamsKey := &prfderpb.PrfBasedDeriverKey{
+		Version: prfBasedDeriverKeyVersion,
+		PrfKey:  prfKey,
+	}
+	serializedMissingParamsKey, err := proto.Marshal(missingParamsKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", serializedMissingParamsKey, err)
+	}
+	aesGCMKey := &aesgcmpb.AesGcmKey{Version: 0, KeyValue: random.GetRandomBytes(32)}
+	serializedAESGCMKey, err := proto.Marshal(aesGCMKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", aesGCMKey, err)
+	}
+	for _, test := range []struct {
+		name          string
+		serializedKey []byte
+	}{
+		{
+			name: "nil key",
+		},
+		{
+			name:          "zero-length key",
+			serializedKey: []byte{},
+		},
+		{
+			name:          "missing params",
+			serializedKey: serializedMissingParamsKey,
+		},
+		{
+			name:          "wrong key type",
+			serializedKey: serializedAESGCMKey,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := km.Primitive(test.serializedKey); err == nil {
+				t.Error("Primitive() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerPrimitiveRejectsInvalidKeys(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+
+	validPRFKey, err := registry.NewKeyData(prf.HKDFSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+	}
+	validKey := &prfderpb.PrfBasedDeriverKey{
+		Version: 0,
+		PrfKey:  validPRFKey,
+		Params: &prfderpb.PrfBasedDeriverParams{
+			DerivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+	}
+	serializedValidKey, err := proto.Marshal(validKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKey, err)
+	}
+	if _, err := km.Primitive(serializedValidKey); err != nil {
+		t.Errorf("Primitive() err = %v, want nil", err)
+	}
+
+	invalidPRFKey, err := registry.NewKeyData(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name           string
+		version        uint32
+		prfKey         *tinkpb.KeyData
+		derKeyTemplate *tinkpb.KeyTemplate
+	}{
+		{
+			name:           "invalid version",
+			version:        100,
+			prfKey:         validKey.GetPrfKey(),
+			derKeyTemplate: validKey.GetParams().GetDerivedKeyTemplate(),
+		},
+		{
+			name:           "invalid PRF key",
+			version:        validKey.GetVersion(),
+			prfKey:         invalidPRFKey,
+			derKeyTemplate: validKey.GetParams().GetDerivedKeyTemplate(),
+		},
+		{
+			name:           "invalid derived key template",
+			version:        validKey.GetVersion(),
+			prfKey:         validKey.GetPrfKey(),
+			derKeyTemplate: aead.AES128CTRHMACSHA256KeyTemplate(),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			key := &prfderpb.PrfBasedDeriverKey{
+				Version: test.version,
+				PrfKey:  test.prfKey,
+				Params: &prfderpb.PrfBasedDeriverParams{
+					DerivedKeyTemplate: test.derKeyTemplate,
+				},
+			}
+			serializedKey, err := proto.Marshal(key)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", key, err)
+			}
+			if _, err := km.Primitive(serializedKey); err == nil {
+				t.Error("Primitive() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerNewKey(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	prfs := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "HKDF-SHA256",
+			template: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+	}
+	derivations := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "AES128GCM",
+			template: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCM",
+			template: aead.AES256GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCMNoPrefix",
+			template: aead.AES256GCMNoPrefixKeyTemplate(),
+		},
+	}
+	for _, prf := range prfs {
+		for _, der := range derivations {
+			for _, salt := range [][]byte{nil, []byte("salt")} {
+				name := fmt.Sprintf("%s_%s", prf.name, der.name)
+				if salt != nil {
+					name += "_with_salt"
+				}
+				t.Run(name, func(t *testing.T) {
+					keyFormat := &prfderpb.PrfBasedDeriverKeyFormat{
+						PrfKeyTemplate: prf.template,
+						Params: &prfderpb.PrfBasedDeriverParams{
+							DerivedKeyTemplate: der.template,
+						},
+					}
+					serializedKeyFormat, err := proto.Marshal(keyFormat)
+					if err != nil {
+						t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+					}
+					k, err := km.NewKey(serializedKeyFormat)
+					if err != nil {
+						t.Errorf("NewKey() err = %v, want nil", err)
+					}
+					key, ok := k.(*prfderpb.PrfBasedDeriverKey)
+					if !ok {
+						t.Fatal("key is not PrfBasedDeriverKey")
+					}
+					if key.GetVersion() != prfBasedDeriverKeyVersion {
+						t.Errorf("GetVersion() = %d, want 0", key.GetVersion())
+					}
+					prfKeyData := key.GetPrfKey()
+					if got, want := prfKeyData.GetTypeUrl(), prf.template.GetTypeUrl(); got != want {
+						t.Errorf("GetTypeUrl() = %q, want %q", got, want)
+					}
+					if got, want := prfKeyData.GetKeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+						t.Errorf("GetKeyMaterialType() = %s, want %s", got, want)
+					}
+					if diff := cmp.Diff(key.GetParams().GetDerivedKeyTemplate(), der.template, protocmp.Transform()); diff != "" {
+						t.Errorf("GetDerivedKeyTemplate() diff = %s", diff)
+					}
+				})
+			}
+		}
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerNewKeyData(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	prfs := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "HKDF-SHA256",
+			template: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+	}
+	derivations := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "AES128GCM",
+			template: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCM",
+			template: aead.AES256GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256GCMNoPrefix",
+			template: aead.AES256GCMNoPrefixKeyTemplate(),
+		},
+	}
+	for _, prf := range prfs {
+		for _, der := range derivations {
+			for _, salt := range [][]byte{nil, []byte("salt")} {
+				name := fmt.Sprintf("%s_%s", prf.name, der.name)
+				if salt != nil {
+					name += "_with_salt"
+				}
+				t.Run(name, func(t *testing.T) {
+					keyFormat := &prfderpb.PrfBasedDeriverKeyFormat{
+						PrfKeyTemplate: prf.template,
+						Params: &prfderpb.PrfBasedDeriverParams{
+							DerivedKeyTemplate: der.template,
+						},
+					}
+					serializedKeyFormat, err := proto.Marshal(keyFormat)
+					if err != nil {
+						t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+					}
+					keyData, err := km.NewKeyData(serializedKeyFormat)
+					if err != nil {
+						t.Errorf("NewKeyData() err = %v, want nil", err)
+					}
+					if keyData.GetTypeUrl() != prfBasedDeriverTypeURL {
+						t.Errorf("GetTypeUrl() = %s, want %s", keyData.GetTypeUrl(), prfBasedDeriverTypeURL)
+					}
+					if keyData.GetKeyMaterialType() != tinkpb.KeyData_SYMMETRIC {
+						t.Errorf("GetKeyMaterialType() = %s, want %s", keyData.GetKeyMaterialType(), tinkpb.KeyData_SYMMETRIC)
+					}
+					key := &prfderpb.PrfBasedDeriverKey{}
+					if err := proto.Unmarshal(keyData.GetValue(), key); err != nil {
+						t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+					}
+					if key.GetVersion() != prfBasedDeriverKeyVersion {
+						t.Errorf("GetVersion() = %d, want %d", key.GetVersion(), prfBasedDeriverKeyVersion)
+					}
+					prfKeyData := key.GetPrfKey()
+					if got, want := prfKeyData.GetTypeUrl(), prf.template.GetTypeUrl(); got != want {
+						t.Errorf("GetTypeUrl() = %q, want %q", got, want)
+					}
+					if got, want := prfKeyData.GetKeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+						t.Errorf("GetKeyMaterialType() = %s, want %s", got, want)
+					}
+					if diff := cmp.Diff(key.GetParams().GetDerivedKeyTemplate(), der.template, protocmp.Transform()); diff != "" {
+						t.Errorf("GetDerivedKeyTemplate() diff = %s", diff)
+					}
+				})
+			}
+		}
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerNewKeyAndNewKeyDataRejectsIncorrectKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	missingParamsKeyFormat := &prfderpb.PrfBasedDeriverKeyFormat{
+		PrfKeyTemplate: prf.HKDFSHA256PRFKeyTemplate(),
+	}
+	serializedMissingParamsKeyFormat, err := proto.Marshal(missingParamsKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", missingParamsKeyFormat, err)
+	}
+	aesGCMKeyFormat := &aesgcmpb.AesGcmKeyFormat{KeySize: 32, Version: 0}
+	serializedAESGCMKeyFormat, err := proto.Marshal(aesGCMKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", aesGCMKeyFormat, err)
+	}
+	for _, test := range []struct {
+		name                string
+		serializedKeyFormat []byte
+	}{
+		{
+			name: "nil key",
+		},
+		{
+			name:                "zero-length key",
+			serializedKeyFormat: []byte{},
+		},
+		{
+			name:                "missing params",
+			serializedKeyFormat: serializedMissingParamsKeyFormat,
+		},
+		{
+			name:                "wrong key type",
+			serializedKeyFormat: serializedAESGCMKeyFormat,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := km.NewKey(test.serializedKeyFormat); err == nil {
+				t.Error("NewKey() err = nil, want non-nil")
+			}
+			if _, err := km.NewKeyData(test.serializedKeyFormat); err == nil {
+				t.Error("NewKeyData() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerNewKeyAndNewKeyDataRejectsInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+
+	validKeyFormat := &prfderpb.PrfBasedDeriverKeyFormat{
+		PrfKeyTemplate: prf.HKDFSHA256PRFKeyTemplate(),
+		Params: &prfderpb.PrfBasedDeriverParams{
+			DerivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+	}
+	serializedValidKeyFormat, err := proto.Marshal(validKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKeyFormat, err)
+	}
+	if _, err := km.NewKey(serializedValidKeyFormat); err != nil {
+		t.Errorf("Primitive() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name           string
+		prfKeyTemplate *tinkpb.KeyTemplate
+		derKeyTemplate *tinkpb.KeyTemplate
+	}{
+		{
+			"invalid PRF key template",
+			aead.AES128GCMKeyTemplate(),
+			validKeyFormat.GetParams().GetDerivedKeyTemplate(),
+		},
+		{
+			"invalid derived key template",
+			validKeyFormat.GetPrfKeyTemplate(),
+			aead.AES128CTRHMACSHA256KeyTemplate(),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat := &prfderpb.PrfBasedDeriverKeyFormat{
+				PrfKeyTemplate: test.prfKeyTemplate,
+				Params: &prfderpb.PrfBasedDeriverParams{
+					DerivedKeyTemplate: test.derKeyTemplate,
+				},
+			}
+			serializedKeyFormat, err := proto.Marshal(keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+			}
+			if _, err := km.NewKey(serializedKeyFormat); err == nil {
+				t.Error("NewKey() err = nil, want non-nil")
+			}
+			if _, err := km.NewKeyData(serializedKeyFormat); err == nil {
+				t.Error("NewKeyData() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerDoesSupport(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	if !km.DoesSupport(prfBasedDeriverTypeURL) {
+		t.Errorf("DoesSupport(%q) = false, want true", prfBasedDeriverTypeURL)
+	}
+	if unsupported := "unsupported.key.type"; km.DoesSupport(unsupported) {
+		t.Errorf("DoesSupport(%q) = true, want false", unsupported)
+	}
+}
+
+func TestPRFBasedDeriverKeyManagerTypeURL(t *testing.T) {
+	km, err := registry.GetKeyManager(prfBasedDeriverTypeURL)
+	if err != nil {
+		t.Fatalf("GetKeyManager(%q) err = %v, want nil", prfBasedDeriverTypeURL, err)
+	}
+	if km.TypeURL() != prfBasedDeriverTypeURL {
+		t.Errorf("TypeURL() = %q, want %q", km.TypeURL(), prfBasedDeriverTypeURL)
+	}
+}
diff --git a/go/keyderivation/prf_based_deriver_test.go b/go/keyderivation/prf_based_deriver_test.go
new file mode 100644
index 0000000..24d0dbf
--- /dev/null
+++ b/go/keyderivation/prf_based_deriver_test.go
@@ -0,0 +1,326 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyderivation
+
+import (
+	"bytes"
+	"encoding/hex"
+	"fmt"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/daead"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/prf"
+	"github.com/google/tink/go/signature"
+	"github.com/google/tink/go/streamingaead"
+	aesgcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
+	hkdfpb "github.com/google/tink/go/proto/hkdf_prf_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func TestPRFBasedDeriver(t *testing.T) {
+	prfs := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "HKDF_SHA256",
+			template: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+	}
+	// Derivation names match KEY_TEMPLATE_NAMES in
+	// https://github.com/google/tink/blob/cd96c47ced3f72199832573cdccf18719dc7c73b/testing/cross_language/util/utilities.py.
+	derivations := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "AES128_GCM",
+			template: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256_GCM",
+			template: aead.AES256GCMKeyTemplate(),
+		},
+		{
+			name:     "AES256_GCM_RAW",
+			template: aead.AES256GCMNoPrefixKeyTemplate(),
+		},
+		{
+			name:     "XCHACHA20_POLY1305",
+			template: aead.XChaCha20Poly1305KeyTemplate(),
+		},
+		{
+			name:     "AES256_SIV",
+			template: daead.AESSIVKeyTemplate(),
+		},
+		{
+			name:     "HMAC_SHA256_128BITTAG",
+			template: mac.HMACSHA256Tag128KeyTemplate(),
+		},
+		{
+			name:     "HMAC_SHA256_256BITTAG",
+			template: mac.HMACSHA256Tag256KeyTemplate(),
+		},
+		{
+			name:     "HMAC_SHA512_256BITTAG",
+			template: mac.HMACSHA512Tag256KeyTemplate(),
+		},
+		{
+			name:     "HMAC_SHA512_512BITTAG",
+			template: mac.HMACSHA512Tag512KeyTemplate(),
+		},
+		{
+			name:     "HKDF_SHA256",
+			template: prf.HKDFSHA256PRFKeyTemplate(),
+		},
+		{
+			name:     "HMAC_SHA256_PRF",
+			template: prf.HMACSHA256PRFKeyTemplate(),
+		},
+		{
+			name:     "HMAC_SHA512_PRF",
+			template: prf.HMACSHA512PRFKeyTemplate(),
+		},
+		{
+			name:     "ED25519",
+			template: signature.ED25519KeyTemplate(),
+		},
+		{
+			name:     "AES128_GCM_HKDF_4KB",
+			template: streamingaead.AES128GCMHKDF4KBKeyTemplate(),
+		},
+		{
+			name:     "AES128_GCM_HKDF_1MB",
+			template: streamingaead.AES128GCMHKDF1MBKeyTemplate(),
+		},
+		{
+			name:     "AES256_GCM_HKDF_4KB",
+			template: streamingaead.AES256GCMHKDF4KBKeyTemplate(),
+		},
+		{
+			name:     "AES256_GCM_HKDF_1MB",
+			template: streamingaead.AES256GCMHKDF1MBKeyTemplate(),
+		},
+	}
+	salts := [][]byte{nil, []byte("salt")}
+	for _, prf := range prfs {
+		for _, der := range derivations {
+			for _, salt := range salts {
+				name := fmt.Sprintf("%s_%s", prf.name, der.name)
+				if salt != nil {
+					name += "_with_salt"
+				}
+				t.Run(name, func(t *testing.T) {
+					prfKeyData, err := registry.NewKeyData(prf.template)
+					if err != nil {
+						t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+					}
+					d, err := newPRFBasedDeriver(prfKeyData, der.template)
+					if err != nil {
+						t.Fatalf("newPRFBasedDeriver() err = %v, want nil", err)
+					}
+					if _, err := d.DeriveKeyset(salt); err != nil {
+						t.Errorf("DeriveKeyset() err = %v, want nil", err)
+					}
+					// We cannot test the derived keyset handle because, at this point, it
+					// is filled with placeholder values for the key ID, status, and
+					// output prefix type fields.
+				})
+			}
+		}
+	}
+}
+
+func TestPRFBasedDeriverWithHKDFRFCVectorForAESGCM(t *testing.T) {
+	// This is the only HKDF vector that uses an accepted hash function and has
+	// key size >= 32-bytes.
+	// https://www.rfc-editor.org/rfc/rfc5869#appendix-A.2
+	vec := struct {
+		hash   commonpb.HashType
+		key    string
+		salt   string
+		info   string
+		outLen int
+		okm    string
+	}{
+		hash:   commonpb.HashType_SHA256,
+		key:    "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f",
+		salt:   "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+		info:   "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+		outLen: 82,
+		okm:    "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87",
+	}
+	prfKeyValue, err := hex.DecodeString(vec.key)
+	if err != nil {
+		t.Fatalf("hex.DecodeString() err = %v, want nil", err)
+	}
+	prfSalt, err := hex.DecodeString(vec.salt)
+	if err != nil {
+		t.Fatalf("hex.DecodeString() err = %v, want nil", err)
+	}
+	derivationSalt, err := hex.DecodeString(vec.info)
+	if err != nil {
+		t.Fatalf("hex.DecodeString() err = %v, want nil", err)
+	}
+	wantKeyValue, err := hex.DecodeString(vec.okm)
+	if err != nil {
+		t.Fatalf("hex.DecodeString() err = %v, want nil", err)
+	}
+
+	prfKey := &hkdfpb.HkdfPrfKey{
+		Version: 0,
+		Params: &hkdfpb.HkdfPrfParams{
+			Hash: vec.hash,
+			Salt: prfSalt,
+		},
+		KeyValue: prfKeyValue,
+	}
+	serializedPRFKey, err := proto.Marshal(prfKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	prfKeyData := &tinkpb.KeyData{
+		TypeUrl:         prf.HKDFSHA256PRFKeyTemplate().GetTypeUrl(),
+		Value:           serializedPRFKey,
+		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+	}
+
+	for _, test := range []struct {
+		name               string
+		derivedKeyTemplate *tinkpb.KeyTemplate
+	}{
+		{
+			name:               "AES128_GCM",
+			derivedKeyTemplate: aead.AES128GCMKeyTemplate(),
+		},
+		{
+			name:               "AES256_GCM",
+			derivedKeyTemplate: aead.AES256GCMKeyTemplate(),
+		},
+		{
+			name:               "AES256_GCM_RAW",
+			derivedKeyTemplate: aead.AES256GCMNoPrefixKeyTemplate(),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			// Derive keyset.
+			d, err := newPRFBasedDeriver(prfKeyData, test.derivedKeyTemplate)
+			if err != nil {
+				t.Fatalf("newPRFBasedDeriver() err = %v, want nil", err)
+			}
+			derivedHandle, err := d.DeriveKeyset(derivationSalt)
+			if err != nil {
+				t.Fatalf("DeriveKeyset() err = %v, want nil", err)
+			}
+			derivedKeyset := insecurecleartextkeyset.KeysetMaterial(derivedHandle)
+
+			// Verify keyset.
+			if len(derivedKeyset.GetKey()) != 1 {
+				t.Fatalf("len(keyset) = %d, want 1", len(derivedKeyset.GetKey()))
+			}
+			key := derivedKeyset.GetKey()[0]
+			if derivedKeyset.GetPrimaryKeyId() != key.GetKeyId() {
+				t.Fatal("keyset has no primary key")
+			}
+			// Verify key attributes set by prfBasedDeriver.
+			if got, want := key.GetStatus(), tinkpb.KeyStatusType_UNKNOWN_STATUS; got != want {
+				t.Errorf("derived key status = %s, want %s", got, want)
+			}
+			if got, want := key.GetOutputPrefixType(), tinkpb.OutputPrefixType_UNKNOWN_PREFIX; got != want {
+				t.Errorf("derived key output prefix type = %s, want %s", got, want)
+			}
+			// Verify key value.
+			derivedKeyFormat := &aesgcmpb.AesGcmKeyFormat{}
+			if err := proto.Unmarshal(test.derivedKeyTemplate.GetValue(), derivedKeyFormat); err != nil {
+				t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+			}
+			wantKeySize := int(derivedKeyFormat.GetKeySize())
+			aesGCMKey := &aesgcmpb.AesGcmKey{}
+			if err := proto.Unmarshal(key.GetKeyData().GetValue(), aesGCMKey); err != nil {
+				t.Fatalf("proto.Unmarshal() err = %v, want nil", err)
+			}
+			gotKeyValue := aesGCMKey.GetKeyValue()
+			if len(gotKeyValue) != wantKeySize {
+				t.Errorf("derived key value length = %d, want %d", len(gotKeyValue), wantKeySize)
+			}
+			if !bytes.Equal(gotKeyValue, wantKeyValue[:wantKeySize]) {
+				t.Errorf("derived key value = %q, want %q", gotKeyValue, wantKeyValue[:wantKeySize])
+			}
+		})
+	}
+}
+
+func TestNewPRFBasedDeriverRejectsInvalidInputs(t *testing.T) {
+	validPRFKeyData, err := registry.NewKeyData(prf.HKDFSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+	}
+	validDerivedKeyTemplate := aead.AES128GCMKeyTemplate()
+	if _, err := newPRFBasedDeriver(validPRFKeyData, validDerivedKeyTemplate); err != nil {
+		t.Fatalf("newPRFBasedDeriver() err = %v, want nil", err)
+	}
+	invalidPRFKeyData, err := registry.NewKeyData(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("registry.NewKeyData() err = %v, want nil", err)
+	}
+	// The derivation of KeysetDeriver keyset handles is not supported, i.e. a
+	// KeysetDeriver key template cannot be used as the derivedKeyTemplate
+	// argument in newPRFBasedDeriver().
+	invalidDerivedKeyTemplate, err := CreatePRFBasedKeyTemplate(prf.HKDFSHA256PRFKeyTemplate(), aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("CreatePRFBasedKeyTemplate() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name               string
+		prfKeyData         *tinkpb.KeyData
+		derivedKeyTemplate *tinkpb.KeyTemplate
+	}{
+		{
+			name: "nil inputs",
+		},
+		{
+			name:               "nil PRF key data",
+			derivedKeyTemplate: validDerivedKeyTemplate,
+		},
+		{
+			name:       "nil derived template",
+			prfKeyData: validPRFKeyData,
+		},
+		{
+			name:               "invalid PRF key data",
+			prfKeyData:         invalidPRFKeyData,
+			derivedKeyTemplate: validDerivedKeyTemplate,
+		},
+		{
+			name:               "invalid derived template",
+			prfKeyData:         validPRFKeyData,
+			derivedKeyTemplate: invalidDerivedKeyTemplate,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := newPRFBasedDeriver(test.prfKeyData, test.derivedKeyTemplate); err == nil {
+				t.Errorf("newPRFBasedDeriver() err = nil, want non-nil")
+			}
+		})
+	}
+}
diff --git a/go/keyset/BUILD.bazel b/go/keyset/BUILD.bazel
index c8ec4e6..f673e87 100644
--- a/go/keyset/BUILD.bazel
+++ b/go/keyset/BUILD.bazel
@@ -13,6 +13,7 @@
         "keyset.go",
         "manager.go",
         "mem_io.go",
+        "option.go",
         "reader.go",
         "validation.go",
         "writer.go",
@@ -40,18 +41,24 @@
         "binary_io_test.go",
         "handle_test.go",
         "json_io_test.go",
+        "keyset_test.go",
         "manager_test.go",
+        "mem_io_test.go",
         "validation_test.go",
     ],
     deps = [
         ":keyset",
-        "//aead/subtle",
+        "//aead",
+        "//insecurecleartextkeyset",
         "//mac",
         "//proto/common_go_proto",
         "//proto/tink_go_proto",
+        "//signature",
         "//subtle/random",
+        "//testing/fakekms",
         "//testkeyset",
         "//testutil",
+        "//tink",
         "@org_golang_google_protobuf//proto",
     ],
 )
diff --git a/go/keyset/binary_io.go b/go/keyset/binary_io.go
index 3363d0d..788fa1e 100644
--- a/go/keyset/binary_io.go
+++ b/go/keyset/binary_io.go
@@ -18,7 +18,6 @@
 
 import (
 	"io"
-	"io/ioutil"
 
 	"google.golang.org/protobuf/proto"
 
@@ -56,7 +55,7 @@
 }
 
 func read(r io.Reader, msg proto.Message) error {
-	data, err := ioutil.ReadAll(r)
+	data, err := io.ReadAll(r)
 	if err != nil {
 		return err
 	}
diff --git a/go/keyset/handle.go b/go/keyset/handle.go
index 8bcfaa6..d7ce0d8 100644
--- a/go/keyset/handle.go
+++ b/go/keyset/handle.go
@@ -34,18 +34,31 @@
 // Handle provides access to a Keyset protobuf, to limit the exposure of actual protocol
 // buffers that hold sensitive key material.
 type Handle struct {
-	ks *tinkpb.Keyset
+	ks          *tinkpb.Keyset
+	annotations map[string]string
+}
+
+func newWithOptions(ks *tinkpb.Keyset, opts ...Option) (*Handle, error) {
+	h := &Handle{ks: ks}
+	if err := applyOptions(h, opts...); err != nil {
+		return nil, err
+	}
+	return h, nil
 }
 
 // NewHandle creates a keyset handle that contains a single fresh key generated according
 // to the given KeyTemplate.
 func NewHandle(kt *tinkpb.KeyTemplate) (*Handle, error) {
-	ksm := NewManager()
-	err := ksm.Rotate(kt)
+	manager := NewManager()
+	keyID, err := manager.Add(kt)
 	if err != nil {
 		return nil, fmt.Errorf("keyset.Handle: cannot generate new keyset: %s", err)
 	}
-	handle, err := ksm.Handle()
+	err = manager.SetPrimary(keyID)
+	if err != nil {
+		return nil, fmt.Errorf("keyset.Handle: cannot set primary: %s", err)
+	}
+	handle, err := manager.Handle()
 	if err != nil {
 		return nil, fmt.Errorf("keyset.Handle: cannot get keyset handle: %s", err)
 	}
@@ -58,7 +71,7 @@
 	if ks == nil {
 		return nil, errors.New("keyset.Handle: nil keyset")
 	}
-	h := &Handle{ks}
+	h := &Handle{ks: ks}
 	if h.hasSecrets() {
 		// If you need to do this, you have to use func insecurecleartextkeyset.Read() instead.
 		return nil, errors.New("importing unencrypted secret key material is forbidden")
@@ -81,7 +94,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return &Handle{ks}, nil
+	return &Handle{ks: ks}, nil
 }
 
 // ReadWithNoSecrets tries to create a keyset.Handle from a keyset obtained via reader.
@@ -118,7 +131,7 @@
 		PrimaryKeyId: h.ks.PrimaryKeyId,
 		Key:          pubKeys,
 	}
-	return &Handle{ks}, nil
+	return &Handle{ks: ks}, nil
 }
 
 // String returns a string representation of the managed keyset.
@@ -188,6 +201,7 @@
 		return nil, fmt.Errorf("registry.PrimitivesWithKeyManager: invalid keyset: %s", err)
 	}
 	primitiveSet := primitiveset.New()
+	primitiveSet.Annotations = h.annotations
 	for _, key := range h.ks.Key {
 		if key.Status != tinkpb.KeyStatusType_ENABLED {
 			continue
diff --git a/go/keyset/handle_test.go b/go/keyset/handle_test.go
index 36485d2..3d19f6e 100644
--- a/go/keyset/handle_test.go
+++ b/go/keyset/handle_test.go
@@ -17,191 +17,299 @@
 package keyset_test
 
 import (
-	"strings"
+	"bytes"
 	"testing"
 
 	"google.golang.org/protobuf/proto"
-	"github.com/google/tink/go/aead/subtle"
+	"github.com/google/tink/go/aead"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/signature"
 	"github.com/google/tink/go/testkeyset"
 	"github.com/google/tink/go/testutil"
-
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 func TestNewHandle(t *testing.T) {
-	kt := mac.HMACSHA256Tag128KeyTemplate()
-	kh, err := keyset.NewHandle(kt)
+	template := mac.HMACSHA256Tag128KeyTemplate()
+	handle, err := keyset.NewHandle(template)
 	if err != nil {
-		t.Errorf("unexpected error: %s", err)
+		t.Errorf("keyset.NewHandle(template) = %v, want nil", err)
 	}
-	ks := testkeyset.KeysetMaterial(kh)
+	ks := testkeyset.KeysetMaterial(handle)
 	if len(ks.Key) != 1 {
-		t.Errorf("incorrect number of keys in the keyset: %d", len(ks.Key))
+		t.Errorf("len(ks.Key) = %d, want 1", len(ks.Key))
 	}
 	key := ks.Key[0]
 	if ks.PrimaryKeyId != key.KeyId {
-		t.Errorf("incorrect primary key id, expect %d, got %d", key.KeyId, ks.PrimaryKeyId)
+		t.Errorf("ks.PrimaryKeyId = %d, want %d", ks.PrimaryKeyId, key.KeyId)
 	}
-	if key.KeyData.TypeUrl != kt.TypeUrl {
-		t.Errorf("incorrect type url, expect %s, got %s", kt.TypeUrl, key.KeyData.TypeUrl)
+	if key.KeyData.TypeUrl != template.TypeUrl {
+		t.Errorf("key.KeyData.TypeUrl = %v, want %v", key.KeyData.TypeUrl, template.TypeUrl)
 	}
-	if _, err = mac.New(kh); err != nil {
-		t.Errorf("cannot get primitive from generated keyset handle: %s", err)
+	if _, err = mac.New(handle); err != nil {
+		t.Errorf("mac.New(handle) err = %v, want nil", err)
 	}
 }
 
-func TestNewHandleWithInvalidInput(t *testing.T) {
-	// template unregistered TypeUrl
-	template := mac.HMACSHA256Tag128KeyTemplate()
-	template.TypeUrl = "some unknown TypeUrl"
-	if _, err := keyset.NewHandle(template); err == nil {
-		t.Errorf("expect an error when TypeUrl is not registered")
+func TestNewHandleWithInvalidTypeURLFails(t *testing.T) {
+	// template with unknown TypeURL
+	invalidTemplate := mac.HMACSHA256Tag128KeyTemplate()
+	invalidTemplate.TypeUrl = "some unknown TypeURL"
+	if _, err := keyset.NewHandle(invalidTemplate); err == nil {
+		t.Errorf("keyset.NewHandle(invalidTemplate) err = nil, want error")
 	}
-	// nil
+}
+
+func TestNewHandleWithNilTemplateFails(t *testing.T) {
 	if _, err := keyset.NewHandle(nil); err == nil {
-		t.Errorf("expect an error when template is nil")
+		t.Error("keyset.NewHandle(nil) err = nil, want error")
 	}
 }
 
-func TestRead(t *testing.T) {
-	masterKey, err := subtle.NewAESGCM([]byte(strings.Repeat("A", 32)))
+func TestWriteAndReadInBinary(t *testing.T) {
+	keysetEncryptionHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
 	if err != nil {
-		t.Errorf("subtle.NewAESGCM(): %v", err)
+		t.Errorf("keyset.NewHandle(aead.AES128GCMKeyTemplate()) err = %v, want nil", err)
+	}
+	keysetEncryptionAead, err := aead.New(keysetEncryptionHandle)
+	if err != nil {
+		t.Errorf("aead.New(keysetEncryptionHandle) err = %v, want nil", err)
 	}
 
-	// Create a keyset
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_SYMMETRIC)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	ks := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	h, _ := testkeyset.NewHandle(ks)
-
-	memKeyset := &keyset.MemReaderWriter{}
-	if err := h.Write(memKeyset, masterKey); err != nil {
-		t.Fatalf("handle.Write(): %v", err)
-	}
-	h2, err := keyset.Read(memKeyset, masterKey)
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
 	if err != nil {
-		t.Fatalf("keyset.Read(): %v", err)
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
 	}
-	if !proto.Equal(testkeyset.KeysetMaterial(h), testkeyset.KeysetMaterial(h2)) {
-		t.Fatalf("Decrypt failed: got %v, want %v", h2, h)
+
+	buff := &bytes.Buffer{}
+	err = handle.Write(keyset.NewBinaryWriter(buff), keysetEncryptionAead)
+	if err != nil {
+		t.Fatalf("handle.Write(keyset.NewBinaryWriter(buff), keysetEncryptionAead) err = %v, want nil", err)
+	}
+	encrypted := buff.Bytes()
+
+	gotHandle, err := keyset.Read(keyset.NewBinaryReader(bytes.NewBuffer(encrypted)), keysetEncryptionAead)
+	if err != nil {
+		t.Fatalf("keyset.Read() err = %v, want nil", err)
+	}
+
+	if !proto.Equal(testkeyset.KeysetMaterial(gotHandle), testkeyset.KeysetMaterial(handle)) {
+		t.Fatalf("keyset.Read() = %v, want %v", gotHandle, handle)
 	}
 }
 
-func TestReadWithAssociatedData(t *testing.T) {
-	masterKey, err := subtle.NewAESGCM([]byte(strings.Repeat("A", 32)))
+func TestWriteAndReadInJSON(t *testing.T) {
+	keysetEncryptionHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
 	if err != nil {
-		t.Fatalf("subtle.NewAESGCM(): %v", err)
+		t.Errorf("keyset.NewHandle(aead.AES128GCMKeyTemplate()) err = %v, want nil", err)
+	}
+	keysetEncryptionAead, err := aead.New(keysetEncryptionHandle)
+	if err != nil {
+		t.Errorf("aead.New(keysetEncryptionHandle) err = %v, want nil", err)
 	}
 
-	// Create a keyset
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_SYMMETRIC)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	keySet := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	handle, _ := testkeyset.NewHandle(keySet)
-
-	memKeyset := &keyset.MemReaderWriter{}
-	if err := handle.WriteWithAssociatedData(memKeyset, masterKey, []byte{0x01, 0x02}); err != nil {
-		t.Fatalf("handle.Write(): %v", err)
-	}
-	handle2, err := keyset.ReadWithAssociatedData(memKeyset, masterKey, []byte{0x01, 0x02})
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
 	if err != nil {
-		t.Fatalf("keyset.Read(): %v", err)
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
 	}
+
+	buff := &bytes.Buffer{}
+	err = handle.Write(keyset.NewJSONWriter(buff), keysetEncryptionAead)
+	if err != nil {
+		t.Fatalf("h.Write(keyset.NewJSONWriter(buff), keysetEncryptionAead) err = %v, want nil", err)
+	}
+	encrypted := buff.Bytes()
+
+	gotHandle, err := keyset.Read(keyset.NewJSONReader(bytes.NewBuffer(encrypted)), keysetEncryptionAead)
+	if err != nil {
+		t.Fatalf("keyset.Read() err = %v, want nil", err)
+	}
+
+	if !proto.Equal(testkeyset.KeysetMaterial(gotHandle), testkeyset.KeysetMaterial(handle)) {
+		t.Fatalf("keyset.Read() = %v, want %v", gotHandle, handle)
+	}
+}
+
+func TestWriteAndReadWithAssociatedData(t *testing.T) {
+	keysetEncryptionHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Errorf("keyset.NewHandle(aead.AES128GCMKeyTemplate()) err = %v, want nil", err)
+	}
+	keysetEncryptionAead, err := aead.New(keysetEncryptionHandle)
+	if err != nil {
+		t.Errorf("aead.New(keysetEncryptionHandle) err = %v, want nil", err)
+	}
+
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
+	}
+	associatedData := []byte{0x01, 0x02}
+
+	buff := &bytes.Buffer{}
+	err = handle.WriteWithAssociatedData(keyset.NewBinaryWriter(buff), keysetEncryptionAead, associatedData)
+	if err != nil {
+		t.Fatalf("handle.WriteWithAssociatedData() err = %v, want nil", err)
+	}
+	encrypted := buff.Bytes()
+
+	handle2, err := keyset.ReadWithAssociatedData(keyset.NewBinaryReader(bytes.NewBuffer(encrypted)), keysetEncryptionAead, associatedData)
+	if err != nil {
+		t.Fatalf("keyset.ReadWithAssociatedData() err = %v, want nil", err)
+	}
+
 	if !proto.Equal(testkeyset.KeysetMaterial(handle), testkeyset.KeysetMaterial(handle2)) {
-		t.Errorf("Decrypt failed: got %v, want %v", handle2, handle)
+		t.Errorf("keyset.ReadWithAssociatedData() = %v, want %v", handle2, handle)
 	}
 }
 
 func TestReadWithMismatchedAssociatedData(t *testing.T) {
-	masterKey, err := subtle.NewAESGCM([]byte(strings.Repeat("A", 32)))
+	keysetEncryptionHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
 	if err != nil {
-		t.Fatalf("subtle.NewAESGCM(): %v", err)
+		t.Errorf("keyset.NewHandle(aead.AES128GCMKeyTemplate()) err = %v, want nil", err)
+	}
+	keysetEncryptionAead, err := aead.New(keysetEncryptionHandle)
+	if err != nil {
+		t.Errorf("aead.New(keysetEncryptionHandle) err = %v, want nil", err)
 	}
 
-	// Create a keyset
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_SYMMETRIC)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	keySet := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	handle, _ := testkeyset.NewHandle(keySet)
-
-	memKeyset := &keyset.MemReaderWriter{}
-	if err := handle.WriteWithAssociatedData(memKeyset, masterKey, []byte{0x01, 0x02}); err != nil {
-		t.Fatalf("handle.Write(): %v", err)
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
 	}
-	_, err = keyset.ReadWithAssociatedData(memKeyset, masterKey, []byte{0x01, 0x03})
+	associatedData := []byte{0x01, 0x02}
+
+	buff := &bytes.Buffer{}
+	err = handle.WriteWithAssociatedData(keyset.NewBinaryWriter(buff), keysetEncryptionAead, associatedData)
+	if err != nil {
+		t.Fatalf("handle.WriteWithAssociatedData() err = %v, want nil", err)
+	}
+	encrypted := buff.Bytes()
+
+	invalidAssociatedData := []byte{0x01, 0x03}
+	_, err = keyset.ReadWithAssociatedData(keyset.NewBinaryReader(bytes.NewBuffer(encrypted)), keysetEncryptionAead, invalidAssociatedData)
 	if err == nil {
-		t.Fatalf("keyset.Read() was expected to fail")
+		t.Errorf("keyset.ReadWithAssociatedData() err = nil, want err")
 	}
 }
 
-func TestReadWithNoSecrets(t *testing.T) {
-	// Create a keyset containing public key material
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_ASYMMETRIC_PUBLIC)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	ks := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	h, _ := testkeyset.NewHandle(ks)
-
-	memKeyset := &keyset.MemReaderWriter{}
-	if err := h.WriteWithNoSecrets(memKeyset); err != nil {
-		t.Fatalf("handle.WriteWithNoSecrets(): %v", err)
-	}
-	h2, err := keyset.ReadWithNoSecrets(memKeyset)
+func TestWriteAndReadWithNoSecrets(t *testing.T) {
+	// Create a keyset that contains a public key.
+	privateHandle, err := keyset.NewHandle(signature.ECDSAP256KeyTemplate())
 	if err != nil {
-		t.Fatalf("keyset.ReadWithNoSecrets(): %v", err)
+		t.Fatalf("keyset.NewHandle(signature.ECDSAP256KeyTemplate()) err = %v, want nil", err)
 	}
-	if !proto.Equal(testkeyset.KeysetMaterial(h), testkeyset.KeysetMaterial(h2)) {
-		t.Fatalf("Decrypt failed: got %v, want %v", h2, h)
+	handle, err := privateHandle.Public()
+	if err != nil {
+		t.Fatalf("privateHandle.Public() err = %v, want nil", err)
+	}
+
+	buff := &bytes.Buffer{}
+	err = handle.WriteWithNoSecrets(keyset.NewBinaryWriter(buff))
+	if err != nil {
+		t.Fatalf("handle.WriteWithAssociatedData(keyset.NewBinaryWriter(buff), masterKey, associatedData) err = %v, want nil", err)
+	}
+	serialized := buff.Bytes()
+
+	handle2, err := keyset.ReadWithNoSecrets(keyset.NewBinaryReader(bytes.NewBuffer(serialized)))
+	if err != nil {
+		t.Fatalf("keyset.ReadWithNoSecrets() err = %v, want nil", err)
+	}
+
+	if !proto.Equal(testkeyset.KeysetMaterial(handle), testkeyset.KeysetMaterial(handle2)) {
+		t.Fatalf("keyset.ReadWithNoSecrets() = %v, want %v", handle2, handle)
 	}
 }
 
-func TestWithNoSecretsFunctionsFailWhenHandlingSecretKeyMaterial(t *testing.T) {
-	// Create a keyset containing secret key material (symmetric)
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_SYMMETRIC)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	ks := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	h, _ := testkeyset.NewHandle(ks)
-
-	if err := h.WriteWithNoSecrets(&keyset.MemReaderWriter{}); err == nil {
-		t.Error("handle.WriteWithNoSecrets() should fail when exporting secret key material")
+func TestWriteWithNoSecretsFailsWithSymmetricSecretKey(t *testing.T) {
+	// Create a keyset that contains a symmetric secret key.
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(aead.AES256GCMKeyTemplate()) err = %v, want nil", err)
 	}
 
-	if _, err := keyset.ReadWithNoSecrets(&keyset.MemReaderWriter{Keyset: testkeyset.KeysetMaterial(h)}); err == nil {
-		t.Error("keyset.ReadWithNoSecrets should fail when importing secret key material")
+	buff := &bytes.Buffer{}
+	err = handle.WriteWithNoSecrets(keyset.NewBinaryWriter(buff))
+	if err == nil {
+		t.Error("handle.WriteWithNoSecrets() = nil, want error")
 	}
 }
 
-func TestWithNoSecretsFunctionsFailWhenUnknownKeyMaterial(t *testing.T) {
-	// Create a keyset containing secret key material (symmetric)
+func TestReadWithNoSecretsFailsWithSymmetricSecretKey(t *testing.T) {
+	// Create a keyset that contains a symmetric secret key.
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(aead.AES256GCMKeyTemplate()) err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	err = testkeyset.Write(handle, keyset.NewBinaryWriter(buff))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)) err = %v, want nil", err)
+	}
+	serialized := buff.Bytes()
+
+	_, err = keyset.ReadWithNoSecrets(keyset.NewBinaryReader(bytes.NewBuffer(serialized)))
+	if err == nil {
+		t.Error("keyset.ReadWithNoSecrets() = nil, want error")
+	}
+}
+
+func TestWriteWithNoSecretsFailsWithPrivateKey(t *testing.T) {
+	// Create a keyset that contains a private key.
+	handle, err := keyset.NewHandle(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(signature.ECDSAP256KeyTemplate()) err = %v, want nil", err)
+	}
+
+	buff := &bytes.Buffer{}
+	if err := handle.WriteWithNoSecrets(keyset.NewBinaryWriter(buff)); err == nil {
+		t.Error("handle.WriteWithNoSecrets() = nil, want error")
+	}
+}
+
+func TestReadWithNoSecretsFailsWithPrivateKey(t *testing.T) {
+	// Create a keyset that contains a private key.
+	handle, err := keyset.NewHandle(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(signature.ECDSAP256KeyTemplate()) err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	err = testkeyset.Write(handle, keyset.NewBinaryWriter(buff))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)) err = %v, want nil", err)
+	}
+	serialized := buff.Bytes()
+
+	_, err = keyset.ReadWithNoSecrets(keyset.NewBinaryReader(bytes.NewBuffer(serialized)))
+	if err == nil {
+		t.Error("keyset.ReadWithNoSecrets() = nil, want error")
+	}
+}
+
+func TestWriteAndReadWithNoSecretsFailsWithUnknownKeyMaterial(t *testing.T) {
+	// Create a keyset that contains unknown key material.
 	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_UNKNOWN_KEYMATERIAL)
 	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
 	ks := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	h, _ := testkeyset.NewHandle(ks)
-
-	if err := h.WriteWithNoSecrets(&keyset.MemReaderWriter{}); err == nil {
-		t.Error("handle.WriteWithNoSecrets() should fail when exporting secret key material")
+	handle, err := testkeyset.NewHandle(ks)
+	if err != nil {
+		t.Fatal(err)
+	}
+	serialized, err := proto.Marshal(ks)
+	if err != nil {
+		t.Fatal(err)
 	}
 
-	if _, err := keyset.ReadWithNoSecrets(&keyset.MemReaderWriter{Keyset: testkeyset.KeysetMaterial(h)}); err == nil {
-		t.Error("keyset.ReadWithNoSecrets should fail when importing secret key material")
-	}
-}
-
-func TestWithNoSecretsFunctionsFailWithAsymmetricPrivateKeyMaterial(t *testing.T) {
-	// Create a keyset containing secret key material (asymmetric)
-	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_ASYMMETRIC_PRIVATE)
-	key := testutil.NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 1, tinkpb.OutputPrefixType_TINK)
-	ks := testutil.NewKeyset(1, []*tinkpb.Keyset_Key{key})
-	h, _ := testkeyset.NewHandle(ks)
-
-	if err := h.WriteWithNoSecrets(&keyset.MemReaderWriter{}); err == nil {
-		t.Error("handle.WriteWithNoSecrets() should fail when exporting secret key material")
+	buff := &bytes.Buffer{}
+	err = handle.WriteWithNoSecrets(keyset.NewBinaryWriter(buff))
+	if err == nil {
+		t.Error("handle.WriteWithNoSecrets() = nil, want error")
 	}
 
-	if _, err := keyset.ReadWithNoSecrets(&keyset.MemReaderWriter{Keyset: testkeyset.KeysetMaterial(h)}); err == nil {
-		t.Error("keyset.ReadWithNoSecrets should fail when importing secret key material")
+	_, err = keyset.ReadWithNoSecrets(keyset.NewBinaryReader(bytes.NewBuffer(serialized)))
+	if err == nil {
+		t.Error("handle.ReadWithNoSecrets() = nil, want error")
 	}
 }
 
diff --git a/go/keyset/json_io.go b/go/keyset/json_io.go
index 4be64b5..d18f20e 100644
--- a/go/keyset/json_io.go
+++ b/go/keyset/json_io.go
@@ -18,7 +18,6 @@
 
 import (
 	"io"
-	"io/ioutil"
 
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
@@ -61,7 +60,7 @@
 }
 
 func (bkr *JSONReader) readJSON(r io.Reader, msg proto.Message) error {
-	b, err := ioutil.ReadAll(r)
+	b, err := io.ReadAll(r)
 	if err != nil {
 		return err
 	}
diff --git a/go/keyset/json_io_test.go b/go/keyset/json_io_test.go
index 1cd3c80..8582c67 100644
--- a/go/keyset/json_io_test.go
+++ b/go/keyset/json_io_test.go
@@ -169,7 +169,7 @@
 	}
 }
 
-func TestJSONReaderNegativeIds(t *testing.T) {
+func TestJSONReaderRejectsNegativeKeyIds(t *testing.T) {
 	gcmkey := []byte(testutil.NewAESGCMKey(0, 16).String())
 	jsonKeyset := fmt.Sprintf(`{
          "primaryKeyId": -10,
@@ -194,6 +194,32 @@
 	}
 }
 
+func TestJSONReaderRejectsKeyIdLargerThanUint32(t *testing.T) {
+	// 4294967296 = 2^32, which is too large for uint32.
+	gcmkey := []byte(testutil.NewAESGCMKey(0, 16).String())
+	jsonKeyset := fmt.Sprintf(`{
+         "primaryKeyId": 4294967296,
+         "key":[
+            {
+               "keyData":{
+                  "typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey",
+                  "keyMaterialType":"SYMMETRIC",
+                  "value": %q
+               },
+               "outputPrefixType":"TINK",
+               "keyId": 4294967296,
+               "status":"ENABLED"
+            }
+         ]
+      }`, base64.StdEncoding.EncodeToString(gcmkey))
+	r := keyset.NewJSONReader(bytes.NewBufferString(jsonKeyset))
+
+	_, err := r.Read()
+	if err == nil {
+		t.Fatalf("Expected failure due to negative key id")
+	}
+}
+
 // Tests that large IDs (>2^31) are written correctly.
 func TestJSONWriterLargeId(t *testing.T) {
 	eaxkey := []byte(testutil.NewHMACKey(commonpb.HashType_SHA512, 32).String())
diff --git a/go/keyset/keyset.go b/go/keyset/keyset.go
index 8a7be4a..d29e7f6 100644
--- a/go/keyset/keyset.go
+++ b/go/keyset/keyset.go
@@ -26,8 +26,8 @@
 // keysetHandle is used by package insecurecleartextkeyset and package
 // testkeyset (via package internal) to create a keyset.Handle from cleartext
 // key material.
-func keysetHandle(ks *tinkpb.Keyset) *Handle {
-	return &Handle{ks}
+func keysetHandle(ks *tinkpb.Keyset, opts ...Option) (*Handle, error) {
+	return newWithOptions(ks, opts...)
 }
 
 // keysetMaterial is used by package insecurecleartextkeyset and package
diff --git a/go/keyset/keyset_test.go b/go/keyset/keyset_test.go
new file mode 100644
index 0000000..df55f57
--- /dev/null
+++ b/go/keyset/keyset_test.go
@@ -0,0 +1,95 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyset_test
+
+// [START encrypted-keyset-example]
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/testing/fakekms"
+)
+
+// The fake KMS should only be used in tests. It is not secure.
+const keyURI = "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE"
+
+func Example_encryptedKeyset() {
+	// Get a KEK (key encryption key) AEAD. This is usually a remote AEAD to a KMS. In this example,
+	// we use a fake KMS to avoid making RPCs.
+	client, err := fakekms.NewClient(keyURI)
+	if err != nil {
+		log.Fatal(err)
+	}
+	kekAEAD, err := client.GetAEAD(keyURI)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Generate a new keyset handle for the primitive we want to use.
+	newHandle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Choose some associated data. This is the context in which the keyset will be used.
+	keysetAssociatedData := []byte("keyset encryption example")
+
+	// Encrypt the keyset with the KEK AEAD and the associated data.
+	buf := new(bytes.Buffer)
+	writer := keyset.NewBinaryWriter(buf)
+	err = newHandle.WriteWithAssociatedData(writer, kekAEAD, keysetAssociatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	encryptedKeyset := buf.Bytes()
+
+	// The encrypted keyset can now be stored.
+
+	// To use the primitive, we first need to decrypt the keyset. We use the same
+	// KEK AEAD and the same associated data that we used to encrypt it.
+	reader := keyset.NewBinaryReader(bytes.NewReader(encryptedKeyset))
+	handle, err := keyset.ReadWithAssociatedData(reader, kekAEAD, keysetAssociatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Get the primitive.
+	primitive, err := aead.New(handle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Use the primitive.
+	plaintext := []byte("message")
+	associatedData := []byte("example encryption")
+	ciphertext, err := primitive.Encrypt(plaintext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	decrypted, err := primitive.Decrypt(ciphertext, associatedData)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(string(decrypted))
+	// Output: message
+}
+
+// [END encrypted-keyset-example]
diff --git a/go/keyset/manager.go b/go/keyset/manager.go
index e61cdcb..660e318 100644
--- a/go/keyset/manager.go
+++ b/go/keyset/manager.go
@@ -46,22 +46,6 @@
 	return ret
 }
 
-// Rotate generates a fresh key using the given key template and
-// sets the new key as the primary key.
-//
-// Deprecated: please use Add instead. Rotate adds a new key and immediately promotes it to primary.
-// However, when performing keyset rotation, you almost never want a newly added key to immediately be set as the primary key.
-// Instead, you want to allow sufficient time for key propagation to occur.
-func (km *Manager) Rotate(kt *tinkpb.KeyTemplate) error {
-	keyID, err := km.Add(kt)
-	if err != nil {
-		return err
-	}
-	// Set the new key as the primary key
-	km.ks.PrimaryKeyId = keyID
-	return nil
-}
-
 // Add generates and adds a fresh key using the given key template.
 // the key is enabled on creation, but not set to primary.
 // It returns the ID of the new key
@@ -179,7 +163,7 @@
 
 // Handle creates a new Handle for the managed keyset.
 func (km *Manager) Handle() (*Handle, error) {
-	return &Handle{km.ks}, nil
+	return &Handle{ks: km.ks}, nil
 }
 
 // newKeyID generates a key id that has not been used by any key in the keyset.
diff --git a/go/keyset/manager_test.go b/go/keyset/manager_test.go
index 10ac8be..c5fe2e6 100644
--- a/go/keyset/manager_test.go
+++ b/go/keyset/manager_test.go
@@ -32,9 +32,13 @@
 	// Create a keyset that contains a single HmacKey.
 	ksm := keyset.NewManager()
 	kt := mac.HMACSHA256Tag128KeyTemplate()
-	err := ksm.Rotate(kt)
+	keyID, err := ksm.Add(kt)
 	if err != nil {
-		t.Errorf("cannot rotate when key template is available: %s", err)
+		t.Errorf("cannot add key: %s", err)
+	}
+	err = ksm.SetPrimary(keyID)
+	if err != nil {
+		t.Errorf("cannot set primary key: %s", err)
 	}
 	h, err := ksm.Handle()
 	if err != nil {
@@ -56,11 +60,14 @@
 	// Create a keyset that contains a single HmacKey.
 	ksm1 := keyset.NewManager()
 	kt := mac.HMACSHA256Tag128KeyTemplate()
-	err := ksm1.Rotate(kt)
+	keyID1, err := ksm1.Add(kt)
 	if err != nil {
-		t.Errorf("cannot rotate when key template is available: %s", err)
+		t.Errorf("cannot add key: %s", err)
 	}
-
+	err = ksm1.SetPrimary(keyID1)
+	if err != nil {
+		t.Errorf("cannot set primary key: %s", err)
+	}
 	h1, err := ksm1.Handle()
 	if err != nil {
 		t.Errorf("cannot get keyset handle: %s", err)
@@ -68,7 +75,14 @@
 	ks1 := testkeyset.KeysetMaterial(h1)
 
 	ksm2 := keyset.NewManagerFromHandle(h1)
-	ksm2.Rotate(kt)
+	keyID2, err := ksm2.Add(kt)
+	if err != nil {
+		t.Errorf("cannot add key: %s", err)
+	}
+	err = ksm2.SetPrimary(keyID2)
+	if err != nil {
+		t.Errorf("cannot set primary key: %s", err)
+	}
 	h2, err := ksm2.Handle()
 	if err != nil {
 		t.Errorf("cannot get keyset handle: %s", err)
@@ -115,29 +129,6 @@
 	}
 }
 
-func TestUnknowOutputPrefixTypeFails(t *testing.T) {
-	ksm1 := keyset.NewManager()
-	kt := mac.HMACSHA256Tag128KeyTemplate()
-	kt.OutputPrefixType = tinkpb.OutputPrefixType_UNKNOWN_PREFIX
-	err := ksm1.Rotate(kt)
-	if err == nil {
-		t.Errorf("ksm1.Rotate(kt) where kt has an unknown prefix succeeded, want error")
-	}
-}
-
-func TestKeysetManagerWithNilKeysetTemplate(t *testing.T) {
-	// ops with nil template should fail
-	ksm1 := keyset.NewManager()
-	err := ksm1.Rotate(nil)
-	if err == nil {
-		t.Error("ksm1.Rotate succeeded, but want error")
-	}
-	_, err = ksm1.Add(nil)
-	if err == nil {
-		t.Errorf("ksm1.Add succeeded, but want error")
-	}
-}
-
 func TestKeysetManagerAdd(t *testing.T) {
 	ksm1 := keyset.NewManager()
 	kt := mac.HMACSHA256Tag128KeyTemplate()
@@ -165,7 +156,16 @@
 	}
 }
 
-func TestKeysetManagerAddWithBadTemplate(t *testing.T) {
+func TestKeysetManagerAddWithNilKeysetTemplateFails(t *testing.T) {
+	// ops with nil template should fail
+	ksm1 := keyset.NewManager()
+	_, err := ksm1.Add(nil)
+	if err == nil {
+		t.Errorf("ksm1.Add succeeded, but want error")
+	}
+}
+
+func TestKeysetManagerAddWithInvalidTypeUrlFails(t *testing.T) {
 	ksm1 := keyset.NewManager()
 	kt := &tinkpb.KeyTemplate{
 		TypeUrl:          "invalid type",
@@ -177,6 +177,16 @@
 	}
 }
 
+func TestKeysetManagerAddWithUnknownOutputPrefixTypeFails(t *testing.T) {
+	ksm1 := keyset.NewManager()
+	kt := mac.HMACSHA256Tag128KeyTemplate()
+	kt.OutputPrefixType = tinkpb.OutputPrefixType_UNKNOWN_PREFIX
+	_, err := ksm1.Add(kt)
+	if err == nil {
+		t.Errorf("ksm1.Add(kt) where kt has an unknown prefix succeeded, want error")
+	}
+}
+
 func TestKeysetManagerEnable(t *testing.T) {
 	keyID := uint32(42)
 	keyData := testutil.NewKeyData("some type url", []byte{0}, tinkpb.KeyData_SYMMETRIC)
@@ -574,10 +584,6 @@
 	if err == nil {
 		t.Errorf("ksm1.Add succeeded on empty manager, want error")
 	}
-	err = ksm1.Rotate(mac.HMACSHA256Tag128KeyTemplate())
-	if err == nil {
-		t.Errorf("ksm1.Rotate succeeded on empty manager, want error")
-	}
 	err = ksm1.SetPrimary(0)
 	if err == nil {
 		t.Errorf("ksm1.SetPrimary succeeded on empty manager, want error")
diff --git a/go/keyset/mem_io.go b/go/keyset/mem_io.go
index cbe87f7..911021e 100644
--- a/go/keyset/mem_io.go
+++ b/go/keyset/mem_io.go
@@ -19,6 +19,9 @@
 import tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 
 // MemReaderWriter implements keyset.Reader and keyset.Writer for *tinkpb.Keyset and *tinkpb.EncryptedKeyset.
+//
+// Deprecated: Use [keyset.NewBinaryReader] or [keyset.NewBinaryWriter] instead. See tests in
+// mem_io_test.go for examples on how to use them instead.
 type MemReaderWriter struct {
 	Keyset          *tinkpb.Keyset
 	EncryptedKeyset *tinkpb.EncryptedKeyset
diff --git a/go/keyset/mem_io_test.go b/go/keyset/mem_io_test.go
new file mode 100644
index 0000000..055d243
--- /dev/null
+++ b/go/keyset/mem_io_test.go
@@ -0,0 +1,341 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyset_test
+
+import (
+	"bytes"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/signature"
+	"github.com/google/tink/go/testkeyset"
+	"github.com/google/tink/go/tink"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func TestConvertProtoKeysetIntoHandleInTests(t *testing.T) {
+	h, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	protoKeyset := testkeyset.KeysetMaterial(h)
+
+	// In tests, this:
+	wantHandle, err := insecurecleartextkeyset.Read(&keyset.MemReaderWriter{Keyset: protoKeyset})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// can be replaced by this:
+	gotHandle, err := testkeyset.NewHandle(protoKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := testkeyset.KeysetMaterial(gotHandle), testkeyset.KeysetMaterial(wantHandle); !proto.Equal(got, want) {
+		t.Errorf("gotHandle contains %s, want %s", got, want)
+	}
+}
+
+func TestConvertHandleKeysetIntoProtoKeysetInTests(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// In tests, this:
+	writer := &keyset.MemReaderWriter{}
+	if err := insecurecleartextkeyset.Write(handle, writer); err != nil {
+		t.Fatal(err)
+	}
+	wantKeyset := writer.Keyset
+
+	// can be replaced by this:
+	gotKeyset := testkeyset.KeysetMaterial(handle)
+
+	if !proto.Equal(gotKeyset, wantKeyset) {
+		t.Errorf("testkeyset.KeysetMaterial(handle) = %v, want %v", gotKeyset, wantKeyset)
+	}
+}
+
+func TestConvertProtoKeysetIntoHandle(t *testing.T) {
+	h, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	protoKeyset := testkeyset.KeysetMaterial(h)
+
+	// This:
+	wantHandle, err := insecurecleartextkeyset.Read(&keyset.MemReaderWriter{Keyset: protoKeyset})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// can be replaced by this:
+	serializedKeyset, err := proto.Marshal(protoKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := testkeyset.KeysetMaterial(gotHandle), testkeyset.KeysetMaterial(wantHandle); !proto.Equal(got, want) {
+		t.Errorf("gotHandle contains %s, want %s", got, want)
+	}
+}
+
+func TestConvertHandleKeysetIntoProtoKeyset(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// This:
+	writer := &keyset.MemReaderWriter{}
+	if err := insecurecleartextkeyset.Write(handle, writer); err != nil {
+		t.Fatal(err)
+	}
+	wantKeyset := writer.Keyset
+
+	// can be replaced by this:
+	gotKeyset := insecurecleartextkeyset.KeysetMaterial(handle)
+
+	if !proto.Equal(gotKeyset, wantKeyset) {
+		t.Errorf("insecurecleartextkeyset.KeysetMaterial(handle) = %v, want %v", gotKeyset, wantKeyset)
+	}
+}
+
+func TestConvertHandleKeysetIntoSerializedKeyset(t *testing.T) {
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// This:
+	writer := &keyset.MemReaderWriter{}
+	if err := insecurecleartextkeyset.Write(handle, writer); err != nil {
+		t.Fatal(err)
+	}
+	wantSerializedKeyset, err := proto.Marshal(writer.Keyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// can be replaced by this:
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatal(err)
+	}
+	gotSerializedKeyset := buff.Bytes()
+
+	// Since serialization may not be deterministic, we parse the keyset and compare the protos.
+	wantKeyset := new(tinkpb.Keyset)
+	err = proto.Unmarshal(wantSerializedKeyset, wantKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotKeyset := new(tinkpb.Keyset)
+	err = proto.Unmarshal(gotSerializedKeyset, gotKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !proto.Equal(gotKeyset, wantKeyset) {
+		t.Errorf("gotKeyset = %v, want %v", gotKeyset, wantKeyset)
+	}
+}
+
+func TestConvertPublicKeyProtoKeysetIntoHandle(t *testing.T) {
+	privateHandle, err := keyset.NewHandle(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	publicHandle, err := privateHandle.Public()
+	if err != nil {
+		t.Fatal(err)
+	}
+	protoPublicKeyset := testkeyset.KeysetMaterial(publicHandle)
+
+	// This:
+	wantHandle, err := keyset.ReadWithNoSecrets(&keyset.MemReaderWriter{Keyset: protoPublicKeyset})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// can be replaced by this:
+	serializedKeyset, err := proto.Marshal(protoPublicKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotHandle, err := keyset.ReadWithNoSecrets(
+		keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := testkeyset.KeysetMaterial(gotHandle), testkeyset.KeysetMaterial(wantHandle); !proto.Equal(got, want) {
+		t.Errorf("gotHandle contains %s, want %s", got, want)
+	}
+}
+
+func TestConvertPublicKeysetHandleIntoProtoKeyset(t *testing.T) {
+	privateHandle, err := keyset.NewHandle(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	publicHandle, err := privateHandle.Public()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// This:
+	writer := &keyset.MemReaderWriter{}
+	if err := publicHandle.WriteWithNoSecrets(writer); err != nil {
+		t.Fatal(err)
+	}
+	wantKeyset := writer.Keyset
+
+	// can be replaced by this:
+	buff := &bytes.Buffer{}
+	if err := publicHandle.WriteWithNoSecrets(keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatal(err)
+	}
+	serializedKeyset := buff.Bytes()
+	gotKeyset := new(tinkpb.Keyset)
+	err = proto.Unmarshal(serializedKeyset, gotKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !proto.Equal(gotKeyset, wantKeyset) {
+		t.Errorf("gotKeyset = %v, want %v", gotKeyset, wantKeyset)
+	}
+}
+
+func decryptKeyset(encrypted *tinkpb.EncryptedKeyset, keysetEncryptionAEAD tink.AEAD) (*tinkpb.Keyset, error) {
+	decrypted, err := keysetEncryptionAEAD.Decrypt(encrypted.GetEncryptedKeyset(), nil)
+	if err != nil {
+		return nil, err
+	}
+	k := new(tinkpb.Keyset)
+	err = proto.Unmarshal(decrypted, k)
+	if err != nil {
+		return nil, err
+	}
+	return k, err
+}
+
+func TestConvertHandleKeysetIntoProtoEncryptedKeyset(t *testing.T) {
+	kekHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	keysetEncryptionAEAD, err := aead.New(kekHandle)
+	if err != nil {
+		t.Fatal(err)
+	}
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// This:
+	memWriter := &keyset.MemReaderWriter{}
+	if err := handle.Write(memWriter, keysetEncryptionAEAD); err != nil {
+		t.Fatal(err)
+	}
+	wantEncryptedKeyset := memWriter.EncryptedKeyset
+
+	// can be replaced by this:
+	buff := &bytes.Buffer{}
+	if err := handle.Write(keyset.NewBinaryWriter(buff), keysetEncryptionAEAD); err != nil {
+		t.Fatal(err)
+	}
+	serializedKeyset := buff.Bytes()
+	gotEncryptedKeyset := new(tinkpb.EncryptedKeyset)
+	err = proto.Unmarshal(serializedKeyset, gotEncryptedKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	wantKeyset, err := decryptKeyset(wantEncryptedKeyset, keysetEncryptionAEAD)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotKeyset, err := decryptKeyset(gotEncryptedKeyset, keysetEncryptionAEAD)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !proto.Equal(gotKeyset, wantKeyset) {
+		t.Errorf("gotKeyset = %v, want %v", gotKeyset, wantKeyset)
+	}
+}
+
+func TestConvertProtoEncryptedKeysetIntoHandle(t *testing.T) {
+	kekHandle, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	keysetEncryptionAEAD, err := aead.New(kekHandle)
+	if err != nil {
+		t.Fatal(err)
+	}
+	handle, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatal(err)
+	}
+	buff := &bytes.Buffer{}
+	if err := handle.Write(keyset.NewBinaryWriter(buff), keysetEncryptionAEAD); err != nil {
+		t.Fatal(err)
+	}
+	encryptedKeyset := new(tinkpb.EncryptedKeyset)
+	err = proto.Unmarshal(buff.Bytes(), encryptedKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// This:
+	memReader := &keyset.MemReaderWriter{
+		EncryptedKeyset: encryptedKeyset,
+	}
+	wantHandle, err := keyset.Read(memReader, keysetEncryptionAEAD)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// can be replaced by this:
+	serializedKeyset, err := proto.Marshal(encryptedKeyset)
+	if err != nil {
+		t.Fatal(err)
+	}
+	gotHandle, err := keyset.Read(
+		keyset.NewBinaryReader(bytes.NewBuffer(serializedKeyset)),
+		keysetEncryptionAEAD)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := testkeyset.KeysetMaterial(gotHandle), testkeyset.KeysetMaterial(wantHandle); !proto.Equal(got, want) {
+		t.Errorf("gotHandle contains %s, want %s", got, want)
+	}
+}
diff --git a/go/keyset/option.go b/go/keyset/option.go
new file mode 100644
index 0000000..f167239
--- /dev/null
+++ b/go/keyset/option.go
@@ -0,0 +1,48 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package keyset
+
+import "fmt"
+
+// Option is used to pass options for a keyset handle.
+type Option interface {
+	set(k *Handle) error
+}
+
+type option func(*Handle) error
+
+func (o option) set(h *Handle) error { return o(h) }
+
+// WithAnnotations adds monitoring annotations to a keyset handle.
+func WithAnnotations(annotations map[string]string) Option {
+	return option(func(h *Handle) error {
+		if h.annotations != nil {
+			return fmt.Errorf("keyset already contains annotations")
+		}
+		h.annotations = annotations
+		return nil
+	})
+}
+
+func applyOptions(h *Handle, opts ...Option) error {
+	for _, opt := range opts {
+		if err := opt.set(h); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/go/keyset/validation.go b/go/keyset/validation.go
index 646f1be..7fb79ed 100644
--- a/go/keyset/validation.go
+++ b/go/keyset/validation.go
@@ -77,9 +77,6 @@
 	if key == nil {
 		return fmt.Errorf("ValidateKey() called with nil")
 	}
-	if key.KeyId == 0 {
-		return fmt.Errorf("key has zero key id: %d", key.KeyId)
-	}
 	if key.KeyData == nil {
 		return fmt.Errorf("key %d has no key data", key.KeyId)
 	}
diff --git a/go/mac/BUILD.bazel b/go/mac/BUILD.bazel
index 8642224..477cec1 100644
--- a/go/mac/BUILD.bazel
+++ b/go/mac/BUILD.bazel
@@ -19,8 +19,12 @@
         "//core/cryptofmt",
         "//core/primitiveset",
         "//core/registry",
+        "//internal/internalregistry",
+        "//internal/monitoringutil",
+        "//internal/tinkerror",
         "//keyset",
         "//mac/subtle",
+        "//monitoring",
         "//proto/aes_cmac_go_proto",
         "//proto/common_go_proto",
         "//proto/hmac_go_proto",
@@ -37,6 +41,7 @@
         "aes_cmac_key_manager_test.go",
         "hmac_key_manager_test.go",
         "mac_factory_test.go",
+        "mac_init_test.go",
         "mac_key_templates_test.go",
         "mac_test.go",
     ],
@@ -44,8 +49,13 @@
         ":mac",
         "//core/cryptofmt",
         "//core/registry",
+        "//insecurecleartextkeyset",
+        "//internal/internalregistry",
+        "//internal/testing/stubkeymanager",
         "//keyset",
+        "//mac/internal/mactest",
         "//mac/subtle",
+        "//monitoring",
         "//proto/aes_cmac_go_proto",
         "//proto/common_go_proto",
         "//proto/hmac_go_proto",
@@ -53,9 +63,12 @@
         "//signature",
         "//subtle",
         "//subtle/random",
+        "//testing/fakemonitoring",
         "//testkeyset",
         "//testutil",
         "//tink",
+        "@com_github_google_go_cmp//cmp",
+        "@com_github_google_go_cmp//cmp/cmpopts",
         "@org_golang_google_protobuf//proto",
     ],
 )
diff --git a/go/mac/aes_cmac_key_manager_test.go b/go/mac/aes_cmac_key_manager_test.go
index 4abce1a..9a39918 100644
--- a/go/mac/aes_cmac_key_manager_test.go
+++ b/go/mac/aes_cmac_key_manager_test.go
@@ -23,7 +23,6 @@
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/mac/subtle"
 	subtleMac "github.com/google/tink/go/mac/subtle"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
@@ -270,7 +269,7 @@
 // validateCMACPrimitive checks whether the given primitive matches the given AESCMACKey
 func validateCMACPrimitive(p interface{}, key *cmacpb.AesCmacKey) error {
 	cmacPrimitive := p.(*subtleMac.AESCMAC)
-	keyPrimitive, err := subtle.NewAESCMAC(key.KeyValue, key.Params.TagSize)
+	keyPrimitive, err := subtleMac.NewAESCMAC(key.KeyValue, key.Params.TagSize)
 	if err != nil {
 		return fmt.Errorf("Could not create AES CMAC with key material %q and tag size %d: %s", hex.EncodeToString(key.KeyValue), key.Params.TagSize, err)
 	}
diff --git a/go/mac/hmac_key_manager.go b/go/mac/hmac_key_manager.go
index cebc1c8..ffeb4ac 100644
--- a/go/mac/hmac_key_manager.go
+++ b/go/mac/hmac_key_manager.go
@@ -19,6 +19,7 @@
 import (
 	"errors"
 	"fmt"
+	"io"
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/keyset"
@@ -95,7 +96,7 @@
 	return &tinkpb.KeyData{
 		TypeUrl:         hmacTypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
@@ -109,6 +110,38 @@
 	return hmacTypeURL
 }
 
+// KeyMaterialType returns the key material type of this key manager.
+func (km *hmacKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+func (km *hmacKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errInvalidHMACKeyFormat
+	}
+	keyFormat := new(hmacpb.HmacKeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidHMACKeyFormat
+	}
+	if err := km.validateKeyFormat(keyFormat); err != nil {
+		return nil, fmt.Errorf("hmac_key_manager: invalid key format: %v", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), hmacKeyVersion); err != nil {
+		return nil, fmt.Errorf("hmac_key_manager: invalid key version: %s", err)
+	}
+
+	keyValue := make([]byte, keyFormat.GetKeySize())
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("hmac_key_manager: not enough pseudorandomness given")
+	}
+	return &hmacpb.HmacKey{
+		Version:  hmacKeyVersion,
+		Params:   keyFormat.Params,
+		KeyValue: keyValue,
+	}, nil
+}
+
 // validateKey validates the given HMACKey. It only validates the version of the
 // key because other parameters will be validated in primitive construction.
 func (km *hmacKeyManager) validateKey(key *hmacpb.HmacKey) error {
diff --git a/go/mac/hmac_key_manager_test.go b/go/mac/hmac_key_manager_test.go
index 7c39dcf..a397cb9 100644
--- a/go/mac/hmac_key_manager_test.go
+++ b/go/mac/hmac_key_manager_test.go
@@ -18,12 +18,15 @@
 
 import (
 	"bytes"
+	"encoding/hex"
 	"fmt"
 	"reflect"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	subtleMac "github.com/google/tink/go/mac/subtle"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/subtle"
@@ -208,6 +211,214 @@
 	}
 }
 
+func TestHMACKeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestHMACDeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&hmacpb.HmacKeyFormat{
+		Version: testutil.HMACKeyVersion,
+		KeySize: 16,
+		Params: &hmacpb.HmacParams{
+			Hash:    commonpb.HashType_SHA256,
+			TagSize: 10,
+		},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	rand := random.GetRandomBytes(16)
+	buf := &bytes.Buffer{}
+	buf.Write(rand) // never returns a non-nil error
+	k, err := keyManager.DeriveKey(keyFormat, buf)
+	if err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+	key := k.(*hmacpb.HmacKey)
+	if got, want := len(key.GetKeyValue()), 16; got != want {
+		t.Errorf("key length = %d, want %d", got, want)
+	}
+	if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+		t.Errorf("incorrect derived key: diff = %v", diff)
+	}
+}
+
+func TestHMACDeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	validKeyFormat := &hmacpb.HmacKeyFormat{
+		Version: testutil.HMACKeyVersion,
+		KeySize: 16,
+		Params: &hmacpb.HmacParams{
+			Hash:    commonpb.HashType_SHA256,
+			TagSize: 10,
+		},
+	}
+	serializedValidKeyFormat, err := proto.Marshal(validKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKeyFormat, err)
+	}
+	buf := bytes.NewBuffer(random.GetRandomBytes(validKeyFormat.KeySize))
+	if _, err := keyManager.DeriveKey(serializedValidKeyFormat, buf); err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name    string
+		version uint32
+		keySize uint32
+		hash    commonpb.HashType
+		tagSize uint32
+	}{
+		{
+			name:    "invalid version",
+			version: 10,
+			keySize: validKeyFormat.KeySize,
+			hash:    validKeyFormat.Params.Hash,
+			tagSize: validKeyFormat.Params.TagSize,
+		},
+		{
+			name:    "invalid key size",
+			version: validKeyFormat.Version,
+			keySize: 10,
+			hash:    validKeyFormat.Params.Hash,
+			tagSize: validKeyFormat.Params.TagSize,
+		},
+		{
+			name:    "invalid hash",
+			version: validKeyFormat.Version,
+			keySize: validKeyFormat.KeySize,
+			hash:    commonpb.HashType_UNKNOWN_HASH,
+			tagSize: validKeyFormat.Params.TagSize,
+		},
+		{
+			name:    "invalid tag size",
+			version: validKeyFormat.Version,
+			keySize: validKeyFormat.KeySize,
+			hash:    validKeyFormat.Params.Hash,
+			tagSize: 9,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat, err := proto.Marshal(&hmacpb.HmacKeyFormat{
+				Version: test.version,
+				KeySize: test.keySize,
+				Params: &hmacpb.HmacParams{
+					Hash:    test.hash,
+					TagSize: test.tagSize,
+				},
+			})
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.keySize))
+			if _, err := keyManager.DeriveKey(keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHMACDeriveKeyFailsWithMalformedKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "nil",
+			keyFormat: nil,
+		},
+		{
+			name:      "empty",
+			keyFormat: []byte{},
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(16))
+			if _, err := keyManager.DeriveKey(test.keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHMACDeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&hmacpb.HmacKeyFormat{
+		Version: testutil.HMACKeyVersion,
+		KeySize: 16,
+		Params: &hmacpb.HmacParams{
+			Hash:    commonpb.HashType_SHA256,
+			TagSize: 10,
+		},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(16))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(15))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func genInvalidHMACKeys() []proto.Message {
 	badVersionKey := testutil.NewHMACKey(commonpb.HashType_SHA256, 32)
 	badVersionKey.Version++
diff --git a/go/mac/internal/mactest/BUILD.bazel b/go/mac/internal/mactest/BUILD.bazel
new file mode 100644
index 0000000..c2a0d5d
--- /dev/null
+++ b/go/mac/internal/mactest/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+# TODO(felobato): describe this package.
+
+go_library(
+    name = "mactest",
+    srcs = ["mactest.go"],
+    importpath = "github.com/google/tink/go/mac/internal/mactest",
+    visibility = ["//mac:__subpackages__"],
+    deps = ["//tink"],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":mactest",
+    visibility = ["//mac:__subpackages__"],
+)
+
+go_test(
+    name = "mactest_test",
+    srcs = ["mactest_test.go"],
+    deps = [":mactest"],
+)
diff --git a/go/mac/internal/mactest/mactest.go b/go/mac/internal/mactest/mactest.go
new file mode 100644
index 0000000..41d614e
--- /dev/null
+++ b/go/mac/internal/mactest/mactest.go
@@ -0,0 +1,39 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Package mactest has testing utilities for the MAC primitive
+package mactest
+
+import (
+	"github.com/google/tink/go/tink"
+)
+
+// AlwaysFailingMAC fails compute and verify operations.
+type AlwaysFailingMAC struct {
+	Error error
+}
+
+var _ (tink.MAC) = (*AlwaysFailingMAC)(nil)
+
+// ComputeMAC returns an error on compute.
+func (m *AlwaysFailingMAC) ComputeMAC(data []byte) ([]byte, error) {
+	return nil, m.Error
+}
+
+// VerifyMAC returns an error on verify.
+func (m *AlwaysFailingMAC) VerifyMAC(mac []byte, data []byte) error {
+	return m.Error
+}
diff --git a/go/mac/internal/mactest/mactest_test.go b/go/mac/internal/mactest/mactest_test.go
new file mode 100644
index 0000000..ff63fc2
--- /dev/null
+++ b/go/mac/internal/mactest/mactest_test.go
@@ -0,0 +1,35 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package mactest_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/google/tink/go/mac/internal/mactest"
+)
+
+func TestAlwaysFailingMACAlwayFails(t *testing.T) {
+	wantErr := errors.New("panic at the kernel")
+	p := &mactest.AlwaysFailingMAC{Error: wantErr}
+	if _, err := p.ComputeMAC([]byte("valid_data")); !errors.Is(err, wantErr) {
+		t.Errorf("p.ComputeMac() err = %v, want %v", err, wantErr)
+	}
+	if err := p.VerifyMAC([]byte("data"), []byte("tag")); !errors.Is(err, wantErr) {
+		t.Errorf("p.VerifyMac() err = %v, want %v", err, wantErr)
+	}
+}
diff --git a/go/mac/mac.go b/go/mac/mac.go
index fafe49d..5a10809 100644
--- a/go/mac/mac.go
+++ b/go/mac/mac.go
@@ -25,12 +25,16 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 )
 
 func init() {
 	if err := registry.RegisterKeyManager(new(hmacKeyManager)); err != nil {
 		panic(fmt.Sprintf("mac.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(hmacTypeURL); err != nil {
+		panic(fmt.Sprintf("mac.init() failed: %v", err))
+	}
 	if err := registry.RegisterKeyManager(new(aescmacKeyManager)); err != nil {
 		panic(fmt.Sprintf("mac.init() failed: %v", err))
 	}
diff --git a/go/mac/mac_factory.go b/go/mac/mac_factory.go
index 25b7d39..e7da300 100644
--- a/go/mac/mac_factory.go
+++ b/go/mac/mac_factory.go
@@ -21,8 +21,10 @@
 
 	"github.com/google/tink/go/core/cryptofmt"
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/tink"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -33,33 +35,28 @@
 )
 
 // New creates a MAC primitive from the given keyset handle.
-func New(h *keyset.Handle) (tink.MAC, error) {
-	return NewWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewWithKeyManager creates a MAC primitive from the given keyset handle and a custom key manager.
-//
-// Deprecated: Use [New].
-func NewWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.MAC, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func New(handle *keyset.Handle) (tink.MAC, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("mac_factory: cannot obtain primitive set: %s", err)
 	}
-
 	return newWrappedMAC(ps)
 }
 
 // wrappedMAC is a MAC implementation that uses the underlying primitive set to compute and
 // verify MACs.
 type wrappedMAC struct {
-	ps *primitiveset.PrimitiveSet
+	ps            *primitiveset.PrimitiveSet
+	computeLogger monitoring.Logger
+	verifyLogger  monitoring.Logger
 }
 
+var _ (tink.MAC) = (*wrappedMAC)(nil)
+
 func newWrappedMAC(ps *primitiveset.PrimitiveSet) (*wrappedMAC, error) {
 	if _, ok := (ps.Primary.Primitive).(tink.MAC); !ok {
 		return nil, fmt.Errorf("mac_factory: not a MAC primitive")
 	}
-
 	for _, primitives := range ps.Entries {
 		for _, p := range primitives {
 			if _, ok := (p.Primitive).(tink.MAC); !ok {
@@ -67,11 +64,43 @@
 			}
 		}
 	}
+	computeLogger, verifyLogger, err := createLoggers(ps)
+	if err != nil {
+		return nil, err
+	}
+	return &wrappedMAC{
+		ps:            ps,
+		computeLogger: computeLogger,
+		verifyLogger:  verifyLogger,
+	}, nil
+}
 
-	ret := new(wrappedMAC)
-	ret.ps = ps
-
-	return ret, nil
+func createLoggers(ps *primitiveset.PrimitiveSet) (monitoring.Logger, monitoring.Logger, error) {
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, &monitoringutil.DoNothingLogger{}, nil
+	}
+	client := internalregistry.GetMonitoringClient()
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, nil, err
+	}
+	computeLogger, err := client.NewLogger(&monitoring.Context{
+		Primitive:   "mac",
+		APIFunction: "compute",
+		KeysetInfo:  keysetInfo,
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+	verifyLogger, err := client.NewLogger(&monitoring.Context{
+		Primitive:   "mac",
+		APIFunction: "verify",
+		KeysetInfo:  keysetInfo,
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+	return computeLogger, verifyLogger, nil
 }
 
 // ComputeMAC calculates a MAC over the given data using the primary primitive
@@ -85,6 +114,7 @@
 	if m.ps.Primary.PrefixType == tinkpb.OutputPrefixType_LEGACY {
 		d := data
 		if len(d) >= maxInt {
+			m.computeLogger.LogFailure()
 			return nil, fmt.Errorf("mac_factory: data too long")
 		}
 		data = make([]byte, 0, len(d)+1)
@@ -93,9 +123,17 @@
 	}
 	mac, err := primitive.ComputeMAC(data)
 	if err != nil {
+		m.computeLogger.LogFailure()
 		return nil, err
 	}
-	return append([]byte(primary.Prefix), mac...), nil
+	m.computeLogger.Log(primary.KeyID, len(data))
+	if len(primary.Prefix) == 0 {
+		return mac, nil
+	}
+	output := make([]byte, 0, len(primary.Prefix)+len(mac))
+	output = append(output, primary.Prefix...)
+	output = append(output, mac...)
+	return output, nil
 }
 
 var errInvalidMAC = fmt.Errorf("mac_factory: invalid mac")
@@ -107,6 +145,7 @@
 	// clearly insecure, thus should be discouraged.
 	prefixSize := cryptofmt.NonRawPrefixSize
 	if len(mac) <= prefixSize {
+		m.verifyLogger.LogFailure()
 		return errInvalidMAC
 	}
 
@@ -119,11 +158,12 @@
 			entry := entries[i]
 			p, ok := (entry.Primitive).(tink.MAC)
 			if !ok {
-				return fmt.Errorf("mac_factory: not an MAC primitive")
+				return fmt.Errorf("mac_factory internal error: not a MAC primitive")
 			}
 			if entry.PrefixType == tinkpb.OutputPrefixType_LEGACY {
 				d := data
 				if len(d) >= maxInt {
+					m.verifyLogger.LogFailure()
 					return fmt.Errorf("mac_factory: data too long")
 				}
 				data = make([]byte, 0, len(d)+1)
@@ -131,6 +171,7 @@
 				data = append(data, byte(0))
 			}
 			if err = p.VerifyMAC(macNoPrefix, data); err == nil {
+				m.verifyLogger.Log(entry.KeyID, len(data))
 				return nil
 			}
 		}
@@ -142,15 +183,17 @@
 		for i := 0; i < len(entries); i++ {
 			p, ok := (entries[i].Primitive).(tink.MAC)
 			if !ok {
-				return fmt.Errorf("mac_factory: not an MAC primitive")
+				return fmt.Errorf("mac_factory internal error: not a MAC primitive")
 			}
 
 			if err = p.VerifyMAC(mac, data); err == nil {
+				m.verifyLogger.Log(entries[i].KeyID, len(data))
 				return nil
 			}
 		}
 	}
 
 	// nothing worked
+	m.verifyLogger.LogFailure()
 	return errInvalidMAC
 }
diff --git a/go/mac/mac_factory_test.go b/go/mac/mac_factory_test.go
index 5a58888..b1b3f94 100644
--- a/go/mac/mac_factory_test.go
+++ b/go/mac/mac_factory_test.go
@@ -17,15 +17,26 @@
 package mac_test
 
 import (
+	"bytes"
 	"fmt"
 	"strings"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/cryptofmt"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/mac/internal/mactest"
 	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/signature"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testing/fakemonitoring"
 	"github.com/google/tink/go/testkeyset"
 	"github.com/google/tink/go/testutil"
 	"github.com/google/tink/go/tink"
@@ -181,8 +192,7 @@
 	}
 }
 
-func verifyMacPrimitive(computePrimitive tink.MAC, verifyPrimitive tink.MAC,
-	expectedPrefix string, tagSize uint32) error {
+func verifyMacPrimitive(computePrimitive, verifyPrimitive tink.MAC, expectedPrefix string, tagSize uint32) error {
 	data := []byte("hello")
 	tag, err := computePrimitive.ComputeMAC(data)
 	if err != nil {
@@ -242,3 +252,410 @@
 		t.Fatalf("calling New() with good *keyset.Handle failed: %s", err)
 	}
 }
+
+func TestPrimitiveFactoryMonitoringWithoutAnnotationsDoesNotLog(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(mac.HMACSHA256Tag256KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle(mac.HMACSHA256Tag256KeyTemplate()) err = %v, want nil", err)
+	}
+	p, err := mac.New(kh)
+	if err != nil {
+		t.Fatalf("mac.New() err = %v, want nil", err)
+	}
+	data := []byte("data")
+	tag, err := p.ComputeMAC(data)
+	if err != nil {
+		t.Fatalf("p.ComputeMAC() err = %v, want nil", err)
+	}
+	if err := p.VerifyMAC(tag, data); err != nil {
+		t.Fatalf("p.Verify() err = %v, want nil", err)
+	}
+	got := client.Events()
+	if len(got) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(got))
+	}
+}
+
+func TestFactoryWithMonitoringPrimitiveWithMultipleKeysLogsComputeVerify(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	manager := keyset.NewManager()
+	keyIDs := make([]uint32, 4, 4)
+	var err error
+	for i, tm := range []*tinkpb.KeyTemplate{
+		mac.HMACSHA256Tag256KeyTemplate(),
+		mac.HMACSHA256Tag128KeyTemplate(),
+		mac.HMACSHA512Tag512KeyTemplate(),
+		mac.AESCMACTag128KeyTemplate(),
+	} {
+		keyIDs[i], err = manager.Add(tm)
+		if err != nil {
+			t.Fatalf("manager.Add() err = %v, want nil", err)
+		}
+	}
+	if err := manager.SetPrimary(keyIDs[1]); err != nil {
+		t.Fatalf("manager.SetPrimary(%d) err = %v, want nil", keyIDs[1], err)
+	}
+	if err := manager.Disable(keyIDs[0]); err != nil {
+		t.Fatalf("manager.Disable(%d) err = %v, want nil", keyIDs[0], err)
+	}
+	kh, err := manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := mac.New(mh)
+	if err != nil {
+		t.Fatalf("mac.New() err = %v, want nil", err)
+	}
+	data := random.GetRandomBytes(50)
+	tag, err := p.ComputeMAC(data)
+	if err != nil {
+		t.Fatalf("p.ComputeMAC() err = %v, want nil", err)
+	}
+	if err := p.VerifyMAC(tag, data); err != nil {
+		t.Fatalf("p.VerifyMAC() err = %v, want nil", err)
+	}
+	failures := len(client.Failures())
+	if failures != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", failures)
+	}
+	got := client.Events()
+	wantKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: kh.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HmacKey",
+				KeyPrefix: "TINK",
+			},
+			{
+				KeyID:     keyIDs[2],
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HmacKey",
+				KeyPrefix: "TINK",
+			},
+			{
+				KeyID:     keyIDs[3],
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.AesCmacKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+			Context: monitoring.NewContext(
+				"mac",
+				"compute",
+				wantKeysetInfo,
+			),
+		},
+		{
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+			Context: monitoring.NewContext(
+				"mac",
+				"verify",
+				wantKeysetInfo,
+			),
+		},
+	}
+	// sort by keyID to avoid non deterministic order.
+	entryLessFunc := func(a, b *monitoring.Entry) bool {
+		return a.KeyID < b.KeyID
+	}
+	if !cmp.Equal(got, want, cmpopts.SortSlices(entryLessFunc)) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsComputeFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	// Since this key type will be registered in the registry,
+	// we create a very unique typeURL to avoid colliding with other tests.
+	typeURL := "TestPrimitiveFactoryWithMonitoringComputeFailureIsLogged"
+	template := &tinkpb.KeyTemplate{
+		TypeUrl:          typeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_LEGACY,
+	}
+	km := &stubkeymanager.StubKeyManager{
+		URL:  typeURL,
+		Prim: &mactest.AlwaysFailingMAC{Error: fmt.Errorf("system failure")},
+		Key:  &hmacpb.HmacKey{},
+		KeyData: &tinkpb.KeyData{
+			TypeUrl: template.TypeUrl,
+			Value:   []byte("some_data"),
+		},
+	}
+	if err := registry.RegisterKeyManager(km); err != nil {
+		t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	m, err := mac.New(mh)
+	if err != nil {
+		t.Fatalf("mac.New() err = %v, want nil", err)
+	}
+	if _, err := m.ComputeMAC([]byte("some_data")); err == nil {
+		t.Fatalf("m.ComputeMAC() err = nil, want non-nil error")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"mac",
+				"compute",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   typeURL,
+							KeyPrefix: "LEGACY",
+						},
+					},
+				),
+			),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsVerifyFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(mac.HMACSHA256Tag256KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	m, err := mac.New(mh)
+	if err != nil {
+		t.Fatalf("mac.New() err = %v, want nil", err)
+	}
+	if err := m.VerifyMAC(nil, nil); err == nil {
+		t.Fatalf("m.VerifyMAC() err = nil, want non-nil error")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"mac",
+				"verify",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.HmacKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsMultiplePrimitivesLogOperations(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	templates := []*tinkpb.KeyTemplate{
+		mac.HMACSHA256Tag256KeyTemplate(),
+		mac.AESCMACTag128KeyTemplate()}
+	handles := make([]*keyset.Handle, len(templates))
+	var err error
+	annotations := map[string]string{"foo": "bar"}
+	for i, tm := range templates {
+		handles[i], err = keyset.NewHandle(tm)
+		if err != nil {
+			t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+		}
+		// Annotations are only supported throught the `insecurecleartextkeyset` API.
+		buff := &bytes.Buffer{}
+		if err := insecurecleartextkeyset.Write(handles[i], keyset.NewBinaryWriter(buff)); err != nil {
+			t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+		}
+		mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+		if err != nil {
+			t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+		}
+		p, err := mac.New(mh)
+		if err != nil {
+			t.Fatalf("mac.New() err = %v, want nil", err)
+		}
+		if _, err := p.ComputeMAC([]byte(tm.GetTypeUrl())); err != nil {
+			t.Fatalf("p.ComputeMAC() err = %v, want nil", err)
+		}
+	}
+	got := client.Events()
+	want := []*fakemonitoring.LogEvent{
+		{
+			KeyID:    handles[0].KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(templates[0].GetTypeUrl()),
+			Context: monitoring.NewContext(
+				"mac",
+				"compute",
+				monitoring.NewKeysetInfo(
+					annotations,
+					handles[0].KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     handles[0].KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.HmacKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+		{
+			KeyID:    handles[1].KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(templates[1].GetTypeUrl()),
+			Context: monitoring.NewContext(
+				"mac",
+				"compute",
+				monitoring.NewKeysetInfo(
+					annotations,
+					handles[1].KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     handles[1].KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.AesCmacKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsComputeVerifyLogs(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("registry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(mac.HMACSHA256Tag256KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	// Annotations are only supported throught the `insecurecleartextkeyset` API.
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	p, err := mac.New(mh)
+	if err != nil {
+		t.Fatalf("mac.New() err = %v, want nil", err)
+	}
+	data := []byte("data")
+	tag, err := p.ComputeMAC(data)
+	if err != nil {
+		t.Fatalf("p.ComputeMAC() err = %v, want nil", err)
+	}
+	if err := p.VerifyMAC(tag, data); err != nil {
+		t.Fatalf("p.Verify() err = %v, want nil", err)
+	}
+	got := client.Events()
+	wantKeysetInfo := monitoring.NewKeysetInfo(
+		annotations,
+		kh.KeysetInfo().GetPrimaryKeyId(),
+		[]*monitoring.Entry{
+			{
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HmacKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	)
+	want := []*fakemonitoring.LogEvent{
+		&fakemonitoring.LogEvent{
+			Context:  monitoring.NewContext("mac", "compute", wantKeysetInfo),
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+		&fakemonitoring.LogEvent{
+			Context:  monitoring.NewContext("mac", "verify", wantKeysetInfo),
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
diff --git a/go/mac/mac_init_test.go b/go/mac/mac_init_test.go
new file mode 100644
index 0000000..e22109c
--- /dev/null
+++ b/go/mac/mac_init_test.go
@@ -0,0 +1,37 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package mac_test
+
+import (
+	"testing"
+
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestMacInit(t *testing.T) {
+	// Check that the HMAC key manager is in the global registry.
+	_, err := registry.GetKeyManager(testutil.HMACTypeURL)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+	// Check that the AES CMAC key manager is in the global registry.
+	_, err = registry.GetKeyManager(testutil.AESCMACTypeURL)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+}
diff --git a/go/mac/mac_key_templates.go b/go/mac/mac_key_templates.go
index 8949bc4..6877254 100644
--- a/go/mac/mac_key_templates.go
+++ b/go/mac/mac_key_templates.go
@@ -17,7 +17,10 @@
 package mac
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	cmacpb "github.com/google/tink/go/proto/aes_cmac_go_proto"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	hmacpb "github.com/google/tink/go/proto/hmac_go_proto"
@@ -66,9 +69,7 @@
 }
 
 // createHMACKeyTemplate creates a new KeyTemplate for HMAC using the given parameters.
-func createHMACKeyTemplate(keySize uint32,
-	tagSize uint32,
-	hashType commonpb.HashType) *tinkpb.KeyTemplate {
+func createHMACKeyTemplate(keySize, tagSize uint32, hashType commonpb.HashType) *tinkpb.KeyTemplate {
 	params := hmacpb.HmacParams{
 		Hash:    hashType,
 		TagSize: tagSize,
@@ -77,7 +78,10 @@
 		Params:  &params,
 		KeySize: keySize,
 	}
-	serializedFormat, _ := proto.Marshal(&format)
+	serializedFormat, err := proto.Marshal(&format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          hmacTypeURL,
 		Value:            serializedFormat,
@@ -94,7 +98,10 @@
 		Params:  &params,
 		KeySize: keySize,
 	}
-	serializedFormat, _ := proto.Marshal(&format)
+	serializedFormat, err := proto.Marshal(&format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          cmacTypeURL,
 		Value:            serializedFormat,
diff --git a/go/mac/mac_test.go b/go/mac/mac_test.go
index b5fc9ca..cabde4b 100644
--- a/go/mac/mac_test.go
+++ b/go/mac/mac_test.go
@@ -16,54 +16,73 @@
 
 package mac_test
 
+// [START mac-example]
+
 import (
-	"encoding/base64"
+	"bytes"
 	"fmt"
 	"log"
-	"testing"
 
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
-	"github.com/google/tink/go/testutil"
 )
 
-func TestMacInit(t *testing.T) {
-	_, err := registry.GetKeyManager(testutil.HMACTypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-	_, err = registry.GetKeyManager(testutil.AESCMACTypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-}
-
 func Example() {
-	kh, err := keyset.NewHandle(mac.HMACSHA256Tag256KeyTemplate())
-	if err != nil {
-		log.Fatal(err)
-	}
+	// A keyset created with "tinkey create-keyset --key-template=HMAC_SHA256_128BITTAG".
+	// Note that this keyset has the secret key information in cleartext.
+	jsonKeyset := `{
+			"key": [{
+					"keyData": {
+							"keyMaterialType":
+									"SYMMETRIC",
+							"typeUrl":
+									"type.googleapis.com/google.crypto.tink.HmacKey",
+							"value":
+									"EgQIAxAQGiA0LQjovcydWhVQV3k8W9ZSRkd7Ei4Y/TRWApE8guwV4Q=="
+					},
+					"keyId": 1892702217,
+					"outputPrefixType": "TINK",
+					"status": "ENABLED"
+			}],
+			"primaryKeyId": 1892702217
+	}`
 
-	// TODO: save the keyset to a safe location. DO NOT hardcode it in source code.
+	// Create a keyset handle from the cleartext keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the exposure of accessing the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use a insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
 	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
 	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
-
-	m, err := mac.New(kh)
+	keysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(jsonKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	msg := []byte("this data needs to be authenticated")
-	tag, err := m.ComputeMAC(msg)
+	// Retrieve the MAC primitive we want to use from the keyset handle.
+	primitive, err := mac.New(keysetHandle)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	if m.VerifyMAC(tag, msg); err != nil {
+	// Use the primitive to create a MAC tag for some data. In this case the primary
+	// key of the keyset will be used (which is also the only key in this example).
+	data := []byte("data")
+	tag, err := primitive.ComputeMAC(data)
+	if err != nil {
 		log.Fatal(err)
 	}
 
-	fmt.Printf("Message: %s\n", msg)
-	fmt.Printf("Authentication tag: %s\n", base64.StdEncoding.EncodeToString(tag))
+	// Use the primitive to verify the tag. VerifyMAC finds the correct key in
+	// the keyset. If no key is found or verification fails, it returns an error.
+	err = primitive.VerifyMAC(tag, data)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("tag is valid")
+	// Output: tag is valid
 }
+
+// [END mac-example]
diff --git a/go/monitoring/monitoring.go b/go/monitoring/monitoring.go
index 2071e1e..aced15f 100644
--- a/go/monitoring/monitoring.go
+++ b/go/monitoring/monitoring.go
@@ -33,11 +33,24 @@
 	DoNotUse KeyStatus = 20
 )
 
+func (status KeyStatus) String() string {
+	switch status {
+	case Enabled:
+		return "ENABLED"
+	case Disabled:
+		return "DISABLED"
+	case Destroyed:
+		return "DESTROYED"
+	}
+	return "UNKNOWN"
+}
+
 // Entry represents each entry inside a Keyset.
 type Entry struct {
-	Status         KeyStatus
-	KeyID          uint32
-	FormatAsString string
+	Status    KeyStatus
+	KeyID     uint32
+	KeyType   string
+	KeyPrefix string
 }
 
 // KeysetInfo represents a keyset in a certain point in time for the
diff --git a/go/prf/BUILD.bazel b/go/prf/BUILD.bazel
index dc81201..f0d2290 100644
--- a/go/prf/BUILD.bazel
+++ b/go/prf/BUILD.bazel
@@ -19,7 +19,11 @@
     deps = [
         "//core/primitiveset",
         "//core/registry",
+        "//internal/internalregistry",
+        "//internal/monitoringutil",
+        "//internal/tinkerror",
         "//keyset",
+        "//monitoring",
         "//prf/subtle",
         "//proto/aes_cmac_prf_go_proto",
         "//proto/common_go_proto",
@@ -44,8 +48,11 @@
     deps = [
         ":prf",
         "//core/registry",
+        "//insecurecleartextkeyset",
+        "//internal/internalregistry",
         "//keyset",
         "//mac",
+        "//monitoring",
         "//prf/subtle",
         "//proto/aes_cmac_prf_go_proto",
         "//proto/common_go_proto",
@@ -53,7 +60,10 @@
         "//proto/hmac_prf_go_proto",
         "//proto/tink_go_proto",
         "//subtle/random",
+        "//testing/fakemonitoring",
         "//testutil",
+        "@com_github_google_go_cmp//cmp",
+        "@com_github_google_go_cmp//cmp/cmpopts",
         "@org_golang_google_protobuf//proto",
     ],
 )
diff --git a/go/prf/hkdf_prf_key_manager.go b/go/prf/hkdf_prf_key_manager.go
index da5c779..2f9ceba 100644
--- a/go/prf/hkdf_prf_key_manager.go
+++ b/go/prf/hkdf_prf_key_manager.go
@@ -19,8 +19,10 @@
 import (
 	"errors"
 	"fmt"
+	"io"
 
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/prf/subtle"
 	"github.com/google/tink/go/subtle/random"
@@ -40,6 +42,9 @@
 // hkdfprfKeyManager generates new HKDF PRF keys and produces new instances of HKDF.
 type hkdfprfKeyManager struct{}
 
+// Assert that hkdfprfKeyManager implements the KeyManager interface.
+var _ registry.KeyManager = (*hkdfprfKeyManager)(nil)
+
 // Primitive constructs a HKDF instance for the given serialized HKDFKey.
 func (km *hkdfprfKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
 	if len(serializedKey) == 0 {
@@ -95,7 +100,7 @@
 	return &tinkpb.KeyData{
 		TypeUrl:         hkdfprfTypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
@@ -109,6 +114,39 @@
 	return hkdfprfTypeURL
 }
 
+// KeyMaterialType returns the key material type of this KeyManager.
+func (km *hkdfprfKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+func (km *hkdfprfKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errInvalidHKDFPRFKeyFormat
+	}
+	keyFormat := new(hkdfpb.HkdfPrfKeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidHKDFPRFKeyFormat
+	}
+	if err := km.validateKeyFormat(keyFormat); err != nil {
+		return nil, fmt.Errorf("hkdf_prf_key_manager: invalid key format: %s", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), hkdfprfKeyVersion); err != nil {
+		return nil, fmt.Errorf("hkdf_prf_key_manager: invalid key version: %s", err)
+	}
+
+	keyValue := make([]byte, keyFormat.GetKeySize())
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("hkdf_prf_key_manager: not enough pseudorandomness given")
+	}
+
+	return &hkdfpb.HkdfPrfKey{
+		Version:  hkdfprfKeyVersion,
+		Params:   keyFormat.Params,
+		KeyValue: keyValue,
+	}, nil
+}
+
 // validateKey validates the given HKDFPRFKey. It only validates the version of the
 // key because other parameters will be validated in primitive construction.
 func (km *hkdfprfKeyManager) validateKey(key *hkdfpb.HkdfPrfKey) error {
diff --git a/go/prf/hkdf_prf_key_manager_test.go b/go/prf/hkdf_prf_key_manager_test.go
index c45a1ed..9e3df00 100644
--- a/go/prf/hkdf_prf_key_manager_test.go
+++ b/go/prf/hkdf_prf_key_manager_test.go
@@ -17,12 +17,15 @@
 package prf_test
 
 import (
+	"bytes"
 	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/prf"
 	"github.com/google/tink/go/prf/subtle"
 	"github.com/google/tink/go/subtle/random"
@@ -207,6 +210,212 @@
 	}
 }
 
+func TestHKDFKeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HKDFPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HKDFPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestHKDFDeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HKDFPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HKDFPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	var keySize uint32 = 32
+	for _, test := range []struct {
+		name     string
+		hashType commonpb.HashType
+		salt     []byte
+	}{
+		{
+			name:     "SHA256",
+			hashType: commonpb.HashType_SHA256,
+			salt:     make([]byte, 0),
+		},
+		{
+			name:     "SHA256/salt",
+			hashType: commonpb.HashType_SHA256,
+			salt:     []byte{0x01, 0x03, 0x42},
+		},
+		{
+			name:     "SHA512",
+			hashType: commonpb.HashType_SHA512,
+			salt:     make([]byte, 0),
+		},
+		{
+			name:     "SHA512/salt",
+			hashType: commonpb.HashType_SHA512,
+			salt:     []byte{0x01, 0x03, 0x42},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat := testutil.NewHKDFPRFKeyFormat(test.hashType, test.salt)
+			serializedKeyFormat, err := proto.Marshal(keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+			}
+
+			rand := random.GetRandomBytes(keySize)
+			buf := &bytes.Buffer{}
+			buf.Write(rand) // never returns a non-nil error
+
+			k, err := keyManager.DeriveKey(serializedKeyFormat, buf)
+			if err != nil {
+				t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+			}
+			key := k.(*hkdfpb.HkdfPrfKey)
+			if got, want := len(key.GetKeyValue()), int(keySize); got != want {
+				t.Errorf("key length = %d, want %d", got, want)
+			}
+			if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+				t.Errorf("incorrect derived key: diff = %v", diff)
+			}
+		})
+	}
+}
+
+func TestHKDFDeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HKDFPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HKDFPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	var keySize uint32 = 32
+	validKeyFormat := &hkdfpb.HkdfPrfKeyFormat{
+		Params:  testutil.NewHKDFPRFParams(commonpb.HashType_SHA256, make([]byte, 0)),
+		KeySize: keySize,
+		Version: 0,
+	}
+	serializedValidKeyFormat, err := proto.Marshal(validKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKeyFormat, err)
+	}
+	buf := bytes.NewBuffer(random.GetRandomBytes(keySize))
+	if _, err := keyManager.DeriveKey(serializedValidKeyFormat, buf); err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name      string
+		keyFormat *hkdfpb.HkdfPrfKeyFormat
+		randLen   uint32
+	}{
+		{
+			name: "invalid key size",
+			keyFormat: &hkdfpb.HkdfPrfKeyFormat{
+				Params:  validKeyFormat.GetParams(),
+				KeySize: 16,
+				Version: validKeyFormat.GetVersion(),
+			},
+			randLen: keySize,
+		},
+		{
+			name:      "not enough randomness",
+			keyFormat: validKeyFormat,
+			randLen:   16,
+		},
+		{
+			name: "invalid version",
+			keyFormat: &hkdfpb.HkdfPrfKeyFormat{
+				Params:  validKeyFormat.GetParams(),
+				KeySize: validKeyFormat.GetKeySize(),
+				Version: 100000,
+			},
+			randLen: keySize,
+		},
+		{
+			name:      "empty key format",
+			keyFormat: &hkdfpb.HkdfPrfKeyFormat{},
+			randLen:   keySize,
+		},
+		{
+			name:    "nil key format",
+			randLen: keySize,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			serializedKeyFormat, err := proto.Marshal(test.keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", test.keyFormat, err)
+			}
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.randLen))
+			if _, err := keyManager.DeriveKey(serializedKeyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHKDFDeriveKeyFailsWithMalformedSerializedKeyFormat(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HKDFPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HKDFPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	var keySize uint32 = 32
+	malformedSerializedKeyFormat := random.GetRandomBytes(
+		uint32(
+			proto.Size(&hkdfpb.HkdfPrfKeyFormat{
+				Params:  testutil.NewHKDFPRFParams(commonpb.HashType_SHA256, make([]byte, 0)),
+				KeySize: keySize,
+				Version: 0,
+			})))
+
+	buf := bytes.NewBuffer(random.GetRandomBytes(keySize))
+	if _, err := keyManager.DeriveKey(malformedSerializedKeyFormat, buf); err == nil {
+		t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+	}
+}
+
+func TestAESGCMDeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HKDFPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HKDFPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(testutil.NewHKDFPRFKeyFormat(commonpb.HashType_SHA256, []byte("salty")))
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	var keySize uint32 = 32
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(keySize))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(keySize - 1))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func genInvalidHKDFKeys() []proto.Message {
 	badVersionKey := testutil.NewHKDFPRFKey(commonpb.HashType_SHA256, make([]byte, 0))
 	badVersionKey.Version++
diff --git a/go/prf/hmac_prf_key_manager.go b/go/prf/hmac_prf_key_manager.go
index e5d8917..ce8070f 100644
--- a/go/prf/hmac_prf_key_manager.go
+++ b/go/prf/hmac_prf_key_manager.go
@@ -19,6 +19,7 @@
 import (
 	"errors"
 	"fmt"
+	"io"
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/keyset"
@@ -72,11 +73,10 @@
 	if err := km.validateKeyFormat(keyFormat); err != nil {
 		return nil, fmt.Errorf("hmac_prf_key_manager: invalid key format: %s", err)
 	}
-	keyValue := random.GetRandomBytes(keyFormat.KeySize)
 	return &hmacpb.HmacPrfKey{
 		Version:  hmacprfKeyVersion,
 		Params:   keyFormat.Params,
-		KeyValue: keyValue,
+		KeyValue: random.GetRandomBytes(keyFormat.KeySize),
 	}, nil
 }
 
@@ -95,7 +95,7 @@
 	return &tinkpb.KeyData{
 		TypeUrl:         hmacprfTypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
@@ -121,6 +121,38 @@
 	return subtle.ValidateHMACPRFParams(hash, keySize)
 }
 
+// KeyMaterialType returns the key material type of this key manager.
+func (km *hmacprfKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+func (km *hmacprfKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errInvalidHMACPRFKeyFormat
+	}
+	keyFormat := new(hmacpb.HmacPrfKeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidHMACPRFKeyFormat
+	}
+	if err := km.validateKeyFormat(keyFormat); err != nil {
+		return nil, fmt.Errorf("hmac_key_manager: invalid key format: %v", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), hmacprfKeyVersion); err != nil {
+		return nil, fmt.Errorf("hmac_key_manager: invalid key version: %s", err)
+	}
+
+	keyValue := make([]byte, keyFormat.GetKeySize())
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("hmac_key_manager: not enough pseudorandomness given")
+	}
+	return &hmacpb.HmacPrfKey{
+		Version:  hmacprfKeyVersion,
+		Params:   keyFormat.GetParams(),
+		KeyValue: keyValue,
+	}, nil
+}
+
 // validateKeyFormat validates the given HMACKeyFormat
 func (km *hmacprfKeyManager) validateKeyFormat(format *hmacpb.HmacPrfKeyFormat) error {
 	if format.Params == nil {
diff --git a/go/prf/hmac_prf_key_manager_test.go b/go/prf/hmac_prf_key_manager_test.go
index 3b29716..62acc0e 100644
--- a/go/prf/hmac_prf_key_manager_test.go
+++ b/go/prf/hmac_prf_key_manager_test.go
@@ -17,12 +17,15 @@
 package prf_test
 
 import (
+	"bytes"
 	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/prf"
 	"github.com/google/tink/go/prf/subtle"
 	"github.com/google/tink/go/subtle/random"
@@ -207,6 +210,191 @@
 	}
 }
 
+func TestHMACKeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestHMACDeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&hmacpb.HmacPrfKeyFormat{
+		Version: testutil.HMACPRFKeyVersion,
+		KeySize: 16,
+		Params:  &hmacpb.HmacPrfParams{Hash: commonpb.HashType_SHA256},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	rand := random.GetRandomBytes(16)
+	buf := &bytes.Buffer{}
+	buf.Write(rand) // Never returns a non-nil error.
+	k, err := keyManager.DeriveKey(keyFormat, buf)
+	if err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+	key := k.(*hmacpb.HmacPrfKey)
+	if got, want := len(key.GetKeyValue()), 16; got != want {
+		t.Errorf("key length = %d, want %d", got, want)
+	}
+	if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+		t.Errorf("incorrect derived key: diff = %v", diff)
+	}
+}
+
+func TestHMACDeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	validKeyFormat := &hmacpb.HmacPrfKeyFormat{
+		Version: testutil.HMACPRFKeyVersion,
+		KeySize: 16,
+		Params:  &hmacpb.HmacPrfParams{Hash: commonpb.HashType_SHA256},
+	}
+	serializedValidKeyFormat, err := proto.Marshal(validKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKeyFormat, err)
+	}
+	buf := bytes.NewBuffer(random.GetRandomBytes(validKeyFormat.KeySize))
+	if _, err := keyManager.DeriveKey(serializedValidKeyFormat, buf); err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name    string
+		version uint32
+		keySize uint32
+		hash    commonpb.HashType
+	}{
+		{
+			name:    "invalid version",
+			version: 10,
+			keySize: validKeyFormat.KeySize,
+			hash:    validKeyFormat.Params.Hash,
+		},
+		{
+			name:    "invalid key size",
+			version: validKeyFormat.Version,
+			keySize: 10,
+			hash:    validKeyFormat.Params.Hash,
+		},
+		{
+			name:    "invalid hash",
+			version: validKeyFormat.Version,
+			keySize: validKeyFormat.KeySize,
+			hash:    commonpb.HashType_UNKNOWN_HASH,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat, err := proto.Marshal(&hmacpb.HmacPrfKeyFormat{
+				Version: test.version,
+				KeySize: test.keySize,
+				Params:  &hmacpb.HmacPrfParams{Hash: test.hash},
+			})
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.keySize))
+			if _, err := keyManager.DeriveKey(keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHMACDeriveKeyFailsWithMalformedKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "nil",
+			keyFormat: nil,
+		},
+		{
+			name:      "empty",
+			keyFormat: []byte{},
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(16))
+			if _, err := keyManager.DeriveKey(test.keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestHMACDeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.HMACPRFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.HMACPRFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&hmacpb.HmacPrfKeyFormat{
+		Version: testutil.HMACPRFKeyVersion,
+		KeySize: 16,
+		Params:  &hmacpb.HmacPrfParams{Hash: commonpb.HashType_SHA256},
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(16))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(15))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func genInvalidHMACPRFKeys() []proto.Message {
 	badVersionKey := testutil.NewHMACPRFKey(commonpb.HashType_SHA256)
 	badVersionKey.Version++
diff --git a/go/prf/prf_key_templates.go b/go/prf/prf_key_templates.go
index 740240f..5c01c5a 100644
--- a/go/prf/prf_key_templates.go
+++ b/go/prf/prf_key_templates.go
@@ -17,7 +17,10 @@
 package prf
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	cmacpb "github.com/google/tink/go/proto/aes_cmac_prf_go_proto"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	hkdfpb "github.com/google/tink/go/proto/hkdf_prf_go_proto"
@@ -64,7 +67,10 @@
 		Params:  &params,
 		KeySize: keySize,
 	}
-	serializedFormat, _ := proto.Marshal(&format)
+	serializedFormat, err := proto.Marshal(&format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          hmacprfTypeURL,
 		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
@@ -82,7 +88,10 @@
 		Params:  &params,
 		KeySize: keySize,
 	}
-	serializedFormat, _ := proto.Marshal(&format)
+	serializedFormat, err := proto.Marshal(&format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          hkdfprfTypeURL,
 		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
@@ -95,7 +104,10 @@
 	format := cmacpb.AesCmacPrfKeyFormat{
 		KeySize: keySize,
 	}
-	serializedFormat, _ := proto.Marshal(&format)
+	serializedFormat, err := proto.Marshal(&format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          aescmacprfTypeURL,
 		OutputPrefixType: tinkpb.OutputPrefixType_RAW,
diff --git a/go/prf/prf_set.go b/go/prf/prf_set.go
index cd1d2e4..1039d38 100644
--- a/go/prf/prf_set.go
+++ b/go/prf/prf_set.go
@@ -21,18 +21,21 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/monitoring"
 )
 
 // The PRF interface is an abstraction for an element of a pseudo random
 // function family, selected by a key. It has the following property:
-//   * It is deterministic. PRF.compute(input, length) will always return the
+//   - It is deterministic. PRF.compute(input, length) will always return the
 //     same output if the same key is used. PRF.compute(input, length1) will be
 //     a prefix of PRF.compute(input, length2) if length1 < length2 and the same
 //     key is used.
-//   * It is indistinguishable from a random function:
+//   - It is indistinguishable from a random function:
 //     Given the evaluation of n different inputs, an attacker cannot
 //     distinguish between the PRF and random bytes on an input different from
 //     the n that are known.
+//
 // Use cases for PRF are deterministic redaction of PII, keyed hash functions,
 // creating sub IDs that do not allow joining with the original dataset without
 // knowing the key.
@@ -55,6 +58,24 @@
 	ComputePRF(input []byte, outputLength uint32) ([]byte, error)
 }
 
+type monitoredPRF struct {
+	prf    PRF
+	keyID  uint32
+	logger monitoring.Logger
+}
+
+var _ PRF = (*monitoredPRF)(nil)
+
+func (w *monitoredPRF) ComputePRF(input []byte, outputLength uint32) ([]byte, error) {
+	p, err := w.prf.ComputePRF(input, outputLength)
+	if err != nil {
+		w.logger.LogFailure()
+		return nil, err
+	}
+	w.logger.Log(w.keyID, len(input))
+	return p, nil
+}
+
 // Set is a set of PRFs. A Tink Keyset can be converted into a set of PRFs using this primitive. Every
 // key in the keyset corresponds to a PRF in the prf.Set.
 // Every PRF in the set is given an ID, which is the same ID as the key id in
@@ -79,9 +100,15 @@
 	if err := registry.RegisterKeyManager(new(hmacprfKeyManager)); err != nil {
 		panic(fmt.Sprintf("prf.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(hmacprfTypeURL); err != nil {
+		panic(fmt.Sprintf("prf.init() failed: %v", err))
+	}
 	if err := registry.RegisterKeyManager(new(hkdfprfKeyManager)); err != nil {
 		panic(fmt.Sprintf("prf.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(hkdfprfTypeURL); err != nil {
+		panic(fmt.Sprintf("prf.init() failed: %v", err))
+	}
 	if err := registry.RegisterKeyManager(new(aescmacprfKeyManager)); err != nil {
 		panic(fmt.Sprintf("prf.init() failed: %v", err))
 	}
diff --git a/go/prf/prf_set_factory.go b/go/prf/prf_set_factory.go
index e8d80b5..49c0ce3 100644
--- a/go/prf/prf_set_factory.go
+++ b/go/prf/prf_set_factory.go
@@ -20,24 +20,18 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 )
 
 // NewPRFSet creates a prf.Set primitive from the given keyset handle.
-func NewPRFSet(h *keyset.Handle) (*Set, error) {
-	return NewPRFSetWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewPRFSetWithKeyManager creates a prf.Set primitive from the given keyset handle and a custom key manager.
-//
-// Deprecated: Use [New].
-func NewPRFSetWithKeyManager(h *keyset.Handle, km registry.KeyManager) (*Set, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func NewPRFSet(handle *keyset.Handle) (*Set, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("prf_set_factory: cannot obtain primitive set: %s", err)
 	}
-
 	return wrapPRFset(ps)
 }
 
@@ -48,7 +42,10 @@
 	}
 	set.PrimaryID = ps.Primary.KeyID
 	set.PRFs = make(map[uint32]PRF)
-
+	logger, err := createLogger(ps)
+	if err != nil {
+		return nil, err
+	}
 	entries, err := ps.RawEntries()
 	if err != nil {
 		return nil, fmt.Errorf("Could not get raw entries: %v", err)
@@ -64,8 +61,26 @@
 		if !ok {
 			return nil, fmt.Errorf("prf_set_factory: not a PRF primitive")
 		}
-		set.PRFs[entry.KeyID] = prf
+		set.PRFs[entry.KeyID] = &monitoredPRF{
+			prf:    prf,
+			keyID:  entry.KeyID,
+			logger: logger,
+		}
 	}
-
 	return set, nil
 }
+
+func createLogger(ps *primitiveset.PrimitiveSet) (monitoring.Logger, error) {
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, nil
+	}
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, err
+	}
+	return internalregistry.GetMonitoringClient().NewLogger(&monitoring.Context{
+		KeysetInfo:  keysetInfo,
+		Primitive:   "prf",
+		APIFunction: "compute",
+	})
+}
diff --git a/go/prf/prf_set_factory_test.go b/go/prf/prf_set_factory_test.go
index 54ebf1f..dc7ec0b 100644
--- a/go/prf/prf_set_factory_test.go
+++ b/go/prf/prf_set_factory_test.go
@@ -17,13 +17,20 @@
 package prf_test
 
 import (
+	"bytes"
 	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/prf"
+	"github.com/google/tink/go/testing/fakemonitoring"
 	"github.com/google/tink/go/testutil"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -33,19 +40,15 @@
 )
 
 func addKeyAndReturnID(m *keyset.Manager, template *tinkpb.KeyTemplate) (uint32, error) {
-	err := m.Rotate(template)
+	keyID, err := m.Add(template)
 	if err != nil {
-		return 0, fmt.Errorf("Could not add template: %v", err)
+		return 0, fmt.Errorf("Could not add key from the given template: %v", err)
 	}
-	h, err := m.Handle()
+	err = m.SetPrimary(keyID)
 	if err != nil {
-		return 0, fmt.Errorf("Could not obtain handle: %v", err)
+		return 0, fmt.Errorf("Could set key as primary: %v", err)
 	}
-	p, err := h.Primitives()
-	if err != nil {
-		return 0, fmt.Errorf("Could not obtain primitives: %v", err)
-	}
-	return p.Primary.KeyID, nil
+	return keyID, nil
 }
 
 func TestFactoryBasic(t *testing.T) {
@@ -165,7 +168,7 @@
 		t.Errorf("Expected non RAW prefix to fail to create prf.Set")
 	}
 	m := keyset.NewManagerFromHandle(h)
-	err = m.Rotate(prf.HMACSHA256PRFKeyTemplate())
+	_, err = addKeyAndReturnID(m, prf.HMACSHA256PRFKeyTemplate())
 	if err != nil {
 		t.Errorf("Expected to be able to add keys to the keyset: %v", err)
 	}
@@ -191,7 +194,7 @@
 		t.Errorf("Expected non PRF primitive to fail to create prf.Set")
 	}
 	m := keyset.NewManagerFromHandle(h)
-	err = m.Rotate(prf.HMACSHA256PRFKeyTemplate())
+	_, err = addKeyAndReturnID(m, prf.HMACSHA256PRFKeyTemplate())
 	if err != nil {
 		t.Errorf("Expected to be able to add keys to the keyset: %v", err)
 	}
@@ -223,3 +226,228 @@
 		}
 	}
 }
+
+func TestPrimitiveFactoryComputePRFWithoutAnnotationsDoesNothing(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(prf.HMACSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	prfSet, err := prf.NewPRFSet(kh)
+	if err != nil {
+		t.Fatalf("prf.NewPRFSet() err = %v, want nil", err)
+	}
+	if _, err := prfSet.ComputePrimaryPRF([]byte("input_data"), 32); err != nil {
+		t.Fatalf("prfSet.ComputePrimaryPRF() err = %v, want nil", err)
+	}
+	failures := len(client.Failures())
+	if failures != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", failures)
+	}
+	got := client.Events()
+	if got != nil {
+		t.Errorf("client.Events() = %v, want nil", got)
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsComputePRFFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(prf.HMACSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	prfSet, err := prf.NewPRFSet(mh)
+	if err != nil {
+		t.Fatalf("prf.NewPRFSet() err = %v, want nil", err)
+	}
+	data := []byte("input_data")
+	if _, err := prfSet.ComputePrimaryPRF(data, 64); err == nil {
+		t.Fatalf("prfSet.ComputePrimaryPRF() err = nil, want non-nil errors")
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"prf",
+				"compute",
+				&monitoring.KeysetInfo{
+					Annotations: annotations,
+					Entries: []*monitoring.Entry{
+						{
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.HmacPrfKey",
+							KeyPrefix: "RAW",
+						},
+					},
+					PrimaryKeyID: kh.KeysetInfo().GetPrimaryKeyId(),
+				},
+			),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryIndividualPrfWithAnnotatonsLogsCompute(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(prf.HMACSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	manager := keyset.NewManagerFromHandle(kh)
+	hmac512KeyID, err := manager.Add(prf.HMACSHA512PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("manager.Add() err = %v, want nil", err)
+	}
+	aesKeyID, err := manager.Add(prf.AESCMACPRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("manager.Add() err = %v, want nil", err)
+	}
+	kh, err = manager.Handle()
+	if err != nil {
+		t.Fatalf("manager.Handle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	prfSet, err := prf.NewPRFSet(mh)
+	if err != nil {
+		t.Fatalf("prf.NewPRFSet() err = %v, want nil", err)
+	}
+	for _, p := range prfSet.PRFs {
+		if _, err := p.ComputePRF([]byte("input_data"), 16); err != nil {
+			t.Fatalf("p.ComputePRF() err = %v, want nil", err)
+		}
+
+	}
+	got := client.Events()
+	wantKeysetInfo := &monitoring.KeysetInfo{
+		PrimaryKeyID: kh.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HmacPrfKey",
+				KeyPrefix: "RAW",
+			},
+			{
+				KeyID:     hmac512KeyID,
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HmacPrfKey",
+				KeyPrefix: "RAW",
+			},
+			{
+				KeyID:     aesKeyID,
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.AesCmacPrfKey",
+				KeyPrefix: "RAW",
+			},
+		},
+		Annotations: annotations,
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("prf", "compute", wantKeysetInfo),
+			KeyID:    kh.KeysetInfo().GetKeyInfo()[0].GetKeyId(),
+			NumBytes: len("input_data"),
+		},
+		{
+			Context:  monitoring.NewContext("prf", "compute", wantKeysetInfo),
+			KeyID:    kh.KeysetInfo().GetKeyInfo()[1].GetKeyId(),
+			NumBytes: len("input_data"),
+		},
+		{
+			Context:  monitoring.NewContext("prf", "compute", wantKeysetInfo),
+			KeyID:    kh.KeysetInfo().GetKeyInfo()[2].GetKeyId(),
+			NumBytes: len("input_data"),
+		},
+	}
+	eventCmp := func(a, b *fakemonitoring.LogEvent) bool {
+		return a.KeyID < b.KeyID
+	}
+	if !cmp.Equal(got, want, cmpopts.SortSlices(eventCmp)) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+
+}
+
+func TestPrimitiveFactoryWithMonitoringAnnotationsLogsComputePRF(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	kh, err := keyset.NewHandle(prf.HMACSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	mh, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	prfSet, err := prf.NewPRFSet(mh)
+	if err != nil {
+		t.Fatalf("prf.NewPRFSet() err = %v, want nil", err)
+	}
+	data := []byte("some_data")
+	if _, err := prfSet.ComputePrimaryPRF(data, 20); err != nil {
+		t.Fatalf("prfSet.ComputePrimaryPRF() err = %v, want nil", err)
+	}
+	got := client.Events()
+	wantKeysetInfo := &monitoring.KeysetInfo{
+		PrimaryKeyID: kh.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.HmacPrfKey",
+				KeyPrefix: "RAW",
+			},
+		},
+		Annotations: annotations,
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("prf", "compute", wantKeysetInfo),
+			KeyID:    kh.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+	}
+	if !cmp.Equal(got, want) {
+		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want))
+	}
+}
diff --git a/go/proto/aes_cmac_go_proto/BUILD.bazel b/go/proto/aes_cmac_go_proto/BUILD.bazel
index 0b7c1ca..d4c31fa 100644
--- a/go/proto/aes_cmac_go_proto/BUILD.bazel
+++ b/go/proto/aes_cmac_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_cmac_go_proto",
     srcs = ["aes_cmac.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_cmac_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_cmac_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_cmac_go_proto/aes_cmac.pb.go b/go/proto/aes_cmac_go_proto/aes_cmac.pb.go
index 31a9043..eaf0b51 100644
--- a/go/proto/aes_cmac_go_proto/aes_cmac.pb.go
+++ b/go/proto/aes_cmac_go_proto/aes_cmac.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_cmac.proto
 
 package aes_cmac_go_proto
@@ -226,12 +226,12 @@
 	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
 	0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x41, 0x65, 0x73, 0x43,
 	0x6d, 0x61, 0x63, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d,
-	0x73, 0x42, 0x50, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x73, 0x42, 0x53, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
 	0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
-	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x63, 0x6d, 0x61, 0x63, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6f, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x63, 0x6d, 0x61, 0x63, 0x5f, 0x67, 0x6f,
+	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_cmac_prf_go_proto/BUILD.bazel b/go/proto/aes_cmac_prf_go_proto/BUILD.bazel
index 056ea93..b84b8df 100644
--- a/go/proto/aes_cmac_prf_go_proto/BUILD.bazel
+++ b/go/proto/aes_cmac_prf_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_cmac_prf_go_proto",
     srcs = ["aes_cmac_prf.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_cmac_prf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_cmac_prf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_cmac_prf_go_proto/aes_cmac_prf.pb.go b/go/proto/aes_cmac_prf_go_proto/aes_cmac_prf.pb.go
index f6401cc..a174f66 100644
--- a/go/proto/aes_cmac_prf_go_proto/aes_cmac_prf.pb.go
+++ b/go/proto/aes_cmac_prf_go_proto/aes_cmac_prf.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_cmac_prf.proto
 
 package aes_cmac_prf_go_proto
@@ -163,13 +163,13 @@
 	0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
 	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f,
 	0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53,
-	0x69, 0x7a, 0x65, 0x42, 0x54, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x69, 0x7a, 0x65, 0x42, 0x57, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
 	0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
-	0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x63, 0x6d, 0x61, 0x63, 0x5f, 0x70, 0x72, 0x66,
-	0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x33,
+	0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x63, 0x6d, 0x61, 0x63, 0x5f,
+	0x70, 0x72, 0x66, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_ctr_go_proto/BUILD.bazel b/go/proto/aes_ctr_go_proto/BUILD.bazel
index 721b8b4..715a9cc 100644
--- a/go/proto/aes_ctr_go_proto/BUILD.bazel
+++ b/go/proto/aes_ctr_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_ctr_go_proto",
     srcs = ["aes_ctr.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_ctr_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_ctr_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_ctr_go_proto/aes_ctr.pb.go b/go/proto/aes_ctr_go_proto/aes_ctr.pb.go
index 4058298..81002ce 100644
--- a/go/proto/aes_ctr_go_proto/aes_ctr.pb.go
+++ b/go/proto/aes_ctr_go_proto/aes_ctr.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_ctr.proto
 
 package aes_ctr_go_proto
@@ -225,13 +225,13 @@
 	0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x41, 0x65, 0x73, 0x43, 0x74, 0x72, 0x50, 0x61,
 	0x72, 0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1b, 0x0a, 0x09,
 	0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
-	0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x4f, 0x0a, 0x1c, 0x63, 0x6f, 0x6d,
+	0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x52, 0x0a, 0x1c, 0x63, 0x6f, 0x6d,
 	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74,
-	0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74,
+	0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74,
 	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74,
-	0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x63, 0x74,
-	0x72, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x33,
+	0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73,
+	0x5f, 0x63, 0x74, 0x72, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_ctr_hmac_aead_go_proto/BUILD.bazel b/go/proto/aes_ctr_hmac_aead_go_proto/BUILD.bazel
index 8721e2e..98ebc40 100644
--- a/go/proto/aes_ctr_hmac_aead_go_proto/BUILD.bazel
+++ b/go/proto/aes_ctr_hmac_aead_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_ctr_hmac_aead_go_proto",
     srcs = ["aes_ctr_hmac_aead.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_ctr_hmac_aead_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/aes_ctr_go_proto",
         "//proto/hmac_go_proto",
@@ -16,5 +16,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_ctr_hmac_aead_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_ctr_hmac_aead_go_proto/aes_ctr_hmac_aead.pb.go b/go/proto/aes_ctr_hmac_aead_go_proto/aes_ctr_hmac_aead.pb.go
index d7b061c..c28d1e4 100644
--- a/go/proto/aes_ctr_hmac_aead_go_proto/aes_ctr_hmac_aead.pb.go
+++ b/go/proto/aes_ctr_hmac_aead_go_proto/aes_ctr_hmac_aead.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_ctr_hmac_aead.proto
 
 package aes_ctr_hmac_aead_go_proto
@@ -190,13 +190,14 @@
 	0x74, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x36, 0x0a, 0x08, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x6b, 0x65,
 	0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
 	0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x48, 0x6d, 0x61,
-	0x63, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x68, 0x6d, 0x61, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x59, 0x0a,
+	0x63, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x68, 0x6d, 0x61, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x5c, 0x0a,
 	0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70,
 	0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a,
-	0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67,
-	0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65,
-	0x73, 0x5f, 0x63, 0x74, 0x72, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x61, 0x65, 0x61, 0x64, 0x5f,
-	0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x2f, 0x61, 0x65, 0x73, 0x5f, 0x63, 0x74, 0x72, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x61, 0x65,
+	0x61, 0x64, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_ctr_hmac_streaming_go_proto/BUILD.bazel b/go/proto/aes_ctr_hmac_streaming_go_proto/BUILD.bazel
index 7cd611d..322c960 100644
--- a/go/proto/aes_ctr_hmac_streaming_go_proto/BUILD.bazel
+++ b/go/proto/aes_ctr_hmac_streaming_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_ctr_hmac_streaming_go_proto",
     srcs = ["aes_ctr_hmac_streaming.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_ctr_hmac_streaming_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "//proto/hmac_go_proto",
@@ -16,5 +16,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_ctr_hmac_streaming_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_ctr_hmac_streaming_go_proto/aes_ctr_hmac_streaming.pb.go b/go/proto/aes_ctr_hmac_streaming_go_proto/aes_ctr_hmac_streaming.pb.go
index 8104b40..a8c57b0 100644
--- a/go/proto/aes_ctr_hmac_streaming_go_proto/aes_ctr_hmac_streaming.pb.go
+++ b/go/proto/aes_ctr_hmac_streaming_go_proto/aes_ctr_hmac_streaming.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_ctr_hmac_streaming.proto
 
 package aes_ctr_hmac_streaming_go_proto
@@ -283,14 +283,14 @@
 	0x73, 0x43, 0x74, 0x72, 0x48, 0x6d, 0x61, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e,
 	0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12,
 	0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x5e, 0x0a, 0x1c,
+	0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x61, 0x0a, 0x1c,
 	0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74,
-	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3c,
+	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f,
 	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73,
-	0x5f, 0x63, 0x74, 0x72, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
-	0x69, 0x6e, 0x67, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x33,
+	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+	0x61, 0x65, 0x73, 0x5f, 0x63, 0x74, 0x72, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x73, 0x74, 0x72,
+	0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_gcm_go_proto/BUILD.bazel b/go/proto/aes_gcm_go_proto/BUILD.bazel
index 396bf34..d16e4ac 100644
--- a/go/proto/aes_gcm_go_proto/BUILD.bazel
+++ b/go/proto/aes_gcm_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_gcm_go_proto",
     srcs = ["aes_gcm.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_gcm_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_gcm_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_gcm_go_proto/aes_gcm.pb.go b/go/proto/aes_gcm_go_proto/aes_gcm.pb.go
index 239d0d3..e8bb48e 100644
--- a/go/proto/aes_gcm_go_proto/aes_gcm.pb.go
+++ b/go/proto/aes_gcm_go_proto/aes_gcm.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_gcm.proto
 
 package aes_gcm_go_proto
@@ -36,8 +36,6 @@
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
-// only allowing IV size in bytes = 12 and tag size in bytes = 16
-// Thus, accept no params.
 type AesGcmKeyFormat struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -93,7 +91,6 @@
 	return 0
 }
 
-// key_type: type.googleapis.com/google.crypto.tink.AesGcmKey
 type AesGcmKey struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -164,12 +161,13 @@
 	0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
 	0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79,
 	0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65,
-	0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x4f, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
+	0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x5b, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
 	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
 	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b,
-	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x67, 0x63, 0x6d, 0x5f, 0x67,
-	0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x67, 0x63,
+	0x6d, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x06, 0x54, 0x49, 0x4e,
+	0x4b, 0x50, 0x42, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_gcm_hkdf_streaming_go_proto/BUILD.bazel b/go/proto/aes_gcm_hkdf_streaming_go_proto/BUILD.bazel
index e62be5b..cbb789e 100644
--- a/go/proto/aes_gcm_hkdf_streaming_go_proto/BUILD.bazel
+++ b/go/proto/aes_gcm_hkdf_streaming_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_gcm_hkdf_streaming_go_proto",
     srcs = ["aes_gcm_hkdf_streaming.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_gcm_hkdf_streaming_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_gcm_hkdf_streaming_go_proto/aes_gcm_hkdf_streaming.pb.go b/go/proto/aes_gcm_hkdf_streaming_go_proto/aes_gcm_hkdf_streaming.pb.go
index b5a2789..1803cb1 100644
--- a/go/proto/aes_gcm_hkdf_streaming_go_proto/aes_gcm_hkdf_streaming.pb.go
+++ b/go/proto/aes_gcm_hkdf_streaming_go_proto/aes_gcm_hkdf_streaming.pb.go
@@ -19,8 +19,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_gcm_hkdf_streaming.proto
 
 package aes_gcm_hkdf_streaming_go_proto
@@ -271,13 +271,14 @@
 	0x48, 0x6b, 0x64, 0x66, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72,
 	0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6b,
 	0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
-	0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x5e, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e,
+	0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x61, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e,
 	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69,
-	0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68,
+	0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68,
 	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69,
-	0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x67, 0x63, 0x6d,
-	0x5f, 0x68, 0x6b, 0x64, 0x66, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x5f,
-	0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f,
+	0x67, 0x63, 0x6d, 0x5f, 0x68, 0x6b, 0x64, 0x66, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69,
+	0x6e, 0x67, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_gcm_siv_go_proto/BUILD.bazel b/go/proto/aes_gcm_siv_go_proto/BUILD.bazel
index 69cb782..a94a75a 100644
--- a/go/proto/aes_gcm_siv_go_proto/BUILD.bazel
+++ b/go/proto/aes_gcm_siv_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_gcm_siv_go_proto",
     srcs = ["aes_gcm_siv.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_gcm_siv_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_gcm_siv_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_gcm_siv_go_proto/aes_gcm_siv.pb.go b/go/proto/aes_gcm_siv_go_proto/aes_gcm_siv.pb.go
index b05f43c..8a10c6d 100644
--- a/go/proto/aes_gcm_siv_go_proto/aes_gcm_siv.pb.go
+++ b/go/proto/aes_gcm_siv_go_proto/aes_gcm_siv.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_gcm_siv.proto
 
 package aes_gcm_siv_go_proto
@@ -165,12 +165,12 @@
 	0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73,
 	0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65,
 	0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65,
-	0x42, 0x53, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
+	0x42, 0x56, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
 	0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2f, 0x61, 0x65, 0x73, 0x5f, 0x67, 0x63, 0x6d, 0x5f, 0x73, 0x69, 0x76, 0x5f, 0x67, 0x6f, 0x5f,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x50, 0x01, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x67, 0x63, 0x6d, 0x5f, 0x73, 0x69, 0x76, 0x5f,
+	0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/aes_siv_go_proto/BUILD.bazel b/go/proto/aes_siv_go_proto/BUILD.bazel
index 677a7b9..1a1f1c8 100644
--- a/go/proto/aes_siv_go_proto/BUILD.bazel
+++ b/go/proto/aes_siv_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "aes_siv_go_proto",
     srcs = ["aes_siv.pb.go"],
     importpath = "github.com/google/tink/go/proto/aes_siv_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":aes_siv_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/aes_siv_go_proto/aes_siv.pb.go b/go/proto/aes_siv_go_proto/aes_siv.pb.go
index 4407c68..4615065 100644
--- a/go/proto/aes_siv_go_proto/aes_siv.pb.go
+++ b/go/proto/aes_siv_go_proto/aes_siv.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/aes_siv.proto
 
 package aes_siv_go_proto
@@ -164,12 +164,13 @@
 	0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
 	0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79,
 	0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65,
-	0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x4f, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
+	0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x52, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
 	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
 	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b,
-	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x73, 0x69, 0x76, 0x5f, 0x67,
-	0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x65, 0x73, 0x5f, 0x73, 0x69,
+	0x76, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/chacha20_poly1305_go_proto/BUILD.bazel b/go/proto/chacha20_poly1305_go_proto/BUILD.bazel
index 2b75863..dd94cf4 100644
--- a/go/proto/chacha20_poly1305_go_proto/BUILD.bazel
+++ b/go/proto/chacha20_poly1305_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "chacha20_poly1305_go_proto",
     srcs = ["chacha20_poly1305.pb.go"],
     importpath = "github.com/google/tink/go/proto/chacha20_poly1305_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":chacha20_poly1305_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/chacha20_poly1305_go_proto/chacha20_poly1305.pb.go b/go/proto/chacha20_poly1305_go_proto/chacha20_poly1305.pb.go
index 201a91c..816b05d 100644
--- a/go/proto/chacha20_poly1305_go_proto/chacha20_poly1305.pb.go
+++ b/go/proto/chacha20_poly1305_go_proto/chacha20_poly1305.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/chacha20_poly1305.proto
 
 package chacha20_poly1305_go_proto
@@ -146,13 +146,13 @@
 	0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
 	0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
 	0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42,
-	0x59, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72,
+	0x5c, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72,
 	0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50,
-	0x01, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f,
-	0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
-	0x63, 0x68, 0x61, 0x63, 0x68, 0x61, 0x32, 0x30, 0x5f, 0x70, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30,
-	0x35, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x33,
+	0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f,
+	0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x2f, 0x63, 0x68, 0x61, 0x63, 0x68, 0x61, 0x32, 0x30, 0x5f, 0x70, 0x6f, 0x6c, 0x79,
+	0x31, 0x33, 0x30, 0x35, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/common_go_proto/BUILD.bazel b/go/proto/common_go_proto/BUILD.bazel
index c80b1c4..5af225d 100644
--- a/go/proto/common_go_proto/BUILD.bazel
+++ b/go/proto/common_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "common_go_proto",
     srcs = ["common.pb.go"],
     importpath = "github.com/google/tink/go/proto/common_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":common_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/common_go_proto/common.pb.go b/go/proto/common_go_proto/common.pb.go
index 16a2293..c6d05ec 100644
--- a/go/proto/common_go_proto/common.pb.go
+++ b/go/proto/common_go_proto/common.pb.go
@@ -18,8 +18,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/common.proto
 
 package common_go_proto
@@ -231,12 +231,12 @@
 	0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x48, 0x41, 0x33, 0x38, 0x34, 0x10, 0x02, 0x12, 0x0a,
 	0x0a, 0x06, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x48,
 	0x41, 0x35, 0x31, 0x32, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x48, 0x41, 0x32, 0x32, 0x34,
-	0x10, 0x05, 0x42, 0x4e, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x10, 0x05, 0x42, 0x51, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
 	0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
+	0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x67, 0x6f, 0x5f,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/ecdsa_go_proto/BUILD.bazel b/go/proto/ecdsa_go_proto/BUILD.bazel
index c2ccb9b..f9f16ba 100644
--- a/go/proto/ecdsa_go_proto/BUILD.bazel
+++ b/go/proto/ecdsa_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "ecdsa_go_proto",
     srcs = ["ecdsa.pb.go"],
     importpath = "github.com/google/tink/go/proto/ecdsa_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":ecdsa_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/ecdsa_go_proto/ecdsa.pb.go b/go/proto/ecdsa_go_proto/ecdsa.pb.go
index e3c3247..e1a6159 100644
--- a/go/proto/ecdsa_go_proto/ecdsa.pb.go
+++ b/go/proto/ecdsa_go_proto/ecdsa.pb.go
@@ -18,8 +18,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/ecdsa.proto
 
 package ecdsa_go_proto
@@ -49,10 +49,11 @@
 	EcdsaSignatureEncoding_IEEE_P1363 EcdsaSignatureEncoding = 1
 	// The signature is encoded using ASN.1
 	// (https://tools.ietf.org/html/rfc5480#appendix-A):
-	// ECDSA-Sig-Value :: = SEQUENCE {
-	//  r INTEGER,
-	//  s INTEGER
-	// }
+	//
+	//	ECDSA-Sig-Value :: = SEQUENCE {
+	//	 r INTEGER,
+	//	 s INTEGER
+	//	}
 	EcdsaSignatureEncoding_DER EcdsaSignatureEncoding = 2
 )
 
@@ -318,7 +319,8 @@
 	unknownFields protoimpl.UnknownFields
 
 	// Required.
-	Params *EcdsaParams `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"`
+	Params  *EcdsaParams `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"`
+	Version uint32       `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"`
 }
 
 func (x *EcdsaKeyFormat) Reset() {
@@ -360,6 +362,13 @@
 	return nil
 }
 
+func (x *EcdsaKeyFormat) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
 var File_third_party_tink_proto_ecdsa_proto protoreflect.FileDescriptor
 
 var file_third_party_tink_proto_ecdsa_proto_rawDesc = []byte{
@@ -398,22 +407,24 @@
 	0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x45, 0x63, 0x64, 0x73, 0x61, 0x50, 0x75, 0x62, 0x6c, 0x69,
 	0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12,
 	0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x49, 0x0a, 0x0e,
+	0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x63, 0x0a, 0x0e,
 	0x45, 0x63, 0x64, 0x73, 0x61, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x37,
 	0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f,
 	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74,
 	0x69, 0x6e, 0x6b, 0x2e, 0x45, 0x63, 0x64, 0x73, 0x61, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52,
-	0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x2a, 0x47, 0x0a, 0x16, 0x45, 0x63, 0x64, 0x73, 0x61,
-	0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e,
-	0x67, 0x12, 0x14, 0x0a, 0x10, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x4e, 0x43,
-	0x4f, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x45, 0x45, 0x45, 0x5f,
-	0x50, 0x31, 0x33, 0x36, 0x33, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x45, 0x52, 0x10, 0x02,
-	0x42, 0x4d, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
-	0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
-	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
+	0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+	0x6e, 0x2a, 0x47, 0x0a, 0x16, 0x45, 0x63, 0x64, 0x73, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
+	0x75, 0x72, 0x65, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x10, 0x55,
+	0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x4e, 0x43, 0x4f, 0x44, 0x49, 0x4e, 0x47, 0x10,
+	0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x45, 0x45, 0x45, 0x5f, 0x50, 0x31, 0x33, 0x36, 0x33, 0x10,
+	0x01, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x45, 0x52, 0x10, 0x02, 0x42, 0x50, 0x0a, 0x1c, 0x63, 0x6f,
+	0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e,
+	0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69,
+	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
+	0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x63,
+	0x64, 0x73, 0x61, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/ecies_aead_hkdf_go_proto/BUILD.bazel b/go/proto/ecies_aead_hkdf_go_proto/BUILD.bazel
index 5d412a6..ce44b48 100644
--- a/go/proto/ecies_aead_hkdf_go_proto/BUILD.bazel
+++ b/go/proto/ecies_aead_hkdf_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "ecies_aead_hkdf_go_proto",
     srcs = ["ecies_aead_hkdf.pb.go"],
     importpath = "github.com/google/tink/go/proto/ecies_aead_hkdf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "//proto/tink_go_proto",
@@ -16,5 +16,5 @@
 alias(
     name = "go_default_library",
     actual = ":ecies_aead_hkdf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/ecies_aead_hkdf_go_proto/ecies_aead_hkdf.pb.go b/go/proto/ecies_aead_hkdf_go_proto/ecies_aead_hkdf.pb.go
index bd89fd0..913ddf3 100644
--- a/go/proto/ecies_aead_hkdf_go_proto/ecies_aead_hkdf.pb.go
+++ b/go/proto/ecies_aead_hkdf_go_proto/ecies_aead_hkdf.pb.go
@@ -18,8 +18,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/ecies_aead_hkdf.proto
 
 package ecies_aead_hkdf_go_proto
@@ -490,13 +490,13 @@
 	0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
 	0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x45, 0x63, 0x69, 0x65, 0x73,
 	0x41, 0x65, 0x61, 0x64, 0x48, 0x6b, 0x64, 0x66, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x06,
-	0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, 0x57, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
+	0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, 0x5a, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
 	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
 	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b,
-	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x63, 0x69, 0x65, 0x73, 0x5f, 0x61, 0x65, 0x61,
-	0x64, 0x5f, 0x68, 0x6b, 0x64, 0x66, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
-	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x63, 0x69, 0x65, 0x73, 0x5f,
+	0x61, 0x65, 0x61, 0x64, 0x5f, 0x68, 0x6b, 0x64, 0x66, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/ed25519_go_proto/BUILD.bazel b/go/proto/ed25519_go_proto/BUILD.bazel
index 83eee4d..36b489b 100644
--- a/go/proto/ed25519_go_proto/BUILD.bazel
+++ b/go/proto/ed25519_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "ed25519_go_proto",
     srcs = ["ed25519.pb.go"],
     importpath = "github.com/google/tink/go/proto/ed25519_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":ed25519_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/ed25519_go_proto/ed25519.pb.go b/go/proto/ed25519_go_proto/ed25519.pb.go
index cbffbfe..5d60730 100644
--- a/go/proto/ed25519_go_proto/ed25519.pb.go
+++ b/go/proto/ed25519_go_proto/ed25519.pb.go
@@ -20,8 +20,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/ed25519.proto
 
 package ed25519_go_proto
@@ -239,12 +239,13 @@
 	0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72,
 	0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x45, 0x64, 0x32, 0x35, 0x35, 0x31,
 	0x39, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c,
-	0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x4f, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f,
+	0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x52, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f,
 	0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
 	0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x5f, 0x67, 0x6f,
-	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39,
+	0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x33,
 }
 
 var (
diff --git a/go/proto/hkdf_prf_go_proto/BUILD.bazel b/go/proto/hkdf_prf_go_proto/BUILD.bazel
index 916df1c..a6e6a22 100644
--- a/go/proto/hkdf_prf_go_proto/BUILD.bazel
+++ b/go/proto/hkdf_prf_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "hkdf_prf_go_proto",
     srcs = ["hkdf_prf.pb.go"],
     importpath = "github.com/google/tink/go/proto/hkdf_prf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":hkdf_prf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/hkdf_prf_go_proto/hkdf_prf.pb.go b/go/proto/hkdf_prf_go_proto/hkdf_prf.pb.go
index 686f5c9..108e538 100644
--- a/go/proto/hkdf_prf_go_proto/hkdf_prf.pb.go
+++ b/go/proto/hkdf_prf_go_proto/hkdf_prf.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/hkdf_prf.proto
 
 package hkdf_prf_proto
@@ -43,8 +43,12 @@
 	unknownFields protoimpl.UnknownFields
 
 	Hash common_go_proto.HashType `protobuf:"varint,1,opt,name=hash,proto3,enum=google.crypto.tink.HashType" json:"hash,omitempty"`
-	// Salt, optional in RFC 5869. Using "" is equivalent to zeros of length up to
-	// the block length of the HMac.
+	// Optional.
+	//
+	// An unspecified or zero-length value is equivalent to a sequence of zeros
+	// (0x00) with a length equal to the output size of hash.
+	//
+	// See https://rfc-editor.org/rfc/rfc5869.
 	Salt []byte `protobuf:"bytes,2,opt,name=salt,proto3" json:"salt,omitempty"`
 }
 
@@ -250,13 +254,13 @@
 	0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x73,
 	0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53, 0x69,
 	0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
-	0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x4d, 0x0a, 0x1c,
+	0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x50, 0x0a, 0x1c,
 	0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74,
-	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b,
+	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e,
 	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x6b, 0x64,
-	0x66, 0x5f, 0x70, 0x72, 0x66, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
+	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+	0x68, 0x6b, 0x64, 0x66, 0x5f, 0x70, 0x72, 0x66, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/hmac_go_proto/BUILD.bazel b/go/proto/hmac_go_proto/BUILD.bazel
index 7d7f9ee..7b46370 100644
--- a/go/proto/hmac_go_proto/BUILD.bazel
+++ b/go/proto/hmac_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "hmac_go_proto",
     srcs = ["hmac.pb.go"],
     importpath = "github.com/google/tink/go/proto/hmac_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":hmac_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/hmac_go_proto/hmac.pb.go b/go/proto/hmac_go_proto/hmac.pb.go
index 8d85372..ec2f7e4 100644
--- a/go/proto/hmac_go_proto/hmac.pb.go
+++ b/go/proto/hmac_go_proto/hmac.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/hmac.proto
 
 package hmac_go_proto
@@ -249,12 +249,12 @@
 	0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65,
 	0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
 	0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42,
-	0x4c, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72,
+	0x4f, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72,
 	0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50,
-	0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f,
-	0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
-	0x68, 0x6d, 0x61, 0x63, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f,
+	0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x2f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/hmac_prf_go_proto/BUILD.bazel b/go/proto/hmac_prf_go_proto/BUILD.bazel
index 38c53cb..af46b53 100644
--- a/go/proto/hmac_prf_go_proto/BUILD.bazel
+++ b/go/proto/hmac_prf_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "hmac_prf_go_proto",
     srcs = ["hmac_prf.pb.go"],
     importpath = "github.com/google/tink/go/proto/hmac_prf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":hmac_prf_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/hmac_prf_go_proto/hmac_prf.pb.go b/go/proto/hmac_prf_go_proto/hmac_prf.pb.go
index c5ec747..a0d3b2a 100644
--- a/go/proto/hmac_prf_go_proto/hmac_prf.pb.go
+++ b/go/proto/hmac_prf_go_proto/hmac_prf.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/hmac_prf.proto
 
 package hmac_prf_go_proto
@@ -240,13 +240,13 @@
 	0x61, 0x6d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18,
 	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x18,
 	0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52,
-	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x50, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e,
+	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x53, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e,
 	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69,
-	0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68,
+	0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68,
 	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69,
-	0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x70, 0x72,
-	0x66, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x33,
+	0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x6d, 0x61, 0x63,
+	0x5f, 0x70, 0x72, 0x66, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/hpke_go_proto/BUILD.bazel b/go/proto/hpke_go_proto/BUILD.bazel
index ea19efc..a54b2cf 100644
--- a/go/proto/hpke_go_proto/BUILD.bazel
+++ b/go/proto/hpke_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "hpke_go_proto",
     srcs = ["hpke.pb.go"],
     importpath = "github.com/google/tink/go/proto/hpke_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":hpke_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/hpke_go_proto/hpke.pb.go b/go/proto/hpke_go_proto/hpke.pb.go
index ff5e521..e65033f 100644
--- a/go/proto/hpke_go_proto/hpke.pb.go
+++ b/go/proto/hpke_go_proto/hpke.pb.go
@@ -1,4 +1,4 @@
-// Copyright 2022 Google LLC
+// Copyright 2021 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.12.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/hpke.proto
 
 package hpke_proto
@@ -41,6 +41,9 @@
 const (
 	HpkeKem_KEM_UNKNOWN              HpkeKem = 0
 	HpkeKem_DHKEM_X25519_HKDF_SHA256 HpkeKem = 1
+	HpkeKem_DHKEM_P256_HKDF_SHA256   HpkeKem = 2
+	HpkeKem_DHKEM_P384_HKDF_SHA384   HpkeKem = 3
+	HpkeKem_DHKEM_P521_HKDF_SHA512   HpkeKem = 4
 )
 
 // Enum value maps for HpkeKem.
@@ -48,10 +51,16 @@
 	HpkeKem_name = map[int32]string{
 		0: "KEM_UNKNOWN",
 		1: "DHKEM_X25519_HKDF_SHA256",
+		2: "DHKEM_P256_HKDF_SHA256",
+		3: "DHKEM_P384_HKDF_SHA384",
+		4: "DHKEM_P521_HKDF_SHA512",
 	}
 	HpkeKem_value = map[string]int32{
 		"KEM_UNKNOWN":              0,
 		"DHKEM_X25519_HKDF_SHA256": 1,
+		"DHKEM_P256_HKDF_SHA256":   2,
+		"DHKEM_P384_HKDF_SHA384":   3,
+		"DHKEM_P521_HKDF_SHA512":   4,
 	}
 )
 
@@ -87,6 +96,8 @@
 const (
 	HpkeKdf_KDF_UNKNOWN HpkeKdf = 0
 	HpkeKdf_HKDF_SHA256 HpkeKdf = 1
+	HpkeKdf_HKDF_SHA384 HpkeKdf = 2
+	HpkeKdf_HKDF_SHA512 HpkeKdf = 3
 )
 
 // Enum value maps for HpkeKdf.
@@ -94,10 +105,14 @@
 	HpkeKdf_name = map[int32]string{
 		0: "KDF_UNKNOWN",
 		1: "HKDF_SHA256",
+		2: "HKDF_SHA384",
+		3: "HKDF_SHA512",
 	}
 	HpkeKdf_value = map[string]int32{
 		"KDF_UNKNOWN": 0,
 		"HKDF_SHA256": 1,
+		"HKDF_SHA384": 2,
+		"HKDF_SHA512": 3,
 	}
 )
 
@@ -458,24 +473,31 @@
 	0x72, 0x61, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x67, 0x6f, 0x6f,
 	0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e,
 	0x48, 0x70, 0x6b, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61,
-	0x6d, 0x73, 0x2a, 0x38, 0x0a, 0x07, 0x48, 0x70, 0x6b, 0x65, 0x4b, 0x65, 0x6d, 0x12, 0x0f, 0x0a,
-	0x0b, 0x4b, 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1c,
-	0x0a, 0x18, 0x44, 0x48, 0x4b, 0x45, 0x4d, 0x5f, 0x58, 0x32, 0x35, 0x35, 0x31, 0x39, 0x5f, 0x48,
-	0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x01, 0x2a, 0x2b, 0x0a, 0x07,
-	0x48, 0x70, 0x6b, 0x65, 0x4b, 0x64, 0x66, 0x12, 0x0f, 0x0a, 0x0b, 0x4b, 0x44, 0x46, 0x5f, 0x55,
-	0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x48, 0x4b, 0x44, 0x46,
-	0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x01, 0x2a, 0x55, 0x0a, 0x08, 0x48, 0x70, 0x6b,
-	0x65, 0x41, 0x65, 0x61, 0x64, 0x12, 0x10, 0x0a, 0x0c, 0x41, 0x45, 0x41, 0x44, 0x5f, 0x55, 0x4e,
-	0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x31,
-	0x32, 0x38, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f,
-	0x32, 0x35, 0x36, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41,
-	0x43, 0x48, 0x41, 0x32, 0x30, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x10, 0x03,
-	0x42, 0x49, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
-	0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x50, 0x01, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2f, 0x68, 0x70, 0x6b, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
+	0x6d, 0x73, 0x2a, 0x8c, 0x01, 0x0a, 0x07, 0x48, 0x70, 0x6b, 0x65, 0x4b, 0x65, 0x6d, 0x12, 0x0f,
+	0x0a, 0x0b, 0x4b, 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12,
+	0x1c, 0x0a, 0x18, 0x44, 0x48, 0x4b, 0x45, 0x4d, 0x5f, 0x58, 0x32, 0x35, 0x35, 0x31, 0x39, 0x5f,
+	0x48, 0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x1a, 0x0a,
+	0x16, 0x44, 0x48, 0x4b, 0x45, 0x4d, 0x5f, 0x50, 0x32, 0x35, 0x36, 0x5f, 0x48, 0x4b, 0x44, 0x46,
+	0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x44, 0x48, 0x4b,
+	0x45, 0x4d, 0x5f, 0x50, 0x33, 0x38, 0x34, 0x5f, 0x48, 0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41,
+	0x33, 0x38, 0x34, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x44, 0x48, 0x4b, 0x45, 0x4d, 0x5f, 0x50,
+	0x35, 0x32, 0x31, 0x5f, 0x48, 0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41, 0x35, 0x31, 0x32, 0x10,
+	0x04, 0x2a, 0x4d, 0x0a, 0x07, 0x48, 0x70, 0x6b, 0x65, 0x4b, 0x64, 0x66, 0x12, 0x0f, 0x0a, 0x0b,
+	0x4b, 0x44, 0x46, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a,
+	0x0b, 0x48, 0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x0f,
+	0x0a, 0x0b, 0x48, 0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41, 0x33, 0x38, 0x34, 0x10, 0x02, 0x12,
+	0x0f, 0x0a, 0x0b, 0x48, 0x4b, 0x44, 0x46, 0x5f, 0x53, 0x48, 0x41, 0x35, 0x31, 0x32, 0x10, 0x03,
+	0x2a, 0x55, 0x0a, 0x08, 0x48, 0x70, 0x6b, 0x65, 0x41, 0x65, 0x61, 0x64, 0x12, 0x10, 0x0a, 0x0c,
+	0x41, 0x45, 0x41, 0x44, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0f,
+	0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x01, 0x12,
+	0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x02,
+	0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, 0x32, 0x30, 0x5f, 0x50, 0x4f, 0x4c,
+	0x59, 0x31, 0x33, 0x30, 0x35, 0x10, 0x03, 0x42, 0x4c, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e,
+	0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75,
+	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e,
+	0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x70, 0x6b, 0x65, 0x5f,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/jwt_ecdsa_go_proto/BUILD.bazel b/go/proto/jwt_ecdsa_go_proto/BUILD.bazel
index ce5c265..5f2cf19 100644
--- a/go/proto/jwt_ecdsa_go_proto/BUILD.bazel
+++ b/go/proto/jwt_ecdsa_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "jwt_ecdsa_go_proto",
     srcs = ["jwt_ecdsa.pb.go"],
     importpath = "github.com/google/tink/go/proto/jwt_ecdsa_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":jwt_ecdsa_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/jwt_ecdsa_go_proto/jwt_ecdsa.pb.go b/go/proto/jwt_ecdsa_go_proto/jwt_ecdsa.pb.go
index 500270a..30990c9 100644
--- a/go/proto/jwt_ecdsa_go_proto/jwt_ecdsa.pb.go
+++ b/go/proto/jwt_ecdsa_go_proto/jwt_ecdsa.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.15.8
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/jwt_ecdsa.proto
 
 package jwt_ecdsa_go_proto
@@ -36,13 +36,14 @@
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
+// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.4
 type JwtEcdsaAlgorithm int32
 
 const (
 	JwtEcdsaAlgorithm_ES_UNKNOWN JwtEcdsaAlgorithm = 0
-	JwtEcdsaAlgorithm_ES256      JwtEcdsaAlgorithm = 1
-	JwtEcdsaAlgorithm_ES384      JwtEcdsaAlgorithm = 2
-	JwtEcdsaAlgorithm_ES512      JwtEcdsaAlgorithm = 3
+	JwtEcdsaAlgorithm_ES256      JwtEcdsaAlgorithm = 1 // ECDSA using P-256 and SHA-256
+	JwtEcdsaAlgorithm_ES384      JwtEcdsaAlgorithm = 2 // ECDSA using P-384 and SHA-384
+	JwtEcdsaAlgorithm_ES512      JwtEcdsaAlgorithm = 3 // ECDSA using P-521 and SHA-512
 )
 
 // Enum value maps for JwtEcdsaAlgorithm.
@@ -94,8 +95,10 @@
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Version   uint32                       `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
-	Algorithm JwtEcdsaAlgorithm            `protobuf:"varint,2,opt,name=algorithm,proto3,enum=google.crypto.tink.JwtEcdsaAlgorithm" json:"algorithm,omitempty"`
+	Version   uint32            `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Algorithm JwtEcdsaAlgorithm `protobuf:"varint,2,opt,name=algorithm,proto3,enum=google.crypto.tink.JwtEcdsaAlgorithm" json:"algorithm,omitempty"`
+	// Affine coordinates of the public key in big-endian representation. The
+	// public key is a point (x, y) on the curve defined by algorithm.
 	X         []byte                       `protobuf:"bytes,3,opt,name=x,proto3" json:"x,omitempty"`
 	Y         []byte                       `protobuf:"bytes,4,opt,name=y,proto3" json:"y,omitempty"`
 	CustomKid *JwtEcdsaPublicKey_CustomKid `protobuf:"bytes,5,opt,name=custom_kid,json=customKid,proto3" json:"custom_kid,omitempty"`
@@ -380,13 +383,13 @@
 	0x63, 0x64, 0x73, 0x61, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x0e, 0x0a,
 	0x0a, 0x45, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a,
 	0x05, 0x45, 0x53, 0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x53, 0x33, 0x38,
-	0x34, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x53, 0x35, 0x31, 0x32, 0x10, 0x03, 0x42, 0x51,
+	0x34, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x53, 0x35, 0x31, 0x32, 0x10, 0x03, 0x42, 0x54,
 	0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79,
 	0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
-	0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6a,
-	0x77, 0x74, 0x5f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x2f, 0x6a, 0x77, 0x74, 0x5f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x67, 0x6f, 0x5f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/jwt_hmac_go_proto/BUILD.bazel b/go/proto/jwt_hmac_go_proto/BUILD.bazel
index 31db3fd..44874cc 100644
--- a/go/proto/jwt_hmac_go_proto/BUILD.bazel
+++ b/go/proto/jwt_hmac_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "jwt_hmac_go_proto",
     srcs = ["jwt_hmac.pb.go"],
     importpath = "github.com/google/tink/go/proto/jwt_hmac_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":jwt_hmac_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/jwt_hmac_go_proto/jwt_hmac.pb.go b/go/proto/jwt_hmac_go_proto/jwt_hmac.pb.go
index 3317134..53d078b 100644
--- a/go/proto/jwt_hmac_go_proto/jwt_hmac.pb.go
+++ b/go/proto/jwt_hmac_go_proto/jwt_hmac.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.15.8
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/jwt_hmac.proto
 
 package jwt_hmac_go_proto
@@ -36,13 +36,14 @@
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
+// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
 type JwtHmacAlgorithm int32
 
 const (
 	JwtHmacAlgorithm_HS_UNKNOWN JwtHmacAlgorithm = 0
-	JwtHmacAlgorithm_HS256      JwtHmacAlgorithm = 1
-	JwtHmacAlgorithm_HS384      JwtHmacAlgorithm = 2
-	JwtHmacAlgorithm_HS512      JwtHmacAlgorithm = 3
+	JwtHmacAlgorithm_HS256      JwtHmacAlgorithm = 1 // HMAC using SHA-256
+	JwtHmacAlgorithm_HS384      JwtHmacAlgorithm = 2 // HMAC using SHA-384
+	JwtHmacAlgorithm_HS512      JwtHmacAlgorithm = 3 // HMAC using SHA-512
 )
 
 // Enum value maps for JwtHmacAlgorithm.
@@ -307,12 +308,13 @@
 	0x74, 0x68, 0x6d, 0x12, 0x0e, 0x0a, 0x0a, 0x48, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
 	0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x53, 0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x09,
 	0x0a, 0x05, 0x48, 0x53, 0x33, 0x38, 0x34, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x53, 0x35,
-	0x31, 0x32, 0x10, 0x03, 0x42, 0x50, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x31, 0x32, 0x10, 0x03, 0x42, 0x53, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
 	0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
-	0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6a, 0x77, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x67, 0x6f,
-	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67,
+	0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6a, 0x77, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63,
+	0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x33,
 }
 
 var (
diff --git a/go/proto/jwt_rsa_ssa_pkcs1_go_proto/BUILD.bazel b/go/proto/jwt_rsa_ssa_pkcs1_go_proto/BUILD.bazel
index 5ba931a..fb32eab 100644
--- a/go/proto/jwt_rsa_ssa_pkcs1_go_proto/BUILD.bazel
+++ b/go/proto/jwt_rsa_ssa_pkcs1_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "jwt_rsa_ssa_pkcs1_go_proto",
     srcs = ["jwt_rsa_ssa_pkcs1.pb.go"],
     importpath = "github.com/google/tink/go/proto/jwt_rsa_ssa_pkcs1_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":jwt_rsa_ssa_pkcs1_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/jwt_rsa_ssa_pkcs1_go_proto/jwt_rsa_ssa_pkcs1.pb.go b/go/proto/jwt_rsa_ssa_pkcs1_go_proto/jwt_rsa_ssa_pkcs1.pb.go
index 5243ed0..5df7a8a 100644
--- a/go/proto/jwt_rsa_ssa_pkcs1_go_proto/jwt_rsa_ssa_pkcs1.pb.go
+++ b/go/proto/jwt_rsa_ssa_pkcs1_go_proto/jwt_rsa_ssa_pkcs1.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.12.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/jwt_rsa_ssa_pkcs1.proto
 
 package rsa_ssa_pkcs1_go_proto
@@ -467,12 +467,13 @@
 	0x0e, 0x0a, 0x0a, 0x52, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12,
 	0x09, 0x0a, 0x05, 0x52, 0x53, 0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x53,
 	0x33, 0x38, 0x34, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x53, 0x35, 0x31, 0x32, 0x10, 0x03,
-	0x42, 0x55, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
+	0x42, 0x58, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
 	0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x50, 0x01, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2f, 0x72, 0x73, 0x61, 0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x6b, 0x63, 0x73, 0x31, 0x5f, 0x67,
-	0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x50, 0x01, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x73, 0x61, 0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x6b, 0x63, 0x73,
+	0x31, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/jwt_rsa_ssa_pss_go_proto/BUILD.bazel b/go/proto/jwt_rsa_ssa_pss_go_proto/BUILD.bazel
new file mode 100644
index 0000000..752ce5a
--- /dev/null
+++ b/go/proto/jwt_rsa_ssa_pss_go_proto/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "jwt_rsa_ssa_pss_go_proto",
+    srcs = ["jwt_rsa_ssa_pss.pb.go"],
+    importpath = "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@org_golang_google_protobuf//reflect/protoreflect",
+        "@org_golang_google_protobuf//runtime/protoimpl",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":jwt_rsa_ssa_pss_go_proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/proto/jwt_rsa_ssa_pss_go_proto/jwt_rsa_ssa_pss.pb.go b/go/proto/jwt_rsa_ssa_pss_go_proto/jwt_rsa_ssa_pss.pb.go
new file mode 100644
index 0000000..d2ee9a7
--- /dev/null
+++ b/go/proto/jwt_rsa_ssa_pss_go_proto/jwt_rsa_ssa_pss.pb.go
@@ -0,0 +1,585 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
+// source: third_party/tink/proto/jwt_rsa_ssa_pss.proto
+
+package jwt_rsa_ssa_pss_go_proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.5
+type JwtRsaSsaPssAlgorithm int32
+
+const (
+	JwtRsaSsaPssAlgorithm_PS_UNKNOWN JwtRsaSsaPssAlgorithm = 0
+	JwtRsaSsaPssAlgorithm_PS256      JwtRsaSsaPssAlgorithm = 1 // RSASSA-PSS using SHA-256 and MGF1 with SHA-256
+	JwtRsaSsaPssAlgorithm_PS384      JwtRsaSsaPssAlgorithm = 2 // RSASSA-PSS using SHA-384 and MGF1 with SHA-384
+	JwtRsaSsaPssAlgorithm_PS512      JwtRsaSsaPssAlgorithm = 3 // RSASSA-PSS using SHA-512 and MGF1 with SHA-512
+)
+
+// Enum value maps for JwtRsaSsaPssAlgorithm.
+var (
+	JwtRsaSsaPssAlgorithm_name = map[int32]string{
+		0: "PS_UNKNOWN",
+		1: "PS256",
+		2: "PS384",
+		3: "PS512",
+	}
+	JwtRsaSsaPssAlgorithm_value = map[string]int32{
+		"PS_UNKNOWN": 0,
+		"PS256":      1,
+		"PS384":      2,
+		"PS512":      3,
+	}
+)
+
+func (x JwtRsaSsaPssAlgorithm) Enum() *JwtRsaSsaPssAlgorithm {
+	p := new(JwtRsaSsaPssAlgorithm)
+	*p = x
+	return p
+}
+
+func (x JwtRsaSsaPssAlgorithm) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (JwtRsaSsaPssAlgorithm) Descriptor() protoreflect.EnumDescriptor {
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_enumTypes[0].Descriptor()
+}
+
+func (JwtRsaSsaPssAlgorithm) Type() protoreflect.EnumType {
+	return &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_enumTypes[0]
+}
+
+func (x JwtRsaSsaPssAlgorithm) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use JwtRsaSsaPssAlgorithm.Descriptor instead.
+func (JwtRsaSsaPssAlgorithm) EnumDescriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescGZIP(), []int{0}
+}
+
+// key_type: type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey
+type JwtRsaSsaPssPublicKey struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version   uint32                `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Algorithm JwtRsaSsaPssAlgorithm `protobuf:"varint,2,opt,name=algorithm,proto3,enum=google.crypto.tink.JwtRsaSsaPssAlgorithm" json:"algorithm,omitempty"`
+	// Modulus.
+	// Unsigned big integer in big-endian representation.
+	N []byte `protobuf:"bytes,3,opt,name=n,proto3" json:"n,omitempty"`
+	// Public exponent.
+	// Unsigned big integer in big-endian representation.
+	E         []byte                           `protobuf:"bytes,4,opt,name=e,proto3" json:"e,omitempty"`
+	CustomKid *JwtRsaSsaPssPublicKey_CustomKid `protobuf:"bytes,5,opt,name=custom_kid,json=customKid,proto3" json:"custom_kid,omitempty"`
+}
+
+func (x *JwtRsaSsaPssPublicKey) Reset() {
+	*x = JwtRsaSsaPssPublicKey{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *JwtRsaSsaPssPublicKey) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JwtRsaSsaPssPublicKey) ProtoMessage() {}
+
+func (x *JwtRsaSsaPssPublicKey) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use JwtRsaSsaPssPublicKey.ProtoReflect.Descriptor instead.
+func (*JwtRsaSsaPssPublicKey) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *JwtRsaSsaPssPublicKey) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *JwtRsaSsaPssPublicKey) GetAlgorithm() JwtRsaSsaPssAlgorithm {
+	if x != nil {
+		return x.Algorithm
+	}
+	return JwtRsaSsaPssAlgorithm_PS_UNKNOWN
+}
+
+func (x *JwtRsaSsaPssPublicKey) GetN() []byte {
+	if x != nil {
+		return x.N
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPublicKey) GetE() []byte {
+	if x != nil {
+		return x.E
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPublicKey) GetCustomKid() *JwtRsaSsaPssPublicKey_CustomKid {
+	if x != nil {
+		return x.CustomKid
+	}
+	return nil
+}
+
+// key_type: type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey
+type JwtRsaSsaPssPrivateKey struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version   uint32                 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	PublicKey *JwtRsaSsaPssPublicKey `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+	// Private exponent.
+	// Unsigned big integer in big-endian representation.
+	D []byte `protobuf:"bytes,3,opt,name=d,proto3" json:"d,omitempty"`
+	// The following parameters are used to optimize RSA signature computation.
+	// The prime factor p of n.
+	// Unsigned big integer in big-endian representation.
+	P []byte `protobuf:"bytes,4,opt,name=p,proto3" json:"p,omitempty"`
+	// The prime factor q of n.
+	// Unsigned big integer in big-endian representation.
+	Q []byte `protobuf:"bytes,5,opt,name=q,proto3" json:"q,omitempty"`
+	// d mod (p - 1).
+	// Unsigned big integer in big-endian representation.
+	Dp []byte `protobuf:"bytes,6,opt,name=dp,proto3" json:"dp,omitempty"`
+	// d mod (q - 1).
+	// Unsigned big integer in big-endian representation.
+	Dq []byte `protobuf:"bytes,7,opt,name=dq,proto3" json:"dq,omitempty"`
+	// Chinese Remainder Theorem coefficient q^(-1) mod p.
+	// Unsigned big integer in big-endian representation.
+	Crt []byte `protobuf:"bytes,8,opt,name=crt,proto3" json:"crt,omitempty"`
+}
+
+func (x *JwtRsaSsaPssPrivateKey) Reset() {
+	*x = JwtRsaSsaPssPrivateKey{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *JwtRsaSsaPssPrivateKey) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JwtRsaSsaPssPrivateKey) ProtoMessage() {}
+
+func (x *JwtRsaSsaPssPrivateKey) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use JwtRsaSsaPssPrivateKey.ProtoReflect.Descriptor instead.
+func (*JwtRsaSsaPssPrivateKey) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetPublicKey() *JwtRsaSsaPssPublicKey {
+	if x != nil {
+		return x.PublicKey
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetD() []byte {
+	if x != nil {
+		return x.D
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetP() []byte {
+	if x != nil {
+		return x.P
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetQ() []byte {
+	if x != nil {
+		return x.Q
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetDp() []byte {
+	if x != nil {
+		return x.Dp
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetDq() []byte {
+	if x != nil {
+		return x.Dq
+	}
+	return nil
+}
+
+func (x *JwtRsaSsaPssPrivateKey) GetCrt() []byte {
+	if x != nil {
+		return x.Crt
+	}
+	return nil
+}
+
+type JwtRsaSsaPssKeyFormat struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version           uint32                `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Algorithm         JwtRsaSsaPssAlgorithm `protobuf:"varint,2,opt,name=algorithm,proto3,enum=google.crypto.tink.JwtRsaSsaPssAlgorithm" json:"algorithm,omitempty"`
+	ModulusSizeInBits uint32                `protobuf:"varint,3,opt,name=modulus_size_in_bits,json=modulusSizeInBits,proto3" json:"modulus_size_in_bits,omitempty"`
+	PublicExponent    []byte                `protobuf:"bytes,4,opt,name=public_exponent,json=publicExponent,proto3" json:"public_exponent,omitempty"`
+}
+
+func (x *JwtRsaSsaPssKeyFormat) Reset() {
+	*x = JwtRsaSsaPssKeyFormat{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *JwtRsaSsaPssKeyFormat) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JwtRsaSsaPssKeyFormat) ProtoMessage() {}
+
+func (x *JwtRsaSsaPssKeyFormat) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use JwtRsaSsaPssKeyFormat.ProtoReflect.Descriptor instead.
+func (*JwtRsaSsaPssKeyFormat) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *JwtRsaSsaPssKeyFormat) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *JwtRsaSsaPssKeyFormat) GetAlgorithm() JwtRsaSsaPssAlgorithm {
+	if x != nil {
+		return x.Algorithm
+	}
+	return JwtRsaSsaPssAlgorithm_PS_UNKNOWN
+}
+
+func (x *JwtRsaSsaPssKeyFormat) GetModulusSizeInBits() uint32 {
+	if x != nil {
+		return x.ModulusSizeInBits
+	}
+	return 0
+}
+
+func (x *JwtRsaSsaPssKeyFormat) GetPublicExponent() []byte {
+	if x != nil {
+		return x.PublicExponent
+	}
+	return nil
+}
+
+// Optional, custom kid header value to be used with "RAW" keys.
+// "TINK" keys with this value set will be rejected.
+type JwtRsaSsaPssPublicKey_CustomKid struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
+}
+
+func (x *JwtRsaSsaPssPublicKey_CustomKid) Reset() {
+	*x = JwtRsaSsaPssPublicKey_CustomKid{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *JwtRsaSsaPssPublicKey_CustomKid) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JwtRsaSsaPssPublicKey_CustomKid) ProtoMessage() {}
+
+func (x *JwtRsaSsaPssPublicKey_CustomKid) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use JwtRsaSsaPssPublicKey_CustomKid.ProtoReflect.Descriptor instead.
+func (*JwtRsaSsaPssPublicKey_CustomKid) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescGZIP(), []int{0, 0}
+}
+
+func (x *JwtRsaSsaPssPublicKey_CustomKid) GetValue() string {
+	if x != nil {
+		return x.Value
+	}
+	return ""
+}
+
+var File_third_party_tink_proto_jwt_rsa_ssa_pss_proto protoreflect.FileDescriptor
+
+var file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDesc = []byte{
+	0x0a, 0x2c, 0x74, 0x68, 0x69, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x79, 0x2f, 0x74, 0x69,
+	0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6a, 0x77, 0x74, 0x5f, 0x72, 0x73, 0x61,
+	0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69,
+	0x6e, 0x6b, 0x22, 0x8d, 0x02, 0x0a, 0x15, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61,
+	0x50, 0x73, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07,
+	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x47, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69,
+	0x74, 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x4a,
+	0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x41, 0x6c, 0x67, 0x6f, 0x72,
+	0x69, 0x74, 0x68, 0x6d, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12,
+	0x0c, 0x0a, 0x01, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x6e, 0x12, 0x0c, 0x0a,
+	0x01, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x65, 0x12, 0x52, 0x0a, 0x0a, 0x63,
+	0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6b, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x33, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e,
+	0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73,
+	0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f,
+	0x6d, 0x4b, 0x69, 0x64, 0x52, 0x09, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4b, 0x69, 0x64, 0x1a,
+	0x21, 0x0a, 0x09, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4b, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c,
+	0x75, 0x65, 0x22, 0xd8, 0x01, 0x0a, 0x16, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61,
+	0x50, 0x73, 0x73, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a,
+	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07,
+	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69,
+	0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x67, 0x6f,
+	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b,
+	0x2e, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x50, 0x75, 0x62,
+	0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65,
+	0x79, 0x12, 0x0c, 0x0a, 0x01, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x64, 0x12,
+	0x0c, 0x0a, 0x01, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x70, 0x12, 0x0c, 0x0a,
+	0x01, 0x71, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x64,
+	0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x64, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x64,
+	0x71, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x64, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x63,
+	0x72, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x63, 0x72, 0x74, 0x22, 0xd4, 0x01,
+	0x0a, 0x15, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x4b, 0x65,
+	0x79, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
+	0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+	0x6e, 0x12, 0x47, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72,
+	0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61,
+	0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x52,
+	0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x6f,
+	0x64, 0x75, 0x6c, 0x75, 0x73, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x6e, 0x5f, 0x62, 0x69,
+	0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x75,
+	0x73, 0x53, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x42, 0x69, 0x74, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x70,
+	0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x04,
+	0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x45, 0x78, 0x70, 0x6f,
+	0x6e, 0x65, 0x6e, 0x74, 0x2a, 0x48, 0x0a, 0x15, 0x4a, 0x77, 0x74, 0x52, 0x73, 0x61, 0x53, 0x73,
+	0x61, 0x50, 0x73, 0x73, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x0e, 0x0a,
+	0x0a, 0x50, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a,
+	0x05, 0x50, 0x53, 0x32, 0x35, 0x36, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x53, 0x33, 0x38,
+	0x34, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x53, 0x35, 0x31, 0x32, 0x10, 0x03, 0x42, 0x5a,
+	0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79,
+	0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
+	0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x2f, 0x6a, 0x77, 0x74, 0x5f, 0x72, 0x73, 0x61, 0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x73,
+	0x73, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
+}
+
+var (
+	file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescOnce sync.Once
+	file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescData = file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDesc
+)
+
+func file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescGZIP() []byte {
+	file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescOnce.Do(func() {
+		file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescData = protoimpl.X.CompressGZIP(file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescData)
+	})
+	return file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDescData
+}
+
+var file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_goTypes = []interface{}{
+	(JwtRsaSsaPssAlgorithm)(0),              // 0: google.crypto.tink.JwtRsaSsaPssAlgorithm
+	(*JwtRsaSsaPssPublicKey)(nil),           // 1: google.crypto.tink.JwtRsaSsaPssPublicKey
+	(*JwtRsaSsaPssPrivateKey)(nil),          // 2: google.crypto.tink.JwtRsaSsaPssPrivateKey
+	(*JwtRsaSsaPssKeyFormat)(nil),           // 3: google.crypto.tink.JwtRsaSsaPssKeyFormat
+	(*JwtRsaSsaPssPublicKey_CustomKid)(nil), // 4: google.crypto.tink.JwtRsaSsaPssPublicKey.CustomKid
+}
+var file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_depIdxs = []int32{
+	0, // 0: google.crypto.tink.JwtRsaSsaPssPublicKey.algorithm:type_name -> google.crypto.tink.JwtRsaSsaPssAlgorithm
+	4, // 1: google.crypto.tink.JwtRsaSsaPssPublicKey.custom_kid:type_name -> google.crypto.tink.JwtRsaSsaPssPublicKey.CustomKid
+	1, // 2: google.crypto.tink.JwtRsaSsaPssPrivateKey.public_key:type_name -> google.crypto.tink.JwtRsaSsaPssPublicKey
+	0, // 3: google.crypto.tink.JwtRsaSsaPssKeyFormat.algorithm:type_name -> google.crypto.tink.JwtRsaSsaPssAlgorithm
+	4, // [4:4] is the sub-list for method output_type
+	4, // [4:4] is the sub-list for method input_type
+	4, // [4:4] is the sub-list for extension type_name
+	4, // [4:4] is the sub-list for extension extendee
+	0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_init() }
+func file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_init() {
+	if File_third_party_tink_proto_jwt_rsa_ssa_pss_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*JwtRsaSsaPssPublicKey); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*JwtRsaSsaPssPrivateKey); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*JwtRsaSsaPssKeyFormat); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*JwtRsaSsaPssPublicKey_CustomKid); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDesc,
+			NumEnums:      1,
+			NumMessages:   4,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_goTypes,
+		DependencyIndexes: file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_depIdxs,
+		EnumInfos:         file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_enumTypes,
+		MessageInfos:      file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_msgTypes,
+	}.Build()
+	File_third_party_tink_proto_jwt_rsa_ssa_pss_proto = out.File
+	file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_rawDesc = nil
+	file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_goTypes = nil
+	file_third_party_tink_proto_jwt_rsa_ssa_pss_proto_depIdxs = nil
+}
diff --git a/go/proto/kms_aead_go_proto/BUILD.bazel b/go/proto/kms_aead_go_proto/BUILD.bazel
new file mode 100644
index 0000000..34a444b
--- /dev/null
+++ b/go/proto/kms_aead_go_proto/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "kms_aead_go_proto",
+    srcs = ["kms_aead.pb.go"],
+    importpath = "github.com/google/tink/go/proto/kms_aead_go_proto",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@org_golang_google_protobuf//reflect/protoreflect",
+        "@org_golang_google_protobuf//runtime/protoimpl",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":kms_aead_go_proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/proto/kms_aead_go_proto/kms_aead.pb.go b/go/proto/kms_aead_go_proto/kms_aead.pb.go
new file mode 100644
index 0000000..0b83f32
--- /dev/null
+++ b/go/proto/kms_aead_go_proto/kms_aead.pb.go
@@ -0,0 +1,248 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
+// source: third_party/tink/proto/kms_aead.proto
+
+package kms_aead_go_proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type KmsAeadKeyFormat struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Required.
+	// The location of a KMS key.
+	// With Google Cloud KMS, valid values have this format:
+	// gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.
+	// With AWS KMS, valid values have this format:
+	// aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>
+	KeyUri string `protobuf:"bytes,1,opt,name=key_uri,json=keyUri,proto3" json:"key_uri,omitempty"`
+}
+
+func (x *KmsAeadKeyFormat) Reset() {
+	*x = KmsAeadKeyFormat{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_kms_aead_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *KmsAeadKeyFormat) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KmsAeadKeyFormat) ProtoMessage() {}
+
+func (x *KmsAeadKeyFormat) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_kms_aead_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use KmsAeadKeyFormat.ProtoReflect.Descriptor instead.
+func (*KmsAeadKeyFormat) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_kms_aead_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *KmsAeadKeyFormat) GetKeyUri() string {
+	if x != nil {
+		return x.KeyUri
+	}
+	return ""
+}
+
+// There is no actual key material in the key.
+type KmsAeadKey struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	// The key format also contains the params.
+	Params *KmsAeadKeyFormat `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"`
+}
+
+func (x *KmsAeadKey) Reset() {
+	*x = KmsAeadKey{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_kms_aead_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *KmsAeadKey) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KmsAeadKey) ProtoMessage() {}
+
+func (x *KmsAeadKey) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_kms_aead_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use KmsAeadKey.ProtoReflect.Descriptor instead.
+func (*KmsAeadKey) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_kms_aead_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *KmsAeadKey) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *KmsAeadKey) GetParams() *KmsAeadKeyFormat {
+	if x != nil {
+		return x.Params
+	}
+	return nil
+}
+
+var File_third_party_tink_proto_kms_aead_proto protoreflect.FileDescriptor
+
+var file_third_party_tink_proto_kms_aead_proto_rawDesc = []byte{
+	0x0a, 0x25, 0x74, 0x68, 0x69, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x79, 0x2f, 0x74, 0x69,
+	0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6b, 0x6d, 0x73, 0x5f, 0x61, 0x65, 0x61,
+	0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x22, 0x2b, 0x0a, 0x10, 0x4b,
+	0x6d, 0x73, 0x41, 0x65, 0x61, 0x64, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12,
+	0x17, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x06, 0x6b, 0x65, 0x79, 0x55, 0x72, 0x69, 0x22, 0x64, 0x0a, 0x0a, 0x4b, 0x6d, 0x73, 0x41,
+	0x65, 0x61, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+	0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+	0x12, 0x3c, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x24, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f,
+	0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x4b, 0x6d, 0x73, 0x41, 0x65, 0x61, 0x64, 0x4b, 0x65, 0x79,
+	0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, 0x53,
+	0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79,
+	0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
+	0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x2f, 0x6b, 0x6d, 0x73, 0x5f, 0x61, 0x65, 0x61, 0x64, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_third_party_tink_proto_kms_aead_proto_rawDescOnce sync.Once
+	file_third_party_tink_proto_kms_aead_proto_rawDescData = file_third_party_tink_proto_kms_aead_proto_rawDesc
+)
+
+func file_third_party_tink_proto_kms_aead_proto_rawDescGZIP() []byte {
+	file_third_party_tink_proto_kms_aead_proto_rawDescOnce.Do(func() {
+		file_third_party_tink_proto_kms_aead_proto_rawDescData = protoimpl.X.CompressGZIP(file_third_party_tink_proto_kms_aead_proto_rawDescData)
+	})
+	return file_third_party_tink_proto_kms_aead_proto_rawDescData
+}
+
+var file_third_party_tink_proto_kms_aead_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_third_party_tink_proto_kms_aead_proto_goTypes = []interface{}{
+	(*KmsAeadKeyFormat)(nil), // 0: google.crypto.tink.KmsAeadKeyFormat
+	(*KmsAeadKey)(nil),       // 1: google.crypto.tink.KmsAeadKey
+}
+var file_third_party_tink_proto_kms_aead_proto_depIdxs = []int32{
+	0, // 0: google.crypto.tink.KmsAeadKey.params:type_name -> google.crypto.tink.KmsAeadKeyFormat
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_third_party_tink_proto_kms_aead_proto_init() }
+func file_third_party_tink_proto_kms_aead_proto_init() {
+	if File_third_party_tink_proto_kms_aead_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_third_party_tink_proto_kms_aead_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*KmsAeadKeyFormat); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_kms_aead_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*KmsAeadKey); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_third_party_tink_proto_kms_aead_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_third_party_tink_proto_kms_aead_proto_goTypes,
+		DependencyIndexes: file_third_party_tink_proto_kms_aead_proto_depIdxs,
+		MessageInfos:      file_third_party_tink_proto_kms_aead_proto_msgTypes,
+	}.Build()
+	File_third_party_tink_proto_kms_aead_proto = out.File
+	file_third_party_tink_proto_kms_aead_proto_rawDesc = nil
+	file_third_party_tink_proto_kms_aead_proto_goTypes = nil
+	file_third_party_tink_proto_kms_aead_proto_depIdxs = nil
+}
diff --git a/go/proto/kms_envelope_go_proto/BUILD.bazel b/go/proto/kms_envelope_go_proto/BUILD.bazel
index 33a7d77..d446417 100644
--- a/go/proto/kms_envelope_go_proto/BUILD.bazel
+++ b/go/proto/kms_envelope_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "kms_envelope_go_proto",
     srcs = ["kms_envelope.pb.go"],
     importpath = "github.com/google/tink/go/proto/kms_envelope_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/tink_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":kms_envelope_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/kms_envelope_go_proto/kms_envelope.pb.go b/go/proto/kms_envelope_go_proto/kms_envelope.pb.go
index 60753ce..c3b26f3 100644
--- a/go/proto/kms_envelope_go_proto/kms_envelope.pb.go
+++ b/go/proto/kms_envelope_go_proto/kms_envelope.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/kms_envelope.proto
 
 package kms_envelope_go_proto
@@ -181,12 +181,13 @@
 	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b,
 	0x2e, 0x4b, 0x6d, 0x73, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x41, 0x65, 0x61, 0x64,
 	0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d,
-	0x73, 0x42, 0x54, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x73, 0x42, 0x57, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
 	0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x50, 0x01, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
-	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x2f, 0x6b, 0x6d, 0x73, 0x5f, 0x65, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x5f, 0x67,
-	0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6b, 0x6d, 0x73, 0x5f, 0x65, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70,
+	0x65, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/prf_based_deriver_go_proto/BUILD.bazel b/go/proto/prf_based_deriver_go_proto/BUILD.bazel
new file mode 100644
index 0000000..ea91174
--- /dev/null
+++ b/go/proto/prf_based_deriver_go_proto/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "prf_based_deriver_go_proto",
+    srcs = ["prf_based_deriver.pb.go"],
+    importpath = "github.com/google/tink/go/proto/prf_based_deriver_go_proto",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//proto/tink_go_proto",
+        "@org_golang_google_protobuf//reflect/protoreflect",
+        "@org_golang_google_protobuf//runtime/protoimpl",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":prf_based_deriver_go_proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/proto/prf_based_deriver_go_proto/prf_based_deriver.pb.go b/go/proto/prf_based_deriver_go_proto/prf_based_deriver.pb.go
new file mode 100644
index 0000000..8831c63
--- /dev/null
+++ b/go/proto/prf_based_deriver_go_proto/prf_based_deriver.pb.go
@@ -0,0 +1,346 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
+// source: third_party/tink/proto/prf_based_deriver.proto
+
+package prf_based_deriver_go_proto
+
+import (
+	tink_go_proto "github.com/google/tink/go/proto/tink_go_proto"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type PrfBasedDeriverParams struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	DerivedKeyTemplate *tink_go_proto.KeyTemplate `protobuf:"bytes,1,opt,name=derived_key_template,json=derivedKeyTemplate,proto3" json:"derived_key_template,omitempty"`
+}
+
+func (x *PrfBasedDeriverParams) Reset() {
+	*x = PrfBasedDeriverParams{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PrfBasedDeriverParams) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PrfBasedDeriverParams) ProtoMessage() {}
+
+func (x *PrfBasedDeriverParams) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PrfBasedDeriverParams.ProtoReflect.Descriptor instead.
+func (*PrfBasedDeriverParams) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_prf_based_deriver_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PrfBasedDeriverParams) GetDerivedKeyTemplate() *tink_go_proto.KeyTemplate {
+	if x != nil {
+		return x.DerivedKeyTemplate
+	}
+	return nil
+}
+
+type PrfBasedDeriverKeyFormat struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	PrfKeyTemplate *tink_go_proto.KeyTemplate `protobuf:"bytes,1,opt,name=prf_key_template,json=prfKeyTemplate,proto3" json:"prf_key_template,omitempty"`
+	Params         *PrfBasedDeriverParams     `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"`
+}
+
+func (x *PrfBasedDeriverKeyFormat) Reset() {
+	*x = PrfBasedDeriverKeyFormat{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PrfBasedDeriverKeyFormat) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PrfBasedDeriverKeyFormat) ProtoMessage() {}
+
+func (x *PrfBasedDeriverKeyFormat) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PrfBasedDeriverKeyFormat.ProtoReflect.Descriptor instead.
+func (*PrfBasedDeriverKeyFormat) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_prf_based_deriver_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PrfBasedDeriverKeyFormat) GetPrfKeyTemplate() *tink_go_proto.KeyTemplate {
+	if x != nil {
+		return x.PrfKeyTemplate
+	}
+	return nil
+}
+
+func (x *PrfBasedDeriverKeyFormat) GetParams() *PrfBasedDeriverParams {
+	if x != nil {
+		return x.Params
+	}
+	return nil
+}
+
+// key_type: type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey
+type PrfBasedDeriverKey struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version uint32                 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	PrfKey  *tink_go_proto.KeyData `protobuf:"bytes,2,opt,name=prf_key,json=prfKey,proto3" json:"prf_key,omitempty"`
+	Params  *PrfBasedDeriverParams `protobuf:"bytes,3,opt,name=params,proto3" json:"params,omitempty"`
+}
+
+func (x *PrfBasedDeriverKey) Reset() {
+	*x = PrfBasedDeriverKey{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PrfBasedDeriverKey) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PrfBasedDeriverKey) ProtoMessage() {}
+
+func (x *PrfBasedDeriverKey) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PrfBasedDeriverKey.ProtoReflect.Descriptor instead.
+func (*PrfBasedDeriverKey) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_prf_based_deriver_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *PrfBasedDeriverKey) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *PrfBasedDeriverKey) GetPrfKey() *tink_go_proto.KeyData {
+	if x != nil {
+		return x.PrfKey
+	}
+	return nil
+}
+
+func (x *PrfBasedDeriverKey) GetParams() *PrfBasedDeriverParams {
+	if x != nil {
+		return x.Params
+	}
+	return nil
+}
+
+var File_third_party_tink_proto_prf_based_deriver_proto protoreflect.FileDescriptor
+
+var file_third_party_tink_proto_prf_based_deriver_proto_rawDesc = []byte{
+	0x0a, 0x2e, 0x74, 0x68, 0x69, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x79, 0x2f, 0x74, 0x69,
+	0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x66, 0x5f, 0x62, 0x61, 0x73,
+	0x65, 0x64, 0x5f, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x12, 0x12, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e,
+	0x74, 0x69, 0x6e, 0x6b, 0x1a, 0x21, 0x74, 0x68, 0x69, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74,
+	0x79, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x69, 0x6e,
+	0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6a, 0x0a, 0x15, 0x50, 0x72, 0x66, 0x42, 0x61,
+	0x73, 0x65, 0x64, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73,
+	0x12, 0x51, 0x0a, 0x14, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x5f,
+	0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f,
+	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74,
+	0x69, 0x6e, 0x6b, 0x2e, 0x4b, 0x65, 0x79, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52,
+	0x12, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x54, 0x65, 0x6d, 0x70, 0x6c,
+	0x61, 0x74, 0x65, 0x22, 0xa8, 0x01, 0x0a, 0x18, 0x50, 0x72, 0x66, 0x42, 0x61, 0x73, 0x65, 0x64,
+	0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74,
+	0x12, 0x49, 0x0a, 0x10, 0x70, 0x72, 0x66, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x65, 0x6d, 0x70,
+	0x6c, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e,
+	0x4b, 0x65, 0x79, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x70, 0x72, 0x66,
+	0x4b, 0x65, 0x79, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x70,
+	0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x67, 0x6f,
+	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b,
+	0x2e, 0x50, 0x72, 0x66, 0x42, 0x61, 0x73, 0x65, 0x64, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x72,
+	0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x22, 0xa7,
+	0x01, 0x0a, 0x12, 0x50, 0x72, 0x66, 0x42, 0x61, 0x73, 0x65, 0x64, 0x44, 0x65, 0x72, 0x69, 0x76,
+	0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12,
+	0x34, 0x0a, 0x07, 0x70, 0x72, 0x66, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f,
+	0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x06, 0x70,
+	0x72, 0x66, 0x4b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63,
+	0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x50, 0x72, 0x66, 0x42, 0x61,
+	0x73, 0x65, 0x64, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73,
+	0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, 0x5c, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69,
+	0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69,
+	0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x66, 0x5f,
+	0x62, 0x61, 0x73, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x67, 0x6f,
+	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_third_party_tink_proto_prf_based_deriver_proto_rawDescOnce sync.Once
+	file_third_party_tink_proto_prf_based_deriver_proto_rawDescData = file_third_party_tink_proto_prf_based_deriver_proto_rawDesc
+)
+
+func file_third_party_tink_proto_prf_based_deriver_proto_rawDescGZIP() []byte {
+	file_third_party_tink_proto_prf_based_deriver_proto_rawDescOnce.Do(func() {
+		file_third_party_tink_proto_prf_based_deriver_proto_rawDescData = protoimpl.X.CompressGZIP(file_third_party_tink_proto_prf_based_deriver_proto_rawDescData)
+	})
+	return file_third_party_tink_proto_prf_based_deriver_proto_rawDescData
+}
+
+var file_third_party_tink_proto_prf_based_deriver_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_third_party_tink_proto_prf_based_deriver_proto_goTypes = []interface{}{
+	(*PrfBasedDeriverParams)(nil),     // 0: google.crypto.tink.PrfBasedDeriverParams
+	(*PrfBasedDeriverKeyFormat)(nil),  // 1: google.crypto.tink.PrfBasedDeriverKeyFormat
+	(*PrfBasedDeriverKey)(nil),        // 2: google.crypto.tink.PrfBasedDeriverKey
+	(*tink_go_proto.KeyTemplate)(nil), // 3: google.crypto.tink.KeyTemplate
+	(*tink_go_proto.KeyData)(nil),     // 4: google.crypto.tink.KeyData
+}
+var file_third_party_tink_proto_prf_based_deriver_proto_depIdxs = []int32{
+	3, // 0: google.crypto.tink.PrfBasedDeriverParams.derived_key_template:type_name -> google.crypto.tink.KeyTemplate
+	3, // 1: google.crypto.tink.PrfBasedDeriverKeyFormat.prf_key_template:type_name -> google.crypto.tink.KeyTemplate
+	0, // 2: google.crypto.tink.PrfBasedDeriverKeyFormat.params:type_name -> google.crypto.tink.PrfBasedDeriverParams
+	4, // 3: google.crypto.tink.PrfBasedDeriverKey.prf_key:type_name -> google.crypto.tink.KeyData
+	0, // 4: google.crypto.tink.PrfBasedDeriverKey.params:type_name -> google.crypto.tink.PrfBasedDeriverParams
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_third_party_tink_proto_prf_based_deriver_proto_init() }
+func file_third_party_tink_proto_prf_based_deriver_proto_init() {
+	if File_third_party_tink_proto_prf_based_deriver_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PrfBasedDeriverParams); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PrfBasedDeriverKeyFormat); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_prf_based_deriver_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PrfBasedDeriverKey); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_third_party_tink_proto_prf_based_deriver_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_third_party_tink_proto_prf_based_deriver_proto_goTypes,
+		DependencyIndexes: file_third_party_tink_proto_prf_based_deriver_proto_depIdxs,
+		MessageInfos:      file_third_party_tink_proto_prf_based_deriver_proto_msgTypes,
+	}.Build()
+	File_third_party_tink_proto_prf_based_deriver_proto = out.File
+	file_third_party_tink_proto_prf_based_deriver_proto_rawDesc = nil
+	file_third_party_tink_proto_prf_based_deriver_proto_goTypes = nil
+	file_third_party_tink_proto_prf_based_deriver_proto_depIdxs = nil
+}
diff --git a/go/proto/rsa_ssa_pkcs1_go_proto/BUILD.bazel b/go/proto/rsa_ssa_pkcs1_go_proto/BUILD.bazel
index 6baaebd..6eafd00 100644
--- a/go/proto/rsa_ssa_pkcs1_go_proto/BUILD.bazel
+++ b/go/proto/rsa_ssa_pkcs1_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "rsa_ssa_pkcs1_go_proto",
     srcs = ["rsa_ssa_pkcs1.pb.go"],
     importpath = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "//proto/common_go_proto",
         "@org_golang_google_protobuf//reflect/protoreflect",
@@ -15,5 +15,5 @@
 alias(
     name = "go_default_library",
     actual = ":rsa_ssa_pkcs1_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/rsa_ssa_pkcs1_go_proto/rsa_ssa_pkcs1.pb.go b/go/proto/rsa_ssa_pkcs1_go_proto/rsa_ssa_pkcs1.pb.go
index 78f5a1a..48a2f86 100644
--- a/go/proto/rsa_ssa_pkcs1_go_proto/rsa_ssa_pkcs1.pb.go
+++ b/go/proto/rsa_ssa_pkcs1_go_proto/rsa_ssa_pkcs1.pb.go
@@ -19,8 +19,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.28.0
-// 	protoc        v3.12.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/rsa_ssa_pkcs1.proto
 
 package rsa_ssa_pkcs1_go_proto
@@ -406,13 +406,13 @@
 	0x28, 0x0d, 0x52, 0x11, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x75, 0x73, 0x53, 0x69, 0x7a, 0x65, 0x49,
 	0x6e, 0x42, 0x69, 0x74, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
 	0x65, 0x78, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e,
-	0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x45, 0x78, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x42, 0x55,
+	0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x45, 0x78, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x42, 0x58,
 	0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79,
 	0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
-	0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72,
-	0x73, 0x61, 0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x6b, 0x63, 0x73, 0x31, 0x5f, 0x67, 0x6f, 0x5f,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x2f, 0x72, 0x73, 0x61, 0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x6b, 0x63, 0x73, 0x31, 0x5f,
+	0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/rsa_ssa_pss_go_proto/BUILD.bazel b/go/proto/rsa_ssa_pss_go_proto/BUILD.bazel
new file mode 100644
index 0000000..02d779b
--- /dev/null
+++ b/go/proto/rsa_ssa_pss_go_proto/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "rsa_ssa_pss_go_proto",
+    srcs = ["rsa_ssa_pss.pb.go"],
+    importpath = "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//proto/common_go_proto",
+        "@org_golang_google_protobuf//reflect/protoreflect",
+        "@org_golang_google_protobuf//runtime/protoimpl",
+    ],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":rsa_ssa_pss_go_proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/proto/rsa_ssa_pss_go_proto/rsa_ssa_pss.pb.go b/go/proto/rsa_ssa_pss_go_proto/rsa_ssa_pss.pb.go
new file mode 100644
index 0000000..52a8b26
--- /dev/null
+++ b/go/proto/rsa_ssa_pss_go_proto/rsa_ssa_pss.pb.go
@@ -0,0 +1,551 @@
+// Copyright 2018 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Definitions for RSA SSA (Signature Schemes with Appendix) using PSS
+// (Probabilistic Signature Scheme ) encoding
+// (https://tools.ietf.org/html/rfc8017#section-8.1).
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
+// source: third_party/tink/proto/rsa_ssa_pss.proto
+
+package rsa_ssa_pss_go_proto
+
+import (
+	common_go_proto "github.com/google/tink/go/proto/common_go_proto"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type RsaSsaPssParams struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Hash function used in computing hash of the signing message
+	// (see https://tools.ietf.org/html/rfc8017#section-9.1.1).
+	// Required.
+	SigHash common_go_proto.HashType `protobuf:"varint,1,opt,name=sig_hash,json=sigHash,proto3,enum=google.crypto.tink.HashType" json:"sig_hash,omitempty"`
+	// Hash function used in MGF1 (a mask generation function based on a
+	// hash function) (see https://tools.ietf.org/html/rfc8017#appendix-B.2.1).
+	// Required.
+	Mgf1Hash common_go_proto.HashType `protobuf:"varint,2,opt,name=mgf1_hash,json=mgf1Hash,proto3,enum=google.crypto.tink.HashType" json:"mgf1_hash,omitempty"`
+	// Salt length (see https://tools.ietf.org/html/rfc8017#section-9.1.1)
+	// Required.
+	SaltLength int32 `protobuf:"varint,3,opt,name=salt_length,json=saltLength,proto3" json:"salt_length,omitempty"`
+}
+
+func (x *RsaSsaPssParams) Reset() {
+	*x = RsaSsaPssParams{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RsaSsaPssParams) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RsaSsaPssParams) ProtoMessage() {}
+
+func (x *RsaSsaPssParams) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RsaSsaPssParams.ProtoReflect.Descriptor instead.
+func (*RsaSsaPssParams) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *RsaSsaPssParams) GetSigHash() common_go_proto.HashType {
+	if x != nil {
+		return x.SigHash
+	}
+	return common_go_proto.HashType(0)
+}
+
+func (x *RsaSsaPssParams) GetMgf1Hash() common_go_proto.HashType {
+	if x != nil {
+		return x.Mgf1Hash
+	}
+	return common_go_proto.HashType(0)
+}
+
+func (x *RsaSsaPssParams) GetSaltLength() int32 {
+	if x != nil {
+		return x.SaltLength
+	}
+	return 0
+}
+
+// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPssPublicKey
+type RsaSsaPssPublicKey struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Required.
+	Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	// Required.
+	Params *RsaSsaPssParams `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"`
+	// Modulus.
+	// Unsigned big integer in bigendian representation.
+	N []byte `protobuf:"bytes,3,opt,name=n,proto3" json:"n,omitempty"`
+	// Public exponent.
+	// Unsigned big integer in bigendian representation.
+	E []byte `protobuf:"bytes,4,opt,name=e,proto3" json:"e,omitempty"`
+}
+
+func (x *RsaSsaPssPublicKey) Reset() {
+	*x = RsaSsaPssPublicKey{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RsaSsaPssPublicKey) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RsaSsaPssPublicKey) ProtoMessage() {}
+
+func (x *RsaSsaPssPublicKey) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RsaSsaPssPublicKey.ProtoReflect.Descriptor instead.
+func (*RsaSsaPssPublicKey) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *RsaSsaPssPublicKey) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *RsaSsaPssPublicKey) GetParams() *RsaSsaPssParams {
+	if x != nil {
+		return x.Params
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPublicKey) GetN() []byte {
+	if x != nil {
+		return x.N
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPublicKey) GetE() []byte {
+	if x != nil {
+		return x.E
+	}
+	return nil
+}
+
+// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey
+type RsaSsaPssPrivateKey struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Required.
+	Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	// Required.
+	PublicKey *RsaSsaPssPublicKey `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+	// Private exponent.
+	// Unsigned big integer in bigendian representation.
+	// Required.
+	D []byte `protobuf:"bytes,3,opt,name=d,proto3" json:"d,omitempty"`
+	// The following parameters are used to optimize RSA signature computation.
+	// The prime factor p of n.
+	// Unsigned big integer in bigendian representation.
+	// Required.
+	P []byte `protobuf:"bytes,4,opt,name=p,proto3" json:"p,omitempty"`
+	// The prime factor q of n.
+	// Unsigned big integer in bigendian representation.
+	// Required.
+	Q []byte `protobuf:"bytes,5,opt,name=q,proto3" json:"q,omitempty"`
+	// d mod (p - 1).
+	// Unsigned big integer in bigendian representation.
+	// Required.
+	Dp []byte `protobuf:"bytes,6,opt,name=dp,proto3" json:"dp,omitempty"`
+	// d mod (q - 1).
+	// Unsigned big integer in bigendian representation.
+	// Required.
+	Dq []byte `protobuf:"bytes,7,opt,name=dq,proto3" json:"dq,omitempty"`
+	// Chinese Remainder Theorem coefficient q^(-1) mod p.
+	// Unsigned big integer in bigendian representation.
+	// Required.
+	Crt []byte `protobuf:"bytes,8,opt,name=crt,proto3" json:"crt,omitempty"`
+}
+
+func (x *RsaSsaPssPrivateKey) Reset() {
+	*x = RsaSsaPssPrivateKey{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RsaSsaPssPrivateKey) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RsaSsaPssPrivateKey) ProtoMessage() {}
+
+func (x *RsaSsaPssPrivateKey) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RsaSsaPssPrivateKey.ProtoReflect.Descriptor instead.
+func (*RsaSsaPssPrivateKey) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *RsaSsaPssPrivateKey) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *RsaSsaPssPrivateKey) GetPublicKey() *RsaSsaPssPublicKey {
+	if x != nil {
+		return x.PublicKey
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPrivateKey) GetD() []byte {
+	if x != nil {
+		return x.D
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPrivateKey) GetP() []byte {
+	if x != nil {
+		return x.P
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPrivateKey) GetQ() []byte {
+	if x != nil {
+		return x.Q
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPrivateKey) GetDp() []byte {
+	if x != nil {
+		return x.Dp
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPrivateKey) GetDq() []byte {
+	if x != nil {
+		return x.Dq
+	}
+	return nil
+}
+
+func (x *RsaSsaPssPrivateKey) GetCrt() []byte {
+	if x != nil {
+		return x.Crt
+	}
+	return nil
+}
+
+type RsaSsaPssKeyFormat struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Required.
+	Params *RsaSsaPssParams `protobuf:"bytes,1,opt,name=params,proto3" json:"params,omitempty"`
+	// Required.
+	ModulusSizeInBits uint32 `protobuf:"varint,2,opt,name=modulus_size_in_bits,json=modulusSizeInBits,proto3" json:"modulus_size_in_bits,omitempty"`
+	// Required.
+	PublicExponent []byte `protobuf:"bytes,3,opt,name=public_exponent,json=publicExponent,proto3" json:"public_exponent,omitempty"`
+}
+
+func (x *RsaSsaPssKeyFormat) Reset() {
+	*x = RsaSsaPssKeyFormat{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RsaSsaPssKeyFormat) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RsaSsaPssKeyFormat) ProtoMessage() {}
+
+func (x *RsaSsaPssKeyFormat) ProtoReflect() protoreflect.Message {
+	mi := &file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RsaSsaPssKeyFormat.ProtoReflect.Descriptor instead.
+func (*RsaSsaPssKeyFormat) Descriptor() ([]byte, []int) {
+	return file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *RsaSsaPssKeyFormat) GetParams() *RsaSsaPssParams {
+	if x != nil {
+		return x.Params
+	}
+	return nil
+}
+
+func (x *RsaSsaPssKeyFormat) GetModulusSizeInBits() uint32 {
+	if x != nil {
+		return x.ModulusSizeInBits
+	}
+	return 0
+}
+
+func (x *RsaSsaPssKeyFormat) GetPublicExponent() []byte {
+	if x != nil {
+		return x.PublicExponent
+	}
+	return nil
+}
+
+var File_third_party_tink_proto_rsa_ssa_pss_proto protoreflect.FileDescriptor
+
+var file_third_party_tink_proto_rsa_ssa_pss_proto_rawDesc = []byte{
+	0x0a, 0x28, 0x74, 0x68, 0x69, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x79, 0x2f, 0x74, 0x69,
+	0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x73, 0x61, 0x5f, 0x73, 0x73, 0x61,
+	0x5f, 0x70, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x1a, 0x23,
+	0x74, 0x68, 0x69, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x79, 0x2f, 0x74, 0x69, 0x6e, 0x6b,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x22, 0xa6, 0x01, 0x0a, 0x0f, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73,
+	0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x37, 0x0a, 0x08, 0x73, 0x69, 0x67, 0x5f, 0x68,
+	0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x48,
+	0x61, 0x73, 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x73, 0x69, 0x67, 0x48, 0x61, 0x73, 0x68,
+	0x12, 0x39, 0x0a, 0x09, 0x6d, 0x67, 0x66, 0x31, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79,
+	0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x54, 0x79, 0x70,
+	0x65, 0x52, 0x08, 0x6d, 0x67, 0x66, 0x31, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x73,
+	0x61, 0x6c, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05,
+	0x52, 0x0a, 0x73, 0x61, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x87, 0x01, 0x0a,
+	0x12, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63,
+	0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a,
+	0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69,
+	0x6e, 0x6b, 0x2e, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x50, 0x61, 0x72, 0x61,
+	0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x6e, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x6e, 0x12, 0x0c, 0x0a, 0x01, 0x65, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x01, 0x65, 0x22, 0xd2, 0x01, 0x0a, 0x13, 0x52, 0x73, 0x61, 0x53, 0x73,
+	0x61, 0x50, 0x73, 0x73, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x18,
+	0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
+	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x45, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c,
+	0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e,
+	0x6b, 0x2e, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69,
+	0x63, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12,
+	0x0c, 0x0a, 0x01, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x64, 0x12, 0x0c, 0x0a,
+	0x01, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x70, 0x12, 0x0c, 0x0a, 0x01, 0x71,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x70, 0x18,
+	0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x64, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x71, 0x18,
+	0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x64, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x72, 0x74,
+	0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x63, 0x72, 0x74, 0x22, 0xab, 0x01, 0x0a, 0x12,
+	0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x46, 0x6f, 0x72, 0x6d,
+	0x61, 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x23, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70,
+	0x74, 0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x52, 0x73, 0x61, 0x53, 0x73, 0x61, 0x50, 0x73,
+	0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12,
+	0x2f, 0x0a, 0x14, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x75, 0x73, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f,
+	0x69, 0x6e, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6d,
+	0x6f, 0x64, 0x75, 0x6c, 0x75, 0x73, 0x53, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x42, 0x69, 0x74, 0x73,
+	0x12, 0x27, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x6e,
+	0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69,
+	0x63, 0x45, 0x78, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x42, 0x56, 0x0a, 0x1c, 0x63, 0x6f, 0x6d,
+	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x74,
+	0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x34, 0x67, 0x69, 0x74,
+	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x74,
+	0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x73, 0x61,
+	0x5f, 0x73, 0x73, 0x61, 0x5f, 0x70, 0x73, 0x73, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescOnce sync.Once
+	file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescData = file_third_party_tink_proto_rsa_ssa_pss_proto_rawDesc
+)
+
+func file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescGZIP() []byte {
+	file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescOnce.Do(func() {
+		file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescData = protoimpl.X.CompressGZIP(file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescData)
+	})
+	return file_third_party_tink_proto_rsa_ssa_pss_proto_rawDescData
+}
+
+var file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_third_party_tink_proto_rsa_ssa_pss_proto_goTypes = []interface{}{
+	(*RsaSsaPssParams)(nil),       // 0: google.crypto.tink.RsaSsaPssParams
+	(*RsaSsaPssPublicKey)(nil),    // 1: google.crypto.tink.RsaSsaPssPublicKey
+	(*RsaSsaPssPrivateKey)(nil),   // 2: google.crypto.tink.RsaSsaPssPrivateKey
+	(*RsaSsaPssKeyFormat)(nil),    // 3: google.crypto.tink.RsaSsaPssKeyFormat
+	(common_go_proto.HashType)(0), // 4: google.crypto.tink.HashType
+}
+var file_third_party_tink_proto_rsa_ssa_pss_proto_depIdxs = []int32{
+	4, // 0: google.crypto.tink.RsaSsaPssParams.sig_hash:type_name -> google.crypto.tink.HashType
+	4, // 1: google.crypto.tink.RsaSsaPssParams.mgf1_hash:type_name -> google.crypto.tink.HashType
+	0, // 2: google.crypto.tink.RsaSsaPssPublicKey.params:type_name -> google.crypto.tink.RsaSsaPssParams
+	1, // 3: google.crypto.tink.RsaSsaPssPrivateKey.public_key:type_name -> google.crypto.tink.RsaSsaPssPublicKey
+	0, // 4: google.crypto.tink.RsaSsaPssKeyFormat.params:type_name -> google.crypto.tink.RsaSsaPssParams
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_third_party_tink_proto_rsa_ssa_pss_proto_init() }
+func file_third_party_tink_proto_rsa_ssa_pss_proto_init() {
+	if File_third_party_tink_proto_rsa_ssa_pss_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RsaSsaPssParams); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RsaSsaPssPublicKey); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RsaSsaPssPrivateKey); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RsaSsaPssKeyFormat); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_third_party_tink_proto_rsa_ssa_pss_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   4,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_third_party_tink_proto_rsa_ssa_pss_proto_goTypes,
+		DependencyIndexes: file_third_party_tink_proto_rsa_ssa_pss_proto_depIdxs,
+		MessageInfos:      file_third_party_tink_proto_rsa_ssa_pss_proto_msgTypes,
+	}.Build()
+	File_third_party_tink_proto_rsa_ssa_pss_proto = out.File
+	file_third_party_tink_proto_rsa_ssa_pss_proto_rawDesc = nil
+	file_third_party_tink_proto_rsa_ssa_pss_proto_goTypes = nil
+	file_third_party_tink_proto_rsa_ssa_pss_proto_depIdxs = nil
+}
diff --git a/go/proto/tink_go_proto/tink.pb.go b/go/proto/tink_go_proto/tink.pb.go
index 190ce78..1cdf439 100644
--- a/go/proto/tink_go_proto/tink.pb.go
+++ b/go/proto/tink_go_proto/tink.pb.go
@@ -18,8 +18,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/tink.proto
 
 package tink_go_proto
@@ -95,14 +95,14 @@
 // entirely by the primitive, but the prefix has to be one of the following
 // 4 types:
 //   - Legacy: prefix is 5 bytes, starts with \x00 and followed by a 4-byte
-//             key id that is computed from the key material. In addition to
-//             that, signature schemes and MACs will add a \x00 byte to the
-//             end of the data being signed / MACed when operating on keys
-//             with this OutputPrefixType.
+//     key id that is computed from the key material. In addition to
+//     that, signature schemes and MACs will add a \x00 byte to the
+//     end of the data being signed / MACed when operating on keys
+//     with this OutputPrefixType.
 //   - Crunchy: prefix is 5 bytes, starts with \x00 and followed by a 4-byte
-//             key id that is generated randomly.
+//     key id that is generated randomly.
 //   - Tink  : prefix is 5 bytes, starts with \x01 and followed by 4-byte
-//             key id that is generated randomly.
+//     key id that is generated randomly.
 //   - Raw   : prefix is 0 byte, i.e., empty.
 type OutputPrefixType int32
 
@@ -781,13 +781,13 @@
 	0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x50, 0x52, 0x45, 0x46, 0x49, 0x58, 0x10, 0x00,
 	0x12, 0x08, 0x0a, 0x04, 0x54, 0x49, 0x4e, 0x4b, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x45,
 	0x47, 0x41, 0x43, 0x59, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x41, 0x57, 0x10, 0x03, 0x12,
-	0x0b, 0x0a, 0x07, 0x43, 0x52, 0x55, 0x4e, 0x43, 0x48, 0x59, 0x10, 0x04, 0x42, 0x55, 0x0a, 0x1c,
+	0x0b, 0x0a, 0x07, 0x43, 0x52, 0x55, 0x4e, 0x43, 0x48, 0x59, 0x10, 0x04, 0x42, 0x58, 0x0a, 0x1c,
 	0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74,
-	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2a,
+	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d,
 	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x69, 0x6e,
-	0x6b, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x06, 0x54, 0x49, 0x4e,
-	0x4b, 0x50, 0x42, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+	0x74, 0x69, 0x6e, 0x6b, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x06,
+	0x54, 0x49, 0x4e, 0x4b, 0x50, 0x42, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/proto/xchacha20_poly1305_go_proto/BUILD.bazel b/go/proto/xchacha20_poly1305_go_proto/BUILD.bazel
index a3d833b..e445fde 100644
--- a/go/proto/xchacha20_poly1305_go_proto/BUILD.bazel
+++ b/go/proto/xchacha20_poly1305_go_proto/BUILD.bazel
@@ -4,7 +4,7 @@
     name = "xchacha20_poly1305_go_proto",
     srcs = ["xchacha20_poly1305.pb.go"],
     importpath = "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
         "@org_golang_google_protobuf//reflect/protoreflect",
         "@org_golang_google_protobuf//runtime/protoimpl",
@@ -14,5 +14,5 @@
 alias(
     name = "go_default_library",
     actual = ":xchacha20_poly1305_go_proto",
-    visibility = ["//:__subpackages__"],
+    visibility = ["//visibility:public"],
 )
diff --git a/go/proto/xchacha20_poly1305_go_proto/xchacha20_poly1305.pb.go b/go/proto/xchacha20_poly1305_go_proto/xchacha20_poly1305.pb.go
index d8801d4..5f5289b 100644
--- a/go/proto/xchacha20_poly1305_go_proto/xchacha20_poly1305.pb.go
+++ b/go/proto/xchacha20_poly1305_go_proto/xchacha20_poly1305.pb.go
@@ -16,8 +16,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.27.1
-// 	protoc        v3.11.4
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
 // source: third_party/tink/proto/xchacha20_poly1305.proto
 
 package xchacha20_poly1305_go_proto
@@ -154,13 +154,14 @@
 	0x30, 0x35, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12,
 	0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x5a, 0x0a, 0x1c,
+	0x28, 0x0c, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x5d, 0x0a, 0x1c,
 	0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74,
-	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38,
+	0x6f, 0x2e, 0x74, 0x69, 0x6e, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3b,
 	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x78, 0x63, 0x68,
-	0x61, 0x63, 0x68, 0x61, 0x32, 0x30, 0x5f, 0x70, 0x6f, 0x6c, 0x79, 0x31, 0x33, 0x30, 0x35, 0x5f,
-	0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x65, 0x2f, 0x74, 0x69, 0x6e, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+	0x78, 0x63, 0x68, 0x61, 0x63, 0x68, 0x61, 0x32, 0x30, 0x5f, 0x70, 0x6f, 0x6c, 0x79, 0x31, 0x33,
+	0x30, 0x35, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/go/signature/BUILD.bazel b/go/signature/BUILD.bazel
index 81a1869..ca12255 100644
--- a/go/signature/BUILD.bazel
+++ b/go/signature/BUILD.bazel
@@ -15,6 +15,8 @@
         "rsa.go",
         "rsassapkcs1_signer_key_manager.go",
         "rsassapkcs1_verifier_key_manager.go",
+        "rsassapss_signer_key_manager.go",
+        "rsassapss_verifier_key_manager.go",
         "signature.go",
         "signature_key_templates.go",
         "signer_factory.go",
@@ -26,18 +28,22 @@
         "//core/cryptofmt",
         "//core/primitiveset",
         "//core/registry",
+        "//internal/internalregistry",
+        "//internal/monitoringutil",
+        "//internal/signature",
+        "//internal/tinkerror",
         "//keyset",
+        "//monitoring",
         "//proto/common_go_proto",
         "//proto/ecdsa_go_proto",
         "//proto/ed25519_go_proto",
         "//proto/rsa_ssa_pkcs1_go_proto",
+        "//proto/rsa_ssa_pss_go_proto",
         "//proto/tink_go_proto",
-        "//signature/internal",
         "//signature/subtle",
         "//subtle",
         "//tink",
         "@org_golang_google_protobuf//proto",
-        "@org_golang_x_crypto//ed25519",
     ],
 )
 
@@ -50,25 +56,35 @@
         "ed25519_verifier_key_manager_test.go",
         "rsassapkcs1_signer_key_manager_test.go",
         "rsassapkcs1_verifier_key_manager_test.go",
+        "rsassapss_signer_key_manager_test.go",
+        "rsassapss_verifier_key_manager_test.go",
         "signature_factory_test.go",
+        "signature_init_test.go",
         "signature_key_templates_test.go",
         "signature_test.go",
     ],
     deps = [
         ":signature",
         "//core/registry",
+        "//insecurecleartextkeyset",
+        "//internal/internalregistry",
+        "//internal/signature",
+        "//internal/testing/stubkeymanager",
         "//keyset",
         "//mac",
+        "//monitoring",
         "//proto/common_go_proto",
         "//proto/ecdsa_go_proto",
         "//proto/ed25519_go_proto",
         "//proto/rsa_ssa_pkcs1_go_proto",
+        "//proto/rsa_ssa_pss_go_proto",
         "//proto/tink_go_proto",
-        "//signature/internal",
         "//signature/subtle",
         "//subtle/random",
+        "//testing/fakemonitoring",
         "//testkeyset",
         "//testutil",
+        "//tink",
         "@com_github_google_go_cmp//cmp",
         "@org_golang_google_protobuf//proto",
         "@org_golang_google_protobuf//testing/protocmp",
diff --git a/go/signature/ed25519_signer_key_manager.go b/go/signature/ed25519_signer_key_manager.go
index 83401e2..fbb4bcb 100644
--- a/go/signature/ed25519_signer_key_manager.go
+++ b/go/signature/ed25519_signer_key_manager.go
@@ -17,11 +17,11 @@
 package signature
 
 import (
+	"crypto/ed25519"
 	"crypto/rand"
 	"errors"
 	"fmt"
-
-	"golang.org/x/crypto/ed25519"
+	"io"
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/keyset"
@@ -65,22 +65,19 @@
 }
 
 // NewKey creates a new ED25519PrivateKey according to specification the given serialized ED25519KeyFormat.
-func (km *ed25519SignerKeyManager) NewKey(serializedKey []byte) (proto.Message, error) {
-	public, private, err := ed25519.GenerateKey(rand.Reader)
+func (km *ed25519SignerKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	pub, priv, err := ed25519.GenerateKey(rand.Reader)
 	if err != nil {
 		return nil, fmt.Errorf("ed25519_signer_key_manager: cannot generate ED25519 key: %s", err)
 	}
-
-	publicProto := &ed25519pb.Ed25519PublicKey{
+	return &ed25519pb.Ed25519PrivateKey{
 		Version:  ed25519SignerKeyVersion,
-		KeyValue: public,
-	}
-	privateProto := &ed25519pb.Ed25519PrivateKey{
-		Version:   ed25519SignerKeyVersion,
-		PublicKey: publicProto,
-		KeyValue:  private.Seed(),
-	}
-	return privateProto, nil
+		KeyValue: priv.Seed(),
+		PublicKey: &ed25519pb.Ed25519PublicKey{
+			Version:  ed25519SignerKeyVersion,
+			KeyValue: pub,
+		},
+	}, nil
 }
 
 // NewKeyData creates a new KeyData according to specification in  the given
@@ -97,7 +94,7 @@
 	return &tinkpb.KeyData{
 		TypeUrl:         ed25519SignerTypeURL,
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
@@ -128,6 +125,37 @@
 	return ed25519SignerTypeURL
 }
 
+// KeyMaterialType returns the key material type of this key manager.
+func (km *ed25519SignerKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_ASYMMETRIC_PRIVATE
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+// Unlike NewKey, DeriveKey validates serializedKeyFormat's version.
+func (km *ed25519SignerKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	keyFormat := new(ed25519pb.Ed25519KeyFormat)
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, fmt.Errorf("ed25519_signer_key_manager: %v", err)
+	}
+	err := keyset.ValidateKeyVersion(keyFormat.Version, ed25519SignerKeyVersion)
+	if err != nil {
+		return nil, fmt.Errorf("ed25519_signer_key_manager: %v", err)
+	}
+
+	pub, priv, err := ed25519.GenerateKey(pseudorandomness)
+	if err != nil {
+		return nil, fmt.Errorf("ed25519_signer_key_manager: cannot generate ED25519 key: %s", err)
+	}
+	return &ed25519pb.Ed25519PrivateKey{
+		Version:  ed25519SignerKeyVersion,
+		KeyValue: priv.Seed(),
+		PublicKey: &ed25519pb.Ed25519PublicKey{
+			Version:  ed25519SignerKeyVersion,
+			KeyValue: pub,
+		},
+	}, nil
+}
+
 // validateKey validates the given ED25519PrivateKey.
 func (km *ed25519SignerKeyManager) validateKey(key *ed25519pb.Ed25519PrivateKey) error {
 	if err := keyset.ValidateKeyVersion(key.Version, ed25519SignerKeyVersion); err != nil {
diff --git a/go/signature/ed25519_signer_key_manager_test.go b/go/signature/ed25519_signer_key_manager_test.go
index efab432..fb95797 100644
--- a/go/signature/ed25519_signer_key_manager_test.go
+++ b/go/signature/ed25519_signer_key_manager_test.go
@@ -17,11 +17,16 @@
 package signature_test
 
 import (
+	"bytes"
+	"crypto/ed25519"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/signature/subtle"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
@@ -156,6 +161,145 @@
 	}
 }
 
+func TestED25519KeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.ED25519SignerTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.ED25519SignerTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tinkpb.KeyData_ASYMMETRIC_PRIVATE; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestED25519DeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.ED25519SignerTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.ED25519SignerTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&ed25519pb.Ed25519KeyFormat{Version: testutil.ED25519SignerKeyVersion})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			// nil unmarshals to an empty proto, which implies version = 0.
+			name:      "nil",
+			keyFormat: nil,
+		},
+		{
+			// An empty proto implies version = 0.
+			name:      "empty",
+			keyFormat: []byte{},
+		},
+		{
+			name:      "specified",
+			keyFormat: keyFormat,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			rand := random.GetRandomBytes(ed25519.SeedSize)
+			buf := &bytes.Buffer{}
+			buf.Write(rand) // never returns a non-nil error
+			k, err := keyManager.DeriveKey(test.keyFormat, buf)
+			if err != nil {
+				t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+			}
+			key := k.(*ed25519pb.Ed25519PrivateKey)
+			if got, want := len(key.KeyValue), ed25519.SeedSize; got != want {
+				t.Errorf("private key length = %d, want %d", got, want)
+			}
+			if diff := cmp.Diff(key.KeyValue, rand[:ed25519.SeedSize]); diff != "" {
+				t.Errorf("incorrect derived private key: diff = %v", diff)
+			}
+			if got, want := len(key.PublicKey.KeyValue), ed25519.PublicKeySize; got != want {
+				t.Errorf("public key length = %d, want %d", got, want)
+			}
+			if diff := cmp.Diff(key.PublicKey.KeyValue, []byte(ed25519.NewKeyFromSeed(rand))[32:]); diff != "" {
+				t.Errorf("incorrect derived public key: diff = %v", diff)
+			}
+		})
+	}
+}
+
+func TestED25519DeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.ED25519SignerTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.ED25519SignerTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	invalidVersion, err := proto.Marshal(&ed25519pb.Ed25519KeyFormat{Version: 10})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "invalid version",
+			keyFormat: invalidVersion,
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(ed25519.SeedSize))
+			if _, err := keyManager.DeriveKey(test.keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestED25519DeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.ED25519SignerTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.ED25519SignerTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&ed25519pb.Ed25519KeyFormat{Version: 0})
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(ed25519.SeedSize))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(ed25519.SeedSize - 1))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func validateED25519PrivateKey(key *ed25519pb.Ed25519PrivateKey) error {
 	if key.Version != testutil.ED25519SignerKeyVersion {
 		return fmt.Errorf("incorrect private key's version: expect %d, got %d",
diff --git a/go/signature/ed25519_verifier_key_manager.go b/go/signature/ed25519_verifier_key_manager.go
index b741d99..9b4353a 100644
--- a/go/signature/ed25519_verifier_key_manager.go
+++ b/go/signature/ed25519_verifier_key_manager.go
@@ -17,9 +17,9 @@
 package signature
 
 import (
+	"crypto/ed25519"
 	"fmt"
 
-	"golang.org/x/crypto/ed25519"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/signature/subtle"
diff --git a/go/signature/internal/BUILD.bazel b/go/signature/internal/BUILD.bazel
deleted file mode 100644
index 39315df..0000000
--- a/go/signature/internal/BUILD.bazel
+++ /dev/null
@@ -1,40 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-
-go_library(
-    name = "internal",
-    srcs = [
-        "internal.go",
-        "rsa.go",
-        "rsassapkcs1_signer.go",
-        "rsassapkcs1_verifier.go",
-    ],
-    importpath = "github.com/google/tink/go/signature/internal",
-    visibility = ["//signature:__subpackages__"],
-    deps = [
-        "//subtle",
-        "//tink",
-    ],
-)
-
-alias(
-    name = "go_default_library",
-    actual = ":internal",
-    visibility = ["//signature:__subpackages__"],
-)
-
-go_test(
-    name = "internal_test",
-    srcs = [
-        "rsa_test.go",
-        "rsassapkcs1_signer_verifier_test.go",
-    ],
-    data = [
-        "@wycheproof//testvectors:all",
-    ],
-    deps = [
-        ":internal",
-        "//subtle",
-        "//subtle/random",
-        "//testutil",
-    ],
-)
diff --git a/go/signature/internal/internal.go b/go/signature/internal/internal.go
deleted file mode 100644
index b6ce176..0000000
--- a/go/signature/internal/internal.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Package internal implements digital signatures.
-package internal
diff --git a/go/signature/internal/rsa.go b/go/signature/internal/rsa.go
deleted file mode 100644
index 5daadf5..0000000
--- a/go/signature/internal/rsa.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package internal
-
-import (
-	"crypto"
-	"crypto/rsa"
-	"fmt"
-	"hash"
-
-	"github.com/google/tink/go/subtle"
-)
-
-const (
-	rsaMinModulusSizeInBits  = 2048
-	rsaDefaultPublicExponent = 65537
-)
-
-// RSAValidModulusSizeInBits the size in bits for an RSA key.
-func RSAValidModulusSizeInBits(m int) error {
-	if m < rsaMinModulusSizeInBits {
-		return fmt.Errorf("modulus size too small, must be >= %d", rsaMinModulusSizeInBits)
-	}
-	return nil
-}
-
-// RSAValidPublicExponent validates a public RSA exponent.
-func RSAValidPublicExponent(e int) error {
-	// crypto/rsa uses the following hardcoded public exponent value.
-	if e != rsaDefaultPublicExponent {
-		return fmt.Errorf("invalid public exponent")
-	}
-	return nil
-}
-
-// HashSafeForSignature checks whether a hash function is safe to use with digital signatures
-// that require collision resistance.
-func HashSafeForSignature(hashAlg string) error {
-	switch hashAlg {
-	case "SHA256", "SHA384", "SHA512":
-		return nil
-	default:
-		return fmt.Errorf("hash function not safe for digital signatures: %q", hashAlg)
-	}
-}
-
-func validRSAPublicKey(publicKey *rsa.PublicKey) error {
-	if err := RSAValidModulusSizeInBits(publicKey.N.BitLen()); err != nil {
-		return err
-	}
-	return RSAValidPublicExponent(publicKey.E)
-}
-
-func hashID(hashAlg string) (crypto.Hash, error) {
-	switch hashAlg {
-	case "SHA256":
-		return crypto.SHA256, nil
-	case "SHA384":
-		return crypto.SHA384, nil
-	case "SHA512":
-		return crypto.SHA512, nil
-	default:
-		return 0, fmt.Errorf("invalid hash function: %q", hashAlg)
-	}
-}
-
-func rsaHashFunc(hashAlg string) (func() hash.Hash, crypto.Hash, error) {
-	if err := HashSafeForSignature(hashAlg); err != nil {
-		return nil, 0, err
-	}
-	hashFunc := subtle.GetHashFunc(hashAlg)
-	if hashFunc == nil {
-		return nil, 0, fmt.Errorf("invalid hash function: %q", hashAlg)
-	}
-	hashID, err := hashID(hashAlg)
-	if err != nil {
-		return nil, 0, err
-	}
-	return hashFunc, hashID, nil
-}
diff --git a/go/signature/internal/rsa_test.go b/go/signature/internal/rsa_test.go
deleted file mode 100644
index 447a468..0000000
--- a/go/signature/internal/rsa_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package internal_test
-
-import (
-	"testing"
-
-	"github.com/google/tink/go/signature/internal"
-)
-
-func TestValidatePublicExponent(t *testing.T) {
-	if err := internal.RSAValidPublicExponent(65537); err != nil {
-		t.Errorf("ValidPublicExponent(65537) err = %v, want nil", err)
-	}
-}
-
-func TestValidateInvalidPublicExponentFails(t *testing.T) {
-	if err := internal.RSAValidPublicExponent(3); err == nil {
-		t.Errorf("ValidPublicExponent(3) err = nil, want error")
-	}
-}
-
-func TestValidateModulusSizeInBits(t *testing.T) {
-	if err := internal.RSAValidModulusSizeInBits(2048); err != nil {
-		t.Errorf("ValidModulusSizeInBits(2048) err = %v, want nil", err)
-	}
-}
-
-func TestValidateInvalidModulusSizeInBitsFails(t *testing.T) {
-	if err := internal.RSAValidModulusSizeInBits(1024); err == nil {
-		t.Errorf("ValidModulusSizeInBits(1024) err = nil, want error")
-	}
-}
-
-func TestHashSafeForSignature(t *testing.T) {
-	for _, h := range []string{
-		"SHA256",
-		"SHA384",
-		"SHA512",
-	} {
-		t.Run(h, func(t *testing.T) {
-			if err := internal.HashSafeForSignature(h); err != nil {
-				t.Errorf("HashSafeForSignature(%q)  err = %v, want nil", h, err)
-			}
-		})
-	}
-}
-
-func TestHashNotSafeForSignatureFails(t *testing.T) {
-	for _, h := range []string{
-		"SHA1",
-		"SHA224",
-		"MD5",
-	} {
-		t.Run(h, func(t *testing.T) {
-			if err := internal.HashSafeForSignature(h); err == nil {
-				t.Errorf("HashSafeForSignature(%q)  err = nil, want error", h)
-			}
-		})
-	}
-}
diff --git a/go/signature/internal/rsassapkcs1_signer.go b/go/signature/internal/rsassapkcs1_signer.go
deleted file mode 100644
index 9195670..0000000
--- a/go/signature/internal/rsassapkcs1_signer.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package internal
-
-import (
-	"crypto"
-	"crypto/rand"
-	"crypto/rsa"
-	"hash"
-
-	"github.com/google/tink/go/subtle"
-	"github.com/google/tink/go/tink"
-)
-
-// RSA_SSA_PKCS1_Signer is an implementation of Signer for RSA-SSA-PKCS1.
-type RSA_SSA_PKCS1_Signer struct {
-	privateKey *rsa.PrivateKey
-	hashFunc   func() hash.Hash
-	hashID     crypto.Hash
-}
-
-var _ (tink.Signer) = (*RSA_SSA_PKCS1_Signer)(nil)
-
-// New_RSA_SSA_PKCS1_Signer creates a new intance of RSA_SSA_PKCS1_Signer.
-func New_RSA_SSA_PKCS1_Signer(hashAlg string, privKey *rsa.PrivateKey) (*RSA_SSA_PKCS1_Signer, error) {
-	if err := validRSAPublicKey(privKey.Public().(*rsa.PublicKey)); err != nil {
-		return nil, err
-	}
-	hashFunc, hashID, err := rsaHashFunc(hashAlg)
-	if err != nil {
-		return nil, err
-	}
-	return &RSA_SSA_PKCS1_Signer{
-		privateKey: privKey,
-		hashFunc:   hashFunc,
-		hashID:     hashID,
-	}, nil
-}
-
-// Sign computes a signature for the given data.
-func (s *RSA_SSA_PKCS1_Signer) Sign(data []byte) ([]byte, error) {
-	digest, err := subtle.ComputeHash(s.hashFunc, data)
-	if err != nil {
-		return nil, err
-	}
-	return rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashID, digest)
-}
diff --git a/go/signature/internal/rsassapkcs1_signer_verifier_test.go b/go/signature/internal/rsassapkcs1_signer_verifier_test.go
deleted file mode 100644
index 6ba9738..0000000
--- a/go/signature/internal/rsassapkcs1_signer_verifier_test.go
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package internal_test
-
-import (
-	"crypto/rand"
-	"crypto/rsa"
-	"fmt"
-	"math/big"
-	"testing"
-
-	"github.com/google/tink/go/signature/internal"
-	"github.com/google/tink/go/subtle/random"
-	"github.com/google/tink/go/subtle"
-	"github.com/google/tink/go/testutil"
-)
-
-func TestRSASSAPKCS1SignVerify(t *testing.T) {
-	data := random.GetRandomBytes(20)
-	hash := "SHA256"
-	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
-	if err != nil {
-		t.Fatalf("rsa.GenerateKey(rand.Reader, 2048) err = %v, want nil", err)
-	}
-	signer, err := internal.New_RSA_SSA_PKCS1_Signer(hash, privKey)
-	if err != nil {
-		t.Fatalf("New_RSA_SSA_PKCS1_Signer() err = %v, want nil", err)
-	}
-	verifier, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, &privKey.PublicKey)
-	if err != nil {
-		t.Fatalf("New_RSA_SSA_PKCS1_Verifier() err = %v, want nil", err)
-	}
-	signature, err := signer.Sign(data)
-	if err != nil {
-		t.Fatalf("Sign() err = %v, want nil", err)
-	}
-	if err := verifier.Verify(signature, data); err != nil {
-		t.Fatalf("Verify() err = %v, want nil", err)
-	}
-}
-
-func TestRSASSAPKCS1ModifySignatureFails(t *testing.T) {
-	data := random.GetRandomBytes(20)
-	hash := "SHA256"
-	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
-	if err != nil {
-		t.Fatalf("rsa.GenerateKey(rand.Reader, 2048) err = %v, want nil", err)
-	}
-	signer, err := internal.New_RSA_SSA_PKCS1_Signer(hash, privKey)
-	if err != nil {
-		t.Fatalf("New_RSA_SSA_PKCS1_Signer() err = %v, want nil", err)
-	}
-	signature, err := signer.Sign(data)
-	if err != nil {
-		t.Fatalf("Sign() err = %v, want nil", err)
-	}
-	verifier, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, &privKey.PublicKey)
-	if err != nil {
-		t.Fatalf("New_RSA_SSA_PKCS1_Verifier() err = %v, want nil", err)
-	}
-	appendSig := append(signature, 0x01)
-	if err := verifier.Verify(appendSig, data); err == nil {
-		t.Fatalf("Verify() err = nil, want error")
-	}
-	truncSig := signature[:len(signature)-2]
-	if err := verifier.Verify(truncSig, data); err == nil {
-		t.Fatalf("Verify() err = nil, want error")
-	}
-	signature[0] <<= 1
-	if err := verifier.Verify(truncSig, data); err == nil {
-		t.Fatalf("Verify() err = nil, want error")
-	}
-}
-
-func TestNewRSASSAPKCS1SignerVerifierInvalidInput(t *testing.T) {
-	validPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
-	if err != nil {
-		t.Fatalf("rsa.GenerateKey(rand.Reader, 2048) err = %v, want nil", err)
-	}
-	rsaShortModulusKey, err := rsa.GenerateKey(rand.Reader, 1024)
-	if err != nil {
-		t.Fatalf("decoding rsa short modulus: %v", err)
-	}
-	testCases := []struct {
-		name    string
-		hash    string
-		privKey *rsa.PrivateKey
-	}{
-		{
-			name:    "weak signature hash algorithm",
-			hash:    "SHA1",
-			privKey: validPrivKey,
-		},
-		{
-			name: "invalid public key exponent",
-			hash: "SHA256",
-			privKey: &rsa.PrivateKey{
-				D:           validPrivKey.D,
-				Primes:      validPrivKey.Primes,
-				Precomputed: validPrivKey.Precomputed,
-				PublicKey: rsa.PublicKey{
-					N: validPrivKey.PublicKey.N,
-					E: 3,
-				},
-			},
-		},
-		{
-			name:    "small modulus size",
-			hash:    "SHA256",
-			privKey: rsaShortModulusKey,
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			if _, err := internal.New_RSA_SSA_PKCS1_Signer(tc.hash, tc.privKey); err == nil {
-				t.Errorf("New_RSA_SSA_PKCS1_Signer() err = nil, want error")
-			}
-			if _, err := internal.New_RSA_SSA_PKCS1_Verifier(tc.hash, &tc.privKey.PublicKey); err == nil {
-				t.Errorf("New_RSA_SSA_PKCS1_Verifier() err = nil, want error")
-			}
-		})
-	}
-}
-
-type rsaSSAPKCS1Suite struct {
-	testutil.WycheproofSuite
-	TestGroups []*rsaSSAPKCS1Group `json:"testGroups"`
-}
-
-type rsaSSAPKCS1Group struct {
-	testutil.WycheproofGroup
-	SHA   string             `json:"sha"`
-	E     testutil.HexBytes  `json:"e"`
-	N     testutil.HexBytes  `json:"n"`
-	Type  string             `json:"type"`
-	Tests []*rsaSSAPKCS1Case `json:"tests"`
-}
-
-type rsaSSAPKCS1Case struct {
-	testutil.WycheproofCase
-	Message   testutil.HexBytes `json:"msg"`
-	Signature testutil.HexBytes `json:"sig"`
-}
-
-func TestRSASSAPKCS1WycheproofCases(t *testing.T) {
-	testutil.SkipTestIfTestSrcDirIsNotSet(t)
-
-	testsRan := 0
-	for _, v := range []string{
-		"rsa_signature_2048_sha256_test.json",
-		"rsa_signature_3072_sha512_test.json",
-		"rsa_signature_4096_sha512_test.json",
-	} {
-		suite := &rsaSSAPKCS1Suite{}
-		if err := testutil.PopulateSuite(suite, v); err != nil {
-			t.Fatalf("testutil.PopulateSuite() err = %v, want nil", err)
-		}
-		for _, group := range suite.TestGroups {
-			hash := subtle.ConvertHashName(group.SHA)
-			if hash == "" {
-				t.Fatalf("invalid hash name")
-			}
-			publicKey := &rsa.PublicKey{
-				E: int(new(big.Int).SetBytes(group.E).Uint64()),
-				N: new(big.Int).SetBytes(group.N),
-			}
-			if publicKey.E != 65537 {
-				// golang "crypto/rsa" only supports 65537 as an exponent.
-				if _, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, publicKey); err == nil {
-					t.Errorf("NewRSASSAPKCS1Verifier() err = nil, want error")
-				}
-				continue
-			}
-			verifier, err := internal.New_RSA_SSA_PKCS1_Verifier(hash, publicKey)
-			if err != nil {
-				t.Fatalf("NewRSASSAPKCS1Verifier() err = %v, want nil", err)
-			}
-			for _, test := range group.Tests {
-				caseName := fmt.Sprintf("%s: %s-%s:Case-%d", v, group.Type, group.SHA, test.CaseID)
-				t.Run(caseName, func(t *testing.T) {
-					testsRan++
-					err := verifier.Verify(test.Signature, test.Message)
-					switch test.Result {
-					case "valid":
-						if err != nil {
-							t.Errorf("Verify() err = %v, want nil", err)
-						}
-					case "invalid":
-						if err == nil {
-							t.Errorf("Verify() err = nil, want error")
-						}
-					case "acceptable":
-						// TODO(b/230489047): Inspect flags to appropriately handle acceptable test cases.
-					default:
-						t.Errorf("unsupported test result: %q", test.Result)
-					}
-				})
-			}
-		}
-	}
-	if testsRan != 716 {
-		t.Errorf("testsRan = %d, want = %d", testsRan, 716)
-	}
-}
diff --git a/go/signature/internal/rsassapkcs1_verifier.go b/go/signature/internal/rsassapkcs1_verifier.go
deleted file mode 100644
index cc166ee..0000000
--- a/go/signature/internal/rsassapkcs1_verifier.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package internal
-
-import (
-	"crypto"
-	"crypto/rsa"
-	"hash"
-
-	"github.com/google/tink/go/subtle"
-	"github.com/google/tink/go/tink"
-)
-
-// RSA_SSA_PKCS1_Verifier is an implementation of Verifier for RSA-SSA-PKCS1.
-type RSA_SSA_PKCS1_Verifier struct {
-	publicKey *rsa.PublicKey
-	hashFunc  func() hash.Hash
-	hashID    crypto.Hash
-}
-
-var _ tink.Verifier = (*RSA_SSA_PKCS1_Verifier)(nil)
-
-// New_RSA_SSA_PKCS1_Verifier creates a new intance of RSASSAPKCS1Verifier.
-func New_RSA_SSA_PKCS1_Verifier(hashAlg string, pubKey *rsa.PublicKey) (*RSA_SSA_PKCS1_Verifier, error) {
-	if err := validRSAPublicKey(pubKey); err != nil {
-		return nil, err
-	}
-	hashFunc, hashID, err := rsaHashFunc(hashAlg)
-	if err != nil {
-		return nil, err
-	}
-	return &RSA_SSA_PKCS1_Verifier{
-		publicKey: pubKey,
-		hashFunc:  hashFunc,
-		hashID:    hashID,
-	}, nil
-}
-
-// Verify verifies whether the given signaure is valid for the given data.
-// It returns an error if the signature is not valid; nil otherwise.
-func (v *RSA_SSA_PKCS1_Verifier) Verify(signature, data []byte) error {
-	hashed, err := subtle.ComputeHash(v.hashFunc, data)
-	if err != nil {
-		return err
-	}
-	return rsa.VerifyPKCS1v15(v.publicKey, v.hashID, hashed, signature)
-}
diff --git a/go/signature/proto.go b/go/signature/proto.go
index 5151d2a..c235ad3 100644
--- a/go/signature/proto.go
+++ b/go/signature/proto.go
@@ -31,9 +31,7 @@
 }
 
 // newECDSAPrivateKey creates a ECDSAPrivateKey with the specified paramaters.
-func newECDSAPrivateKey(version uint32,
-	publicKey *ecdsapb.EcdsaPublicKey,
-	keyValue []byte) *ecdsapb.EcdsaPrivateKey {
+func newECDSAPrivateKey(version uint32, publicKey *ecdsapb.EcdsaPublicKey, keyValue []byte) *ecdsapb.EcdsaPrivateKey {
 	return &ecdsapb.EcdsaPrivateKey{
 		Version:   version,
 		PublicKey: publicKey,
@@ -42,9 +40,7 @@
 }
 
 // newECDSAPublicKey creates a ECDSAPublicKey with the specified paramaters.
-func newECDSAPublicKey(version uint32,
-	params *ecdsapb.EcdsaParams,
-	x []byte, y []byte) *ecdsapb.EcdsaPublicKey {
+func newECDSAPublicKey(version uint32, params *ecdsapb.EcdsaParams, x, y []byte) *ecdsapb.EcdsaPublicKey {
 	return &ecdsapb.EcdsaPublicKey{
 		Version: version,
 		Params:  params,
diff --git a/go/signature/rsa.go b/go/signature/rsa.go
index bfaab5f..7625bf7 100644
--- a/go/signature/rsa.go
+++ b/go/signature/rsa.go
@@ -17,9 +17,10 @@
 package signature
 
 import (
+	"fmt"
 	"math/big"
 
-	"github.com/google/tink/go/signature/internal"
+	internal "github.com/google/tink/go/internal/signature"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 )
 
@@ -34,7 +35,11 @@
 	if err := internal.RSAValidModulusSizeInBits(modSizeBits); err != nil {
 		return err
 	}
-	return internal.RSAValidPublicExponent(int(bytesToBigInt(pubExponent).Uint64()))
+	e := bytesToBigInt(pubExponent)
+	if !e.IsInt64() {
+		return fmt.Errorf("public exponent can't fit in a 64 bit integer")
+	}
+	return internal.RSAValidPublicExponent(int(e.Int64()))
 }
 
 func hashName(h commonpb.HashType) string {
diff --git a/go/signature/rsassapkcs1_signer_key_manager.go b/go/signature/rsassapkcs1_signer_key_manager.go
index 4a15a50..d8e97c0 100644
--- a/go/signature/rsassapkcs1_signer_key_manager.go
+++ b/go/signature/rsassapkcs1_signer_key_manager.go
@@ -25,8 +25,8 @@
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	internal "github.com/google/tink/go/internal/signature"
 	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/signature/internal"
 	rsassapkcs1pb "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -53,40 +53,47 @@
 	if err := proto.Unmarshal(serializedKey, key); err != nil {
 		return nil, err
 	}
-	if err := keyset.ValidateKeyVersion(key.Version, rsaSSAPKCS1SignerKeyVersion); err != nil {
+	if err := validateRSAPKCS1PrivateKey(key); err != nil {
 		return nil, err
 	}
-	if len(key.GetD()) == 0 ||
-		len(key.GetPublicKey().GetN()) == 0 ||
-		len(key.GetPublicKey().GetE()) == 0 ||
-		len(key.GetP()) == 0 ||
-		len(key.GetQ()) == 0 ||
-		len(key.GetDp()) == 0 ||
-		len(key.GetDq()) == 0 ||
-		len(key.GetCrt()) == 0 {
-		return nil, errInvalidRSASSAPKCS1SignKey
-	}
-	e := bytesToBigInt(key.PublicKey.E)
-	if !e.IsInt64() {
-		return nil, fmt.Errorf("rsassapkcs1_signer_key_manager: public exponent can't fit in 64 bit number")
-	}
 	privKey := &rsa.PrivateKey{
-		D: bytesToBigInt(key.D),
+		D: bytesToBigInt(key.GetD()),
 		PublicKey: rsa.PublicKey{
-			N: bytesToBigInt(key.PublicKey.N),
-			E: int(e.Uint64()),
+			N: bytesToBigInt(key.GetPublicKey().GetN()),
+			E: int(bytesToBigInt(key.GetPublicKey().GetE()).Int64()),
 		},
 		Primes: []*big.Int{
-			bytesToBigInt(key.P),
-			bytesToBigInt(key.Q),
+			bytesToBigInt(key.GetP()),
+			bytesToBigInt(key.GetQ()),
 		},
 		Precomputed: rsa.PrecomputedValues{
-			Dp:   bytesToBigInt(key.Dp),
-			Dq:   bytesToBigInt(key.Dq),
-			Qinv: bytesToBigInt(key.Crt),
+			Dp:   bytesToBigInt(key.GetDp()),
+			Dq:   bytesToBigInt(key.GetDq()),
+			Qinv: bytesToBigInt(key.GetCrt()),
 		},
 	}
-	return internal.New_RSA_SSA_PKCS1_Signer(hashName(key.PublicKey.Params.HashType), privKey)
+	h := hashName(key.GetPublicKey().GetParams().GetHashType())
+	if err := internal.Validate_RSA_SSA_PKCS1(h, privKey); err != nil {
+		return nil, err
+	}
+	return internal.New_RSA_SSA_PKCS1_Signer(h, privKey)
+}
+
+func validateRSAPKCS1PrivateKey(privKey *rsassapkcs1pb.RsaSsaPkcs1PrivateKey) error {
+	if err := keyset.ValidateKeyVersion(privKey.Version, rsaSSAPKCS1SignerKeyVersion); err != nil {
+		return err
+	}
+	if len(privKey.GetD()) == 0 ||
+		len(privKey.GetPublicKey().GetN()) == 0 ||
+		len(privKey.GetPublicKey().GetE()) == 0 ||
+		len(privKey.GetP()) == 0 ||
+		len(privKey.GetQ()) == 0 ||
+		len(privKey.GetDp()) == 0 ||
+		len(privKey.GetDq()) == 0 ||
+		len(privKey.GetCrt()) == 0 {
+		return errInvalidRSASSAPKCS1SignKey
+	}
+	return validateRSAPKCS1PublicKey(privKey.GetPublicKey())
 }
 
 func (km *rsaSSAPKCS1SignerKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
@@ -98,8 +105,8 @@
 		return nil, err
 	}
 	if err := validateRSAPubKeyParams(
-		keyFormat.Params.HashType,
-		int(keyFormat.ModulusSizeInBits),
+		keyFormat.GetParams().GetHashType(),
+		int(keyFormat.GetModulusSizeInBits()),
 		keyFormat.GetPublicExponent()); err != nil {
 		return nil, err
 	}
@@ -149,22 +156,10 @@
 	if err := proto.Unmarshal(serializedPrivKey, privKey); err != nil {
 		return nil, err
 	}
-	if privKey.GetPublicKey() == nil {
-		return nil, errInvalidRSASSAPKCS1SignKey
-	}
-	if err := keyset.ValidateKeyVersion(privKey.GetVersion(), rsaSSAPKCS1SignerKeyVersion); err != nil {
+	if err := validateRSAPKCS1PrivateKey(privKey); err != nil {
 		return nil, err
 	}
-	if err := keyset.ValidateKeyVersion(privKey.GetPublicKey().GetVersion(), rsaSSAPKCS1VerifierKeyVersion); err != nil {
-		return nil, err
-	}
-	if err := validateRSAPubKeyParams(
-		privKey.GetPublicKey().Params.HashType,
-		bytesToBigInt(privKey.GetPublicKey().GetN()).BitLen(),
-		privKey.GetPublicKey().GetE()); err != nil {
-		return nil, err
-	}
-	serializedPubKey, err := proto.Marshal(privKey.PublicKey)
+	serializedPubKey, err := proto.Marshal(privKey.GetPublicKey())
 	if err != nil {
 		return nil, err
 	}
diff --git a/go/signature/rsassapkcs1_signer_key_manager_test.go b/go/signature/rsassapkcs1_signer_key_manager_test.go
index 549381f..501323a 100644
--- a/go/signature/rsassapkcs1_signer_key_manager_test.go
+++ b/go/signature/rsassapkcs1_signer_key_manager_test.go
@@ -17,15 +17,16 @@
 package signature_test
 
 import (
+	"encoding/hex"
 	"math/big"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/testing/protocmp"
 	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/signature/internal"
+	internal "github.com/google/tink/go/internal/signature"
 	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/tink"
 	cpb "github.com/google/tink/go/proto/common_go_proto"
 	rsassapkcs1pb "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
@@ -58,11 +59,15 @@
 	}
 }
 
-func TestRSASSAPKCS1SignerKeyManagerPublicData(t *testing.T) {
+func TestRSASSAPKCS1SignerKeyManagerPublicKeyData(t *testing.T) {
 	skm, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
 	if err != nil {
 		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
 	}
+	vkm, err := registry.GetKeyManager(rsaPKCS1PublicTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PublicTypeURL, err)
+	}
 	privKey, err := makeValidRSAPKCS1Key()
 	if err != nil {
 		t.Fatalf("makeValidRSAPKCS1Key() err = %v, want nil", err)
@@ -71,92 +76,18 @@
 	if err != nil {
 		t.Fatalf("proto.Marshal() err = %v, want nil", err)
 	}
-	serializedPublic, err := proto.Marshal(privKey.PublicKey)
-	if err != nil {
-		t.Fatalf("proto.Marshal() err = %v, want nil", err)
-	}
 	got, err := skm.(registry.PrivateKeyManager).PublicKeyData(serializedPrivate)
 	if err != nil {
 		t.Fatalf("PublicKeyData() err = %v, want nil", err)
 	}
-	want := &tinkpb.KeyData{
-		TypeUrl:         rsaPKCS1PublicTypeURL,
-		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
-		Value:           serializedPublic,
+	if got.GetKeyMaterialType() != tinkpb.KeyData_ASYMMETRIC_PUBLIC {
+		t.Errorf("GetKeyMaterialType() = %q, want %q", got.GetKeyMaterialType(), tinkpb.KeyData_ASYMMETRIC_PUBLIC)
 	}
-	if !cmp.Equal(got, want, protocmp.Transform()) {
-		t.Errorf("got = %v, want = %v, with diff: %v", got, want, cmp.Diff(got, want, protocmp.Transform()))
+	if got.GetTypeUrl() != rsaPKCS1PublicTypeURL {
+		t.Errorf("GetTypeUrl() = %q, want %q", got.GetTypeUrl(), rsaPKCS1PublicTypeURL)
 	}
-}
-
-func TestRSASSAPKCS1SignerKeyManagerPublicDataFailsWithInvalidInput(t *testing.T) {
-	type testCase struct {
-		name   string
-		pubKey *rsassapkcs1pb.RsaSsaPkcs1PublicKey
-	}
-	validPrivKey, err := makeValidRSAPKCS1Key()
-	if err != nil {
-		t.Fatalf("makeValidRSAPKCS1Key() err = %v, want nil", err)
-	}
-	skm, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
-	if err != nil {
-		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
-	}
-	for _, tc := range []testCase{
-		{
-			name:   "nil public key",
-			pubKey: nil,
-		},
-		{
-			name: "invalid hash function",
-			pubKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
-				Version: validPrivKey.GetVersion(),
-				Params: &rsassapkcs1pb.RsaSsaPkcs1Params{
-					HashType: cpb.HashType_SHA1,
-				},
-				N: validPrivKey.GetPublicKey().GetN(),
-				E: validPrivKey.GetPublicKey().GetE(),
-			},
-		},
-		{
-			name: "invalid exponent",
-			pubKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
-				Version: validPrivKey.GetVersion(),
-				Params:  validPrivKey.GetPublicKey().GetParams(),
-				N:       validPrivKey.GetPublicKey().GetN(),
-				E:       []byte{0x08},
-			},
-		},
-		{
-			name: "invalid modulus",
-			pubKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
-				Version: validPrivKey.GetVersion(),
-				Params:  validPrivKey.GetPublicKey().GetParams(),
-				N:       []byte{0x00, 0x00, 0x00},
-				E:       validPrivKey.GetPublicKey().GetE(),
-			},
-		},
-		{
-			name: "invalid version",
-			pubKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
-				Version: validPrivKey.GetVersion() + 1,
-				Params:  validPrivKey.GetPublicKey().GetParams(),
-				N:       validPrivKey.GetPublicKey().GetN(),
-				E:       validPrivKey.GetPublicKey().GetE(),
-			},
-		},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			InvalidPub := validPrivKey
-			InvalidPub.PublicKey = tc.pubKey
-			serializedPrivateKey, err := proto.Marshal(InvalidPub)
-			if err != nil {
-				t.Fatalf("proto.Marshal() err= %v, want nil", err)
-			}
-			if _, err := skm.(registry.PrivateKeyManager).PublicKeyData(serializedPrivateKey); err == nil {
-				t.Fatalf("PublicKeyData() err = nil, want error")
-			}
-		})
+	if _, err := vkm.Primitive(got.GetValue()); err != nil {
+		t.Errorf("Primitive() err = %v, want nil", err)
 	}
 }
 
@@ -210,12 +141,19 @@
 func TestRSASSAPKCS1SignerKeyManagerPrimitiveWithInvalidInputFails(t *testing.T) {
 	km, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
 	if err != nil {
-		t.Errorf("cannot obtain RSASSAPKCS1Signer key manager: %s", err)
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
 	}
 	validPrivKey, err := makeValidRSAPKCS1Key()
 	if err != nil {
 		t.Fatalf("makeValidRSAPKCS1Key() err = %v, want nil", err)
 	}
+	serializedValidPrivate, err := proto.Marshal(validPrivKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := km.Primitive(serializedValidPrivate); err != nil {
+		t.Fatalf("Primitive(serializedValidPrivate) err = %v, want nil", err)
+	}
 	type testCase struct {
 		name string
 		key  *rsassapkcs1pb.RsaSsaPkcs1PrivateKey
@@ -245,7 +183,7 @@
 		{
 			name: "invalid hash algorithm ",
 			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
-				Version: validPrivKey.Version,
+				Version: validPrivKey.GetVersion(),
 				PublicKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
 					Version: validPrivKey.GetPublicKey().GetVersion(),
 					E:       validPrivKey.GetPublicKey().GetE(),
@@ -265,7 +203,7 @@
 		{
 			name: "invalid modulus",
 			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
-				Version: validPrivKey.Version,
+				Version: validPrivKey.GetVersion(),
 				PublicKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
 					Version: validPrivKey.GetPublicKey().GetVersion(),
 					E:       validPrivKey.GetPublicKey().GetE(),
@@ -283,7 +221,7 @@
 		{
 			name: "invalid public key exponent",
 			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
-				Version: validPrivKey.Version,
+				Version: validPrivKey.GetVersion(),
 				PublicKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
 					Version: validPrivKey.GetPublicKey().GetVersion(),
 					E:       []byte{0x06},
@@ -299,39 +237,82 @@
 			},
 		},
 		{
-			name: "invalid private key",
+			name: "invalid private key D value",
 			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
-				Version: validPrivKey.Version,
-				PublicKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
-					Version: validPrivKey.GetPublicKey().GetVersion(),
-					E:       validPrivKey.GetPublicKey().GetE(),
-					N:       validPrivKey.GetPublicKey().GetN(),
-					Params:  validPrivKey.GetPublicKey().GetParams(),
-				},
-				D:   nil,
-				P:   nil,
-				Q:   nil,
-				Dp:  validPrivKey.GetDp(),
-				Dq:  validPrivKey.GetDq(),
-				Crt: validPrivKey.GetCrt(),
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         nil,
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+
+		{
+			name: "invalid private key P value",
+			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         nil,
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
 			},
 		},
 		{
-			name: "invalid precomputed values in private key",
+			name: "invalid private key Q value",
 			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
-				Version: validPrivKey.Version,
-				PublicKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
-					Version: validPrivKey.GetPublicKey().GetVersion(),
-					E:       validPrivKey.GetPublicKey().GetE(),
-					N:       validPrivKey.GetPublicKey().GetN(),
-					Params:  validPrivKey.GetPublicKey().GetParams(),
-				},
-				D:   validPrivKey.GetD(),
-				P:   validPrivKey.GetP(),
-				Q:   validPrivKey.GetQ(),
-				Dp:  nil,
-				Dq:  nil,
-				Crt: nil,
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         nil,
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid precomputed Dp values in private key",
+			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        nil,
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid precomputed Dq values in private key",
+			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        nil,
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			name: "invalid precomputed Crt values in private key",
+			key: &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       nil,
 			},
 		},
 	} {
@@ -343,14 +324,37 @@
 			if _, err := km.Primitive(serializedKey); err == nil {
 				t.Errorf("Primitive() err = nil, want error")
 			}
+			if _, err := km.(registry.PrivateKeyManager).PublicKeyData(serializedKey); err == nil {
+				t.Errorf("PublicKeyData() err = nil, want error")
+			}
 		})
 	}
 }
 
+func TestRSASSAPKCS1SignerKeyManagerPrimitiveWithCorruptedKeyFails(t *testing.T) {
+	km, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
+	}
+	corruptedPrivKey, err := makeValidRSAPKCS1Key()
+	if err != nil {
+		t.Fatalf("makeValidRSAPKCS1Key() err = %v, want nil", err)
+	}
+	corruptedPrivKey.P[5] = byte(uint8(corruptedPrivKey.P[5] + 1))
+	corruptedPrivKey.P[10] = byte(uint8(corruptedPrivKey.P[10] + 1))
+	serializedCorruptedPrivate, err := proto.Marshal(corruptedPrivKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := km.Primitive(serializedCorruptedPrivate); err == nil {
+		t.Errorf("Primitive() err = nil, want error")
+	}
+}
+
 func TestRSASSAPKCS1SignerKeyManagerPrimitiveNewKey(t *testing.T) {
 	km, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
 	if err != nil {
-		t.Errorf("cannot obtain RSASSAPKCS1Signer key manager: %s", err)
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
 	}
 	validPrivKey, err := makeValidRSAPKCS1Key()
 	if err != nil {
@@ -399,7 +403,7 @@
 	}
 	km, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
 	if err != nil {
-		t.Errorf("cannot obtain RSASSAPKCS1Signer key manager: %s", err)
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
 	}
 	for _, tc := range []testCase{
 		{
@@ -452,7 +456,7 @@
 func TestRSASSAPKCS1SignerKeyManagerPrimitiveNewKeyData(t *testing.T) {
 	km, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
 	if err != nil {
-		t.Errorf("cannot obtain RSASSAPKCS1Signer key manager: %s", err)
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
 	}
 	keyFormat := &rsassapkcs1pb.RsaSsaPkcs1KeyFormat{
 		ModulusSizeInBits: 2048,
@@ -470,9 +474,51 @@
 		t.Fatalf("NewKeyData() err = %v, want nil", err)
 	}
 	if keyData.GetTypeUrl() != rsaPKCS1PrivateKeyTypeURL {
-		t.Fatalf("GetTypeUrl() = %v, want %v", keyData.GetTypeUrl(), rsaPKCS1PrivateKeyTypeURL)
+		t.Errorf("GetTypeUrl() = %v, want %v", keyData.GetTypeUrl(), rsaPKCS1PrivateKeyTypeURL)
 	}
 	if keyData.GetKeyMaterialType() != tinkpb.KeyData_ASYMMETRIC_PRIVATE {
-		t.Fatalf("GetKeyMaterialType() = %v, want %v", keyData.GetKeyMaterialType(), tinkpb.KeyData_ASYMMETRIC_PRIVATE)
+		t.Errorf("GetKeyMaterialType() = %v, want %v", keyData.GetKeyMaterialType(), tinkpb.KeyData_ASYMMETRIC_PRIVATE)
+	}
+	if _, err := km.Primitive(keyData.GetValue()); err != nil {
+		t.Errorf("Primitive() err = %v, want nil", err)
+	}
+}
+
+func TestRSASSAPKCS1SignerKeyManagerPrimitiveNISTTestVectors(t *testing.T) {
+	km, err := registry.GetKeyManager(rsaPKCS1PrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PrivateKeyTypeURL, err)
+	}
+	for _, tc := range nistPKCS1TestVectors {
+		t.Run(tc.name, func(t *testing.T) {
+			key, err := tc.ToProtoKey()
+			if err != nil {
+				t.Fatalf("tc.ToProtoKey() err = %v, want nil", err)
+			}
+			serializedKey, err := proto.Marshal(key)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			p, err := km.Primitive(serializedKey)
+			if err != nil {
+				t.Fatalf("km.Primitive() err = %v, want nil", err)
+			}
+			msg, err := hex.DecodeString(tc.msg)
+			if err != nil {
+				t.Fatalf("hex.DecodeString(tc.msg) err = %v, want nil", err)
+			}
+			signer, ok := p.(tink.Signer)
+			if !ok {
+				t.Fatalf("primitive isn't a Tink.Signer")
+			}
+			sig, err := signer.Sign(msg)
+			if err != nil {
+				t.Fatalf("p.(tink.Signer).Sign(msg) err = %v, want nil", err)
+			}
+			gotSig := hex.EncodeToString(sig)
+			if !cmp.Equal(gotSig, tc.sig) {
+				t.Errorf("Sign() = %q, want %q", gotSig, tc.sig)
+			}
+		})
 	}
 }
diff --git a/go/signature/rsassapkcs1_verifier_key_manager.go b/go/signature/rsassapkcs1_verifier_key_manager.go
index 24f8aa3..6942621 100644
--- a/go/signature/rsassapkcs1_verifier_key_manager.go
+++ b/go/signature/rsassapkcs1_verifier_key_manager.go
@@ -20,12 +20,11 @@
 	"crypto/rsa"
 	"errors"
 	"fmt"
-	"math/big"
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	internal "github.com/google/tink/go/internal/signature"
 	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/signature/internal"
 	rsassapkcs1pb "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -51,20 +50,26 @@
 	if err := proto.Unmarshal(serializedKey, key); err != nil {
 		return nil, err
 	}
-	if err := keyset.ValidateKeyVersion(key.Version, rsaSSAPKCS1VerifierKeyVersion); err != nil {
+	if err := validateRSAPKCS1PublicKey(key); err != nil {
 		return nil, err
 	}
-	e := new(big.Int).SetBytes(key.E)
-	if !e.IsInt64() {
-		return nil, fmt.Errorf("rsassapkcs1_verifier_key_manager: public exponent can't fit in 64 bit number")
-	}
 	keyData := &rsa.PublicKey{
-		E: int(e.Int64()),
-		N: new(big.Int).SetBytes(key.N),
+		E: int(bytesToBigInt(key.GetE()).Int64()),
+		N: bytesToBigInt(key.GetN()),
 	}
 	return internal.New_RSA_SSA_PKCS1_Verifier(hashName(key.Params.HashType), keyData)
 }
 
+func validateRSAPKCS1PublicKey(pubKey *rsassapkcs1pb.RsaSsaPkcs1PublicKey) error {
+	if err := keyset.ValidateKeyVersion(pubKey.GetVersion(), rsaSSAPKCS1VerifierKeyVersion); err != nil {
+		return err
+	}
+	return validateRSAPubKeyParams(
+		pubKey.GetParams().GetHashType(),
+		bytesToBigInt(pubKey.GetN()).BitLen(),
+		pubKey.GetE())
+}
+
 func (km *rsaSSAPKCS1VerifierKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
 	return nil, errRSASSAPKCS1NotImplemented
 }
diff --git a/go/signature/rsassapkcs1_verifier_key_manager_test.go b/go/signature/rsassapkcs1_verifier_key_manager_test.go
index 3fc93ce..583f218 100644
--- a/go/signature/rsassapkcs1_verifier_key_manager_test.go
+++ b/go/signature/rsassapkcs1_verifier_key_manager_test.go
@@ -19,13 +19,15 @@
 import (
 	"crypto/rand"
 	"crypto/rsa"
+	"encoding/hex"
 	"math/big"
 	"testing"
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/signature/internal"
+	internal "github.com/google/tink/go/internal/signature"
 	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/tink"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	rsassapkcs1pb "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto"
 )
@@ -207,3 +209,208 @@
 		})
 	}
 }
+
+// The following keys are from:
+// https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/Digital-Signatures
+// Publication FIPS 186-2
+// Signature Verification PKCS1 v1.5
+// Only keys with public exponent 65537 (aka: F4, 0x010001) where chosen since golang rsa/crypto
+// doesn't support other exponent values.
+// Precomputed values (dp, dq, crt) where calculated from the private key (p, q, d) using rsa/crypto
+// to avoid recomputing on each test.
+var (
+	nist2048PKCS1Key = &nistRSATestKey{
+		n:   "a911245a2cfb33d8ee375df9439f74e669c03a8d9acad25bd27acf3cd8bea7eb9dbe470155c7c72782c94861f7b573cd325639fb070e9ba6e621991aefa45106182e4d264be7068035595d7549052989b3e7fd04cabc94012c1278a0ef8672b1a51dd1a9e276816ba497dea24b4febe3dd8e977707bcd230ca6fb6f8a8bff9e6ba24fbadcd93f00126b19b396a38e6ef86d18fef945b9154c1963fb488c7025953511f86d05638bfe056493730bc6778446e59cd3c5c3acf07a0a3a64943793652f10e3292aa7a6d25a03181cc6f6ba0658d909e59ce2a02bacc9766fd8c4fbd4ed9c23a866844b8a794d49e505f9f944870a71aadbe5338039825c2dff81af3",
+		e:   "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001",
+		p:   "bf96de108963b5113399b664765efe046e2dafdb70d6e5e29dc6ec89b789b059348d74d89129c7ade9ddb404c6dc3a3437c7fc9f23bc38dadc8ffd0ff757999f5c2d510b993056147ccdf421e03d0be2c74ec333a9677c430cc604f5550d0d86defdde71488e3db889c699a5cacb44dcdae2f3cca38695e783e6f6250827efb1",
+		q:   "e1e7e33618d1b64d6862c132e4b1cd5fabad20ce62bd97bce2a3f5ad2da67bb0a7f0b9e48335a33b7b95e77ec4c47e91416881f9f7c23f9bc1918cc644335c74260e90cd7b2e0fed802f19e78c5ed80a431b38630d82f982c74a0381b8ecf943c60810fae90574e216357b2535002316d9529cb56420f3cc82dea37cb624e1e3",
+		d:   "290d117f97d672f3647c2b24402832b153d22a25820567688645ed95ffa6e38d116347486ab4b485c27aef4962653bb60257ef82256785a1d3d52aa0e0b94c37279dee7bb308688aaee98108de6f1373ed2c12429c9b8770756c12c03908b346b129f963bfaa38a8937190cc656f057ef1a812dd0312f51285c4f46f9241f3028ea6a61db0e9255976469f5d5542ced55ee2d6f4afe766c0a70f49871d369dd8f3a82a7141639efd4a1f4a4009821c3c2b9f5c5f5eef99a5f00fbd8bc8191a3654e8f8d8ce12d90e5ff2a4c530b76306c8c56e0549a6f277ab2af3a60cccbf4bb4b2cb47f04f211f8b86aa653bf6913f3b5ed190c51b5958e40597a2dfd30061",
+		dp:  "bce35c3a8789d3098b8b1fa4ba837b03193157f10cb6025dc35a4cf896085ce2060af4c9538d127de7559a571f4c1ee23ea09ff2b203af36304091a9fd1cd3aba6f052b811a6f3272dc8cbc9de4fb1793b30ef08ef1ac50b41fbb505bf7da7f971be6f61d6bbce243349a7502ab8ef42a357203080847f248b09d961b741d071",
+		dq:  "629669813d59a03eadf4932e1bc240c7a4cb7c8ab56ada62b3622ca07450b8a042da7ab5f05123389d59b15a9092d44d9e06f6da5936ebbd94bf6979496044d3e79be9b3d33329fe5337bb0d63242d126570e6adcbc2c21341d7da29edc375910f468bea84713e2e40d4fc3623a838a80b15d39011ef939647f2d3d464453a53",
+		crt: "1261dce1e3d5d006948baf609ab5d114780c85c42b7065ddf0ea41d0d9e1bce2b154086551e425f36f46fb0da62ba7e054b95e5207ee08a6e1aa68cc611abe3c57e20a275cef95b4234b2539e1ed48e5a2363c4f2cb84178dbf6d7bf968a722ece1687376703099ccc182a30d5761b5aa8aedd33941997c63f678f4aba1c0d82",
+	}
+	nist3072PKCS1Key = &nistRSATestKey{
+		n:   "f33d3234f13272c3b2b6821ce4805663ff2e8b0d2a47de363d97fc9cc879cc6b40f9e53aea695dc538a0d2b558498829aac327eacbcd889e172b34f90745c5d528b7e82605f1a58fd228ec7fd4b6f476f393864f48dc47097c8a780a2ecc02f748138dbd7df99c52d822a2e5154c6047fb0eeb4f49da38edcae3c32d3fde435f291f96cad1e09e1030ad7efb4944b69e074d0d7964becb3cb86238d8d293bef3030d141d14868bc21fa133e9de1115f749991cf86ef506e663ac162b2c8567ff131a6b467a6f564d6c588860becbd88970354198ecdd4f1f4baee8f8bdaf7255835385f5673625f113550b123628a0be3994d91c3a19a82e5d73448dabf684ee6794fba7a2b1afbee0287e5a11180c29ce0896795d52ac7f408fe28e8e9116fe0b61a1083f95c5227d62d5537b5040b79e21b3a8e83c225bf3efebb2f808541e97d28a2468359fc60f588e74faad611262064628a25d8d61f9d03d8b21cca515595aaf2343a759b74a6a8afeffca139a389aa281995cd18e16a9cf7b7ff0dddb",
+		e:   "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001",
+		p:   "f3fcc6aae575312778d9e896acfd7c1aa4c5524f20453e8bab255363164afa7124b2425587a077fa0bfaf61b12ef3f0540dc4c9e777122a60610a53d1d75b0a5859c654a8ddfc2ff4860758bf5a6f264bf8bc2baa7551eb7be23bc06978be992fc81d890e07a3abf95d20eee3f6bbbc089985cac96395b473b2741c66bd2ccbef228432f66b906c15b19694dd786c29f06cbc17b2e6400dde4e3db85819382b3d05a4c3009f092d40d05ed5b2e0428a214e15a7aca09b47120b9ea6cb4084fcd",
+		q:   "ff36fcdc519fe10c69aa0dde2cf3bc72cf2ce9a54ac063d809b523f4e5d7ddaa5413d500dc21f409ce661bf33018b748fba3966d874bf96f4313eafea9decfba71d540ae0508161a3658c4762d94ebb3a4e228c45661315db30c20fbd9e20e24c044e4f0b49e6ec80949b16e0ab07f3d32b248b39f48332cc3686df05d29c170a7276acdd129259aaf018ae3afb49b6e0ddb9e404a492863daee7be71dfb11279047794e45f399a9665796d32d5e65956a13a6fbe992b36bf4c842f5f519ac47",
+		d:   "078ad6bde08572c73c5e94bdf0f1efd12da3a8fe7c035027236acd3e4ea86edd1bddd92ca601d012b7aa4e4e543caccdb49f54e35ecac7ee8b03b522a1b09957fe3ebc4d42dd96dcd4f9f2e215dba47749c9ec7369ec21571b21af638ac6f05d45980c4af0c1e6db2f70dcd738a803cb9cee7dffc3e7a735ec6c2541b270cc6b0225ef71939220f9ef35cf5c5b0dc291e237bd456333d803d12883e0694b2e891a3248ca36b838a8de27e154624fea5149857214b15ac4b4d67e0ab944ff5000552c66f833239f3d4c27a76cb14dd181d3f52a12cb4f3f9518dcb68c9db923e677d6f733febc93152c311880205a5b73ef323c07b87cd55549156b0452656f0f11a2c430924f426a22117c04dea477588fe092bbad1a4bf2a0a7b6befcffa3f91f4d308ff24515dae15897e9ab00bf755f9a366057b66f095573a9f881bc48e3711de800b3c0155c976957c40eab8834c7b7911d426fd7484e895fc8d8bb5ef7b562da7a846fd6bd013cef55ff3b11de43fe6da0c90b59dba4bb060ce4caf3cd",
+		dp:  "7c2a7fd028c5e325fb52aa1344261c2a5300384b1c5920e3634db38a11a6469d9dd739fadcf2c51bf34cdc421af8b651ae186ec5967374f698cf8fc7f25e1a6fa1f75d74fb8e8c65ee2768aab971249a3100a730e64763428ef9108f2a4081b5d3db20a35a19da1bf5dad8ce5668353c5ec9b32001b35ec8794a1927296835da56d2369ec0e01897fe0c88929cc46ff70e365358a4db2fb5bcee58a130b82923e93c8ad947b5ae834bbd6075ae8d5f405ceff263dabde59e4cd15083d17b0961",
+		dq:  "6a6ac44f1dddfb9a10692f35282b4db5d5bb55856dc10120f1134df5ececf0e9f7faf9034dc6fe9a242d21946ac6b38e4417373f5e7e0879235027d99e7d60c2ce7a6c68e38236ad21622c3156da54d9e873c129f516bbdde52db6872d97fbebc91c3116494a12c9684e0924e86225fc1faa85741883a38b13c3f4ab983d3402c4404461a3c8737ca7628e46585a87c1011845496b704bde2f48e7f33be6178616bc26d1c38b4ad47eef20ddd77a18039062b76b2d3ed57fbb66d1bcfb41843b",
+		crt: "9779d59e838e714e0f04fc936d40863b2bf492f446eb90cfe58b27205c2745e59dd7b5becd7de1f01f1d5a5b1d40e4d2281d668d545d4efc0bedaf5e7cdb4fed310661403f5f31e0e8ea23adab382ea09e3610c7548d0ed835bc693206fbee91a66f1ab9c2c80bdda3c26e1bcfd78bbf0f5fc13d646922d395848f27f5316380b196d4ee422e77309c06b9439216d3fff1c762ef662bc3b683e05b5c29af42fd765c7bc1b1356f1618d02ac9f36ca1637bc6651f8478743bd4f83ad37c5fc915",
+	}
+	nist4096PKCS1Key = &nistRSATestKey{
+		n:   "b10935650754c1a27e54f9ee525c77045d1a056baedc8c1ba05a3d66322c8e2e41689ca02b8e6ec55d3331cd714e161b35c774311540c6410cc3b130e65cec325377ccf18a5d77054afaa737e65b68662327627f5d1092df98c08b60a3ba67599b1406986f441528d2f1f5d6fe5ec6b8aa797dbdb3162876b08298237af81c7004863576af572ddd66c487cf32a82d0f9fe260f8f2681613208aec55723622f50d3cb3bd33b1226d5b5b9ea2c97a2c59054e31536849ec2f0f2597c077e621ddea0a22ddfb1df925c9f1941cd431afce0398d28e736cfeab3dd7f062a65a2a4c1ddca1f32afa03b4ddfd8b7ca40cc0ce2ed42229d753f03cc6d8bcdca76ab7c2703c5ddad08aa9bf9a27740a8b23168c6b55917bd3d5c1c057c34d1efb1411da51ab745519bd92b9432fea8fadcb9a70e5dc3adcd3081d3333507b7b998886e988296628dd7265e64eab557712556cc492377f15a078dcb620a6e7f051a1fb8754efdca3de253dd0aad4205b032659e4a4fb307f074fbde340702b5911acb34737d7834600cf2ac77f62f83ccc9ba78ff911021b9f8ad1bf421430ae0d64916944b504542c0f6263c48e5e233ef1869df1efc2ccf6f4a95e4db2de7b4cb1a992bef6573511bab4bf2cd58e72a0c235f56dfc182df4dfffb69433a5677415f35d84f5125f5200eb57868cce6b74c5833417990e318372c9239a36dca1a0b28809",
+		e:   "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001",
+		p:   "bd4e8bb7fd7ef1e39d71de06b0001bdadcc81b0edf2226e0d056b7eea70b2249000279cc1c04b1ac2919014fc3fb8b62baca3e261601fb0a58a9f67f03cd60085b2d43906d36ad014f321012a9bde9617478a0c10201afd53f2207de3648afd1d737afadf7fd2c0b9824d4f66b2c7dfe93390888ac088c680c27b1b2486659ccfa8986c8c23f78f18b5815a410328e629e7440221bffd8ec4722bba3420da5234f898f8cd7e6d7dcb1349bc4a0b665b41d930e3957cfdc88797aee5b2b27dafb5ba0949e3dd892f490212dfd249f4b1d99fd3b72695ee0652997127f0b9b417fa8365ba9fd103b978309d9baa9d401902cc107cb8d2af7ce04660900e3707ab3",
+		q:   "ef67f69838735c055145d21fccb42298642177fe3fadc39070a95e4fc04ff058aeaf9070b4eb2de1cca72d8533bc55206d2ce9f2895b148da67c89e5b6496ba682f76bcaef69306a7fa4fbd41a838bdf0fab3e7b56c27a8c18dc4bf970364dff7427cdcc6f532b49712282370a718b7d5287bfc02c4abc35ccb2eab3777f5e0d8a27ff9ebe13e725aa0a0cd48aee1fa33ea6b4ea965ba42fcce7af3c528a6675cedf4969640f2ca73345dfd322620df9dcf16520195df8232061e2bc89c12de24838f255e7b1c17713ba435d5a351e263350198b3fb881b8ce0acb5aa58b7afaff184489d167c9af21e40e2ba9fa69b44a3854329385c97df0de24dc283a4053",
+		d:   "25e0b881e32da938611b4156525ce24216c168837fa84479ecb72207e9984adb6eb7393bb3d607b1469d9b7c3f4fdbbefaa4b0218850919a7d66a954b315129c39eb99f7dc08df5c4c8c90968f3ce37b66ee184ef3d485f83d308521aa2649d28c319eafa2aec87031a1ff5d7e933ca56a2410593425fb865981b7976fca021b9d7c3198312fcfea5d0093a62b4a7c49a985c005c3a7ad816e270b25c507fc36be1c4cc0a07cb7c6fa130240062793b18047189aa5e79b16fe80a6955191f5910b701bc1aee6dcd5cefd57194bf54d8e208ae41202744190d5ec8bcc2f977f11461a5cb4306fc9b73afff2863a7b580d454bb1fb8dccb1cbef27945109a8f5a3d2b1a32566aa4e8c01a62a1d735117ff5a6a3cc7e70756e7df10e97ee75c5f8ef89fb0a97d7a35698069f59a9f365d4396ab9a764c2fbefb840faf5f57737801b73ac6f9f524167b4f3a567aff999a0db10d55d82155720a5ddc63c35b6a632a3da59c16bd0c143c437661dcd339652ae42f54f8b2d8e52672613772abfe8cf0d6ebbbccc764b51b3eda2ae28d4ba8747a430ccc32c73baea63d050b86210c485ac9554606070764cd06b423efdda4059fc45ffd7193f7123d14014d5bbe5b542476e7bfdc4905731a0d9987fe4a68cf6077c3df63778af08a1f4eb8f00a4a8e03aa8726f43fa829d87c2d0a32e16b47a3f0472a6368ec50a144234c802e6919",
+		dp:  "0ae4891f962386d19d0e9f42ed3fa45aac978b0f0901d310de8c0edb599b4766c1ec628bbf14fa1038f12a652796c2c7748e0c936e72c0ba30addef42208e03cbada58e7e790dcd5957400fec1eb9e912ffd7cea7e2e10ab098df0bbf58dab283ce50463d3402b17a3b282da87023161c3a0e57fcfbe522dee7d1e396ef70cb5c1b8c61ba929b3d0da3ec04807729144d56f44fd7175004b60307c71816c7d9311918dc401ec53816c64e58da3ddbaee69413bf14abf3826562f1fa5f94ebac7f9d6bc967a628ada2daceb1384d6f1a08b6ac9cfe486440d2e1e763eff30f8ccdaa5fe1242f07b2d55a9ec70543351bfb5038a6a48fe2ef218c8b23dedd85c07",
+		dq:  "9bf8c68e8b9094ae1e31f7e0a1d3e60a148a3d8bd65ed5df5f96e88bdac5f9d73d0fc271bb5cd10a9ff376d3a64e17c3c57d1279e20505d1f75a81d8b7b703bc7aecc93c7057bea453bee01662a3bb57baf49d036c15ce13420b1c30496c07cadb192799fe19584543c0f0c6fd35d663f285e0664a34f283b6760634a030c9ccd66a92be1026155cd37832bdb239cb40e68b63a8c606b4643401e987ca5ac2c013e42306d79a8f43eb42a5bcff5494b869ba97609f463a68602b85b5c1a5aac816b78b226e8dbf765dd2e71a85afbf91b1b288c1d0e4db16d49df1b87fcbec766405a2798b852bbfbebbbe83b1fd242ac2840a4edd0fb7a3266f03e2af0eac63",
+		crt: "87f4c4ddef9c241eae1215e92f30e2252519ac18c6cdf8906bca21627fa7288bab9a0efd1ea6fb8562479bec1de7ce7cf2f86cc862df35a3766559a87a4dc2a12d20f30815c68bfdee87beabfb5cce965f5fddb0ad59153df8bce3acfa279aad0202f94f2a9b48049e8a3e5a1af82d63269b5d8ea864b8b1239d2b5a74496a116b23070932f7a7f4b13140e17826bfed5004f7247dfe1a8a0943b9f6fc0a856fb96159c61a2584380aca3fab48b892639b80f089b35b65182a973878b1b16cde5c2c0b0bff46884b8793aa8564d06eab99f6ef407199b7477f8b811ed1434368895cc5ffe2354a04023e9ba0598da9838db89adcb9986c37e805ff0b083da119",
+	}
+)
+
+type nistRSAPKCS1TestVector struct {
+	name     string
+	sig      string
+	msg      string
+	hashType commonpb.HashType
+	key      *nistRSATestKey
+}
+
+func (t *nistRSAPKCS1TestVector) ToProtoKey() (*rsassapkcs1pb.RsaSsaPkcs1PrivateKey, error) {
+	e, err := hex.DecodeString(t.key.e)
+	if err != nil {
+		return nil, err
+	}
+	n, err := hex.DecodeString(t.key.n)
+	if err != nil {
+		return nil, err
+	}
+	d, err := hex.DecodeString(t.key.d)
+	if err != nil {
+		return nil, err
+	}
+	p, err := hex.DecodeString(t.key.p)
+	if err != nil {
+		return nil, err
+	}
+	q, err := hex.DecodeString(t.key.q)
+	if err != nil {
+		return nil, err
+	}
+	dp, err := hex.DecodeString(t.key.dp)
+	if err != nil {
+		return nil, err
+	}
+	dq, err := hex.DecodeString(t.key.dq)
+	if err != nil {
+		return nil, err
+	}
+	crt, err := hex.DecodeString(t.key.crt)
+	if err != nil {
+		return nil, err
+	}
+	return &rsassapkcs1pb.RsaSsaPkcs1PrivateKey{
+		Version: 0,
+		PublicKey: &rsassapkcs1pb.RsaSsaPkcs1PublicKey{
+			Version: 0,
+			E:       e,
+			N:       n,
+			Params: &rsassapkcs1pb.RsaSsaPkcs1Params{
+				HashType: t.hashType,
+			},
+		},
+		D:   d,
+		P:   p,
+		Q:   q,
+		Dq:  dq,
+		Dp:  dp,
+		Crt: crt,
+	}, nil
+}
+
+// The following test vectors are from:
+// https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/Digital-Signatures
+// Publication FIPS 186-2
+// Signature Verification PKCS1 v1.5
+var nistPKCS1TestVectors = []nistRSAPKCS1TestVector{
+	{
+		name:     "RSA_SSA_PKCS1_2048_SHA256",
+		sig:      "794d0a45bc9fc6febb586e319dfa6924c888594802b9deb9668963fdb309bf02817960a7457106fc474f91601436e8954cbb6815350b2c51b53c968d2c48cc1799550d5d03b41f6e5a8c3c264d2e2fe0b5b8ff53fdcb9dd111c985cb488d7086e6548b4077ec00721c9cb500fe07a031c2030e8ad1dd0112c34ffd9091d77a187aac8661b298eee39eb615f9715c4c48a6762ede55a466ec7f3cdb6a937cfc80188a85d8f8d3a2a80b199ce5e6375af8f02f06d706a34d9cf38318903965db54aaa7d3fa7a7ee58034cd58c8435739c8906366e2ddba293f2fb2c15f07fa4951014471e7f677d3bdacffc4c68a906e08d68b39f9010746cbacd22980cee73e8d",
+		msg:      "6918d6328ca0a8b64bbe81d91cdea519911b59fc2dbd53af76006fec4b18a320787135ce883b2b2edb26041bf86aa52c230b9620335b6e7f9ec08c7ed6b70823d819e9ab019e9929249f966fdb2069311a0ddc680ac468f514d4ed873b04a6beb0985b91a0cfd8ed51b09f9e6d06da739eaa939d5a00275901c4f8cf25076339",
+		key:      nist2048PKCS1Key,
+		hashType: commonpb.HashType_SHA256,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_2048_SHA384",
+		sig:      "80d7286710e5f165eeb63d2936e8e313ac5999bd297d35590c2b6ffaa4b7b7ed30003f0b83c1d1996c8593a37bc5d7b501a3d126ac6124779a23718497002d9dd2ce891b83d185e333af05460c6deb65a509640f775a0d3c70e55112c2e4af19f4ccec7af9ebb34226164f1b47d50b8ecc1dc3e0fc09aa15abfce5aa3998b5c2b1fde261c35eb43220f0d64529f723801d7faff841faba8709f6ea7751f30428dfc58045c84995107ee013ca4a84f65b99adde1abdefb5428f834ac8da04dafb9beaf1813f73a4bff2ec94120e3a702aed1184c387ebda2abf1959970724299b9a05f4ad313d91beb5344fe1fe13b3fc3386c279f031c77d74bdc9bc97e22455",
+		msg:      "752fbf49ae63c7853b3ef6f52ed324e53867925bd5d4c49dc42b93f3ba9d7eae579c4169593da98f10e1a61e1214a2aa2fb511a4a75849dc9be89445c29184f85ddc877c6d1cbb45230a047a98ac5bfcbe7b69a397c454cba44fd90fa13f9b546f39ba0a52c8a8ae5c0038932962f8e3cd00c1e00be28c70c8a787d9be6f69c9",
+		key:      nist2048PKCS1Key,
+		hashType: commonpb.HashType_SHA384,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_2048_SHA512",
+		sig:      "25b408187418c512e7d36eac17edc64999e94011b4d5088ab926d29e433a69e24ebac43146a1371635fc78c3d215a66ea46d0a734b1607fdb9c3848a1404545ff60582abd579d978902ed399eb5dcdfacde0ca02145480246e1a22af5ddca7080aec216895d3528a8756e0c1a2059d392f87576fe896e411ddf02bd6be81ae2a654e0a15542a6b533b776703e2057b01ad02f5430d97c691f80219e46319de527541f0bfcf0b4e1b510059fa20779eb44b1ef293b7a8318447ed25793037ddfd1877cd98514c81575613f36d946670f632779eaf629a593fe99110e781cb38101a3cbd54d7871983dbf868a00cfcb17bd330309d43b8df8d4f05eb4c43649cdf",
+		msg:      "5cb7a0a74095a6f284a13d4392aac30a73a9211df0520bf2f7e9831240579fca2f7d8d24fdc8d4161306dcf8b678b13be92804598f7c7308d10c0ab3bfb1092a3adee799113498b76a500c3f64e8f8a4fa16d8012bf3354e576823daed410ff54383b7edc5007a3d5228d200e3221fac6e1ca6fc0adfd92e53a6d96f10303994",
+		key:      nist2048PKCS1Key,
+		hashType: commonpb.HashType_SHA512,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_3072_SHA256",
+		sig:      "2e4d98c10e126e544d2b74bbcf0a4f03f081d4725a6661c0683d3612b01ddb2dd6f0391ef3679f39dc0785b37ac275f0d6841ed3a44ff7f6a407a085129a164faf63ff2aaed18ec6a5f7d27b48d017a50b24c8c4859ad2bb680ace5f3af57f2eac2d7337c0dd7403ffb2e8bac69dfdc98e62a07354b2fc93d03e499ee2d126647edaba094196b693e98cb98ad2817ba3f7f522c8f786b6ef82632ef5a00d5d4f42db6d26709909ca751aa8174037c924628852ce78e2829493d6c741d3558adcf713734697754e55e7b3bea0d8717da8aba2b2874527a0b3a8d2e1433344dd6bbaee1ebc7fa352539d94c6b45f915c6979b8e18be8934d28d770806c6ff893660dffd0e948a8cce11ac870507f88c1f86caa875ad721326dcb4f0d5ecc2d2538c4fabcc6c756ce4a59bcdafc44568787cf2bd9650486da8c85fd0b87794547e179e498cb6a5b26f71f9987d703f2d53418411c1f9a3a2e6705637b7bbda9660641c0eb037eb432b2d7515d1ffa99175cfdb8a2a4eed013b947faa4bf1c3cbd59",
+		msg:      "dbbd09ff7b1c707c2bd52014adbb773ceb146aa72637c8724f5ac39fb5a5acc4e18bfe04be599e3ffe0f6186b870cfd2565527f5ae46a3d06f4bfcc7bf00317222e885959e03c6531da3d59e127b63f25ff9f94377d63b34bfeb6d893b4636df667da49a61427120503885450205bb05d0a9e879c70e1a0409df1dce709e4473",
+		key:      nist3072PKCS1Key,
+		hashType: commonpb.HashType_SHA256,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_3072_SHA384",
+		sig:      "879c4df9b66227ce30e6403b620a488c72329b400ec67667a600bcfe3d0ea893f3051c8076eee81e0fece30c4153552ae2b2c774888657adb5300bedaf4940d6d6ce9310a476093b3590fca889e8ead464809be4da3370c21784e4740453df999bb9f7c290ea16e4b0e4ca666ddf23c757474fa9b0dbef769b1876e1eb553ee3a1c14c201c101164d5d1e5f628a7ebaf65c0f0f27f239ff67a4fe659d347cf50921b5859d79d86f8bfd2a25eabfcf76eb606b4151516c482c74ca3e54ede03e99086e61c0708e8ad9c94c1fe3640bf811f4e2c0e62d17d593f2e86adfd0798f04616adf9367b0f77f40de77135301debb490bfc76ba710878ddf91651a5fa025c348b6066a49853d6ece7bc79cdcd8ae709a77b96701c1ba9c4a91663e3790bad5c5c48017fcfd005a1b03d47d07dd3c511aefeb4c766dd19e377d2d82329a222b18ee0899e166cfb37c0b1b3226123f80a33c096fa4ba784719c4d9e52e60ccddb6da8575e705a36dbee7d97093f830283c71abbbb85c06daa913f96dede4ce",
+		msg:      "88af470625e6b5be3358cf6a8698a5655ef321838f044746df83bc55b05e8529df0b120aeb1c7b3a5a1409705879f887a22a7b78a2f30186db5fb7b888cd4e8c80c6feea5d8ecb57ddf9076b8980188594947bbd0533091a19b87906e2f727fe3589138ec3652d7d86b0d0455fd78cc5fdda283a00ebe76c5e370b25060498e7",
+		key:      nist3072PKCS1Key,
+		hashType: commonpb.HashType_SHA384,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_3072_SHA512",
+		sig:      "aa3ee99d39ad0332ca48deeb68b5966962018906f1e2e97d0d711df66e600fc875faf8da141212ea9bc49b2934f19bcb26f819b3633381b23f74f0c1d486c8fed6f6c17cacaac222e55c8c534ca2cb3921ca6f0cc19d045fa5456b4ffbc496d0d3079f19d93bba5600048b783a905abc3f30fe4b33b057a203b5f9d01254bdeb02896cc11b713fdca55e3ecc67e243fddf19130e2f3c79bd984558feef0fe877e071b0a13323e700de83244e2bcc09a602eaa06bb39d832d58e30450dcd96b30052778c07d4b209d5929bb6cc80800c95963a24bea449189301b50b5f26a899739dd0a4af6eba05e7e625c96cabda3beb2e4b0d11ccf72252b2d524d76a104a6dcef635517d78f248c78837422ac900557e1671958116500c673e2fc9a08e40d4eff8d44146f43fcc5f4086d276ba026e9953425a11b7be0036ddba0648f12424c3359cd8db8a0490b1bab3b35d3550840b4937ad1986aadedab74fcab27fec7561ae2bbcbb16d28d173efdf049fa8a1daae66db51f4c03fc91d1cda85d2cddc",
+		msg:      "112b7aa9fec5c7ac703a49c298c4e29586083296f79b099e6ec422d586840ed486d36e06362d6554f14d4d01fc39a9d2167e5b7350bd18161f66185db0c4127a7b3a67951bcc9e6d5de010439746e4f43b773e65ab1ab4234fcd024be621fea819e6ce1c56aa5841db7cfa4f11f67b411c7a4233b0bbc60f267f4668cfa4baa6",
+		key:      nist3072PKCS1Key,
+		hashType: commonpb.HashType_SHA512,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_4096_SHA256",
+		sig:      "2df1e58e7491739bb027c6315a70eebbcf37b8e5958df07578a589a47cbaf1edb23ff2e676f2f273a1cc0babd22e0bff874529cfd6479ae5c7250f06579eb212dd3f4058c476abae8e94c89afe05746c3aea93155cf03195ce5f4eced399d2b61aab7f4060b69844cbff6303d264c4755be78af001d125af461fececad8f46a9c4b07420ca63c4212f80a751fcca6a4737684543fcf07b39089baa9995394766f69239479e7c9778c644e022dd4ec7e07a769aa75db2571e58a5e0ba1e4377e9677092bc9dee9d9dbb448441da8f4385b4d4f8ccff4b3dc3c3c3ac8ba11a6ce8caafa930108ba3603c5b0ef65a02a7afebebf605aa88511513a69b3086fdfc25c588c4d61a06219d0d5643410d3ae4d78b2f695efe4e0b82161c53bb9d4b8a83692bb16de8da18b4a6c2abbd0f6b0e24229077fd6c3bca918bc9d9f4518598238df0c925f8587fff0852c44e8107ccbc1ce6a9be3b0941c3b28bb03c87eeb959d719dba9a64a338c7b9931cdf6bb169686de1f8de0e1fa74d03419d164f2c8bd2030562705d1470415e48144181dfd31cdd4219b5d22f9a4c659923cf5c4edbde18e8277dae11264c11423c5481402e80af223f0c4faea0c2c7aefacbf513962c2f16af353ffae1414b408f726eeb946d7c4c8577e72d8f1d49ae2301cf70abee46d286a6fac1b888c334538abb3b830fae595bbb19ea9dce46a343da031117c",
+		msg:      "8f937d50d24a1a6ed858d2e3de56e5c23b917d5a936c87b84effc06d48041391caf42207ba6d23030ed7edca864752b99ba3b089b308c3d19668bdcc2578995d4ac9ac502b347de3a37cd685f22f1bddb3cddb0e0f2ca53a311b1d45f9464edbf55a42b48d69d0167d8fb69c89d6e8376b57277211a2d4fa0560075d2d37dc12",
+		key:      nist4096PKCS1Key,
+		hashType: commonpb.HashType_SHA256,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_4096_SHA384",
+		sig:      "0471456bd38ff2a5adeb19b73c35de60f07d7907479f9e6d5735dca75ebd3ef499194917287ae0c3334a5b97f2be41598917a5d880b3d021c61a7acfe9083441661f56fb984ec6717986c558a10d108fac9e00bbd5adbd817a7684edd424e612c8a7f60ff1c8056067f0eee1c6ce3a4ceb6c27c735f0fdc93cfb521b529e1002659e6fe9cddcc79c218a84ee59b7355a43b47b5c4ece8535e0874cca866e3981dec3d1996d4dd05afab27bb5dae9d6ac9dc39e957329eb273254c4e7784a29db26696bd0ff872eede9fff869c35487643755d9cd5bae01c7858b123e4f9688b1d2607f349d52c828dd6d76ff41514565564d39038814841a0441c232411c8afcb15073739f5d5c537138b9bbda60bfd2cfea2847678ffbab73eb02fcaba9e4cabd7768bc0c3a6009dd78f02a8d791f5e1e30d7358b5a642cf0a1a838b954b76a15386109926595b2862de80ddfea98e6218efb925cf5d6434b93045cad5bfd1af36e63a98c14b8f5c6974f04e5e52243bf7cffdc8cb9c0a35fc3250943d07037b319cafef02a2fd21c39aaa3da9f171f1e9c9613ff97d3a6770e7639ecbfa7e47804d8833e8212064d091e02801869bcb2bbbfc2be5d21b2da790ddd7973b6f5b9d6c763cfa503c6243851461490dfab31cccc6621fd91fc1ae28b4b1d393cb1e41d397852acd98fa35b93b6b4dd92e2d5fac7665d8c0b3f7d03ad3048b6854b",
+		msg:      "450171e919ecc10bbf1d05f41eee02eab8eede088a31263433eb1652593d14b7494f7de1dd9fa465a39918283b6726d2010a9f80edaadcbab72afea43e007a782a44d633d006ad8f57e5f93219dd92254c61f187e2aa3a83ad4e7a7fc7142c9631070349bac9e371232a307880f94cc9e11b5b8532d94a78607e0f4bbe2232fb",
+		key:      nist4096PKCS1Key,
+		hashType: commonpb.HashType_SHA384,
+	},
+	{
+		name:     "RSA_SSA_PKCS1_4096_SHA512",
+		sig:      "6836dff1bea541916b9ccce2695e921ff27a737ac54f4a5c3b2facee80a8dd3bbfe86c93a1af8da5c6b3a92c445dfac7e215fa9f3d67c9589dba858a223f326afeb8f5d0f92f28ac4e3671c22b5b4e0b4266f776aca928bb0309929f2c452b62d462675cef09388e5720cf0cb99351d44059790720db82ce014d7e795826833284bd4205c64e1330e30e09ff6220f62c013c117ace4f4286cd46e52694c4925aba12a5278e47de910dcdd820396febe5179bbc6ccecdac1883bd408530bde92e93c49db2c6fbb42e9705f29703763b21a8172489d2831889bb060505040940e60f7c5cb9f58327c3d3f7cf7e18ad60e877edb65222a699d4acdcb358fbc87e1e461468cfdc82a8cd7fb1f82e05378737f4aa741a63ddf6039b23e1aa19d94e7087915899685add8a8da8f64a93707be0b6354c8e9a8aefc7484bb45cd0beca493a4a0aef0b6aaae801866b905683c582bf39f9cc0768d880c6e4b331da86506b298c180cf95fa45e532483c55544371468088b4e378f37976c397e09f89081f0f0b6f4ba3be6929957f55f14a88f6beb456b70e6fbc6227e98c1e8a4a6f734c9080c13ed58bcfb3456cbbc217653835000f54956d839d4073571d8a42fa2294df1a747e88af53a16df1834203009cccd6d61304739872ab92be79a4462125ebc8bba909ca2b4d91b9e520d6c681c2f92070f156e31a6875122993e1d94fc3568",
+		msg:      "42c5ea617a25f019329ee172e4932485518dabd01983249189597473b4a6616cc5ba8ee693e0ad1d76e0f0c85ac8c0fb11ecb24cee2cb7358f7593b9fa8b904aec0573eb6d99af92a899d9d0fabe5cb349256eec9797422dd60d7fd5fe73f2cf5ead7fb72fd85e3f6fd284d2edfc5e77a03ec5f73c4c2f420728220fe9e9efc3",
+		key:      nist4096PKCS1Key,
+		hashType: commonpb.HashType_SHA512,
+	},
+}
+
+func TestRSASSAPKCS1VerifierPrimitiveNISTTestVectors(t *testing.T) {
+	vkm, err := registry.GetKeyManager(rsaPKCS1PublicTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", rsaPKCS1PublicTypeURL, err)
+	}
+	for _, tc := range nistPKCS1TestVectors {
+		t.Run(tc.name, func(t *testing.T) {
+			privKey, err := tc.ToProtoKey()
+			if err != nil {
+				t.Fatalf("tc.ToProtoKey() err = %v, want nil", err)
+			}
+			serializedPubKey, err := proto.Marshal(privKey.GetPublicKey())
+			if err != nil {
+				t.Fatalf("proto.Marshall() err = %v, want nil", err)
+			}
+			p, err := vkm.Primitive(serializedPubKey)
+			if err != nil {
+				t.Fatalf("Primitive() err = %v, want nil", err)
+			}
+			sig, err := hex.DecodeString(tc.sig)
+			if err != nil {
+				t.Fatalf("hex.DecodeString(tc.sig) err = %v, want nil", err)
+			}
+			msg, err := hex.DecodeString(tc.msg)
+			if err != nil {
+				t.Fatalf("hex.DecodeString(tc.msg) err = %v, want nil", err)
+			}
+			if err := p.(tink.Verifier).Verify(sig, msg); err != nil {
+				t.Fatalf("Verify() err = %v, want nil", err)
+			}
+		})
+	}
+}
diff --git a/go/signature/rsassapss_signer_key_manager.go b/go/signature/rsassapss_signer_key_manager.go
new file mode 100644
index 0000000..db7262b
--- /dev/null
+++ b/go/signature/rsassapss_signer_key_manager.go
@@ -0,0 +1,188 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+
+	"errors"
+	"math/big"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	internal "github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/keyset"
+	rsassapsspb "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	rsaSSAPSSSignerKeyVersion = 0
+	rsaSSAPSSSignerTypeURL    = "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey"
+)
+
+var (
+	errInvalidRSASSAPSSSignKey = errors.New("rsassapss_signer_key_manager: invalid key")
+)
+
+type rsaSSAPSSSignerKeyManager struct{}
+
+var _ registry.PrivateKeyManager = (*rsaSSAPSSSignerKeyManager)(nil)
+
+func (km *rsaSSAPSSSignerKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if len(serializedKey) == 0 {
+		return nil, errInvalidRSASSAPSSSignKey
+	}
+	key := &rsassapsspb.RsaSsaPssPrivateKey{}
+	if err := proto.Unmarshal(serializedKey, key); err != nil {
+		return nil, err
+	}
+	if err := validateRSAPSSPrivateKey(key); err != nil {
+		return nil, err
+	}
+	privKey := &rsa.PrivateKey{
+		PublicKey: rsa.PublicKey{
+			N: bytesToBigInt(key.GetPublicKey().GetN()),
+			E: int(bytesToBigInt(key.GetPublicKey().GetE()).Uint64()),
+		},
+		D: bytesToBigInt(key.GetD()),
+		Primes: []*big.Int{
+			bytesToBigInt(key.GetP()),
+			bytesToBigInt(key.GetQ()),
+		},
+		Precomputed: rsa.PrecomputedValues{
+			Dp:   bytesToBigInt(key.GetDp()),
+			Dq:   bytesToBigInt(key.GetDq()),
+			Qinv: bytesToBigInt(key.GetCrt()),
+		},
+	}
+	params := key.GetPublicKey().GetParams()
+	if err := internal.Validate_RSA_SSA_PSS(hashName(params.GetSigHash()), int(params.GetSaltLength()), privKey); err != nil {
+		return nil, err
+	}
+	return internal.New_RSA_SSA_PSS_Signer(hashName(params.GetSigHash()), int(params.GetSaltLength()), privKey)
+}
+
+func validateRSAPSSPrivateKey(privKey *rsassapsspb.RsaSsaPssPrivateKey) error {
+	if err := keyset.ValidateKeyVersion(privKey.GetVersion(), rsaSSAPSSSignerKeyVersion); err != nil {
+		return err
+	}
+	if err := validateRSAPSSPublicKey(privKey.GetPublicKey()); err != nil {
+		return err
+	}
+	if len(privKey.GetD()) == 0 ||
+		len(privKey.GetPublicKey().GetN()) == 0 ||
+		len(privKey.GetPublicKey().GetE()) == 0 ||
+		len(privKey.GetP()) == 0 ||
+		len(privKey.GetQ()) == 0 ||
+		len(privKey.GetDp()) == 0 ||
+		len(privKey.GetDq()) == 0 ||
+		len(privKey.GetCrt()) == 0 {
+		return errInvalidRSASSAPSSSignKey
+	}
+	return nil
+}
+
+func (km *rsaSSAPSSSignerKeyManager) PublicKeyData(serializedPrivKey []byte) (*tinkpb.KeyData, error) {
+	if serializedPrivKey == nil {
+		return nil, errInvalidRSASSAPSSSignKey
+	}
+	privKey := &rsassapsspb.RsaSsaPssPrivateKey{}
+	if err := proto.Unmarshal(serializedPrivKey, privKey); err != nil {
+		return nil, err
+	}
+	if err := validateRSAPSSPrivateKey(privKey); err != nil {
+		return nil, err
+	}
+	serializedPubKey, err := proto.Marshal(privKey.GetPublicKey())
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         rsaSSAPSSVerifierTypeURL,
+		Value:           serializedPubKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC,
+	}, nil
+}
+
+func (km *rsaSSAPSSSignerKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, fmt.Errorf("invalid key format")
+	}
+	keyFormat := &rsassapsspb.RsaSsaPssKeyFormat{}
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, err
+	}
+	params := keyFormat.GetParams()
+	if params.GetSigHash() != params.GetMgf1Hash() {
+		return nil, fmt.Errorf("signature hash and mgf1 hash must be the same")
+	}
+	if params.GetSaltLength() < 0 {
+		return nil, fmt.Errorf("salt length can't be negative")
+	}
+	if err := validateRSAPubKeyParams(
+		params.GetSigHash(),
+		int(keyFormat.GetModulusSizeInBits()),
+		keyFormat.GetPublicExponent()); err != nil {
+		return nil, err
+	}
+	privKey, err := rsa.GenerateKey(rand.Reader, int(keyFormat.GetModulusSizeInBits()))
+	if err != nil {
+		return nil, err
+	}
+	return &rsassapsspb.RsaSsaPssPrivateKey{
+		Version: rsaSSAPSSSignerKeyVersion,
+		PublicKey: &rsassapsspb.RsaSsaPssPublicKey{
+			Version: rsaSSAPSSSignerKeyVersion,
+			Params:  keyFormat.GetParams(),
+			N:       privKey.PublicKey.N.Bytes(),
+			E:       big.NewInt(int64(privKey.PublicKey.E)).Bytes(),
+		},
+		D:   privKey.D.Bytes(),
+		P:   privKey.Primes[0].Bytes(),
+		Q:   privKey.Primes[1].Bytes(),
+		Dp:  privKey.Precomputed.Dp.Bytes(),
+		Dq:  privKey.Precomputed.Dq.Bytes(),
+		Crt: privKey.Precomputed.Qinv.Bytes(),
+	}, nil
+}
+
+func (km *rsaSSAPSSSignerKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	key, err := km.NewKey(serializedKeyFormat)
+	if err != nil {
+		return nil, err
+	}
+	serializedKey, err := proto.Marshal(key)
+	if err != nil {
+		return nil, err
+	}
+	return &tinkpb.KeyData{
+		TypeUrl:         rsaSSAPSSSignerTypeURL,
+		Value:           serializedKey,
+		KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE,
+	}, nil
+}
+
+func (km *rsaSSAPSSSignerKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == rsaSSAPSSSignerTypeURL
+}
+
+func (km *rsaSSAPSSSignerKeyManager) TypeURL() string {
+	return rsaSSAPSSSignerTypeURL
+}
diff --git a/go/signature/rsassapss_signer_key_manager_test.go b/go/signature/rsassapss_signer_key_manager_test.go
new file mode 100644
index 0000000..1da735d
--- /dev/null
+++ b/go/signature/rsassapss_signer_key_manager_test.go
@@ -0,0 +1,617 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature_test
+
+import (
+	"math/big"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/tink"
+	cpb "github.com/google/tink/go/proto/common_go_proto"
+	rsppb "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto"
+	tpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	rsaPSSTestPrivateKeyTypeURL = "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey"
+	rsaPSSTestPrivateKeyVersion = 0
+)
+
+func TestRSASSAPSSSignerKeyManagerDoesSupport(t *testing.T) {
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	if !skm.DoesSupport(rsaPSSTestPrivateKeyTypeURL) {
+		t.Errorf("DoesSupport(%q) err = false, want true", rsaPSSTestPrivateKeyTypeURL)
+	}
+	if skm.DoesSupport("fake.type.url") {
+		t.Errorf("DoesSupport(%q) err = true, want false", "fake.type.url")
+	}
+}
+
+func TestRSASSAPSSSignerKeyManagerTypeURL(t *testing.T) {
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	if skm.TypeURL() != rsaPSSTestPrivateKeyTypeURL {
+		t.Errorf("TypeURL() = %q, want %q", skm.TypeURL(), rsaPSSTestPrivateKeyTypeURL)
+	}
+}
+
+func TestRSASSAPSSSignerGetPrimitive(t *testing.T) {
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	privKey, err := makeValidRSAPSSKey()
+	if err != nil {
+		t.Fatalf("makeValidRSAPSSKey() err = %v, want nil", err)
+	}
+	serializedPrivate, err := proto.Marshal(privKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	p, err := skm.Primitive(serializedPrivate)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	signer := p.(tink.Signer)
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	serializedPublic, err := proto.Marshal(privKey.GetPublicKey())
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	p, err = vkm.Primitive(serializedPublic)
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	verifier := p.(tink.Verifier)
+	data := random.GetRandomBytes(80)
+	signature, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("Sign() err = %v, want nil", err)
+	}
+	if err := verifier.Verify(signature, data); err != nil {
+		t.Fatalf("Verify() err = %v, want nil", err)
+	}
+}
+
+func mergePrivPub(priv *rsppb.RsaSsaPssPrivateKey, pub *rsppb.RsaSsaPssPublicKey) *rsppb.RsaSsaPssPrivateKey {
+	return &rsppb.RsaSsaPssPrivateKey{
+		Version:   priv.GetVersion(),
+		PublicKey: pub,
+		D:         priv.GetD(),
+		P:         priv.GetP(),
+		Q:         priv.GetQ(),
+		Dp:        priv.GetDp(),
+		Dq:        priv.GetDq(),
+		Crt:       priv.GetCrt(),
+	}
+}
+
+func TestRSASSAPSSSignerGetPrimitiveWithInvalidInput(t *testing.T) {
+	type testCase struct {
+		tag     string
+		privKey *rsppb.RsaSsaPssPrivateKey
+	}
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	validPrivKey, err := makeValidRSAPSSKey()
+	if err != nil {
+		t.Fatalf("makeValidRSAPSSKey() err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			tag:     "empty private key",
+			privKey: &rsppb.RsaSsaPssPrivateKey{},
+		},
+		{
+			tag: "invalid private key version",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion() + 1,
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			tag: "invalid private key D",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         nil,
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			tag: "invalid private key P",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         nil,
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			tag: "invalid private key Q",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         nil,
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			tag: "invalid private key Dp",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        nil,
+				Dq:        validPrivKey.GetDq(),
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			tag: "invalid private key Dq",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        nil,
+				Crt:       validPrivKey.GetCrt(),
+			},
+		},
+		{
+			tag: "invalid private key Crt",
+			privKey: &rsppb.RsaSsaPssPrivateKey{
+				Version:   validPrivKey.GetVersion(),
+				PublicKey: validPrivKey.GetPublicKey(),
+				D:         validPrivKey.GetD(),
+				P:         validPrivKey.GetP(),
+				Q:         validPrivKey.GetQ(),
+				Dp:        validPrivKey.GetDp(),
+				Dq:        validPrivKey.GetDq(),
+				Crt:       nil,
+			},
+		},
+		{
+			tag:     "empty public key",
+			privKey: mergePrivPub(validPrivKey, &rsppb.RsaSsaPssPublicKey{}),
+		},
+		{
+			tag: "invalid public key version",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion() + 1,
+					Params:  validPrivKey.GetPublicKey().GetParams(),
+					N:       validPrivKey.GetPublicKey().GetN(),
+					E:       validPrivKey.GetPublicKey().GetE(),
+				}),
+		},
+		{
+			tag: "different sig and mgf1 hash functions",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params: &rsppb.RsaSsaPssParams{
+						SigHash:    cpb.HashType_SHA256,
+						Mgf1Hash:   cpb.HashType_SHA384,
+						SaltLength: validPrivKey.GetPublicKey().GetParams().GetSaltLength(),
+					},
+					N: validPrivKey.GetPublicKey().GetN(),
+					E: validPrivKey.GetPublicKey().GetE(),
+				}),
+		},
+		{
+			tag: "negative salt length",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params: &rsppb.RsaSsaPssParams{
+						SigHash:    validPrivKey.GetPublicKey().GetParams().GetSigHash(),
+						Mgf1Hash:   validPrivKey.GetPublicKey().GetParams().GetMgf1Hash(),
+						SaltLength: -1,
+					},
+					N: validPrivKey.GetPublicKey().GetN(),
+					E: validPrivKey.GetPublicKey().GetE(),
+				}),
+		},
+		{
+			tag: "invalid hash function",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params: &rsppb.RsaSsaPssParams{
+						SigHash:    cpb.HashType_UNKNOWN_HASH,
+						Mgf1Hash:   cpb.HashType_UNKNOWN_HASH,
+						SaltLength: validPrivKey.GetPublicKey().GetParams().GetSaltLength(),
+					},
+					N: validPrivKey.GetPublicKey().GetN(),
+					E: validPrivKey.GetPublicKey().GetE(),
+				}),
+		},
+		{
+			tag: "unsafe hash function",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params: &rsppb.RsaSsaPssParams{
+						SigHash:    cpb.HashType_SHA1,
+						Mgf1Hash:   cpb.HashType_SHA1,
+						SaltLength: validPrivKey.GetPublicKey().GetParams().GetSaltLength(),
+					},
+					N: validPrivKey.GetPublicKey().GetN(),
+					E: validPrivKey.GetPublicKey().GetE(),
+				}),
+		},
+		{
+			tag: "invalid modulus",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params:  validPrivKey.GetPublicKey().GetParams(),
+					N:       []byte{0x00},
+					E:       validPrivKey.GetPublicKey().GetE(),
+				}),
+		},
+		{
+			tag: "invalid exponent",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params:  validPrivKey.GetPublicKey().GetParams(),
+					N:       validPrivKey.GetPublicKey().GetN(),
+					E:       []byte{0x01},
+				}),
+		},
+		{
+			tag: "exponent larger than 64 bits",
+			privKey: mergePrivPub(
+				validPrivKey,
+				&rsppb.RsaSsaPssPublicKey{
+					Version: validPrivKey.GetPublicKey().GetVersion(),
+					Params:  validPrivKey.GetPublicKey().GetParams(),
+					N:       validPrivKey.GetPublicKey().GetN(),
+					E:       random.GetRandomBytes(32),
+				}),
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			serializedPrivKey, err := proto.Marshal(tc.privKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := skm.Primitive(serializedPrivKey); err == nil {
+				t.Errorf("Primitive() err = nil, want error")
+			}
+			if _, err := skm.(registry.PrivateKeyManager).PublicKeyData(serializedPrivKey); err == nil {
+				t.Errorf("PublicKeyData() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestRSASSAPSSSignerGetPrimitiveWithCorruptedPrivateKey(t *testing.T) {
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	validPrivKey, err := makeValidRSAPSSKey()
+	if err != nil {
+		t.Fatalf("makeValidRSAPSSKey() err = %v, want nil", err)
+	}
+	corruptedPrivKey := validPrivKey
+	corruptedPrivKey.P[5] <<= 1
+	corruptedPrivKey.P[20] <<= 1
+	serializedPrivKey, err := proto.Marshal(corruptedPrivKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := skm.Primitive(serializedPrivKey); err == nil {
+		t.Errorf("Primitive() err = nil, want error")
+	}
+}
+
+func TestRSASSAPSSSignerNewKey(t *testing.T) {
+	keyFormat := &rsppb.RsaSsaPssKeyFormat{
+		Params: &rsppb.RsaSsaPssParams{
+			SigHash:    cpb.HashType_SHA256,
+			Mgf1Hash:   cpb.HashType_SHA256,
+			SaltLength: 32,
+		},
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedKeyFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	key, err := skm.NewKey(serializedKeyFormat)
+	if err != nil {
+		t.Fatalf("NewKey() err = %v, want nil", err)
+	}
+	privKey, ok := key.(*rsppb.RsaSsaPssPrivateKey)
+	if !ok {
+		t.Fatalf("key isn't a *rsppb.RsaSsaPssPrivateKey")
+	}
+	if privKey.GetVersion() != rsaPSSTestPrivateKeyVersion {
+		t.Errorf("privKey.GetVersion() = %d, want %d", privKey.GetVersion(), rsaPSSTestPrivateKeyVersion)
+	}
+	if privKey.GetD() == nil {
+		t.Error("GetD() == nil, want []byte{}")
+	}
+	if privKey.GetP() == nil {
+		t.Error("GetP() == nil, want []byte{}")
+	}
+	if privKey.GetQ() == nil {
+		t.Error("GetQ() == nil, want []byte{}")
+	}
+	if privKey.GetDp() == nil {
+		t.Error("GetDp() == nil, want []byte{}")
+	}
+	if privKey.GetDq() == nil {
+		t.Error("GetDq() == nil, want []byte{}")
+	}
+	if privKey.GetCrt() == nil {
+		t.Error("GetCrt() == nil, want []byte{}")
+	}
+	pubKey := privKey.GetPublicKey()
+	if !cmp.Equal(pubKey.GetE(), keyFormat.GetPublicExponent()) {
+		t.Errorf("GetE() = %v, want %v", pubKey.GetE(), keyFormat.GetPublicExponent())
+	}
+	n := uint32(new(big.Int).SetBytes(pubKey.GetN()).BitLen())
+	if !cmp.Equal(n, keyFormat.GetModulusSizeInBits()) {
+		t.Errorf("Modulus size in bits = %q, want %q", n, keyFormat.GetModulusSizeInBits())
+	}
+	if !cmp.Equal(pubKey.GetParams(), keyFormat.GetParams(), protocmp.Transform()) {
+		t.Errorf("GetParams() = %v, want %v", pubKey.GetParams(), keyFormat.GetParams())
+	}
+}
+
+func TestRSASSAPSSSignerNewKeyData(t *testing.T) {
+	keyFormat := &rsppb.RsaSsaPssKeyFormat{
+		Params: &rsppb.RsaSsaPssParams{
+			SigHash:    cpb.HashType_SHA256,
+			Mgf1Hash:   cpb.HashType_SHA256,
+			SaltLength: 32,
+		},
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedKeyFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	keyData, err := skm.NewKeyData(serializedKeyFormat)
+	if err != nil {
+		t.Fatalf("skm.NewKeyData() err = %v, want nil", err)
+	}
+	if keyData.GetKeyMaterialType() != tpb.KeyData_ASYMMETRIC_PRIVATE {
+		t.Errorf("keyData.GetKeyMaterialType() = %v, want %v", keyData.GetKeyMaterialType(), tpb.KeyData_ASYMMETRIC_PRIVATE)
+	}
+	if keyData.GetTypeUrl() != rsaPSSTestPrivateKeyTypeURL {
+		t.Errorf("keyData.GetTypeUrl() = %q, want %q", keyData.GetTypeUrl(), rsaPSSTestPrivateKeyTypeURL)
+	}
+	// Creating a primitive does a self key test which signs and verifies data.
+	s, err := skm.Primitive(keyData.GetValue())
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	signer, ok := s.(tink.Signer)
+	if !ok {
+		t.Fatal("Primitive() return type isn't a tink.Signer")
+	}
+	data := random.GetRandomBytes(50)
+	sig, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("signer.Sign() err = %v, want nil", err)
+	}
+	pubKeyData, err := skm.(registry.PrivateKeyManager).PublicKeyData(keyData.GetValue())
+	if err != nil {
+		t.Fatalf("PublicKeyData() err = %v, want nil", err)
+	}
+	v, err := vkm.Primitive(pubKeyData.GetValue())
+	if err != nil {
+		t.Fatalf("Primitive() err = %v, want nil", err)
+	}
+	verifier, ok := v.(tink.Verifier)
+	if !ok {
+		t.Fatal("Primitive() return type isn't a tink.Verifier")
+	}
+	if err := verifier.Verify(sig, data); err != nil {
+		t.Fatalf("verifier.Verify() err = %v, want nil", err)
+	}
+}
+
+func TestRSASSAPSSSignerNewKeyFailsWithInvalidFormat(t *testing.T) {
+	type testCase struct {
+		tag       string
+		keyFormat *rsppb.RsaSsaPssKeyFormat
+	}
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	validKeyFormat := &rsppb.RsaSsaPssKeyFormat{
+		Params: &rsppb.RsaSsaPssParams{
+			SigHash:    cpb.HashType_SHA256,
+			Mgf1Hash:   cpb.HashType_SHA256,
+			SaltLength: 32,
+		},
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedKeyFormat, err := proto.Marshal(validKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := skm.NewKeyData(serializedKeyFormat); err != nil {
+		t.Fatalf("NewKeyData() err = %v, want nil", err)
+	}
+	for _, tc := range []testCase{
+		{
+			tag: "unsafe hash function",
+			keyFormat: &rsppb.RsaSsaPssKeyFormat{
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    cpb.HashType_SHA224,
+					Mgf1Hash:   cpb.HashType_SHA224,
+					SaltLength: validKeyFormat.GetParams().GetSaltLength(),
+				},
+				ModulusSizeInBits: validKeyFormat.GetModulusSizeInBits(),
+				PublicExponent:    validKeyFormat.GetPublicExponent(),
+			},
+		},
+		{
+			tag: "different signature and mgf1 hash function",
+			keyFormat: &rsppb.RsaSsaPssKeyFormat{
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    cpb.HashType_SHA384,
+					Mgf1Hash:   cpb.HashType_SHA512,
+					SaltLength: validKeyFormat.GetParams().GetSaltLength(),
+				},
+				ModulusSizeInBits: validKeyFormat.GetModulusSizeInBits(),
+				PublicExponent:    validKeyFormat.GetPublicExponent(),
+			},
+		},
+		{
+			tag: "negative salt length",
+			keyFormat: &rsppb.RsaSsaPssKeyFormat{
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    validKeyFormat.GetParams().GetSigHash(),
+					Mgf1Hash:   validKeyFormat.GetParams().GetMgf1Hash(),
+					SaltLength: -1,
+				},
+				ModulusSizeInBits: validKeyFormat.GetModulusSizeInBits(),
+				PublicExponent:    validKeyFormat.GetPublicExponent(),
+			},
+		},
+		{
+			tag: "insecure modulus size",
+			keyFormat: &rsppb.RsaSsaPssKeyFormat{
+				Params:            validKeyFormat.GetParams(),
+				ModulusSizeInBits: 2047,
+				PublicExponent:    validKeyFormat.GetPublicExponent(),
+			},
+		},
+		{
+			tag: "invalid public exponent",
+			keyFormat: &rsppb.RsaSsaPssKeyFormat{
+				Params:            validKeyFormat.GetParams(),
+				ModulusSizeInBits: validKeyFormat.GetModulusSizeInBits(),
+				PublicExponent:    []byte{0x00, 0x00, 0x03},
+			},
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			serializedKeyFormat, err := proto.Marshal(tc.keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := skm.NewKey(serializedKeyFormat); err == nil {
+				t.Fatalf("NewKey() err = nil, want error")
+			}
+			if _, err := skm.NewKeyData(serializedKeyFormat); err == nil {
+				t.Fatalf("NewKeyData() err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestRSASSAPSSSignerPublicKeyData(t *testing.T) {
+	skm, err := registry.GetKeyManager(rsaPSSTestPrivateKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPrivateKeyTypeURL)
+	}
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	validPrivKey, err := makeValidRSAPSSKey()
+	if err != nil {
+		t.Fatalf("makeValidRSAPSSKey() err = %v, want nil", err)
+	}
+	serializedPrivKey, err := proto.Marshal(validPrivKey)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	pubKeyData, err := skm.(registry.PrivateKeyManager).PublicKeyData(serializedPrivKey)
+	if err != nil {
+		t.Fatalf("PublicKeyData() err = %v, want nil", err)
+	}
+	if pubKeyData.GetKeyMaterialType() != tpb.KeyData_ASYMMETRIC_PUBLIC {
+		t.Errorf("GetKeyMaterialType() = %v, want %v", pubKeyData.GetKeyMaterialType(), tpb.KeyData_ASYMMETRIC_PUBLIC)
+	}
+	if pubKeyData.GetTypeUrl() != rsaPSSTestPublicKeyTypeURL {
+		t.Errorf("GetTypeUrl() = %q, want %q", pubKeyData.GetTypeUrl(), rsaPSSTestPublicKeyTypeURL)
+	}
+	if _, err := vkm.Primitive(pubKeyData.GetValue()); err != nil {
+		t.Fatalf("vkm.Primitive() err = %v, want nil", err)
+	}
+}
diff --git a/go/signature/rsassapss_verifier_key_manager.go b/go/signature/rsassapss_verifier_key_manager.go
new file mode 100644
index 0000000..41d74ad
--- /dev/null
+++ b/go/signature/rsassapss_verifier_key_manager.go
@@ -0,0 +1,95 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature
+
+import (
+	"crypto/rsa"
+	"errors"
+	"fmt"
+	"math/big"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	internal "github.com/google/tink/go/internal/signature"
+	"github.com/google/tink/go/keyset"
+	rsassapsspb "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+const (
+	rsaSSAPSSVerifierKeyVersion = 0
+	rsaSSAPSSVerifierTypeURL    = "type.googleapis.com/google.crypto.tink.RsaSsaPssPublicKey"
+)
+
+var (
+	errInvalidRSASSAPSSVerifierKey = errors.New("rsassapss_verifier_key_manager: invalid key")
+	errRSASSAPSSNotImplemented     = errors.New("rsassapss_verifier_key_manager: not implemented")
+)
+
+type rsaSSAPSSVerifierKeyManager struct{}
+
+var _ (registry.KeyManager) = (*rsaSSAPSSVerifierKeyManager)(nil)
+
+func (km *rsaSSAPSSVerifierKeyManager) Primitive(serializedKey []byte) (interface{}, error) {
+	if len(serializedKey) == 0 {
+		return nil, errInvalidRSASSAPSSVerifierKey
+	}
+	key := &rsassapsspb.RsaSsaPssPublicKey{}
+	if err := proto.Unmarshal(serializedKey, key); err != nil {
+		return nil, errInvalidRSASSAPSSVerifierKey
+	}
+	if err := validateRSAPSSPublicKey(key); err != nil {
+		return nil, err
+	}
+	pubKey := &rsa.PublicKey{
+		E: int(new(big.Int).SetBytes(key.E).Uint64()),
+		N: new(big.Int).SetBytes(key.N),
+	}
+	return internal.New_RSA_SSA_PSS_Verifier(hashName(key.GetParams().GetSigHash()), int(key.GetParams().GetSaltLength()), pubKey)
+}
+
+func validateRSAPSSPublicKey(pubKey *rsassapsspb.RsaSsaPssPublicKey) error {
+	if err := keyset.ValidateKeyVersion(pubKey.GetVersion(), rsaSSAPSSVerifierKeyVersion); err != nil {
+		return err
+	}
+	if pubKey.GetParams().GetSigHash() != pubKey.GetParams().GetMgf1Hash() {
+		return fmt.Errorf("signature hash and MGF1 hash function must match")
+	}
+	if pubKey.GetParams().GetSaltLength() < 0 {
+		return fmt.Errorf("salt length can't be negative")
+	}
+	return validateRSAPubKeyParams(
+		pubKey.GetParams().GetSigHash(),
+		new(big.Int).SetBytes(pubKey.GetN()).BitLen(),
+		pubKey.GetE())
+}
+
+func (km *rsaSSAPSSVerifierKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) {
+	return nil, errRSASSAPSSNotImplemented
+}
+
+func (km *rsaSSAPSSVerifierKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) {
+	return nil, errRSASSAPSSNotImplemented
+}
+
+func (km *rsaSSAPSSVerifierKeyManager) DoesSupport(typeURL string) bool {
+	return typeURL == rsaSSAPSSVerifierTypeURL
+}
+
+func (km *rsaSSAPSSVerifierKeyManager) TypeURL() string {
+	return rsaSSAPSSVerifierTypeURL
+}
diff --git a/go/signature/rsassapss_verifier_key_manager_test.go b/go/signature/rsassapss_verifier_key_manager_test.go
new file mode 100644
index 0000000..98940d0
--- /dev/null
+++ b/go/signature/rsassapss_verifier_key_manager_test.go
@@ -0,0 +1,412 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"encoding/hex"
+	"fmt"
+	"math/big"
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/tink"
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
+	rsppb "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto"
+)
+
+const (
+	rsaPSSTestPublicKeyTypeURL = "type.googleapis.com/google.crypto.tink.RsaSsaPssPublicKey"
+)
+
+func makeValidRSAPSSKey() (*rsppb.RsaSsaPssPrivateKey, error) {
+	rsaKey, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		return nil, err
+	}
+	return &rsppb.RsaSsaPssPrivateKey{
+		Version: 0,
+		PublicKey: &rsppb.RsaSsaPssPublicKey{
+			N:       rsaKey.PublicKey.N.Bytes(),
+			E:       big.NewInt(int64(rsaKey.PublicKey.E)).Bytes(),
+			Version: 0,
+			Params: &rsppb.RsaSsaPssParams{
+				SigHash:    commonpb.HashType_SHA256,
+				Mgf1Hash:   commonpb.HashType_SHA256,
+				SaltLength: 32,
+			},
+		},
+		D:   rsaKey.D.Bytes(),
+		P:   rsaKey.Primes[0].Bytes(),
+		Q:   rsaKey.Primes[1].Bytes(),
+		Dp:  rsaKey.Precomputed.Dp.Bytes(),
+		Dq:  rsaKey.Precomputed.Dq.Bytes(),
+		Crt: rsaKey.Precomputed.Qinv.Bytes(),
+	}, nil
+}
+
+func TestRSASSAPSSVerifierNewKeyNotSupported(t *testing.T) {
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	keyFormat := &rsppb.RsaSsaPssKeyFormat{
+		Params: &rsppb.RsaSsaPssParams{
+			SigHash:    commonpb.HashType_SHA256,
+			Mgf1Hash:   commonpb.HashType_SHA256,
+			SaltLength: 32,
+		},
+		ModulusSizeInBits: 3072,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedKeyFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal() err = %v, want nil", err)
+	}
+	if _, err := vkm.NewKey(serializedKeyFormat); err == nil {
+		t.Errorf("NewKey() err = nil, want error")
+	}
+	if _, err := vkm.NewKeyData(serializedKeyFormat); err == nil {
+		t.Errorf("NewKeyData() err = nil, want error")
+	}
+}
+
+func TestRSASSAPSSVerifierDoesSupport(t *testing.T) {
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	if !vkm.DoesSupport(rsaPSSTestPublicKeyTypeURL) {
+		t.Errorf("DoesSupport(%q) = %v, want true", rsaPSSTestPublicKeyTypeURL, vkm.DoesSupport(rsaPSSTestPublicKeyTypeURL))
+	}
+	if vkm.DoesSupport("fake.key.type") {
+		t.Errorf("DoesSupport(%q) = %v, want false", "fake.key.type", vkm.DoesSupport("fake.key.type"))
+	}
+}
+
+func TestRSASSAPSSVerifierTypeURL(t *testing.T) {
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	if vkm.TypeURL() != rsaPSSTestPublicKeyTypeURL {
+		t.Errorf("TypeURL() = %q, want %q", vkm.TypeURL(), rsaPSSTestPublicKeyTypeURL)
+	}
+}
+
+type nistRSATestKey struct {
+	// public keys only require `n` and `e` to be set.
+	n   string
+	e   string
+	d   string
+	p   string
+	q   string
+	dp  string
+	dq  string
+	crt string
+}
+
+// The following keys are from:
+// https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/Digital-Signatures
+// Publication FIPS 186-4
+// Signature Verification PSS
+// Only keys with public exponent 65537 (aka: F4, 0x010001) where chosen since golang rsa/crypto
+// doesn't support other exponent values.
+var (
+	rsaPSS2048NISTKey = &nistRSATestKey{
+		n: "c6e0ed537a2d85cf1c4effad6419884d824ceabf5200e755691cb7328acd6a755fe85798502ccaec9e55d47afd0cf3258ebe920b50c5fd9d72897462bd0e459bbdf902b63d17195b2ef54908980be12aa7489f8af274b92c0cbc16aed2fa46f782d5517b666edfb2e5e5efeaff7e24965e26472e51980b0cfe457d297e6aa5dacb8e728dc6f58130f925a13275c3cace62f820db1e13cc5274c58ff4d7837671a1bf5f80d6ad8699c568df8d24dd0f152ded36ef4861f59b354bba96a076913a25facf4722737e6deed95b69a00fb2bced0feeedea4ff01a92605cfe26a6b39553d0c74e5650eb3779705e135c4b2fa518a8d4339c53efab4bb0058238def555",
+		e: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001",
+	}
+	rsaPSS3072NISTKey = &nistRSATestKey{
+		n: "a5f3da0aaf54b45f99a5d7085f213c3721cbe7e83b3e6c3fe0f5a84c7e387ba513392c28a9010d3b618c03847e6b11bbbbe4d5e47fc97ea696250699e96ecd911404f7b806957038a68bb59a520f2d90182d183e035204a914e6ac03c2bc6d3f9d7856b25f9041b56df310de3feb30aa468a0668a1e5da9cdb185956caa5d75e1cdcac2db823173495619105367231b7f2de7528a8a79ec9fdbbab601178a204a5aa4e19759eb16ea4bab87bf48bb1790f9fc6eb4d5674d3fbc11b922558d4e568e454b26a7178f3e147beb0c8ca6ecff5e52af248ac07d6a189393e17232adff2f7423f56b94b9a7d61fde23a9558ac7a3bc7c06748a5da11759f92baf4e386bb0212565b5beecf31d063cfab71af896b3d734750d9bca07343bfb3c28645226e9dad3070fc247c71c078e974934941000a79d01abab14d21f5e608c4e4d13deec1aef298e1247c50b47bfee6162f352f41cdba8628d1d628848c876cfb102dacce7fa160c04d3aabc8667a142a710b7f495fd350c4862a653d15c33d9266fd",
+		e: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001",
+	}
+	rsaPSS4096NISTKey = &nistRSATestKey{
+		n: "d7e8df622c35d46a2b92aa4a4167415178f57aa2b14f996008e41244a2c6e4e39e897579fa4d4fce60199033ee922c0f09fb8fe6572eed3a17a3c7a4ee33c8a715b6185200735c4dab77cab436b30b4d7a75900da2e87c823f233c45cd074db5805e70d582b31e3532f55aa73162302298c1c14dad186aa558c88840b04d8ce503a8a766089b66c1b0b4dcfa609e8376dee4913dfff6c3f8dcf2e962c7b72c867f67ad2b2750e920ea19ea518ee6b9d2149ff730afbeadae29b1cca3b73a61e867a700c12d3fce1b10f56b3611e4c37fe11042cf4230d7d966e5d1cdbc4e53d7049cd7f5066762db27193560f842a234f9d6d018f6bfa92a36d90c4b695e63e1ff8af82933431443a98dfaf17e780038f8cca2672ede3529aafe1d38ef73a5939c8664b3f39b0f45207cfe862f7059a8d36dcaa19588c1294e9720bc72474717e9924d1ab206aa16bf09a3dbf6cd6bdbd093870553bf6a14ea71bc24f892977d0f2adf22673813f923228fefa114c3333a40e86cd97f64bcedcaf0a2516c4a11cbd7ad2898b684f39b15435136c13e0aa1866d10b59c0a5b6338bca491daa62cf22e91edacf26cb6a3e6a9a6ce9671ef4e07612de935a9b0cc0ec437c6f7a23e895aac5708ebd7ef9d222b70370fcdd44cb8a1caf6f9009ae6fe6283e5508d5270b5b052fa7901311724f418e8c65fedfdfb9ac51f2bc98c30c909aaeb3fbe9d",
+		e: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001",
+	}
+)
+
+type nistRSAPSSTestVector struct {
+	name     string
+	msg      string
+	sig      string
+	hashFunc commonpb.HashType
+	saltLen  int
+	pubKey   *nistRSATestKey
+}
+
+// The following test vectors are from:
+// https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/Digital-Signatures
+// Publication FIPS 186-4
+// Signature Verification PSS
+var nistRSAPSSTestVectors = []nistRSAPSSTestVector{
+	{
+		name:     "RSA_SSA_PSS_2048_SHA256_10",
+		msg:      "81eaf473d40896dbf4deac0f35c63bd1e129147c76e7aa8d0ef921631f55a7436411079f1bcc7b98714ac2c13b5e7326e60d918db1f05ffb19da767a95bb141a84c4b73664ccebf844f3601f7c853f009b21becba11af3106f1de5827b14e9fac84b2cbf16d18c045622acb260024768e8acc4c0ae2c0bd5f60a98023828cdec",
+		sig:      "40d59ebc6cb7b960cbda0db353f9b85d77e7c03f84447fb8e91b96a5a7377abc329d1f55c85e0dbedbc2886ce191d9e2cf3be05b33d6bbd2ba92b85eee2ff89cd6ee29cd531e42016e6aba1d620fe55e44480c033e8a59c0852dd1caffbc2ce82969e3a9f44ceff79f89993b9ebf3741b2ccab0b9516f2e128656a5b2ad5251e20c6ce0c26a14eef7ee86458942ddbe95ccc1f67b253e43e72117f49595dab5ba423496ece12825435661112666dbae71aaffd5a8f1d58db9dc02e0d70fe3ac36a87b8eeed4f20c00fd4303f9f767d03bca1a619bbe4b08e4e53b5cb69d2ba0235063e04ca392334d9979a41c42a66ca8b9721edcf76989ba89f3a170bb2e485",
+		hashFunc: commonpb.HashType_SHA256,
+		saltLen:  10,
+		pubKey:   rsaPSS2048NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_2048_SHA384_10",
+		msg:      "32a7b1479acf505db793f3ebed953f4e31c9ecad1a3479df3af31e89ae7e0387f42eaf8efdfdc30f838ee85e9d6d06139197b7b1e93dfb85c9c52dd17f12352a5c05001fc2432d1b7f39098d595ebe45eab8c721afa2a7ea5bccdb7971830d1e11338a42122af64a529e3fbf4af2cface635064893ece7d5991111c8ab5bf12a",
+		sig:      "0cb375ecc34a9f36b88bf56ebf1235387ffacfd3dd09c48e872897caca60af9e386496aafd0d4b1fd8fb4714fac925edda6f34633c3bb08f7cca3d9ad8b76472de8c9f91cb7518648d368fbeb31d1a7cb39a40a7b17ee2f7bace9bd99ba08295aadd856cd6902ee6c96d5c1291dc299a7f3528a869f62fb8fbd51817ffe6490ed6e0007d7981ab12b8f4ce0d7432e8c3213fae2b81006f333714b513eba0414c161fab6ea23338567995f273e3269c44a587ad835c320d1e5ff553db4c47126680cd58293231915cf7aefb80690499243eda83f5347a300e070568baee2745b20c68688dad6e3807afcb34c72cdaeb9a571089c7f8c63d1b6ffdbe2fd13330e6",
+		hashFunc: commonpb.HashType_SHA384,
+		saltLen:  10,
+		pubKey:   rsaPSS2048NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_2048_SHA512_10",
+		msg:      "35a37946e52678ee378f5f176838ef08f3c21392b1ad204645255be5b71fbc185fa5f161056ea65246b204fd393c77ab53c1b5d18870fc3fb3ca9a9b38b4b30ee8cb3f3d25f7527b4643a03c3dec40cd76b7b04303881ab2f731d59f0f882fb798bc6ac18ce904d1ffe93cbeb96ed1d7254d0dd26a1d0205d70114d984c2b77b",
+		sig:      "56279ccd2d37e8113625732cd3f3b61b4ef9325160c7f6af7077c25049d32742607ae3f845bd66cd6752813c26067fdc23f08008cf6a531124e9ebc9264f7cfd6d6eeff15daf97dad22565ec36b69125e7b27fc93892f6ff42ce8f265dc2cf2e5758ba0d67968e800e73fd47131008f5adc863919ea0cc153cf7efff134b0bcbd0af5505ab49af7b75d63a8aa7976b8fe77baf5a699a7e38a60eb7ca64e834f3e0f89da5b6a343a01f7657fd842c091a6208503bc75de8f95de0d871a6fc114b594ff99d615825fd3b896933381452536d68d9e034f65abf3412e8e32002689e102a8bb69991e04a7ff681b62e48ee687badf8690b2ccee4bf245cd0a25dcf21",
+		hashFunc: commonpb.HashType_SHA512,
+		saltLen:  10,
+		pubKey:   rsaPSS2048NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_3072_SHA256_10",
+		msg:      "886f83a22335aee35be0f76ec4c32e644c29467e1ba459fcbea2ebdf8541735829651880b207b84998d02eb529e6d5462a0648b5c1d36ce7936db11c2946946a9831696a61bc573196c0a4813e363241fb4c4a2beab999c5cb4d789262cc71891cfcfec6f6fd93809bd9df3bcc5c503e0526d5485efee77faf69caa9f77b109e",
+		sig:      "92ae66dbda65a60a5f70031c3562ece74e615486acde2276c20ea01d82f7bab23af2aba27d62749850d49d2689381f1e875ade766dfe7b7f02fd601f3401dcf319caef080d73caecc08a2b4576d4cc3704bde1b7495bce086846a8a01488d00aecd54d4045f0b9e31262b460a94f0563e3d9eeb86d8f9403e5eef0d223ceb74e058a8095db8efe228d6755715bbaaafe1ab375df1112d740d951db72f6f4d25fae951a26d4c1d99f3b5a7bf311fa9cf580860b8c1b434e03d3ae0500b6457a2582275db531037a781aefc9d4f5820167a2a9cf86595fed5596246fa7fd2c8c2df0042caf9e25c59b289f58474145bf50950ecb05271afd7ce21da967b415f0de136ce5ba01fa7948bff66fea0a8063882cf88469a495ae75bba4ffb539ee5176731ec3778477f643669f94de0e4b7c856bcc511f56ad8aa23edca0c1d84d2c19abbdd191bfc0ca898fdb4b8100c44df99e5afcc93faf227a01a63c15bfd26e5e3f49fb34a98ea8a851703aad68463513d3a2ee6a4a2fe9fee958205dfd2db254",
+		hashFunc: commonpb.HashType_SHA256,
+		saltLen:  10,
+		pubKey:   rsaPSS3072NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_3072_SHA384_10",
+		msg:      "5e5ca00767fad960921dcbbf16eb8e2ee85ad6db8caa6dbe2c33e17ce7607a8c6bfc6e98c6ac582679bde777ce20d3af6cb3163729c358601fceab49028d7802b131b9f10aee697503b639caf647852d3d678640ff6ec9af4906e014612f57185786eadbdcc6f497578f2b8036668bee82fe90bdb7b5a8f4d262e8a6ab4efe16",
+		sig:      "2ad995df3355aafb5dc71f06f10e42ef27d5a755351806961dec23ed08dc9cf0cd8d80a40fffe5ee54bdc71f355f661b59ff7a438a642b96d3e6ef95a54e5fd7d7af188b307914b8b8d05cbc09a046545be5c53908027c7324dd84b42f2a0054768161c6b1bad21de778babfe626f74bc325fed37dbba68648ad0b70881ee765825a215c23f21ae7f4805da65ae14fa8320cfa0cc43396e7a2193317695587bd8b4a3e935607465a0d29aa44f80ffb33e95604d087362a9a297aa8585cfcb4e1781bab34fbe7ea5503fd9a1deeec50caa56d7361d1159de065d2acf667d8bd46026cbca2cf8492e4ab6be427400db381b64e220c1cca709edc2768e80db78920912a929031c9bd0e13ad9fd6560c343904dc1bad633c3cdb7563dd4cf444ef2f7d8df047fe3ded5b277f94d56abf819943303fa6d9f0b55ecc20f98e92e6f1d34761148e7a51c51d5bd012cd6032aa2b3f0646059df34045f837c7eeaca6fa16357aa2c48922e34d4fb48183d11ca049684ae198c053de473bc167531b712516",
+		hashFunc: commonpb.HashType_SHA384,
+		saltLen:  10,
+		pubKey:   rsaPSS3072NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_3072_SHA512_10",
+		msg:      "be7d5fca06c75896b6bbb0333a625d876b851447bf121975bcc05527b3f6a98baaea82289a06ad66db8f6d51dc88cb9a17d42ede449c2b2bcdf09ec183b1fa158faedd1cab0de8c592edcdc8b449a99e2f1f95d0fbd2777564ce1ff6be6a8f155412992ea1a5b0bcc31cf81e2c6d9f9c9bae70a54a7ea55a69a1fd51ccea0f92",
+		sig:      "04625331d8a8a03eb818e027407341feb037706e7421ae3a9c95a4119d98ee4c47c3262562ac6785c0c5fbed288478a1b69b5d51185780f4f3a3e897f453f89e279336ceaded9901d0a696ec0ced21dbca16e31b7996b642b7778e1752365f7a17c95d9e10c750c72d373deb940423fdbe63074be971590dfa9b2f6d629eae702cf715e052eb1631a934994f5bda15eaf2bf032a0059804145b50c8c68401458dd34d972d4747faa894bb830dafae440cd81096756a0897d8656c60ea7665922b0b0c21055fd411e9487e63213cc6c0ce5193fee48b99942685649de2d89f260800e8797bd4d572aa0c92b56466b431e5355c417123303a4fc908d7be8e1528724cd19906fd117a8390b8b9a61399a8284425b15025f34122f2bada19a18e3859d5dc03d6ae30aac2c7145288927aece2b2a5c2769d4de0fe90523362ab416f253a66265cb3b31613e4fb37f23319432a28838fa8adf1c3f807bbffb9d6eea78409ceb985f5d301daede0486e0fa933f81d632d3ac9690a7be4bef961c194132",
+		hashFunc: commonpb.HashType_SHA512,
+		saltLen:  10,
+		pubKey:   rsaPSS3072NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_4096_SHA256_10",
+		sig:      "40a35351e11b783060d83bcafce26acf0d60e4b65c1aa487d705b9cc46393f7604c7237d168448a74c1acec7cfdedbd16c4c7bb25df9a1d89d3eb6d99e008cb61bcd466f9a30fbb4299eda0d9a327fa4102294ef08205e814bc44ba2a0243429f84a550b9d0b9dc19c823e92608a1b7e35f29d62eb11194ea55dd413ed82f35c25a1ad83f1d8844abf46b841cdbea149bd0a17bd6bd728e55d610de19618686eacab67a84fd6dc03c05deabd13bb83a26a6978d529fa15b468c35eca64c75209fb2a5cf6b624436710765e6507226b4e46686d247c17769c90e1839be25a5725262f82980bb097d7a06695d56426e244e01174dfbe9288611dfc6da3da181287c08a90488e5dc0698f9d5ec9dd0ac9dee0fe2bde7cb2f1c1aa5b61aefa44dc681f3e090b8de0e7e3eb52ca10a9c2d029aa0d8318f26e33e54d591f831a7f368db305da474fed7a7720ad3915cefd59c4c5b35d5066a48c8a9fc2e2bbb1430e7ba125c332c8d450e6d313c992878d80b4667cbfa255b9f79b82cabe2c24752c3da0917a4cbba9280279a7621724b6987e04e39f13bbd1ceec95031eb061142922160fd4c55af93148c36e36fb423ce113035d0d441aef27f7a60de29afda06425f2c51fc73c297ee8151e8a0e05b307db6f9a076271f07099cd902aff495461b7dcdd32cece0949d87e7f630f0ee5052d0e65c4496ae6645efad9e54052797388",
+		msg:      "158009419260a400e8eb9d7f65c65c9c3fdc67d3d99aca0c425fbcb7fe2e7f1b0aa788eb1a35e01b2588caf12346a65f16fd1590475d5ec1d2a411526459ea1d443df706907ffdd3ca2f193f93f5a349b50357d26748b767cde6ab5cbfe76b1acb2b9eb97da5c4d2ddc8d18e3a3b1a0326d475c1c2c49ca73c0fd3fc9540cbbb",
+		hashFunc: commonpb.HashType_SHA256,
+		saltLen:  10,
+		pubKey:   rsaPSS4096NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_4096_SHA384_10",
+		sig:      "a8edbfcefecfc5bd4782ec1701a99fb5c5901e07d4fd004cab64e4331c70595ed7704a5747066d19ec4f70051932d833eedd46b0d21bef7111559d4439fdc859fd538a86088fb19cff2ba3d8a1f907b0eb9ec0a6aecab645cfd05e3f8297288c015b51bc761f4a431cdeeb441155a3f465f13758018803cb724bf062213ce1f6a59b4ad5516465a2064b8920c41af8df2c2d406f776ee0ac66f19ec0ecc0874d19bc1ff5a41a0db246d66d0814c4befc7430f668e94af3a2188c7d8d44a4426ec4801d652519db2badd641382dd7ef567be5458eed7a23429ecfb3d38d96534846dc4d62bfa2106156b714fd380e6f3a31702818c3349e6d7a5aa0a452ec8645936a37767c62b03aaf7f907ce70567dbb915a8c86b1ea1c32786e5c8f160319db7641accdd53bf9d7b8f648e1082c5521c41347dc58d1455c6e72bbea93f4ecf762f980b08ef2c31567190078d7c6b772a9816bdbc1d40b5ccd0d4979b69b78e1a401bec0bb957192dd31046a2dcc2489fae00dc8fa293f89ec8b36b56acac3f87bd92e04ba6ad5df7d46d322d0cf2b930a499a35971e2a1850651a7cf7e59e930f62bba68e436836b5e47490d92e35d3dca827e4448c8c92af7d6796663ab7fabbbcef0bc9f61fc00c822e406ed8fcea1b42286ae054c40c97c1eb2656c59bd3c6b18566e32f27c0e8f5ded3afdb28f175ab90b1980667666c5d06162f975f5",
+		msg:      "3874dd769d0426ee7dcbdceb67a9ad770e1781e34b15a45f656328c88ff485c1b2a083056d195afc5b20178c94f94131761cbd50a52defc8502e22cbb6f42aece9d74778d2ae4d0a76fb025a7762c856de607c7417399d463d32b14f9901e4156582f377d5ab484158c267fe1bcd880dce4b85f7ac21f700b5d79cfc3e04fd64",
+		hashFunc: commonpb.HashType_SHA384,
+		saltLen:  10,
+		pubKey:   rsaPSS4096NISTKey,
+	},
+	{
+		name:     "RSA_SSA_PSS_4096_SHA384_10",
+		sig:      "3e6544351f1d6e4e76e354446b416544b54494831e99ac6adfdf68ca28dc8ea30cda2085d7f8ac0fec27d03c8ab0705835d647822277ec7b7bccd8c6857a9c017a6139cd88f0bf9a4559f1308445ffeafd94e010aff4127773b8ccbcaba0e8184d8ce8c990cea9d3b9f41b889bf5451eb2b6afd89b5bfe1e681e2447c4020ee93369e3bff4bfdebd03ba5b17ff78bb5dff04e4ec440d080ea26ef9aba76e9872ec271d56d678ce63588eaa50da25c005193030e0deb4b842e6aca4c645c094754a41e61263a5056852902b70dafa5381bddfa3dd6d881c5c67c33009d4fc8843c55e8662bec083ead69b1c005ea7aea175c08cf27c838aa9bb2c3eb2e08a7e458efce3f394118d069aa6e0663f7339753c49f14a1209fe8d3e546d0dad553e144939c208a48b4797afc24b9eb1788b65f568b8a815359a78bc92185f59c9532666bff497b56035a98f645d28cf12c1063f83cf736c6f38016a9626a886144cc90ad9dcae6a0d36fbd8377f13cf03342f59fbdd99f3985e17a364f0a2835332f4eb494ef16b63101f05dbc826ced2afb213da2aa368b2895fbe809a92873c6547e9755c35097c32ffc2c62ff395cec8e50a2d7ad50ed99f3daa8bfc0d16c9a63ae9fb150c88b49162d489a2cb8b0dbf260c113a9f9883728fc089e0af3026bf9a4fb3b8ef4ef85ff7f055b13b403bececb9f62bc6922153bed8b2a78b71168cea",
+		msg:      "b1a82d51fc2abf919b68f369f3057136f8f2f1204337f0fb66f0a76f7c953d57047f3c68efa84213f7b3f9ac332c48cbe810cbf3a39081718412c587dd7980cafca69cc9443ebcef83ae2aab7f6d10cdd281ec34f8453ea6a76983ff5e3a678e412437bc247595eee6636fad005132055d4e3a2a6ddf8e6275feca1e29625c6a",
+		hashFunc: commonpb.HashType_SHA512,
+		saltLen:  10,
+		pubKey:   rsaPSS4096NISTKey,
+	},
+}
+
+func (t *nistRSAPSSTestVector) ProtoKey() (*rsppb.RsaSsaPssPublicKey, error) {
+	e, err := hex.DecodeString(t.pubKey.e)
+	if err != nil {
+		return nil, fmt.Errorf("hex.DecodeString(t.pubKey.e) err = %v, want nil", err)
+	}
+	n, err := hex.DecodeString(t.pubKey.n)
+	if err != nil {
+		return nil, fmt.Errorf("hex.DecodeString(t.pubKey.n) err = %v, want nil", err)
+	}
+	return &rsppb.RsaSsaPssPublicKey{
+		Version: 0,
+		Params: &rsppb.RsaSsaPssParams{
+			SigHash:    t.hashFunc,
+			Mgf1Hash:   t.hashFunc,
+			SaltLength: int32(t.saltLen),
+		},
+		E: e,
+		N: n,
+	}, nil
+}
+
+func TestRSASSAPSSVerifierPrimitive(t *testing.T) {
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	for _, tc := range nistRSAPSSTestVectors {
+		t.Run("nist test vector", func(t *testing.T) {
+			k, err := tc.ProtoKey()
+			if err != nil {
+				t.Fatalf("tc.ProtoKey() err = %v, want nil", err)
+			}
+			sig, err := hex.DecodeString(tc.sig)
+			if err != nil {
+				t.Fatalf("hex.DecodeString() err = %v, want nil", err)
+			}
+			msg, err := hex.DecodeString(tc.msg)
+			if err != nil {
+				t.Fatalf("hex.DecodeString() err = %v, want nil", err)
+			}
+			serializedPublic, err := proto.Marshal(k)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			v, err := vkm.Primitive(serializedPublic)
+			if err != nil {
+				t.Fatalf("Primitive() err = %v, want nil", err)
+			}
+			verifier, ok := v.(tink.Verifier)
+			if !ok {
+				t.Fatalf("primitive isn't a tink verifier")
+			}
+			if err := verifier.Verify(sig, msg); err != nil {
+				t.Errorf("verifier.Verify() err = %v, want nil", err)
+			}
+		})
+	}
+}
+
+func TestRSASSAPSSVerifierPrimitiveFailsWithInvalidKey(t *testing.T) {
+	type testCase struct {
+		tag    string
+		pubKey *rsppb.RsaSsaPssPublicKey
+	}
+	vkm, err := registry.GetKeyManager(rsaPSSTestPublicKeyTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", err, rsaPSSTestPublicKeyTypeURL)
+	}
+	privKey, err := makeValidRSAPSSKey()
+	if err != nil {
+		t.Fatalf("makeValidRSAPSSKey() err = %v, want nil", err)
+	}
+	validPubKey := privKey.GetPublicKey()
+	for _, tc := range []testCase{
+		{
+			tag:    "empty public key",
+			pubKey: &rsppb.RsaSsaPssPublicKey{},
+		},
+		{
+			tag: "invalid public key version",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion() + 1,
+				Params:  validPubKey.GetParams(),
+				N:       validPubKey.GetN(),
+				E:       validPubKey.GetE(),
+			},
+		},
+		{
+			tag: "different sig and mgf1 hash functions",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    commonpb.HashType_SHA256,
+					Mgf1Hash:   commonpb.HashType_SHA384,
+					SaltLength: validPubKey.GetParams().GetSaltLength(),
+				},
+				N: validPubKey.GetN(),
+				E: validPubKey.GetE(),
+			},
+		},
+		{
+			tag: "negative salt length",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    validPubKey.GetParams().GetSigHash(),
+					Mgf1Hash:   validPubKey.GetParams().GetMgf1Hash(),
+					SaltLength: -1,
+				},
+				N: validPubKey.GetN(),
+				E: validPubKey.GetE(),
+			},
+		},
+		{
+			tag: "invalid hash function",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    commonpb.HashType_UNKNOWN_HASH,
+					Mgf1Hash:   commonpb.HashType_UNKNOWN_HASH,
+					SaltLength: validPubKey.GetParams().GetSaltLength(),
+				},
+				N: validPubKey.GetN(),
+				E: validPubKey.GetE(),
+			},
+		},
+		{
+			tag: "unsafe hash function",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params: &rsppb.RsaSsaPssParams{
+					SigHash:    commonpb.HashType_SHA1,
+					Mgf1Hash:   commonpb.HashType_SHA1,
+					SaltLength: validPubKey.GetParams().GetSaltLength(),
+				},
+				N: validPubKey.GetN(),
+				E: validPubKey.GetE(),
+			},
+		},
+		{
+			tag: "invalid modulus",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params:  validPubKey.GetParams(),
+				N:       []byte{0x00},
+				E:       validPubKey.GetE(),
+			},
+		},
+		{
+			tag: "invalid exponent",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params:  validPubKey.GetParams(),
+				N:       validPubKey.GetN(),
+				E:       []byte{0x01},
+			},
+		},
+		{
+			tag: "exponent larger than 64 bits",
+			pubKey: &rsppb.RsaSsaPssPublicKey{
+				Version: validPubKey.GetVersion(),
+				Params:  validPubKey.GetParams(),
+				N:       validPubKey.GetN(),
+				E:       random.GetRandomBytes(32),
+			},
+		},
+	} {
+		t.Run(tc.tag, func(t *testing.T) {
+			serializedPubKey, err := proto.Marshal(tc.pubKey)
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			if _, err := vkm.Primitive(serializedPubKey); err == nil {
+				t.Errorf("Primitive() err = nil, want error")
+			}
+		})
+	}
+}
diff --git a/go/signature/signature.go b/go/signature/signature.go
index 117d6ae..9d16a03 100644
--- a/go/signature/signature.go
+++ b/go/signature/signature.go
@@ -24,6 +24,7 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 )
 
 func init() {
@@ -39,6 +40,9 @@
 	if err := registry.RegisterKeyManager(new(ed25519SignerKeyManager)); err != nil {
 		panic(fmt.Sprintf("signature.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(ed25519SignerTypeURL); err != nil {
+		panic(fmt.Sprintf("signature.init() failed: %v", err))
+	}
 	if err := registry.RegisterKeyManager(new(ed25519VerifierKeyManager)); err != nil {
 		panic(fmt.Sprintf("signature.init() failed: %v", err))
 	}
@@ -50,4 +54,12 @@
 	if err := registry.RegisterKeyManager(new(rsaSSAPKCS1VerifierKeyManager)); err != nil {
 		panic(fmt.Sprintf("signature.init() failed: %v", err))
 	}
+
+	// RSA SSA PSS
+	if err := registry.RegisterKeyManager(new(rsaSSAPSSSignerKeyManager)); err != nil {
+		panic(fmt.Sprintf("signature.init() failed: %v", err))
+	}
+	if err := registry.RegisterKeyManager(new(rsaSSAPSSVerifierKeyManager)); err != nil {
+		panic(fmt.Sprintf("signature.init() failed: %v", err))
+	}
 }
diff --git a/go/signature/signature_factory_test.go b/go/signature/signature_factory_test.go
index 6437a9d..b070cc4 100644
--- a/go/signature/signature_factory_test.go
+++ b/go/signature/signature_factory_test.go
@@ -17,13 +17,22 @@
 package signature_test
 
 import (
+	"bytes"
+	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/testing/stubkeymanager"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/signature"
 	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testing/fakemonitoring"
 	"github.com/google/tink/go/testkeyset"
 	"github.com/google/tink/go/testutil"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
@@ -120,10 +129,7 @@
 	}
 }
 
-func newECDSAKeysetKeypair(hashType commonpb.HashType,
-	curve commonpb.EllipticCurveType,
-	outputPrefixType tinkpb.OutputPrefixType,
-	keyID uint32) (*tinkpb.Keyset_Key, *tinkpb.Keyset_Key) {
+func newECDSAKeysetKeypair(hashType commonpb.HashType, curve commonpb.EllipticCurveType, outputPrefixType tinkpb.OutputPrefixType, keyID uint32) (*tinkpb.Keyset_Key, *tinkpb.Keyset_Key) {
 	key := testutil.NewRandomECDSAPrivateKey(hashType, curve)
 	serializedKey, _ := proto.Marshal(key)
 	keyData := testutil.NewKeyData(testutil.ECDSASignerTypeURL,
@@ -177,3 +183,311 @@
 		t.Errorf("signature.NewVerifier(goodPublicKH) err = %v, want nil", err)
 	}
 }
+
+func TestPrimitiveFactorySignVerifyWithoutAnnotationsDoesNothing(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	privHandle, err := keyset.NewHandle(signature.ED25519KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	signer, err := signature.NewSigner(privHandle)
+	if err != nil {
+		t.Fatalf("signature.NewSigner() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	verifier, err := signature.NewVerifier(pubHandle)
+	if err != nil {
+		t.Fatalf("signature.NewVerifier() err = %v, want nil", err)
+	}
+	data := []byte("some_important_data")
+	sig, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("signer.Sign() err = %v, want nil", err)
+	}
+	if err := verifier.Verify(sig, data); err != nil {
+		t.Fatalf("verifier.Verify() err = %v, want nil", err)
+	}
+	if len(client.Events()) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(client.Events()))
+	}
+	if len(client.Failures()) != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", len(client.Failures()))
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsLogSignVerify(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	handle, err := keyset.NewHandle(signature.ED25519KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	signer, err := signature.NewSigner(privHandle)
+	if err != nil {
+		t.Fatalf("signature.NewSigner() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	buff.Reset()
+	if err := insecurecleartextkeyset.Write(pubHandle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	pubHandle, err = insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	verifier, err := signature.NewVerifier(pubHandle)
+	if err != nil {
+		t.Fatalf("signature.NewVerifier() err = %v, want nil", err)
+	}
+	data := []byte("some_important_data")
+	sig, err := signer.Sign(data)
+	if err != nil {
+		t.Fatalf("signer.Sign() err = %v, want nil", err)
+	}
+	if err := verifier.Verify(sig, data); err != nil {
+		t.Fatalf("verifier.Verify() err = %v, want nil", err)
+	}
+	if len(client.Failures()) != 0 {
+		t.Errorf("len(client.Failures()) = %d, want 0", len(client.Failures()))
+	}
+	got := client.Events()
+	wantVerifyKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: pubHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     pubHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.Ed25519PublicKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	wantSignKeysetInfo := &monitoring.KeysetInfo{
+		Annotations:  annotations,
+		PrimaryKeyID: privHandle.KeysetInfo().GetPrimaryKeyId(),
+		Entries: []*monitoring.Entry{
+			{
+				KeyID:     privHandle.KeysetInfo().GetPrimaryKeyId(),
+				Status:    monitoring.Enabled,
+				KeyType:   "tink.Ed25519PrivateKey",
+				KeyPrefix: "TINK",
+			},
+		},
+	}
+	want := []*fakemonitoring.LogEvent{
+		{
+			Context:  monitoring.NewContext("public_key_sign", "sign", wantSignKeysetInfo),
+			KeyID:    privHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+		{
+			Context:  monitoring.NewContext("public_key_verify", "verify", wantVerifyKeysetInfo),
+			KeyID:    privHandle.KeysetInfo().GetPrimaryKeyId(),
+			NumBytes: len(data),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+type alwaysFailingSigner struct{}
+
+func (a *alwaysFailingSigner) Sign(data []byte) ([]byte, error) { return nil, fmt.Errorf("failed") }
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsSignFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	typeURL := "TestPrimitiveFactoryMonitoringWithAnnotationsSignFailureIsLogged" + "PrivateKeyManager"
+	km := &stubkeymanager.StubPrivateKeyManager{
+		StubKeyManager: stubkeymanager.StubKeyManager{
+			URL:  typeURL,
+			Prim: &alwaysFailingSigner{},
+			KeyData: &tinkpb.KeyData{
+				TypeUrl:         typeURL,
+				Value:           []byte("serialized_key"),
+				KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE,
+			},
+		},
+	}
+	if err := registry.RegisterKeyManager(km); err != nil {
+		t.Fatalf("registry.RegisterKeyManager() err = %v, want nil", err)
+	}
+	template := &tinkpb.KeyTemplate{
+		TypeUrl:          typeURL,
+		OutputPrefixType: tinkpb.OutputPrefixType_LEGACY,
+	}
+	kh, err := keyset.NewHandle(template)
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	if err := insecurecleartextkeyset.Write(kh, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	annotations := map[string]string{"foo": "bar"}
+	privHandle, err := insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	signer, err := signature.NewSigner(privHandle)
+	if err != nil {
+		t.Fatalf("signature.NewSigner() err = %v, want nil", err)
+	}
+	if _, err := signer.Sign([]byte("some_data")); err == nil {
+		t.Fatalf("signer.Sign() err = nil, want error")
+	}
+	if len(client.Events()) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(client.Events()))
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"public_key_sign",
+				"sign",
+				monitoring.NewKeysetInfo(
+					annotations,
+					kh.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     kh.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   typeURL,
+							KeyPrefix: "LEGACY",
+						},
+					},
+				),
+			),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestPrimitiveFactoryMonitoringWithAnnotationsVerifyFailureIsLogged(t *testing.T) {
+	defer internalregistry.ClearMonitoringClient()
+	client := fakemonitoring.NewClient("fake-client")
+	if err := internalregistry.RegisterMonitoringClient(client); err != nil {
+		t.Fatalf("internalregistry.RegisterMonitoringClient() err = %v, want nil", err)
+	}
+	privHandle, err := keyset.NewHandle(signature.ED25519KeyTemplate())
+	if err != nil {
+		t.Fatalf("keyset.NewHandle() err = %v, want nil", err)
+	}
+	pubHandle, err := privHandle.Public()
+	if err != nil {
+		t.Fatalf("privHandle.Public() err = %v, want nil", err)
+	}
+	buff := &bytes.Buffer{}
+	annotations := map[string]string{"foo": "bar"}
+	if err := insecurecleartextkeyset.Write(pubHandle, keyset.NewBinaryWriter(buff)); err != nil {
+		t.Fatalf("insecurecleartextkeyset.Write() err = %v, want nil", err)
+	}
+	pubHandle, err = insecurecleartextkeyset.Read(keyset.NewBinaryReader(buff), keyset.WithAnnotations(annotations))
+	if err != nil {
+		t.Fatalf("insecurecleartextkeyset.Read() err = %v, want nil", err)
+	}
+	verifier, err := signature.NewVerifier(pubHandle)
+	if err != nil {
+		t.Fatalf("signature.NewVerifier() err = %v, want nil", err)
+	}
+	if err := verifier.Verify([]byte("some_invalid_signature"), []byte("some_invalid_data")); err == nil {
+		t.Fatalf("verifier.Verify() err = nil, want error")
+	}
+	if len(client.Events()) != 0 {
+		t.Errorf("len(client.Events()) = %d, want 0", len(client.Events()))
+	}
+	got := client.Failures()
+	want := []*fakemonitoring.LogFailure{
+		{
+			Context: monitoring.NewContext(
+				"public_key_verify",
+				"verify",
+				monitoring.NewKeysetInfo(
+					annotations,
+					pubHandle.KeysetInfo().GetPrimaryKeyId(),
+					[]*monitoring.Entry{
+						{
+							KeyID:     pubHandle.KeysetInfo().GetPrimaryKeyId(),
+							Status:    monitoring.Enabled,
+							KeyType:   "tink.Ed25519PublicKey",
+							KeyPrefix: "TINK",
+						},
+					},
+				),
+			),
+		},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("%v", diff)
+	}
+}
+
+func TestVerifyWithLegacyKeyDoesNotHaveSideEffectOnMessage(t *testing.T) {
+	privateKey, publicKey := newECDSAKeysetKeypair(commonpb.HashType_SHA256,
+		commonpb.EllipticCurveType_NIST_P256,
+		tinkpb.OutputPrefixType_LEGACY,
+		2)
+	privateKeyset := testutil.NewKeyset(privateKey.KeyId, []*tinkpb.Keyset_Key{privateKey})
+	privateHandle, err := testkeyset.NewHandle(privateKeyset)
+	if err != nil {
+		t.Fatalf("testkeyset.NewHandle(privateHandle) err = %v, want nil", err)
+	}
+	publicKeyset := testutil.NewKeyset(publicKey.KeyId, []*tinkpb.Keyset_Key{publicKey})
+	publicHandle, err := testkeyset.NewHandle(publicKeyset)
+	if err != nil {
+		t.Fatalf("testkeyset.NewHandle(publicKeyset) err = %v, want nil", err)
+	}
+	signer, err := signature.NewSigner(privateHandle)
+	if err != nil {
+		t.Fatalf("signature.NewSigner(privateHandle) err = %v, want nil", err)
+	}
+	verifier, err := signature.NewVerifier(publicHandle)
+	if err != nil {
+		t.Fatalf("signature.NewVerifier(publicHandle) err = %v, want nil", err)
+	}
+
+	data := []byte("data")
+	message := data[:3] // Let message be a slice of data.
+
+	sig, err := signer.Sign(message)
+	if err != nil {
+		t.Fatalf("signer.Sign(message) err = %v, want nil", err)
+	}
+	err = verifier.Verify(sig, message)
+	if err != nil {
+		t.Fatalf("verifier.Verify(sig, message) err = %v, want nil", err)
+	}
+	wantData := []byte("data")
+	if !bytes.Equal(data, wantData) {
+		t.Errorf("data = %q, want: %q", data, wantData)
+	}
+}
diff --git a/go/signature/signature_init_test.go b/go/signature/signature_init_test.go
new file mode 100644
index 0000000..a9c4f51
--- /dev/null
+++ b/go/signature/signature_init_test.go
@@ -0,0 +1,36 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package signature_test
+
+import (
+	"testing"
+
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/testutil"
+)
+
+func TestSignatureInit(t *testing.T) {
+	// Check that the ECDSA signer key manager is in the global registry.
+	if _, err := registry.GetKeyManager(testutil.ECDSASignerTypeURL); err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+
+	// Check that the ECDSA verifier key manager is in the global registry.
+	if _, err := registry.GetKeyManager(testutil.ECDSAVerifierTypeURL); err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+}
diff --git a/go/signature/signature_key_templates.go b/go/signature/signature_key_templates.go
index 46d3cb2..75c62d7 100644
--- a/go/signature/signature_key_templates.go
+++ b/go/signature/signature_key_templates.go
@@ -17,10 +17,14 @@
 package signature
 
 import (
+	"fmt"
+
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	ecdsapb "github.com/google/tink/go/proto/ecdsa_go_proto"
 	rsppb "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto"
+	rspsspb "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
@@ -67,20 +71,6 @@
 		tinkpb.OutputPrefixType_RAW)
 }
 
-// ECDSAP384KeyTemplate is a KeyTemplate that generates a new ECDSA private key with the following parameters:
-//   - Hash function: SHA512
-//   - Curve: NIST P-384
-//   - Signature encoding: DER
-//   - Output prefix type: TINK
-//
-// Deprecated: Use [ECDSAP384SHA384KeyTemplate] or [ECDSAP384SHA512KeyTemplate] instead.
-func ECDSAP384KeyTemplate() *tinkpb.KeyTemplate {
-	return createECDSAKeyTemplate(commonpb.HashType_SHA512,
-		commonpb.EllipticCurveType_NIST_P384,
-		ecdsapb.EcdsaSignatureEncoding_DER,
-		tinkpb.OutputPrefixType_TINK)
-}
-
 // ECDSAP384SHA384KeyTemplate is a KeyTemplate that generates a new ECDSA private key with the following parameters:
 //   - Hash function: SHA384
 //   - Curve: NIST P-384
@@ -93,6 +83,18 @@
 		tinkpb.OutputPrefixType_TINK)
 }
 
+// ECDSAP384SHA384KeyWithoutPrefixTemplate is a KeyTemplate that generates a new ECDSA private key with the following parameters:
+//   - Hash function: SHA384
+//   - Curve: NIST P-384
+//   - Signature encoding: DER
+//   - Output prefix type: RAW
+func ECDSAP384SHA384KeyWithoutPrefixTemplate() *tinkpb.KeyTemplate {
+	return createECDSAKeyTemplate(commonpb.HashType_SHA384,
+		commonpb.EllipticCurveType_NIST_P384,
+		ecdsapb.EcdsaSignatureEncoding_DER,
+		tinkpb.OutputPrefixType_RAW)
+}
+
 // ECDSAP384SHA512KeyTemplate is a KeyTemplate that generates a new ECDSA private key with the following parameters:
 //   - Hash function: SHA512
 //   - Curve: NIST P-384
@@ -135,7 +137,7 @@
 //   - Hash function: SHA512
 //   - Curve: NIST P-521
 //   - Signature encoding: DER
-//   - Output prefix type: TINK
+//   - Output prefix type: RAW
 func ECDSAP521KeyWithoutPrefixTemplate() *tinkpb.KeyTemplate {
 	return createECDSAKeyTemplate(commonpb.HashType_SHA512,
 		commonpb.EllipticCurveType_NIST_P521,
@@ -145,15 +147,17 @@
 
 // createECDSAKeyTemplate creates a KeyTemplate containing a EcdasKeyFormat
 // with the given parameters.
-func createECDSAKeyTemplate(hashType commonpb.HashType, curve commonpb.EllipticCurveType,
-	encoding ecdsapb.EcdsaSignatureEncoding, prefixType tinkpb.OutputPrefixType) *tinkpb.KeyTemplate {
+func createECDSAKeyTemplate(hashType commonpb.HashType, curve commonpb.EllipticCurveType, encoding ecdsapb.EcdsaSignatureEncoding, prefixType tinkpb.OutputPrefixType) *tinkpb.KeyTemplate {
 	params := &ecdsapb.EcdsaParams{
 		HashType: hashType,
 		Curve:    curve,
 		Encoding: encoding,
 	}
 	format := &ecdsapb.EcdsaKeyFormat{Params: params}
-	serializedFormat, _ := proto.Marshal(format)
+	serializedFormat, err := proto.Marshal(format)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          ecdsaSignerTypeURL,
 		Value:            serializedFormat,
@@ -185,7 +189,10 @@
 		ModulusSizeInBits: modulusSizeInBits,
 		PublicExponent:    []byte{0x01, 0x00, 0x01},
 	}
-	serializedFormat, _ := proto.Marshal(keyFormat)
+	serializedFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          rsaSSAPKCS1SignerTypeURL,
 		OutputPrefixType: prefixType,
@@ -193,6 +200,27 @@
 	}
 }
 
+func create_RSA_SSA_PSS_Template(prefixType tinkpb.OutputPrefixType, hashType commonpb.HashType, saltLength int32, modulusSizeInBits uint32) *tinkpb.KeyTemplate {
+	keyFormat := &rspsspb.RsaSsaPssKeyFormat{
+		Params: &rspsspb.RsaSsaPssParams{
+			SigHash:    hashType,
+			Mgf1Hash:   hashType,
+			SaltLength: saltLength,
+		},
+		ModulusSizeInBits: modulusSizeInBits,
+		PublicExponent:    []byte{0x01, 0x00, 0x01},
+	}
+	serializedFormat, err := proto.Marshal(keyFormat)
+	if err != nil {
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key format: %s", err))
+	}
+	return &tinkpb.KeyTemplate{
+		TypeUrl:          rsaSSAPSSSignerTypeURL,
+		OutputPrefixType: prefixType,
+		Value:            serializedFormat,
+	}
+}
+
 // RSA_SSA_PKCS1_3072_SHA256_F4_Key_Template is a KeyTemplate that generates a new RSA SSA PKCS1 private key with the following
 // parameters:
 //   - Modulus size in bits: 3072.
@@ -232,3 +260,51 @@
 func RSA_SSA_PKCS1_4096_SHA512_F4_RAW_Key_Template() *tinkpb.KeyTemplate {
 	return create_RSA_SSA_PKCS1_Template(tinkpb.OutputPrefixType_RAW, commonpb.HashType_SHA512, 4096)
 }
+
+// RSA_SSA_PSS_3072_SHA256_32_F4_Key_Template is a KeyTemplate that generates a new RSA SSA PSS private key with the following
+// parameters:
+//   - Modulus size in bits: 3072.
+//   - Signature hash: SHA256.
+//   - MGF1 hash: SHA256.
+//   - Salt length: 32 (i.e., SHA256's output length).
+//   - Public Exponent: 65537 (aka F4).
+//   - OutputPrefixType: TINK
+func RSA_SSA_PSS_3072_SHA256_32_F4_Key_Template() *tinkpb.KeyTemplate {
+	return create_RSA_SSA_PSS_Template(tinkpb.OutputPrefixType_TINK, commonpb.HashType_SHA256, 32, 3072)
+}
+
+// RSA_SSA_PSS_3072_SHA256_32_F4_Raw_Key_Template is a KeyTemplate that generates a new RSA SSA PSS private key with the following
+// parameters:
+//   - Modulus size in bits: 3072.
+//   - Signature hash: SHA256.
+//   - MGF1 hash: SHA256.
+//   - Salt length: 32 (i.e., SHA256's output length).
+//   - Public Exponent: 65537 (aka F4).
+//   - OutputPrefixType: RAW
+func RSA_SSA_PSS_3072_SHA256_32_F4_Raw_Key_Template() *tinkpb.KeyTemplate {
+	return create_RSA_SSA_PSS_Template(tinkpb.OutputPrefixType_RAW, commonpb.HashType_SHA256, 32, 3072)
+}
+
+// RSA_SSA_PSS_4096_SHA512_64_F4_Key_Template is a KeyTemplate that generates a new RSA SSA PSS private key with the following
+// parameters:
+//   - Modulus size in bits: 4096.
+//   - Signature hash: SHA512.
+//   - MGF1 hash: SHA512.
+//   - Salt length: 64 (i.e., SHA512's output length).
+//   - Public Exponent: 65537 (aka F4).
+//   - OutputPrefixType: TINK
+func RSA_SSA_PSS_4096_SHA512_64_F4_Key_Template() *tinkpb.KeyTemplate {
+	return create_RSA_SSA_PSS_Template(tinkpb.OutputPrefixType_TINK, commonpb.HashType_SHA512, 64, 4096)
+}
+
+// RSA_SSA_PSS_4096_SHA512_64_F4_Raw_Key_Template is a KeyTemplate that generates a new RSA SSA PSS private key with the following
+// parameters:
+//   - Modulus size in bits: 4096.
+//   - Signature hash: SHA512.
+//   - MGF1 hash: SHA512.
+//   - Salt length: 64 (i.e., SHA512's output length).
+//   - Public Exponent: 65537 (aka F4).
+//   - OutputPrefixType: RAW
+func RSA_SSA_PSS_4096_SHA512_64_F4_Raw_Key_Template() *tinkpb.KeyTemplate {
+	return create_RSA_SSA_PSS_Template(tinkpb.OutputPrefixType_RAW, commonpb.HashType_SHA512, 64, 4096)
+}
diff --git a/go/signature/signature_key_templates_test.go b/go/signature/signature_key_templates_test.go
index 31267e8..6d8d301 100644
--- a/go/signature/signature_key_templates_test.go
+++ b/go/signature/signature_key_templates_test.go
@@ -44,6 +44,8 @@
 			template: signature.ECDSAP256KeyWithoutPrefixTemplate()},
 		{name: "ECDSA_P384_NO_PREFIX",
 			template: signature.ECDSAP384KeyWithoutPrefixTemplate()},
+		{name: "ECDSA_P384_SHA384_NO_PREFIX",
+			template: signature.ECDSAP384SHA384KeyWithoutPrefixTemplate()},
 		{name: "ECDSA_P521_NO_PREFIX",
 			template: signature.ECDSAP521KeyWithoutPrefixTemplate()},
 		{name: "RSA_SSA_PKCS1_3072_SHA256_F4",
@@ -54,6 +56,14 @@
 			template: signature.RSA_SSA_PKCS1_4096_SHA512_F4_Key_Template()},
 		{name: "RSA_SSA_PKCS1_4096_SHA512_F4_RAW",
 			template: signature.RSA_SSA_PKCS1_4096_SHA512_F4_RAW_Key_Template()},
+		{name: "RSA_SSA_PSS_3072_SHA256_32_F4",
+			template: signature.RSA_SSA_PSS_3072_SHA256_32_F4_Key_Template()},
+		{name: "RSA_SSA_PSS_3072_SHA256_32_F4_RAW",
+			template: signature.RSA_SSA_PSS_3072_SHA256_32_F4_Raw_Key_Template()},
+		{name: "RSA_SSA_PSS_4096_SHA512_64_F4",
+			template: signature.RSA_SSA_PSS_4096_SHA512_64_F4_Key_Template()},
+		{name: "RSA_SSA_PSS_4096_SHA512_64_F4_RAW",
+			template: signature.RSA_SSA_PSS_4096_SHA512_64_F4_Raw_Key_Template()},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
@@ -69,7 +79,6 @@
 	if err != nil {
 		return fmt.Errorf("keyset.NewHandle(tc.template) failed: %s", err)
 	}
-
 	signer, err := signature.NewSigner(privateHandle)
 	if err != nil {
 		return fmt.Errorf("signature.NewSigner(privateHandle) failed: %s", err)
diff --git a/go/signature/signature_test.go b/go/signature/signature_test.go
index 9f629f6..9edac35 100644
--- a/go/signature/signature_test.go
+++ b/go/signature/signature_test.go
@@ -16,69 +16,104 @@
 
 package signature_test
 
+// [START signature-example]
+
 import (
-	"encoding/base64"
+	"bytes"
 	"fmt"
 	"log"
-	"testing"
 
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/signature"
-	"github.com/google/tink/go/testutil"
 )
 
-func TestSignatureInit(t *testing.T) {
-	// check for ECDSASignerKeyManager
-	_, err := registry.GetKeyManager(testutil.ECDSASignerTypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-
-	// check for ECDSAVerifierKeyManager
-	_, err = registry.GetKeyManager(testutil.ECDSAVerifierTypeURL)
-	if err != nil {
-		t.Errorf("unexpected error: %s", err)
-	}
-}
-
 func Example() {
-	kh, err := keyset.NewHandle(signature.ECDSAP256KeyTemplate()) // Other key templates can also be used.
-	if err != nil {
-		log.Fatal(err)
-	}
+	// A private keyset created with
+	// "tinkey create-keyset --key-template=ECDSA_P256 --out private_keyset.cfg".
+	// Note that this keyset has the secret key information in cleartext.
+	privateJSONKeyset := `{
+		"key": [{
+			"keyData": {
+					"keyMaterialType":
+							"ASYMMETRIC_PRIVATE",
+					"typeUrl":
+							"type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+					"value":
+							"EkwSBggDEAIYAhogEiSZ9u2nDtvZuDgWgGsVTIZ5/V08N4ycUspTX0RYRrkiIHpEwHxQd1bImkyMvV2bqtUbgMh5uPSTdnUEGrPXdt56GiEA3iUi+CRN71qy0fOCK66xAW/IvFyjOGtxjppRhSFUneo="
+			},
+			"keyId": 611814836,
+			"outputPrefixType": "TINK",
+			"status": "ENABLED"
+		}],
+		"primaryKeyId": 611814836
+	}`
 
-	// TODO: save the private keyset to a safe location. DO NOT hardcode it in source code.
+	// The corresponding public keyset created with
+	// "tinkey create-public-keyset --in private_keyset.cfg"
+	publicJSONKeyset := `{
+      "key": [{
+          "keyData": {
+              "keyMaterialType":
+                  "ASYMMETRIC_PUBLIC",
+              "typeUrl":
+                  "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+              "value":
+                  "EgYIAxACGAIaIBIkmfbtpw7b2bg4FoBrFUyGef1dPDeMnFLKU19EWEa5IiB6RMB8UHdWyJpMjL1dm6rVG4DIebj0k3Z1BBqz13beeg=="
+          },
+          "keyId": 611814836,
+          "outputPrefixType": "TINK",
+          "status": "ENABLED"
+      }],
+      "primaryKeyId": 611814836
+  }`
+
+	// Create a keyset handle from the cleartext private keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the access of the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use a insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
 	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
 	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
-
-	s, err := signature.NewSigner(kh)
+	privateKeysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(privateJSONKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	msg := []byte("this data needs to be signed")
-	sig, err := s.Sign(msg)
+	// Retrieve the Signer primitive from privateKeysetHandle.
+	signer, err := signature.NewSigner(privateKeysetHandle)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	pubkh, err := kh.Public()
+	// Use the primitive to sign a message. In this case, the primary key of the
+	// keyset will be used (which is also the only key in this example).
+	data := []byte("data")
+	sig, err := signer.Sign(data)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// TODO: share the public with the verifier.
-
-	v, err := signature.NewVerifier(pubkh)
+	// Create a keyset handle from the keyset containing the public key. Because the
+	// public keyset does not contain any secrets, we can use [keyset.ReadWithNoSecrets].
+	publicKeysetHandle, err := keyset.ReadWithNoSecrets(
+		keyset.NewJSONReader(bytes.NewBufferString(publicJSONKeyset)))
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	if err := v.Verify(sig, msg); err != nil {
+	// Retrieve the Verifier primitive from publicKeysetHandle.
+	verifier, err := signature.NewVerifier(publicKeysetHandle)
+	if err != nil {
 		log.Fatal(err)
 	}
 
-	fmt.Printf("Message: %s\n", msg)
-	fmt.Printf("Signature: %s\n", base64.StdEncoding.EncodeToString(sig))
+	if err = verifier.Verify(sig, data); err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("sig is valid")
+	// Output: sig is valid
 }
+
+// [END signature-example]
diff --git a/go/signature/signer_factory.go b/go/signature/signer_factory.go
index 2465541..c98f394 100644
--- a/go/signature/signer_factory.go
+++ b/go/signature/signer_factory.go
@@ -20,32 +20,27 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/tink"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 // NewSigner returns a Signer primitive from the given keyset handle.
-func NewSigner(h *keyset.Handle) (tink.Signer, error) {
-	return NewSignerWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewSignerWithKeyManager returns a Signer primitive from the given keyset handle and custom key manager.
-//
-// Deprecated: Use [NewSigner].
-func NewSignerWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.Signer, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func NewSigner(handle *keyset.Handle) (tink.Signer, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("public_key_sign_factory: cannot obtain primitive set: %s", err)
 	}
-
 	return newWrappedSigner(ps)
 }
 
 // wrappedSigner is an Signer implementation that uses the underlying primitive set for signing.
 type wrappedSigner struct {
-	ps *primitiveset.PrimitiveSet
+	ps     *primitiveset.PrimitiveSet
+	logger monitoring.Logger
 }
 
 // Asserts that wrappedSigner implements the Signer interface.
@@ -63,11 +58,30 @@
 			}
 		}
 	}
+	logger, err := createSignerLogger(ps)
+	if err != nil {
+		return nil, err
+	}
+	return &wrappedSigner{
+		ps:     ps,
+		logger: logger,
+	}, nil
+}
 
-	ret := new(wrappedSigner)
-	ret.ps = ps
-
-	return ret, nil
+func createSignerLogger(ps *primitiveset.PrimitiveSet) (monitoring.Logger, error) {
+	// only keysets which contain annotations are monitored.
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, nil
+	}
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, err
+	}
+	return internalregistry.GetMonitoringClient().NewLogger(&monitoring.Context{
+		KeysetInfo:  keysetInfo,
+		Primitive:   "public_key_sign",
+		APIFunction: "sign",
+	})
 }
 
 // Sign signs the given data and returns the signature concatenated with the identifier of the
@@ -81,6 +95,7 @@
 
 	var signedData []byte
 	if primary.PrefixType == tinkpb.OutputPrefixType_LEGACY {
+		signedData = make([]byte, 0, len(data)+1)
 		signedData = append(signedData, data...)
 		signedData = append(signedData, byte(0))
 	} else {
@@ -89,7 +104,15 @@
 
 	signature, err := signer.Sign(signedData)
 	if err != nil {
+		s.logger.LogFailure()
 		return nil, err
 	}
-	return append([]byte(primary.Prefix), signature...), nil
+	s.logger.Log(primary.KeyID, len(data))
+	if len(primary.Prefix) == 0 {
+		return signature, nil
+	}
+	output := make([]byte, 0, len(primary.Prefix)+len(signature))
+	output = append(output, primary.Prefix...)
+	output = append(output, signature...)
+	return output, nil
 }
diff --git a/go/signature/subtle/BUILD.bazel b/go/signature/subtle/BUILD.bazel
index 163f6e9..d22464b 100644
--- a/go/signature/subtle/BUILD.bazel
+++ b/go/signature/subtle/BUILD.bazel
@@ -14,10 +14,7 @@
         "subtle.go",
     ],
     importpath = "github.com/google/tink/go/signature/subtle",
-    deps = [
-        "//subtle",
-        "@org_golang_x_crypto//ed25519",
-    ],
+    deps = ["//subtle"],
 )
 
 go_test(
@@ -36,7 +33,6 @@
         "//subtle",
         "//subtle/random",
         "//testutil",
-        "@org_golang_x_crypto//ed25519",
     ],
 )
 
diff --git a/go/signature/subtle/ecdsa_signer.go b/go/signature/subtle/ecdsa_signer.go
index d656481..ec6fc49 100644
--- a/go/signature/subtle/ecdsa_signer.go
+++ b/go/signature/subtle/ecdsa_signer.go
@@ -36,10 +36,7 @@
 }
 
 // NewECDSASigner creates a new instance of ECDSASigner.
-func NewECDSASigner(hashAlg string,
-	curve string,
-	encoding string,
-	keyValue []byte) (*ECDSASigner, error) {
+func NewECDSASigner(hashAlg, curve, encoding string, keyValue []byte) (*ECDSASigner, error) {
 	privKey := new(ecdsa.PrivateKey)
 	c := subtle.GetCurve(curve)
 	if c == nil {
@@ -52,9 +49,7 @@
 }
 
 // NewECDSASignerFromPrivateKey creates a new instance of ECDSASigner
-func NewECDSASignerFromPrivateKey(hashAlg string,
-	encoding string,
-	privateKey *ecdsa.PrivateKey) (*ECDSASigner, error) {
+func NewECDSASignerFromPrivateKey(hashAlg, encoding string, privateKey *ecdsa.PrivateKey) (*ECDSASigner, error) {
 	if privateKey.Curve == nil {
 		return nil, errors.New("ecdsa_signer: privateKey.Curve can't be nil")
 	}
diff --git a/go/signature/subtle/ed25519_signer.go b/go/signature/subtle/ed25519_signer.go
index 5df6b63..921ce33 100644
--- a/go/signature/subtle/ed25519_signer.go
+++ b/go/signature/subtle/ed25519_signer.go
@@ -17,7 +17,7 @@
 package subtle
 
 import (
-	"golang.org/x/crypto/ed25519"
+	"crypto/ed25519"
 )
 
 // ED25519Signer is an implementation of Signer for ED25519.
diff --git a/go/signature/subtle/ed25519_signer_verifier_test.go b/go/signature/subtle/ed25519_signer_verifier_test.go
index ccbc325..6e67212 100644
--- a/go/signature/subtle/ed25519_signer_verifier_test.go
+++ b/go/signature/subtle/ed25519_signer_verifier_test.go
@@ -18,13 +18,12 @@
 
 import (
 	"bytes"
+	"crypto/ed25519"
 	"crypto/rand"
 	"encoding/hex"
 	"fmt"
 	"testing"
 
-	"golang.org/x/crypto/ed25519"
-
 	subtleSignature "github.com/google/tink/go/signature/subtle"
 	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
diff --git a/go/signature/subtle/ed25519_verifier.go b/go/signature/subtle/ed25519_verifier.go
index 1cad411..9e67276 100644
--- a/go/signature/subtle/ed25519_verifier.go
+++ b/go/signature/subtle/ed25519_verifier.go
@@ -17,10 +17,9 @@
 package subtle
 
 import (
+	"crypto/ed25519"
 	"errors"
 	"fmt"
-
-	"golang.org/x/crypto/ed25519"
 )
 
 var errInvalidED25519Signature = errors.New("ed25519: invalid signature")
diff --git a/go/signature/verifier_factory.go b/go/signature/verifier_factory.go
index c8a0e61..19f5fcf 100644
--- a/go/signature/verifier_factory.go
+++ b/go/signature/verifier_factory.go
@@ -22,22 +22,17 @@
 
 	"github.com/google/tink/go/core/cryptofmt"
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
+	"github.com/google/tink/go/internal/monitoringutil"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/monitoring"
 	"github.com/google/tink/go/tink"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
 // NewVerifier returns a Verifier primitive from the given keyset handle.
-func NewVerifier(h *keyset.Handle) (tink.Verifier, error) {
-	return NewVerifierWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewVerifierWithKeyManager returns a Verifier primitive from the given keyset handle and custom key manager.
-//
-// Deprecated: Use [NewVerifier].
-func NewVerifierWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.Verifier, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func NewVerifier(handle *keyset.Handle) (tink.Verifier, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("verifier_factory: cannot obtain primitive set: %s", err)
 	}
@@ -47,7 +42,8 @@
 // verifierSet is a Verifier implementation that uses the
 // underlying primitive set for verifying.
 type wrappedVerifier struct {
-	ps *primitiveset.PrimitiveSet
+	ps     *primitiveset.PrimitiveSet
+	logger monitoring.Logger
 }
 
 // Asserts that verifierSet implements the Verifier interface.
@@ -65,11 +61,30 @@
 			}
 		}
 	}
+	logger, err := createVerifierLogger(ps)
+	if err != nil {
+		return nil, err
+	}
+	return &wrappedVerifier{
+		ps:     ps,
+		logger: logger,
+	}, nil
+}
 
-	ret := new(wrappedVerifier)
-	ret.ps = ps
-
-	return ret, nil
+func createVerifierLogger(ps *primitiveset.PrimitiveSet) (monitoring.Logger, error) {
+	// only keysets which contain annotations are monitored.
+	if len(ps.Annotations) == 0 {
+		return &monitoringutil.DoNothingLogger{}, nil
+	}
+	keysetInfo, err := monitoringutil.KeysetInfoFromPrimitiveSet(ps)
+	if err != nil {
+		return nil, err
+	}
+	return internalregistry.GetMonitoringClient().NewLogger(&monitoring.Context{
+		KeysetInfo:  keysetInfo,
+		Primitive:   "public_key_verify",
+		APIFunction: "verify",
+	})
 }
 
 var errInvalidSignature = errors.New("verifier_factory: invalid signature")
@@ -89,7 +104,9 @@
 		for i := 0; i < len(entries); i++ {
 			var signedData []byte
 			if entries[i].PrefixType == tinkpb.OutputPrefixType_LEGACY {
-				signedData = append(data, byte(0))
+				signedData = make([]byte, 0, len(data)+1)
+				signedData = append(signedData, data...)
+				signedData = append(signedData, byte(0))
 			} else {
 				signedData = data
 			}
@@ -100,6 +117,7 @@
 			}
 
 			if err = verifier.Verify(signatureNoPrefix, signedData); err == nil {
+				v.logger.Log(entries[i].KeyID, len(signedData))
 				return nil
 			}
 		}
@@ -115,10 +133,11 @@
 			}
 
 			if err = verifier.Verify(signature, data); err == nil {
+				v.logger.Log(entries[i].KeyID, len(data))
 				return nil
 			}
 		}
 	}
-
+	v.logger.LogFailure()
 	return errInvalidSignature
 }
diff --git a/go/streamingaead/BUILD.bazel b/go/streamingaead/BUILD.bazel
index c7147ec..e4ac280 100644
--- a/go/streamingaead/BUILD.bazel
+++ b/go/streamingaead/BUILD.bazel
@@ -20,6 +20,8 @@
         "//aead/subtle",
         "//core/primitiveset",
         "//core/registry",
+        "//internal/internalregistry",
+        "//internal/tinkerror",
         "//keyset",
         "//mac/subtle",
         "//proto/aes_ctr_hmac_streaming_go_proto",
@@ -47,6 +49,8 @@
     embed = [":streamingaead"],
     deps = [
         "//core/registry",
+        "//insecurecleartextkeyset",
+        "//internal/internalregistry",
         "//keyset",
         "//mac",
         "//proto/aes_ctr_hmac_streaming_go_proto",
@@ -58,6 +62,7 @@
         "//testkeyset",
         "//testutil",
         "//tink",
+        "@com_github_google_go_cmp//cmp",
         "@org_golang_google_protobuf//proto",
     ],
 )
diff --git a/go/streamingaead/aes_ctr_hmac_key_manager.go b/go/streamingaead/aes_ctr_hmac_key_manager.go
index 612821d..8c336bb 100644
--- a/go/streamingaead/aes_ctr_hmac_key_manager.go
+++ b/go/streamingaead/aes_ctr_hmac_key_manager.go
@@ -156,11 +156,11 @@
 	if err := subtleaead.ValidateAESKeySize(params.DerivedKeySize); err != nil {
 		return err
 	}
-	if params.HkdfHashType == commonpb.HashType_UNKNOWN_HASH {
-		return errors.New("unknown HKDF hash type")
+	if params.HkdfHashType != commonpb.HashType_SHA1 && params.HkdfHashType != commonpb.HashType_SHA256 && params.HkdfHashType != commonpb.HashType_SHA512 {
+		return fmt.Errorf("Invalid HKDF hash type (%s)", params.HkdfHashType)
 	}
-	if params.HmacParams.Hash == commonpb.HashType_UNKNOWN_HASH {
-		return errors.New("uknown tag algorithm")
+	if params.HmacParams.Hash != commonpb.HashType_SHA1 && params.HmacParams.Hash != commonpb.HashType_SHA256 && params.HmacParams.Hash != commonpb.HashType_SHA512 {
+		return fmt.Errorf("Invalid tag algorithm (%s)", params.HmacParams.Hash)
 	}
 	hmacHash := commonpb.HashType_name[int32(params.HmacParams.Hash)]
 	if err := subtlemac.ValidateHMACParams(hmacHash, subtle.AESCTRHMACKeySizeInBytes, params.HmacParams.TagSize); err != nil {
@@ -170,5 +170,8 @@
 	if params.CiphertextSegmentSize < minSegmentSize {
 		return fmt.Errorf("ciphertext segment size must be at least (derivedKeySize + noncePrefixInBytes + tagSizeInBytes + 2)")
 	}
+	if params.CiphertextSegmentSize > 0x7fffffff {
+		return fmt.Errorf("ciphertext segment size must be at most 2^31 - 1")
+	}
 	return nil
 }
diff --git a/go/streamingaead/aes_ctr_hmac_key_manager_test.go b/go/streamingaead/aes_ctr_hmac_key_manager_test.go
index f4a3248..3fb8b6a 100644
--- a/go/streamingaead/aes_ctr_hmac_key_manager_test.go
+++ b/go/streamingaead/aes_ctr_hmac_key_manager_test.go
@@ -246,6 +246,17 @@
 
 		// bad version
 		testutil.NewAESCTRHMACKey(testutil.AESCTRHMACKeyVersion+1, 16, commonpb.HashType_SHA256, 16, commonpb.HashType_SHA256, 16, 4096),
+
+		// bad ciphertext_segment_size
+		testutil.NewAESCTRHMACKey(testutil.AESCTRHMACKeyVersion, 16, commonpb.HashType_SHA256, 16, commonpb.HashType_SHA256, 16, 2147483648),
+
+		// bad hmac params hash type
+		testutil.NewAESCTRHMACKey(testutil.AESCTRHMACKeyVersion, 16, commonpb.HashType_SHA256, 16, commonpb.HashType_SHA224, 16, 4096),
+		testutil.NewAESCTRHMACKey(testutil.AESCTRHMACKeyVersion, 16, commonpb.HashType_SHA256, 16, commonpb.HashType_SHA384, 16, 4096),
+
+		// bad hkdf hash type
+		testutil.NewAESCTRHMACKey(testutil.AESCTRHMACKeyVersion, 16, commonpb.HashType_SHA224, 16, commonpb.HashType_SHA256, 16, 4096),
+		testutil.NewAESCTRHMACKey(testutil.AESCTRHMACKeyVersion, 16, commonpb.HashType_SHA384, 16, commonpb.HashType_SHA256, 16, 4096),
 	}
 }
 
diff --git a/go/streamingaead/aes_gcm_hkdf_key_manager.go b/go/streamingaead/aes_gcm_hkdf_key_manager.go
index b253c17..28eaede 100644
--- a/go/streamingaead/aes_gcm_hkdf_key_manager.go
+++ b/go/streamingaead/aes_gcm_hkdf_key_manager.go
@@ -19,6 +19,7 @@
 import (
 	"errors"
 	"fmt"
+	"io"
 
 	"google.golang.org/protobuf/proto"
 	subtleaead "github.com/google/tink/go/aead/subtle"
@@ -103,7 +104,7 @@
 	return &tinkpb.KeyData{
 		TypeUrl:         km.TypeURL(),
 		Value:           serializedKey,
-		KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+		KeyMaterialType: km.KeyMaterialType(),
 	}, nil
 }
 
@@ -117,10 +118,41 @@
 	return aesGCMHKDFTypeURL
 }
 
+// KeyMaterialType returns the key material type of this key manager.
+func (km *aesGCMHKDFKeyManager) KeyMaterialType() tinkpb.KeyData_KeyMaterialType {
+	return tinkpb.KeyData_SYMMETRIC
+}
+
+// DeriveKey derives a new key from serializedKeyFormat and pseudorandomness.
+func (km *aesGCMHKDFKeyManager) DeriveKey(serializedKeyFormat []byte, pseudorandomness io.Reader) (proto.Message, error) {
+	if len(serializedKeyFormat) == 0 {
+		return nil, errInvalidAESGCMHKDFKeyFormat
+	}
+	keyFormat := &ghpb.AesGcmHkdfStreamingKeyFormat{}
+	if err := proto.Unmarshal(serializedKeyFormat, keyFormat); err != nil {
+		return nil, errInvalidAESGCMHKDFKeyFormat
+	}
+	if err := km.validateKeyFormat(keyFormat); err != nil {
+		return nil, fmt.Errorf("aes_gcm_hkdf_key_manager: invalid key format: %v", err)
+	}
+	if err := keyset.ValidateKeyVersion(keyFormat.GetVersion(), aesGCMHKDFKeyVersion); err != nil {
+		return nil, fmt.Errorf("aes_gcm_hkdf_key_manager: invalid key version: %s", err)
+	}
+
+	keyValue := make([]byte, keyFormat.GetKeySize())
+	if _, err := io.ReadFull(pseudorandomness, keyValue); err != nil {
+		return nil, fmt.Errorf("aes_gcm_hkdf_key_manager: not enough pseudorandomness given")
+	}
+	return &ghpb.AesGcmHkdfStreamingKey{
+		Version:  aesGCMHKDFKeyVersion,
+		KeyValue: keyValue,
+		Params:   keyFormat.Params,
+	}, nil
+}
+
 // validateKey validates the given AESGCMHKDFKey.
 func (km *aesGCMHKDFKeyManager) validateKey(key *ghpb.AesGcmHkdfStreamingKey) error {
-	err := keyset.ValidateKeyVersion(key.Version, aesGCMHKDFKeyVersion)
-	if err != nil {
+	if err := keyset.ValidateKeyVersion(key.Version, aesGCMHKDFKeyVersion); err != nil {
 		return fmt.Errorf("aes_gcm_hkdf_key_manager: %s", err)
 	}
 	keySize := uint32(len(key.KeyValue))
@@ -149,9 +181,12 @@
 	if err := subtleaead.ValidateAESKeySize(params.DerivedKeySize); err != nil {
 		return fmt.Errorf("aes_gcm_hkdf_key_manager: %s", err)
 	}
-	if params.HkdfHashType == commonpb.HashType_UNKNOWN_HASH {
+	if params.HkdfHashType != commonpb.HashType_SHA1 && params.HkdfHashType != commonpb.HashType_SHA256 && params.HkdfHashType != commonpb.HashType_SHA512 {
 		return errors.New("unknown HKDF hash type")
 	}
+	if params.CiphertextSegmentSize > 0x7fffffff {
+		return errors.New("CiphertextSegmentSize must be at most 2^31 - 1")
+	}
 	minSegmentSize := params.DerivedKeySize + subtle.AESGCMHKDFNoncePrefixSizeInBytes + subtle.AESGCMHKDFTagSizeInBytes + 2
 	if params.CiphertextSegmentSize < minSegmentSize {
 		return fmt.Errorf("ciphertext segment_size must be at least (derivedKeySize + noncePrefixInBytes + tagSizeInBytes + 2)")
diff --git a/go/streamingaead/aes_gcm_hkdf_key_manager_test.go b/go/streamingaead/aes_gcm_hkdf_key_manager_test.go
index 06c3170..477c11a 100644
--- a/go/streamingaead/aes_gcm_hkdf_key_manager_test.go
+++ b/go/streamingaead/aes_gcm_hkdf_key_manager_test.go
@@ -18,12 +18,16 @@
 
 import (
 	"bytes"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 	"github.com/google/tink/go/streamingaead/subtle"
+	"github.com/google/tink/go/subtle/random"
 	"github.com/google/tink/go/testutil"
 	gcmhkdfpb "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
@@ -244,6 +248,260 @@
 	}
 }
 
+func TestAESGCMHKDFKeyMaterialType(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMHKDFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMHKDFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	if got, want := keyManager.KeyMaterialType(), tinkpb.KeyData_SYMMETRIC; got != want {
+		t.Errorf("KeyMaterialType() = %v, want %v", got, want)
+	}
+}
+
+func TestAESGCMHKDFDeriveKey(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMHKDFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMHKDFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	for _, keySize := range []uint32{16, 32} {
+		for _, derivedKeySize := range []uint32{16, 32} {
+			keyFormat := &gcmhkdfpb.AesGcmHkdfStreamingKeyFormat{
+				Version: testutil.AESGCMHKDFKeyVersion,
+				Params: &gcmhkdfpb.AesGcmHkdfStreamingParams{
+					CiphertextSegmentSize: derivedKeySize + subtle.AESGCMHKDFNoncePrefixSizeInBytes + subtle.AESGCMHKDFTagSizeInBytes + 2,
+					DerivedKeySize:        derivedKeySize,
+					HkdfHashType:          commonpb.HashType_SHA256,
+				},
+				KeySize: keySize,
+			}
+			serializedKeyFormat, err := proto.Marshal(keyFormat)
+			if err != nil {
+				t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+			}
+			rand := random.GetRandomBytes(keySize)
+			buf := &bytes.Buffer{}
+			buf.Write(rand) // never returns a non-nil error
+			k, err := keyManager.DeriveKey(serializedKeyFormat, buf)
+			if err != nil {
+				t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+			}
+			key := k.(*gcmhkdfpb.AesGcmHkdfStreamingKey)
+			if got, want := len(key.GetKeyValue()), int(keySize); got != want {
+				t.Errorf("key length = %d, want %d", got, want)
+			}
+			if diff := cmp.Diff(key.GetKeyValue(), rand); diff != "" {
+				t.Errorf("incorrect derived key: diff = %v", diff)
+			}
+		}
+	}
+}
+
+func TestAESGCMHKDFDeriveKeyFailsWithInvalidKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMHKDFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMHKDFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+
+	validKeyFormat := &gcmhkdfpb.AesGcmHkdfStreamingKeyFormat{
+		Version: testutil.AESGCMHKDFKeyVersion,
+		Params: &gcmhkdfpb.AesGcmHkdfStreamingParams{
+			CiphertextSegmentSize: 16 + subtle.AESGCMHKDFNoncePrefixSizeInBytes + subtle.AESGCMHKDFTagSizeInBytes + 2,
+			DerivedKeySize:        16,
+			HkdfHashType:          commonpb.HashType_SHA256,
+		},
+		KeySize: 16,
+	}
+	serializedValidKeyFormat, err := proto.Marshal(validKeyFormat)
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", validKeyFormat, err)
+	}
+	buf := bytes.NewBuffer(random.GetRandomBytes(validKeyFormat.KeySize))
+	if _, err := keyManager.DeriveKey(serializedValidKeyFormat, buf); err != nil {
+		t.Fatalf("keyManager.DeriveKey() err = %v, want nil", err)
+	}
+
+	for _, test := range []struct {
+		name                  string
+		version               uint32
+		keySize               uint32
+		ciphertextSegmentSize uint32
+		derivedKeySize        uint32
+		hkdfHashType          commonpb.HashType
+	}{
+		{
+			name:                  "invalid version",
+			version:               10,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: validKeyFormat.Params.CiphertextSegmentSize,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          validKeyFormat.Params.HkdfHashType,
+		},
+		{
+			name:                  "invalid key size",
+			version:               validKeyFormat.Version,
+			keySize:               10,
+			ciphertextSegmentSize: validKeyFormat.Params.CiphertextSegmentSize,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          validKeyFormat.Params.HkdfHashType,
+		},
+		{
+			name:                  "invalid ciphertext segment size",
+			version:               validKeyFormat.Version,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: 10,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          validKeyFormat.Params.HkdfHashType,
+		},
+		{
+			name:                  "invalid ciphertext segment size",
+			version:               validKeyFormat.Version,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: 2147483648,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          validKeyFormat.Params.HkdfHashType,
+		},
+		{
+			name:                  "invalid derived key size",
+			version:               validKeyFormat.Version,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: validKeyFormat.Params.CiphertextSegmentSize,
+			derivedKeySize:        10,
+			hkdfHashType:          validKeyFormat.Params.HkdfHashType,
+		},
+		{
+			name:                  "invalid HKDF hash type",
+			version:               validKeyFormat.Version,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: validKeyFormat.Params.CiphertextSegmentSize,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          commonpb.HashType_UNKNOWN_HASH,
+		},
+		{
+			name:                  "invalid HKDF hash type",
+			version:               validKeyFormat.Version,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: validKeyFormat.Params.CiphertextSegmentSize,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          commonpb.HashType_SHA224,
+		},
+		{
+			name:                  "invalid HKDF hash type",
+			version:               validKeyFormat.Version,
+			keySize:               validKeyFormat.KeySize,
+			ciphertextSegmentSize: validKeyFormat.Params.CiphertextSegmentSize,
+			derivedKeySize:        validKeyFormat.Params.DerivedKeySize,
+			hkdfHashType:          commonpb.HashType_SHA384,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			keyFormat, err := proto.Marshal(&gcmhkdfpb.AesGcmHkdfStreamingKeyFormat{
+				Version: test.version,
+				KeySize: test.keySize,
+				Params: &gcmhkdfpb.AesGcmHkdfStreamingParams{
+					CiphertextSegmentSize: test.ciphertextSegmentSize,
+					DerivedKeySize:        test.derivedKeySize,
+					HkdfHashType:          test.hkdfHashType,
+				},
+			})
+			if err != nil {
+				t.Fatalf("proto.Marshal() err = %v, want nil", err)
+			}
+			buf := bytes.NewBuffer(random.GetRandomBytes(test.keySize))
+			if _, err := keyManager.DeriveKey(keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestAESGCMHKDFDeriveKeyFailsWithMalformedKeyFormats(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMHKDFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMHKDFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	// Proto messages start with a VarInt, which always ends with a byte with the
+	// MSB unset, so 0x80 is invalid.
+	invalidSerialization, err := hex.DecodeString("80")
+	if err != nil {
+		t.Errorf("hex.DecodeString() err = %v, want nil", err)
+	}
+	for _, test := range []struct {
+		name      string
+		keyFormat []byte
+	}{
+		{
+			name:      "nil",
+			keyFormat: nil,
+		},
+		{
+			name:      "empty",
+			keyFormat: []byte{},
+		},
+		{
+			name:      "invalid serialization",
+			keyFormat: invalidSerialization,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(random.GetRandomBytes(32))
+			if _, err := keyManager.DeriveKey(test.keyFormat, buf); err == nil {
+				t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+			}
+		})
+	}
+}
+
+func TestAESGCMHKDFDeriveKeyFailsWithInsufficientRandomness(t *testing.T) {
+	km, err := registry.GetKeyManager(testutil.AESGCMHKDFTypeURL)
+	if err != nil {
+		t.Fatalf("registry.GetKeyManager(%q) err = %v, want nil", testutil.AESGCMHKDFTypeURL, err)
+	}
+	keyManager, ok := km.(internalregistry.DerivableKeyManager)
+	if !ok {
+		t.Fatalf("key manager is not DerivableKeyManager")
+	}
+	keyFormat, err := proto.Marshal(&gcmhkdfpb.AesGcmHkdfStreamingKeyFormat{
+		Version: testutil.AESGCMHKDFKeyVersion,
+		Params: &gcmhkdfpb.AesGcmHkdfStreamingParams{
+			CiphertextSegmentSize: 16 + subtle.AESGCMHKDFNoncePrefixSizeInBytes + subtle.AESGCMHKDFTagSizeInBytes + 2,
+			DerivedKeySize:        16,
+			HkdfHashType:          commonpb.HashType_SHA256,
+		},
+		KeySize: 16,
+	})
+	if err != nil {
+		t.Fatalf("proto.Marshal(%v) err = %v, want nil", keyFormat, err)
+	}
+	{
+		buf := bytes.NewBuffer(random.GetRandomBytes(16))
+		if _, err := keyManager.DeriveKey(keyFormat, buf); err != nil {
+			t.Errorf("keyManager.DeriveKey() err = %v, want nil", err)
+		}
+	}
+	{
+		insufficientBuf := bytes.NewBuffer(random.GetRandomBytes(15))
+		if _, err := keyManager.DeriveKey(keyFormat, insufficientBuf); err == nil {
+			t.Errorf("keyManager.DeriveKey() err = nil, want non-nil")
+		}
+	}
+}
+
 func genInvalidAESGCMHKDFKeys() []proto.Message {
 	return []proto.Message{
 		// not a AESGCMHKDFKey
diff --git a/go/streamingaead/decrypt_reader.go b/go/streamingaead/decrypt_reader.go
index 4bb4418..319c3e1 100644
--- a/go/streamingaead/decrypt_reader.go
+++ b/go/streamingaead/decrypt_reader.go
@@ -20,6 +20,7 @@
 	"errors"
 	"io"
 
+	"github.com/google/tink/go/core/primitiveset"
 	"github.com/google/tink/go/tink"
 )
 
@@ -48,7 +49,12 @@
 		return 0, errKeyNotFound
 	}
 
-	entries, err := dr.wrapped.ps.RawEntries()
+	// For legacy reasons (Tink always encrypted with non-RAW keys) we use all
+	// primitives, even those which have output_prefix_type != RAW.
+	var allEntries []*primitiveset.Entry
+	for _, entryList := range dr.wrapped.ps.Entries {
+		allEntries = append(allEntries, entryList...)
+	}
 	if err != nil {
 		return 0, err
 	}
@@ -57,7 +63,7 @@
 	ur := &unreader{r: dr.cr}
 
 	// find proper key to decrypt ciphertext
-	for _, e := range entries {
+	for _, e := range allEntries {
 		sa, ok := e.Primitive.(tink.StreamingAEAD)
 		if !ok {
 			continue
diff --git a/go/streamingaead/streamingaead.go b/go/streamingaead/streamingaead.go
index 68d3a7f..acdf4cc 100644
--- a/go/streamingaead/streamingaead.go
+++ b/go/streamingaead/streamingaead.go
@@ -24,12 +24,16 @@
 	"fmt"
 
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/internal/internalregistry"
 )
 
 func init() {
 	if err := registry.RegisterKeyManager(new(aesGCMHKDFKeyManager)); err != nil {
 		panic(fmt.Sprintf("streamingaead.init() failed: %v", err))
 	}
+	if err := internalregistry.AllowKeyDerivation(aesGCMHKDFTypeURL); err != nil {
+		panic(fmt.Sprintf("streamingaead.init() failed: %v", err))
+	}
 
 	if err := registry.RegisterKeyManager(new(aesCTRHMACKeyManager)); err != nil {
 		panic(fmt.Sprintf("streamingaead.init() failed: %v", err))
diff --git a/go/streamingaead/streamingaead_factory.go b/go/streamingaead/streamingaead_factory.go
index 9528581..6cb1bd0 100644
--- a/go/streamingaead/streamingaead_factory.go
+++ b/go/streamingaead/streamingaead_factory.go
@@ -21,21 +21,13 @@
 	"io"
 
 	"github.com/google/tink/go/core/primitiveset"
-	"github.com/google/tink/go/core/registry"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/tink"
 )
 
 // New returns a StreamingAEAD primitive from the given keyset handle.
-func New(h *keyset.Handle) (tink.StreamingAEAD, error) {
-	return NewWithKeyManager(h, nil /*keyManager*/)
-}
-
-// NewWithKeyManager returns a StreamingAEAD primitive from the given keyset handle and custom key manager.
-//
-// Deprecated: Use [New].
-func NewWithKeyManager(h *keyset.Handle, km registry.KeyManager) (tink.StreamingAEAD, error) {
-	ps, err := h.PrimitivesWithKeyManager(km)
+func New(handle *keyset.Handle) (tink.StreamingAEAD, error) {
+	ps, err := handle.Primitives()
 	if err != nil {
 		return nil, fmt.Errorf("streamingaead_factory: cannot obtain primitive set: %s", err)
 	}
diff --git a/go/streamingaead/streamingaead_factory_test.go b/go/streamingaead/streamingaead_factory_test.go
index 8c5f812..f340f25 100644
--- a/go/streamingaead/streamingaead_factory_test.go
+++ b/go/streamingaead/streamingaead_factory_test.go
@@ -24,6 +24,7 @@
 	"strings"
 	"testing"
 
+	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
 	"github.com/google/tink/go/streamingaead"
@@ -31,9 +32,15 @@
 	"github.com/google/tink/go/testkeyset"
 	"github.com/google/tink/go/testutil"
 	"github.com/google/tink/go/tink"
+	ghpb "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto"
+	commonpb "github.com/google/tink/go/proto/common_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
 
+const (
+	aesGCMHKDFTypeURL = "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey"
+)
+
 func TestFactoryMultipleKeys(t *testing.T) {
 	keyset := testutil.NewTestAESGCMHKDFKeyset()
 
@@ -126,7 +133,6 @@
 	return nil
 }
 
-
 func TestFactoryWithInvalidPrimitiveSetType(t *testing.T) {
 	wrongKH, err := keyset.NewHandle(mac.HMACSHA256Tag128KeyTemplate())
 	if err != nil {
@@ -150,3 +156,60 @@
 		t.Fatalf("New() failed with good *keyset.Handle: %s", err)
 	}
 }
+
+func TestFactoryWithKeysetWithTinkKeys(t *testing.T) {
+	key := &ghpb.AesGcmHkdfStreamingKey{
+		Version:  0,
+		KeyValue: []byte("0123456789abcdef"),
+		Params: &ghpb.AesGcmHkdfStreamingParams{
+			CiphertextSegmentSize: 512,
+			DerivedKeySize:        16,
+			HkdfHashType:          commonpb.HashType_SHA1,
+		},
+	}
+	value1, err := proto.Marshal(key)
+	if err != nil {
+		t.Fatalf("proto.Marshal(key) err = %q, want nil", err)
+	}
+	key.KeyValue = []byte("ABCDEF0123456789")
+	value2, err := proto.Marshal(key)
+	if err != nil {
+		t.Fatalf("proto.Marshal(key) err = %q, want nil", err)
+	}
+
+	keyset := &tinkpb.Keyset{
+		PrimaryKeyId: 1,
+		Key: []*tinkpb.Keyset_Key{{
+			KeyData: &tinkpb.KeyData{
+				TypeUrl:         aesGCMHKDFTypeURL,
+				Value:           value1,
+				KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+			},
+			OutputPrefixType: tinkpb.OutputPrefixType_TINK,
+			KeyId:            1,
+			Status:           tinkpb.KeyStatusType_ENABLED,
+		}, &tinkpb.Keyset_Key{
+			KeyData: &tinkpb.KeyData{
+				TypeUrl:         aesGCMHKDFTypeURL,
+				Value:           value2,
+				KeyMaterialType: tinkpb.KeyData_SYMMETRIC,
+			},
+			OutputPrefixType: tinkpb.OutputPrefixType_RAW,
+			KeyId:            2,
+			Status:           tinkpb.KeyStatusType_ENABLED,
+		}},
+	}
+
+	keysetHandle, err := testkeyset.NewHandle(keyset)
+	if err != nil {
+		t.Fatalf("testkeyset.NewHandle(keyset) err = %q, want nil", err)
+	}
+	a, err := streamingaead.New(keysetHandle)
+	if err != nil {
+		t.Errorf("streamingaead.New(keysetHandle) err = %q, want nil", err)
+	}
+
+	if err := validateFactoryCipher(a, a); err != nil {
+		t.Errorf("Encryption & Decryption with TINK key should succeed")
+	}
+}
diff --git a/go/streamingaead/streamingaead_key_templates.go b/go/streamingaead/streamingaead_key_templates.go
index 0c0f8d9..7494c62 100644
--- a/go/streamingaead/streamingaead_key_templates.go
+++ b/go/streamingaead/streamingaead_key_templates.go
@@ -20,6 +20,7 @@
 	"fmt"
 
 	"google.golang.org/protobuf/proto"
+	"github.com/google/tink/go/internal/tinkerror"
 	ctrhmacpb "github.com/google/tink/go/proto/aes_ctr_hmac_streaming_go_proto"
 	gcmhkdfpb "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto"
 	commonpb "github.com/google/tink/go/proto/common_go_proto"
@@ -68,60 +69,55 @@
 
 // AES128CTRHMACSHA256Segment4KBKeyTemplate is a KeyTemplate that generates an
 // AES-CTR-HMAC key with the following parameters:
-//		- Main key size: 16 bytes
-//		- HKDF algorthim: HMAC-SHA256
-//		- AES-CTR derived key size: 16 bytes
-//		- Tag algorithm: HMAC-SHA256
-//		- Tag size: 32 bytes
-//		- Ciphertext segment size: 4096 bytes (4 KB)
+//   - Main key size: 16 bytes
+//   - HKDF algorthim: HMAC-SHA256
+//   - AES-CTR derived key size: 16 bytes
+//   - Tag algorithm: HMAC-SHA256
+//   - Tag size: 32 bytes
+//   - Ciphertext segment size: 4096 bytes (4 KB)
 func AES128CTRHMACSHA256Segment4KBKeyTemplate() *tinkpb.KeyTemplate {
 	return newAESCTRHMACKeyTemplate(16, commonpb.HashType_SHA256, 16, commonpb.HashType_SHA256, 32, 4096)
 }
 
 // AES128CTRHMACSHA256Segment1MBKeyTemplate is a KeyTemplate that generates an
 // AES-CTR-HMAC key with the following parameters:
-//		- Main key size: 16 bytes
-//		- HKDF algorthim: HMAC-SHA256
-//		- AES-CTR derived key size: 16 bytes
-//		- Tag algorithm: HMAC-SHA256
-//		- Tag size: 32 bytes
-//		- Ciphertext segment size: 1048576 bytes (1 MB)
+//   - Main key size: 16 bytes
+//   - HKDF algorthim: HMAC-SHA256
+//   - AES-CTR derived key size: 16 bytes
+//   - Tag algorithm: HMAC-SHA256
+//   - Tag size: 32 bytes
+//   - Ciphertext segment size: 1048576 bytes (1 MB)
 func AES128CTRHMACSHA256Segment1MBKeyTemplate() *tinkpb.KeyTemplate {
 	return newAESCTRHMACKeyTemplate(16, commonpb.HashType_SHA256, 16, commonpb.HashType_SHA256, 32, 1048576)
 }
 
 // AES256CTRHMACSHA256Segment4KBKeyTemplate is a KeyTemplate that generates an
 // AES-CTR-HMAC key with the following parameters:
-//		- Main key size: 32 bytes
-//		- HKDF algorthim: HMAC-SHA256
-//		- AES-CTR derived key size: 32 bytes
-//		- Tag algorithm: HMAC-SHA256
-//		- Tag size: 32 bytes
-//		- Ciphertext segment size: 4096 bytes (4 KB)
+//   - Main key size: 32 bytes
+//   - HKDF algorthim: HMAC-SHA256
+//   - AES-CTR derived key size: 32 bytes
+//   - Tag algorithm: HMAC-SHA256
+//   - Tag size: 32 bytes
+//   - Ciphertext segment size: 4096 bytes (4 KB)
 func AES256CTRHMACSHA256Segment4KBKeyTemplate() *tinkpb.KeyTemplate {
 	return newAESCTRHMACKeyTemplate(32, commonpb.HashType_SHA256, 32, commonpb.HashType_SHA256, 32, 4096)
 }
 
 // AES256CTRHMACSHA256Segment1MBKeyTemplate is a KeyTemplate that generates an
 // AES-CTR-HMAC key with the following parameters:
-//		- Main key size: 32 bytes
-//		- HKDF algorthim: HMAC-SHA256
-//		- AES-CTR derived key size: 32 bytes
-//		- Tag algorithm: HMAC-SHA256
-//		- Tag size: 32 bytes
-//		- Ciphertext segment size: 1048576 bytes (1 MB)
+//   - Main key size: 32 bytes
+//   - HKDF algorthim: HMAC-SHA256
+//   - AES-CTR derived key size: 32 bytes
+//   - Tag algorithm: HMAC-SHA256
+//   - Tag size: 32 bytes
+//   - Ciphertext segment size: 1048576 bytes (1 MB)
 func AES256CTRHMACSHA256Segment1MBKeyTemplate() *tinkpb.KeyTemplate {
 	return newAESCTRHMACKeyTemplate(32, commonpb.HashType_SHA256, 32, commonpb.HashType_SHA256, 32, 1048576)
 }
 
 // newAESGCMHKDFKeyTemplate creates a KeyTemplate containing a AesGcmHkdfStreamingKeyFormat with
 // specified parameters.
-func newAESGCMHKDFKeyTemplate(
-	mainKeySize uint32,
-	hkdfHashType commonpb.HashType,
-	derivedKeySize uint32,
-	ciphertextSegmentSize uint32,
-) *tinkpb.KeyTemplate {
+func newAESGCMHKDFKeyTemplate(mainKeySize uint32, hkdfHashType commonpb.HashType, derivedKeySize, ciphertextSegmentSize uint32) *tinkpb.KeyTemplate {
 	serializedFormat, err := proto.Marshal(&gcmhkdfpb.AesGcmHkdfStreamingKeyFormat{
 		KeySize: mainKeySize,
 		Params: &gcmhkdfpb.AesGcmHkdfStreamingParams{
@@ -131,7 +127,7 @@
 		},
 	})
 	if err != nil {
-		panic(fmt.Sprintf("failed to marshal key: %s", err))
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key: %s", err))
 	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          aesGCMHKDFTypeURL,
@@ -142,14 +138,7 @@
 
 // newAESCTRHMACKeyTemplate creates a KeyTemplate containing a
 // AesCtrHmacStreamingKeyFormat with the specified parameters.
-func newAESCTRHMACKeyTemplate(
-	mainKeySize uint32,
-	hkdfHashType commonpb.HashType,
-	derivedKeySize uint32,
-	tagAlg commonpb.HashType,
-	tagSize uint32,
-	ciphertextSegmentSize uint32,
-) *tinkpb.KeyTemplate {
+func newAESCTRHMACKeyTemplate(mainKeySize uint32, hkdfHashType commonpb.HashType, derivedKeySize uint32, tagAlg commonpb.HashType, tagSize, ciphertextSegmentSize uint32) *tinkpb.KeyTemplate {
 	serializedFormat, err := proto.Marshal(&ctrhmacpb.AesCtrHmacStreamingKeyFormat{
 		KeySize: mainKeySize,
 		Params: &ctrhmacpb.AesCtrHmacStreamingParams{
@@ -163,7 +152,7 @@
 		},
 	})
 	if err != nil {
-		panic(fmt.Sprintf("failed to marshal key: %s", err))
+		tinkerror.Fail(fmt.Sprintf("failed to marshal key: %s", err))
 	}
 	return &tinkpb.KeyTemplate{
 		TypeUrl:          aesCTRHMACTypeURL,
diff --git a/go/streamingaead/streamingaead_key_templates_test.go b/go/streamingaead/streamingaead_key_templates_test.go
index 226448e..0d79cbd 100644
--- a/go/streamingaead/streamingaead_key_templates_test.go
+++ b/go/streamingaead/streamingaead_key_templates_test.go
@@ -18,7 +18,7 @@
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	"testing"
 
 	"github.com/google/tink/go/keyset"
@@ -91,9 +91,9 @@
 			if err != nil {
 				t.Fatalf("primitive.NewDecryptingReader(buf, aad) failed: %v", err)
 			}
-			decrypted, err := ioutil.ReadAll(r)
+			decrypted, err := io.ReadAll(r)
 			if err != nil {
-				t.Fatalf("ioutil.ReadAll(r) failed: %v", err)
+				t.Fatalf("io.ReadAll(r) failed: %v", err)
 			}
 			if !bytes.Equal(decrypted, plaintext) {
 				t.Errorf("decrypted data doesn't match plaintext, got: %q, want: ''", decrypted)
diff --git a/go/streamingaead/streamingaead_test.go b/go/streamingaead/streamingaead_test.go
index 477c554..d02488b 100644
--- a/go/streamingaead/streamingaead_test.go
+++ b/go/streamingaead/streamingaead_test.go
@@ -16,116 +16,133 @@
 
 package streamingaead_test
 
+// [START streaming-aead-example]
+
 import (
+	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"log"
 	"os"
 	"path/filepath"
 
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/streamingaead"
 )
 
 func Example() {
-	dir, err := ioutil.TempDir("", "streamingaead")
+	// A keyset created with "tinkey create-keyset --key-template=AES256_CTR_HMAC_SHA256_1MB". Note
+	// that this keyset has the secret key information in cleartext.
+	jsonKeyset := `{
+    "primaryKeyId": 1720777699,
+    "key": [{
+        "keyData": {
+            "typeUrl": "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            "keyMaterialType": "SYMMETRIC",
+            "value": "Eg0IgCAQIBgDIgQIAxAgGiDtesd/4gCnQdTrh+AXodwpm2b6BFJkp043n+8mqx0YGw=="
+        },
+        "outputPrefixType": "RAW",
+        "keyId": 1720777699,
+        "status": "ENABLED"
+    }]
+	}`
+
+	// Create a keyset handle from the cleartext keyset in the previous
+	// step. The keyset handle provides abstract access to the underlying keyset to
+	// limit the exposure of accessing the raw key material. WARNING: In practice,
+	// it is unlikely you will want to use an insecurecleartextkeyset, as it implies
+	// that your key material is passed in cleartext, which is a security risk.
+	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
+	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
+	keysetHandle, err := insecurecleartextkeyset.Read(
+		keyset.NewJSONReader(bytes.NewBufferString(jsonKeyset)))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Retrieve the StreamingAEAD primitive we want to use from the keyset handle.
+	primitive, err := streamingaead.New(keysetHandle)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Create a file with the plaintext.
+	dir, err := os.MkdirTemp("", "streamingaead")
 	if err != nil {
 		log.Fatal(err)
 	}
 	defer os.RemoveAll(dir)
-
-	var (
-		srcFilename = filepath.Join(dir, "plaintext.src")
-		ctFilename  = filepath.Join(dir, "ciphertext.bin")
-		dstFilename = filepath.Join(dir, "plaintext.dst")
-	)
-
-	if err := ioutil.WriteFile(srcFilename, []byte("this data needs to be encrypted"), 0666); err != nil {
+	plaintextPath := filepath.Join(dir, "plaintext")
+	if err := os.WriteFile(plaintextPath, []byte("this data needs to be encrypted"), 0666); err != nil {
 		log.Fatal(err)
 	}
-
-	kh, err := keyset.NewHandle(streamingaead.AES256GCMHKDF4KBKeyTemplate())
+	plaintextFile, err := os.Open(plaintextPath)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	// TODO: save the keyset to a safe location. DO NOT hardcode it in source code.
-	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
-	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
+	// associatedData defines the context of the encryption. Here, we include the path of the
+	// plaintext file.
+	associatedData := []byte("associatedData for " + plaintextPath)
 
-	// Encrypt file.
-
-	a, err := streamingaead.New(kh)
+	// Encrypt the plaintext file and write the output to the ciphertext file. In this case the
+	// primary key of the keyset will be used (which is also the only key in this example).
+	ciphertextPath := filepath.Join(dir, "ciphertext")
+	ciphertextFile, err := os.Create(ciphertextPath)
 	if err != nil {
 		log.Fatal(err)
 	}
-
-	srcFile, err := os.Open(srcFilename)
+	w, err := primitive.NewEncryptingWriter(ciphertextFile, associatedData)
 	if err != nil {
 		log.Fatal(err)
 	}
-
-	ctFile, err := os.Create(ctFilename)
-	if err != nil {
+	if _, err := io.Copy(w, plaintextFile); err != nil {
 		log.Fatal(err)
 	}
-
-	aad := []byte("this data needs to be authenticated, but not encrypted")
-	w, err := a.NewEncryptingWriter(ctFile, aad)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	if _, err := io.Copy(w, srcFile); err != nil {
-		log.Fatal(err)
-	}
-
 	if err := w.Close(); err != nil {
 		log.Fatal(err)
 	}
-
-	if err := ctFile.Close(); err != nil {
+	if err := ciphertextFile.Close(); err != nil {
 		log.Fatal(err)
 	}
-	if err := srcFile.Close(); err != nil {
+	if err := plaintextFile.Close(); err != nil {
 		log.Fatal(err)
 	}
 
-	// Decrypt file.
-
-	ctFile, err = os.Open(ctFilename)
+	// Decrypt the ciphertext file and write the output to the decrypted file. The
+	// decryption finds the correct key in the keyset and decrypts the ciphertext.
+	// If no key is found or decryption fails, it returns an error.
+	ciphertextFile, err = os.Open(ciphertextPath)
 	if err != nil {
 		log.Fatal(err)
 	}
-
-	dstFile, err := os.Create(dstFilename)
+	decryptedPath := filepath.Join(dir, "decrypted")
+	decryptedFile, err := os.Create(decryptedPath)
 	if err != nil {
 		log.Fatal(err)
 	}
-
-	r, err := a.NewDecryptingReader(ctFile, aad)
+	r, err := primitive.NewDecryptingReader(ciphertextFile, associatedData)
 	if err != nil {
 		log.Fatal(err)
 	}
-
-	if _, err := io.Copy(dstFile, r); err != nil {
+	if _, err := io.Copy(decryptedFile, r); err != nil {
+		log.Fatal(err)
+	}
+	if err := decryptedFile.Close(); err != nil {
+		log.Fatal(err)
+	}
+	if err := ciphertextFile.Close(); err != nil {
 		log.Fatal(err)
 	}
 
-	if err := dstFile.Close(); err != nil {
-		log.Fatal(err)
-	}
-	if err := ctFile.Close(); err != nil {
-		log.Fatal(err)
-	}
-
-	b, err := ioutil.ReadFile(dstFilename)
-
+	// Print the content of the decrypted file.
+	b, err := os.ReadFile(decryptedPath)
 	if err != nil {
 		log.Fatal(err)
 	}
-
 	fmt.Println(string(b))
 	// Output: this data needs to be encrypted
 }
+
+// [END streaming-aead-example]
diff --git a/go/streamingaead/subtle/aes_ctr_hmac.go b/go/streamingaead/subtle/aes_ctr_hmac.go
index 8ed10ae..52edcbf 100644
--- a/go/streamingaead/subtle/aes_ctr_hmac.go
+++ b/go/streamingaead/subtle/aes_ctr_hmac.go
@@ -76,15 +76,7 @@
 // ciphertextSegmentSize is the size of ciphertext segments.
 //
 // firstSegmentOffset is the offset of the first ciphertext segment.
-func NewAESCTRHMAC(
-	mainKey []byte,
-	hkdfAlg string,
-	keySizeInBytes int,
-	tagAlg string,
-	tagSizeInBytes int,
-	ciphertextSegmentSize int,
-	firstSegmentOffset int,
-) (*AESCTRHMAC, error) {
+func NewAESCTRHMAC(mainKey []byte, hkdfAlg string, keySizeInBytes int, tagAlg string, tagSizeInBytes, ciphertextSegmentSize, firstSegmentOffset int) (*AESCTRHMAC, error) {
 	if len(mainKey) < 16 || len(mainKey) < keySizeInBytes {
 		return nil, errors.New("mainKey too short")
 	}
@@ -126,11 +118,18 @@
 	return 1 + a.keySizeInBytes + AESCTRHMACNoncePrefixSizeInBytes
 }
 
-// deriveKeyMaterial returns a key derived from the main key using salt and aad
-// as parameters.
-func (a *AESCTRHMAC) deriveKeyMaterial(salt, aad []byte) ([]byte, error) {
+// deriveKeys returns an AES of size a.keySizeInBytes and an HMAC key of size AESCTRHMACKeySizeInBytes.
+//
+// They are derived from the main key using salt and aad as parameters.
+func (a *AESCTRHMAC) deriveKeys(salt, aad []byte) ([]byte, []byte, error) {
 	keyMaterialSize := a.keySizeInBytes + AESCTRHMACKeySizeInBytes
-	return subtle.ComputeHKDF(a.hkdfAlg, a.MainKey, salt, aad, uint32(keyMaterialSize))
+	km, err := subtle.ComputeHKDF(a.hkdfAlg, a.MainKey, salt, aad, uint32(keyMaterialSize))
+	if err != nil {
+		return nil, nil, err
+	}
+	aesKey := km[:a.keySizeInBytes]
+	hmacKey := km[a.keySizeInBytes:]
+	return aesKey, hmacKey, nil
 }
 
 type aesCTRHMACSegmentEncrypter struct {
@@ -178,20 +177,16 @@
 	salt := random.GetRandomBytes(uint32(a.keySizeInBytes))
 	noncePrefix := random.GetRandomBytes(AESCTRHMACNoncePrefixSizeInBytes)
 
-	km, err := a.deriveKeyMaterial(salt, aad)
+	aesKey, hmacKey, err := a.deriveKeys(salt, aad)
 	if err != nil {
 		return nil, err
 	}
 
-	aesKey := make([]byte, a.keySizeInBytes)
-	copy(aesKey, km)
 	blockCipher, err := aes.NewCipher(aesKey)
 	if err != nil {
 		return nil, err
 	}
 
-	hmacKey := make([]byte, AESCTRHMACKeySizeInBytes)
-	copy(hmacKey, km[a.keySizeInBytes:])
 	hmac, err := subtlemac.NewHMAC(a.tagAlg, hmacKey, uint32(a.tagSizeInBytes))
 	if err != nil {
 		return nil, err
@@ -279,20 +274,16 @@
 		return nil, fmt.Errorf("cannot read noncePrefix: %v", err)
 	}
 
-	km, err := a.deriveKeyMaterial(salt, aad)
+	aesKey, hmacKey, err := a.deriveKeys(salt, aad)
 	if err != nil {
 		return nil, err
 	}
 
-	aesKey := make([]byte, a.keySizeInBytes)
-	copy(aesKey, km)
 	blockCipher, err := aes.NewCipher(aesKey)
 	if err != nil {
 		return nil, err
 	}
 
-	hmacKey := make([]byte, AESCTRHMACKeySizeInBytes)
-	copy(hmacKey, km[a.keySizeInBytes:])
 	hmac, err := subtlemac.NewHMAC(a.tagAlg, hmacKey, uint32(a.tagSizeInBytes))
 	if err != nil {
 		return nil, err
diff --git a/go/streamingaead/subtle/aes_gcm_hkdf.go b/go/streamingaead/subtle/aes_gcm_hkdf.go
index 3f31d03..8cf4c52 100644
--- a/go/streamingaead/subtle/aes_gcm_hkdf.go
+++ b/go/streamingaead/subtle/aes_gcm_hkdf.go
@@ -70,13 +70,7 @@
 // ciphertextSegmentSize argument is the size of ciphertext segments.
 //
 // firstSegmentOffset argument is the offset of the first ciphertext segment.
-func NewAESGCMHKDF(
-	mainKey []byte,
-	hkdfAlg string,
-	keySizeInBytes int,
-	ciphertextSegmentSize int,
-	firstSegmentOffset int,
-) (*AESGCMHKDF, error) {
+func NewAESGCMHKDF(mainKey []byte, hkdfAlg string, keySizeInBytes, ciphertextSegmentSize, firstSegmentOffset int) (*AESGCMHKDF, error) {
 	if len(mainKey) < 16 || len(mainKey) < keySizeInBytes {
 		return nil, errors.New("mainKey too short")
 	}
@@ -131,8 +125,8 @@
 }
 
 func (e aesGCMHKDFSegmentEncrypter) EncryptSegment(segment, nonce []byte) ([]byte, error) {
-	result := make([]byte, len(segment))
-	result = e.cipher.Seal(result[0:0], nonce, segment, nil)
+	result := make([]byte, 0, len(segment))
+	result = e.cipher.Seal(result, nonce, segment, nil)
 	return result, nil
 }
 
diff --git a/go/subtle/BUILD.bazel b/go/subtle/BUILD.bazel
index 9f5ed79..2ecd542 100644
--- a/go/subtle/BUILD.bazel
+++ b/go/subtle/BUILD.bazel
@@ -24,9 +24,8 @@
         "x25519_test.go",
     ],
     data = ["@wycheproof//testvectors:xdh"],
-    embed = [":subtle"],
     deps = [
-        "//subtle/random",
+        ":subtle",
         "//testutil",
         "@org_golang_x_crypto//curve25519",
     ],
diff --git a/go/subtle/hkdf_test.go b/go/subtle/hkdf_test.go
index ca1f15b..175c5b2 100644
--- a/go/subtle/hkdf_test.go
+++ b/go/subtle/hkdf_test.go
@@ -14,147 +14,175 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-package subtle
+package subtle_test
 
 import (
 	"encoding/hex"
-	"fmt"
-	"strings"
 	"testing"
 
-	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/subtle"
 )
 
-// Tests the implementation against the test vectors from RFC 5869.
-var hkdfTests = []struct {
-	hashAlg     string
-	key         string
-	salt        string
-	info        string
-	tagSize     uint32
-	expectedKDF string
-}{
-	{
-		hashAlg:     "SHA256",
-		key:         "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
-		salt:        "000102030405060708090a0b0c",
-		info:        "f0f1f2f3f4f5f6f7f8f9",
-		tagSize:     42,
-		expectedKDF: "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
-	},
-	{
-		hashAlg: "SHA256",
-		key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
-			"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" +
-			"404142434445464748494a4b4c4d4e4f",
-		salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" +
-			"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +
-			"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
-		info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
-			"d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
-			"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
-		tagSize: 82,
-		expectedKDF: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c" +
-			"59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71" +
-			"cc30c58179ec3e87c14c01d5c1f3434f1d87",
-	},
-	{
-		hashAlg: "SHA256",
-		key:     "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
-		salt:    "",
-		info:    "",
-		tagSize: 42,
-		expectedKDF: "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" +
-			"9d201395faa4b61a96c8",
-	},
-	{
-		hashAlg:     "SHA1",
-		key:         "0b0b0b0b0b0b0b0b0b0b0b",
-		salt:        "000102030405060708090a0b0c",
-		info:        "f0f1f2f3f4f5f6f7f8f9",
-		tagSize:     42,
-		expectedKDF: "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896",
-	},
-	{
-		hashAlg: "SHA1",
-		key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
-			"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" +
-			"404142434445464748494a4b4c4d4e4f",
-		salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" +
-			"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +
-			"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
-		info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
-			"d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
-			"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
-		tagSize: 82,
-		expectedKDF: "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe" +
-			"8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e" +
-			"927336d0441f4c4300e2cff0d0900b52d3b4",
-	},
-	{
-		hashAlg: "SHA1",
-		key:     "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
-		salt:    "",
-		info:    "",
-		tagSize: 42,
-		expectedKDF: "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0" +
-			"ea00033de03984d34918",
-	},
-	{
-		hashAlg: "SHA1",
-		key:     "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
-		salt:    "",
-		info:    "",
-		tagSize: 42,
-		expectedKDF: "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5" +
-			"673a081d70cce7acfc48",
-	},
-}
-
 func TestHKDFBasic(t *testing.T) {
-	for ti, test := range hkdfTests {
-		k, _ := hex.DecodeString(test.key)
-		s, _ := hex.DecodeString(test.salt)
-		i, _ := hex.DecodeString(test.info)
+	// Test vectors from RFC 5869, Appendix A.
+	//
+	// The name and desc fields align with the content from the RFC for easy
+	// cross referencing.
+	var tests = []struct {
+		name    string
+		desc    string
+		hashAlg string
+		key     string
+		salt    string
+		info    string
+		tagSize uint32
+		okm     string
+	}{
+		{
+			name:    "TestCase1",
+			desc:    "Basic test case with SHA-256",
+			hashAlg: "SHA256",
+			key:     "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+			salt:    "000102030405060708090a0b0c",
+			info:    "f0f1f2f3f4f5f6f7f8f9",
+			tagSize: 42,
+			okm:     "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
+		},
+		{
+			name:    "TestCase2",
+			desc:    "Test with SHA-256 and longer inputs/outputs",
+			hashAlg: "SHA256",
+			key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
+				"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" +
+				"404142434445464748494a4b4c4d4e4f",
+			salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" +
+				"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +
+				"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+			info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+				"d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+				"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+			tagSize: 82,
+			okm: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c" +
+				"59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71" +
+				"cc30c58179ec3e87c14c01d5c1f3434f1d87",
+		},
+		{
+			name:    "TestCase3",
+			desc:    "Test with SHA-256 and zero-length salt/info",
+			hashAlg: "SHA256",
+			key:     "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+			salt:    "",
+			info:    "",
+			tagSize: 42,
+			okm: "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" +
+				"9d201395faa4b61a96c8",
+		},
+		{
+			name:    "TestCase4",
+			desc:    "Basic test case with SHA-1",
+			hashAlg: "SHA1",
+			key:     "0b0b0b0b0b0b0b0b0b0b0b",
+			salt:    "000102030405060708090a0b0c",
+			info:    "f0f1f2f3f4f5f6f7f8f9",
+			tagSize: 42,
+			okm:     "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896",
+		},
+		{
+			name:    "TestCase5",
+			desc:    "Test with SHA-1 and longer inputs/outputs",
+			hashAlg: "SHA1",
+			key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
+				"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" +
+				"404142434445464748494a4b4c4d4e4f",
+			salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" +
+				"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +
+				"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+			info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+				"d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+				"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+			tagSize: 82,
+			okm: "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe" +
+				"8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e" +
+				"927336d0441f4c4300e2cff0d0900b52d3b4",
+		},
+		{
+			name:    "TestCase6",
+			desc:    "Test with SHA-1 and zero-length salt/info",
+			hashAlg: "SHA1",
+			key:     "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+			salt:    "",
+			info:    "",
+			tagSize: 42,
+			okm: "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0" +
+				"ea00033de03984d34918",
+		},
+		{
+			name:    "TestCase7",
+			desc:    "Test with SHA-1, salt not provided (defaults to HashLen zero octets), zero-length info",
+			hashAlg: "SHA1",
+			key:     "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
+			salt:    "",
+			info:    "",
+			tagSize: 42,
+			okm: "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5" +
+				"673a081d70cce7acfc48",
+		},
+	}
 
-		result, err := ComputeHKDF(test.hashAlg, k, s, i, test.tagSize)
-		r := hex.EncodeToString(result)
-		fmt.Printf("Test no: %d\n", ti)
-		fmt.Printf("Length of tag :%d\n", test.tagSize)
-		fmt.Printf("Length of result :%d\n", len(r))
-		fmt.Printf("Length of expected :%d\n\n\n", len(test.expectedKDF))
-		if err != nil {
-			t.Errorf("mac computation failed in test case %d: %s", ti, err)
-		}
-		if r != test.expectedKDF {
-			t.Errorf("incorrect hkdf in test case %d: expect %s, got %s",
-				ti, test.expectedKDF, r)
-		}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			key, err := hex.DecodeString(test.key)
+			if err != nil {
+				t.Fatalf("%s\nhex.DecodeString(key) err = %v", test.desc, err)
+			}
+			salt, err := hex.DecodeString(test.salt)
+			if err != nil {
+				t.Fatalf("%s\nFailed decoding salt: %v", test.desc, err)
+			}
+			info, err := hex.DecodeString(test.info)
+			if err != nil {
+				t.Fatalf("%s\nFailed decoding info: %v", test.desc, err)
+			}
+
+			okm, err := subtle.ComputeHKDF(test.hashAlg, key, salt, info, test.tagSize)
+			if err != nil {
+				t.Errorf("%s\nsubtle.ComputeHKDF() err = %v, want nil", test.desc, err)
+			}
+			if got, want := hex.EncodeToString(okm), test.okm; got != want {
+				t.Errorf("%s\nsubtle.ComputeHKDF() = %q, want %q", test.desc, got, want)
+			}
+		})
 	}
 }
 
 func TestNewHMACWithInvalidInput(t *testing.T) {
-	// invalid hash algorithm
-	_, err := ComputeHKDF("SHA0", random.GetRandomBytes(16), nil, nil, 32)
-	if err == nil || !strings.Contains(err.Error(), "invalid hash algorithm") {
-		t.Errorf("expect an error when hash algorithm is invalid")
+	var tests = []struct {
+		name    string
+		hashAlg string
+		tagSize uint32
+	}{
+		{
+			name:    "invalid algorithm",
+			hashAlg: "SHA0",
+			tagSize: 32,
+		},
+		{
+			name:    "tag too short",
+			hashAlg: "SHA256",
+			tagSize: 9,
+		},
+		{
+			name:    "tag too big",
+			hashAlg: "SHA512",
+			tagSize: 16323,
+		},
 	}
-	// tag too short
-	_, err = ComputeHKDF("SHA256", random.GetRandomBytes(16), nil, nil, 9)
-	if err == nil || !strings.Contains(err.Error(), "tag size too small") {
-		t.Errorf("expect an error when tag size is too small")
-	}
-	// tag too big
-	_, err = ComputeHKDF("SHA1", random.GetRandomBytes(16), nil, nil, 5101)
-	if err == nil || !strings.Contains(err.Error(), "tag size too big") {
-		t.Errorf("expect an error when tag size is too big")
-	}
-	_, err = ComputeHKDF("SHA256", random.GetRandomBytes(16), nil, nil, 8162)
-	if err == nil || !strings.Contains(err.Error(), "tag size too big") {
-		t.Errorf("expect an error when tag size is too big")
-	}
-	_, err = ComputeHKDF("SHA512", random.GetRandomBytes(16), nil, nil, 16323)
-	if err == nil || !strings.Contains(err.Error(), "tag size too big") {
-		t.Errorf("expect an error when tag size is too big")
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := subtle.ComputeHKDF(test.hashAlg, nil, nil, nil, test.tagSize); err == nil {
+				t.Fatalf("subtle.ComputeHKDF(%q, nil, nil, nil, %d) err is nil, want not nil", test.hashAlg, test.tagSize)
+			}
+		})
 	}
 }
diff --git a/go/testdata/README.md b/go/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/go/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/go/testdata/aws/README.md b/go/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/go/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/go/testdata/gcp/README.md b/go/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/go/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/go/testkeyset/BUILD.bazel b/go/testkeyset/BUILD.bazel
index b77a2af..4a6f73d 100644
--- a/go/testkeyset/BUILD.bazel
+++ b/go/testkeyset/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 package(default_visibility = ["//:__subpackages__"])  # keep
 
@@ -22,3 +22,16 @@
     actual = ":testkeyset",
     visibility = ["//visibility:public"],
 )
+
+go_test(
+    name = "testkeyset_test",
+    srcs = ["testkeyset_test.go"],
+    deps = [
+        ":testkeyset",
+        "//insecurecleartextkeyset",
+        "//keyset",
+        "//mac",
+        "//proto/tink_go_proto",
+        "@com_github_google_go_cmp//cmp",
+    ],
+)
diff --git a/go/testkeyset/testkeyset.go b/go/testkeyset/testkeyset.go
index 1aa9f68..d862912 100644
--- a/go/testkeyset/testkeyset.go
+++ b/go/testkeyset/testkeyset.go
@@ -26,10 +26,8 @@
 )
 
 var (
-	// KeysetHandle creates a keyset.Handle from cleartext key material.
-	KeysetHandle = internal.KeysetHandle.(func(*tinkpb.Keyset) *keyset.Handle)
-	// KeysetMaterial returns the key material contained in a keyset.Handle.
-	KeysetMaterial = internal.KeysetMaterial.(func(*keyset.Handle) *tinkpb.Keyset)
+	keysetHandle   = internal.KeysetHandle.(func(*tinkpb.Keyset, ...keyset.Option) (*keyset.Handle, error))
+	keysetMaterial = internal.KeysetMaterial.(func(*keyset.Handle) *tinkpb.Keyset)
 
 	errInvalidKeyset = errors.New("cleartextkeyset: invalid keyset")
 	errInvalidHandle = errors.New("cleartextkeyset: invalid handle")
@@ -42,7 +40,7 @@
 	if ks == nil || len(ks.Key) == 0 {
 		return nil, errInvalidKeyset
 	}
-	return KeysetHandle(ks), nil
+	return keysetHandle(ks)
 }
 
 // Read creates a keyset.Handle from a cleartext keyset obtained via r.
@@ -54,12 +52,12 @@
 	if err != nil || ks == nil || len(ks.Key) == 0 {
 		return nil, errInvalidKeyset
 	}
-	return KeysetHandle(ks), nil
+	return keysetHandle(ks)
 }
 
 // Write exports the keyset from h to the given writer w without encrypting it.
 // Storing secret key material in an unencrypted fashion is dangerous. If feasible, you should use
-// func keyset.Handle.Write() instead.
+// [keyset.Handle.Write] instead.
 func Write(h *keyset.Handle, w keyset.Writer) error {
 	if h == nil {
 		return errInvalidHandle
@@ -69,3 +67,24 @@
 	}
 	return w.Write(KeysetMaterial(h))
 }
+
+// KeysetMaterial returns the key material contained in a keyset.Handle.
+func KeysetMaterial(h *keyset.Handle) *tinkpb.Keyset {
+	return keysetMaterial(h)
+}
+
+// KeysetHandle creates a keyset.Handle from cleartext key material.
+//
+// Callers should verify that the returned *keyset.Handle isn't nil.
+//
+// Deprecated: Use [NewHandle].
+func KeysetHandle(ks *tinkpb.Keyset) *keyset.Handle {
+	kh, err := keysetHandle(ks)
+	if err != nil {
+		// This *keyset.Handle can only return errors when *keyset.Option arguments
+		// are provided. To maintain backwards compatibility and avoid panic, it returns
+		// a nil value if an error happens.
+		return nil
+	}
+	return kh
+}
diff --git a/go/testkeyset/testkeyset_test.go b/go/testkeyset/testkeyset_test.go
new file mode 100644
index 0000000..e8fee8a
--- /dev/null
+++ b/go/testkeyset/testkeyset_test.go
@@ -0,0 +1,67 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package testkeyset_test
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/testkeyset"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+func makeKeyset(template *tinkpb.KeyTemplate) (*tinkpb.Keyset, error) {
+	h, err := keyset.NewHandle(template)
+	if err != nil {
+		return nil, err
+	}
+	return insecurecleartextkeyset.KeysetMaterial(h), nil
+}
+
+func TestNewHandleCallsAreConsistent(t *testing.T) {
+	ks, err := makeKeyset(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("makeKeyset(mac.HMACSHA256Tag128KeyTemplate()) err = %v, want nil", err)
+	}
+	handle1, err := testkeyset.NewHandle(ks)
+	if err != nil {
+		t.Fatalf("testkeyset.NewHandle() err = %v, want nil", err)
+	}
+	p1, err := mac.New(handle1)
+	if err != nil {
+		t.Fatalf("mac.New(handle1) err = %v, want nil", err)
+	}
+	p2, err := mac.New(testkeyset.KeysetHandle(ks))
+	if err != nil {
+		t.Fatalf("mac.New(testkeyset.KeysetHandle(ks)) err = %v, want nil", err)
+	}
+	data := []byte("data")
+	m1, err := p1.ComputeMAC(data)
+	if err != nil {
+		t.Fatalf("p1.ComputeMAC(data) err = %v, want nil", err)
+	}
+	m2, err := p2.ComputeMAC(data)
+	if err != nil {
+		t.Fatalf("p2.ComputeMAC(data) err = %v, want nil", err)
+	}
+	if !cmp.Equal(m1, m2) {
+		t.Errorf("MAC mistmatch, got = %v, want %v", m1, m2)
+	}
+}
diff --git a/go/testutil/BUILD.bazel b/go/testutil/BUILD.bazel
index 2bc9ead..0ff8c1a 100644
--- a/go/testutil/BUILD.bazel
+++ b/go/testutil/BUILD.bazel
@@ -36,7 +36,6 @@
         "//subtle/random",
         "//tink",
         "@org_golang_google_protobuf//proto",
-        "@org_golang_x_crypto//ed25519",
     ],
 )
 
diff --git a/go/testutil/hybrid/BUILD.bazel b/go/testutil/hybrid/BUILD.bazel
index 6a56f74..9d9ffd7 100644
--- a/go/testutil/hybrid/BUILD.bazel
+++ b/go/testutil/hybrid/BUILD.bazel
@@ -8,10 +8,10 @@
     srcs = ["private_key.go"],
     importpath = "github.com/google/tink/go/testutil/hybrid",
     deps = [
-        "//insecurecleartextkeyset",
         "//keyset",
         "//proto/hpke_go_proto",
         "//proto/tink_go_proto",
+        "//testkeyset",
         "@org_golang_google_protobuf//proto",
     ],
 )
@@ -22,11 +22,11 @@
     deps = [
         ":hybrid",
         "//hybrid",
-        "//insecurecleartextkeyset",
         "//keyset",
         "//proto/hpke_go_proto",
         "//proto/tink_go_proto",
         "//subtle/random",
+        "//testkeyset",
         "@org_golang_google_protobuf//proto",
     ],
 )
diff --git a/go/testutil/hybrid/private_key.go b/go/testutil/hybrid/private_key.go
index 8548f0c..665bbd1 100644
--- a/go/testutil/hybrid/private_key.go
+++ b/go/testutil/hybrid/private_key.go
@@ -23,8 +23,8 @@
 	"fmt"
 
 	"google.golang.org/protobuf/proto"
-	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/testkeyset"
 	hpkepb "github.com/google/tink/go/proto/hpke_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
 )
@@ -88,8 +88,7 @@
 			},
 		},
 	}
-
-	return insecurecleartextkeyset.Read(&keyset.MemReaderWriter{Keyset: ks})
+	return testkeyset.NewHandle(ks)
 }
 
 // hpkeParamsFromTemplate returns HPKE params after verifying that template is
diff --git a/go/testutil/hybrid/private_key_test.go b/go/testutil/hybrid/private_key_test.go
index 4d123f9..866a568 100644
--- a/go/testutil/hybrid/private_key_test.go
+++ b/go/testutil/hybrid/private_key_test.go
@@ -22,9 +22,9 @@
 
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/hybrid"
-	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/subtle/random"
+	"github.com/google/tink/go/testkeyset"
 	testutilhybrid "github.com/google/tink/go/testutil/hybrid"
 	hpkepb "github.com/google/tink/go/proto/hpke_go_proto"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
@@ -111,17 +111,12 @@
 func privPubKeyBytes(t *testing.T, handle *keyset.Handle) ([]byte, []byte) {
 	t.Helper()
 
-	// Write Handle to MemReaderWriter.
-	got := &keyset.MemReaderWriter{}
-	if err := insecurecleartextkeyset.Write(handle, got); err != nil {
-		t.Fatalf("Write(%v) err = %v, want nil", handle, err)
-	}
-	if len(got.Keyset.GetKey()) != 1 {
-		t.Fatalf("len(gotPriv.Keyset) = %d", len(got.Keyset.GetKey()))
+	ks := testkeyset.KeysetMaterial(handle)
+	if len(ks.GetKey()) != 1 {
+		t.Fatalf("got len(ks.GetKey()) = %d, want 1", len(ks.GetKey()))
 	}
 
-	// Extract HpkePrivateKey from MemReaderWriter.
-	serializedPrivKey := got.Keyset.GetKey()[0].GetKeyData().GetValue()
+	serializedPrivKey := ks.GetKey()[0].GetKeyData().GetValue()
 	privKey := &hpkepb.HpkePrivateKey{}
 	if err := proto.Unmarshal(serializedPrivKey, privKey); err != nil {
 		t.Fatalf("Unmarshal(%v) = err %v, want nil", serializedPrivKey, err)
diff --git a/go/testutil/testutil.go b/go/testutil/testutil.go
index 9f422e7..ac89311 100644
--- a/go/testutil/testutil.go
+++ b/go/testutil/testutil.go
@@ -20,6 +20,7 @@
 import (
 	"bytes"
 	"crypto/ecdsa"
+	"crypto/ed25519"
 	"crypto/rand"
 	"encoding/gob"
 	"errors"
@@ -29,7 +30,6 @@
 	"strconv"
 	"strings"
 
-	"golang.org/x/crypto/ed25519"
 	"google.golang.org/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
 	subtledaead "github.com/google/tink/go/daead/subtle"
@@ -153,6 +153,28 @@
 	return nil, fmt.Errorf("AlwaysFailingAead will always fail on decryption: %v", a.Error)
 }
 
+// AlwaysFailingDeterministicAead fails encryption and decryption operations.
+type AlwaysFailingDeterministicAead struct {
+	Error error
+}
+
+var _ (tink.DeterministicAEAD) = (*AlwaysFailingDeterministicAead)(nil)
+
+// NewAlwaysFailingDeterministicAead creates a new always failing AEAD.
+func NewAlwaysFailingDeterministicAead(err error) tink.DeterministicAEAD {
+	return &AlwaysFailingDeterministicAead{Error: err}
+}
+
+// EncryptDeterministically returns an error on encryption.
+func (a *AlwaysFailingDeterministicAead) EncryptDeterministically(plaintext []byte, associatedData []byte) ([]byte, error) {
+	return nil, fmt.Errorf("AlwaysFailingDeterministicAead will always fail on encryption: %v", a.Error)
+}
+
+// DecryptDeterministically returns an error on decryption.
+func (a *AlwaysFailingDeterministicAead) DecryptDeterministically(ciphertext []byte, associatedData []byte) ([]byte, error) {
+	return nil, fmt.Errorf("AlwaysFailingDeterministicAead will always fail on decryption: %v", a.Error)
+}
+
 // TestKeyManager is key manager which can be setup to return an arbitrary primitive for a type URL
 // useful for testing.
 type TestKeyManager struct {
@@ -290,8 +312,7 @@
 }
 
 // NewTestHMACKeyset creates a new Keyset containing a HMACKey.
-func NewTestHMACKeyset(tagSize uint32,
-	primaryOutputPrefixType tinkpb.OutputPrefixType) *tinkpb.Keyset {
+func NewTestHMACKeyset(tagSize uint32, primaryOutputPrefixType tinkpb.OutputPrefixType) *tinkpb.Keyset {
 	keyData := NewHMACKeyData(commonpb.HashType_SHA256, tagSize)
 	return NewTestKeyset(keyData, primaryOutputPrefixType)
 }
@@ -308,8 +329,7 @@
 }
 
 // NewTestKeyset creates a new test Keyset.
-func NewTestKeyset(keyData *tinkpb.KeyData,
-	primaryOutputPrefixType tinkpb.OutputPrefixType) *tinkpb.Keyset {
+func NewTestKeyset(keyData *tinkpb.KeyData, primaryOutputPrefixType tinkpb.OutputPrefixType) *tinkpb.Keyset {
 	primaryKey := NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 42, primaryOutputPrefixType)
 	rawKey := NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 43, tinkpb.OutputPrefixType_RAW)
 	legacyKey := NewKey(keyData, tinkpb.KeyStatusType_ENABLED, 44, tinkpb.OutputPrefixType_LEGACY)
@@ -330,9 +350,7 @@
 }
 
 // NewECDSAParams creates a ECDSAParams with the specified parameters.
-func NewECDSAParams(hashType commonpb.HashType,
-	curve commonpb.EllipticCurveType,
-	encoding ecdsapb.EcdsaSignatureEncoding) *ecdsapb.EcdsaParams {
+func NewECDSAParams(hashType commonpb.HashType, curve commonpb.EllipticCurveType, encoding ecdsapb.EcdsaSignatureEncoding) *ecdsapb.EcdsaParams {
 	return &ecdsapb.EcdsaParams{
 		HashType: hashType,
 		Curve:    curve,
@@ -346,9 +364,7 @@
 }
 
 // NewECDSAPrivateKey creates a ECDSAPrivateKey with the specified paramaters.
-func NewECDSAPrivateKey(version uint32,
-	publicKey *ecdsapb.EcdsaPublicKey,
-	keyValue []byte) *ecdsapb.EcdsaPrivateKey {
+func NewECDSAPrivateKey(version uint32, publicKey *ecdsapb.EcdsaPublicKey, keyValue []byte) *ecdsapb.EcdsaPrivateKey {
 	return &ecdsapb.EcdsaPrivateKey{
 		Version:   version,
 		PublicKey: publicKey,
@@ -357,9 +373,7 @@
 }
 
 // NewECDSAPublicKey creates a ECDSAPublicKey with the specified paramaters.
-func NewECDSAPublicKey(version uint32,
-	params *ecdsapb.EcdsaParams,
-	x []byte, y []byte) *ecdsapb.EcdsaPublicKey {
+func NewECDSAPublicKey(version uint32, params *ecdsapb.EcdsaParams, x, y []byte) *ecdsapb.EcdsaPublicKey {
 	return &ecdsapb.EcdsaPublicKey{
 		Version: version,
 		Params:  params,
@@ -436,7 +450,7 @@
 }
 
 // NewAESGCMSIVKey creates a randomly generated AESGCMSIVKey.
-func NewAESGCMSIVKey(keyVersion uint32, keySize uint32) *gcmsivpb.AesGcmSivKey {
+func NewAESGCMSIVKey(keyVersion, keySize uint32) *gcmsivpb.AesGcmSivKey {
 	keyValue := random.GetRandomBytes(keySize)
 	return &gcmsivpb.AesGcmSivKey{
 		Version:  keyVersion,
@@ -461,13 +475,7 @@
 }
 
 // NewAESGCMHKDFKey creates a randomly generated AESGCMHKDFKey.
-func NewAESGCMHKDFKey(
-	keyVersion uint32,
-	keySize uint32,
-	derivedKeySize uint32,
-	hkdfHashType commonpb.HashType,
-	ciphertextSegmentSize uint32,
-) *gcmhkdfpb.AesGcmHkdfStreamingKey {
+func NewAESGCMHKDFKey(keyVersion, keySize, derivedKeySize uint32, hkdfHashType commonpb.HashType, ciphertextSegmentSize uint32) *gcmhkdfpb.AesGcmHkdfStreamingKey {
 	keyValue := random.GetRandomBytes(keySize)
 	return &gcmhkdfpb.AesGcmHkdfStreamingKey{
 		Version:  keyVersion,
@@ -481,12 +489,7 @@
 }
 
 // NewAESGCMHKDFKeyData creates a KeyData containing a randomly generated AESGCMHKDFKey.
-func NewAESGCMHKDFKeyData(
-	keySize uint32,
-	derivedKeySize uint32,
-	hkdfHashType commonpb.HashType,
-	ciphertextSegmentSize uint32,
-) *tinkpb.KeyData {
+func NewAESGCMHKDFKeyData(keySize, derivedKeySize uint32, hkdfHashType commonpb.HashType, ciphertextSegmentSize uint32) *tinkpb.KeyData {
 	serializedKey, err := proto.Marshal(NewAESGCMHKDFKey(AESGCMHKDFKeyVersion, keySize, derivedKeySize, hkdfHashType, ciphertextSegmentSize))
 	if err != nil {
 		log.Fatalf("failed serializing proto: %v", err)
@@ -495,12 +498,7 @@
 }
 
 // NewAESGCMHKDFKeyFormat returns a new AESGCMHKDFKeyFormat.
-func NewAESGCMHKDFKeyFormat(
-	keySize uint32,
-	derivedKeySize uint32,
-	hkdfHashType commonpb.HashType,
-	ciphertextSegmentSize uint32,
-) *gcmhkdfpb.AesGcmHkdfStreamingKeyFormat {
+func NewAESGCMHKDFKeyFormat(keySize, derivedKeySize uint32, hkdfHashType commonpb.HashType, ciphertextSegmentSize uint32) *gcmhkdfpb.AesGcmHkdfStreamingKeyFormat {
 	return &gcmhkdfpb.AesGcmHkdfStreamingKeyFormat{
 		KeySize: keySize,
 		Params: &gcmhkdfpb.AesGcmHkdfStreamingParams{
@@ -512,15 +510,7 @@
 }
 
 // NewAESCTRHMACKey creates a randomly generated AESCTRHMACKey.
-func NewAESCTRHMACKey(
-	keyVersion uint32,
-	keySize uint32,
-	hkdfHashType commonpb.HashType,
-	derivedKeySize uint32,
-	hashType commonpb.HashType,
-	tagSize uint32,
-	ciphertextSegmentSize uint32,
-) *ctrhmacpb.AesCtrHmacStreamingKey {
+func NewAESCTRHMACKey(keyVersion, keySize uint32, hkdfHashType commonpb.HashType, derivedKeySize uint32, hashType commonpb.HashType, tagSize, ciphertextSegmentSize uint32) *ctrhmacpb.AesCtrHmacStreamingKey {
 	keyValue := random.GetRandomBytes(keySize)
 	return &ctrhmacpb.AesCtrHmacStreamingKey{
 		Version:  keyVersion,
@@ -538,14 +528,7 @@
 }
 
 // NewAESCTRHMACKeyFormat returns a new AESCTRHMACKeyFormat.
-func NewAESCTRHMACKeyFormat(
-	keySize uint32,
-	hkdfHashType commonpb.HashType,
-	derivedKeySize uint32,
-	hashType commonpb.HashType,
-	tagSize uint32,
-	ciphertextSegmentSize uint32,
-) *ctrhmacpb.AesCtrHmacStreamingKeyFormat {
+func NewAESCTRHMACKeyFormat(keySize uint32, hkdfHashType commonpb.HashType, derivedKeySize uint32, hashType commonpb.HashType, tagSize, ciphertextSegmentSize uint32) *ctrhmacpb.AesCtrHmacStreamingKeyFormat {
 	return &ctrhmacpb.AesCtrHmacStreamingKeyFormat{
 		KeySize: keySize,
 		Params: &ctrhmacpb.AesCtrHmacStreamingParams{
@@ -621,9 +604,13 @@
 func NewHMACKeysetManager() *keyset.Manager {
 	ksm := keyset.NewManager()
 	kt := mac.HMACSHA256Tag128KeyTemplate()
-	err := ksm.Rotate(kt)
+	keyID, err := ksm.Add(kt)
 	if err != nil {
-		panic(fmt.Sprintf("cannot rotate keyset manager: %s", err))
+		panic(fmt.Sprintf("cannot add key: %v", err))
+	}
+	err = ksm.SetPrimary(keyID)
+	if err != nil {
+		panic(fmt.Sprintf("cannot set primary key: %v", err))
 	}
 	return ksm
 }
@@ -717,9 +704,7 @@
 }
 
 // NewKeyData creates a new KeyData with the specified parameters.
-func NewKeyData(typeURL string,
-	value []byte,
-	materialType tinkpb.KeyData_KeyMaterialType) *tinkpb.KeyData {
+func NewKeyData(typeURL string, value []byte, materialType tinkpb.KeyData_KeyMaterialType) *tinkpb.KeyData {
 	return &tinkpb.KeyData{
 		TypeUrl:         typeURL,
 		Value:           value,
@@ -728,10 +713,7 @@
 }
 
 // NewKey creates a new Key with the specified parameters.
-func NewKey(keyData *tinkpb.KeyData,
-	status tinkpb.KeyStatusType,
-	keyID uint32,
-	prefixType tinkpb.OutputPrefixType) *tinkpb.Keyset_Key {
+func NewKey(keyData *tinkpb.KeyData, status tinkpb.KeyStatusType, keyID uint32, prefixType tinkpb.OutputPrefixType) *tinkpb.Keyset_Key {
 	return &tinkpb.Keyset_Key{
 		KeyData:          keyData,
 		Status:           status,
@@ -741,8 +723,7 @@
 }
 
 // NewKeyset creates a new Keyset with the specified parameters.
-func NewKeyset(primaryKeyID uint32,
-	keys []*tinkpb.Keyset_Key) *tinkpb.Keyset {
+func NewKeyset(primaryKeyID uint32, keys []*tinkpb.Keyset_Key) *tinkpb.Keyset {
 	return &tinkpb.Keyset{
 		PrimaryKeyId: primaryKeyID,
 		Key:          keys,
@@ -831,8 +812,7 @@
 //
 // Note: Having a correlation of zero is only a necessary but not sufficient
 // condition for independence.
-func ZTestCrosscorrelationUniformStrings(bytes1,
-	bytes2 []byte) error {
+func ZTestCrosscorrelationUniformStrings(bytes1, bytes2 []byte) error {
 	if len(bytes1) != len(bytes2) {
 		return fmt.Errorf(
 			"Strings are not of equal length")
diff --git a/go/tink/streamingaead.go b/go/tink/streamingaead.go
index 70ced9b..79eed38 100644
--- a/go/tink/streamingaead.go
+++ b/go/tink/streamingaead.go
@@ -33,13 +33,13 @@
 */
 type StreamingAEAD interface {
 	// NewEncryptingWriter returns a wrapper around underlying io.Writer, such that any write-operation
-	// via the wrapper results in AEAD-encryption of the written data, using aad
-	// as associated authenticated data. The associated data is not included in the ciphertext
+	// via the wrapper results in AEAD-encryption of the written data, using associatedData
+	// as associated data. The associated data is not included in the ciphertext
 	// and has to be passed in as parameter for decryption.
-	NewEncryptingWriter(w io.Writer, aad []byte) (io.WriteCloser, error)
+	NewEncryptingWriter(w io.Writer, associatedData []byte) (io.WriteCloser, error)
 
 	// NewDecryptingReader returns a wrapper around underlying io.Reader, such that any read-operation
 	// via the wrapper results in AEAD-decryption of the underlying ciphertext,
-	// using aad as associated authenticated data.
-	NewDecryptingReader(r io.Reader, aad []byte) (io.Reader, error)
+	// using associatedData as associated data.
+	NewDecryptingReader(r io.Reader, associatedData []byte) (io.Reader, error)
 }
diff --git a/go/tink/version.go b/go/tink/version.go
index 6aaf8f3..ed4bddb 100644
--- a/go/tink/version.go
+++ b/go/tink/version.go
@@ -18,5 +18,5 @@
 
 const (
 	// Version is the current version of Tink.
-	Version = "1.7.0"
+	Version = "2.0.0"
 )
diff --git a/go/tink_go_deps.bzl b/go/tink_go_deps.bzl
deleted file mode 100644
index dc869b5..0000000
--- a/go/tink_go_deps.bzl
+++ /dev/null
@@ -1,43 +0,0 @@
-"""
-Dependencies of Go Tink.
-"""
-
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-# Deprecated.
-def tink_go_deps():
-    """ Deprecated function that loads dependencies of Go Tink.
-
-    This function should not be used anymore. Instead, these dependencies should be declared directly in the WORKSPACE.
-    See: https://github.com/bazelbuild/bazel-gazelle#setup
-    """
-    if not native.existing_rule("io_bazel_rules_go"):
-        # Release from 2022-01-24
-        http_archive(
-            name = "io_bazel_rules_go",
-            sha256 = "d6b2513456fe2229811da7eb67a444be7785f5323c6708b38d851d2b51e54d83",
-            urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.30.0/rules_go-v0.30.0.zip",
-                "https://github.com/bazelbuild/rules_go/releases/download/v0.30.0/rules_go-v0.30.0.zip",
-            ],
-        )
-
-    if not native.existing_rule("bazel_gazelle"):
-        # Release from 2021-10-11
-        http_archive(
-            name = "bazel_gazelle",
-            sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
-            urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
-                "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
-            ],
-        )
-
-    if not native.existing_rule("wycheproof"):
-        # Commit from 2019-12-17
-        http_archive(
-            name = "wycheproof",
-            strip_prefix = "wycheproof-d8ed1ba95ac4c551db67f410c06131c3bc00a97c",
-            url = "https://github.com/google/wycheproof/archive/d8ed1ba95ac4c551db67f410c06131c3bc00a97c.zip",
-            sha256 = "eb1d558071acf1aa6d677d7f1cabec2328d1cf8381496c17185bd92b52ce7545",
-        )
diff --git a/go/tink_go_deps_init.bzl b/go/tink_go_deps_init.bzl
deleted file mode 100644
index f22bc55..0000000
--- a/go/tink_go_deps_init.bzl
+++ /dev/null
@@ -1,20 +0,0 @@
-"""
-Initialization of dependencies of Go Tink.
-"""
-
-load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
-load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
-load("@com_github_google_tink_go//:deps.bzl", "go_dependencies")
-
-# Deprecated.
-def tink_go_deps_init():
-    """ Deprecated function to initializes dependencies of Go Tink.
-
-    This should not be used anymore. Instead, each workspace should generate
-    its own go_dependencies() using gazelle. And go_rules_dependencies(),
-    go_register_toolchains() and gazelle_dependencies() should be called in the WORKSPACE.
-    """
-    go_rules_dependencies()
-    go_register_toolchains()
-    gazelle_dependencies()
-    go_dependencies()
diff --git a/java_src/.bazelversion b/java_src/.bazelversion
index ac14c3d..09b254e 100644
--- a/java_src/.bazelversion
+++ b/java_src/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/java_src/BUILD.bazel b/java_src/BUILD.bazel
index 72803ee..e7b143d 100644
--- a/java_src/BUILD.bazel
+++ b/java_src/BUILD.bazel
@@ -1,7 +1,6 @@
 ## This file is created using "create_main_build_file.py".
 
 load("//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-load("//tools:check_deps.bzl", "check_deps")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -29,6 +28,7 @@
         "//src/main/java/com/google/crypto/tink:catalogue",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
         "//src/main/java/com/google/crypto/tink:config",
+        "//src/main/java/com/google/crypto/tink:configuration",
         "//src/main/java/com/google/crypto/tink:crypto_format",
         "//src/main/java/com/google/crypto/tink:deterministic_aead",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt",
@@ -54,6 +54,7 @@
         "//src/main/java/com/google/crypto/tink:pem_key_type",
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink:private_key",
         "//src/main/java/com/google/crypto/tink:private_key_manager",
         "//src/main/java/com/google/crypto/tink:private_key_manager_impl",
         "//src/main/java/com/google/crypto/tink:privileged_registry",
@@ -63,22 +64,46 @@
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink:secret_key_access",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink:util",
         "//src/main/java/com/google/crypto/tink:version",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aead_factory",
+        "//src/main/java/com/google/crypto/tink/aead:aead_key",
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/aead:aead_parameters",
         "//src/main/java/com/google/crypto/tink/aead:aead_wrapper",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_proto_serialization",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_key",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key",
         "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_proto_serialization",
         "//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key",
         "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_proto_serialization",
         "//src/main/java/com/google/crypto/tink/aead/internal:cha_cha20_util",
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20",
@@ -96,13 +121,23 @@
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_status",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_key",
         "//src/main/java/com/google/crypto/tink/daead:aes_siv_key_manager",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_proto_serialization",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_factory",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_parameters",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_wrapper",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
         "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_private_key_manager",
         "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_public_key_manager",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_private_key",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_public_key",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_decrypt_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_decrypt_factory",
@@ -111,6 +146,9 @@
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_encrypt_factory",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_encrypt_wrapper",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_key_templates",
+        "//src/main/java/com/google/crypto/tink/hybrid:hybrid_parameters",
+        "//src/main/java/com/google/crypto/tink/hybrid:hybrid_private_key",
+        "//src/main/java/com/google/crypto/tink/hybrid:hybrid_public_key",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_util",
         "//src/main/java/com/google/crypto/tink/hybrid:registry_ecies_aead_hkdf_dem_helper",
         "//src/main/java/com/google/crypto/tink/hybrid/internal:aes_gcm_hpke_aead",
@@ -137,7 +175,15 @@
         "//src/main/java/com/google/crypto/tink/hybrid/subtle:rsa_kem",
         "//src/main/java/com/google/crypto/tink/hybrid/subtle:rsa_kem_hybrid_decrypt",
         "//src/main/java/com/google/crypto/tink/hybrid/subtle:rsa_kem_hybrid_encrypt",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
         "//src/main/java/com/google/crypto/tink/internal:build_dispatched_code",
+        "//src/main/java/com/google/crypto/tink/internal:curve25519",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter",
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
+        "//src/main/java/com/google/crypto/tink/internal:internal_configuration",
+        "//src/main/java/com/google/crypto/tink/internal:json_parser",
         "//src/main/java/com/google/crypto/tink/internal:key_parser",
         "//src/main/java/com/google/crypto/tink/internal:key_serializer",
         "//src/main/java/com/google/crypto/tink/internal:key_status_type_proto_converter",
@@ -147,27 +193,40 @@
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters",
         "//src/main/java/com/google/crypto/tink/internal:monitoring_util",
         "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
         "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
         "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_registry",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:registry_configuration",
         "//src/main/java/com/google/crypto/tink/internal:serialization",
         "//src/main/java/com/google/crypto/tink/internal:serialization_registry",
         "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
         "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/jwt:json_util",
         "//src/main/java/com/google/crypto/tink/jwt:jwk_set_converter",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_public_key",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_format",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key_manager",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_proto_serialization",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_invalid_exception",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_internal",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_parameters",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_wrapper",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_names",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_sign",
@@ -181,9 +240,22 @@
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pss_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pss_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_private_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_public_key",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_validator",
         "//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key_templates",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver_wrapper",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_key",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key_manager",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
@@ -191,51 +263,103 @@
         "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
         "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
         "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_wrapper",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
         "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_proto_serialization",
         "//src/main/java/com/google/crypto/tink/mac:mac_config",
         "//src/main/java/com/google/crypto/tink/mac:mac_factory",
         "//src/main/java/com/google/crypto/tink/mac:mac_key",
         "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
         "//src/main/java/com/google/crypto/tink/mac:mac_parameters",
         "//src/main/java/com/google/crypto/tink/mac:mac_wrapper",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
         "//src/main/java/com/google/crypto/tink/mac/internal:aes_util",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_computation",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_impl",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_verification",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_computation",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_impl",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_verification",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key",
         "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key",
         "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key",
         "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/prf:predefined_prf_parameters",
         "//src/main/java/com/google/crypto/tink/prf:prf_config",
+        "//src/main/java/com/google/crypto/tink/prf:prf_key",
         "//src/main/java/com/google/crypto/tink/prf:prf_key_templates",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "//src/main/java/com/google/crypto/tink/prf:prf_set_wrapper",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_public_key",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_verify_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:predefined_signature_parameters",
         "//src/main/java/com/google/crypto/tink/signature:public_key_sign_config",
         "//src/main/java/com/google/crypto/tink/signature:public_key_sign_factory",
         "//src/main/java/com/google/crypto/tink/signature:public_key_sign_wrapper",
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_config",
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_factory",
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_wrapper",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_public_key",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_public_key",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:signature_config",
         "//src/main/java/com/google/crypto/tink/signature:signature_key_templates",
+        "//src/main/java/com/google/crypto/tink/signature:signature_parameters",
         "//src/main/java/com/google/crypto/tink/signature:signature_pem_keyset_reader",
+        "//src/main/java/com/google/crypto/tink/signature:signature_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:signature_public_key",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key_manager",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key_manager",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_proto_serialization",
         "//src/main/java/com/google/crypto/tink/streamingaead:input_stream_decrypter",
+        "//src/main/java/com/google/crypto/tink/streamingaead:predefined_streaming_aead_parameters",
         "//src/main/java/com/google/crypto/tink/streamingaead:readable_byte_channel_decrypter",
         "//src/main/java/com/google/crypto/tink/streamingaead:seekable_byte_channel_decrypter",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_factory",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_helper",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_key",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_parameters",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_util",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_wrapper",
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming",
@@ -247,10 +371,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:base64",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
         "//src/main/java/com/google/crypto/tink/subtle:cha_cha20",
-        "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_base",
         "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_poly1305",
-        "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_poly1305_base",
-        "//src/main/java/com/google/crypto/tink/subtle:curve25519",
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_dem_helper",
@@ -258,21 +379,18 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_encrypt",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_hkdf_recipient_kem",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_hkdf_sender_kem",
-        "//src/main/java/com/google/crypto/tink/subtle:ed25519_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_sign",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:encrypt_then_authenticate",
         "//src/main/java/com/google/crypto/tink/subtle:engine_wrapper",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
-        "//src/main/java/com/google/crypto/tink/subtle:field25519",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:hkdf",
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher",
         "//src/main/java/com/google/crypto/tink/subtle:kwp",
         "//src/main/java/com/google/crypto/tink/subtle:nonce_based_streaming_aead_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:pem_key_type",
-        "//src/main/java/com/google/crypto/tink/subtle:poly1305",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
@@ -302,6 +420,7 @@
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key",
         "//src/main/java/com/google/crypto/tink/util:bytes",
         "//src/main/java/com/google/crypto/tink/util:keys_downloader",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes",
     ],
 )
@@ -329,6 +448,7 @@
         "//src/main/java/com/google/crypto/tink:catalogue-android",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle-android",
         "//src/main/java/com/google/crypto/tink:config-android",
+        "//src/main/java/com/google/crypto/tink:configuration-android",
         "//src/main/java/com/google/crypto/tink:crypto_format-android",
         "//src/main/java/com/google/crypto/tink:deterministic_aead-android",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt-android",
@@ -354,6 +474,7 @@
         "//src/main/java/com/google/crypto/tink:pem_key_type-android",
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
+        "//src/main/java/com/google/crypto/tink:private_key-android",
         "//src/main/java/com/google/crypto/tink:private_key_manager-android",
         "//src/main/java/com/google/crypto/tink:private_key_manager_impl-android",
         "//src/main/java/com/google/crypto/tink:privileged_registry-android",
@@ -363,22 +484,46 @@
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
         "//src/main/java/com/google/crypto/tink:secret_key_access-android",
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format-android",
         "//src/main/java/com/google/crypto/tink:util-android",
         "//src/main/java/com/google/crypto/tink:version-android",
         "//src/main/java/com/google/crypto/tink/aead:aead_config-android",
         "//src/main/java/com/google/crypto/tink/aead:aead_factory-android",
+        "//src/main/java/com/google/crypto/tink/aead:aead_key-android",
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/aead:aead_parameters-android",
         "//src/main/java/com/google/crypto/tink/aead:aead_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key-android",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_key-android",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key-android",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key-android",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key-android",
         "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager-android",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead-android",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key-android",
         "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters-android",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/aead/internal:cha_cha20_util-android",
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_aes_gcm_jce-android",
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20-android",
@@ -396,13 +541,23 @@
         "//src/main/java/com/google/crypto/tink/config:tink_fips-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_status-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_key-android",
         "//src/main/java/com/google/crypto/tink/daead:aes_siv_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters-android",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config-android",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_factory-android",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key-android",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_parameters-android",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters-android",
         "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_private_key_manager-android",
         "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_public_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_private_key-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_public_key-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_decrypt_config-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_decrypt_factory-android",
@@ -411,6 +566,9 @@
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_encrypt_factory-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_encrypt_wrapper-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hybrid_parameters-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hybrid_private_key-android",
+        "//src/main/java/com/google/crypto/tink/hybrid:hybrid_public_key-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_util-android",
         "//src/main/java/com/google/crypto/tink/hybrid:registry_ecies_aead_hkdf_dem_helper-android",
         "//src/main/java/com/google/crypto/tink/hybrid/internal:aes_gcm_hpke_aead-android",
@@ -442,6 +600,14 @@
         "//src/main/java/com/google/crypto/tink/integration/android:android_keystore_kms_client",
         "//src/main/java/com/google/crypto/tink/integration/android:shared_pref_keyset_reader",
         "//src/main/java/com/google/crypto/tink/integration/android:shared_pref_keyset_writer",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:curve25519-android",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter-android",
+        "//src/main/java/com/google/crypto/tink/internal:field25519-android",
+        "//src/main/java/com/google/crypto/tink/internal:internal_configuration-android",
+        "//src/main/java/com/google/crypto/tink/internal:json_parser-android",
         "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
         "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
         "//src/main/java/com/google/crypto/tink/internal:key_status_type_proto_converter-android",
@@ -451,27 +617,40 @@
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters-android",
         "//src/main/java/com/google/crypto/tink/internal:monitoring_util-android",
         "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
         "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:registry_configuration-android",
         "//src/main/java/com/google/crypto/tink/internal:serialization-android",
         "//src/main/java/com/google/crypto/tink/internal:serialization_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
         "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/jwt:json_util-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwk_set_converter-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_private_key-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_public_key-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_sign_key_manager-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_verify_key_manager-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_format-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_invalid_exception-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_internal-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_key-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_parameters-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_wrapper-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_names-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_sign-android",
@@ -485,9 +664,22 @@
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pss_sign_key_manager-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pss_verify_key_manager-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_parameters-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_signature_public_key-android",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_validator-android",
         "//src/main/java/com/google/crypto/tink/jwt:raw_jwt-android",
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_parameters-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_key-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_parameters-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager-android",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key-android",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key_manager-android",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters-android",
@@ -495,51 +687,103 @@
         "//src/main/java/com/google/crypto/tink/mac:chunked_mac-android",
         "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation-android",
         "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key-android",
         "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_config-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_factory-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_key-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_key_templates-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_parameters-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters-android",
         "//src/main/java/com/google/crypto/tink/mac/internal:aes_util-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_impl-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_verification-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_impl-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_verification-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info-android",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key-android",
         "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key-android",
         "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key-android",
         "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/prf:predefined_prf_parameters-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_config-android",
+        "//src/main/java/com/google/crypto/tink/prf:prf_key-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_set-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_set_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters-android",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_private_key-android",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_public_key-android",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager-android",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_verify_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters-android",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key-android",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key-android",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/signature:predefined_signature_parameters-android",
         "//src/main/java/com/google/crypto/tink/signature:public_key_sign_config-android",
         "//src/main/java/com/google/crypto/tink/signature:public_key_sign_factory-android",
         "//src/main/java/com/google/crypto/tink/signature:public_key_sign_wrapper-android",
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_config-android",
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_factory-android",
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_private_key-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_public_key-android",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_sign_key_manager-android",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_verify_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_private_key-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_public_key-android",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_sign_key_manager-android",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_verify_key_manager-android",
         "//src/main/java/com/google/crypto/tink/signature:signature_config-android",
         "//src/main/java/com/google/crypto/tink/signature:signature_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/signature:signature_parameters-android",
         "//src/main/java/com/google/crypto/tink/signature:signature_pem_keyset_reader-android",
+        "//src/main/java/com/google/crypto/tink/signature:signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink/signature:signature_public_key-android",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_parameters-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_proto_serialization-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_parameters-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_proto_serialization-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:input_stream_decrypter-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:predefined_streaming_aead_parameters-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:readable_byte_channel_decrypter-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:seekable_byte_channel_decrypter-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_factory-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_helper-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_key-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_parameters-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_util-android",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_wrapper-android",
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming-android",
@@ -551,10 +795,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:base64-android",
         "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
         "//src/main/java/com/google/crypto/tink/subtle:cha_cha20-android",
-        "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_base-android",
         "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_poly1305-android",
-        "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_poly1305_base-android",
-        "//src/main/java/com/google/crypto/tink/subtle:curve25519-android",
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_dem_helper-android",
@@ -562,21 +803,18 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_encrypt-android",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_hkdf_recipient_kem-android",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_hkdf_sender_kem-android",
-        "//src/main/java/com/google/crypto/tink/subtle:ed25519_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_sign-android",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify-android",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:encrypt_then_authenticate-android",
         "//src/main/java/com/google/crypto/tink/subtle:engine_wrapper-android",
         "//src/main/java/com/google/crypto/tink/subtle:enums-android",
-        "//src/main/java/com/google/crypto/tink/subtle:field25519-android",
         "//src/main/java/com/google/crypto/tink/subtle:hex-android",
         "//src/main/java/com/google/crypto/tink/subtle:hkdf-android",
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher-android",
         "//src/main/java/com/google/crypto/tink/subtle:kwp-android",
         "//src/main/java/com/google/crypto/tink/subtle:nonce_based_streaming_aead_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:pem_key_type-android",
-        "//src/main/java/com/google/crypto/tink/subtle:poly1305-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac-android",
@@ -604,7 +842,7 @@
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:internal_key_handle-android",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key-android",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
-        "//src/main/java/com/google/crypto/tink/util:keys_downloader-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
         "//src_android/main/java/com/google/crypto/tink/internal:build_dispatched_code",
     ],
@@ -617,7 +855,7 @@
         "Automatic-Module-Name: com.google.crypto.tink.integration.awskms",
     ],
     root_packages = [
-        "com.google.crypto.tink",
+        "com.google.crypto.tink.integration.awskms",
     ],
     deps = [
         "//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_aead",
@@ -632,18 +870,10 @@
         "Automatic-Module-Name: com.google.crypto.tink.integration.gcpkms",
     ],
     root_packages = [
-        "com.google.crypto.tink",
+        "com.google.crypto.tink.integration.gcpkms",
     ],
     deps = [
         "//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_aead",
         "//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
     ],
 )
-
-# Check that tink-android depends on protobuf-lite, not the full version.
-check_deps(
-    name = "tink-android-dep-checks",
-    disallowed_deps = ["@com_google_protobuf//java/core:core"],
-    required_deps = ["@com_google_protobuf//java/lite:lite"],
-    deps = [":tink-android-unshaded"],
-)
diff --git a/java_src/WORKSPACE b/java_src/WORKSPACE
index 990691d..950cc7b 100644
--- a/java_src/WORKSPACE
+++ b/java_src/WORKSPACE
@@ -8,10 +8,6 @@
 
 tink_java_deps_init()
 
-load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
-
-rbe_autoconfig(name = "rbe_default")
-
 load("@rules_jvm_external//:defs.bzl", "maven_install")
 
 maven_install(
diff --git a/java_src/examples/.bazelversion b/java_src/examples/.bazelversion
index ac14c3d..09b254e 100644
--- a/java_src/examples/.bazelversion
+++ b/java_src/examples/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/java_src/examples/WORKSPACE b/java_src/examples/WORKSPACE
index 82e8c0e..41909d7 100644
--- a/java_src/examples/WORKSPACE
+++ b/java_src/examples/WORKSPACE
@@ -41,7 +41,7 @@
 maven_install(
     artifacts = TINK_MAVEN_ARTIFACTS + [
       "args4j:args4j:2.33",
-      "com.google.cloud:google-cloud-storage:2.3.0",
+      "com.google.cloud:google-cloud-storage:2.17.2",
     ],
     repositories = [
         "https://maven.google.com",
diff --git a/java_src/examples/aead/AeadExample.java b/java_src/examples/aead/AeadExample.java
index 84281e5..bca79b1 100644
--- a/java_src/examples/aead/AeadExample.java
+++ b/java_src/examples/aead/AeadExample.java
@@ -17,15 +17,13 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.aead.AeadConfig;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.nio.file.Files;
-import java.security.GeneralSecurityException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 /**
  * A command-line utility for encrypting small files with AEAD.
@@ -54,9 +52,9 @@
       System.exit(1);
     }
     String mode = args[0];
-    File keyFile = new File(args[1]);
-    File inputFile = new File(args[2]);
-    File outputFile = new File(args[3]);
+    Path keyFile = Paths.get(args[1]);
+    Path inputFile = Paths.get(args[2]);
+    Path outputFile = Paths.get(args[3]);
     byte[] associatedData = new byte[0];
     if (args.length == 5) {
       associatedData = args[4].getBytes(UTF_8);
@@ -65,42 +63,26 @@
     AeadConfig.register();
 
     // Read the keyset into a KeysetHandle.
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());
 
     // Get the primitive.
-    Aead aead = null;
-    try {
-      aead = handle.getPrimitive(Aead.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
+    Aead aead = handle.getPrimitive(Aead.class);
 
     // Use the primitive to encrypt/decrypt files.
     if (MODE_ENCRYPT.equals(mode)) {
-      byte[] plaintext = Files.readAllBytes(inputFile.toPath());
+      byte[] plaintext = Files.readAllBytes(inputFile);
       byte[] ciphertext = aead.encrypt(plaintext, associatedData);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(ciphertext);
-      }
+      Files.write(outputFile, ciphertext);
     } else if (MODE_DECRYPT.equals(mode)) {
-      byte[] ciphertext = Files.readAllBytes(inputFile.toPath());
+      byte[] ciphertext = Files.readAllBytes(inputFile);
       byte[] plaintext = aead.decrypt(ciphertext, associatedData);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(plaintext);
-      }
+      Files.write(outputFile, plaintext);
     } else {
       System.err.println("The first argument must be either encrypt or decrypt, got: " + mode);
       System.exit(1);
     }
-
-    System.exit(0);
   }
 
   private AeadExample() {}
diff --git a/java_src/examples/aead/BUILD.bazel b/java_src/examples/aead/BUILD.bazel
index 6d57106..a8fe408 100644
--- a/java_src/examples/aead/BUILD.bazel
+++ b/java_src/examples/aead/BUILD.bazel
@@ -10,9 +10,9 @@
     main_class = "aead.AeadExample",
     deps = [
         "@tink_java//src/main/java/com/google/crypto/tink:aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
     ],
 )
diff --git a/examples/android/helloworld/README.md b/java_src/examples/android/helloworld/README.md
similarity index 100%
rename from examples/android/helloworld/README.md
rename to java_src/examples/android/helloworld/README.md
diff --git a/java_src/examples/android/helloworld/app/build.gradle b/java_src/examples/android/helloworld/app/build.gradle
new file mode 100644
index 0000000..82ad967
--- /dev/null
+++ b/java_src/examples/android/helloworld/app/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 29
+    defaultConfig {
+        applicationId "com.helloworld"
+        minSdkVersion 23
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+   compileOptions {
+        sourceCompatibility 1.8
+        targetCompatibility 1.8
+   }
+   lintOptions {
+        abortOnError false
+   }
+}
+
+apply from: "maven_${mavenLocation}.gradle"
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
+        exclude group: 'com.android.support', module: 'support-annotations'
+        // This is already included in protobuf-lite that Tink depends on.
+        exclude group: 'com.google.code.findbugs'
+    })
+    implementation 'com.android.support:appcompat-v7:26.+'
+    implementation 'com.android.support:design:26.+'
+    testImplementation 'junit:junit:4.12'
+
+    // Tink HEAD-SNAPSHOT for Android.
+    // In production apps, please use a named version, e.g., 1.4.0.
+    implementation 'com.google.crypto.tink:tink-android:HEAD-SNAPSHOT'
+
+    // An artificial dependency to test whether Tink can co-exist with other
+    // protobuf dependencies. Please remove from production apps.
+    implementation 'com.google.protobuf:protobuf-lite:3.0.1'
+}
diff --git a/examples/android/helloworld/app/maven_local.gradle b/java_src/examples/android/helloworld/app/maven_local.gradle
similarity index 100%
rename from examples/android/helloworld/app/maven_local.gradle
rename to java_src/examples/android/helloworld/app/maven_local.gradle
diff --git a/examples/android/helloworld/app/maven_snapshot.gradle b/java_src/examples/android/helloworld/app/maven_snapshot.gradle
similarity index 100%
rename from examples/android/helloworld/app/maven_snapshot.gradle
rename to java_src/examples/android/helloworld/app/maven_snapshot.gradle
diff --git a/examples/android/helloworld/app/src/main/AndroidManifest.xml b/java_src/examples/android/helloworld/app/src/main/AndroidManifest.xml
similarity index 100%
rename from examples/android/helloworld/app/src/main/AndroidManifest.xml
rename to java_src/examples/android/helloworld/app/src/main/AndroidManifest.xml
diff --git a/examples/android/helloworld/app/src/main/java/com/helloworld/MainActivity.java b/java_src/examples/android/helloworld/app/src/main/java/com/helloworld/MainActivity.java
similarity index 100%
rename from examples/android/helloworld/app/src/main/java/com/helloworld/MainActivity.java
rename to java_src/examples/android/helloworld/app/src/main/java/com/helloworld/MainActivity.java
diff --git a/java_src/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java b/java_src/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java
new file mode 100644
index 0000000..56a5eb5
--- /dev/null
+++ b/java_src/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java
@@ -0,0 +1,55 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.helloworld;
+
+import android.app.Application;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.integration.android.AndroidKeysetManager;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/** A custom application that initializes the Tink runtime at application startup. */
+public class TinkApplication extends Application {
+  private static final String TAG = TinkApplication.class.toString();
+  private static final String PREF_FILE_NAME = "hello_world_pref";
+  private static final String TINK_KEYSET_NAME = "hello_world_keyset";
+  private static final String MASTER_KEY_URI = "android-keystore://hello_world_master_key";
+  public Aead aead;
+
+  @Override
+  public final void onCreate() {
+    super.onCreate();
+    try {
+      TinkConfig.register();
+      aead = getOrGenerateNewKeysetHandle().getPrimitive(Aead.class);
+    } catch (GeneralSecurityException | IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private KeysetHandle getOrGenerateNewKeysetHandle() throws IOException, GeneralSecurityException {
+    return new AndroidKeysetManager.Builder()
+        .withSharedPref(getApplicationContext(), TINK_KEYSET_NAME, PREF_FILE_NAME)
+        .withKeyTemplate(AeadKeyTemplates.AES256_GCM)
+        .withMasterKeyUri(MASTER_KEY_URI)
+        .build()
+        .getKeysetHandle();
+  }
+}
diff --git a/examples/android/helloworld/app/src/main/res/layout/activity_main.xml b/java_src/examples/android/helloworld/app/src/main/res/layout/activity_main.xml
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/layout/activity_main.xml
rename to java_src/examples/android/helloworld/app/src/main/res/layout/activity_main.xml
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/java_src/examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
rename to java_src/examples/android/helloworld/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/examples/android/helloworld/app/src/main/res/values/colors.xml b/java_src/examples/android/helloworld/app/src/main/res/values/colors.xml
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/values/colors.xml
rename to java_src/examples/android/helloworld/app/src/main/res/values/colors.xml
diff --git a/examples/android/helloworld/app/src/main/res/values/dimens.xml b/java_src/examples/android/helloworld/app/src/main/res/values/dimens.xml
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/values/dimens.xml
rename to java_src/examples/android/helloworld/app/src/main/res/values/dimens.xml
diff --git a/examples/android/helloworld/app/src/main/res/values/strings.xml b/java_src/examples/android/helloworld/app/src/main/res/values/strings.xml
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/values/strings.xml
rename to java_src/examples/android/helloworld/app/src/main/res/values/strings.xml
diff --git a/examples/android/helloworld/app/src/main/res/values/styles.xml b/java_src/examples/android/helloworld/app/src/main/res/values/styles.xml
similarity index 100%
rename from examples/android/helloworld/app/src/main/res/values/styles.xml
rename to java_src/examples/android/helloworld/app/src/main/res/values/styles.xml
diff --git a/java_src/examples/android/helloworld/build.gradle b/java_src/examples/android/helloworld/build.gradle
new file mode 100644
index 0000000..775d290
--- /dev/null
+++ b/java_src/examples/android/helloworld/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+    dependencies {
+        // See AGP compatibility with Gradle here:
+        // https://developer.android.com/studio/releases/gradle-plugin#updating-gradle.
+        classpath 'com.android.tools.build:gradle:4.2.2'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/java_src/examples/android/helloworld/gradle.properties b/java_src/examples/android/helloworld/gradle.properties
new file mode 100644
index 0000000..8faa474
--- /dev/null
+++ b/java_src/examples/android/helloworld/gradle.properties
@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# Preferred source for Maven packages:
+#   "local": Local Maven repository (i.e. for testing without publishing)
+#   "snapshot": Maven Central snapshot repository (i.e. for testing published
+#       snapshots)
+mavenLocation=snapshot
+android.useAndroidX=true
diff --git a/java_src/examples/android/helloworld/gradle/wrapper/gradle-wrapper.jar b/java_src/examples/android/helloworld/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..943f0cb
--- /dev/null
+++ b/java_src/examples/android/helloworld/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/java_src/examples/android/helloworld/gradle/wrapper/gradle-wrapper.properties b/java_src/examples/android/helloworld/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..5083229
--- /dev/null
+++ b/java_src/examples/android/helloworld/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/java_src/examples/android/helloworld/gradlew b/java_src/examples/android/helloworld/gradlew
new file mode 100755
index 0000000..65dcd68
--- /dev/null
+++ b/java_src/examples/android/helloworld/gradlew
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/java_src/examples/android/helloworld/gradlew.bat b/java_src/examples/android/helloworld/gradlew.bat
new file mode 100755
index 0000000..93e3f59
--- /dev/null
+++ b/java_src/examples/android/helloworld/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/android/helloworld/settings.gradle b/java_src/examples/android/helloworld/settings.gradle
similarity index 100%
rename from examples/android/helloworld/settings.gradle
rename to java_src/examples/android/helloworld/settings.gradle
diff --git a/java_src/examples/cleartextkeyset/BUILD.bazel b/java_src/examples/cleartextkeyset/BUILD.bazel
index 32797f1..1e9f245 100644
--- a/java_src/examples/cleartextkeyset/BUILD.bazel
+++ b/java_src/examples/cleartextkeyset/BUILD.bazel
@@ -8,12 +8,12 @@
     main_class = "cleartextkeyset.CleartextKeysetExample",
     deps = [
         "@tink_java//src/main/java/com/google/crypto/tink:aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_writer",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
     ],
 )
 
diff --git a/java_src/examples/cleartextkeyset/CleartextKeysetExample.java b/java_src/examples/cleartextkeyset/CleartextKeysetExample.java
index a36a2ab..03c4531 100644
--- a/java_src/examples/cleartextkeyset/CleartextKeysetExample.java
+++ b/java_src/examples/cleartextkeyset/CleartextKeysetExample.java
@@ -14,24 +14,23 @@
 // [START cleartext-keyset-example]
 package cleartextkeyset;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
-import com.google.crypto.tink.JsonKeysetWriter;
-import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.aead.AeadConfig;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.GeneralSecurityException;
 
 /**
- * A command-line utility for generating, storing and using cleartext AES128_GCM keysets.
+ * A command-line utility for generating, storing and using AES128_GCM keysets.
  *
- * <h1>WARNING: It loads cleartext keys from disk - this is not recommended!
+ * <h1>WARNING: Loading a Keyset from disk is often a security problem -- hence this needs {@code
+ * InsecureSecretKeyAccess.get()}.
  *
  * <p>It requires the following arguments:
  *
@@ -62,58 +61,44 @@
       System.err.print("The first argument should be either encrypt, decrypt or generate");
       System.exit(1);
     }
-    File keyFile = new File(args[1]);
+    Path keyFile = Paths.get(args[1]);
 
     // Initialise Tink: register all AEAD key types with the Tink runtime
     AeadConfig.register();
 
     if (MODE_GENERATE.equals(mode)) {
       // [START generate-a-new-keyset]
-      KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("AES128_GCM"));
+      KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
       // [END generate-a-new-keyset]
 
       // [START store-a-cleartext-keyset]
-      CleartextKeysetHandle.write(handle, JsonKeysetWriter.withFile(keyFile));
+      String serializedKeyset =
+          TinkJsonProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get());
+      Files.write(keyFile, serializedKeyset.getBytes(UTF_8));
       // [END store-a-cleartext-keyset]
-      System.exit(0);
+      return;
     }
 
     // Use the primitive to encrypt/decrypt files
 
-    // Read the cleartext keyset
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Error reading key: " + ex);
-      System.exit(1);
-    }
+    // Read the keyset from disk
+    String serializedKeyset = new String(Files.readAllBytes(keyFile), UTF_8);
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
 
     // Get the primitive
-    Aead aead = null;
-    try {
-      aead = handle.getPrimitive(Aead.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Error creating primitive: %s " + ex);
-      System.exit(1);
-    }
+    Aead aead = handle.getPrimitive(Aead.class);
 
     byte[] input = Files.readAllBytes(Paths.get(args[2]));
-    File outputFile = new File(args[3]);
+    Path outputFile = Paths.get(args[3]);
 
     if (MODE_ENCRYPT.equals(mode)) {
       byte[] ciphertext = aead.encrypt(input, EMPTY_ASSOCIATED_DATA);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(ciphertext);
-      }
+      Files.write(outputFile, ciphertext);
     } else if (MODE_DECRYPT.equals(mode)) {
       byte[] plaintext = aead.decrypt(input, EMPTY_ASSOCIATED_DATA);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(plaintext);
-      }
+      Files.write(outputFile, plaintext);
     }
-
-    System.exit(0);
   }
 
   private CleartextKeysetExample() {}
diff --git a/java_src/examples/deterministicaead/BUILD.bazel b/java_src/examples/deterministicaead/BUILD.bazel
index 00107f3..77804d7 100644
--- a/java_src/examples/deterministicaead/BUILD.bazel
+++ b/java_src/examples/deterministicaead/BUILD.bazel
@@ -9,10 +9,10 @@
     srcs = ["DeterministicAeadExample.java"],
     main_class = "deterministicaead.DeterministicAeadExample",
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
         "@tink_java//src/main/java/com/google/crypto/tink:deterministic_aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
     ],
 )
diff --git a/java_src/examples/deterministicaead/DeterministicAeadExample.java b/java_src/examples/deterministicaead/DeterministicAeadExample.java
index 588386d..d479db0 100644
--- a/java_src/examples/deterministicaead/DeterministicAeadExample.java
+++ b/java_src/examples/deterministicaead/DeterministicAeadExample.java
@@ -16,16 +16,14 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.DeterministicAead;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.nio.file.Files;
-import java.security.GeneralSecurityException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 /**
  * A command-line utility for encrypting small files with Deterministic AEAD.
@@ -54,9 +52,9 @@
       System.exit(1);
     }
     String mode = args[0];
-    File keyFile = new File(args[1]);
-    File inputFile = new File(args[2]);
-    File outputFile = new File(args[3]);
+    Path keyFile = Paths.get(args[1]);
+    Path inputFile = Paths.get(args[2]);
+    Path outputFile = Paths.get(args[3]);
     byte[] associatedData = new byte[0];
     if (args.length == 5) {
       associatedData = args[4].getBytes(UTF_8);
@@ -66,36 +64,22 @@
     DeterministicAeadConfig.register();
 
     // Read the keyset into a KeysetHandle
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());
 
     // Get the primitive
-    DeterministicAead daead = null;
-    try {
-      daead = handle.getPrimitive(DeterministicAead.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
+    DeterministicAead daead = handle.getPrimitive(DeterministicAead.class);
 
     // Use the primitive to encrypt/decrypt files.
     if (MODE_ENCRYPT.equals(mode)) {
-      byte[] plaintext = Files.readAllBytes(inputFile.toPath());
+      byte[] plaintext = Files.readAllBytes(inputFile);
       byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(ciphertext);
-      }
+      Files.write(outputFile, ciphertext);
     } else if (MODE_DECRYPT.equals(mode)) {
-      byte[] ciphertext = Files.readAllBytes(inputFile.toPath());
+      byte[] ciphertext = Files.readAllBytes(inputFile);
       byte[] plaintext = daead.decryptDeterministically(ciphertext, associatedData);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(plaintext);
-      }
+      Files.write(outputFile, plaintext);
     } else {
       System.err.println("The first argument must be either encrypt or decrypt, got: " + mode);
       System.exit(1);
diff --git a/java_src/examples/encryptedkeyset/BUILD.bazel b/java_src/examples/encryptedkeyset/BUILD.bazel
index 9dcd3ec..516afd6 100644
--- a/java_src/examples/encryptedkeyset/BUILD.bazel
+++ b/java_src/examples/encryptedkeyset/BUILD.bazel
@@ -8,12 +8,12 @@
     main_class = "encryptedkeyset.EncryptedKeysetExample",
     deps = [
         "@tink_java//src/main/java/com/google/crypto/tink:aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_writer",
         "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
         "@tink_java//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "@tink_java//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
     ],
 )
diff --git a/java_src/examples/encryptedkeyset/EncryptedKeysetExample.java b/java_src/examples/encryptedkeyset/EncryptedKeysetExample.java
index d6e704c..68ed792 100644
--- a/java_src/examples/encryptedkeyset/EncryptedKeysetExample.java
+++ b/java_src/examples/encryptedkeyset/EncryptedKeysetExample.java
@@ -14,20 +14,18 @@
 // [START encrypted-keyset-example]
 package encryptedkeyset;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.JsonKeysetReader;
-import com.google.crypto.tink.JsonKeysetWriter;
-import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.aead.KmsAeadKeyManager;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.GeneralSecurityException;
 import java.util.Optional;
 
 /**
@@ -64,7 +62,7 @@
       System.err.print("The first argument should be either encrypt, decrypt or generate");
       System.exit(1);
     }
-    File keyFile = new File(args[1]);
+    Path keyFile = Paths.get(args[1]);
     String kekUri = args[2];
     String gcpCredentialFilename = args[3];
 
@@ -72,71 +70,49 @@
     AeadConfig.register();
 
     // Read the GCP credentials and set up client
-    try {
-      GcpKmsClient.register(Optional.of(kekUri), Optional.of(gcpCredentialFilename));
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Error initializing GCP client: " + ex);
-      System.exit(1);
-    }
+    GcpKmsClient.register(Optional.of(kekUri), Optional.of(gcpCredentialFilename));
 
     // From the key-encryption key (KEK) URI, create a remote AEAD primitive for encrypting Tink
     // keysets.
-    Aead kekAead = null;
-    try {
-      KeysetHandle handle = KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(kekUri));
-      kekAead = handle.getPrimitive(Aead.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Error creating primitive: %s " + ex);
-      System.exit(1);
-    }
+    KeysetHandle kekHandle = KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(kekUri));
+    Aead kekAead = kekHandle.getPrimitive(Aead.class);
 
     if (MODE_GENERATE.equals(mode)) {
       // [START generate-a-new-keyset]
-      KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("AES128_GCM"));
+      KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
       // [END generate-a-new-keyset]
 
       // [START encrypt-a-keyset]
-      handle.write(JsonKeysetWriter.withFile(keyFile), kekAead);
+      String serializedEncryptedKeyset =
+          TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+              handle, kekAead, EMPTY_ASSOCIATED_DATA);
+      Files.write(keyFile, serializedEncryptedKeyset.getBytes(UTF_8));
       // [END encrypt-a-keyset]
-      System.exit(0);
+      return;
     }
 
     // Use the primitive to encrypt/decrypt files
 
     // Read the encrypted keyset
-    KeysetHandle handle = null;
-    try {
-      handle = KeysetHandle.read(JsonKeysetReader.withFile(keyFile), kekAead);
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Error reading key: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), kekAead, EMPTY_ASSOCIATED_DATA);
 
     // Get the primitive
-    Aead aead = null;
-    try {
-      aead = handle.getPrimitive(Aead.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Error creating primitive: %s " + ex);
-      System.exit(1);
-    }
+    Aead aead = handle.getPrimitive(Aead.class);
 
-    byte[] input = Files.readAllBytes(Paths.get(args[4]));
-    File outputFile = new File(args[5]);
+    Path inputFile = Paths.get(args[4]);
+    Path outputFile = Paths.get(args[5]);
 
     if (MODE_ENCRYPT.equals(mode)) {
-      byte[] ciphertext = aead.encrypt(input, EMPTY_ASSOCIATED_DATA);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(ciphertext);
-      }
+      byte[] plaintext = Files.readAllBytes(inputFile);
+      byte[] ciphertext = aead.encrypt(plaintext, EMPTY_ASSOCIATED_DATA);
+      Files.write(outputFile, ciphertext);
     } else if (MODE_DECRYPT.equals(mode)) {
-      byte[] plaintext = aead.decrypt(input, EMPTY_ASSOCIATED_DATA);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(plaintext);
-      }
+      byte[] ciphertext = Files.readAllBytes(inputFile);
+      byte[] plaintext = aead.decrypt(ciphertext, EMPTY_ASSOCIATED_DATA);
+      Files.write(outputFile, plaintext);
     }
-
-    System.exit(0);
   }
 
   private EncryptedKeysetExample() {}
diff --git a/java_src/examples/helloworld/BUILD.bazel b/java_src/examples/helloworld/BUILD.bazel
deleted file mode 100644
index 06f6430..0000000
--- a/java_src/examples/helloworld/BUILD.bazel
+++ /dev/null
@@ -1,32 +0,0 @@
-load("@bazel_skylib//rules:build_test.bzl", "build_test")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_binary(
-    name = "helloworld",
-    srcs = glob([
-        "src/main/**/*.java",
-    ]),
-    main_class = "com.helloworld.HelloWorld",
-    runtime_deps = [
-        "@com_google_protobuf//:protobuf_javalite",
-        "@maven//:com_google_code_gson_gson",
-    ],
-    deps = [
-        "@maven//:args4j_args4j",
-        "@tink_java//src/main/java/com/google/crypto/tink:aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_writer",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
-        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
-        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
-    ],
-)
-
-build_test(
-    name = "helloworld_buildtest",
-    targets = [":helloworld"],
-)
diff --git a/java_src/examples/helloworld/README.md b/java_src/examples/helloworld/README.md
deleted file mode 100644
index 5082029..0000000
--- a/java_src/examples/helloworld/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Java Hello World
-
-This is a command-line tool that can encrypt and decrypt small files with
-AES128-GCM.
-
-It demonstrates the basic steps of using Tink, namely generating or loading
-key material, obtaining a primitive, and using the primitive to do crypto.
-
-It also shows how to add a dependency on Tink using Maven. Please checkout
-the pom.xml file.
-
-## Build and Run
-
-### Maven
-
-```shell
-git clone https://github.com/google/tink
-cd tink/examples/java_src/
-mvn package
-echo foo > foo.txt
-mvn exec:java -Dexec.args="encrypt --keyset test.cfg --in foo.txt --out bar.encrypted"
-mvn exec:java -Dexec.args="decrypt --keyset test.cfg --in bar.encrypted --out foo2.txt"
-cat foo2.txt
-```
-
-### Bazel
-
-```shell
-git clone https://github.com/google/tink
-cd tink/examples/java_src
-bazel build ...
-echo foo > foo.txt
-./bazel-bin/helloworld/helloworld encrypt --keyset test.cfg --in foo.txt --out bar.encrypted
-./bazel-bin/helloworld/helloworld decrypt --keyset test.cfg --in bar.encrypted --out foo2.txt
-cat foo2.txt
-```
diff --git a/java_src/examples/helloworld/pom.xml b/java_src/examples/helloworld/pom.xml
deleted file mode 100644
index ceb7c8a..0000000
--- a/java_src/examples/helloworld/pom.xml
+++ /dev/null
@@ -1,112 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>com.helloworld</groupId>
-  <artifactId>helloworld</artifactId>
-  <version>0.1.0</version>
-  <packaging>jar</packaging>
-
-  <name>Tink for Java HelloWorld</name>
-  <url>https://github.com/google/tink/tree/master/examples/java_src/helloworld</url>
-
-  <repositories>
-    <repository>
-      <id>sonatype-snapshots</id>
-      <name>sonatype-snapshots</name>
-      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
-      <snapshots>
-        <enabled>true</enabled>
-        <updatePolicy>always</updatePolicy>
-      </snapshots>
-      <releases>
-        <updatePolicy>always</updatePolicy>
-      </releases>
-    </repository>
-  </repositories>
-
-  <properties>
-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-    <java.version>1.7</java.version>
-    <maven-enforcer-plugin.version>1.3.1</maven-enforcer-plugin.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>args4j</groupId>
-      <artifactId>args4j</artifactId>
-      <version>2.33</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink</artifactId>
-      <version>HEAD-SNAPSHOT</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink-awskms</artifactId>
-      <version>HEAD-SNAPSHOT</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink-gcpkms</artifactId>
-      <version>HEAD-SNAPSHOT</version>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <version>3.5.1</version>
-        <configuration>
-          <source>${java.version}</source>
-          <target>${java.version}</target>
-          <compilerArgument>-Werror</compilerArgument>
-          <compilerArgument>-Xlint:deprecation</compilerArgument>
-        </configuration>
-      </plugin>
-
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>exec-maven-plugin</artifactId>
-        <version>1.2.1</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>java</goal>
-            </goals>
-          </execution>
-        </executions>
-        <configuration>
-          <mainClass>com.helloworld.HelloWorld</mainClass>
-        </configuration>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-enforcer-plugin</artifactId>
-        <version>${maven-enforcer-plugin.version}</version>
-        <executions>
-          <execution>
-            <id>enforce</id>
-            <configuration>
-              <rules>
-                <DependencyConvergence/>
-              </rules>
-              <fail>true</fail>
-            </configuration>
-            <goals>
-              <goal>enforce</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/java_src/examples/helloworld/src/main/java/com/helloworld/Commands.java b/java_src/examples/helloworld/src/main/java/com/helloworld/Commands.java
deleted file mode 100644
index 05f5a71..0000000
--- a/java_src/examples/helloworld/src/main/java/com/helloworld/Commands.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.helloworld;
-
-import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
-import com.google.crypto.tink.JsonKeysetWriter;
-import com.google.crypto.tink.KeyTemplates;
-import com.google.crypto.tink.KeysetHandle;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.security.GeneralSecurityException;
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.SubCommand;
-import org.kohsuke.args4j.spi.SubCommandHandler;
-import org.kohsuke.args4j.spi.SubCommands;
-
-/** Defines the different sub-commands and their parameters, for command-line invocation. */
-public final class Commands {
-  /** An interface for a command-line sub-command. */
-  interface Command {
-    public void run() throws Exception;
-  }
-
-  static class Options {
-    @Option(
-      name = "--keyset",
-      required = true,
-      usage = "The path to the keyset, generate new if does not exist"
-    )
-    File keyset;
-
-    @Option(name = "--in", required = true, usage = "The input filename")
-    File inFile;
-
-    @Option(name = "--out", required = true, usage = "The output filename")
-    File outFile;
-  }
-
-  /** Loads a KeysetHandle from {@code keyset} or generate a new one if it doesn't exist. */
-  private static KeysetHandle getKeysetHandle(File keyset)
-      throws GeneralSecurityException, IOException {
-    if (keyset.exists()) {
-      // Read the cleartext keyset from disk.
-      // WARNING: reading cleartext keysets is a bad practice. Tink supports reading/writing
-      // encrypted keysets, see
-      // https://github.com/google/tink/blob/master/docs/JAVA-HOWTO.md#loading-existing-keysets.
-      return CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyset));
-    }
-    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("AES128_GCM"));
-    CleartextKeysetHandle.write(handle, JsonKeysetWriter.withFile(keyset));
-    return handle;
-  }
-
-  /**
-   * Encrypts a file.
-   */
-  public static class EncryptCommand extends Options implements Command {
-    @Override
-    public void run() throws Exception {
-      // 1. Obtain a keyset handle.
-      KeysetHandle handle = getKeysetHandle(keyset);
-      // 2. Get a primitive.
-      Aead aead = handle.getPrimitive(Aead.class);
-      // 3. Do crypto. It's that simple!
-      byte[] plaintext = Files.readAllBytes(inFile.toPath());
-      byte[] ciphertext = aead.encrypt(plaintext, new byte[0] /* associatedData */);
-      try (FileOutputStream stream = new FileOutputStream(outFile)) {
-        stream.write(ciphertext);
-      }
-    }
-  }
-
-  /**
-   * Decrypts a file.
-   */
-  public static class DecryptCommand extends Options implements Command {
-    @Override
-    public void run() throws Exception {
-      KeysetHandle handle = getKeysetHandle(keyset);
-      Aead aead = handle.getPrimitive(Aead.class);
-      byte[] ciphertext = Files.readAllBytes(inFile.toPath());
-      byte[] plaintext = aead.decrypt(ciphertext, new byte[0] /* associatedData */);
-      try (FileOutputStream stream = new FileOutputStream(outFile)) {
-        stream.write(plaintext);
-      }
-    }
-  }
-
-  @Argument(
-    metaVar = "command",
-    required = true,
-    handler = SubCommandHandler.class,
-    usage = "The subcommand to run"
-  )
-  @SubCommands({
-    @SubCommand(name = "encrypt", impl = EncryptCommand.class),
-    @SubCommand(name = "decrypt", impl = DecryptCommand.class)
-  })
-  Command command;
-}
diff --git a/java_src/examples/helloworld/src/main/java/com/helloworld/HelloWorld.java b/java_src/examples/helloworld/src/main/java/com/helloworld/HelloWorld.java
deleted file mode 100644
index ce647ce..0000000
--- a/java_src/examples/helloworld/src/main/java/com/helloworld/HelloWorld.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2017 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.helloworld;
-
-import com.google.crypto.tink.aead.AeadConfig;
-import java.security.GeneralSecurityException;
-import org.kohsuke.args4j.CmdLineException;
-import org.kohsuke.args4j.CmdLineParser;
-
-/**
- * A command-line tool that can encrypt and decrypt small files with AES128-GCM.
- *
- * <p>This application uses the <a href="https://github.com/google/tink">Tink<a/> crypto library.
- */
-public final class HelloWorld {
-  public static void main(String[] args) throws Exception {
-    // Register all AEAD key types with the Tink runtime.
-    AeadConfig.register();
-
-    Commands commands = new Commands();
-    CmdLineParser parser = new CmdLineParser(commands);
-    try {
-      parser.parseArgument(args);
-    } catch (CmdLineException e) {
-      System.out.println(e);
-      e.getParser().printUsage(System.out);
-      System.exit(1);
-    }
-    try {
-      commands.command.run();
-    } catch (GeneralSecurityException e) {
-      System.out.println("Cannot encrypt or decrypt, got error: " + e.toString());
-      System.exit(1);
-    }
-  }
-
-  private HelloWorld() {}
-}
diff --git a/java_src/examples/hybrid/BUILD.bazel b/java_src/examples/hybrid/BUILD.bazel
index 8b2adc7..6b915c2 100644
--- a/java_src/examples/hybrid/BUILD.bazel
+++ b/java_src/examples/hybrid/BUILD.bazel
@@ -12,11 +12,11 @@
     srcs = ["HybridExample.java"],
     main_class = "hybrid.HybridExample",
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
         "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
         "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
     ],
 )
diff --git a/java_src/examples/hybrid/HybridExample.java b/java_src/examples/hybrid/HybridExample.java
index b3d812c..21fc0ad 100644
--- a/java_src/examples/hybrid/HybridExample.java
+++ b/java_src/examples/hybrid/HybridExample.java
@@ -16,17 +16,15 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.HybridDecrypt;
 import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.hybrid.HybridConfig;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.nio.file.Files;
-import java.security.GeneralSecurityException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 /**
  * A command-line utility for hybrid encryption.
@@ -56,10 +54,10 @@
       System.err.println("Incorrect mode. Please select encrypt or decrypt.");
       System.exit(1);
     }
-    File keyFile = new File(args[1]);
-    File inputFile = new File(args[2]);
-    byte[] input = Files.readAllBytes(inputFile.toPath());
-    File outputFile = new File(args[3]);
+    Path keyFile = Paths.get(args[1]);
+    Path inputFile = Paths.get(args[2]);
+    byte[] input = Files.readAllBytes(inputFile);
+    Path outputFile = Paths.get(args[3]);
     byte[] contextInfo = new byte[0];
     if (args.length == 5) {
       contextInfo = args[4].getBytes(UTF_8);
@@ -69,47 +67,24 @@
     HybridConfig.register();
 
     // Read the keyset into a KeysetHandle.
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());
 
     if (mode.equals("encrypt")) {
       // Get the primitive.
-      HybridEncrypt encryptor = null;
-      try {
-        encryptor = handle.getPrimitive(HybridEncrypt.class);
-      } catch (GeneralSecurityException ex) {
-        System.err.println("Cannot create primitive, got error: " + ex);
-        System.exit(1);
-      }
+      HybridEncrypt encryptor = handle.getPrimitive(HybridEncrypt.class);
 
       // Use the primitive to encrypt data.
       byte[] ciphertext = encryptor.encrypt(input, contextInfo);
-      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-        stream.write(ciphertext);
-      }
-      System.exit(0);
-    }
+      Files.write(outputFile, ciphertext);
+    } else {
+      HybridDecrypt decryptor = handle.getPrimitive(HybridDecrypt.class);
 
-    // Get the primitive.
-    HybridDecrypt decryptor = null;
-    try {
-      decryptor = handle.getPrimitive(HybridDecrypt.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
+      // Use the primitive to decrypt data.
+      byte[] plaintext = decryptor.decrypt(input, contextInfo);
+      Files.write(outputFile, plaintext);
     }
-
-    // Use the primitive to decrypt data.
-    byte[] plaintext = decryptor.decrypt(input, contextInfo);
-    try (FileOutputStream stream = new FileOutputStream(outputFile)) {
-      stream.write(plaintext);
-    }
-    System.exit(0);
   }
 
   private HybridExample() {}
diff --git a/java_src/examples/hybrid/README.md b/java_src/examples/hybrid/README.md
index 8709f98..d4d0d1f 100644
--- a/java_src/examples/hybrid/README.md
+++ b/java_src/examples/hybrid/README.md
@@ -22,24 +22,24 @@
 
 ```shell
 git clone https://github.com/google/tink
-cd tink/examples/java_src
-bazel build ...
+cd tink/java_src/examples
+bazel build hybrid_example
 ```
 
 Encrypt a file:
 
 ```shell
 echo "some data" > testdata.txt
-./bazel-bin/hybrid/hybrid_example encrypt \
-    ./hybrid/hybrid_test_public_keyset.json \
+../bazel-bin/hybrid/hybrid_example encrypt \
+    ../hybrid/hybrid_test_public_keyset.json \
     testdata.txt testdata.txt.encrypted
 ```
 
 Decrypt a file:
 
 ```shell
-./bazel-bin/hybrid/hybrid_example decrypt \
-    ./hybrid/hybrid_test_private_keyset.json \
+../bazel-bin/hybrid/hybrid_example decrypt \
+    ../hybrid/hybrid_test_private_keyset.json \
     testdata.txt.encrypted testdata.txt.decrypted
 
 diff testdata.txt testdata.txt.decrypted
diff --git a/java_src/examples/jwt/BUILD.bazel b/java_src/examples/jwt/BUILD.bazel
index 4a56b96..fe06f8e 100644
--- a/java_src/examples/jwt/BUILD.bazel
+++ b/java_src/examples/jwt/BUILD.bazel
@@ -11,9 +11,9 @@
     srcs = ["JwtSign.java"],
     main_class = "jwt.JwtSign",
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_sign",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
@@ -25,9 +25,9 @@
     srcs = ["JwtGeneratePublicJwkSet.java"],
     main_class = "jwt.JwtGeneratePublicJwkSet",
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwk_set_converter",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config",
     ],
@@ -44,7 +44,6 @@
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_validator",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
-        "@tink_java//src/main/java/com/google/crypto/tink/tinkkey:key_access",
     ],
 )
 
diff --git a/java_src/examples/jwt/JwtGeneratePublicJwkSet.java b/java_src/examples/jwt/JwtGeneratePublicJwkSet.java
index 51f3ef2..7260558 100644
--- a/java_src/examples/jwt/JwtGeneratePublicJwkSet.java
+++ b/java_src/examples/jwt/JwtGeneratePublicJwkSet.java
@@ -16,15 +16,14 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.jwt.JwkSetConverter;
 import com.google.crypto.tink.jwt.JwtSignatureConfig;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 /**
  * A command-line example for generating the public JWT keyset in JWK set format.
@@ -46,29 +45,22 @@
       System.exit(1);
     }
 
-    File privateKeysetFile = new File(args[0]);
-    File publicJwkSetFile = new File(args[1]);
+    Path privateKeysetFile = Paths.get(args[0]);
+    Path publicJwkSetFile = Paths.get(args[1]);
 
     // Register all JWT signature key types with the Tink runtime.
     JwtSignatureConfig.register();
 
     // Read the keyset into a KeysetHandle.
-    KeysetHandle privateKeysetHandle = null;
-    try {
-      privateKeysetHandle =
-          CleartextKeysetHandle.read(JsonKeysetReader.withFile(privateKeysetFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle privateKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(privateKeysetFile), UTF_8),
+            InsecureSecretKeyAccess.get());
 
     // Export the public keyset as JWK set.
     String publicJwkSet =
         JwkSetConverter.fromPublicKeysetHandle(privateKeysetHandle.getPublicKeysetHandle());
-    try (FileOutputStream stream = new FileOutputStream(publicJwkSetFile)) {
-      stream.write(publicJwkSet.getBytes(UTF_8));
-    }
-    System.exit(0);
+    Files.write(publicJwkSetFile, publicJwkSet.getBytes(UTF_8));
   }
 
   private JwtGeneratePublicJwkSet() {}
diff --git a/java_src/examples/jwt/JwtSign.java b/java_src/examples/jwt/JwtSign.java
index 674b9de..6183c5a 100644
--- a/java_src/examples/jwt/JwtSign.java
+++ b/java_src/examples/jwt/JwtSign.java
@@ -16,16 +16,15 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.jwt.JwtPublicKeySign;
 import com.google.crypto.tink.jwt.JwtSignatureConfig;
 import com.google.crypto.tink.jwt.RawJwt;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.Instant;
 
 /**
@@ -49,42 +48,30 @@
       System.exit(1);
     }
 
-    File privateKeysetFile = new File(args[0]);
+    Path privateKeysetFile = Paths.get(args[0]);
     String audience = args[1];
-    File tokenFile = new File(args[2]);
+    Path tokenFile = Paths.get(args[2]);
 
     // Register all JWT signature key types with the Tink runtime.
     JwtSignatureConfig.register();
 
     // Read the private keyset into a KeysetHandle.
-    KeysetHandle privateKeysetHandle = null;
-    try {
-      privateKeysetHandle =
-          CleartextKeysetHandle.read(JsonKeysetReader.withFile(privateKeysetFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle privateKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(privateKeysetFile), UTF_8),
+            InsecureSecretKeyAccess.get());
 
     // Get the primitive.
-    JwtPublicKeySign signer = null;
-    try {
-      signer = privateKeysetHandle.getPrimitive(JwtPublicKeySign.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
+    JwtPublicKeySign signer = privateKeysetHandle.getPrimitive(JwtPublicKeySign.class);
 
     // Use the primitive to sign a token that expires in 100 seconds.
-    RawJwt rawJwt = RawJwt.newBuilder()
-        .addAudience(audience)
-        .setExpiration(Instant.now().plusSeconds(100))
-        .build();
+    RawJwt rawJwt =
+        RawJwt.newBuilder()
+            .addAudience(audience)
+            .setExpiration(Instant.now().plusSeconds(100))
+            .build();
     String signedToken = signer.signAndEncode(rawJwt);
-    try (FileOutputStream stream = new FileOutputStream(tokenFile)) {
-      stream.write(signedToken.getBytes(UTF_8));
-    }
-    System.exit(0);
+    Files.write(tokenFile, signedToken.getBytes(UTF_8));
   }
 
   private JwtSign() {}
diff --git a/java_src/examples/jwt/JwtVerify.java b/java_src/examples/jwt/JwtVerify.java
index 985a188..4c8d790 100644
--- a/java_src/examples/jwt/JwtVerify.java
+++ b/java_src/examples/jwt/JwtVerify.java
@@ -22,11 +22,9 @@
 import com.google.crypto.tink.jwt.JwtSignatureConfig;
 import com.google.crypto.tink.jwt.JwtValidator;
 import com.google.crypto.tink.jwt.VerifiedJwt;
-import com.google.crypto.tink.tinkkey.KeyAccess;
-import java.io.File;
-import java.io.IOException;
 import java.nio.file.Files;
-import java.security.GeneralSecurityException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
@@ -50,24 +48,19 @@
       System.exit(1);
     }
 
-    File publicJwkSetFile = new File(args[0]);
+    Path publicJwkSetFile = Paths.get(args[0]);
     String audience = args[1];
-    File tokenFile = new File(args[2]);
+    Path tokenFile = Paths.get(args[2]);
 
     // Register all JWT signature key types with the Tink runtime.
     JwtSignatureConfig.register();
 
     // Read the public keyset in JWK set format into a KeysetHandle.
-    KeysetHandle publicKeysetHandle = null;
-    try {
-      String publicJwkSet = new String(Files.readAllBytes(publicJwkSetFile.toPath()), UTF_8);
-      publicKeysetHandle = JwkSetConverter.toKeysetHandle(publicJwkSet, KeyAccess.publicAccess());
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle publicKeysetHandle =
+        JwkSetConverter.toPublicKeysetHandle(
+            new String(Files.readAllBytes(publicJwkSetFile), UTF_8));
 
-    List<String> lines = Files.readAllLines(tokenFile.toPath());
+    List<String> lines = Files.readAllLines(tokenFile, UTF_8);
     if (lines.size() != 1) {
       System.err.printf("The signature file should contain only one line,  got %d", lines.size());
       System.exit(1);
@@ -75,26 +68,13 @@
     String signedToken = lines.get(0).trim();
 
     // Get the primitive.
-    JwtPublicKeyVerify verifier = null;
-    try {
-      verifier = publicKeysetHandle.getPrimitive(JwtPublicKeyVerify.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
+    JwtPublicKeyVerify verifier = publicKeysetHandle.getPrimitive(JwtPublicKeyVerify.class);
 
     // Use the primitive to verify a token.
-    try {
-      JwtValidator validator = JwtValidator.newBuilder().expectAudience(audience).build();
-      VerifiedJwt verifiedJwt = verifier.verifyAndDecode(signedToken, validator);
-      long seconds = ChronoUnit.SECONDS.between(Instant.now(), verifiedJwt.getExpiration());
-      System.out.println("Token is valid and expires in " + seconds + " seconds.");
-    } catch (GeneralSecurityException ex) {
-      System.err.println("JWT verification failed.");
-      System.exit(1);
-    }
-
-    System.exit(0);
+    JwtValidator validator = JwtValidator.newBuilder().expectAudience(audience).build();
+    VerifiedJwt verifiedJwt = verifier.verifyAndDecode(signedToken, validator);
+    long seconds = ChronoUnit.SECONDS.between(Instant.now(), verifiedJwt.getExpiration());
+    System.out.println("Token is valid and expires in " + seconds + " seconds.");
   }
 
   private JwtVerify() {}
diff --git a/java_src/examples/mac/BUILD.bazel b/java_src/examples/mac/BUILD.bazel
index 360dfa5..7d0b60a 100644
--- a/java_src/examples/mac/BUILD.bazel
+++ b/java_src/examples/mac/BUILD.bazel
@@ -10,11 +10,11 @@
     main_class = "mac.MacExample",
     deps = [
         "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:mac",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hex",
     ],
 )
 
diff --git a/java_src/examples/mac/MacExample.java b/java_src/examples/mac/MacExample.java
index 6ac1f08..6ab407c 100644
--- a/java_src/examples/mac/MacExample.java
+++ b/java_src/examples/mac/MacExample.java
@@ -16,19 +16,14 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.mac.MacConfig;
-import com.google.crypto.tink.subtle.Hex;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.GeneralSecurityException;
-import java.util.List;
 
 /**
  * A command-line utility for checking file integrity with a Message Authentication Code (MAC).
@@ -55,54 +50,29 @@
       System.err.println("Incorrect mode. Please select compute or verify.");
       System.exit(1);
     }
-    File keyFile = new File(args[1]);
+    Path keyFile = Paths.get(args[1]);
     byte[] msg = Files.readAllBytes(Paths.get(args[2]));
-    File macFile = new File(args[3]);
+    Path macFile = Paths.get(args[3]);
 
     // Register all MAC key types with the Tink runtime.
     MacConfig.register();
 
     // Read the keyset into a KeysetHandle.
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());
 
     // Get the primitive.
-    Mac macPrimitive = null;
-    try {
-      macPrimitive = handle.getPrimitive(Mac.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
+    Mac macPrimitive = handle.getPrimitive(Mac.class);
 
     if (mode.equals("compute")) {
-      byte[] mac = macPrimitive.computeMac(msg);
-      try (FileOutputStream stream = new FileOutputStream(macFile)) {
-        stream.write(Hex.encode(mac).getBytes(UTF_8));
-      }
-      System.exit(0);
+      byte[] macTag = macPrimitive.computeMac(msg);
+      Files.write(macFile, macTag);
+    } else {
+      byte[] macTag = Files.readAllBytes(macFile);
+      // This will throw a GeneralSecurityException if verification fails.
+      macPrimitive.verifyMac(macTag, msg);
     }
-
-    List<String> lines = Files.readAllLines(macFile.toPath());
-    if (lines.size() != 1) {
-      System.err.printf("The MAC file should contain only one line, got %d", lines.size());
-      System.exit(1);
-    }
-
-    byte[] mac = Hex.decode(lines.get(0).trim());
-    try {
-      macPrimitive.verifyMac(mac, msg);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("MAC verification failed.");
-      System.exit(1);
-    }
-
-    System.exit(0);
   }
 
   private MacExample() {}
diff --git a/java_src/examples/signature/BUILD.bazel b/java_src/examples/signature/BUILD.bazel
index 794d1b2..7e03f04 100644
--- a/java_src/examples/signature/BUILD.bazel
+++ b/java_src/examples/signature/BUILD.bazel
@@ -12,13 +12,12 @@
     srcs = ["SignatureExample.java"],
     main_class = "signature.SignatureExample",
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
         "@tink_java//src/main/java/com/google/crypto/tink:public_key_verify",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/signature:signature_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hex",
     ],
 )
 
diff --git a/java_src/examples/signature/SignatureExample.java b/java_src/examples/signature/SignatureExample.java
index 679042f..a0937ea 100644
--- a/java_src/examples/signature/SignatureExample.java
+++ b/java_src/examples/signature/SignatureExample.java
@@ -16,20 +16,15 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PublicKeySign;
 import com.google.crypto.tink.PublicKeyVerify;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.signature.SignatureConfig;
-import com.google.crypto.tink.subtle.Hex;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.GeneralSecurityException;
-import java.util.List;
 
 /**
  * A command-line utility for digitally signing and verifying a file.
@@ -58,65 +53,33 @@
       System.err.println("Incorrect mode. Please select sign or verify.");
       System.exit(1);
     }
-    File keyFile = new File(args[1]);
+    Path keyFile = Paths.get(args[1]);
     byte[] msg = Files.readAllBytes(Paths.get(args[2]));
-    File signatureFile = new File(args[3]);
+    Path signatureFile = Paths.get(args[3]);
 
     // Register all signature key types with the Tink runtime.
     SignatureConfig.register();
 
     // Read the keyset into a KeysetHandle.
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());
 
     if (mode.equals("sign")) {
       // Get the primitive.
-      PublicKeySign signer = null;
-      try {
-        signer = handle.getPrimitive(PublicKeySign.class);
-      } catch (GeneralSecurityException ex) {
-        System.err.println("Cannot create primitive, got error: " + ex);
-        System.exit(1);
-      }
+      PublicKeySign signer = handle.getPrimitive(PublicKeySign.class);
 
       // Use the primitive to sign data.
       byte[] signature = signer.sign(msg);
-      try (FileOutputStream stream = new FileOutputStream(signatureFile)) {
-        stream.write(Hex.encode(signature).getBytes(UTF_8));
-      }
-      System.exit(0);
-    }
+      Files.write(signatureFile, signature);
+    } else {
+      byte[] signature = Files.readAllBytes(signatureFile);
 
-    List<String> lines = Files.readAllLines(signatureFile.toPath());
-    if (lines.size() != 1) {
-      System.err.printf("The signature file should contain only one line,  got %d", lines.size());
-      System.exit(1);
-    }
-    byte[] signature = Hex.decode(lines.get(0).trim());
+      // Get the primitive.
+      PublicKeyVerify verifier = handle.getPrimitive(PublicKeyVerify.class);
 
-    // Get the primitive.
-    PublicKeyVerify verifier = null;
-    try {
-      verifier = handle.getPrimitive(PublicKeyVerify.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
-
-    // Use the primitive to verify data.
-    try {
       verifier.verify(signature, msg);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Signature verification failed.");
-      System.exit(1);
     }
-
-    System.exit(0);
   }
 
   private SignatureExample() {}
diff --git a/java_src/examples/streamingaead/BUILD.bazel b/java_src/examples/streamingaead/BUILD.bazel
index 9d93df9..78a2bad 100644
--- a/java_src/examples/streamingaead/BUILD.bazel
+++ b/java_src/examples/streamingaead/BUILD.bazel
@@ -11,10 +11,10 @@
     srcs = ["StreamingAeadExample.java"],
     main_class = "streamingaead.StreamingAeadExample",
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
         "@tink_java//src/main/java/com/google/crypto/tink:streaming_aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
     ],
 )
diff --git a/java_src/examples/streamingaead/StreamingAeadExample.java b/java_src/examples/streamingaead/StreamingAeadExample.java
index 8b12075..ac2d32d 100644
--- a/java_src/examples/streamingaead/StreamingAeadExample.java
+++ b/java_src/examples/streamingaead/StreamingAeadExample.java
@@ -16,17 +16,20 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.security.GeneralSecurityException;
 
 /**
@@ -46,6 +49,7 @@
 public final class StreamingAeadExample {
   private static final String MODE_ENCRYPT = "encrypt";
   private static final String MODE_DECRYPT = "decrypt";
+  private static final int BLOCK_SIZE_IN_BYTES = 8 * 1024;
 
   public static void main(String[] args) throws Exception {
     if (args.length != 4 && args.length != 5) {
@@ -56,9 +60,9 @@
       System.exit(1);
     }
     String mode = args[0];
-    File keyFile = new File(args[1]);
-    File inputFile = new File(args[2]);
-    File outputFile = new File(args[3]);
+    Path keyFile = Paths.get(args[1]);
+    Path inputFile = Paths.get(args[2]);
+    Path outputFile = Paths.get(args[3]);
     byte[] associatedData = new byte[0];
     if (args.length == 5) {
       associatedData = args[4].getBytes(UTF_8);
@@ -68,22 +72,12 @@
     StreamingAeadConfig.register();
 
     // Read the keyset into a KeysetHandle
-    KeysetHandle handle = null;
-    try {
-      handle = CleartextKeysetHandle.read(JsonKeysetReader.withFile(keyFile));
-    } catch (GeneralSecurityException | IOException ex) {
-      System.err.println("Cannot read keyset, got error: " + ex);
-      System.exit(1);
-    }
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());
 
     // Get the primitive
-    StreamingAead streamingAead = null;
-    try {
-      streamingAead = handle.getPrimitive(StreamingAead.class);
-    } catch (GeneralSecurityException ex) {
-      System.err.println("Cannot create primitive, got error: " + ex);
-      System.exit(1);
-    }
+    StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class);
 
     // Use the primitive to encrypt/decrypt files
     if (MODE_ENCRYPT.equals(mode)) {
@@ -94,39 +88,52 @@
       System.err.println("The first argument must be either encrypt or decrypt, got: " + mode);
       System.exit(1);
     }
-
-    System.exit(0);
   }
 
   private static void encryptFile(
-      StreamingAead streamingAead, File inputFile, File outputFile, byte[] associatedData)
+      StreamingAead streamingAead, Path inputFile, Path outputFile, byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    try (OutputStream ciphertextStream =
-            streamingAead.newEncryptingStream(new FileOutputStream(outputFile), associatedData);
-        InputStream plaintextStream = new FileInputStream(inputFile)) {
-      byte[] chunk = new byte[1024];
-      int chunkLen = 0;
-      while ((chunkLen = plaintextStream.read(chunk)) != -1) {
-        ciphertextStream.write(chunk, 0, chunkLen);
+    try (WritableByteChannel plaintextChannel =
+            streamingAead.newEncryptingChannel(
+                FileChannel.open(outputFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE),
+                associatedData);
+        FileChannel inputChannel = FileChannel.open(inputFile, StandardOpenOption.READ)) {
+      ByteBuffer byteBuffer = ByteBuffer.allocate(BLOCK_SIZE_IN_BYTES);
+      while (true) {
+        int read = inputChannel.read(byteBuffer);
+        if (read <= 0) {
+          return;
+        }
+        byteBuffer.flip();
+        while (byteBuffer.hasRemaining()) {
+          plaintextChannel.write(byteBuffer);
+        }
+        byteBuffer.clear();
       }
     }
   }
 
   private static void decryptFile(
-      StreamingAead streamingAead, File inputFile, File outputFile, byte[] associatedData)
+      StreamingAead streamingAead, Path inputFile, Path outputFile, byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    InputStream ciphertextStream =
-        streamingAead.newDecryptingStream(new FileInputStream(inputFile), associatedData);
-
-    OutputStream plaintextStream = new FileOutputStream(outputFile);
-    byte[] chunk = new byte[1024];
-    int chunkLen = 0;
-    while ((chunkLen = ciphertextStream.read(chunk)) != -1) {
-      plaintextStream.write(chunk, 0, chunkLen);
+    try (ReadableByteChannel plaintextChannel =
+            streamingAead.newDecryptingChannel(
+                FileChannel.open(inputFile, StandardOpenOption.READ), associatedData);
+        FileChannel outputChannel =
+            FileChannel.open(outputFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
+      ByteBuffer byteBuffer = ByteBuffer.allocate(BLOCK_SIZE_IN_BYTES);
+      while (true) {
+        int read = plaintextChannel.read(byteBuffer);
+        if (read <= 0) {
+          return;
+        }
+        byteBuffer.flip();
+        while (byteBuffer.hasRemaining()) {
+          outputChannel.write(byteBuffer);
+        }
+        byteBuffer.clear();
+      }
     }
-
-    ciphertextStream.close();
-    plaintextStream.close();
   }
 
   private StreamingAeadExample() {}
diff --git a/java_src/examples/testdata/README.md b/java_src/examples/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/java_src/examples/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/java_src/examples/testdata/aws/README.md b/java_src/examples/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/java_src/examples/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/java_src/examples/testdata/gcp/README.md b/java_src/examples/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/java_src/examples/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/java_src/examples/tink_java_examples_WORKSPACE b/java_src/examples/tink_java_examples_WORKSPACE
deleted file mode 100644
index c3afd71..0000000
--- a/java_src/examples/tink_java_examples_WORKSPACE
+++ /dev/null
@@ -1,29 +0,0 @@
-workspace(name = "tink_java_examples")
-
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-http_archive(
-    name = "tink_java",
-    urls = ["https://github.com/tink-crypto/tink-java/archive/main.zip"],
-    strip_prefix = "tink-java-main",
-)
-
-load("@tink_java//:tink_java_deps.bzl", "tink_java_deps", "TINK_MAVEN_ARTIFACTS")
-
-tink_java_deps()
-
-load("@tink_java//:tink_java_deps_init.bzl", "tink_java_deps_init")
-
-tink_java_deps_init()
-
-load("@rules_jvm_external//:defs.bzl", "maven_install")
-
-maven_install(
-    artifacts = TINK_MAVEN_ARTIFACTS + [
-      "args4j:args4j:2.33",
-    ],
-    repositories = [
-        "https://maven.google.com",
-        "https://repo1.maven.org/maven2",
-    ],
-)
diff --git a/java_src/examples/tink_java_gcpkms_examples_WORKSPACE b/java_src/examples/tink_java_gcpkms_examples_WORKSPACE
deleted file mode 100644
index 08d6aaa..0000000
--- a/java_src/examples/tink_java_gcpkms_examples_WORKSPACE
+++ /dev/null
@@ -1,39 +0,0 @@
-workspace(name = "tink_java_awskms_examples")
-
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-http_archive(
-    name = "tink_java",
-    urls = ["https://github.com/tink-crypto/tink-java/archive/main.zip"],
-    strip_prefix = "tink-java-main",
-)
-
-http_archive(
-    name = "tink_java_gcpkms",
-    urls = ["https://github.com/tink-crypto/tink-java-gcpkms/archive/main.zip"],
-    strip_prefix = "tink-java-gcpkms-main",
-)
-
-load("@tink_java//:tink_java_deps.bzl", "tink_java_deps", "TINK_MAVEN_ARTIFACTS")
-
-tink_java_deps()
-
-load("@tink_java//:tink_java_deps_init.bzl", "tink_java_deps_init")
-
-tink_java_deps_init()
-
-load("@rules_jvm_external//:defs.bzl", "maven_install")
-
-load("@tink_java_gcpkms//:tink_java_gcpkms_deps.bzl", "TINK_JAVA_GCPKMS_MAVEN_ARTIFACTS")
-
-maven_install(
-    artifacts = TINK_MAVEN_ARTIFACTS +
-    TINK_JAVA_GCPKMS_MAVEN_ARTIFACTS + [
-      "args4j:args4j:2.33",
-      "com.google.cloud:google-cloud-storage:2.3.0",
-    ],
-    repositories = [
-        "https://maven.google.com",
-        "https://repo1.maven.org/maven2",
-    ],
-)
diff --git a/java_src/examples/walkthrough/src/main/java/walkthrough/BUILD.bazel b/java_src/examples/walkthrough/src/main/java/walkthrough/BUILD.bazel
new file mode 100644
index 0000000..0ab47df
--- /dev/null
+++ b/java_src/examples/walkthrough/src/main/java/walkthrough/BUILD.bazel
@@ -0,0 +1,44 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+java_library(
+    name = "create_keyset_example",
+    srcs = ["CreateKeysetExample.java"],
+    deps = [
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+    ],
+)
+
+java_library(
+    name = "obtain_and_use_aead_primitive_example",
+    srcs = ["ObtainAndUseAeadPrimitiveExample.java"],
+    deps = [
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+    ],
+)
+
+java_library(
+    name = "write_keyset_example",
+    srcs = ["WriteKeysetExample.java"],
+    deps = [
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_clients",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+    ],
+)
+
+java_library(
+    name = "read_keyset_example",
+    srcs = ["ReadKeysetExample.java"],
+    deps = [
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_clients",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+    ],
+)
diff --git a/java_src/examples/walkthrough/src/main/java/walkthrough/CreateKeysetExample.java b/java_src/examples/walkthrough/src/main/java/walkthrough/CreateKeysetExample.java
new file mode 100644
index 0000000..baae4c5
--- /dev/null
+++ b/java_src/examples/walkthrough/src/main/java/walkthrough/CreateKeysetExample.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+// [START tink_walkthrough_create_keyset]
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import java.security.GeneralSecurityException;
+
+// [START_EXCLUDE]
+/** Example to showcase creating a keyset and getting a {@link KeysetHandle}. */
+final class CreateKeysetExample {
+  private CreateKeysetExample() {}
+  // [END_EXCLUDE]
+
+  /**
+   * Creates a keyset with a single AES128-GCM key and return a handle to it.
+   *
+   * <p>Prerequisites for this example:
+   *
+   * <ul>
+   *   <li>Register AEAD implementations of Tink.
+   * </ul>
+   *
+   * @return a new {@link KeysetHandle} with a single AES128-GCM key.
+   * @throws GeneralSecurityException if any error occours.
+   */
+  static KeysetHandle createAes128GcmKeyset() throws GeneralSecurityException {
+    // Tink provides pre-baked sets of parameters. For example, here we use AES128-GCM's template.
+    // This will generate a new keyset with only *one* key and return a keyset handle to it.
+    return KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+  }
+  // [END tink_walkthrough_create_keyset]
+}
diff --git a/java_src/examples/walkthrough/src/main/java/walkthrough/ObtainAndUseAeadPrimitiveExample.java b/java_src/examples/walkthrough/src/main/java/walkthrough/ObtainAndUseAeadPrimitiveExample.java
new file mode 100644
index 0000000..247c063
--- /dev/null
+++ b/java_src/examples/walkthrough/src/main/java/walkthrough/ObtainAndUseAeadPrimitiveExample.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+// [START tink_walkthrough_obtain_and_use_aead_primitive]
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeysetHandle;
+import java.security.GeneralSecurityException;
+
+// [START_EXCLUDE]
+/** AEAD encryption/decryption example. */
+final class ObtainAndUseAeadPrimitiveExample {
+  private ObtainAndUseAeadPrimitiveExample() {}
+  // [END_EXCLUDE]
+
+  /**
+   * Showcases obtaining an AEAD primitive from {@code keysetHandle} and encrypt/decrypt.
+   *
+   * <p>Prerequisites for this example:
+   *
+   * <ul>
+   *   <li>Register AEAD implementations of Tink.
+   *   <li>Create a keyset and wrap it with a {@link KeysetHandle}.
+   * </ul>
+   *
+   * @return the result of encrypting then decrypting {@code plaintext} using {@code
+   *     associatedData}, with an {@code Aead} primitive obtained from {@code keysetHandle}.
+   * @throws GeneralSecurityException if any error occours.
+   */
+  static byte[] aeadEncryptDecrypt(
+      KeysetHandle keysetHandle, byte[] plaintext, byte[] associatedData)
+      throws GeneralSecurityException {
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    return aead.decrypt(ciphertext, associatedData);
+  }
+  // [END tink_walkthrough_obtain_and_use_aead_primitive]
+}
diff --git a/java_src/examples/walkthrough/src/main/java/walkthrough/ReadKeysetExample.java b/java_src/examples/walkthrough/src/main/java/walkthrough/ReadKeysetExample.java
new file mode 100644
index 0000000..f03ce29
--- /dev/null
+++ b/java_src/examples/walkthrough/src/main/java/walkthrough/ReadKeysetExample.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+// [START tink_walkthrough_read_keyset]
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KmsClients;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import java.security.GeneralSecurityException;
+
+// [START_EXCLUDE]
+/** Examples to read a keyset from an input stream that was encrypted with a KMS key. */
+final class ReadKeysetExample {
+  private ReadKeysetExample() {}
+  // [END_EXCLUDE]
+
+  /**
+   * Deserializes a JSON serialized encrypted keyset {@code serializedKeyset}; the keyset is assumed
+   * to be encrypted through a KMS service using the KMS key {@code kmsKekUri} and {@code
+   * associatedData}.
+   *
+   * <p>Prerequisites for this example:
+   *
+   * <ul>
+   *   <li>Register AEAD implementations of Tink.
+   *   <li>Register a KMS client to {@link KmsClients} that can use {@code kmsKekUri}.
+   *   <li>Create a keyset and serialize it encrypted using the key {@code kmsKekUri}.
+   * </ul>
+   *
+   * @param associatedData the associated data to use for decrypting the keyset. See
+   *     https://developers.google.com/tink/aead#associated_data.
+   * @return the serialized keyset.
+   */
+  static KeysetHandle readEncryptedKeyset(
+      String serializedKeyset, String kmsKekUri, byte[] associatedData)
+      throws GeneralSecurityException {
+    // Get an Aead primitive that uses the KMS service to encrypt/decrypt.
+    Aead kmsKekAead = KmsClients.get(kmsKekUri).getAead(kmsKekUri);
+    return TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+        serializedKeyset, kmsKekAead, associatedData);
+  }
+  // [END tink_walkthrough_read_keyset]
+}
diff --git a/java_src/examples/walkthrough/src/main/java/walkthrough/WriteKeysetExample.java b/java_src/examples/walkthrough/src/main/java/walkthrough/WriteKeysetExample.java
new file mode 100644
index 0000000..90a323d
--- /dev/null
+++ b/java_src/examples/walkthrough/src/main/java/walkthrough/WriteKeysetExample.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+// [START tink_walkthrough_write_keyset]
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KmsClients;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import java.security.GeneralSecurityException;
+
+// [START_EXCLUDE]
+/** Examples to write a keyset to an output stream encrypted with a KMS key. */
+final class WriteKeysetExample {
+  private WriteKeysetExample() {}
+  // [END_EXCLUDE]
+
+  /**
+   * Serializes a keyset with handle {@code keysetHandle} in JSON format; the keyset is encrypted
+   * through a KMS service using the KMS key {@code kmsKekUri} and {@code associatedData}.
+   *
+   * <p>Prerequisites for this example:
+   *
+   * <ul>
+   *   <li>Register AEAD implementations of Tink.
+   *   <li>Register a KMS client to {@link KmsClients} that can use {@code kmsKekUri}.
+   *   <li>Create a keyset and wrap it with a {@link KeysetHandle}.
+   * </ul>
+   *
+   * @param associatedData the associated data to use for encrypting the keyset. See
+   *     https://developers.google.com/tink/aead#associated_data.
+   * @return the serialized keyset.
+   */
+  static String writeEncryptedKeyset(
+      KeysetHandle keysetHandle, String kmsKekUri, byte[] associatedData)
+      throws GeneralSecurityException {
+    // Get an Aead primitive that uses the KMS service to encrypt/decrypt.
+    Aead kmsKekAead = KmsClients.get(kmsKekUri).getAead(kmsKekUri);
+    return TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+        keysetHandle, kmsKekAead, associatedData);
+  }
+  // [END tink_walkthrough_write_keyset]
+}
diff --git a/java_src/examples/walkthrough/src/test/java/walkthrough/BUILD.bazel b/java_src/examples/walkthrough/src/test/java/walkthrough/BUILD.bazel
new file mode 100644
index 0000000..73d5b78
--- /dev/null
+++ b/java_src/examples/walkthrough/src/test/java/walkthrough/BUILD.bazel
@@ -0,0 +1,74 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+java_test(
+    name = "CreateKesetExampleTest",
+    size = "small",
+    srcs = ["CreateKesetExampleTest.java"],
+    deps = [
+        "//walkthrough/src/main/java/walkthrough:create_keyset_example",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+    ],
+)
+
+java_test(
+    name = "ObtainAndUseAeadPrimitiveExampleTest",
+    size = "small",
+    srcs = ["ObtainAndUseAeadPrimitiveExampleTest.java"],
+    deps = [
+        "//walkthrough/src/main/java/walkthrough:obtain_and_use_aead_primitive_example",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+    ],
+)
+
+java_test(
+    name = "WriteKeyserExampleTest",
+    size = "small",
+    srcs = ["WriteKeyserExampleTest.java"],
+    deps = [
+        "//walkthrough/src/main/java/walkthrough:obtain_and_use_aead_primitive_example",
+        "//walkthrough/src/main/java/walkthrough:write_keyset_example",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_clients",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
+        "@tink_java//src/main/java/com/google/crypto/tink/testing:fake_kms_client",
+    ],
+)
+
+java_test(
+    name = "ReadKeysetExampleTest",
+    size = "small",
+    srcs = ["ReadKeysetExampleTest.java"],
+    deps = [
+        "//walkthrough/src/main/java/walkthrough:obtain_and_use_aead_primitive_example",
+        "//walkthrough/src/main/java/walkthrough:read_keyset_example",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_clients",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
+        "@tink_java//src/main/java/com/google/crypto/tink/testing:fake_kms_client",
+    ],
+)
diff --git a/java_src/examples/walkthrough/src/test/java/walkthrough/CreateKesetExampleTest.java b/java_src/examples/walkthrough/src/test/java/walkthrough/CreateKesetExampleTest.java
new file mode 100644
index 0000000..ab38fac
--- /dev/null
+++ b/java_src/examples/walkthrough/src/test/java/walkthrough/CreateKesetExampleTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadConfig;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class CreateKesetExampleTest {
+
+  @Test
+  public void createAes128GcmKeyset_suceeds() throws Exception {
+    AeadConfig.register();
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    KeysetHandle keysetHandle = CreateKeysetExample.createAes128GcmKeyset();
+
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+    assertThat(aead.decrypt(aead.encrypt(plaintext, associatedData), associatedData))
+        .isEqualTo(plaintext);
+  }
+}
diff --git a/java_src/examples/walkthrough/src/test/java/walkthrough/ObtainAndUseAeadPrimitiveExampleTest.java b/java_src/examples/walkthrough/src/test/java/walkthrough/ObtainAndUseAeadPrimitiveExampleTest.java
new file mode 100644
index 0000000..01c93ae
--- /dev/null
+++ b/java_src/examples/walkthrough/src/test/java/walkthrough/ObtainAndUseAeadPrimitiveExampleTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ObtainAndUseAeadPrimitiveExampleTest {
+
+  private static final String SERIALIZED_KEYSET =
+      "{"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
+          + "        \"value\": \"GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg==\""
+          + "      },"
+          + "      \"keyId\": 294406504,"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ],"
+          + "  \"primaryKeyId\": 294406504"
+          + "}";
+
+  @Test
+  public void encryptDecrypt_succeeds() throws Exception {
+    AeadConfig.register();
+    KeysetHandle keysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(SERIALIZED_KEYSET, InsecureSecretKeyAccess.get());
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    byte[] result = ObtainAndUseAeadPrimitiveExample.aeadEncryptDecrypt(
+                keysetHandle, plaintext, associatedData);
+
+    assertThat(result).isEqualTo(plaintext);
+  }
+}
diff --git a/java_src/examples/walkthrough/src/test/java/walkthrough/ReadKeysetExampleTest.java b/java_src/examples/walkthrough/src/test/java/walkthrough/ReadKeysetExampleTest.java
new file mode 100644
index 0000000..44a193e
--- /dev/null
+++ b/java_src/examples/walkthrough/src/test/java/walkthrough/ReadKeysetExampleTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KmsClients;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.testing.FakeKmsClient;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ReadKeysetExampleTest {
+  private static final String KEYSET_TO_SERIALIZE =
+      "{"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
+          + "        \"value\": \"GhD+9l0RANZjzZEZ8PDp7LRW\""
+          + "      },"
+          + "      \"keyId\": 1931667682,"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ],"
+          + "  \"primaryKeyId\": 1931667682"
+          + "}";
+
+  private static final String FAKE_KMS_KEY =
+      "{"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
+          + "        \"value\": \"GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg==\""
+          + "      },"
+          + "      \"keyId\": 294406504,"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ],"
+          + "  \"primaryKeyId\": 294406504"
+          + "}";
+
+  /** Returns a FakeKmsClient key URI from {@code FAKE_KMS_KEY}. */
+  private String getFakeKmsKeyUri() throws Exception {
+    KeysetHandle keysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(FAKE_KMS_KEY, InsecureSecretKeyAccess.get());
+    byte[] serializedKmsKey =
+        TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+    return FakeKmsClient.PREFIX + Base64.urlSafeEncode(serializedKmsKey);
+  }
+
+  @Test
+  public void readEncryptedKeyset_succeedsWithValidInputs() throws Exception {
+    AeadConfig.register();
+    KmsClients.add(new FakeKmsClient());
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] keysetAssociatedData = "keysetAssociatedData".getBytes(UTF_8);
+    String kmsKekKeyUri = getFakeKmsKeyUri();
+
+    KeysetHandle keysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(KEYSET_TO_SERIALIZE, InsecureSecretKeyAccess.get());
+    Aead kmsKekAead = KmsClients.get(kmsKekKeyUri).getAead(kmsKekKeyUri);
+    String serializedEncryptedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, kmsKekAead, keysetAssociatedData);
+
+    KeysetHandle deserializedKeyset =
+        ReadKeysetExample.readEncryptedKeyset(
+            serializedEncryptedKeyset, kmsKekKeyUri, keysetAssociatedData);
+    // Make sure the keyset was read correctly trying to decrypt ciphertext.
+    byte[] decrypted =
+        ObtainAndUseAeadPrimitiveExample.aeadEncryptDecrypt(
+            deserializedKeyset, plaintext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+}
diff --git a/java_src/examples/walkthrough/src/test/java/walkthrough/WriteKeyserExampleTest.java b/java_src/examples/walkthrough/src/test/java/walkthrough/WriteKeyserExampleTest.java
new file mode 100644
index 0000000..289040e
--- /dev/null
+++ b/java_src/examples/walkthrough/src/test/java/walkthrough/WriteKeyserExampleTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package walkthrough;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KmsClients;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.testing.FakeKmsClient;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class WriteKeyserExampleTest {
+  private static final String KEYSET_TO_SERIALIZE =
+      "{"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
+          + "        \"value\": \"GhD+9l0RANZjzZEZ8PDp7LRW\""
+          + "      },"
+          + "      \"keyId\": 1931667682,"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ],"
+          + "  \"primaryKeyId\": 1931667682"
+          + "}";
+
+  private static final String FAKE_KMS_KEY =
+      "{"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
+          + "        \"value\": \"GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg==\""
+          + "      },"
+          + "      \"keyId\": 294406504,"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ],"
+          + "  \"primaryKeyId\": 294406504"
+          + "}";
+
+  /** Returns a FakeKmsClient key URI from {@code FAKE_KMS_KEY}. */
+  private String getFakeKmsKeyUri() throws Exception {
+    KeysetHandle keysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(FAKE_KMS_KEY, InsecureSecretKeyAccess.get());
+    byte[] serializedKmsKey =
+        TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+    return FakeKmsClient.PREFIX + Base64.urlSafeEncode(serializedKmsKey);
+  }
+
+  @Test
+  public void writeEncryptedKeyset_succeedsWithValidInputs() throws Exception {
+    AeadConfig.register();
+    KmsClients.add(new FakeKmsClient());
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] keysetAssociatedData = "keysetAssociatedData".getBytes(UTF_8);
+    String kmsKekKeyUri = getFakeKmsKeyUri();
+    KeysetHandle keysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(KEYSET_TO_SERIALIZE, InsecureSecretKeyAccess.get());
+
+    String serializedEncryptedKeyset =
+        WriteKeysetExample.writeEncryptedKeyset(keysetHandle, kmsKekKeyUri, keysetAssociatedData);
+
+    // Make sure the encrypted keyset was written correctly by loading it and trying to decrypt
+    // ciphertext.
+    Aead kmsKekAead = KmsClients.get(kmsKekKeyUri).getAead(kmsKekKeyUri);
+    KeysetHandle loadedKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            serializedEncryptedKeyset, kmsKekAead, keysetAssociatedData);
+    byte[] decrypted =
+        ObtainAndUseAeadPrimitiveExample.aeadEncryptDecrypt(
+            loadedKeysetHandle, plaintext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+}
diff --git a/java_src/proto/aes_cmac.proto b/java_src/proto/aes_cmac.proto
index b214834..541ff58 100644
--- a/java_src/proto/aes_cmac.proto
+++ b/java_src/proto/aes_cmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_go_proto";
 
 message AesCmacParams {
   uint32 tag_size = 1;
diff --git a/java_src/proto/aes_cmac_prf.proto b/java_src/proto/aes_cmac_prf.proto
index 58e5f67..b2efc6d 100644
--- a/java_src/proto/aes_cmac_prf.proto
+++ b/java_src/proto/aes_cmac_prf.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_prf_go_proto";
 
 // key_type: type.googleapis.com/google.crypto.tink.AesCmacPrfKey
 message AesCmacPrfKey {
diff --git a/java_src/proto/aes_ctr.proto b/java_src/proto/aes_ctr.proto
index ecdb256..721699c 100644
--- a/java_src/proto/aes_ctr.proto
+++ b/java_src/proto/aes_ctr.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_go_proto";
 
 message AesCtrParams {
   uint32 iv_size = 1;
diff --git a/java_src/proto/aes_ctr_hmac_aead.proto b/java_src/proto/aes_ctr_hmac_aead.proto
index dcf541d..91ccb9b 100644
--- a/java_src/proto/aes_ctr_hmac_aead.proto
+++ b/java_src/proto/aes_ctr_hmac_aead.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_aead_go_proto";
 
 message AesCtrHmacAeadKeyFormat {
   AesCtrKeyFormat aes_ctr_key_format = 1;
diff --git a/java_src/proto/aes_ctr_hmac_streaming.proto b/java_src/proto/aes_ctr_hmac_streaming.proto
index 064b630..776e9bd 100644
--- a/java_src/proto/aes_ctr_hmac_streaming.proto
+++ b/java_src/proto/aes_ctr_hmac_streaming.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_streaming_go_proto";
 
 message AesCtrHmacStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/java_src/proto/aes_eax.proto b/java_src/proto/aes_eax.proto
index c673306..c1bf500 100644
--- a/java_src/proto/aes_eax.proto
+++ b/java_src/proto/aes_eax.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_eax_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_eax_go_proto";
 
 // only allowing tag size in bytes = 16
 message AesEaxParams {
diff --git a/java_src/proto/aes_gcm.proto b/java_src/proto/aes_gcm.proto
index fba7a89..2551aa4 100644
--- a/java_src/proto/aes_gcm.proto
+++ b/java_src/proto/aes_gcm.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_go_proto";
 option objc_class_prefix = "TINKPB";
 
 message AesGcmKeyFormat {
diff --git a/java_src/proto/aes_gcm_hkdf_streaming.proto b/java_src/proto/aes_gcm_hkdf_streaming.proto
index 61fb479..5ec7ca4 100644
--- a/java_src/proto/aes_gcm_hkdf_streaming.proto
+++ b/java_src/proto/aes_gcm_hkdf_streaming.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto";
 
 message AesGcmHkdfStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/java_src/proto/aes_gcm_siv.proto b/java_src/proto/aes_gcm_siv.proto
index df9fada..220d79f 100644
--- a/java_src/proto/aes_gcm_siv.proto
+++ b/java_src/proto/aes_gcm_siv.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_siv_go_proto";
 
 // The only allowed IV size is 12 bytes and tag size is 16 bytes.
 // Thus, accept no params.
diff --git a/java_src/proto/aes_siv.proto b/java_src/proto/aes_siv.proto
index 0023027..ccb8d3c 100644
--- a/java_src/proto/aes_siv.proto
+++ b/java_src/proto/aes_siv.proto
@@ -20,7 +20,15 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_siv_go_proto";
+
+// Tink implements RFC 5297 (https://www.rfc-editor.org/rfc/rfc5297) for
+// AES-SIV, putting the SIV/Tag at the beginning of the ciphertext.
+//
+// While the RFC 5297 supports a list of associated datas, Tink only supports
+// exactly one associated data, which corresponds to a list with one element in
+// RFC 5297. An empty associated data is a list with one empty element, and not
+// an empty list.
 
 message AesSivKeyFormat {
   // Only valid value is: 64.
diff --git a/java_src/proto/cached_dek_aead.proto b/java_src/proto/cached_dek_aead.proto
index 10bcde5..9b1a33f 100644
--- a/java_src/proto/cached_dek_aead.proto
+++ b/java_src/proto/cached_dek_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/cached_dek_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_aead_go_proto";
 
 message CachedDekAeadKeyFormat {
   // Required.
diff --git a/java_src/proto/cached_dek_envelope.proto b/java_src/proto/cached_dek_envelope.proto
index 1b096ad..8739b83 100644
--- a/java_src/proto/cached_dek_envelope.proto
+++ b/java_src/proto/cached_dek_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_multiple_files = true;
 option java_package = "com.google.crypto.tink.proto";
-option go_package = "github.com/google/tink/proto/cached_dek_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_envelope_go_proto";
 
 message CachedDekEnvelopeAeadKeyFormat {
   // Required.
diff --git a/java_src/proto/chacha20_poly1305.proto b/java_src/proto/chacha20_poly1305.proto
index 2cd6ead..ef8ab6e 100644
--- a/java_src/proto/chacha20_poly1305.proto
+++ b/java_src/proto/chacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/chacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/chacha20_poly1305_go_proto";
 
 message ChaCha20Poly1305KeyFormat {}
 
diff --git a/java_src/proto/common.proto b/java_src/proto/common.proto
index eaff8d3..4546064 100644
--- a/java_src/proto/common.proto
+++ b/java_src/proto/common.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/common_go_proto";
+option go_package = "github.com/google/tink/go/proto/common_go_proto";
 
 enum EllipticCurveType {
   UNKNOWN_CURVE = 0;
diff --git a/java_src/proto/config.proto b/java_src/proto/config.proto
index ebbd742..cff6506 100644
--- a/java_src/proto/config.proto
+++ b/java_src/proto/config.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/config_go_proto";
+option go_package = "github.com/google/tink/go/proto/config_go_proto";
 
 // An entry that describes a key type to be used with Tink library,
 // specifying the corresponding primitive, key manager, and deprecation status.
diff --git a/java_src/proto/ecdsa.proto b/java_src/proto/ecdsa.proto
index 6ba3970..2ce461f 100644
--- a/java_src/proto/ecdsa.proto
+++ b/java_src/proto/ecdsa.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecdsa_go_proto";
 
 enum EcdsaSignatureEncoding {
   UNKNOWN_ENCODING = 0;
@@ -80,4 +80,5 @@
 message EcdsaKeyFormat {
   // Required.
   EcdsaParams params = 2;
+  uint32 version = 3;
 }
diff --git a/java_src/proto/ecies_aead_hkdf.proto b/java_src/proto/ecies_aead_hkdf.proto
index 9470991..0c06ee3 100644
--- a/java_src/proto/ecies_aead_hkdf.proto
+++ b/java_src/proto/ecies_aead_hkdf.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecies_aead_hkdf_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecies_aead_hkdf_go_proto";
 
 // Protos for keys for ECIES with HKDF and AEAD encryption.
 //
diff --git a/java_src/proto/ed25519.proto b/java_src/proto/ed25519.proto
index 669f33a..613c59f 100644
--- a/java_src/proto/ed25519.proto
+++ b/java_src/proto/ed25519.proto
@@ -23,10 +23,10 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ed25519_go_proto";
+option go_package = "github.com/google/tink/go/proto/ed25519_go_proto";
 
 message Ed25519KeyFormat {
-    uint32 version = 1;
+  uint32 version = 1;
 }
 
 // key_type: type.googleapis.com/google.crypto.tink.Ed25519PublicKey
diff --git a/java_src/proto/empty.proto b/java_src/proto/empty.proto
index 33831a9..beeba07 100644
--- a/java_src/proto/empty.proto
+++ b/java_src/proto/empty.proto
@@ -20,6 +20,6 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/empty_go_proto";
+option go_package = "github.com/google/tink/go/proto/empty_go_proto";
 
 message Empty {}
diff --git a/java_src/proto/hkdf_prf.proto b/java_src/proto/hkdf_prf.proto
index 3d3cbe9..38e69c5 100644
--- a/java_src/proto/hkdf_prf.proto
+++ b/java_src/proto/hkdf_prf.proto
@@ -22,12 +22,16 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hkdf_prf_proto";
+option go_package = "github.com/google/tink/go/proto/hkdf_prf_proto";
 
 message HkdfPrfParams {
   HashType hash = 1;
-  // Salt, optional in RFC 5869. Using "" is equivalent to zeros of length up to
-  // the block length of the HMac.
+  // Optional.
+  //
+  // An unspecified or zero-length value is equivalent to a sequence of zeros
+  // (0x00) with a length equal to the output size of hash.
+  //
+  // See https://rfc-editor.org/rfc/rfc5869.
   bytes salt = 2;
 }
 
diff --git a/java_src/proto/hmac.proto b/java_src/proto/hmac.proto
index 2733e51..bdd4e8a 100644
--- a/java_src/proto/hmac.proto
+++ b/java_src/proto/hmac.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_go_proto";
 
 message HmacParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/java_src/proto/hmac_prf.proto b/java_src/proto/hmac_prf.proto
index 7b3c52d..87ef97d 100644
--- a/java_src/proto/hmac_prf.proto
+++ b/java_src/proto/hmac_prf.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_prf_go_proto";
 
 message HmacPrfParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/java_src/proto/hpke.proto b/java_src/proto/hpke.proto
index 847864a..f794e77 100644
--- a/java_src/proto/hpke.proto
+++ b/java_src/proto/hpke.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hpke_proto";
+option go_package = "github.com/google/tink/go/proto/hpke_proto";
 
 enum HpkeKem {
   KEM_UNKNOWN = 0;
diff --git a/java_src/proto/jwt_ecdsa.proto b/java_src/proto/jwt_ecdsa.proto
index 4c80fe1..ce78b04 100644
--- a/java_src/proto/jwt_ecdsa.proto
+++ b/java_src/proto/jwt_ecdsa.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_ecdsa_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.4
 enum JwtEcdsaAlgorithm {
diff --git a/java_src/proto/jwt_hmac.proto b/java_src/proto/jwt_hmac.proto
index e54a51d..a499638 100644
--- a/java_src/proto/jwt_hmac.proto
+++ b/java_src/proto/jwt_hmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_hmac_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
 enum JwtHmacAlgorithm {
diff --git a/java_src/proto/jwt_rsa_ssa_pkcs1.proto b/java_src/proto/jwt_rsa_ssa_pkcs1.proto
index adf31c8..54a9731 100644
--- a/java_src/proto/jwt_rsa_ssa_pkcs1.proto
+++ b/java_src/proto/jwt_rsa_ssa_pkcs1.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.3
 enum JwtRsaSsaPkcs1Algorithm {
diff --git a/java_src/proto/jwt_rsa_ssa_pss.proto b/java_src/proto/jwt_rsa_ssa_pss.proto
index 4312645..eb2d454 100644
--- a/java_src/proto/jwt_rsa_ssa_pss.proto
+++ b/java_src/proto/jwt_rsa_ssa_pss.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.5
 enum JwtRsaSsaPssAlgorithm {
diff --git a/java_src/proto/kms_aead.proto b/java_src/proto/kms_aead.proto
index e818788..16de8ee 100644
--- a/java_src/proto/kms_aead.proto
+++ b/java_src/proto/kms_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_aead_go_proto";
 
 message KmsAeadKeyFormat {
   // Required.
diff --git a/java_src/proto/kms_envelope.proto b/java_src/proto/kms_envelope.proto
index fa806e6..8b0fd83 100644
--- a/java_src/proto/kms_envelope.proto
+++ b/java_src/proto/kms_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_envelope_go_proto";
 
 message KmsEnvelopeAeadKeyFormat {
   // Required.
diff --git a/java_src/proto/prf_based_deriver.proto b/java_src/proto/prf_based_deriver.proto
index 06dd334..e58a2cd 100644
--- a/java_src/proto/prf_based_deriver.proto
+++ b/java_src/proto/prf_based_deriver.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/prf_based_deriver_go_proto";
+option go_package = "github.com/google/tink/go/proto/prf_based_deriver_go_proto";
 
 message PrfBasedDeriverParams {
   KeyTemplate derived_key_template = 1;
diff --git a/java_src/proto/rsa_ssa_pkcs1.proto b/java_src/proto/rsa_ssa_pkcs1.proto
index 961189d..9797ee0 100644
--- a/java_src/proto/rsa_ssa_pkcs1.proto
+++ b/java_src/proto/rsa_ssa_pkcs1.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 message RsaSsaPkcs1Params {
   // Hash function used in computing hash of the signing message
diff --git a/java_src/proto/rsa_ssa_pss.proto b/java_src/proto/rsa_ssa_pss.proto
index 8e7903f..1150057 100644
--- a/java_src/proto/rsa_ssa_pss.proto
+++ b/java_src/proto/rsa_ssa_pss.proto
@@ -25,7 +25,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto";
 
 message RsaSsaPssParams {
   // Hash function used in computing hash of the signing message
diff --git a/java_src/proto/tink.proto b/java_src/proto/tink.proto
index 1787581..8b3d100 100644
--- a/java_src/proto/tink.proto
+++ b/java_src/proto/tink.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/tink_go_proto";
+option go_package = "github.com/google/tink/go/proto/tink_go_proto";
 option objc_class_prefix = "TINKPB";
 
 // Each instantiation of a Tink primitive is identified by type_url,
diff --git a/java_src/proto/xchacha20_poly1305.proto b/java_src/proto/xchacha20_poly1305.proto
index cc52624..a2613f1 100644
--- a/java_src/proto/xchacha20_poly1305.proto
+++ b/java_src/proto/xchacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/xchacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto";
 
 message XChaCha20Poly1305KeyFormat {
   uint32 version = 1;
diff --git a/java_src/src/main/java/com/google/crypto/tink/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/BUILD.bazel
index 25b39d5..ee46d04 100644
--- a/java_src/src/main/java/com/google/crypto/tink/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/BUILD.bazel
@@ -16,7 +16,6 @@
 java_library(
     name = "streaming_aead",
     srcs = ["StreamingAead.java"],
-    deps = ["@maven//:androidx_annotation_annotation"],
 )
 
 java_library(
@@ -117,6 +116,7 @@
     deps = [
         ":keyset_writer",
         "//proto:tink_java_proto",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -126,6 +126,7 @@
     deps = [
         ":keyset_writer-android",
         "//proto:tink_java_proto_lite",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -136,8 +137,8 @@
         ":keyset_writer",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -148,8 +149,8 @@
         ":keyset_writer-android",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/subtle:base64-android",
-        "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -171,7 +172,8 @@
     deps = [
         ":keyset_reader",
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -181,7 +183,8 @@
     deps = [
         ":keyset_reader-android",
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -192,10 +195,11 @@
         ":keyset_reader",
         ":util",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink/internal:json_parser",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@com_google_protobuf//:protobuf_javalite",
-        "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -206,10 +210,11 @@
         ":keyset_reader-android",
         ":util-android",
         "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink/internal:json_parser-android",
         "//src/main/java/com/google/crypto/tink/subtle:base64-android",
-        "@com_google_protobuf//:protobuf_javalite",
-        "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -219,7 +224,7 @@
     deps = [
         ":key_manager",
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -229,7 +234,7 @@
     deps = [
         ":key_manager-android",
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -239,7 +244,6 @@
     deps = [
         ":registry",
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
     ],
 )
 
@@ -249,7 +253,6 @@
     deps = [
         ":registry-android",
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
     ],
 )
 
@@ -257,9 +260,11 @@
     name = "key_template",
     srcs = ["KeyTemplate.java"],
     deps = [
+        ":parameters",
+        ":tink_proto_parameters_format",
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -267,9 +272,11 @@
     name = "key_template-android",
     srcs = ["KeyTemplate.java"],
     deps = [
+        ":parameters-android",
+        ":tink_proto_parameters_format-android",
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -323,7 +330,7 @@
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -337,7 +344,7 @@
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -348,7 +355,7 @@
         ":keyset_reader",
         ":registry_cluster",
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -359,7 +366,7 @@
         ":keyset_reader-android",
         ":registry_cluster-android",
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -373,7 +380,7 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -387,7 +394,7 @@
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -426,7 +433,7 @@
     srcs = ["KeyManager.java"],
     deps = [
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -435,7 +442,7 @@
     srcs = ["KeyManager.java"],
     deps = [
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -447,7 +454,7 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -459,7 +466,7 @@
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -472,8 +479,6 @@
         ":key",
         ":parameters",
         "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink/annotations:alpha",
-        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
@@ -492,8 +497,6 @@
         ":key-android",
         ":parameters-android",
         "//proto:tink_java_proto_lite",
-        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
-        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key-android",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations-android",
@@ -508,6 +511,7 @@
     srcs = ["Registry.java"],
     deps = [
         ":catalogue",
+        ":key",
         ":key_manager",
         ":key_manager_registry",
         ":key_template",
@@ -517,9 +521,10 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -528,6 +533,7 @@
     srcs = ["Registry.java"],
     deps = [
         ":catalogue-android",
+        ":key-android",
         ":key_manager-android",
         ":key_manager_registry-android",
         ":key_template-android",
@@ -537,9 +543,10 @@
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -551,6 +558,7 @@
     ],
     deps = [
         ":aead",
+        ":configuration",
         ":insecure_secret_key_access",
         ":key",
         ":key_status",
@@ -560,15 +568,17 @@
         ":parameters",
         ":primitive_set",
         ":registry",
+        ":tink_proto_parameters_format",
         ":util",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:internal_configuration",
         "//src/main/java/com/google/crypto/tink/internal:key_status_type_proto_converter",
-        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key",
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:registry_configuration",
         "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
         "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
@@ -577,9 +587,9 @@
         "//src/main/java/com/google/crypto/tink/tinkkey:secret_key_access",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:internal_key_handle",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -591,6 +601,7 @@
     ],
     deps = [
         ":aead-android",
+        ":configuration-android",
         ":insecure_secret_key_access-android",
         ":key-android",
         ":key_status-android",
@@ -600,15 +611,17 @@
         ":parameters-android",
         ":primitive_set-android",
         ":registry-android",
+        ":tink_proto_parameters_format-android",
         ":util-android",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:internal_configuration-android",
         "//src/main/java/com/google/crypto/tink/internal:key_status_type_proto_converter-android",
-        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key-android",
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters-android",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:registry_configuration-android",
         "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
         "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations-android",
@@ -617,9 +630,9 @@
         "//src/main/java/com/google/crypto/tink/tinkkey:secret_key_access-android",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:internal_key_handle-android",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -734,6 +747,24 @@
 )
 
 java_library(
+    name = "private_key",
+    srcs = ["PrivateKey.java"],
+    deps = [
+        ":key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+    ],
+)
+
+android_library(
+    name = "private_key-android",
+    srcs = ["PrivateKey.java"],
+    deps = [
+        ":key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+    ],
+)
+
+java_library(
     name = "parameters",
     srcs = ["Parameters.java"],
     deps = [
@@ -758,11 +789,11 @@
         ":key_manager",
         ":key_manager_impl",
         ":private_key_manager_impl",
-        "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -773,11 +804,11 @@
         ":key_manager-android",
         ":key_manager_impl-android",
         ":private_key_manager_impl-android",
-        "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -843,7 +874,6 @@
 android_library(
     name = "streaming_aead-android",
     srcs = ["StreamingAead.java"],
-    deps = ["@maven//:androidx_annotation_annotation"],
 )
 
 android_library(
@@ -868,3 +898,93 @@
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
+
+java_library(
+    name = "tink_proto_keyset_format",
+    srcs = ["TinkProtoKeysetFormat.java"],
+    deps = [
+        ":aead",
+        ":binary_keyset_reader",
+        ":binary_keyset_writer",
+        ":cleartext_keyset_handle",
+        ":registry_cluster",
+        ":secret_key_access",
+    ],
+)
+
+android_library(
+    name = "tink_proto_keyset_format-android",
+    srcs = ["TinkProtoKeysetFormat.java"],
+    deps = [
+        ":aead-android",
+        ":binary_keyset_reader-android",
+        ":binary_keyset_writer-android",
+        ":cleartext_keyset_handle-android",
+        ":registry_cluster-android",
+        ":secret_key_access-android",
+    ],
+)
+
+java_library(
+    name = "tink_json_proto_keyset_format",
+    srcs = ["TinkJsonProtoKeysetFormat.java"],
+    deps = [
+        ":aead",
+        ":cleartext_keyset_handle",
+        ":json_keyset_reader",
+        ":json_keyset_writer",
+        ":registry_cluster",
+        ":secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+    ],
+)
+
+android_library(
+    name = "tink_json_proto_keyset_format-android",
+    srcs = ["TinkJsonProtoKeysetFormat.java"],
+    deps = [
+        ":aead-android",
+        ":cleartext_keyset_handle-android",
+        ":json_keyset_reader-android",
+        ":json_keyset_writer-android",
+        ":registry_cluster-android",
+        ":secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+    ],
+)
+
+android_library(
+    name = "tink_proto_parameters_format-android",
+    srcs = ["TinkProtoParametersFormat.java"],
+    deps = [
+        ":parameters-android",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "tink_proto_parameters_format",
+    srcs = ["TinkProtoParametersFormat.java"],
+    deps = [
+        ":parameters",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "configuration-android",
+    srcs = ["Configuration.java"],
+)
+
+java_library(
+    name = "configuration",
+    srcs = ["Configuration.java"],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java b/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java
index b5d0f37..2e135cc 100644
--- a/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java
+++ b/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetReader.java
@@ -18,6 +18,7 @@
 
 import com.google.crypto.tink.proto.EncryptedKeyset;
 import com.google.crypto.tink.proto.Keyset;
+import com.google.errorprone.annotations.InlineMe;
 import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -55,9 +56,15 @@
    *
    * <p>Note: the input file won't be read until {@link BinaryKeysetReader#read} or {@link
    * BinaryKeysetReader#readEncrypted} is called.
+   *
+   * @deprecated Inline the function.
    */
+  @InlineMe(
+      replacement = "BinaryKeysetReader.withInputStream(new FileInputStream(file))",
+      imports = {"com.google.crypto.tink.BinaryKeysetReader", "java.io.FileInputStream"})
+  @Deprecated
   public static KeysetReader withFile(File file) throws IOException {
-    return new BinaryKeysetReader(new FileInputStream(file));
+    return withInputStream(new FileInputStream(file));
   }
 
   private BinaryKeysetReader(InputStream stream) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java b/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java
index 8d52cd0..224d6ad 100644
--- a/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/BinaryKeysetWriter.java
@@ -18,6 +18,7 @@
 
 import com.google.crypto.tink.proto.EncryptedKeyset;
 import com.google.crypto.tink.proto.Keyset;
+import com.google.errorprone.annotations.InlineMe;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -45,9 +46,17 @@
     return new BinaryKeysetWriter(stream);
   }
 
-  /** Static method to create a BinaryKeysetWriter that writes to a file. */
+  /**
+   * Static method to create a BinaryKeysetWriter that writes to a file.
+   *
+   * @deprecated Inline the function.
+   */
+  @InlineMe(
+      replacement = "BinaryKeysetWriter.withOutputStream(new FileOutputStream(file))",
+      imports = {"com.google.crypto.tink.BinaryKeysetWriter", "java.io.FileOutputStream"})
+  @Deprecated
   public static KeysetWriter withFile(File file) throws IOException {
-    return new BinaryKeysetWriter(new FileOutputStream(file));
+    return withOutputStream(new FileOutputStream(file));
   }
 
   @Override
diff --git a/java_src/src/main/java/com/google/crypto/tink/CleartextKeysetHandle.java b/java_src/src/main/java/com/google/crypto/tink/CleartextKeysetHandle.java
index 03ba22c..5afe499 100644
--- a/java_src/src/main/java/com/google/crypto/tink/CleartextKeysetHandle.java
+++ b/java_src/src/main/java/com/google/crypto/tink/CleartextKeysetHandle.java
@@ -39,8 +39,10 @@
   /**
    * @return a new {@link KeysetHandle} from {@code serialized} that is a serialized {@link Keyset}
    * @throws GeneralSecurityException
-   * @deprecated use {@link #read} instead
+   * @deprecated Call {@code TinkProtoKeysetFormat.parseKeyset(serialized,
+   *     InsecureSecretKeyAccess.get())} which has the same semantics.
    */
+  @SuppressWarnings("UnusedException")
   @Deprecated
   public static final KeysetHandle parseFrom(final byte[] serialized)
       throws GeneralSecurityException {
diff --git a/java_src/src/main/java/com/google/crypto/tink/Configuration.java b/java_src/src/main/java/com/google/crypto/tink/Configuration.java
new file mode 100644
index 0000000..42faa90
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/Configuration.java
@@ -0,0 +1,24 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+/**
+ * An object of this class represents a collection of algorithms that a user wants Tink to
+ * understand. For most users, one of the predefined {@link Configuration} objects is to be used at
+ * primitive creation time.
+ */
+public abstract class Configuration {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/JsonKeysetReader.java b/java_src/src/main/java/com/google/crypto/tink/JsonKeysetReader.java
index 14c7670..7e988b3 100644
--- a/java_src/src/main/java/com/google/crypto/tink/JsonKeysetReader.java
+++ b/java_src/src/main/java/com/google/crypto/tink/JsonKeysetReader.java
@@ -16,7 +16,7 @@
 
 package com.google.crypto.tink;
 
-import androidx.annotation.RequiresApi;
+import com.google.crypto.tink.internal.JsonParser;
 import com.google.crypto.tink.proto.EncryptedKeyset;
 import com.google.crypto.tink.proto.KeyData;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
@@ -25,19 +25,18 @@
 import com.google.crypto.tink.proto.KeysetInfo;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Base64;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.InlineMe;
 import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonReader;
 import com.google.protobuf.ByteString;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.StringReader;
 import java.nio.charset.Charset;
 import java.nio.file.Path;
 
@@ -51,13 +50,14 @@
 public final class JsonKeysetReader implements KeysetReader {
   private static final Charset UTF_8 = Charset.forName("UTF-8");
 
+  private static final long MAX_KEY_ID = 4294967295L;  // = 2^32 - 1
+  private static final long MIN_KEY_ID = Integer.MIN_VALUE;  // = - 2^31
+
   private final InputStream inputStream;
-  private final JsonObject json;
   private boolean urlSafeBase64 = false;
 
   private JsonKeysetReader(InputStream inputStream) {
     this.inputStream = inputStream;
-    json = null;
   }
 
   /**
@@ -66,7 +66,8 @@
    * <p>Note: the input stream won't be read until {@link JsonKeysetReader#read} or {@link
    * JsonKeysetReader#readEncrypted} is called.
    */
-  public static KeysetReader withInputStream(InputStream input) throws IOException {
+  @SuppressWarnings("CheckedExceptionNotThrown")
+  public static JsonKeysetReader withInputStream(InputStream input) throws IOException {
     return new JsonKeysetReader(input);
   }
 
@@ -75,6 +76,9 @@
    *
    * @deprecated Use {@code #withString}
    */
+  @InlineMe(
+      replacement = "JsonKeysetReader.withString(input.toString())",
+      imports = "com.google.crypto.tink.JsonKeysetReader")
   @Deprecated
   public static JsonKeysetReader withJsonObject(Object input) {
     return withString(input.toString());
@@ -95,9 +99,15 @@
    *
    * <p>Note: the file won't be read until {@link JsonKeysetReader#read} or {@link
    * JsonKeysetReader#readEncrypted} is called.
+   *
+   * @deprecated Method should be inlined.
    */
+  @InlineMe(
+      replacement = "JsonKeysetReader.withInputStream(new FileInputStream(file))",
+      imports = {"com.google.crypto.tink.JsonKeysetReader", "java.io.FileInputStream"})
+  @Deprecated
   public static JsonKeysetReader withFile(File file) throws IOException {
-    return new JsonKeysetReader(new FileInputStream(file));
+    return withInputStream(new FileInputStream(file));
   }
 
   /**
@@ -107,9 +117,19 @@
    * JsonKeysetReader#readEncrypted} is called.
    *
    * <p>This method only works on Android API level 26 or newer.
+   *
+   * @deprecated Method should be inlined.
    */
+  @InlineMe(
+      replacement = "JsonKeysetReader.withInputStream(new FileInputStream(new File(path)))",
+      imports = {
+        "com.google.crypto.tink.JsonKeysetReader",
+        "java.io.File",
+        "java.io.FileInputStream"
+      })
+  @Deprecated
   public static JsonKeysetReader withPath(String path) throws IOException {
-    return withFile(new File(path));
+    return withInputStream(new FileInputStream(new File(path)));
   }
 
   /**
@@ -119,12 +139,18 @@
    * JsonKeysetReader#readEncrypted} is called.
    *
    * <p>This method only works on Android API level 26 or newer.
+   *
+   * @deprecated Method should be inlined.
    */
-  @RequiresApi(26) // https://developer.android.com/reference/java/nio/file/Path
+  @InlineMe(
+      replacement = "JsonKeysetReader.withInputStream(new FileInputStream(path.toFile()))",
+      imports = {"com.google.crypto.tink.JsonKeysetReader", "java.io.FileInputStream"})
+  @Deprecated
   public static JsonKeysetReader withPath(Path path) throws IOException {
-    return withFile(path.toFile());
+    return JsonKeysetReader.withInputStream(new FileInputStream(path.toFile()));
   }
 
+  @CanIgnoreReturnValue
   public JsonKeysetReader withUrlSafeBase64() {
     this.urlSafeBase64 = true;
     return this;
@@ -133,14 +159,8 @@
   @Override
   public Keyset read() throws IOException {
     try {
-      if (json != null) {
-        return keysetFromJson(json);
-      } else {
-        JsonReader jsonReader = new JsonReader(
-            new StringReader(new String(Util.readAll(inputStream), UTF_8)));
-        jsonReader.setLenient(false);
-        return keysetFromJson(Streams.parse(jsonReader).getAsJsonObject());
-      }
+      return keysetFromJson(
+          JsonParser.parse(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject());
     } catch (JsonParseException | IllegalStateException e) {
       throw new IOException(e);
     } finally {
@@ -153,12 +173,8 @@
   @Override
   public EncryptedKeyset readEncrypted() throws IOException {
     try {
-      if (json != null) {
-        return encryptedKeysetFromJson(json);
-      } else {
-        return encryptedKeysetFromJson(
-            JsonParser.parseString(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject());
-      }
+      return encryptedKeysetFromJson(
+          JsonParser.parse(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject());
     } catch (JsonParseException | IllegalStateException e) {
       throw new IOException(e);
     } finally {
@@ -168,11 +184,25 @@
     }
   }
 
-  private Keyset keysetFromJson(JsonObject json) {
+  private static int getKeyId(JsonElement element) throws IOException {
+    long id;
+    try {
+      id = JsonParser.getParsedNumberAsLongOrThrow(element);
+    } catch (NumberFormatException e) {
+      throw new IOException(e);
+    }
+    if (id > MAX_KEY_ID || id < MIN_KEY_ID) {
+      throw new IOException("invalid key id");
+    }
+    // casts large unsigned int32 numbers to negative int32 numbers
+    return (int) element.getAsLong();
+  }
+
+  private Keyset keysetFromJson(JsonObject json) throws IOException {
     validateKeyset(json);
     Keyset.Builder builder = Keyset.newBuilder();
     if (json.has("primaryKeyId")) {
-      builder.setPrimaryKeyId(json.get("primaryKeyId").getAsInt());
+      builder.setPrimaryKeyId(getKeyId(json.get("primaryKeyId")));
     }
     JsonArray keys = json.getAsJsonArray("key");
     for (int i = 0; i < keys.size(); i++) {
@@ -181,7 +211,7 @@
     return builder.build();
   }
 
-  private EncryptedKeyset encryptedKeysetFromJson(JsonObject json) {
+  private EncryptedKeyset encryptedKeysetFromJson(JsonObject json) throws IOException {
     validateEncryptedKeyset(json);
     byte[] encryptedKeyset;
     if (urlSafeBase64) {
@@ -201,20 +231,20 @@
     }
   }
 
-  private Keyset.Key keyFromJson(JsonObject json) {
+  private Keyset.Key keyFromJson(JsonObject json) throws IOException {
     validateKey(json);
     return Keyset.Key.newBuilder()
         .setStatus(getStatus(json.get("status").getAsString()))
-        .setKeyId(json.get("keyId").getAsInt())
+        .setKeyId(getKeyId(json.get("keyId")))
         .setOutputPrefixType(getOutputPrefixType(json.get("outputPrefixType").getAsString()))
         .setKeyData(keyDataFromJson(json.getAsJsonObject("keyData")))
         .build();
   }
 
-  private static KeysetInfo keysetInfoFromJson(JsonObject json) {
+  private static KeysetInfo keysetInfoFromJson(JsonObject json) throws IOException {
     KeysetInfo.Builder builder = KeysetInfo.newBuilder();
     if (json.has("primaryKeyId")) {
-      builder.setPrimaryKeyId(json.get("primaryKeyId").getAsInt());
+      builder.setPrimaryKeyId(getKeyId(json.get("primaryKeyId")));
     }
     if (json.has("keyInfo")) {
       JsonArray keyInfos = json.getAsJsonArray("keyInfo");
@@ -225,10 +255,10 @@
     return builder.build();
   }
 
-  private static KeysetInfo.KeyInfo keyInfoFromJson(JsonObject json) {
+  private static KeysetInfo.KeyInfo keyInfoFromJson(JsonObject json) throws IOException {
     return KeysetInfo.KeyInfo.newBuilder()
         .setStatus(getStatus(json.get("status").getAsString()))
-        .setKeyId(json.get("keyId").getAsInt())
+        .setKeyId(getKeyId(json.get("keyId")))
         .setOutputPrefixType(getOutputPrefixType(json.get("outputPrefixType").getAsString()))
         .setTypeUrl(json.get("typeUrl").getAsString())
         .build();
diff --git a/java_src/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java b/java_src/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java
index 388f57e..2ad6c47 100644
--- a/java_src/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/JsonKeysetWriter.java
@@ -16,12 +16,12 @@
 
 package com.google.crypto.tink;
 
-import androidx.annotation.RequiresApi;
 import com.google.crypto.tink.proto.EncryptedKeyset;
 import com.google.crypto.tink.proto.KeyData;
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.KeysetInfo;
 import com.google.crypto.tink.subtle.Base64;
+import com.google.errorprone.annotations.InlineMe;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
@@ -57,24 +57,49 @@
     return new JsonKeysetWriter(stream);
   }
 
-  /** Static method to create a JsonKeysetWriter that writes to a file. */
+  /**
+   * Static method to create a JsonKeysetWriter that writes to a file.
+   *
+   * @deprecated Method should be inlined.
+   */
+  @InlineMe(
+      replacement = "JsonKeysetWriter.withOutputStream(new FileOutputStream(file))",
+      imports = {"com.google.crypto.tink.JsonKeysetWriter", "java.io.FileOutputStream"})
+  @Deprecated
   public static KeysetWriter withFile(File file) throws IOException {
-    return new JsonKeysetWriter(new FileOutputStream(file));
+    return withOutputStream(new FileOutputStream(file));
   }
 
-  /** Static method to create a JsonKeysetWriter that writes to a file path. */
+  /**
+   * Static method to create a JsonKeysetWriter that writes to a file path.
+   *
+   * @deprecated Method should be inlined.
+   */
+  @InlineMe(
+      replacement = "JsonKeysetWriter.withOutputStream(new FileOutputStream(new File(path)))",
+      imports = {
+        "com.google.crypto.tink.JsonKeysetWriter",
+        "java.io.File",
+        "java.io.FileOutputStream"
+      })
+  @Deprecated
   public static KeysetWriter withPath(String path) throws IOException {
-    return withFile(new File(path));
+    return withOutputStream(new FileOutputStream(new File(path)));
   }
 
   /**
    * Static method to create a JsonKeysetWriter that writes to a file path.
    *
    * <p>This method only works on Android API level 26 or newer.
+   *
+   * @deprecated Method should be inlined.
    */
-  @RequiresApi(26) // https://developer.android.com/reference/java/nio/file/Path
+  @InlineMe(
+      replacement = "JsonKeysetWriter.withOutputStream(new FileOutputStream(path.toFile()))",
+      imports = {"com.google.crypto.tink.JsonKeysetWriter", "java.io.FileOutputStream"})
+  @Deprecated
   public static KeysetWriter withPath(Path path) throws IOException {
-    return withFile(path.toFile());
+    return withOutputStream(new FileOutputStream(path.toFile()));
   }
 
   @Override
diff --git a/java_src/src/main/java/com/google/crypto/tink/KeyManager.java b/java_src/src/main/java/com/google/crypto/tink/KeyManager.java
index c664117..a7e01fd 100644
--- a/java_src/src/main/java/com/google/crypto/tink/KeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/KeyManager.java
@@ -35,7 +35,6 @@
  * @since 1.0.0
  */
 public interface KeyManager<P> {
-  // APIs for primitive development
 
   /**
    * Constructs an instance of P for the key given in {@code serializedKey}, which must be a
@@ -58,36 +57,72 @@
    * PublicKeyVerify}, {@code DeterministicAead}, {@code HybridEncrypt}, and {@code HybridDecrypt}
    * this should be a primitive which <b>ignores</b> the output prefix and assumes "RAW".
    *
+   * <p>This method is not used by Tink. It does not need to be implemented.
+   *
    * @return the new constructed P
    * @throws GeneralSecurityException if the key given in {@code key} is corrupted or not supported
+   * @deprecated Use {@code getPrimitive(serializedKey)} instead.
    */
-  P getPrimitive(MessageLite key) throws GeneralSecurityException;
+  @Deprecated // Unused Interface Method. Will be removed.
+  default P getPrimitive(MessageLite key) throws GeneralSecurityException {
+    throw new UnsupportedOperationException();
+  }
 
   /**
    * Generates a new key according to specification in {@code serializedKeyFormat}, which must be a
    * serialized key format protocol buffer handled by this manager.
    *
+   * <p>This method is not used by Tink anymore. It does not need to be implemented.
+   *
    * @return the new generated key
    * @throws GeneralSecurityException if the specified format is wrong or not supported
+   * @deprecated Use {@code newKeyData(serializedKeyFormat)} instead.
    */
-  MessageLite newKey(ByteString serializedKeyFormat) throws GeneralSecurityException;
+  @Deprecated // Unused Interface Method. Will be removed.
+  default MessageLite newKey(ByteString serializedKeyFormat) throws GeneralSecurityException {
+    throw new UnsupportedOperationException();
+  }
 
   /**
    * Generates a new key according to specification in {@code keyFormat}.
    *
+   * <p>This method is only used by {@link Registry.newKey} which is deprecated and not used by Tink
+   * anymore. This method does not need to be implemented.
+   *
    * @return the new generated key
    * @throws GeneralSecurityException if the specified format is wrong or not supported
+   * @deprecated Use {@code newKeyData(serializedKeyFormat)} instead.
    */
-  MessageLite newKey(MessageLite keyFormat) throws GeneralSecurityException;
+  @Deprecated // Unused Interface Method. Will be removed.
+  default MessageLite newKey(MessageLite keyFormat) throws GeneralSecurityException {
+    throw new UnsupportedOperationException();
+  }
 
-  /** @return true iff this KeyManager supports key type identified by {@code typeUrl}. */
-  boolean doesSupport(String typeUrl);
+  /**
+   * Returns true iff this KeyManager supports key type identified by {@code typeUrl}.
+   *
+   * <p>This method is not used by Tink anymore. It does not need to be implemented.
+   *
+   * @deprecated Use {@code getKeyType()} instead.
+   */
+  @Deprecated // Unused Interface Method. Will be removed.
+  default boolean doesSupport(String typeUrl) {
+    throw new UnsupportedOperationException();
+  }
 
-  /** @return the type URL that identifies the key type of keys managed by this KeyManager. */
+  /** Returns the type URL that identifies the key type of keys managed by this KeyManager. */
   String getKeyType();
 
-  /** @return the version number of this KeyManager. */
-  int getVersion();
+  /**
+   * Returns the version number of this KeyManager.
+   *
+   * <p>This method is not used by Tink anymore. It does not need to be implemented.
+   * @deprecated Do not use it.
+   */
+  @Deprecated // Unused Interface Method. Will be removed.
+  default int getVersion() {
+    throw new UnsupportedOperationException();
+  }
 
   /**
    * Returns the primitive class object of the P. Should be implemented as {@code return P.class;}
@@ -97,12 +132,8 @@
    */
   Class<P> getPrimitiveClass();
 
-  // APIs for Key Management
-
   /**
-   * Generates a new {@code KeyData} according to specification in {@code serializedkeyFormat}.
-   *
-   * <p>This should be used solely by {@link KeysetManager}.
+   * Generates a new {@code KeyData} according to specification in {@code serializedKeyFormat}.
    *
    * @return the new generated key
    * @throws GeneralSecurityException if the specified format is wrong or not supported
diff --git a/java_src/src/main/java/com/google/crypto/tink/KeyManagerRegistry.java b/java_src/src/main/java/com/google/crypto/tink/KeyManagerRegistry.java
index abdfc02..a96ce6e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/KeyManagerRegistry.java
+++ b/java_src/src/main/java/com/google/crypto/tink/KeyManagerRegistry.java
@@ -19,9 +19,6 @@
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
 import com.google.crypto.tink.internal.KeyTypeManager;
 import com.google.crypto.tink.internal.PrivateKeyTypeManager;
-import com.google.crypto.tink.proto.KeyData;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.MessageLite;
 import java.security.GeneralSecurityException;
 import java.util.Collections;
@@ -29,6 +26,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.logging.Logger;
+import javax.annotation.Nullable;
 
 /**
  * An internal API to register KeyManagers and KeyTypeManagers.
@@ -89,22 +87,13 @@
      */
     Class<?> publicKeyManagerClassOrNull();
 
-    /**
-     * Parses a key into a corresponding message lite. Only works if the key type has been
-     * registered with a KeyTypeManager, returns null otherwise.
-     *
-     * <p>Can throw exceptions if validation fails or if parsing fails.
-     */
-    MessageLite parseKey(ByteString serializedKey)
-        throws GeneralSecurityException, InvalidProtocolBufferException;
   }
 
   private static <P> KeyManagerContainer createContainerFor(KeyManager<P> keyManager) {
     final KeyManager<P> localKeyManager = keyManager;
     return new KeyManagerContainer() {
       @Override
-      public <Q> KeyManager<Q> getKeyManager(Class<Q> primitiveClass)
-          throws GeneralSecurityException {
+      public <Q> KeyManager<Q> getKeyManager(Class<Q> primitiveClass) {
         if (!localKeyManager.getPrimitiveClass().equals(primitiveClass)) {
           throw new InternalError(
               "This should never be called, as we always first check supportedPrimitives.");
@@ -130,15 +119,10 @@
       }
 
       @Override
+      @Nullable
       public Class<?> publicKeyManagerClassOrNull() {
         return null;
       }
-
-      @Override
-      public MessageLite parseKey(ByteString serializedKey)
-          throws GeneralSecurityException, InvalidProtocolBufferException {
-        return null;
-      }
     };
   }
 
@@ -173,17 +157,10 @@
       }
 
       @Override
+      @Nullable
       public Class<?> publicKeyManagerClassOrNull() {
         return null;
       }
-
-      @Override
-      public MessageLite parseKey(ByteString serializedKey)
-          throws GeneralSecurityException, InvalidProtocolBufferException {
-        KeyProtoT result = localKeyManager.parseKey(serializedKey);
-        localKeyManager.validateKey(result);
-        return result;
-      }
     };
   }
 
@@ -228,14 +205,6 @@
       public Class<?> publicKeyManagerClassOrNull() {
         return localPublicKeyManager.getClass();
       }
-
-      @Override
-      public MessageLite parseKey(ByteString serializedKey)
-          throws GeneralSecurityException, InvalidProtocolBufferException {
-        KeyProtoT result = localPrivateKeyManager.parseKey(serializedKey);
-        localPrivateKeyManager.validateKey(result);
-        return result;
-      }
     };
   }
 
@@ -247,15 +216,7 @@
     return keyManagerMap.get(typeUrl);
   }
 
-  /** Helper method to check if an instance is not null; taken from guava's Precondition.java */
-  private static <T> T checkNotNull(T reference) {
-    if (reference == null) {
-      throw new NullPointerException();
-    }
-    return reference;
-  }
-
-  private synchronized <P> void registerKeyManagerContainer(
+  private synchronized void registerKeyManagerContainer(
       final KeyManagerContainer containerToInsert, boolean forceOverwrite)
       throws GeneralSecurityException {
     String typeUrl = containerToInsert.getUntypedKeyManager().getKeyType();
@@ -387,32 +348,12 @@
   }
 
   /**
-   * Should not be used since the API is a misuse of Java generics.
-   *
-   * @deprecated
-   */
-  @Deprecated
-  <P> KeyManager<P> getKeyManager(String typeUrl) throws GeneralSecurityException {
-    return getKeyManagerInternal(typeUrl, null);
-  }
-
-  /**
    * @return a {@link KeyManager} for the given {@code typeUrl} and {@code primitiveClass}(if found
    *     and this key type supports this primitive).
    */
   <P> KeyManager<P> getKeyManager(String typeUrl, Class<P> primitiveClass)
       throws GeneralSecurityException {
-    return getKeyManagerInternal(typeUrl, checkNotNull(primitiveClass));
-  }
-
-  private <P> KeyManager<P> getKeyManagerInternal(String typeUrl, Class<P> primitiveClass)
-      throws GeneralSecurityException {
     KeyManagerContainer container = getKeyManagerContainerOrThrow(typeUrl);
-    if (primitiveClass == null) {
-      @SuppressWarnings("unchecked") // Only called from deprecated functions; unavoidable there.
-      KeyManager<P> result = (KeyManager<P>) container.getUntypedKeyManager();
-      return result;
-    }
     if (container.supportedPrimitives().contains(primitiveClass)) {
       return container.getKeyManager(primitiveClass);
     }
@@ -433,18 +374,6 @@
     return container.getUntypedKeyManager();
   }
 
-  /**
-   * Parses the key in a keyData to the corresponding proto message, or returns null.
-   *
-   * <p>Parsing happens if the key type was registered with a KeyTypeManager. If a legacy KeyManager
-   * was used, this returns null.
-   */
-  MessageLite parseKeyData(KeyData keyData)
-      throws GeneralSecurityException, InvalidProtocolBufferException {
-    KeyManagerContainer container = getKeyManagerContainerOrThrow(keyData.getTypeUrl());
-    return container.parseKey(keyData.getValue());
-  }
-
   boolean isEmpty() {
     return keyManagerMap.isEmpty();
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/KeyTemplate.java b/java_src/src/main/java/com/google/crypto/tink/KeyTemplate.java
index 1264403..8a5112c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/KeyTemplate.java
+++ b/java_src/src/main/java/com/google/crypto/tink/KeyTemplate.java
@@ -18,6 +18,7 @@
 
 import com.google.errorprone.annotations.Immutable;
 import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
 
 /** A KeyTemplate specifies how to generate keys of a particular type. */
 @Immutable
@@ -105,4 +106,8 @@
   public OutputPrefixType getOutputPrefixType() {
     return fromProto(kt.getOutputPrefixType());
   }
+
+  public Parameters toParameters() throws GeneralSecurityException {
+    return TinkProtoParametersFormat.parse(kt.toByteArray());
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java b/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java
index 13579e7..234b569 100644
--- a/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java
+++ b/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java
@@ -16,13 +16,13 @@
 
 package com.google.crypto.tink;
 
-
 import com.google.crypto.tink.annotations.Alpha;
-import com.google.crypto.tink.internal.LegacyProtoKey;
+import com.google.crypto.tink.internal.InternalConfiguration;
 import com.google.crypto.tink.internal.LegacyProtoParameters;
 import com.google.crypto.tink.internal.MutableSerializationRegistry;
 import com.google.crypto.tink.internal.ProtoKeySerialization;
 import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.internal.RegistryConfiguration;
 import com.google.crypto.tink.internal.TinkBugException;
 import com.google.crypto.tink.monitoring.MonitoringAnnotations;
 import com.google.crypto.tink.proto.EncryptedKeyset;
@@ -35,6 +35,7 @@
 import com.google.crypto.tink.tinkkey.KeyHandle;
 import com.google.crypto.tink.tinkkey.internal.InternalKeyHandle;
 import com.google.crypto.tink.tinkkey.internal.ProtoKey;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
@@ -76,12 +77,10 @@
    *   <li>By importing an existing key, with {@link KeysetHandle#importKey}
    * </ul>
    *
-   * . 7
-   *
    * <p>All these functions return a {@code KeysetBuilder.Entry}. It is necessary to assign an ID to
-   * a new entry by calling one of {@link Entry#withNextId}, {@link Entry#withFixedId}, or {@link
-   * Entry#withRandomId}. The exception is when an existing key which has an id requirement is
-   * imported (in which case the required ID is used).
+   * a new entry by calling one of {@link Entry#withFixedId} or {@link Entry#withRandomId}. The
+   * exception is when an existing key which has an id requirement is imported (in which case the
+   * required ID is used).
    *
    * <p>It is possible to set the status of an entry by calling {@link Entry#setStatus}. The Status
    * defaults to {@code ENABLED}.
@@ -157,6 +156,7 @@
        * been added to a builder, otherwise they will marked as non-primary once this entry is added
        * to a builder.
        */
+      @CanIgnoreReturnValue
       public Entry makePrimary() {
         if (builder != null) {
           builder.clearPrimary();
@@ -171,6 +171,7 @@
       }
 
       /** Sets the status of this entry. */
+      @CanIgnoreReturnValue
       public Entry setStatus(KeyStatus status) {
         keyStatus = status;
         return this;
@@ -182,6 +183,7 @@
       }
 
       /** Tells Tink to assign a fixed id when this keyset is built. */
+      @CanIgnoreReturnValue
       public Entry withFixedId(int id) {
         this.strategy = KeyIdStrategy.fixedId(id);
         return this;
@@ -196,6 +198,7 @@
        * <p>If an entry is marked as {@code withRandomId}, all subsequent entries also need to be
        * marked with {@code withRandomId}, or else calling {@code build()} will fail.
        */
+      @CanIgnoreReturnValue
       public Entry withRandomId() {
         this.strategy = KeyIdStrategy.randomId();
         return this;
@@ -203,6 +206,10 @@
     }
 
     private final List<KeysetHandle.Builder.Entry> entries = new ArrayList<>();
+    // If set, throw this error on BUILD instead of actually building.
+    @Nullable private GeneralSecurityException errorToThrow = null;
+    private MonitoringAnnotations annotations = MonitoringAnnotations.EMPTY;
+    private boolean buildCalled = false;
 
     private void clearPrimary() {
       for (Builder.Entry entry : entries) {
@@ -211,6 +218,7 @@
     }
 
     /** Adds an entry to a keyset */
+    @CanIgnoreReturnValue
     public KeysetHandle.Builder addEntry(KeysetHandle.Builder.Entry entry) {
       if (entry.builder != null) {
         throw new IllegalStateException("Entry has already been added to a KeysetHandle.Builder");
@@ -223,6 +231,21 @@
       return this;
     }
 
+    /**
+     * Sets MonitoringAnnotations. If not called, then the default value of {@link
+     * MonitoringAnnotations.EMPTY} is used.
+     *
+     * <p>When called twice, the last submitted annotations are used to create the keyset. This
+     * method is not thread-safe, and in case of multithreaded access it cannot be guaranteed which
+     * annotations get set.
+     */
+    @CanIgnoreReturnValue
+    @Alpha
+    public KeysetHandle.Builder setMonitoringAnnotations(MonitoringAnnotations annotations) {
+      this.annotations = annotations;
+      return this;
+    }
+
     /** Returns the number of entries in this builder. */
     public int size() {
       return entries.size();
@@ -237,11 +260,28 @@
       return entries.get(i);
     }
 
-    /** Removes the entry at index {@code i}. */
+    /**
+     * Removes the entry at index {@code i} and returns that entry. Shifts any subsequent entries to
+     * the left (subtracts one from their indices).
+     *
+     * @deprecated Use {@link #deleteAt} or {@link #getAt} instead.
+     */
+    @CanIgnoreReturnValue
+    @Deprecated
     public Builder.Entry removeAt(int i) {
       return entries.remove(i);
     }
 
+    /**
+     * Deletes the entry at index {@code i}. Shifts any subsequent entries to the left (subtracts
+     * one from their indices).
+     */
+    @CanIgnoreReturnValue
+    public KeysetHandle.Builder deleteAt(int i) {
+      entries.remove(i);
+      return this;
+    }
+
     private static void checkIdAssignments(List<KeysetHandle.Builder.Entry> entries)
         throws GeneralSecurityException {
       // We want "withRandomId"-entries after fixed id, as otherwise it might be that we randomly
@@ -258,6 +298,10 @@
       }
     }
 
+    private void setErrorToThrow(GeneralSecurityException errorToThrow) {
+      this.errorToThrow = errorToThrow;
+    }
+
     private static int randomIdNotInSet(Set<Integer> ids) {
       int id = 0;
       while (id == 0 || ids.contains(id)) {
@@ -307,16 +351,10 @@
         return createKeyFromParameters(
             builderEntry.parameters, id, serializeStatus(builderEntry.getStatus()));
       } else {
-        ProtoKeySerialization serializedKey;
-        if (builderEntry.key instanceof LegacyProtoKey) {
-          serializedKey =
-              ((LegacyProtoKey) builderEntry.key).getSerialization(InsecureSecretKeyAccess.get());
-        } else {
-          serializedKey =
-              MutableSerializationRegistry.globalInstance()
-                  .serializeKey(
-                      builderEntry.key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
-        }
+        ProtoKeySerialization serializedKey =
+            MutableSerializationRegistry.globalInstance()
+                .serializeKey(
+                    builderEntry.key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
         @Nullable Integer idRequirement = serializedKey.getIdRequirementOrNull();
         if (idRequirement != null && idRequirement != id) {
           throw new GeneralSecurityException("Wrong ID set for key with ID requirement");
@@ -338,11 +376,20 @@
      *       withRandomId}-entry
      *   <li>There are two entries with the same {@code withFixedId} (including pre-existing keys
      *       and imported keys which have an id requirement).
-     *   <li>There is a {@code withNextId} entry, but there previously was an entry which has an ID
-     *       of {@code MAX_INTEGER}.
+     *   <li>{@code build()} was previously called for {@code withRandomId} entries,
+     *       and hence calling {@code build()} twice would result in a keyset with different
+     *       key IDs.
      * </ul>
      */
     public KeysetHandle build() throws GeneralSecurityException {
+      if (errorToThrow != null) {
+        throw new GeneralSecurityException(
+            "Cannot build keyset due to error in original", errorToThrow);
+      }
+      if (buildCalled) {
+        throw new GeneralSecurityException("KeysetHandle.Builder#build must only be called once");
+      }
+      buildCalled = true;
       Keyset.Builder keysetBuilder = Keyset.newBuilder();
       Integer primaryId = null;
 
@@ -371,7 +418,7 @@
         throw new GeneralSecurityException("No primary was set");
       }
       keysetBuilder.setPrimaryKeyId(primaryId);
-      return new KeysetHandle(keysetBuilder.build());
+      return KeysetHandle.fromKeysetAndAnnotations(keysetBuilder.build(), annotations);
     }
   }
 
@@ -467,7 +514,32 @@
         .build();
   }
 
-  static ProtoKeySerialization toProtoKeySerialization(Keyset.Key protoKey) {
+  /**
+   * Returns an immutable list of key objects for this keyset.
+   *
+   * <p>If a status is unparseable or parsing of a key fails, there will be "null" in the
+   * corresponding entry.
+   */
+  private static List<Entry> getEntriesFromKeyset(Keyset keyset) {
+    List<Entry> result = new ArrayList<>(keyset.getKeyCount());
+    for (Keyset.Key protoKey : keyset.getKeyList()) {
+      int id = protoKey.getKeyId();
+      ProtoKeySerialization protoKeySerialization = toProtoKeySerialization(protoKey);
+      try {
+        Key key =
+            MutableSerializationRegistry.globalInstance()
+                .parseKeyWithLegacyFallback(protoKeySerialization, InsecureSecretKeyAccess.get());
+        result.add(
+            new KeysetHandle.Entry(
+                key, parseStatus(protoKey.getStatus()), id, id == keyset.getPrimaryKeyId()));
+      } catch (GeneralSecurityException e) {
+        result.add(null);
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  private static ProtoKeySerialization toProtoKeySerialization(Keyset.Key protoKey) {
     int id = protoKey.getKeyId();
     @Nullable
     Integer idRequirement = protoKey.getOutputPrefixType() == OutputPrefixType.RAW ? null : id;
@@ -485,29 +557,21 @@
   }
 
   private KeysetHandle.Entry entryByIndex(int i) {
-    Keyset.Key protoKey = keyset.getKey(i);
-    int id = protoKey.getKeyId();
-
-    ProtoKeySerialization protoKeySerialization = toProtoKeySerialization(protoKey);
-    Key key =
-        MutableSerializationRegistry.globalInstance()
-            .parseKeyWithLegacyFallback(protoKeySerialization, InsecureSecretKeyAccess.get());
-    try {
-      return new KeysetHandle.Entry(
-          key, parseStatus(protoKey.getStatus()), id, id == keyset.getPrimaryKeyId());
-    } catch (GeneralSecurityException e) {
-      // This may happen if a keyset without status makes it here; we should reject
-      // such keysets earlier instead.
-      throw new IllegalStateException("Creating an entry failed", e);
+    if (entries.get(i) == null) {
+      // This may happen if a keyset without status makes it here; or if a key has a parser
+      // registered but parsing fails. We should reject such keysets earlier instead.
+      throw new IllegalStateException(
+          "Keyset-Entry at position " + i + " has wrong status or key parsing failed");
     }
+    return entries.get(i);
   }
 
   /**
    * Creates a new entry with a fixed key.
    *
    * <p>If the Key has an IdRequirement, the default will be fixed to this ID. Otherwise, the user
-   * has to specify the ID to be used and call one of {@code withFixedId(i)}, {@code
-   * withRandomId()}, or {@code withNextId()} should on the returned entry.
+   * has to specify the ID to be used and call one of {@code withFixedId(i)} or {@code
+   * withRandomId()} on the returned entry.
    */
   public static KeysetHandle.Builder.Entry importKey(Key key) {
     KeysetHandle.Builder.Entry importedEntry = new KeysetHandle.Builder.Entry(key);
@@ -518,6 +582,13 @@
     return importedEntry;
   }
 
+  /**
+   * Creates a new entry with Status "ENABLED" and a new key created from the named parameters. No
+   * ID is set.
+   *
+   * <p>{@code namedParameters} is the key template name that fully specifies the parameters, e.g.
+   * "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM".
+   */
   public static KeysetHandle.Builder.Entry generateEntryFromParametersName(String namedParameters)
       throws GeneralSecurityException {
     if (!Registry.keyTemplateMap().containsKey(namedParameters)) {
@@ -532,20 +603,33 @@
     return new KeysetHandle.Builder.Entry(parameters);
   }
 
+  /**
+   * Creates a new entry with Status "ENABLED" and a new key created from the parameters. No ID is
+   * set.
+   */
   public static KeysetHandle.Builder.Entry generateEntryFromParameters(Parameters parameters) {
     return new KeysetHandle.Builder.Entry(parameters);
   }
 
   private final Keyset keyset;
+  /* Note: this should be List<@Nullable Entry>; but since we use the Nullable annotation from
+   * javax.annotation it is not possible to do this.
+   *
+   * Contains all entries; but if either parsing the status or the key failed, contains null.
+   */
+  private final List<Entry> entries;
   private final MonitoringAnnotations annotations;
 
-  private KeysetHandle(Keyset keyset) {
+  private KeysetHandle(Keyset keyset, List<Entry> entries) {
     this.keyset = keyset;
+    this.entries = entries;
     this.annotations = MonitoringAnnotations.EMPTY;
   }
 
-  private KeysetHandle(Keyset keyset, MonitoringAnnotations annotations) {
+  private KeysetHandle(
+      Keyset keyset, List<Entry> entries, MonitoringAnnotations annotations) {
     this.keyset = keyset;
+    this.entries = entries;
     this.annotations = annotations;
   }
 
@@ -555,7 +639,9 @@
    */
   static final KeysetHandle fromKeyset(Keyset keyset) throws GeneralSecurityException {
     assertEnoughKeyMaterial(keyset);
-    return new KeysetHandle(keyset);
+    List<Entry> entries = getEntriesFromKeyset(keyset);
+
+    return new KeysetHandle(keyset, entries);
   }
 
   /**
@@ -565,12 +651,11 @@
   static final KeysetHandle fromKeysetAndAnnotations(
       Keyset keyset, MonitoringAnnotations annotations) throws GeneralSecurityException {
     assertEnoughKeyMaterial(keyset);
-    return new KeysetHandle(keyset, annotations);
+    List<Entry> entries = getEntriesFromKeyset(keyset);
+    return new KeysetHandle(keyset, entries, annotations);
   }
 
-  /**
-   * @return the actual keyset data.
-   */
+  /** Returns the actual keyset data. */
   Keyset getKeyset() {
     return keyset;
   }
@@ -584,7 +669,17 @@
   public static Builder newBuilder(KeysetHandle handle) {
     Builder builder = new Builder();
     for (int i = 0; i < handle.size(); ++i) {
-      KeysetHandle.Entry entry = handle.entryByIndex(i);
+      // Currently, this can be null (as old APIs do not check validity e.g. in {@link #fromKeyset})
+      @Nullable KeysetHandle.Entry entry = handle.entries.get(i);
+      if (entry == null) {
+        builder.setErrorToThrow(
+            new GeneralSecurityException(
+                "Keyset-Entry in original keyset at position "
+                    + i
+                    + " has wrong status or key parsing failed"));
+        break;
+      }
+
       KeysetHandle.Builder.Entry builderEntry =
           importKey(entry.getKey()).withFixedId(entry.getId());
       builderEntry.setStatus(entry.getStatus());
@@ -626,7 +721,13 @@
    * entries were inserted when the KeysetHandle was built.
    *
    * <p>Currently, this may throw "IllegalStateException" in case the status entry of the Key in the
-   * keyset was wrongly set. In the future, Tink will throw at parsing time in this case.
+   * keyset was wrongly set. In this case, we call this KeysetHandle invalid. In the future, Tink
+   * will throw at parsing time in this case, and we will not have invalid KeysetHandles.
+   *
+   * <p>If you want to ensure that this does not throw an IllegalStateException, please first
+   * re-parse the KeysetHandle: {@code KeysetHandle guaranteedValid =
+   * KeysetHandle.newBuilder(maybeInvalidHandle).build();} (This would throw a {@code
+   * GeneralSecurityException} if the {@code maybeInvalidHandle} handle is invalid).
    *
    * @throws IndexOutOfBoundsException if i < 0 or i >= size();
    */
@@ -637,7 +738,11 @@
     return entryByIndex(i);
   }
 
-  /** Returns the keyset data as a list of {@link KeyHandle}s. */
+  /**
+   * Returns the keyset data as a list of {@link KeyHandle}s.
+   *
+   * Please do not use this function in new code. Instead, use {@link #getAt}.
+   */
   public List<KeyHandle> getKeys() {
     ArrayList<KeyHandle> result = new ArrayList<>();
     for (Keyset.Key key : keyset.getKeyList()) {
@@ -652,43 +757,65 @@
   }
 
   /**
-   * @return the {@link com.google.crypto.tink.proto.KeysetInfo} that doesn't contain actual key
-   *     material.
+   * Returns the {@link com.google.crypto.tink.proto.KeysetInfo} that doesn't contain actual key
+   * material.
    */
   public KeysetInfo getKeysetInfo() {
     return Util.getKeysetInfo(keyset);
   }
 
   /**
-   * Generates a new {@link KeysetHandle} that contains a single fresh key generated according to
-   * {@code keyTemplate}.
+   * Generates a new {@link KeysetHandle} that contains a single fresh key generated key with the
+   * given {@code Parameters} object.
    *
-   * @throws GeneralSecurityException if the key template is invalid.
-   * @deprecated This method takes a KeyTemplate proto, which is an internal implementation detail.
-   *     Please use the generateNew method that takes a {@link KeyTemplate} POJO.
+   * @throws GeneralSecurityException if no generation method for the given {@code parameters} has
+   *     been registered.
    */
-  @Deprecated
-  public static final KeysetHandle generateNew(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
+  public static final KeysetHandle generateNew(Parameters parameters)
       throws GeneralSecurityException {
-    return KeysetManager.withEmptyKeyset().rotate(keyTemplate).getKeysetHandle();
+    return KeysetHandle.newBuilder()
+        .addEntry(KeysetHandle.generateEntryFromParameters(parameters).withRandomId().makePrimary())
+        .build();
   }
 
   /**
    * Generates a new {@link KeysetHandle} that contains a single fresh key generated according to
    * {@code keyTemplate}.
    *
+   * <p>Please do not use this function. Instead, use {@link #generateNew(Parameters)}.
+   *
+   * <p>For existing usage, try to use refaster
+   * https://github.com/tink-crypto/tink-java/tree/main/tools/refaster to replace usage
+   * automatically. This will replaces calls {@code KeysetHandle.generateNew(XYZKeyTemplates.ABC);}
+   * with {@code KeysetHandle.generateNew(PredefinedXYZParameters.ABC);} which is a NO-OP.
+   *
+   * <p>If this is not possible, please inline the function in your code.
+   *
+   * @throws GeneralSecurityException if the key template is invalid.
+   */
+  public static final KeysetHandle generateNew(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
+      throws GeneralSecurityException {
+    return generateNew(TinkProtoParametersFormat.parse(keyTemplate.toByteArray()));
+  }
+
+  /**
+   * Generates a new {@link KeysetHandle} that contains a single fresh key generated according to
+   * {@code keyTemplate}.
+   *
+   * <p>Please do not use this function. Instead, inline it: replace calls with {@code
+   * generateNew(t)} with {@code generateNew(t.toParameters())}.
+   *
    * @throws GeneralSecurityException if the key template is invalid.
    */
   public static final KeysetHandle generateNew(KeyTemplate keyTemplate)
       throws GeneralSecurityException {
-    return KeysetManager.withEmptyKeyset().rotate(keyTemplate.getProto()).getKeysetHandle();
+    return generateNew(keyTemplate.toParameters());
   }
 
   /**
    * Returns a {@code KeysetHandle} that contains the single {@code KeyHandle} passed as input.
    *
-   * @deprecated Use {@code KeysetManager.withEmptyKeyset().add(keyHandle)
-   *     .setPrimary(keyHandle.getId()).getKeysetHandle()} instead.
+   * @deprecated Use {@link KeysetHandle.Builder.addEntry} instead.
    */
   @Deprecated
   public static final KeysetHandle createFromKey(KeyHandle keyHandle, KeyAccess access)
@@ -729,7 +856,7 @@
       throws GeneralSecurityException, IOException {
     EncryptedKeyset encryptedKeyset = reader.readEncrypted();
     assertEnoughEncryptedKeyMaterial(encryptedKeyset);
-    return new KeysetHandle(decrypt(encryptedKeyset, masterKey, associatedData));
+    return KeysetHandle.fromKeyset(decrypt(encryptedKeyset, masterKey, associatedData));
   }
 
   /**
@@ -745,14 +872,14 @@
   @SuppressWarnings("UnusedException")
   public static final KeysetHandle readNoSecret(KeysetReader reader)
       throws GeneralSecurityException, IOException {
+    byte[] serializedKeyset;
     try {
-      Keyset keyset = reader.read();
-      assertNoSecretKeyMaterial(keyset);
-      return KeysetHandle.fromKeyset(keyset);
+      serializedKeyset = reader.read().toByteArray();
     } catch (InvalidProtocolBufferException e) {
       // Do not propagate InvalidProtocolBufferException to guarantee no key material is leaked
       throw new GeneralSecurityException("invalid keyset");
     }
+    return readNoSecret(serializedKeyset);
   }
 
   /**
@@ -762,6 +889,8 @@
    * <p>This can be used to load public keysets or envelope encryption keysets. Users that need to
    * load cleartext keysets can use {@link CleartextKeysetHandle}.
    *
+   * <p>Note: new code should call {@code TinkProtoKeysetFormat(serialized)} instead.
+   *
    * @return a new {@link KeysetHandle} from {@code serialized} that is a serialized {@link Keyset}
    * @throws GeneralSecurityException if the keyset is invalid
    */
@@ -847,8 +976,7 @@
       // check emptiness here too, in case the encrypted keys unwrapped to nothing?
       assertEnoughKeyMaterial(keyset);
       return keyset;
-    } catch (
-        InvalidProtocolBufferException e) {
+    } catch (InvalidProtocolBufferException e) {
       // Do not propagate InvalidProtocolBufferException to guarantee no key material is leaked
       throw new GeneralSecurityException("invalid keyset, corrupted key material");
     }
@@ -857,7 +985,7 @@
   /**
    * If the managed keyset contains private keys, returns a {@link KeysetHandle} of the public keys.
    *
-   * @throws GenernalSecurityException if the managed keyset is null or if it contains any
+   * @throws GeneralSecurityException if the managed keyset is null or if it contains any
    *     non-private keys.
    */
   public KeysetHandle getPublicKeysetHandle() throws GeneralSecurityException {
@@ -870,7 +998,7 @@
       keysetBuilder.addKey(key.toBuilder().setKeyData(keyData).build());
     }
     keysetBuilder.setPrimaryKeyId(keyset.getPrimaryKeyId());
-    return new KeysetHandle(keysetBuilder.build());
+    return KeysetHandle.fromKeyset(keysetBuilder.build());
   }
 
   private static KeyData createPublicKeyData(KeyData privateKeyData)
@@ -887,7 +1015,16 @@
   @SuppressWarnings("deprecation")
   private static void validate(KeyData keyData) throws GeneralSecurityException {
     // This will throw GeneralSecurityException if the keyData is invalid.
-    Registry.getPrimitive(keyData);
+    // Note: this calls a deprecated function to validate the "KeyData" proto. The usage of this
+    // deprecated function is unfortunate. However, in the end we simply want to remove this call.
+    // The only usage of this is in "getPublicKeysetHandle". This should go away, in principle
+    // the code of getPublicKeysetHandle should simply look at each entry, cast each key to
+    // {@link PrivateKey} (throw a GeneralSecurityException if this fails), call getPublicKey()
+    // and insert the result into a new keyset with the same ID and status, then return the result.
+    // If done like this, there is no reason to validate the returned Key object.
+    // (However, also note that this particular call here isn't very problematic; the problematic
+    // part of Registry.getPrimitive is that it misuses generics, but here we just want any Object).
+    Object unused = Registry.getPrimitive(keyData);
   }
 
   /**
@@ -941,23 +1078,58 @@
     }
   }
 
-  /** Helper function to allow us to have a a name {@code B} for the base primitive. */
+  /** Allows us to have a name {@code B} for the base primitive. */
   private <B, P> P getPrimitiveWithKnownInputPrimitive(
-      Class<P> classObject, Class<B> inputPrimitiveClassObject) throws GeneralSecurityException {
+      InternalConfiguration config, Class<P> classObject, Class<B> inputPrimitiveClassObject)
+      throws GeneralSecurityException {
     Util.validateKeyset(keyset);
     PrimitiveSet.Builder<B> builder = PrimitiveSet.newBuilder(inputPrimitiveClassObject);
     builder.setAnnotations(annotations);
-    for (Keyset.Key key : keyset.getKeyList()) {
-      if (key.getStatus() == KeyStatusType.ENABLED) {
-        B primitive = Registry.getPrimitive(key.getKeyData(), inputPrimitiveClassObject);
-        if (key.getKeyId() == keyset.getPrimaryKeyId()) {
-          builder.addPrimaryPrimitive(primitive, key);
+    for (int i = 0; i < size(); ++i) {
+      Keyset.Key protoKey = keyset.getKey(i);
+      if (protoKey.getStatus().equals(KeyStatusType.ENABLED)) {
+        @Nullable
+        B primitive = getLegacyPrimitiveOrNull(config, protoKey, inputPrimitiveClassObject);
+        @Nullable B fullPrimitive = null;
+        // Entries.get(i) may be null (if the status is invalid in the proto, or parsing failed).
+        if (entries.get(i) != null) {
+          fullPrimitive =
+              getFullPrimitiveOrNull(config, entries.get(i).getKey(), inputPrimitiveClassObject);
+        }
+        if (fullPrimitive == null && primitive == null) {
+          throw new GeneralSecurityException(
+              "Unable to get primitive "
+                  + inputPrimitiveClassObject
+                  + " for key of type "
+                  + protoKey.getKeyData().getTypeUrl());
+        }
+        if (protoKey.getKeyId() == keyset.getPrimaryKeyId()) {
+          builder.addPrimaryFullPrimitiveAndOptionalPrimitive(fullPrimitive, primitive, protoKey);
         } else {
-          builder.addPrimitive(primitive, key);
+          builder.addFullPrimitiveAndOptionalPrimitive(fullPrimitive, primitive, protoKey);
         }
       }
     }
-    return Registry.wrap(builder.build(), classObject);
+    return config.wrap(builder.build(), classObject);
+  }
+
+  /**
+   * Returns a primitive from this keyset using the provided {@link Configuration} to create
+   * resources used in creating the primitive.
+   */
+  public <P> P getPrimitive(Configuration configuration, Class<P> targetClassObject)
+      throws GeneralSecurityException {
+    if (!(configuration instanceof InternalConfiguration)) {
+      throw new GeneralSecurityException(
+          "Currently only subclasses of InternalConfiguration are accepted");
+    }
+    InternalConfiguration internalConfig = (InternalConfiguration) configuration;
+    Class<?> inputPrimitiveClassObject = internalConfig.getInputPrimitiveClass(targetClassObject);
+    if (inputPrimitiveClassObject == null) {
+      throw new GeneralSecurityException("No wrapper found for " + targetClassObject.getName());
+    }
+    return getPrimitiveWithKnownInputPrimitive(
+        internalConfig, targetClassObject, inputPrimitiveClassObject);
   }
 
   /**
@@ -965,16 +1137,14 @@
    * the primitive.
    */
   public <P> P getPrimitive(Class<P> targetClassObject) throws GeneralSecurityException {
-    Class<?> inputPrimitiveClassObject = Registry.getInputPrimitive(targetClassObject);
-    if (inputPrimitiveClassObject == null) {
-      throw new GeneralSecurityException("No wrapper found for " + targetClassObject.getName());
-    }
-    return getPrimitiveWithKnownInputPrimitive(targetClassObject, inputPrimitiveClassObject);
+    return getPrimitive(RegistryConfiguration.get(), targetClassObject);
   }
 
   /**
    * Searches the keyset to find the primary key of this {@code KeysetHandle}, and returns the key
    * wrapped in a {@code KeyHandle}.
+   *
+   * Please do not use this function in new code. Instead, use {@link #getPrimary}.
    */
   public KeyHandle primaryKey() throws GeneralSecurityException {
     int primaryKeyId = keyset.getPrimaryKeyId();
@@ -988,4 +1158,37 @@
     }
     throw new GeneralSecurityException("No primary key found in keyset.");
   }
+
+  @Nullable
+  private static <B> B getLegacyPrimitiveOrNull(
+      InternalConfiguration config, Keyset.Key key, Class<B> inputPrimitiveClassObject)
+      throws GeneralSecurityException {
+    try {
+      return config.getLegacyPrimitive(key.getKeyData(), inputPrimitiveClassObject);
+    } catch (GeneralSecurityException e) {
+      if (e.getMessage().contains("No key manager found for key type ")
+          || e.getMessage().contains(" not supported by key manager of type ")) {
+        // Ignoring because the key may not have a corresponding legacy key manager.
+        return null;
+      }
+      // Otherwise the error is likely legit. Do not swallow.
+      throw e;
+    } catch (UnsupportedOperationException e) {
+      // We are using the new configuration that doesn't work with proto keys.
+      return null;
+    }
+  }
+
+  @Nullable
+  private <B> B getFullPrimitiveOrNull(
+      InternalConfiguration config, Key key, Class<B> inputPrimitiveClassObject)
+      throws GeneralSecurityException {
+    try {
+      return config.getPrimitive(key, inputPrimitiveClassObject);
+    } catch (GeneralSecurityException e) {
+      // Ignoring because the key may not yet have a corresponding class.
+      // TODO(lizatretyakova): stop ignoring when all key classes are migrated from protos.
+      return null;
+    }
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/KeysetManager.java b/java_src/src/main/java/com/google/crypto/tink/KeysetManager.java
index 4423992..d4e586e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/KeysetManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/KeysetManager.java
@@ -26,6 +26,8 @@
 import com.google.crypto.tink.tinkkey.KeyHandle;
 import com.google.crypto.tink.tinkkey.SecretKeyAccess;
 import com.google.crypto.tink.tinkkey.internal.ProtoKey;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.InlineMe;
 import java.security.GeneralSecurityException;
 import javax.annotation.concurrent.GuardedBy;
 
@@ -33,6 +35,10 @@
  * Manages a {@link Keyset} proto, with convenience methods that rotate, disable, enable or destroy
  * keys.
  *
+ * <p>We do not recommend usage of this class. Instead, we recommend you to use a {@link
+ * Keyset.Builder} which has an improved API (in that it e.g. returns the just added objects,
+ * allowing you to manipulate them further).
+ *
  * @since 1.0.0
  */
 public final class KeysetManager {
@@ -64,11 +70,8 @@
    *
    * @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
    *     keyTemplate}
-   * @deprecated Please use {@link #add}. This method adds a new key and immediately promotes it to
-   *     primary. However, when you do keyset rotation, you almost never want to make the new key
-   *     primary, because old binaries don't know the new key yet.
    */
-  @Deprecated
+  @CanIgnoreReturnValue
   public synchronized KeysetManager rotate(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
       throws GeneralSecurityException {
     addNewKey(keyTemplate, true);
@@ -80,10 +83,8 @@
    *
    * @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
    *     keyTemplate}
-   * @deprecated This method takes a KeyTemplate proto, which is an internal implementation detail.
-   *     Please use the add method that takes a {@link KeyTemplate} POJO.
    */
-  @Deprecated
+  @CanIgnoreReturnValue
   public synchronized KeysetManager add(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
       throws GeneralSecurityException {
     addNewKey(keyTemplate, false);
@@ -96,6 +97,7 @@
    * @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
    *     keyTemplate}
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager add(KeyTemplate keyTemplate) throws GeneralSecurityException {
     addNewKey(keyTemplate.getProto(), false);
     return this;
@@ -110,6 +112,7 @@
    * @throws GeneralSecurityException if the {@link KeyHandle}'s key ID collides with another key ID
    *     in the keyset.
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager add(KeyHandle keyHandle) throws GeneralSecurityException {
     ProtoKey pkey;
     try {
@@ -141,32 +144,18 @@
    *     key contained in the {@code KeyHandle}.
    * @throws UnsupportedOperationException if the {@code KeyHandle} contains a {@code TinkKey} which
    *     is not a {@code ProtoKey}.
-   * @deprecated Use KeysetManager.add(KeyHandle) instead.
    */
-  @Deprecated
+  @CanIgnoreReturnValue
   public synchronized KeysetManager add(KeyHandle keyHandle, KeyAccess access)
       throws GeneralSecurityException {
-    ProtoKey pkey;
-    try {
-      pkey = (ProtoKey) keyHandle.getKey(access);
-    } catch (ClassCastException e) {
-      throw new UnsupportedOperationException(
-          "KeyHandles which contain TinkKeys that are not ProtoKeys are not yet supported.", e);
-    }
-    keysetBuilder.addKey(
-        createKeysetKey(pkey.getProtoKey(), KeyTemplate.toProto(pkey.getOutputPrefixType())));
-    return this;
+    return add(keyHandle);
   }
 
   /**
    * Generates a fresh key using {@code keyTemplate} and returns the {@code keyId} of it. In case
    * {@code asPrimary} is true the generated key will be the new primary.
-   *
-   * @deprecated Please use {@link #add}. This method adds a new key and when {@code asPrimary} is
-   *     true immediately promotes it to primary. However, when you do keyset rotation, you almost
-   *     never want to make the new key primary, because old binaries don't know the new key yet.
    */
-  @Deprecated
+  @CanIgnoreReturnValue
   public synchronized int addNewKey(
       com.google.crypto.tink.proto.KeyTemplate keyTemplate, boolean asPrimary)
       throws GeneralSecurityException {
@@ -183,6 +172,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or not enabled
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager setPrimary(int keyId) throws GeneralSecurityException {
     for (int i = 0; i < keysetBuilder.getKeyCount(); i++) {
       Keyset.Key key = keysetBuilder.getKey(i);
@@ -202,9 +192,9 @@
    * Sets the key with {@code keyId} as primary.
    *
    * @throws GeneralSecurityException if the key is not found or not enabled
-   * @deprecated use {@link setPrimary}
    */
-  @Deprecated
+  @InlineMe(replacement = "this.setPrimary(keyId)")
+  @CanIgnoreReturnValue
   public synchronized KeysetManager promote(int keyId) throws GeneralSecurityException {
     return setPrimary(keyId);
   }
@@ -214,6 +204,7 @@
    *
    * @throws GeneralSecurityException if the key is not found
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager enable(int keyId) throws GeneralSecurityException {
     for (int i = 0; i < keysetBuilder.getKeyCount(); i++) {
       Keyset.Key key = keysetBuilder.getKey(i);
@@ -233,6 +224,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or it is the primary key
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager disable(int keyId) throws GeneralSecurityException {
     if (keyId == keysetBuilder.getPrimaryKeyId()) {
       throw new GeneralSecurityException("cannot disable the primary key");
@@ -256,6 +248,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or it is the primary key
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager delete(int keyId) throws GeneralSecurityException {
     if (keyId == keysetBuilder.getPrimaryKeyId()) {
       throw new GeneralSecurityException("cannot delete the primary key");
@@ -276,6 +269,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or it is the primary key
    */
+  @CanIgnoreReturnValue
   public synchronized KeysetManager destroy(int keyId) throws GeneralSecurityException {
     if (keyId == keysetBuilder.getPrimaryKeyId()) {
       throw new GeneralSecurityException("cannot destroy the primary key");
diff --git a/java_src/src/main/java/com/google/crypto/tink/NoSecretKeysetHandle.java b/java_src/src/main/java/com/google/crypto/tink/NoSecretKeysetHandle.java
index 02af3cc..98e0a5c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/NoSecretKeysetHandle.java
+++ b/java_src/src/main/java/com/google/crypto/tink/NoSecretKeysetHandle.java
@@ -36,6 +36,7 @@
    * @throws GeneralSecurityException
    * @deprecated use {@link NoSecretKeysetHandle#read} instead
    */
+  @SuppressWarnings("UnusedException")
   @Deprecated
   public static final KeysetHandle parseFrom(final byte[] serialized)
       throws GeneralSecurityException {
diff --git a/java_src/src/main/java/com/google/crypto/tink/Parameters.java b/java_src/src/main/java/com/google/crypto/tink/Parameters.java
index e5751d8..1611a77 100644
--- a/java_src/src/main/java/com/google/crypto/tink/Parameters.java
+++ b/java_src/src/main/java/com/google/crypto/tink/Parameters.java
@@ -34,8 +34,8 @@
    *
    * <p>In Tink, certain keys change their behavior depending on the key id (for example, an {@link
    * Aead} object can prefix the ciphertext with the big endian encoding of the key id). If this is
-   * the case, such a key should require a unique id in {@link Key#getIdRequirement} and return true
-   * here.
+   * the case, such a key should require a unique id in {@link Key#getIdRequirementOrNull} and
+   * return true here.
    */
   public abstract boolean hasIdRequirement();
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/PrimitiveSet.java b/java_src/src/main/java/com/google/crypto/tink/PrimitiveSet.java
index 52faab8..4449c28 100644
--- a/java_src/src/main/java/com/google/crypto/tink/PrimitiveSet.java
+++ b/java_src/src/main/java/com/google/crypto/tink/PrimitiveSet.java
@@ -23,6 +23,7 @@
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Hex;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -59,8 +60,9 @@
    * information about the primitive.
    */
   public static final class Entry<P> {
-    // The actual primitive.
-    private final P primitive;
+    // If set, this is a primitive of a key.
+    @Nullable private final P fullPrimitive;
+    @Nullable private final P primitive;
     // Identifies the primitive within the set.
     // It is the ciphertext prefix of the corresponding key.
     private final byte[] identifier;
@@ -70,30 +72,49 @@
     private final OutputPrefixType outputPrefixType;
     // The id of the key.
     private final int keyId;
+    private final String keyType;
     private final Key key;
 
     Entry(
-        P primitive,
+        @Nullable P fullPrimitive,
+        @Nullable P primitive,
         final byte[] identifier,
         KeyStatusType status,
         OutputPrefixType outputPrefixType,
         int keyId,
+        String keyType,
         Key key) {
+      this.fullPrimitive = fullPrimitive;
       this.primitive = primitive;
       this.identifier = Arrays.copyOf(identifier, identifier.length);
       this.status = status;
       this.outputPrefixType = outputPrefixType;
       this.keyId = keyId;
+      this.keyType = keyType;
       this.key = key;
     }
 
     /**
+     * Returns the full primitive for this entry.
+     *
+     * <p>This is used in cases when the new Tink Key interface is used and the primitive is
+     * self-sufficient by itself, meaning that all the necessary information to process the
+     * primitive is contained in the primitive (most likely through the new Key interface), as
+     * opposed to the {@code primitive} field (see {@link #getPrimitive} for details).
+     */
+    @Nullable
+    public P getFullPrimitive() {
+      return this.fullPrimitive;
+    }
+
+    /**
      * Returns the primitive for this entry.
      *
      * <p>For primitives of type {@code Mac}, {@code Aead}, {@code PublicKeySign}, {@code
      * PublicKeyVerify}, {@code DeterministicAead}, {@code HybridEncrypt}, and {@code HybridDecrypt}
      * this is a primitive which <b>ignores</b> the output prefix and assumes "RAW".
      */
+    @Nullable
     public P getPrimitive() {
       return this.primitive;
     }
@@ -106,6 +127,7 @@
       return outputPrefixType;
     }
 
+    @Nullable
     public final byte[] getIdentifier() {
       if (identifier == null) {
         return null;
@@ -118,17 +140,25 @@
       return keyId;
     }
 
+    public String getKeyType() {
+      return keyType;
+    }
+
     public Key getKey() {
       return key;
     }
 
+    @Nullable
     public Parameters getParameters() {
+      if (key == null) {
+        return null;
+      }
       return key.getParameters();
     }
   }
 
-  private static <P> Entry<P> addEntryToMap(
-      P primitive, Keyset.Key key, ConcurrentMap<Prefix, List<Entry<P>>> primitives)
+  private static <P> Entry<P> createEntry(
+      @Nullable P fullPrimitive, @Nullable P primitive, Keyset.Key key)
       throws GeneralSecurityException {
     @Nullable Integer idRequirement = key.getKeyId();
     if (key.getOutputPrefixType() == OutputPrefixType.RAW) {
@@ -144,14 +174,21 @@
                     key.getOutputPrefixType(),
                     idRequirement),
                 InsecureSecretKeyAccess.get());
-    Entry<P> entry =
-        new Entry<P>(
-            primitive,
-            CryptoFormat.getOutputPrefix(key),
-            key.getStatus(),
-            key.getOutputPrefixType(),
-            key.getKeyId(),
-            keyObject);
+    return new Entry<P>(
+        fullPrimitive,
+        primitive,
+        CryptoFormat.getOutputPrefix(key),
+        key.getStatus(),
+        key.getOutputPrefixType(),
+        key.getKeyId(),
+        key.getKeyData().getTypeUrl(),
+        keyObject);
+  }
+
+  private static <P> void storeEntryInPrimitiveSet(
+      Entry<P> entry,
+      ConcurrentMap<Prefix, List<Entry<P>>> primitives,
+      List<Entry<P>> primitivesInKeysetOrder) {
     List<Entry<P>> list = new ArrayList<>();
     list.add(entry);
     // Cannot use byte[] as keys in hash map, convert to Prefix wrapper class.
@@ -163,10 +200,10 @@
       newList.add(entry);
       primitives.put(identifier, Collections.unmodifiableList(newList));
     }
-    return entry;
+    primitivesInKeysetOrder.add(entry);
   }
 
-  /** @return the entry with the primary primitive. */
+  /** Returns the entry with the primary primitive. */
   @Nullable
   public Entry<P> getPrimary() {
     return primary;
@@ -180,51 +217,59 @@
     return annotations;
   }
 
-  /** @return all primitives using RAW prefix. */
+  /** Returns all primitives using RAW prefix. */
   public List<Entry<P>> getRawPrimitives() {
     return getPrimitive(CryptoFormat.RAW_PREFIX);
   }
 
-  /** @return the entries with primitive identifed by {@code identifier}. */
+  /** Returns the entries with primitive identifed by {@code identifier}. */
   public List<Entry<P>> getPrimitive(final byte[] identifier) {
     List<Entry<P>> found = primitives.get(new Prefix(identifier));
     return found != null ? found : Collections.<Entry<P>>emptyList();
   }
 
-  /** Returns the entries with primitives identified by the ciphertext prefix of {@code key}. */
-  List<Entry<P>> getPrimitive(Keyset.Key key) throws GeneralSecurityException {
-    return getPrimitive(CryptoFormat.getOutputPrefix(key));
-  }
-
-  /** @return all primitives */
+  /** Returns all primitives. */
   public Collection<List<Entry<P>>> getAll() {
     return primitives.values();
   }
 
+  /** Returns all primitives in the original keyset key order. */
+  public List<Entry<P>> getAllInKeysetOrder() {
+    return Collections.unmodifiableList(primitivesInKeysetOrder);
+  }
+
   /**
-   * The primitives are stored in a hash map of (ciphertext prefix, list of primivies sharing the
+   * The primitives are stored in a hash map of (ciphertext prefix, list of primitives sharing the
    * prefix). This allows quickly retrieving the list of primitives sharing some particular prefix.
    * Because all RAW keys are using an empty prefix, this also quickly allows retrieving them.
    */
   private final ConcurrentMap<Prefix, List<Entry<P>>> primitives;
 
+  /** Stores entries in the original keyset key order. */
+  private final List<Entry<P>> primitivesInKeysetOrder;
+
   private Entry<P> primary;
   private final Class<P> primitiveClass;
   private final MonitoringAnnotations annotations;
   private final boolean isMutable;
 
-  @Deprecated
   private PrimitiveSet(Class<P> primitiveClass) {
     this.primitives = new ConcurrentHashMap<>();
+    this.primitivesInKeysetOrder = new ArrayList<>();
     this.primitiveClass = primitiveClass;
     this.annotations = MonitoringAnnotations.EMPTY;
     this.isMutable = true;
   }
 
-  /** Creates an immutable PrimitiveSet. It is used by the Builder.*/
-  private PrimitiveSet(ConcurrentMap<Prefix, List<Entry<P>>> primitives,
-      Entry<P> primary, MonitoringAnnotations annotations, Class<P> primitiveClass) {
+  /** Creates an immutable PrimitiveSet. It is used by the Builder. */
+  private PrimitiveSet(
+      ConcurrentMap<Prefix, List<Entry<P>>> primitives,
+      List<Entry<P>> primitivesInKeysetOrder,
+      Entry<P> primary,
+      MonitoringAnnotations annotations,
+      Class<P> primitiveClass) {
     this.primitives = primitives;
+    this.primitivesInKeysetOrder = primitivesInKeysetOrder;
     this.primary = primary;
     this.primitiveClass = primitiveClass;
     this.annotations = annotations;
@@ -241,7 +286,8 @@
     return new PrimitiveSet<P>(primitiveClass);
   }
 
-  /** Sets given Entry {@code primary} as the primary one.
+  /**
+   * Sets given Entry {@code primary} as the primary one.
    *
    * @throws IllegalStateException if object has been created by the {@link Builder}.
    * @deprecated use {@link Builder.addPrimaryPrimitive} instead.
@@ -269,10 +315,10 @@
    * Creates an entry in the primitive table.
    *
    * @return the added {@link Entry}
-   *
    * @throws IllegalStateException if object has been created by the {@link Builder}.
    * @deprecated use {@link Builder.addPrimitive} or {@link Builder.addPrimaryPrimitive} instead.
    */
+  @CanIgnoreReturnValue
   @Deprecated
   public Entry<P> addPrimitive(final P primitive, Keyset.Key key)
       throws GeneralSecurityException {
@@ -283,7 +329,9 @@
     if (key.getStatus() != KeyStatusType.ENABLED) {
       throw new GeneralSecurityException("only ENABLED key is allowed");
     }
-    return addEntryToMap(primitive, key, primitives);
+    Entry<P> entry = createEntry(null, primitive, key);
+    storeEntryInPrimitiveSet(entry, primitives, primitivesInKeysetOrder);
+    return entry;
   }
 
   public Class<P> getPrimitiveClass() {
@@ -337,18 +385,29 @@
     // primitives == null indicates that build has been called and the builder can't be used
     // anymore.
     private ConcurrentMap<Prefix, List<Entry<P>>> primitives = new ConcurrentHashMap<>();
+    private final List<Entry<P>> primitivesInKeysetOrder = new ArrayList<>();
     private Entry<P> primary;
     private MonitoringAnnotations annotations;
 
-    private Builder<P> addPrimitive(final P primitive, Keyset.Key key, boolean asPrimary)
+    @CanIgnoreReturnValue
+    private Builder<P> addPrimitive(
+        @Nullable final P fullPrimitive,
+        @Nullable final P primitive,
+        Keyset.Key key,
+        boolean asPrimary)
         throws GeneralSecurityException {
       if (primitives == null) {
         throw new IllegalStateException("addPrimitive cannot be called after build");
       }
+      if (fullPrimitive == null && primitive == null) {
+        throw new GeneralSecurityException(
+            "at least one of the `fullPrimitive` or `primitive` must be set");
+      }
       if (key.getStatus() != KeyStatusType.ENABLED) {
         throw new GeneralSecurityException("only ENABLED key is allowed");
       }
-      Entry<P> entry = addEntryToMap(primitive, key, primitives);
+      Entry<P> entry = createEntry(fullPrimitive, primitive, key);
+      storeEntryInPrimitiveSet(entry, primitives, primitivesInKeysetOrder);
       if (asPrimary) {
         if (this.primary != null) {
           throw new IllegalStateException("you cannot set two primary primitives");
@@ -359,17 +418,41 @@
     }
 
     /* Adds a non-primary primitive.*/
+    @CanIgnoreReturnValue
     public Builder<P> addPrimitive(final P primitive, Keyset.Key key)
         throws GeneralSecurityException {
-      return addPrimitive(primitive, key, false);
+      return addPrimitive(null, primitive, key, false);
     }
 
-    /* Adds the primary primitive. Should be called exactly once per PrimitiveSet.*/
+    /**
+     * Adds the primary primitive. This or addPrimaryFullPrimitiveAndOptionalPrimitive should be
+     * called exactly once per PrimitiveSet.
+     */
+    @CanIgnoreReturnValue
     public Builder<P> addPrimaryPrimitive(final P primitive, Keyset.Key key)
         throws GeneralSecurityException {
-      return addPrimitive(primitive, key, true);
+      return addPrimitive(null, primitive, key, true);
     }
 
+    @CanIgnoreReturnValue
+    public Builder<P> addFullPrimitiveAndOptionalPrimitive(
+        @Nullable final P fullPrimitive, @Nullable final P primitive, Keyset.Key key)
+        throws GeneralSecurityException {
+      return addPrimitive(fullPrimitive, primitive, key, false);
+    }
+
+    /**
+     * Adds the primary primitive and full primitive. This or addPrimaryPrimitive should be called
+     * exactly once per PrimitiveSet.
+     */
+    @CanIgnoreReturnValue
+    public Builder<P> addPrimaryFullPrimitiveAndOptionalPrimitive(
+        @Nullable final P fullPrimitive, @Nullable final P primitive, Keyset.Key key)
+        throws GeneralSecurityException {
+      return addPrimitive(fullPrimitive, primitive, key, true);
+    }
+
+    @CanIgnoreReturnValue
     public Builder<P> setAnnotations(MonitoringAnnotations annotations) {
       if (primitives == null) {
         throw new IllegalStateException("setAnnotations cannot be called after build");
@@ -384,7 +467,8 @@
       }
       // Note that we currently don't enforce that primary must be set.
       PrimitiveSet<P> output =
-          new PrimitiveSet<P>(primitives, primary, annotations, primitiveClass);
+          new PrimitiveSet<P>(
+              primitives, primitivesInKeysetOrder, primary, annotations, primitiveClass);
       this.primitives = null;
       return output;
     }
diff --git a/java_src/src/main/java/com/google/crypto/tink/PrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/PrivateKey.java
new file mode 100644
index 0000000..d3e9caf
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/PrivateKey.java
@@ -0,0 +1,30 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import com.google.crypto.tink.annotations.Alpha;
+
+/**
+ * Interface to be implemented by all private keys, that gives access to the public key.
+ *
+ * <p>Note that in Tink, a private key always includes the public key.
+ */
+@Alpha
+public interface PrivateKey {
+
+  public Key getPublicKey();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/PrivilegedRegistry.java b/java_src/src/main/java/com/google/crypto/tink/PrivilegedRegistry.java
index 72465d6..4e851b3 100644
--- a/java_src/src/main/java/com/google/crypto/tink/PrivilegedRegistry.java
+++ b/java_src/src/main/java/com/google/crypto/tink/PrivilegedRegistry.java
@@ -18,8 +18,6 @@
 
 import com.google.crypto.tink.proto.KeyData;
 import com.google.crypto.tink.proto.KeyTemplate;
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.MessageLite;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 
@@ -34,8 +32,8 @@
   private PrivilegedRegistry() {}
 
   /**
-   * Method to derive a key, using the given {@param keyTemplate}, with the randomness as provided
-   * by the second argument.
+   * Method to derive a key, using the given {@code keyTemplate}, with the randomness as provided by
+   * the second argument.
    *
    * <p>This method is on purpose not in the public interface. Calling it twice using different key
    * templates and the same randomness can completely destroy any security in a system, so we
@@ -47,15 +45,4 @@
       throws GeneralSecurityException {
     return Registry.deriveKey(keyTemplate, randomStream);
   }
-
-  /**
-   * Returns the key proto in the keyData if a corresponding key type manager was registered.
-   * Returns null if the key type was registered with a {@link KeyManager} (and not a {@link
-   * KeyTypeManager}).
-   */
-  public static MessageLite parseKeyData(KeyData keyData)
-      throws GeneralSecurityException, InvalidProtocolBufferException {
-    return Registry.parseKeyData(keyData);
-  }
-
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/Registry.java b/java_src/src/main/java/com/google/crypto/tink/Registry.java
index 3240cde..30e7fbe 100644
--- a/java_src/src/main/java/com/google/crypto/tink/Registry.java
+++ b/java_src/src/main/java/com/google/crypto/tink/Registry.java
@@ -18,6 +18,7 @@
 
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
 import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.internal.MutablePrimitiveRegistry;
 import com.google.crypto.tink.internal.PrivateKeyTypeManager;
 import com.google.crypto.tink.proto.KeyData;
 import com.google.protobuf.ByteString;
@@ -91,9 +92,6 @@
   private static final ConcurrentMap<String, Catalogue<?>> catalogueMap =
       new ConcurrentHashMap<>(); //  name -> catalogue mapping
 
-  private static final ConcurrentMap<Class<?>, PrimitiveWrapper<?, ?>> primitiveWrapperMap =
-      new ConcurrentHashMap<>();
-
   private static final ConcurrentMap<String, KeyTemplate> keyTemplateMap =
       new ConcurrentHashMap<>(); // name -> KeyTemplate mapping
 
@@ -144,10 +142,10 @@
    */
   static synchronized void reset() {
     keyManagerRegistry.set(new KeyManagerRegistry());
+    MutablePrimitiveRegistry.resetGlobalInstanceTestOnly();
     keyDeriverMap.clear();
     newKeyAllowedMap.clear();
     catalogueMap.clear();
-    primitiveWrapperMap.clear();
     keyTemplateMap.clear();
   }
 
@@ -163,8 +161,8 @@
    * @deprecated Catalogues are no longer supported.
    */
   @Deprecated
-  public static synchronized void addCatalogue(String catalogueName, Catalogue<?> catalogue)
-      throws GeneralSecurityException {
+  public static synchronized void addCatalogue(
+      String catalogueName, Catalogue<?> catalogue) throws GeneralSecurityException {
     if (catalogueName == null) {
       throw new IllegalArgumentException("catalogueName must be non-null.");
     }
@@ -368,12 +366,11 @@
    *         <li>The key manager was already registered, but it contains new key templates.
    *         <li>The key manager is new, but it contains existing key templates.
    */
-  private static synchronized <KeyProtoT extends MessageLite, KeyFormatProtoT extends MessageLite>
-      void ensureKeyManagerInsertable(
-          String typeUrl,
-          Map<String, KeyTypeManager.KeyFactory.KeyFormat<KeyFormatProtoT>> keyFormats,
-          boolean newKeyAllowed)
-          throws GeneralSecurityException {
+  private static synchronized <KeyFormatProtoT extends MessageLite> void ensureKeyManagerInsertable(
+      String typeUrl,
+      Map<String, KeyTypeManager.KeyFactory.KeyFormat<KeyFormatProtoT>> keyFormats,
+      boolean newKeyAllowed)
+      throws GeneralSecurityException {
     if (newKeyAllowed && newKeyAllowedMap.containsKey(typeUrl) && !newKeyAllowedMap.get(typeUrl)) {
       throw new GeneralSecurityException("New keys are already disallowed for key type " + typeUrl);
     }
@@ -485,27 +482,7 @@
    */
   public static synchronized <B, P> void registerPrimitiveWrapper(
       final PrimitiveWrapper<B, P> wrapper) throws GeneralSecurityException {
-    if (wrapper == null) {
-      throw new IllegalArgumentException("wrapper must be non-null");
-    }
-    Class<P> classObject = wrapper.getPrimitiveClass();
-    if (primitiveWrapperMap.containsKey(classObject)) {
-      @SuppressWarnings("unchecked") // We know that we only inserted objects of the correct type.
-      PrimitiveWrapper<?, P> existingWrapper =
-          (PrimitiveWrapper<?, P>) primitiveWrapperMap.get(classObject);
-      if (!wrapper.getClass().getName().equals(existingWrapper.getClass().getName())) {
-        logger.warning(
-            "Attempted overwrite of a registered PrimitiveWrapper for type " + classObject);
-        throw new GeneralSecurityException(
-            String.format(
-                "PrimitiveWrapper for primitive (%s) is already registered to be %s, "
-                    + "cannot be re-registered with %s",
-                classObject.getName(),
-                existingWrapper.getClass().getName(),
-                wrapper.getClass().getName()));
-      }
-    }
-    primitiveWrapperMap.put(classObject, wrapper);
+    MutablePrimitiveRegistry.globalInstance().registerPrimitiveWrapper(wrapper);
   }
 
   /**
@@ -514,19 +491,21 @@
    *     typeUrl} instead.
    */
   @Deprecated
-  public static <P> KeyManager<P> getKeyManager(String typeUrl) throws GeneralSecurityException {
-    return keyManagerRegistry.get().getKeyManager(typeUrl);
+  public static <P> KeyManager<P> getKeyManager(String typeUrl)
+      throws GeneralSecurityException {
+    @SuppressWarnings("unchecked") // Unavoidable for the API we implement (hence it is deprecated)
+    KeyManager<P> result = (KeyManager<P>) getUntypedKeyManager(typeUrl);
+    return result;
   }
 
-  /** @return a {@link KeyManager} for the given {@code typeUrl} (if found). */
+  /** Returns a {@link KeyManager} for the given {@code typeUrl} (if found). */
   public static <P> KeyManager<P> getKeyManager(String typeUrl, Class<P> primitiveClass)
       throws GeneralSecurityException {
     return keyManagerRegistry.get().getKeyManager(typeUrl, primitiveClass);
   }
 
-  /** @return a {@link KeyManager} for the given {@code typeUrl} (if found). */
-  public static KeyManager<?> getUntypedKeyManager(String typeUrl)
-      throws GeneralSecurityException {
+  /** Returns a {@link KeyManager} for the given {@code typeUrl} (if found). */
+  public static KeyManager<?> getUntypedKeyManager(String typeUrl) throws GeneralSecurityException {
     return keyManagerRegistry.get().getUntypedKeyManager(typeUrl);
   }
 
@@ -573,7 +552,9 @@
    * {@link KeyManager#newKey} with {@code keyTemplate} as the parameter.
    *
    * @return a new key
+   * @deprecated Use {@code newKeyData} instead.
    */
+  @Deprecated
   public static synchronized MessageLite newKey(
       com.google.crypto.tink.proto.KeyTemplate keyTemplate) throws GeneralSecurityException {
     KeyManager<?> manager = getUntypedKeyManager(keyTemplate.getTypeUrl());
@@ -592,7 +573,9 @@
    * {@link KeyManager#newKey} with {@code format} as the parameter.
    *
    * @return a new key
+   * @deprecated Use {@code newKeyData} instead.
    */
+  @Deprecated
   public static synchronized MessageLite newKey(String typeUrl, MessageLite format)
       throws GeneralSecurityException {
     KeyManager<?> manager = getKeyManager(typeUrl);
@@ -651,14 +634,14 @@
    * KeyManager#getPrimitive} with {@code key} as the parameter.
    *
    * @return a new primitive
-   * @deprecated Use {@code getPrimitive(typeUrl, key, P.class)} instead.
+   * @deprecated Use {@code getPrimitive(typeUrl, serializedKey, P.class)} instead.
    */
-  @Deprecated
   @SuppressWarnings("TypeParameterUnusedInFormals")
+  @Deprecated
   public static <P> P getPrimitive(String typeUrl, MessageLite key)
       throws GeneralSecurityException {
-    KeyManager<P> manager = keyManagerRegistry.get().getKeyManager(typeUrl);
-    return manager.getPrimitive(key);
+    KeyManager<P> manager = getKeyManager(typeUrl);
+    return manager.getPrimitive(key.toByteString());
   }
 
   /**
@@ -668,11 +651,13 @@
    * KeyManager#getPrimitive} with {@code key} as the parameter.
    *
    * @return a new primitive
+   * @deprecated Use {@code getPrimitive(typeUrl, serializedKey, Primitive.class} instead.
    */
-  public static <P> P getPrimitive(String typeUrl, MessageLite key, Class<P> primitiveClass)
-      throws GeneralSecurityException {
+  @Deprecated
+  public static <P> P getPrimitive(
+      String typeUrl, MessageLite key, Class<P> primitiveClass) throws GeneralSecurityException {
     KeyManager<P> manager = keyManagerRegistry.get().getKeyManager(typeUrl, primitiveClass);
-    return manager.getPrimitive(key);
+    return manager.getPrimitive(key.toByteString());
   }
 
   /**
@@ -684,11 +669,11 @@
    * @return a new primitive
    * @deprecated Use {@code getPrimitive(typeUrl, serializedKey, Primitive.class} instead.
    */
-  @Deprecated
   @SuppressWarnings("TypeParameterUnusedInFormals")
+  @Deprecated
   public static <P> P getPrimitive(String typeUrl, ByteString serializedKey)
       throws GeneralSecurityException {
-    KeyManager<P> manager = keyManagerRegistry.get().getKeyManager(typeUrl);
+    KeyManager<P> manager = getKeyManager(typeUrl);
     return manager.getPrimitive(serializedKey);
   }
 
@@ -716,8 +701,8 @@
    * @deprecated Use {@code getPrimitive(typeUrl, serializedKey, Primitive.class)} instead.
    * @return a new primitive
    */
-  @Deprecated
   @SuppressWarnings("TypeParameterUnusedInFormals")
+  @Deprecated
   public static <P> P getPrimitive(String typeUrl, byte[] serializedKey)
       throws GeneralSecurityException {
     return getPrimitive(typeUrl, ByteString.copyFrom(serializedKey));
@@ -745,8 +730,8 @@
    * @return a new primitive
    * @deprecated Use {@code getPrimitive(keyData, Primitive.class)} instead.
    */
-  @Deprecated
   @SuppressWarnings("TypeParameterUnusedInFormals")
+  @Deprecated
   public static <P> P getPrimitive(KeyData keyData) throws GeneralSecurityException {
     return getPrimitive(keyData.getTypeUrl(), keyData.getValue());
   }
@@ -764,28 +749,18 @@
     return getPrimitive(keyData.getTypeUrl(), keyData.getValue(), primitiveClass);
   }
 
+  static <KeyT extends Key, P> P getFullPrimitive(KeyT key, Class<P> primitiveClass)
+      throws GeneralSecurityException {
+    return MutablePrimitiveRegistry.globalInstance().getPrimitive(key, primitiveClass);
+  }
+
   /**
    * Looks up the globally registered PrimitiveWrapper for this primitive and wraps the given
    * PrimitiveSet with it.
    */
   public static <B, P> P wrap(PrimitiveSet<B> primitiveSet, Class<P> clazz)
       throws GeneralSecurityException {
-    @SuppressWarnings("unchecked") // We know that we inserted Class<P> -> PrimitiveWrapper<?, P>
-    PrimitiveWrapper<?, P> wrapper = (PrimitiveWrapper<?, P>) primitiveWrapperMap.get(clazz);
-    if (wrapper == null) {
-      throw new GeneralSecurityException(
-          "No wrapper found for " + primitiveSet.getPrimitiveClass().getName());
-    }
-    if (!wrapper.getInputPrimitiveClass().equals(primitiveSet.getPrimitiveClass())) {
-      throw new GeneralSecurityException(
-          "Wrong input primitive class, expected "
-              + wrapper.getInputPrimitiveClass()
-              + ", got "
-              + primitiveSet.getPrimitiveClass());
-    }
-    @SuppressWarnings("unchecked") // We just checked correctness
-    P result = ((PrimitiveWrapper<B, P>) wrapper).wrap(primitiveSet);
-    return result;
+    return MutablePrimitiveRegistry.globalInstance().wrap(primitiveSet, clazz);
   }
 
   public static <P> P wrap(PrimitiveSet<P> primitiveSet)
@@ -819,21 +794,11 @@
    */
   @Nullable
   public static Class<?> getInputPrimitive(Class<?> wrappedPrimitive) {
-    PrimitiveWrapper<?, ?> wrapper = primitiveWrapperMap.get(wrappedPrimitive);
-    if (wrapper == null) {
+    try {
+      return MutablePrimitiveRegistry.globalInstance().getInputPrimitiveClass(wrappedPrimitive);
+    } catch (GeneralSecurityException e) {
       return null;
     }
-    return wrapper.getInputPrimitiveClass();
-  }
-
-  /**
-   * Returns the key proto in the keyData if a corresponding key type manager was registered.
-   * Returns null if the key type was registered with a {@link KeyManager} (and not a {@link
-   * KeyTypeManager}).
-   */
-  static MessageLite parseKeyData(KeyData keyData)
-      throws GeneralSecurityException, InvalidProtocolBufferException {
-    return keyManagerRegistry.get().parseKeyData(keyData);
   }
 
   /**
@@ -842,6 +807,10 @@
    * @throws GeneralSecurityException if any key manager has already been registered.
    */
   public static synchronized void restrictToFipsIfEmpty() throws GeneralSecurityException {
+    // If we are already using FIPS mode, do nothing.
+    if (TinkFipsUtil.useOnlyFips()) {
+      return;
+    }
     if (keyManagerRegistry.get().isEmpty()) {
       TinkFipsUtil.setFipsRestricted();
       return;
diff --git a/java_src/src/main/java/com/google/crypto/tink/SecretKeyAccess.java b/java_src/src/main/java/com/google/crypto/tink/SecretKeyAccess.java
index 602f7d2..24af4dc 100644
--- a/java_src/src/main/java/com/google/crypto/tink/SecretKeyAccess.java
+++ b/java_src/src/main/java/com/google/crypto/tink/SecretKeyAccess.java
@@ -56,7 +56,17 @@
     return INSTANCE;
   }
 
-  /** Throws an exception if the passed in SecretKeyAccess is null, otherwise returns it. */
+  /**
+   * Throws an exception if the passed in {@link SecretKeyAccess} is null, otherwise returns it.
+   *
+   * <p>Note: Tink has two types of APIs, some which take a nullable {@code SecretKeyAccess}, and
+   * some which take a {@code SecretKeyAccess} without annotation. When an API takes a nullable
+   * {@code SecretKeyAccess}, this indicates that proper usage may call it with {@code null}, hence
+   * we typically want to throw a checked exception and {@code requireAccess} here is appropriate.
+   * Conversely, if an API takes an unannotated {@code SecretKeyAccess}, this indicates that the API
+   * always requires a non-null object. In this case, using it with null warrants should usually
+   * throw a null pointer exception (and one does not want to use {@code requireAccess}).
+   */
   @CanIgnoreReturnValue
   public static SecretKeyAccess requireAccess(@Nullable SecretKeyAccess access)
       throws GeneralSecurityException {
diff --git a/java_src/src/main/java/com/google/crypto/tink/StreamingAead.java b/java_src/src/main/java/com/google/crypto/tink/StreamingAead.java
index b62d3bf..6805870 100644
--- a/java_src/src/main/java/com/google/crypto/tink/StreamingAead.java
+++ b/java_src/src/main/java/com/google/crypto/tink/StreamingAead.java
@@ -16,7 +16,6 @@
 
 package com.google.crypto.tink;
 
-import androidx.annotation.RequiresApi;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -162,7 +161,6 @@
    *     associatedData is not correct.
    * @throws IOException if an IOException occurred while reading from ciphertextDestination.
    */
-  @RequiresApi(24) // https://developer.android.com/reference/java/nio/channels/SeekableByteChannel
   SeekableByteChannel newSeekableDecryptingChannel(
       SeekableByteChannel ciphertextSource, byte[] associatedData)
       throws GeneralSecurityException, IOException;
diff --git a/java_src/src/main/java/com/google/crypto/tink/TinkJsonProtoKeysetFormat.java b/java_src/src/main/java/com/google/crypto/tink/TinkJsonProtoKeysetFormat.java
new file mode 100644
index 0000000..2b4b96a
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/TinkJsonProtoKeysetFormat.java
@@ -0,0 +1,107 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.crypto.tink.internal.Util.UTF_8;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/** Functions to parse and serialize Keyset in Tink's JSON format based on Protobufs. */
+public final class TinkJsonProtoKeysetFormat {
+
+  @SuppressWarnings("UnusedException")
+  public static KeysetHandle parseKeyset(String serializedKeyset, SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (access == null) {
+      throw new NullPointerException("SecretKeyAccess cannot be null");
+    }
+    try {
+      return CleartextKeysetHandle.read(JsonKeysetReader.withString(serializedKeyset));
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Parse keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static String serializeKeyset(KeysetHandle keysetHandle, SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (access == null) {
+      throw new NullPointerException("SecretKeyAccess cannot be null");
+    }
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      CleartextKeysetHandle.write(keysetHandle, JsonKeysetWriter.withOutputStream(outputStream));
+      return new String(outputStream.toByteArray(), UTF_8);
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Serialize keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static KeysetHandle parseKeysetWithoutSecret(String serializedKeyset)
+      throws GeneralSecurityException {
+    try {
+      return KeysetHandle.readNoSecret(JsonKeysetReader.withString(serializedKeyset));
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Parse keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static String serializeKeysetWithoutSecret(KeysetHandle keysetHandle)
+      throws GeneralSecurityException {
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      keysetHandle.writeNoSecret(JsonKeysetWriter.withOutputStream(outputStream));
+      return new String(outputStream.toByteArray(), UTF_8);
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Serialize keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static KeysetHandle parseEncryptedKeyset(
+      String serializedEncryptedKeyset, Aead keysetEncryptionAead, byte[] associatedData)
+      throws GeneralSecurityException {
+    try {
+      return KeysetHandle.readWithAssociatedData(
+          JsonKeysetReader.withString(serializedEncryptedKeyset),
+          keysetEncryptionAead,
+          associatedData);
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Parse keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static String serializeEncryptedKeyset(
+      KeysetHandle keysetHandle, Aead keysetEncryptionAead, byte[] associatedData)
+      throws GeneralSecurityException {
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      keysetHandle.writeWithAssociatedData(
+          JsonKeysetWriter.withOutputStream(outputStream), keysetEncryptionAead, associatedData);
+      return new String(outputStream.toByteArray(), UTF_8);
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Serialize keyset failed");
+    }
+  }
+
+  private TinkJsonProtoKeysetFormat() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/TinkProtoKeysetFormat.java b/java_src/src/main/java/com/google/crypto/tink/TinkProtoKeysetFormat.java
new file mode 100644
index 0000000..5e1c3a1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/TinkProtoKeysetFormat.java
@@ -0,0 +1,101 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/** Functions to parse and serialize Keyset in Tink's binary format based on Protobufs. */
+public final class TinkProtoKeysetFormat {
+
+  @SuppressWarnings("UnusedException")
+  public static KeysetHandle parseKeyset(byte[] serializedKeyset, SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (access == null) {
+      throw new NullPointerException("SecretKeyAccess cannot be null");
+    }
+    try {
+      return CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(serializedKeyset));
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Parse keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static byte[] serializeKeyset(KeysetHandle keysetHandle, SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (access == null) {
+      throw new NullPointerException("SecretKeyAccess cannot be null");
+    }
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      CleartextKeysetHandle.write(keysetHandle, BinaryKeysetWriter.withOutputStream(outputStream));
+      return outputStream.toByteArray();
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Serialize keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static KeysetHandle parseKeysetWithoutSecret(byte[] serializedKeyset)
+      throws GeneralSecurityException {
+    return KeysetHandle.readNoSecret(serializedKeyset);
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static byte[] serializeKeysetWithoutSecret(KeysetHandle keysetHandle)
+      throws GeneralSecurityException {
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      keysetHandle.writeNoSecret(BinaryKeysetWriter.withOutputStream(outputStream));
+      return outputStream.toByteArray();
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Serialize keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static KeysetHandle parseEncryptedKeyset(
+      byte[] serializedEncryptedKeyset, Aead keysetEncryptionAead, byte[] associatedData)
+      throws GeneralSecurityException {
+    try {
+      return KeysetHandle.readWithAssociatedData(
+          BinaryKeysetReader.withBytes(serializedEncryptedKeyset),
+          keysetEncryptionAead,
+          associatedData);
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Parse keyset failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  public static byte[] serializeEncryptedKeyset(
+      KeysetHandle keysetHandle, Aead keysetEncryptionAead, byte[] associatedData)
+      throws GeneralSecurityException {
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      keysetHandle.writeWithAssociatedData(
+          BinaryKeysetWriter.withOutputStream(outputStream), keysetEncryptionAead, associatedData);
+      return outputStream.toByteArray();
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Serialize keyset failed");
+    }
+  }
+
+  private TinkProtoKeysetFormat() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/TinkProtoParametersFormat.java b/java_src/src/main/java/com/google/crypto/tink/TinkProtoParametersFormat.java
new file mode 100644
index 0000000..3726507
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/TinkProtoParametersFormat.java
@@ -0,0 +1,57 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import com.google.crypto.tink.internal.LegacyProtoParameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/** Functions to parse and serialize Parameters in Tink's binary format based on Protobufs. */
+public final class TinkProtoParametersFormat {
+  /**
+   * Serializes a parameters object into a byte[] according to Tink's binary format.
+   */
+  public static byte[] serialize(Parameters parameters) throws GeneralSecurityException {
+    if (parameters instanceof LegacyProtoParameters) {
+      return ((LegacyProtoParameters) parameters).getSerialization().getKeyTemplate().toByteArray();
+    }
+    ProtoParametersSerialization s =
+        MutableSerializationRegistry.globalInstance()
+            .serializeParameters(parameters, ProtoParametersSerialization.class);
+    return s.getKeyTemplate().toByteArray();
+  }
+
+  /**
+   * Parses a byte[] into a parameters object into a byte[] according to Tink's binary format.
+   */
+  public static Parameters parse(byte[] serializedParameters) throws GeneralSecurityException {
+    KeyTemplate t;
+    try {
+      t = KeyTemplate.parseFrom(serializedParameters, ExtensionRegistryLite.getEmptyRegistry());
+    } catch (IOException e) {
+      throw new GeneralSecurityException("Failed to parse proto", e);
+    }
+    return MutableSerializationRegistry.globalInstance()
+        .parseParametersWithLegacyFallback(ProtoParametersSerialization.create(t));
+  }
+
+  private TinkProtoParametersFormat() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AeadConfig.java b/java_src/src/main/java/com/google/crypto/tink/aead/AeadConfig.java
index 322ba25..1d1aeb9 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AeadConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AeadConfig.java
@@ -48,20 +48,25 @@
   public static final String XCHACHA20_POLY1305_TYPE_URL =
       new XChaCha20Poly1305KeyManager().getKeyType();
 
-  /** @deprecated use {@link #register} */
-  @Deprecated public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
+  /**
+   * @deprecated use {@link #register}
+   */
+  @Deprecated
+  public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
 
   /**
    * @deprecated use {@link #register}
    * @since 1.1.0
    */
-  @Deprecated public static final RegistryConfig TINK_1_1_0 = TINK_1_0_0;
+  @Deprecated
+  public static final RegistryConfig TINK_1_1_0 = TINK_1_0_0;
 
   /**
    * @deprecated use {@link #register}
    * @since 1.2.0
    */
-  @Deprecated public static final RegistryConfig LATEST = TINK_1_0_0;
+  @Deprecated
+  public static final RegistryConfig LATEST = TINK_1_0_0;
 
   static {
     try {
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AeadFactory.java b/java_src/src/main/java/com/google/crypto/tink/aead/AeadFactory.java
index 53e87a9..2d3c60c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AeadFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AeadFactory.java
@@ -18,7 +18,6 @@
 
 import com.google.crypto.tink.Aead;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -45,8 +44,9 @@
    *     AeadWrapper} instead.
    */
   @Deprecated
-  public static Aead getPrimitive(KeysetHandle keysetHandle) throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new AeadWrapper());
+  public static Aead getPrimitive(KeysetHandle keysetHandle)
+      throws GeneralSecurityException {
+    AeadWrapper.register();
     return keysetHandle.getPrimitive(Aead.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AeadKey.java b/java_src/src/main/java/com/google/crypto/tink/aead/AeadKey.java
new file mode 100644
index 0000000..6c132cb
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AeadKey.java
@@ -0,0 +1,39 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.Bytes;
+
+/** Represents functions to encrypt and decrypt data using AEAD. */
+public abstract class AeadKey extends Key {
+  /**
+   * Returns a {@link Bytes} instance which is prefixed to the ciphertext.
+   *
+   * <p>In order to make key rotation more efficient, Tink allows every Aead key to be prefixed with
+   * a sequence of bytes. When decrypting data, only keys with matching prefix have to be tried.
+   *
+   * <p>Note that a priori, the output prefix may not be unique in a keyset (i.e., different keys in
+   * a keyset may have the same prefix or, one prefix may be a prefix of the other). To avoid this,
+   * built in Tink keys use the convention that the prefix is either '0x00<big endian key id>' or
+   * '0x01<big endian key id>'. See the Tink keys for details.
+   */
+  public abstract Bytes getOutputPrefix();
+  /** Returns the parameters of this key. */
+  @Override
+  public abstract AeadParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AeadKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/aead/AeadKeyTemplates.java
index 48a7375..cf4734e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AeadKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AeadKeyTemplates.java
@@ -33,6 +33,21 @@
 /**
  * Pre-generated {@link KeyTemplate} for {@link com.google.crypto.tink.Aead} keys.
  *
+ * <p>We recommend to avoid this class to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
  * <p>One can use these templates to generate new {@link com.google.crypto.tink.proto.Keyset} with
  * {@link com.google.crypto.tink.KeysetHandle#generateNew}. To generate a new keyset that contains a
  * single {@link com.google.crypto.tink.proto.AesGcmKey}, one can do:
@@ -44,10 +59,7 @@
  * }</pre>
  *
  * @since 1.0.0
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("AES128_GCM")
  */
-@Deprecated
 public final class AeadKeyTemplates {
   /**
    * A {@link KeyTemplate} that generates new instances of {@link
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/AeadParameters.java
new file mode 100644
index 0000000..6724061
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AeadParameters.java
@@ -0,0 +1,24 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.Parameters;
+import com.google.errorprone.annotations.Immutable;
+
+/** Represents a description of a {@link AeadKey} */
+@Immutable
+public abstract class AeadParameters extends Parameters {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AeadWrapper.java b/java_src/src/main/java/com/google/crypto/tink/aead/AeadWrapper.java
index c81ee40..50b7a92 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AeadWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AeadWrapper.java
@@ -42,6 +42,8 @@
 public class AeadWrapper implements PrimitiveWrapper<Aead, Aead> {
   private static final Logger logger = Logger.getLogger(AeadWrapper.class.getName());
 
+  private static final AeadWrapper WRAPPER = new AeadWrapper();
+
   private static class WrappedAead implements Aead {
     private final PrimitiveSet<Aead> pSet;
     private final MonitoringClient.Logger encLogger;
@@ -131,6 +133,6 @@
   }
 
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new AeadWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKey.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKey.java
new file mode 100644
index 0000000..d9147cc
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKey.java
@@ -0,0 +1,187 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Represents an AES-CTR-HMAC key used for computing AEAD. */
+public final class AesCtrHmacAeadKey extends AeadKey {
+  private final AesCtrHmacAeadParameters parameters;
+  private final SecretBytes aesKeyBytes;
+  private final SecretBytes hmacKeyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for AesCtrHmacAeadKey. */
+  public static class Builder {
+    @Nullable private AesCtrHmacAeadParameters parameters = null;
+    @Nullable private SecretBytes aesKeyBytes = null;
+    @Nullable private SecretBytes hmacKeyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(AesCtrHmacAeadParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setAesKeyBytes(SecretBytes aesKeyBytes) {
+      this.aesKeyBytes = aesKeyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHmacKeyBytes(SecretBytes hmacKeyBytes) {
+      this.hmacKeyBytes = hmacKeyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == AesCtrHmacAeadParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == AesCtrHmacAeadParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == AesCtrHmacAeadParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown AesCtrHmacAeadParameters.Variant: " + parameters.getVariant());
+    }
+
+    public AesCtrHmacAeadKey build() throws GeneralSecurityException {
+      if (parameters == null) {
+        throw new GeneralSecurityException("Cannot build without parameters");
+      }
+
+      if (aesKeyBytes == null || hmacKeyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without key material");
+      }
+
+      if (parameters.getAesKeySizeBytes() != aesKeyBytes.size()) {
+        throw new GeneralSecurityException("AES key size mismatch");
+      }
+
+      if (parameters.getHmacKeySizeBytes() != hmacKeyBytes.size()) {
+        throw new GeneralSecurityException("HMAC key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new AesCtrHmacAeadKey(
+          parameters, aesKeyBytes, hmacKeyBytes, outputPrefix, idRequirement);
+    }
+  }
+
+  private AesCtrHmacAeadKey(
+      AesCtrHmacAeadParameters parameters,
+      SecretBytes aesKeyBytes,
+      SecretBytes hmacKeyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.aesKeyBytes = aesKeyBytes;
+    this.hmacKeyBytes = hmacKeyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying AES key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getAesKeyBytes() {
+    return aesKeyBytes;
+  }
+
+  /** Returns the underlying HMAC key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getHmacKeyBytes() {
+    return hmacKeyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public AesCtrHmacAeadParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesCtrHmacAeadKey)) {
+      return false;
+    }
+    AesCtrHmacAeadKey that = (AesCtrHmacAeadKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.aesKeyBytes.equalsSecretBytes(aesKeyBytes)
+        && that.hmacKeyBytes.equalsSecretBytes(hmacKeyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManager.java
index 0ca4329..6c0b393 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManager.java
@@ -40,6 +40,8 @@
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.IOException;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Collections;
 import java.util.HashMap;
@@ -125,6 +127,40 @@
             .build();
       }
 
+      // To ensure that the derived key can provide key commitment, the AES-CTR key must be derived
+      // before the HMAC key.
+      // Consider the following malicious scenario using a brute-forced key InputStream with a 0 as
+      // its 32nd byte:
+      //     31 bytes || 1 byte of 0s || 16 bytes
+      // We give this stream to party A, saying that it is 32-byte HMAC key || 16-byte AES key. We
+      // also give this stream to party B, saying that it is 31-byte HMAC key || 16-byte AES key.
+      // Since HMAC pads the key with zeroes, this same stream will lead to both parties using the
+      // same HMAC key but different AES keys.
+      @Override
+      public AesCtrHmacAeadKey deriveKey(AesCtrHmacAeadKeyFormat format, InputStream inputStream)
+          throws GeneralSecurityException {
+        validateKeyFormat(format);
+        byte[] aesCtrKeyBytes = new byte[format.getAesCtrKeyFormat().getKeySize()];
+        try {
+          readFully(inputStream, aesCtrKeyBytes);
+        } catch (IOException e) {
+          throw new GeneralSecurityException("Reading pseudorandomness failed", e);
+        }
+        HmacKey hmacKey =
+            new HmacKeyManager().keyFactory().deriveKey(format.getHmacKeyFormat(), inputStream);
+        AesCtrKey aesCtrKey =
+            AesCtrKey.newBuilder()
+                .setParams(format.getAesCtrKeyFormat().getParams())
+                .setVersion(getVersion())
+                .setKeyValue(ByteString.copyFrom(aesCtrKeyBytes))
+                .build();
+        return AesCtrHmacAeadKey.newBuilder()
+            .setVersion(getVersion())
+            .setAesCtrKey(aesCtrKey)
+            .setHmacKey(hmacKey)
+            .build();
+      }
+
       @Override
       public Map<String, KeyFactory.KeyFormat<AesCtrHmacAeadKeyFormat>> keyFormats()
           throws GeneralSecurityException {
@@ -151,6 +187,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesCtrHmacAeadKeyManager(), newKeyAllowed);
+    AesCtrHmacAeadProtoSerialization.register();
   }
 
   /**
@@ -163,10 +200,7 @@
    *       <li>HMAC tag size: 16 bytes
    *       <li>HMAC hash function: SHA256
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_CTR_HMAC_SHA256")}
    */
-  @Deprecated
   public static final KeyTemplate aes128CtrHmacSha256Template() {
     return createKeyTemplate(16, 16, 32, 16, HashType.SHA256);
   }
@@ -181,10 +215,7 @@
    *       <li>HMAC tag size: 32 bytes
    *       <li>HMAC hash function: SHA256
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_CTR_HMAC_SHA256")}
    */
-  @Deprecated
   public static final KeyTemplate aes256CtrHmacSha256Template() {
     return createKeyTemplate(32, 16, 32, 32, HashType.SHA256);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadParameters.java
new file mode 100644
index 0000000..24a7681
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadParameters.java
@@ -0,0 +1,335 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Describes the parameters of an {@link AesCtrHmacAeadKey}. */
+public final class AesCtrHmacAeadParameters extends AeadParameters {
+  private static final int PREFIX_SIZE_IN_BYTES = 5;
+  /**
+   * Describes how the prefix is computed. For AEAD there are three main possibilities: NO_PREFIX
+   * (empty prefix), TINK (prefix the ciphertext with 0x01 followed by a 4-byte key id in big endian
+   * format) or CRUNCHY (prefix the ciphertext with 0x00 followed by a 4-byte key id in big endian
+   * format)
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The Hash algorithm used for the HMAC. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA1 = new HashType("SHA1");
+    public static final HashType SHA224 = new HashType("SHA224");
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builds a new AesCtrHmacAeadParameters instance. */
+  public static final class Builder {
+    @Nullable private Integer aesKeySizeBytes = null;
+    @Nullable private Integer hmacKeySizeBytes = null;
+    @Nullable private Integer ivSizeBytes = null;
+    @Nullable private Integer tagSizeBytes = null;
+    private HashType hashType = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    /** Accepts key sizes of 16, 24 or 32 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setAesKeySizeBytes(int aesKeySizeBytes) throws GeneralSecurityException {
+      if (aesKeySizeBytes != 16 && aesKeySizeBytes != 24 && aesKeySizeBytes != 32) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 16-byte, 24-byte and 32-byte AES keys are supported",
+                aesKeySizeBytes));
+      }
+      this.aesKeySizeBytes = aesKeySizeBytes;
+      return this;
+    }
+
+    /** Accepts key sizes of at least 16 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setHmacKeySizeBytes(int hmacKeySizeBytes) throws GeneralSecurityException {
+      if (hmacKeySizeBytes < 16) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size in bytes %d; HMAC key must be at least 16 bytes",
+                hmacKeySizeBytes));
+      }
+      this.hmacKeySizeBytes = hmacKeySizeBytes;
+      return this;
+    }
+
+    /** IV size must be between 12 and 16 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setIvSizeBytes(int ivSizeBytes) throws GeneralSecurityException {
+      if (ivSizeBytes < 12 || ivSizeBytes > 16) {
+        throw new GeneralSecurityException(
+            String.format(
+                "Invalid IV size in bytes %d; IV size must be between 12 and 16 bytes",
+                ivSizeBytes));
+      }
+      this.ivSizeBytes = ivSizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setTagSizeBytes(int tagSizeBytes) throws GeneralSecurityException {
+      if (tagSizeBytes < 10) {
+        throw new GeneralSecurityException(
+            String.format("Invalid tag size in bytes %d; must be at least 10 bytes", tagSizeBytes));
+      }
+      this.tagSizeBytes = tagSizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHashType(HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    private static void validateTagSizeBytes(int tagSizeBytes, HashType hashType)
+        throws GeneralSecurityException {
+      if (hashType == HashType.SHA1) {
+        if (tagSizeBytes > 20) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 20 bytes for SHA1", tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA224) {
+        if (tagSizeBytes > 28) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 28 bytes for SHA224",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA256) {
+        if (tagSizeBytes > 32) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 32 bytes for SHA256",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA384) {
+        if (tagSizeBytes > 48) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 48 bytes for SHA384",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA512) {
+        if (tagSizeBytes > 64) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 64 bytes for SHA512",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      throw new GeneralSecurityException(
+          "unknown hash type; must be SHA1, SHA224, SHA256, SHA384 or SHA512");
+    }
+
+    public AesCtrHmacAeadParameters build() throws GeneralSecurityException {
+      if (aesKeySizeBytes == null) {
+        throw new GeneralSecurityException("AES key size is not set");
+      }
+      if (hmacKeySizeBytes == null) {
+        throw new GeneralSecurityException("HMAC key size is not set");
+      }
+      if (ivSizeBytes == null) {
+        throw new GeneralSecurityException("iv size is not set");
+      }
+      if (tagSizeBytes == null) {
+        throw new GeneralSecurityException("tag size is not set");
+      }
+      if (hashType == null) {
+        throw new GeneralSecurityException("hash type is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("variant is not set");
+      }
+      validateTagSizeBytes(tagSizeBytes, hashType);
+      return new AesCtrHmacAeadParameters(
+          aesKeySizeBytes, hmacKeySizeBytes, ivSizeBytes, tagSizeBytes, variant, hashType);
+    }
+  }
+
+  private final int aesKeySizeBytes;
+  private final int hmacKeySizeBytes;
+  private final int ivSizeBytes;
+  private final int tagSizeBytes;
+  private final Variant variant;
+  private final HashType hashType;
+
+  private AesCtrHmacAeadParameters(
+      int aesKeySizeBytes,
+      int hmacKeySizeBytes,
+      int ivSizeBytes,
+      int tagSizeBytes,
+      Variant variant,
+      HashType hashType) {
+    this.aesKeySizeBytes = aesKeySizeBytes;
+    this.hmacKeySizeBytes = hmacKeySizeBytes;
+    this.ivSizeBytes = ivSizeBytes;
+    this.tagSizeBytes = tagSizeBytes;
+    this.variant = variant;
+    this.hashType = hashType;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getAesKeySizeBytes() {
+    return aesKeySizeBytes;
+  }
+
+  public int getHmacKeySizeBytes() {
+    return hmacKeySizeBytes;
+  }
+
+  public int getTagSizeBytes() {
+    return tagSizeBytes;
+  }
+
+  public int getIvSizeBytes() {
+    return ivSizeBytes;
+  }
+
+  /**
+   * Returns the size of the overhead added to the actual ciphertext (i.e. the size of the IV plus
+   * the size of the security relevant tag plus the size of the prefix with which this key prefixes
+   * the ciphertext.
+   */
+  public int getCiphertextOverheadSizeBytes() {
+    if (variant == Variant.NO_PREFIX) {
+      return getTagSizeBytes() + getIvSizeBytes();
+    }
+    if (variant == Variant.TINK || variant == Variant.CRUNCHY) {
+      return getTagSizeBytes() + getIvSizeBytes() + PREFIX_SIZE_IN_BYTES;
+    }
+    throw new IllegalStateException("Unknown variant");
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  /** Returns a hash type object. */
+  public HashType getHashType() {
+    return hashType;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesCtrHmacAeadParameters)) {
+      return false;
+    }
+    AesCtrHmacAeadParameters that = (AesCtrHmacAeadParameters) o;
+    return that.getAesKeySizeBytes() == getAesKeySizeBytes()
+        && that.getHmacKeySizeBytes() == getHmacKeySizeBytes()
+        && that.getIvSizeBytes() == getIvSizeBytes()
+        && that.getTagSizeBytes() == getTagSizeBytes()
+        && that.getVariant() == getVariant()
+        && that.getHashType() == getHashType();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        AesCtrHmacAeadParameters.class,
+        aesKeySizeBytes,
+        hmacKeySizeBytes,
+        ivSizeBytes,
+        tagSizeBytes,
+        variant,
+        hashType);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "AesCtrHmacAead Parameters (variant: "
+        + variant
+        + ", hashType: "
+        + hashType
+        + ", "
+        + ivSizeBytes
+        + "-byte IV, and "
+        + tagSizeBytes
+        + "-byte tags, and "
+        + aesKeySizeBytes
+        + "-byte AES key, and "
+        + hmacKeySizeBytes
+        + "-byte HMAC key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadProtoSerialization.java
new file mode 100644
index 0000000..d13a2b7
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesCtrHmacAeadProtoSerialization.java
@@ -0,0 +1,296 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link AesCtrHmacAeadKey} objects and {@link
+ * AesCtrHmacAeadParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesCtrHmacAeadProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<AesCtrHmacAeadParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesCtrHmacAeadProtoSerialization::serializeParameters,
+              AesCtrHmacAeadParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesCtrHmacAeadProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesCtrHmacAeadKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesCtrHmacAeadProtoSerialization::serializeKey,
+          AesCtrHmacAeadKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesCtrHmacAeadProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(AesCtrHmacAeadParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (AesCtrHmacAeadParameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (AesCtrHmacAeadParameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (AesCtrHmacAeadParameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static AesCtrHmacAeadParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return AesCtrHmacAeadParameters.Variant.TINK;
+      case CRUNCHY:
+      case LEGACY:
+        return AesCtrHmacAeadParameters.Variant.CRUNCHY;
+      case RAW:
+        return AesCtrHmacAeadParameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static HashType toProtoHashType(AesCtrHmacAeadParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (AesCtrHmacAeadParameters.HashType.SHA1.equals(hashType)) {
+      return HashType.SHA1;
+    }
+    if (AesCtrHmacAeadParameters.HashType.SHA224.equals(hashType)) {
+      return HashType.SHA224;
+    }
+    if (AesCtrHmacAeadParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (AesCtrHmacAeadParameters.HashType.SHA384.equals(hashType)) {
+      return HashType.SHA384;
+    }
+    if (AesCtrHmacAeadParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static AesCtrHmacAeadParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA1:
+        return AesCtrHmacAeadParameters.HashType.SHA1;
+      case SHA224:
+        return AesCtrHmacAeadParameters.HashType.SHA224;
+      case SHA256:
+        return AesCtrHmacAeadParameters.HashType.SHA256;
+      case SHA384:
+        return AesCtrHmacAeadParameters.HashType.SHA384;
+      case SHA512:
+        return AesCtrHmacAeadParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException("Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.HmacParams getHmacProtoParams(
+      AesCtrHmacAeadParameters parameters) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.HmacParams.newBuilder()
+        .setTagSize(parameters.getTagSizeBytes())
+        .setHash(toProtoHashType(parameters.getHashType()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(
+      AesCtrHmacAeadParameters parameters) throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.newBuilder()
+                    .setAesCtrKeyFormat(
+                        com.google.crypto.tink.proto.AesCtrKeyFormat.newBuilder()
+                            .setParams(
+                                com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                                    .setIvSize(parameters.getIvSizeBytes())
+                                    .build())
+                            .setKeySize(parameters.getAesKeySizeBytes())
+                            .build())
+                    .setHmacKeyFormat(
+                        com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                            .setParams(getHmacProtoParams(parameters))
+                            .setKeySize(parameters.getHmacKeySizeBytes())
+                            .build())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      AesCtrHmacAeadKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesCtrHmacAeadKey.newBuilder()
+            .setAesCtrKey(
+                com.google.crypto.tink.proto.AesCtrKey.newBuilder()
+                    .setParams(
+                        com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                            .setIvSize(key.getParameters().getIvSizeBytes())
+                            .build())
+                    .setKeyValue(
+                        ByteString.copyFrom(
+                            key.getAesKeyBytes()
+                                .toByteArray(SecretKeyAccess.requireAccess(access))))
+                    .build())
+            .setHmacKey(
+                com.google.crypto.tink.proto.HmacKey.newBuilder()
+                    .setParams(getHmacProtoParams(key.getParameters()))
+                    .setKeyValue(
+                        ByteString.copyFrom(
+                            key.getHmacKeyBytes()
+                                .toByteArray(SecretKeyAccess.requireAccess(access))))
+                    .build())
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesCtrHmacAeadParameters parseParameters(
+      ProtoParametersSerialization serialization) throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesCtrHmacAeadProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesCtrHmacAeadParameters failed: ", e);
+    }
+    if (format.getHmacKeyFormat().getVersion() != 0) {
+      throw new GeneralSecurityException("Only version 0 keys are accepted");
+    }
+    return AesCtrHmacAeadParameters.builder()
+        .setAesKeySizeBytes(format.getAesCtrKeyFormat().getKeySize())
+        .setHmacKeySizeBytes(format.getHmacKeyFormat().getKeySize())
+        .setIvSizeBytes(format.getAesCtrKeyFormat().getParams().getIvSize())
+        .setTagSizeBytes(format.getHmacKeyFormat().getParams().getTagSize())
+        .setHashType(toHashType(format.getHmacKeyFormat().getParams().getHash()))
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static AesCtrHmacAeadKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesCtrHmacAeadProtoSerialization.parseKey");
+    }
+    try {
+      com.google.crypto.tink.proto.AesCtrHmacAeadKey protoKey =
+          com.google.crypto.tink.proto.AesCtrHmacAeadKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      if (protoKey.getAesCtrKey().getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys inner AES CTR keys are accepted");
+      }
+      if (protoKey.getHmacKey().getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys inner HMAC keys are accepted");
+      }
+      AesCtrHmacAeadParameters parameters =
+          AesCtrHmacAeadParameters.builder()
+              .setAesKeySizeBytes(protoKey.getAesCtrKey().getKeyValue().size())
+              .setHmacKeySizeBytes(protoKey.getHmacKey().getKeyValue().size())
+              .setIvSizeBytes(protoKey.getAesCtrKey().getParams().getIvSize())
+              .setTagSizeBytes(protoKey.getHmacKey().getParams().getTagSize())
+              .setHashType(toHashType(protoKey.getHmacKey().getParams().getHash()))
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return AesCtrHmacAeadKey.builder()
+          .setParameters(parameters)
+          .setAesKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getAesCtrKey().getKeyValue().toByteArray(),
+                  SecretKeyAccess.requireAccess(access)))
+          .setHmacKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getHmacKey().getKeyValue().toByteArray(),
+                  SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesCtrHmacAeadKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesCtrHmacAeadProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKey.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKey.java
new file mode 100644
index 0000000..05d3344
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKey.java
@@ -0,0 +1,166 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents an AES-EAX key used for computing AEAD.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class AesEaxKey extends AeadKey {
+  private final AesEaxParameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for AesEaxKey. */
+  public static class Builder {
+    @Nullable private AesEaxParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(AesEaxParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == AesEaxParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == AesEaxParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == AesEaxParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown AesEaxParameters.Variant: " + parameters.getVariant());
+    }
+
+    public AesEaxKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new AesEaxKey(parameters, keyBytes, outputPrefix, idRequirement);
+    }
+  }
+
+  private AesEaxKey(
+      AesEaxParameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public AesEaxParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesEaxKey)) {
+      return false;
+    }
+    AesEaxKey that = (AesEaxKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKeyManager.java
index 560ad3c..b85206a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxKeyManager.java
@@ -126,6 +126,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesEaxKeyManager(), newKeyAllowed);
+    AesEaxProtoSerialization.register();
   }
 
   /**
@@ -136,10 +137,7 @@
    *       <li>IV size: 16 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_EAX")}
    */
-  @Deprecated
   public static final KeyTemplate aes128EaxTemplate() {
     return createKeyTemplate(16, 16, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -152,10 +150,7 @@
    *       <li>IV size: 16 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW} (no prefix)
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_EAX_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes128EaxTemplate() {
     return createKeyTemplate(16, 16, KeyTemplate.OutputPrefixType.RAW);
   }
@@ -168,10 +163,7 @@
    *       <li>IV size: 16 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_EAX")}
    */
-  @Deprecated
   public static final KeyTemplate aes256EaxTemplate() {
     return createKeyTemplate(32, 16, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -184,10 +176,7 @@
    *       <li>IV size: 16 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW} (no prefix)
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_EAX_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes256EaxTemplate() {
     return createKeyTemplate(32, 16, KeyTemplate.OutputPrefixType.RAW);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxParameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxParameters.java
new file mode 100644
index 0000000..426f2fa
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxParameters.java
@@ -0,0 +1,195 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Describes the parameters of an {@link AesEaxKey}. */
+public final class AesEaxParameters extends AeadParameters {
+  /**
+   * Describes how the prefix is computed. For AEAD there are three main possibilities: NO_PREFIX
+   * (empty prefix), TINK (prefix the ciphertext with 0x01 followed by a 4-byte key id in big endian
+   * format) or CRUNCHY (prefix the ciphertext with 0x00 followed by a 4-byte key id in big endian
+   * format)
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builds a new AesEaxParameters instance. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    @Nullable private Integer ivSizeBytes = null;
+    @Nullable private Integer tagSizeBytes = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    /**
+     * Accepts key sizes of 16, 24 or 32 bytes. However, some implementation may not support the
+     * full set of parameters at the moment and may restrict the key size to 16 or 32-byte values.
+     */
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes != 16 && keySizeBytes != 24 && keySizeBytes != 32) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 16-byte, 24-byte and 32-byte AES keys are supported",
+                keySizeBytes));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+    /** IV size must be 12 or 16 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setIvSizeBytes(int ivSizeBytes) throws GeneralSecurityException {
+      if (ivSizeBytes != 12 && ivSizeBytes != 16) {
+        throw new GeneralSecurityException(
+            String.format(
+                "Invalid IV size in bytes %d; acceptable values have 12 or 16 bytes", ivSizeBytes));
+      }
+      this.ivSizeBytes = ivSizeBytes;
+      return this;
+    }
+    /**
+     * The tag size accepts values between 0 and 16 bytes. However, some implementation may not
+     * support the full set of parameters at the moment and may restrict the tag size to a fixed
+     * value (i.e. 16 bytes).
+     */
+    @CanIgnoreReturnValue
+    public Builder setTagSizeBytes(int tagSizeBytes) throws GeneralSecurityException {
+      if (tagSizeBytes < 0 || tagSizeBytes > 16) {
+        throw new GeneralSecurityException(
+            String.format(
+                "Invalid tag size in bytes %d; value must be at most 16 bytes", tagSizeBytes));
+      }
+      this.tagSizeBytes = tagSizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public AesEaxParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("Key size is not set");
+      }
+      if (ivSizeBytes == null) {
+        throw new GeneralSecurityException("IV size is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("Variant is not set");
+      }
+
+      if (tagSizeBytes == null) {
+        throw new GeneralSecurityException("Tag size is not set");
+      }
+
+      return new AesEaxParameters(keySizeBytes, ivSizeBytes, tagSizeBytes, variant);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final int ivSizeBytes;
+  private final int tagSizeBytes;
+  private final Variant variant;
+
+  private AesEaxParameters(int keySizeBytes, int ivSizeBytes, int tagSizeBytes, Variant variant) {
+    this.keySizeBytes = keySizeBytes;
+    this.ivSizeBytes = ivSizeBytes;
+    this.tagSizeBytes = tagSizeBytes;
+    this.variant = variant;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  public int getIvSizeBytes() {
+    return ivSizeBytes;
+  }
+
+  public int getTagSizeBytes() {
+    return tagSizeBytes;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesEaxParameters)) {
+      return false;
+    }
+    AesEaxParameters that = (AesEaxParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getIvSizeBytes() == getIvSizeBytes()
+        && that.getTagSizeBytes() == getTagSizeBytes()
+        && that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(AesEaxParameters.class, keySizeBytes, ivSizeBytes, tagSizeBytes, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "AesEax Parameters (variant: "
+        + variant
+        + ", "
+        + ivSizeBytes
+        + "-byte IV, "
+        + tagSizeBytes
+        + "-byte tag, and "
+        + keySizeBytes
+        + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxProtoSerialization.java
new file mode 100644
index 0000000..da134fd
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesEaxProtoSerialization.java
@@ -0,0 +1,217 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/** Methods to serialize and parse {@link AesEaxKey} objects and {@link AesEaxParameters} objects */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesEaxProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesEaxKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<AesEaxParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesEaxProtoSerialization::serializeParameters,
+              AesEaxParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesEaxProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesEaxKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesEaxProtoSerialization::serializeKey, AesEaxKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesEaxProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(AesEaxParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (AesEaxParameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (AesEaxParameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (AesEaxParameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static AesEaxParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return AesEaxParameters.Variant.TINK;
+        /** Parse LEGACY prefix to CRUNCHY, since they act the same for this type of key */
+      case CRUNCHY:
+      case LEGACY:
+        return AesEaxParameters.Variant.CRUNCHY;
+      case RAW:
+        return AesEaxParameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.AesEaxParams getProtoParams(
+      AesEaxParameters parameters) throws GeneralSecurityException {
+    /** Current implementation restricts to 16-byte tag value */
+    if (parameters.getTagSizeBytes() != 16) {
+      throw new GeneralSecurityException(
+          String.format(
+              "Invalid tag size in bytes %d. Currently Tink only supports aes eax keys with tag"
+                  + " size equal to 16 bytes.",
+              parameters.getTagSizeBytes()));
+    }
+    return com.google.crypto.tink.proto.AesEaxParams.newBuilder()
+        .setIvSize(parameters.getIvSizeBytes())
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(AesEaxParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(AesEaxKey key, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+            .setParams(getProtoParams(key.getParameters()))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesEaxParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesEaxParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesEaxKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesEaxKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesEaxParameters failed: ", e);
+    }
+    return AesEaxParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        .setIvSizeBytes(format.getParams().getIvSize())
+        /** Subtle implementation currently restricts tag size to 16 bytes. */
+        .setTagSizeBytes(16)
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static AesEaxKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesEaxParameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.AesEaxKey protoKey =
+          com.google.crypto.tink.proto.AesEaxKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      AesEaxParameters parameters =
+          AesEaxParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setIvSizeBytes(protoKey.getParams().getIvSize())
+              .setTagSizeBytes(16)
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return AesEaxKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesEaxcKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesEaxProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKey.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKey.java
new file mode 100644
index 0000000..0226e5e
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKey.java
@@ -0,0 +1,166 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents an AES-GCM key used for computing AEAD.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class AesGcmKey extends AeadKey {
+  private final AesGcmParameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for AesGcmKey. */
+  public static class Builder {
+    @Nullable private AesGcmParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(AesGcmParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == AesGcmParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == AesGcmParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == AesGcmParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown AesGcmParameters.Variant: " + parameters.getVariant());
+    }
+
+    public AesGcmKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new AesGcmKey(parameters, keyBytes, outputPrefix, idRequirement);
+    }
+  }
+
+  private AesGcmKey(
+      AesGcmParameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public AesGcmParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesGcmKey)) {
+      return false;
+    }
+    AesGcmKey that = (AesGcmKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKeyManager.java
index 29d87a4..e1ff14c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmKeyManager.java
@@ -109,10 +109,7 @@
 
         byte[] pseudorandomness = new byte[format.getKeySize()];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != format.getKeySize()) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return AesGcmKey.newBuilder()
               .setKeyValue(ByteString.copyFrom(pseudorandomness))
               .setVersion(getVersion())
@@ -137,6 +134,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesGcmKeyManager(), newKeyAllowed);
+    AesGcmProtoSerialization.register();
   }
 
   /**
@@ -149,9 +147,7 @@
    *     <p>On Android KitKat (API level 19), the {@link com.google.crypto.tink.Aead} instance
    *     generated by this key template does not support associated data. It might not work at all
    *     in older versions.
-   * @deprecated use {@code KeyTemplates.get("AES128_GCM")}
    */
-  @Deprecated
   public static final KeyTemplate aes128GcmTemplate() {
     return createKeyTemplate(16, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -168,9 +164,7 @@
    *     <p>On Android KitKat (API level 19), the {@link com.google.crypto.tink.Aead} instance
    *     generated by this key template does not support associated data. It might not work at all
    *     in older versions.
-   * @deprecated use {@code KeyTemplates.get("AES128_GCM_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes128GcmTemplate() {
     return createKeyTemplate(16, KeyTemplate.OutputPrefixType.RAW);
   }
@@ -185,9 +179,7 @@
    *     <p>On Android KitKat (API level 19), the {@link com.google.crypto.tink.Aead} instance
    *     generated by this key template does not support associated data. It might not work at all
    *     in older versions.
-   * @deprecated use {@code KeyTemplates.get("AES256_GCM")}
    */
-  @Deprecated
   public static final KeyTemplate aes256GcmTemplate() {
     return createKeyTemplate(32, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -204,9 +196,7 @@
    *     <p>On Android KitKat (API level 19), the {@link com.google.crypto.tink.Aead} instance
    *     generated by this key template does not support associated data. It might not work at all
    *     in older versions.
-   * @deprecated use {@code KeyTemplates.get("AES256_GCM_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes256GcmTemplate() {
     return createKeyTemplate(32, KeyTemplate.OutputPrefixType.RAW);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmParameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmParameters.java
new file mode 100644
index 0000000..0f0dd61
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmParameters.java
@@ -0,0 +1,199 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Describes the parameters of an {@link AesGcmKey} */
+public final class AesGcmParameters extends AeadParameters {
+  /**
+   * Describes how the prefix is computed. For AEAD there are three main possibilities: NO_PREFIX
+   * (empty prefix), TINK (prefix the ciphertext with 0x01 followed by a 4-byte key id in big endian
+   * format) or CRUNCHY (prefix the ciphertext with 0x00 followed by a 4-byte key id in big endian
+   * format)
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /**
+   * Builds a new AesGcmParameters instance. The class AesGcmParameters is not responsible for
+   * checking if all allowed values for the parameters are implemented and satisfy any potential
+   * security policies. Some implementation may not support the full set of parameters at the moment
+   * and may restrict them to certain lengths (i.e. key size may be restricted to 16 or 32 bytes).
+   */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    @Nullable private Integer ivSizeBytes = null;
+    @Nullable private Integer tagSizeBytes = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    /** Accepts key sizes of 16, 24 or 32 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes != 16 && keySizeBytes != 24 && keySizeBytes != 32) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 16-byte, 24-byte and 32-byte AES keys are supported",
+                keySizeBytes));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    /** IV size must greater than 0. */
+    @CanIgnoreReturnValue
+    public Builder setIvSizeBytes(int ivSizeBytes) throws GeneralSecurityException {
+      if (ivSizeBytes <= 0) {
+        throw new GeneralSecurityException(
+            String.format("Invalid IV size in bytes %d; IV size must be positive", ivSizeBytes));
+      }
+      this.ivSizeBytes = ivSizeBytes;
+      return this;
+    }
+    /** Tag size must be one of the following five values: 128, 120, 112, 104 or 96 bytes */
+    @CanIgnoreReturnValue
+    public Builder setTagSizeBytes(int tagSizeBytes) throws GeneralSecurityException {
+      if (tagSizeBytes != 12
+          && tagSizeBytes != 13
+          && tagSizeBytes != 14
+          && tagSizeBytes != 15
+          && tagSizeBytes != 16) {
+        throw new GeneralSecurityException(
+            String.format(
+                "Invalid tag size in bytes %d; value must be one of the following: 12, 13, 14, 15"
+                    + " or 16 bytes",
+                tagSizeBytes));
+      }
+      this.tagSizeBytes = tagSizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public AesGcmParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("Key size is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("Variant is not set");
+      }
+      if (ivSizeBytes == null) {
+        throw new GeneralSecurityException("IV size is not set");
+      }
+
+      if (tagSizeBytes == null) {
+        throw new GeneralSecurityException("Tag size is not set");
+      }
+
+      return new AesGcmParameters(keySizeBytes, ivSizeBytes, tagSizeBytes, variant);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final int ivSizeBytes;
+  private final int tagSizeBytes;
+  private final Variant variant;
+
+  private AesGcmParameters(int keySizeBytes, int ivSizeBytes, int tagSizeBytes, Variant variant) {
+    this.keySizeBytes = keySizeBytes;
+    this.ivSizeBytes = ivSizeBytes;
+    this.tagSizeBytes = tagSizeBytes;
+    this.variant = variant;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  public int getIvSizeBytes() {
+    return ivSizeBytes;
+  }
+
+  public int getTagSizeBytes() {
+    return tagSizeBytes;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesGcmParameters)) {
+      return false;
+    }
+    AesGcmParameters that = (AesGcmParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getIvSizeBytes() == getIvSizeBytes()
+        && that.getTagSizeBytes() == getTagSizeBytes()
+        && that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(AesGcmParameters.class, keySizeBytes, ivSizeBytes, tagSizeBytes, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "AesGcm Parameters (variant: "
+        + variant
+        + ", "
+        + ivSizeBytes
+        + "-byte IV, "
+        + tagSizeBytes
+        + "-byte tag, and "
+        + keySizeBytes
+        + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmProtoSerialization.java
new file mode 100644
index 0000000..2a911af
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmProtoSerialization.java
@@ -0,0 +1,229 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/** Methods to serialize and parse {@link AesGcmKey} objects and {@link AesGcmParameters} objects */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesGcmProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesGcmKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<AesGcmParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesGcmProtoSerialization::serializeParameters,
+              AesGcmParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesGcmProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesGcmKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesGcmProtoSerialization::serializeKey, AesGcmKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesGcmProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(AesGcmParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (AesGcmParameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (AesGcmParameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (AesGcmParameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static AesGcmParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return AesGcmParameters.Variant.TINK;
+        /** Parse LEGACY prefix to CRUNCHY, since they act the same for this type of key */
+      case CRUNCHY:
+      case LEGACY:
+        return AesGcmParameters.Variant.CRUNCHY;
+      case RAW:
+        return AesGcmParameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static void validateParameters(AesGcmParameters parameters)
+      throws GeneralSecurityException {
+    /** Current implementation restricts tag size to 16 bytes */
+    if (parameters.getTagSizeBytes() != 16) {
+      throw new GeneralSecurityException(
+          String.format(
+              "Invalid tag size in bytes %d. Currently Tink only supports serialization of AES GCM"
+                  + " keys with tag size equal to 16 bytes.",
+              parameters.getTagSizeBytes()));
+    }
+    /** Current implementation restricts IV size to 12 bytes */
+    if (parameters.getIvSizeBytes() != 12) {
+      throw new GeneralSecurityException(
+          String.format(
+              "Invalid IV size in bytes %d. Currently Tink only supports serialization of AES GCM"
+                  + " keys with IV size equal to 12 bytes.",
+              parameters.getIvSizeBytes()));
+    }
+  }
+
+  private static ProtoParametersSerialization serializeParameters(AesGcmParameters parameters)
+      throws GeneralSecurityException {
+    validateParameters(parameters);
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder()
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(AesGcmKey key, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    validateParameters(key.getParameters());
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesGcmParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesGcmParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesGcmKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesGcmKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesGcmParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException("Only version 0 parameters are accepted");
+    }
+    return AesGcmParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        /**
+         * Currently, the Tink subtle implementation has the following restrictions: IV is a
+         * uniformly random initialization vector of length 12 and the tag is restricted to 16
+         * bytes.
+         */
+        .setIvSizeBytes(12)
+        .setTagSizeBytes(16)
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static AesGcmKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesGcmParameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.AesGcmKey protoKey =
+          com.google.crypto.tink.proto.AesGcmKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      AesGcmParameters parameters =
+          AesGcmParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setIvSizeBytes(12)
+              .setTagSizeBytes(16)
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return AesGcmKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesGcmKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesGcmProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKey.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKey.java
new file mode 100644
index 0000000..7cc2c76
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKey.java
@@ -0,0 +1,166 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents an AES-GCM-SIV key used for computing AEAD.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class AesGcmSivKey extends AeadKey {
+  private final AesGcmSivParameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for AesGcmSivKey. */
+  public static class Builder {
+    @Nullable private AesGcmSivParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(AesGcmSivParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == AesGcmSivParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == AesGcmSivParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == AesGcmSivParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown AesGcmSivParameters.Variant: " + parameters.getVariant());
+    }
+
+    public AesGcmSivKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new AesGcmSivKey(parameters, keyBytes, outputPrefix, idRequirement);
+    }
+  }
+
+  private AesGcmSivKey(
+      AesGcmSivParameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public AesGcmSivParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesGcmSivKey)) {
+      return false;
+    }
+    AesGcmSivKey that = (AesGcmSivKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKeyManager.java
index e3013d7..e6b845e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivKeyManager.java
@@ -111,10 +111,7 @@
 
         byte[] pseudorandomness = new byte[format.getKeySize()];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != format.getKeySize()) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return AesGcmSivKey.newBuilder()
               .setKeyValue(ByteString.copyFrom(pseudorandomness))
               .setVersion(getVersion())
@@ -152,6 +149,7 @@
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     if (canUseAesGcmSive()) {
       Registry.registerKeyManager(new AesGcmSivKeyManager(), newKeyAllowed);
+      AesGcmSivProtoSerialization.register();
     }
   }
 
@@ -163,10 +161,7 @@
    *   <li>Key size: 16 bytes
    *   <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    * </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_GCM_SIV")}
    */
-  @Deprecated
   public static final KeyTemplate aes128GcmSivTemplate() {
     return createKeyTemplate(16, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -181,10 +176,7 @@
    * </ul>
    *
    * <p>Keys generated from this template should create ciphertexts compatible with other libraries.
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_GCM_SIV_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes128GcmSivTemplate() {
     return createKeyTemplate(16, KeyTemplate.OutputPrefixType.RAW);
   }
@@ -197,10 +189,7 @@
    *   <li>Key size: 32 bytes
    *   <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    * </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_GCM_SIV")}
    */
-  @Deprecated
   public static final KeyTemplate aes256GcmSivTemplate() {
     return createKeyTemplate(32, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -215,10 +204,7 @@
    * </ul>
    *
    * <p>Keys generated from this template should create ciphertexts compatible with other libraries.
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_GCM_SIV_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes256GcmSivTemplate() {
     return createKeyTemplate(32, KeyTemplate.OutputPrefixType.RAW);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivParameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivParameters.java
new file mode 100644
index 0000000..7111ef6
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivParameters.java
@@ -0,0 +1,133 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Describes the parameters of an {@link AesGcmSivSivKey} */
+public final class AesGcmSivParameters extends AeadParameters {
+  /**
+   * Describes how the prefix is computed. For AEAD there are three main possibilities: NO_PREFIX
+   * (empty prefix), TINK (prefix the ciphertext with 0x01 followed by a 4-byte key id in big endian
+   * format) or CRUNCHY (prefix the ciphertext with 0x00 followed by a 4-byte key id in big endian
+   * format)
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builds a new AesGcmSivParameters instance. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    /** Accepts key sizes of 16 and 32 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes != 16 && keySizeBytes != 32) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 16-byte and 32-byte AES keys are supported",
+                keySizeBytes));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public AesGcmSivParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("Key size is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("Variant is not set");
+      }
+      return new AesGcmSivParameters(keySizeBytes, variant);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final Variant variant;
+
+  private AesGcmSivParameters(int keySizeBytes, Variant variant) {
+    this.keySizeBytes = keySizeBytes;
+    this.variant = variant;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesGcmSivParameters)) {
+      return false;
+    }
+    AesGcmSivParameters that = (AesGcmSivParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes() && that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(AesGcmSivParameters.class, keySizeBytes, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "AesGcmSiv Parameters (variant: " + variant + ", " + keySizeBytes + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivProtoSerialization.java
new file mode 100644
index 0000000..e0f50b1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/AesGcmSivProtoSerialization.java
@@ -0,0 +1,203 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link AesGcmSivKey} objects and {@link AesGcmSivParameters}
+ * objects
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesGcmSivProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesGcmSivKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<AesGcmSivParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesGcmSivProtoSerialization::serializeParameters,
+              AesGcmSivParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesGcmSivProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesGcmSivKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesGcmSivProtoSerialization::serializeKey,
+          AesGcmSivKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesGcmSivProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(AesGcmSivParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (AesGcmSivParameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (AesGcmSivParameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (AesGcmSivParameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static AesGcmSivParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return AesGcmSivParameters.Variant.TINK;
+        /** Parse LEGACY prefix to CRUNCHY, since they act the same for this type of key */
+      case CRUNCHY:
+      case LEGACY:
+        return AesGcmSivParameters.Variant.CRUNCHY;
+      case RAW:
+        return AesGcmSivParameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static ProtoParametersSerialization serializeParameters(AesGcmSivParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder()
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      AesGcmSivKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesGcmSivParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesGcmSivParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesGcmSivKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesGcmSivKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesGcmSivParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException("Only version 0 parameters are accepted");
+    }
+    return AesGcmSivParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static AesGcmSivKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesGcmSivParameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.AesGcmSivKey protoKey =
+          com.google.crypto.tink.proto.AesGcmSivKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      AesGcmSivParameters parameters =
+          AesGcmSivParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return AesGcmSivKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesGcmSivKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesGcmSivProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/aead/BUILD.bazel
index 193904b..0d7f3b4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/BUILD.bazel
@@ -8,6 +8,7 @@
     name = "aes_gcm_key_manager",
     srcs = ["AesGcmKeyManager.java"],
     deps = [
+        ":aes_gcm_proto_serialization",
         "//proto:aes_gcm_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
@@ -19,7 +20,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -27,6 +28,7 @@
     name = "aes_gcm_siv_key_manager",
     srcs = ["AesGcmSivKeyManager.java"],
     deps = [
+        ":aes_gcm_siv_proto_serialization",
         "//proto:aes_gcm_siv_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
@@ -37,7 +39,7 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -68,6 +70,7 @@
     name = "aes_ctr_hmac_aead_key_manager",
     srcs = ["AesCtrHmacAeadKeyManager.java"],
     deps = [
+        ":aes_ctr_hmac_aead_proto_serialization",
         ":aes_ctr_key_manager",
         "//proto:aes_ctr_hmac_aead_java_proto",
         "//proto:aes_ctr_java_proto",
@@ -85,7 +88,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:encrypt_then_authenticate",
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -95,7 +98,6 @@
     deps = [
         ":aead_wrapper",
         "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -114,7 +116,7 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -132,6 +134,7 @@
     name = "x_cha_cha20_poly1305_key_manager",
     srcs = ["XChaCha20Poly1305KeyManager.java"],
     deps = [
+        ":x_cha_cha20_poly1305_proto_serialization",
         "//proto:tink_java_proto",
         "//proto:xchacha20_poly1305_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
@@ -142,7 +145,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
         "//src/main/java/com/google/crypto/tink/subtle:x_cha_cha20_poly1305",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -175,10 +178,11 @@
         "//src/main/java/com/google/crypto/tink:kms_client",
         "//src/main/java/com/google/crypto/tink:kms_clients",
         "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -195,7 +199,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -203,6 +207,7 @@
     name = "cha_cha20_poly1305_key_manager",
     srcs = ["ChaCha20Poly1305KeyManager.java"],
     deps = [
+        ":cha_cha20_poly1305_proto_serialization",
         "//proto:chacha20_poly1305_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
@@ -213,7 +218,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_poly1305",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -221,6 +226,7 @@
     name = "aes_eax_key_manager",
     srcs = ["AesEaxKeyManager.java"],
     deps = [
+        ":aes_eax_proto_serialization",
         "//proto:aes_eax_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
@@ -231,7 +237,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_eax_jce",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -254,12 +260,185 @@
     ],
 )
 
+java_library(
+    name = "aead_key",
+    srcs = ["AeadKey.java"],
+    deps = [
+        ":aead_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+    ],
+)
+
+java_library(
+    name = "aead_parameters",
+    srcs = ["AeadParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_eax_parameters",
+    srcs = ["AesEaxParameters.java"],
+    deps = [
+        ":aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_eax_key",
+    srcs = ["AesEaxKey.java"],
+    deps = [
+        ":aead_key",
+        ":aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_eax_proto_serialization",
+    srcs = ["AesEaxProtoSerialization.java"],
+    deps = [
+        ":aes_eax_key",
+        ":aes_eax_parameters",
+        "//proto:aes_eax_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_parameters",
+    srcs = ["AesGcmParameters.java"],
+    deps = [
+        ":aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_key",
+    srcs = ["AesGcmKey.java"],
+    deps = [
+        ":aead_key",
+        ":aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_siv_parameters",
+    srcs = ["AesGcmSivParameters.java"],
+    deps = [
+        ":aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_siv_key",
+    srcs = ["AesGcmSivKey.java"],
+    deps = [
+        ":aead_key",
+        ":aes_gcm_siv_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_proto_serialization",
+    srcs = ["AesGcmProtoSerialization.java"],
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        "//proto:aes_gcm_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_siv_proto_serialization",
+    srcs = ["AesGcmSivProtoSerialization.java"],
+    deps = [
+        ":aes_gcm_siv_key",
+        ":aes_gcm_siv_parameters",
+        "//proto:aes_gcm_siv_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
 # Android libraries
 
 android_library(
     name = "aes_gcm_key_manager-android",
     srcs = ["AesGcmKeyManager.java"],
     deps = [
+        ":aes_gcm_proto_serialization-android",
         "//proto:aes_gcm_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
@@ -271,7 +450,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -279,6 +458,7 @@
     name = "aes_gcm_siv_key_manager-android",
     srcs = ["AesGcmSivKeyManager.java"],
     deps = [
+        ":aes_gcm_siv_proto_serialization-android",
         "//proto:aes_gcm_siv_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
@@ -289,7 +469,7 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -320,6 +500,7 @@
     name = "aes_ctr_hmac_aead_key_manager-android",
     srcs = ["AesCtrHmacAeadKeyManager.java"],
     deps = [
+        ":aes_ctr_hmac_aead_proto_serialization-android",
         ":aes_ctr_key_manager-android",
         "//proto:aes_ctr_hmac_aead_java_proto_lite",
         "//proto:aes_ctr_java_proto_lite",
@@ -337,7 +518,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:encrypt_then_authenticate-android",
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -347,7 +528,6 @@
     deps = [
         ":aead_wrapper-android",
         "//src/main/java/com/google/crypto/tink:aead-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -366,7 +546,7 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -384,6 +564,7 @@
     name = "x_cha_cha20_poly1305_key_manager-android",
     srcs = ["XChaCha20Poly1305KeyManager.java"],
     deps = [
+        ":x_cha_cha20_poly1305_proto_serialization-android",
         "//proto:tink_java_proto_lite",
         "//proto:xchacha20_poly1305_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
@@ -394,7 +575,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:x_cha_cha20_poly1305-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -427,10 +608,11 @@
         "//src/main/java/com/google/crypto/tink:kms_client-android",
         "//src/main/java/com/google/crypto/tink:kms_clients-android",
         "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -447,7 +629,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -455,6 +637,7 @@
     name = "cha_cha20_poly1305_key_manager-android",
     srcs = ["ChaCha20Poly1305KeyManager.java"],
     deps = [
+        ":cha_cha20_poly1305_proto_serialization-android",
         "//proto:chacha20_poly1305_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
@@ -465,7 +648,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:cha_cha20_poly1305-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -473,6 +656,7 @@
     name = "aes_eax_key_manager-android",
     srcs = ["AesEaxKeyManager.java"],
     deps = [
+        ":aes_eax_proto_serialization-android",
         "//proto:aes_eax_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
@@ -483,7 +667,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_eax_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -505,3 +689,507 @@
         "//src/main/java/com/google/crypto/tink/mac:mac_config-android",
     ],
 )
+
+android_library(
+    name = "aead_key-android",
+    srcs = ["AeadKey.java"],
+    deps = [
+        ":aead_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+    ],
+)
+
+android_library(
+    name = "aead_parameters-android",
+    srcs = ["AeadParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_eax_parameters-android",
+    srcs = ["AesEaxParameters.java"],
+    deps = [
+        ":aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_eax_key-android",
+    srcs = ["AesEaxKey.java"],
+    deps = [
+        ":aead_key-android",
+        ":aes_eax_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_eax_proto_serialization-android",
+    srcs = ["AesEaxProtoSerialization.java"],
+    deps = [
+        ":aes_eax_key-android",
+        ":aes_eax_parameters-android",
+        "//proto:aes_eax_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_parameters-android",
+    srcs = ["AesGcmParameters.java"],
+    deps = [
+        ":aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_key-android",
+    srcs = ["AesGcmKey.java"],
+    deps = [
+        ":aead_key-android",
+        ":aes_gcm_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_siv_parameters-android",
+    srcs = ["AesGcmSivParameters.java"],
+    deps = [
+        ":aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_siv_key-android",
+    srcs = ["AesGcmSivKey.java"],
+    deps = [
+        ":aead_key-android",
+        ":aes_gcm_siv_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_proto_serialization-android",
+    srcs = ["AesGcmProtoSerialization.java"],
+    deps = [
+        ":aes_gcm_key-android",
+        ":aes_gcm_parameters-android",
+        "//proto:aes_gcm_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_siv_proto_serialization-android",
+    srcs = ["AesGcmSivProtoSerialization.java"],
+    deps = [
+        ":aes_gcm_siv_key-android",
+        ":aes_gcm_siv_parameters-android",
+        "//proto:aes_gcm_siv_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "cha_cha20_poly1305_key-android",
+    srcs = ["ChaCha20Poly1305Key.java"],
+    deps = [
+        ":aead_key-android",
+        ":cha_cha20_poly1305_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "cha_cha20_poly1305_parameters-android",
+    srcs = ["ChaCha20Poly1305Parameters.java"],
+    deps = [
+        ":aead_parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "cha_cha20_poly1305_key",
+    srcs = ["ChaCha20Poly1305Key.java"],
+    deps = [
+        ":aead_key",
+        ":cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "cha_cha20_poly1305_parameters",
+    srcs = ["ChaCha20Poly1305Parameters.java"],
+    deps = [
+        ":aead_parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "x_cha_cha20_poly1305_key-android",
+    srcs = ["XChaCha20Poly1305Key.java"],
+    deps = [
+        ":aead_key-android",
+        ":x_cha_cha20_poly1305_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "x_cha_cha20_poly1305_parameters-android",
+    srcs = ["XChaCha20Poly1305Parameters.java"],
+    deps = [
+        ":aead_parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "x_cha_cha20_poly1305_key",
+    srcs = ["XChaCha20Poly1305Key.java"],
+    deps = [
+        ":aead_key",
+        ":x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "x_cha_cha20_poly1305_parameters",
+    srcs = ["XChaCha20Poly1305Parameters.java"],
+    deps = [
+        ":aead_parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "cha_cha20_poly1305_proto_serialization-android",
+    srcs = ["ChaCha20Poly1305ProtoSerialization.java"],
+    deps = [
+        ":cha_cha20_poly1305_key-android",
+        ":cha_cha20_poly1305_parameters-android",
+        "//proto:chacha20_poly1305_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "cha_cha20_poly1305_proto_serialization",
+    srcs = ["ChaCha20Poly1305ProtoSerialization.java"],
+    deps = [
+        ":cha_cha20_poly1305_key",
+        ":cha_cha20_poly1305_parameters",
+        "//proto:chacha20_poly1305_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "x_cha_cha20_poly1305_proto_serialization",
+    srcs = ["XChaCha20Poly1305ProtoSerialization.java"],
+    deps = [
+        ":x_cha_cha20_poly1305_key",
+        ":x_cha_cha20_poly1305_parameters",
+        "//proto:tink_java_proto",
+        "//proto:xchacha20_poly1305_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "x_cha_cha20_poly1305_proto_serialization-android",
+    srcs = ["XChaCha20Poly1305ProtoSerialization.java"],
+    deps = [
+        ":x_cha_cha20_poly1305_key-android",
+        ":x_cha_cha20_poly1305_parameters-android",
+        "//proto:tink_java_proto_lite",
+        "//proto:xchacha20_poly1305_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "aes_ctr_hmac_aead_parameters",
+    srcs = ["AesCtrHmacAeadParameters.java"],
+    deps = [
+        ":aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_ctr_hmac_aead_parameters-android",
+    srcs = ["AesCtrHmacAeadParameters.java"],
+    deps = [
+        ":aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_ctr_hmac_aead_key",
+    srcs = ["AesCtrHmacAeadKey.java"],
+    deps = [
+        ":aead_key",
+        ":aes_ctr_hmac_aead_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_ctr_hmac_aead_key-android",
+    srcs = ["AesCtrHmacAeadKey.java"],
+    deps = [
+        ":aead_key-android",
+        ":aes_ctr_hmac_aead_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_ctr_hmac_aead_proto_serialization",
+    srcs = ["AesCtrHmacAeadProtoSerialization.java"],
+    deps = [
+        ":aes_ctr_hmac_aead_key",
+        ":aes_ctr_hmac_aead_parameters",
+        "//proto:aes_ctr_hmac_aead_java_proto",
+        "//proto:aes_ctr_java_proto",
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "aes_ctr_hmac_aead_proto_serialization-android",
+    srcs = ["AesCtrHmacAeadProtoSerialization.java"],
+    deps = [
+        ":aes_ctr_hmac_aead_key-android",
+        ":aes_ctr_hmac_aead_parameters-android",
+        "//proto:aes_ctr_hmac_aead_java_proto_lite",
+        "//proto:aes_ctr_java_proto_lite",
+        "//proto:common_java_proto_lite",
+        "//proto:hmac_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "predefined_aead_parameters-android",
+    srcs = ["PredefinedAeadParameters.java"],
+    deps = [
+        ":aes_ctr_hmac_aead_parameters-android",
+        ":aes_eax_parameters-android",
+        ":aes_gcm_parameters-android",
+        ":cha_cha20_poly1305_parameters-android",
+        ":x_cha_cha20_poly1305_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+    ],
+)
+
+java_library(
+    name = "predefined_aead_parameters",
+    srcs = ["PredefinedAeadParameters.java"],
+    deps = [
+        ":aes_ctr_hmac_aead_parameters",
+        ":aes_eax_parameters",
+        ":aes_gcm_parameters",
+        ":cha_cha20_poly1305_parameters",
+        ":x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305Key.java b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305Key.java
new file mode 100644
index 0000000..9a3c939
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305Key.java
@@ -0,0 +1,146 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the Aead ChaCha20-Poly1305 specified in RFC 8439.
+ *
+ * <p>ChaCha20-Poly1305 allows no parameters; hence the main part here is really just the keys.
+ * However, Tink allows prefixing every ciphertext with an ID-dependent prefix, see {@link
+ * ChaCha20Poly1305Parameters.Variant}.
+ */
+@Alpha
+@Immutable
+public final class ChaCha20Poly1305Key extends AeadKey {
+  private final ChaCha20Poly1305Parameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  private ChaCha20Poly1305Key(
+      ChaCha20Poly1305Parameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  private static Bytes getOutputPrefix(
+      ChaCha20Poly1305Parameters parameters, @Nullable Integer idRequirement) {
+    if (parameters.getVariant() == ChaCha20Poly1305Parameters.Variant.NO_PREFIX) {
+      return Bytes.copyFrom(new byte[] {});
+    }
+    if (parameters.getVariant() == ChaCha20Poly1305Parameters.Variant.CRUNCHY) {
+      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+    }
+    if (parameters.getVariant() == ChaCha20Poly1305Parameters.Variant.TINK) {
+      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+    }
+    throw new IllegalStateException("Unknown Variant: " + parameters.getVariant());
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  @AccessesPartialKey
+  public static ChaCha20Poly1305Key create(SecretBytes secretBytes)
+      throws GeneralSecurityException {
+    return create(ChaCha20Poly1305Parameters.Variant.NO_PREFIX, secretBytes, null);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static ChaCha20Poly1305Key create(
+      ChaCha20Poly1305Parameters.Variant variant,
+      SecretBytes secretBytes,
+      @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+    if (variant != ChaCha20Poly1305Parameters.Variant.NO_PREFIX && idRequirement == null) {
+      throw new GeneralSecurityException(
+          "For given Variant " + variant + " the value of idRequirement must be non-null");
+    }
+    if (variant == ChaCha20Poly1305Parameters.Variant.NO_PREFIX && idRequirement != null) {
+      throw new GeneralSecurityException(
+          "For given Variant NO_PREFIX the value of idRequirement must be null");
+    }
+    if (secretBytes.size() != 32) {
+      throw new GeneralSecurityException(
+          "ChaCha20Poly1305 key must be constructed with key of length 32 bytes, not "
+              + secretBytes.size());
+    }
+    ChaCha20Poly1305Parameters parameters = ChaCha20Poly1305Parameters.create(variant);
+    return new ChaCha20Poly1305Key(
+        parameters, secretBytes, getOutputPrefix(parameters, idRequirement), idRequirement);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public ChaCha20Poly1305Parameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof ChaCha20Poly1305Key)) {
+      return false;
+    }
+    ChaCha20Poly1305Key that = (ChaCha20Poly1305Key) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManager.java
index 2611eb7..76c0b5a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManager.java
@@ -124,13 +124,12 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new ChaCha20Poly1305KeyManager(), newKeyAllowed);
+    ChaCha20Poly1305ProtoSerialization.register();
   }
 
   /**
    * @return a {@link KeyTemplate} that generates new instances of ChaCha20Poly1305 keys.
-   * @deprecated use {@code KeyTemplates.get("CHACHA20_POLY1305")}
    */
-  @Deprecated
   public static final KeyTemplate chaCha20Poly1305Template() {
     return KeyTemplate.create(
         new ChaCha20Poly1305KeyManager().getKeyType(),
@@ -142,9 +141,7 @@
    * @return a {@link KeyTemplate} that generates new instances of ChaCha20Poly1305 keys. Keys
    *     generated from this template create ciphertexts compatible with libsodium and other
    *     libraries.
-   * @deprecated use {@code KeyTemplates.get("CHACHA20_POLY1305_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawChaCha20Poly1305Template() {
     return KeyTemplate.create(
         new ChaCha20Poly1305KeyManager().getKeyType(),
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305Parameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305Parameters.java
new file mode 100644
index 0000000..f507ac7
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305Parameters.java
@@ -0,0 +1,90 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.errorprone.annotations.Immutable;
+import java.util.Objects;
+
+/** Describes the parameters of an {@link ChaChaPoly1305Key}. */
+public final class ChaCha20Poly1305Parameters extends AeadParameters {
+  /**
+   * Describes how the prefix is computed. For AEAD there are three main possibilities: NO_PREFIX
+   * (empty prefix), TINK (prefix the ciphertext with 0x01 followed by a 4-byte key id in big endian
+   * format) or CRUNCHY (prefix the ciphertext with 0x00 followed by a 4-byte key id in big endian
+   * format)
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static ChaCha20Poly1305Parameters create() {
+    return new ChaCha20Poly1305Parameters(Variant.NO_PREFIX);
+  }
+
+  public static ChaCha20Poly1305Parameters create(Variant variant) {
+    return new ChaCha20Poly1305Parameters(variant);
+  }
+
+  private final Variant variant;
+
+  private ChaCha20Poly1305Parameters(Variant variant) {
+    this.variant = variant;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof ChaCha20Poly1305Parameters)) {
+      return false;
+    }
+    ChaCha20Poly1305Parameters that = (ChaCha20Poly1305Parameters) o;
+    return that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(ChaCha20Poly1305Parameters.class, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "ChaCha20Poly1305 Parameters (variant: " + variant + ")";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305ProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305ProtoSerialization.java
new file mode 100644
index 0000000..86df042
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/ChaCha20Poly1305ProtoSerialization.java
@@ -0,0 +1,195 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link ChaCha20Poly1305Key} objects and {@link
+ * ChaCha20Poly1305Parameters} objects
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class ChaCha20Poly1305ProtoSerialization {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<
+          ChaCha20Poly1305Parameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              ChaCha20Poly1305ProtoSerialization::serializeParameters,
+              ChaCha20Poly1305Parameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          ChaCha20Poly1305ProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<ChaCha20Poly1305Key, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          ChaCha20Poly1305ProtoSerialization::serializeKey,
+          ChaCha20Poly1305Key.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          ChaCha20Poly1305ProtoSerialization::parseKey,
+          TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(
+      ChaCha20Poly1305Parameters.Variant variant) throws GeneralSecurityException {
+    if (ChaCha20Poly1305Parameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (ChaCha20Poly1305Parameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (ChaCha20Poly1305Parameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static ChaCha20Poly1305Parameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return ChaCha20Poly1305Parameters.Variant.TINK;
+        /** Parse LEGACY prefix to CRUNCHY, since they act the same for this type of key */
+      case CRUNCHY:
+      case LEGACY:
+        return ChaCha20Poly1305Parameters.Variant.CRUNCHY;
+      case RAW:
+        return ChaCha20Poly1305Parameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static ProtoParametersSerialization serializeParameters(
+      ChaCha20Poly1305Parameters parameters) throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.getDefaultInstance()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      ChaCha20Poly1305Key key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static ChaCha20Poly1305Parameters parseParameters(
+      ProtoParametersSerialization serialization) throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to ChaCha20Poly1305Parameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    // ChaCha20Poly1305KeyFormat is currently an empty proto -- so it's not quite clear if we want
+    // to even do anything here. However, we might add a version field later, so we at least check
+    // that serialization.getKeyTemplate().getValue() is a proto-encoded string.
+    try {
+      Object unused =
+          com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing ChaCha20Poly1305Parameters failed: ", e);
+    }
+    return ChaCha20Poly1305Parameters.create(
+        toVariant(serialization.getKeyTemplate().getOutputPrefixType()));
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static ChaCha20Poly1305Key parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to ChaCha20Poly1305Parameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.ChaCha20Poly1305Key protoKey =
+          com.google.crypto.tink.proto.ChaCha20Poly1305Key.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      return ChaCha20Poly1305Key.create(
+          toVariant(serialization.getOutputPrefixType()),
+          SecretBytes.copyFrom(
+              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)),
+          serialization.getIdRequirementOrNull());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing ChaCha20Poly1305Key failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private ChaCha20Poly1305ProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAead.java b/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAead.java
index fe281d1..de641f4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAead.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAead.java
@@ -22,6 +22,9 @@
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * This primitive implements <a href="https://cloud.google.com/kms/docs/data-encryption-keys">
@@ -47,7 +50,30 @@
   private final Aead remote;
   private static final int LENGTH_ENCRYPTED_DEK = 4;
 
+  private static Set<String> listSupportedDekKeyTypes() {
+    HashSet<String> dekKeyTypeUrls = new HashSet<>();
+    dekKeyTypeUrls.add("type.googleapis.com/google.crypto.tink.AesGcmKey");
+    dekKeyTypeUrls.add("type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key");
+    dekKeyTypeUrls.add("type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key");
+    dekKeyTypeUrls.add("type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey");
+    dekKeyTypeUrls.add("type.googleapis.com/google.crypto.tink.AesGcmSivKey");
+    dekKeyTypeUrls.add("type.googleapis.com/google.crypto.tink.AesEaxKey");
+    return Collections.unmodifiableSet(dekKeyTypeUrls);
+  }
+
+  private static final Set<String> supportedDekKeyTypes = listSupportedDekKeyTypes();
+
+  public static boolean isSupportedDekKeyType(String dekKeyTypeUrl) {
+    return supportedDekKeyTypes.contains(dekKeyTypeUrl);
+  }
+
   public KmsEnvelopeAead(KeyTemplate dekTemplate, Aead remote) {
+    if (!isSupportedDekKeyType(dekTemplate.getTypeUrl())) {
+      throw new IllegalArgumentException(
+          "Unsupported DEK key type: "
+              + dekTemplate.getTypeUrl()
+              + ". Only Tink AEAD key types are supported.");
+    }
     this.dekTemplate = dekTemplate;
     this.remote = remote;
   }
@@ -56,7 +82,7 @@
   public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
       throws GeneralSecurityException {
     // Generate a new DEK.
-    byte[] dek = Registry.newKey(dekTemplate).toByteArray();
+    byte[] dek = Registry.newKeyData(dekTemplate).getValue().toByteArray();
     // Wrap it with remote.
     byte[] encryptedDek = remote.encrypt(dek, EMPTY_AAD);
     // Use DEK to encrypt plaintext.
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManager.java
index 44ebc31..21f11b9 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManager.java
@@ -21,6 +21,7 @@
 import com.google.crypto.tink.KmsClient;
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.internal.KeyTypeManager;
 import com.google.crypto.tink.internal.PrimitiveFactory;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
@@ -37,6 +38,9 @@
  * {@code KmsEnvelopeAead}.
  */
 public class KmsEnvelopeAeadKeyManager extends KeyTypeManager<KmsEnvelopeAeadKey> {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey";
+
   KmsEnvelopeAeadKeyManager() {
     super(
         KmsEnvelopeAeadKey.class,
@@ -53,7 +57,7 @@
 
   @Override
   public String getKeyType() {
-    return "type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey";
+    return TYPE_URL;
   }
 
   @Override
@@ -69,6 +73,12 @@
   @Override
   public void validateKey(KmsEnvelopeAeadKey key) throws GeneralSecurityException {
     Validators.validateVersion(key.getVersion(), getVersion());
+    if (!KmsEnvelopeAead.isSupportedDekKeyType(key.getParams().getDekTemplate().getTypeUrl())) {
+      throw new GeneralSecurityException(
+          "Unsupported DEK key type: "
+              + key.getParams().getDekTemplate().getTypeUrl()
+              + ". Only Tink AEAD key types are supported.");
+    }
   }
 
   @Override
@@ -83,6 +93,12 @@
       @Override
       public void validateKeyFormat(KmsEnvelopeAeadKeyFormat format)
           throws GeneralSecurityException {
+        if (!KmsEnvelopeAead.isSupportedDekKeyType(format.getDekTemplate().getTypeUrl())) {
+          throw new GeneralSecurityException(
+              "Unsupported DEK key type: "
+                  + format.getDekTemplate().getTypeUrl()
+                  + ". Only Tink AEAD key types are supported.");
+        }
         if (format.getKekUri().isEmpty() || !format.hasDekTemplate()) {
           throw new GeneralSecurityException("invalid key format: missing KEK URI or DEK template");
         }
@@ -112,24 +128,35 @@
    * does not generate new key material, but only creates a reference to the remote KEK.
    */
   public static KeyTemplate createKeyTemplate(String kekUri, KeyTemplate dekTemplate) {
-    KmsEnvelopeAeadKeyFormat format = createKeyFormat(kekUri, dekTemplate);
-    return KeyTemplate.create(
-        new KmsEnvelopeAeadKeyManager().getKeyType(),
-        format.toByteArray(),
-        KeyTemplate.OutputPrefixType.RAW);
+    try {
+      KmsEnvelopeAeadKeyFormat format = createKeyFormat(kekUri, dekTemplate);
+      return KeyTemplate.create(TYPE_URL, format.toByteArray(), KeyTemplate.OutputPrefixType.RAW);
+    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
+      // It is in principle possible that this throws: if the "KeyTemplate" is created directly
+      // from a parameters object, but then we cannot serialize it.  However, the only way I can
+      // see this happen is if a user defines their own parameters object and then passes it in
+      // here, hence I think an IllegalArgumentError is appropriate.
+      throw new IllegalArgumentException("Unable to serialize key template", e);
+    }
   }
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new KmsEnvelopeAeadKeyManager(), newKeyAllowed);
   }
 
-  static KmsEnvelopeAeadKeyFormat createKeyFormat(String kekUri, KeyTemplate dekTemplate) {
+  static KmsEnvelopeAeadKeyFormat createKeyFormat(String kekUri, KeyTemplate dekTemplate)
+      throws GeneralSecurityException, InvalidProtocolBufferException {
+    if (!KmsEnvelopeAead.isSupportedDekKeyType(dekTemplate.getTypeUrl())) {
+      throw new IllegalArgumentException(
+          "Unsupported DEK key type: "
+              + dekTemplate.getTypeUrl()
+              + ". Only Tink AEAD key types are supported.");
+    }
+    byte[] serializedTemplate = TinkProtoParametersFormat.serialize(dekTemplate.toParameters());
     return KmsEnvelopeAeadKeyFormat.newBuilder()
         .setDekTemplate(
-            com.google.crypto.tink.proto.KeyTemplate.newBuilder()
-                .setTypeUrl(dekTemplate.getTypeUrl())
-                .setValue(ByteString.copyFrom(dekTemplate.getValue()))
-                .build())
+            com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+                serializedTemplate, ExtensionRegistryLite.getEmptyRegistry()))
         .setKekUri(kekUri)
         .build();
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/PredefinedAeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/PredefinedAeadParameters.java
new file mode 100644
index 0000000..54e4b74
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/PredefinedAeadParameters.java
@@ -0,0 +1,172 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+/**
+ * Pre-generated {@link KeyTemplate} for {@link com.google.crypto.tink.Aead} keys.
+ *
+ * <p>Note: if you want to keep dependencies small, consider inlining the constants here.
+ */
+public final class PredefinedAeadParameters {
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesGcmKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Key size: 16 bytes
+   * </ul>
+   *
+   * <p>On Android KitKat (API level 19), the {@link com.google.crypto.tink.Aead} instance generated
+   * by this key template does not support associated data. It might not work at all in older
+   * versions.
+   */
+  public static final AesGcmParameters AES128_GCM =
+      exceptionIsBug(
+          () ->
+              AesGcmParameters.builder()
+                  .setIvSizeBytes(12)
+                  .setKeySizeBytes(16)
+                  .setTagSizeBytes(16)
+                  .setVariant(AesGcmParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesGcmKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Key size: 32 bytes
+   * </ul>
+   *
+   * <p>On Android KitKat (API level 19), the {@link com.google.crypto.tink.Aead} instance generated
+   * by this key template does not support associated data. It might not work at all in older
+   * versions.
+   */
+  public static final AesGcmParameters AES256_GCM =
+      exceptionIsBug(
+          () ->
+              AesGcmParameters.builder()
+                  .setIvSizeBytes(12)
+                  .setKeySizeBytes(32)
+                  .setTagSizeBytes(16)
+                  .setVariant(AesGcmParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesEaxKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Key size: 16 bytes
+   *   <li>IV size: 16 bytes
+   * </ul>
+   */
+  public static final AesEaxParameters AES128_EAX =
+      exceptionIsBug(
+          () ->
+              AesEaxParameters.builder()
+                  .setIvSizeBytes(16)
+                  .setKeySizeBytes(16)
+                  .setTagSizeBytes(16)
+                  .setVariant(AesEaxParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesEaxKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Key size: 32 bytes
+   *   <li>IV size: 16 bytes
+   * </ul>
+   */
+  public static final AesEaxParameters AES256_EAX =
+      exceptionIsBug(
+          () ->
+              AesEaxParameters.builder()
+                  .setIvSizeBytes(16)
+                  .setKeySizeBytes(32)
+                  .setTagSizeBytes(16)
+                  .setVariant(AesEaxParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCtrHmacAeadKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>AES key size: 16 bytes
+   *   <li>AES CTR IV size: 16 byte
+   *   <li>HMAC key size: 32 bytes
+   *   <li>HMAC tag size: 16 bytes
+   *   <li>HMAC hash function: SHA256
+   * </ul>
+   */
+  public static final AesCtrHmacAeadParameters AES128_CTR_HMAC_SHA256 =
+      exceptionIsBug(
+          () ->
+              AesCtrHmacAeadParameters.builder()
+                  .setAesKeySizeBytes(16)
+                  .setHmacKeySizeBytes(32)
+                  .setTagSizeBytes(16)
+                  .setIvSizeBytes(16)
+                  .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                  .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCtrHmacAeadKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>AES key size: 32 bytes
+   *   <li>AES CTR IV size: 16 byte
+   *   <li>HMAC key size: 32 bytes
+   *   <li>HMAC tag size: 32 bytes
+   *   <li>HMAC hash function: SHA256
+   * </ul>
+   */
+  public static final AesCtrHmacAeadParameters AES256_CTR_HMAC_SHA256 =
+      exceptionIsBug(
+          () ->
+              AesCtrHmacAeadParameters.builder()
+                  .setAesKeySizeBytes(32)
+                  .setHmacKeySizeBytes(32)
+                  .setTagSizeBytes(32)
+                  .setIvSizeBytes(16)
+                  .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                  .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link KeyTemplate} that generates new instances of {@link
+   * com.google.crypto.tink.proto.ChaCha20Poly1305Key}.
+   */
+  public static final ChaCha20Poly1305Parameters CHACHA20_POLY1305 =
+      ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK);
+
+  /**
+   * A {@link KeyTemplate} that generates new instances of {@link
+   * com.google.crypto.tink.proto.XChaCha20Poly1305Key}.
+   */
+  public static final XChaCha20Poly1305Parameters XCHACHA20_POLY1305 =
+      XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK);
+
+  private PredefinedAeadParameters() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305Key.java b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305Key.java
new file mode 100644
index 0000000..a193388
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305Key.java
@@ -0,0 +1,147 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the Aead XChaCha20-Poly1305 proposed in the RFC draft at
+ * https://datatracker.ietf.org/doc/html/draft-arciszewski-xchacha-03.
+ *
+ * <p>XChaCha20-Poly1305 allows no parameters; hence the main part here is really just the key
+ * material. However, Tink allows prefixing every ciphertext with an ID-dependent prefix, see {@link
+ * XChaCha20Poly1305Parameters.Variant}.
+ */
+@Alpha
+@Immutable
+public final class XChaCha20Poly1305Key extends AeadKey {
+  private final XChaCha20Poly1305Parameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  private XChaCha20Poly1305Key(
+      XChaCha20Poly1305Parameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  private static Bytes getOutputPrefix(
+      XChaCha20Poly1305Parameters parameters, @Nullable Integer idRequirement) {
+    if (parameters.getVariant() == XChaCha20Poly1305Parameters.Variant.NO_PREFIX) {
+      return Bytes.copyFrom(new byte[] {});
+    }
+    if (parameters.getVariant() == XChaCha20Poly1305Parameters.Variant.CRUNCHY) {
+      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+    }
+    if (parameters.getVariant() == XChaCha20Poly1305Parameters.Variant.TINK) {
+      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+    }
+    throw new IllegalStateException("Unknown Variant: " + parameters.getVariant());
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  @AccessesPartialKey
+  public static XChaCha20Poly1305Key create(SecretBytes secretBytes)
+      throws GeneralSecurityException {
+    return create(XChaCha20Poly1305Parameters.Variant.NO_PREFIX, secretBytes, null);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static XChaCha20Poly1305Key create(
+      XChaCha20Poly1305Parameters.Variant variant,
+      SecretBytes secretBytes,
+      @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+    if (variant != XChaCha20Poly1305Parameters.Variant.NO_PREFIX && idRequirement == null) {
+      throw new GeneralSecurityException(
+          "For given Variant " + variant + " the value of idRequirement must be non-null");
+    }
+    if (variant == XChaCha20Poly1305Parameters.Variant.NO_PREFIX && idRequirement != null) {
+      throw new GeneralSecurityException(
+          "For given Variant NO_PREFIX the value of idRequirement must be null");
+    }
+    if (secretBytes.size() != 32) {
+      throw new GeneralSecurityException(
+          "XChaCha20Poly1305 key must be constructed with key of length 32 bytes, not "
+              + secretBytes.size());
+    }
+    XChaCha20Poly1305Parameters parameters = XChaCha20Poly1305Parameters.create(variant);
+    return new XChaCha20Poly1305Key(
+        parameters, secretBytes, getOutputPrefix(parameters, idRequirement), idRequirement);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public XChaCha20Poly1305Parameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof XChaCha20Poly1305Key)) {
+      return false;
+    }
+    XChaCha20Poly1305Key that = (XChaCha20Poly1305Key) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManager.java b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManager.java
index 73f0a64..a2efba3 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManager.java
@@ -116,10 +116,7 @@
 
         byte[] pseudorandomness = new byte[KEY_SIZE_IN_BYTES];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != KEY_SIZE_IN_BYTES) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return XChaCha20Poly1305Key.newBuilder()
               .setKeyValue(ByteString.copyFrom(pseudorandomness))
               .setVersion(getVersion())
@@ -149,13 +146,12 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new XChaCha20Poly1305KeyManager(), newKeyAllowed);
+    XChaCha20Poly1305ProtoSerialization.register();
   }
 
   /**
    * @return a {@link KeyTemplate} that generates new instances of XChaCha20Poly1305 keys.
-   * @deprecated use {@code KeyTemplates.get("XCHACHA20_POLY1305")}
    */
-  @Deprecated
   public static final KeyTemplate xChaCha20Poly1305Template() {
     return KeyTemplate.create(
         new XChaCha20Poly1305KeyManager().getKeyType(),
@@ -167,9 +163,7 @@
    * @return a {@link KeyTemplate} that generates new instances of XChaCha20Poly1305 keys. Keys
    *     generated from this template create ciphertexts compatible with libsodium and other
    *     libraries.
-   * @deprecated use {@code KeyTemplates.get("XCHACHA20_POLY1305_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawXChaCha20Poly1305Template() {
     return KeyTemplate.create(
         new XChaCha20Poly1305KeyManager().getKeyType(),
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305Parameters.java b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305Parameters.java
new file mode 100644
index 0000000..b9ed311
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305Parameters.java
@@ -0,0 +1,90 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import com.google.errorprone.annotations.Immutable;
+import java.util.Objects;
+
+/** Describes the parameters of an {@link XChaChaPoly1305Key}. */
+public final class XChaCha20Poly1305Parameters extends AeadParameters {
+  /**
+   * Describes how the prefix is computed. For AEAD there are three main possibilities: NO_PREFIX
+   * (empty prefix), TINK (prefix the ciphertext with 0x01 followed by a 4-byte key id in big endian
+   * format) or CRUNCHY (prefix the ciphertext with 0x00 followed by a 4-byte key id in big endian
+   * format).
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static XChaCha20Poly1305Parameters create() {
+    return new XChaCha20Poly1305Parameters(Variant.NO_PREFIX);
+  }
+
+  public static XChaCha20Poly1305Parameters create(Variant variant) {
+    return new XChaCha20Poly1305Parameters(variant);
+  }
+
+  private final Variant variant;
+
+  private XChaCha20Poly1305Parameters(Variant variant) {
+    this.variant = variant;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof XChaCha20Poly1305Parameters)) {
+      return false;
+    }
+    XChaCha20Poly1305Parameters that = (XChaCha20Poly1305Parameters) o;
+    return that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(XChaCha20Poly1305Parameters.class, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "XChaCha20Poly1305 Parameters (variant: " + variant + ")";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305ProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305ProtoSerialization.java
new file mode 100644
index 0000000..a75d267
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/XChaCha20Poly1305ProtoSerialization.java
@@ -0,0 +1,196 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link XChaCha20Poly1305Key} objects and {@link
+ * XChaCha20Poly1305Parameters} objects
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class XChaCha20Poly1305ProtoSerialization {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<
+          XChaCha20Poly1305Parameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              XChaCha20Poly1305ProtoSerialization::serializeParameters,
+              XChaCha20Poly1305Parameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          XChaCha20Poly1305ProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<XChaCha20Poly1305Key, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          XChaCha20Poly1305ProtoSerialization::serializeKey,
+          XChaCha20Poly1305Key.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          XChaCha20Poly1305ProtoSerialization::parseKey,
+          TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(
+      XChaCha20Poly1305Parameters.Variant variant) throws GeneralSecurityException {
+    if (XChaCha20Poly1305Parameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (XChaCha20Poly1305Parameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (XChaCha20Poly1305Parameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static XChaCha20Poly1305Parameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return XChaCha20Poly1305Parameters.Variant.TINK;
+        /** Parse LEGACY prefix to CRUNCHY, since they act the same for this type of key */
+      case CRUNCHY:
+      case LEGACY:
+        return XChaCha20Poly1305Parameters.Variant.CRUNCHY;
+      case RAW:
+        return XChaCha20Poly1305Parameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static ProtoParametersSerialization serializeParameters(
+      XChaCha20Poly1305Parameters parameters) throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.getDefaultInstance()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      XChaCha20Poly1305Key key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static XChaCha20Poly1305Parameters parseParameters(
+      ProtoParametersSerialization serialization) throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to XChaCha20Poly1305Parameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing XChaCha20Poly1305Parameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException("Only version 0 parameters are accepted");
+    }
+    return XChaCha20Poly1305Parameters.create(
+        toVariant(serialization.getKeyTemplate().getOutputPrefixType()));
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static XChaCha20Poly1305Key parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to XChaCha20Poly1305Parameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.XChaCha20Poly1305Key protoKey =
+          com.google.crypto.tink.proto.XChaCha20Poly1305Key.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      return XChaCha20Poly1305Key.create(
+          toVariant(serialization.getOutputPrefixType()),
+          SecretBytes.copyFrom(
+              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)),
+          serialization.getIdRequirementOrNull());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing XChaCha20Poly1305Key failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private XChaCha20Poly1305ProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel
index 6494333..e5ebcd4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel
@@ -14,8 +14,10 @@
     srcs = ["InsecureNonceAesGcmJce.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
+        "@maven//:com_google_code_findbugs_jsr305",
     ],
 )
 
@@ -94,8 +96,10 @@
     srcs = ["InsecureNonceAesGcmJce.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
+        "@maven//:com_google_code_findbugs_jsr305",
     ],
 )
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJce.java b/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJce.java
index a0c0039..87d9e6d 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJce.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJce.java
@@ -17,12 +17,13 @@
 package com.google.crypto.tink.aead.internal;
 
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.internal.Util;
 import com.google.crypto.tink.subtle.EngineFactory;
-import com.google.crypto.tink.subtle.SubtleUtil;
 import com.google.crypto.tink.subtle.Validators;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import java.security.spec.AlgorithmParameterSpec;
+import javax.annotation.Nullable;
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.GCMParameterSpec;
@@ -148,7 +149,8 @@
 
   private static AlgorithmParameterSpec getParams(final byte[] buf, int offset, int len)
       throws GeneralSecurityException {
-    if (SubtleUtil.isAndroid() && SubtleUtil.androidApiLevel() <= 19) {
+    @Nullable Integer apiLevel = Util.getAndroidApiLevel();
+    if (apiLevel != null && apiLevel <= 19) {
       // GCMParameterSpec should always be present in Java 7 or newer, but it's unsupported on
       // Android devices with API level <= 19. Fortunately, if a modern copy of Conscrypt is present
       // (either through GMS Core or bundled with the app) we can initialize the cipher with just an
diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/subtle/AesGcmSiv.java b/java_src/src/main/java/com/google/crypto/tink/aead/subtle/AesGcmSiv.java
index adf5c78..e6a3053 100644
--- a/java_src/src/main/java/com/google/crypto/tink/aead/subtle/AesGcmSiv.java
+++ b/java_src/src/main/java/com/google/crypto/tink/aead/subtle/AesGcmSiv.java
@@ -57,6 +57,7 @@
   // All instances of this class use a 12 byte IV and a 16 byte tag.
   private static final int IV_SIZE_IN_BYTES = 12;
   private static final int TAG_SIZE_IN_BYTES = 16;
+  private static final boolean HAS_GCM_PARAMETER_SPEC_CLASS = hasGCMParameterSpecClass();
 
   private final SecretKey keySpec;
 
@@ -128,19 +129,26 @@
 
   private static AlgorithmParameterSpec getParams(final byte[] buf, int offset, int len)
       throws GeneralSecurityException {
-    try {
-      Class.forName("javax.crypto.spec.GCMParameterSpec");
+    if (HAS_GCM_PARAMETER_SPEC_CLASS) {
       return new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, buf, offset, len);
-    } catch (ClassNotFoundException e) {
-      if (SubtleUtil.isAndroid()) {
-        // GCMParameterSpec should always be present in Java 7 or newer, but it's missing on Android
-        // devices with API level < 19. Fortunately, with a modern copy of Conscrypt (either through
-        // GMS or bundled with the app) we can initialize the cipher with just an IvParameterSpec.
-        // It will use a tag size of 128 bits. We'd double check the tag size in encrypt().
-        return new IvParameterSpec(buf, offset, len);
-      }
+    }
+    if (SubtleUtil.isAndroid()) {
+      // GCMParameterSpec should always be present in Java 7 or newer, but it's missing on Android
+      // devices with API level < 19. Fortunately, with a modern copy of Conscrypt (either through
+      // GMS or bundled with the app) we can initialize the cipher with just an IvParameterSpec.
+      // It will use a tag size of 128 bits. We'd double check the tag size in encrypt().
+      return new IvParameterSpec(buf, offset, len);
     }
     throw new GeneralSecurityException(
         "cannot use AES-GCM: javax.crypto.spec.GCMParameterSpec not found");
   }
+
+  private static boolean hasGCMParameterSpecClass() {
+    try {
+      Class.forName("javax.crypto.spec.GCMParameterSpec");
+    } catch (ClassNotFoundException e) {
+      return false;
+    }
+    return true;
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/config/TinkConfig.java b/java_src/src/main/java/com/google/crypto/tink/config/TinkConfig.java
index 5b3bed1..b94cbeb 100644
--- a/java_src/src/main/java/com/google/crypto/tink/config/TinkConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/config/TinkConfig.java
@@ -35,48 +35,28 @@
  * }</pre>
  *
  * @since 1.0.0
- * @deprecated Use per-primitive configs, e.g., {@link AeadConfig}, {@link HybridConfig}, etc.
  */
-@Deprecated
 public final class TinkConfig {
-  /** @deprecated */
-  @Deprecated
-  public static final RegistryConfig TINK_1_0_0 =
-      RegistryConfig.newBuilder()
-          .mergeFrom(
-              HybridConfig.TINK_1_0_0) // include AeadConfig.TINK_1_0_0 and MacConfig.TINK_1_0_0
-          .mergeFrom(SignatureConfig.TINK_1_0_0)
-          .setConfigName("TINK_1_0_0")
-          .build();
-
   /**
-   * @deprecated
-   * @since 1.1.0
+   * @deprecated Configs are not supported anymore. Please call {@code TinkConfig.register();}
+   *     instead of accessing this variable.
    */
   @Deprecated
-  public static final RegistryConfig TINK_1_1_0 =
-      RegistryConfig.newBuilder()
-          .mergeFrom(
-              HybridConfig.TINK_1_1_0) // include AeadConfig.TINK_1_0_0 and MacConfig.TINK_1_0_0
-          .mergeFrom(SignatureConfig.TINK_1_1_0)
-          .mergeFrom(DeterministicAeadConfig.TINK_1_1_0)
-          .mergeFrom(StreamingAeadConfig.TINK_1_1_0)
-          .setConfigName("TINK_1_1_0")
-          .build();
+  public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
 
   /**
-   * @deprecated This is not supported anymore.
-   * @since 1.2.0
+   * @deprecated Configs are not supported anymore. Please call {@code TinkConfig.register();}
+   *     instead of accessing this variable.
    */
   @Deprecated
-  public static final RegistryConfig LATEST =
-      RegistryConfig.newBuilder()
-          .mergeFrom(HybridConfig.LATEST) // include AeadConfig.LATEST and MacConfig.LATEST
-          .mergeFrom(SignatureConfig.LATEST)
-          .mergeFrom(DeterministicAeadConfig.LATEST)
-          .mergeFrom(StreamingAeadConfig.LATEST)
-          .setConfigName("TINK")
-          .build();
+  public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
+
+  /**
+   * @deprecated Configs are not supported anymore. Please call {@code TinkConfig.register();}
+   *     instead of accessing this variable.
+   */
+  @Deprecated
+  public static final RegistryConfig LATEST = RegistryConfig.getDefaultInstance();
 
   /**
    * Tries to register with the {@link Registry} all instances of {@link
@@ -90,6 +70,14 @@
     register();
   }
 
+  static {
+    try {
+      init();
+    } catch (GeneralSecurityException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
   /**
    * Tries to register with the {@link Registry} all instances of {@link
    * com.google.crypto.tink.Catalogue} and {@link com.google.crypto.tink.KeyManager} needed to
diff --git a/java_src/src/main/java/com/google/crypto/tink/config/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/config/internal/BUILD.bazel
index 471caae..e11dfc0 100644
--- a/java_src/src/main/java/com/google/crypto/tink/config/internal/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/config/internal/BUILD.bazel
@@ -17,21 +17,17 @@
     srcs = select({
         "//src/main/java/com/google/crypto/tink/config:fips_enabled": ["TinkFipsEnabled.java"],
         "//conditions:default": ["TinkFipsDisabled.java"],
-        }),
-);
+    }),
+)
 
 java_library(
     name = "tink_fips_util",
     srcs = ["TinkFipsUtil.java"],
-    deps = [
-        ":tink_fips_status",
-    ],
+    deps = [":tink_fips_status"],
 )
 
 android_library(
     name = "tink_fips_util-android",
     srcs = ["TinkFipsUtil.java"],
-    deps = [
-        ":tink_fips_status-android",
-    ],
+    deps = [":tink_fips_status-android"],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKey.java b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKey.java
new file mode 100644
index 0000000..c2984bc
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKey.java
@@ -0,0 +1,167 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents an AES--SIV key used for computing deterministic AEAD, as described in
+ * https://www.rfc-editor.org/rfc/rfc5297.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class AesSivKey extends DeterministicAeadKey {
+  private final AesSivParameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for AesSivKey. */
+  public static class Builder {
+    @Nullable private AesSivParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(AesSivParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    public AesSivKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new IllegalArgumentException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new AesSivKey(parameters, keyBytes, outputPrefix, idRequirement);
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == AesSivParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == AesSivParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == AesSivParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown AesSivParameters.Variant: " + parameters.getVariant());
+    }
+  }
+
+  private AesSivKey(
+      AesSivParameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public AesSivParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesSivKey)) {
+      return false;
+    }
+    AesSivKey that = (AesSivKey) o;
+    // Since outputPrefix is a function of parameters and the idRequirement, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKeyManager.java
index a26e490..f19dd37 100644
--- a/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivKeyManager.java
@@ -126,10 +126,7 @@
 
         byte[] pseudorandomness = new byte[KEY_SIZE_IN_BYTES];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != KEY_SIZE_IN_BYTES) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return AesSivKey.newBuilder()
               .setKeyValue(ByteString.copyFrom(pseudorandomness))
               .setVersion(getVersion())
@@ -160,13 +157,12 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesSivKeyManager(), newKeyAllowed);
+    AesSivProtoSerialization.register();
   }
 
   /**
    * @return a {@code KeyTemplate} that generates new instances of AES-SIV-CMAC keys.
-   * @deprecated use {@code KeyTemplates.get("AES256_SIV")}
    */
-  @Deprecated
   public static final KeyTemplate aes256SivTemplate() {
     return createKeyTemplate(KEY_SIZE_IN_BYTES, KeyTemplate.OutputPrefixType.TINK);
   }
@@ -174,9 +170,7 @@
   /**
    * @return A {@code KeyTemplate} that generates new instances of AES-SIV-CMAC keys. Keys generated
    *     from this template create ciphertexts compatible with other libraries.
-   * @deprecated use {@code KeyTemplates.get("AES256_SIV_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes256SivTemplate() {
     return createKeyTemplate(KEY_SIZE_IN_BYTES, KeyTemplate.OutputPrefixType.RAW);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/AesSivParameters.java b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivParameters.java
new file mode 100644
index 0000000..8eb97f6
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivParameters.java
@@ -0,0 +1,128 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Describes the parameters of an {@link AesSivSivKey} */
+public final class AesSivParameters extends DeterministicAeadParameters {
+  /** Enum-like class which describes how the prefix is computed. */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builds a new AesSivParameters instance. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    /** Accepts key sizes of 32, 48 or 64 bytes. */
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes != 32 && keySizeBytes != 48 && keySizeBytes != 64) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 32-byte, 48-byte and 64-byte AES-SIV keys are supported",
+                keySizeBytes));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public AesSivParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("Key size is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("Variant is not set");
+      }
+      return new AesSivParameters(keySizeBytes, variant);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final Variant variant;
+
+  private AesSivParameters(int keySizeBytes, Variant variant) {
+    this.keySizeBytes = keySizeBytes;
+    this.variant = variant;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesSivParameters)) {
+      return false;
+    }
+    AesSivParameters that = (AesSivParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes() && that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(AesSivParameters.class, keySizeBytes, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "AesSiv Parameters (variant: " + variant + ", " + keySizeBytes + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/AesSivProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivProtoSerialization.java
new file mode 100644
index 0000000..79560df
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/AesSivProtoSerialization.java
@@ -0,0 +1,216 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Methods to serialize and parse {@link AesSivKey} objects and {@link AesSivParameters} objects */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesSivProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesSivKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<AesSivParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesSivProtoSerialization::serializeParameters,
+              AesSivParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesSivProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesSivKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesSivProtoSerialization::serializeKey, AesSivKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesSivProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static Map<AesSivParameters.Variant, OutputPrefixType> createVariantToOutputPrefixMap() {
+    Map<AesSivParameters.Variant, OutputPrefixType> result = new HashMap<>();
+    result.put(AesSivParameters.Variant.NO_PREFIX, OutputPrefixType.RAW);
+    result.put(AesSivParameters.Variant.TINK, OutputPrefixType.TINK);
+    result.put(AesSivParameters.Variant.CRUNCHY, OutputPrefixType.CRUNCHY);
+    return Collections.unmodifiableMap(result);
+  }
+
+  private static Map<OutputPrefixType, AesSivParameters.Variant> createOutputPrefixToVariantMap() {
+    Map<OutputPrefixType, AesSivParameters.Variant> result = new EnumMap<>(OutputPrefixType.class);
+    result.put(OutputPrefixType.RAW, AesSivParameters.Variant.NO_PREFIX);
+    result.put(OutputPrefixType.TINK, AesSivParameters.Variant.TINK);
+    result.put(OutputPrefixType.CRUNCHY, AesSivParameters.Variant.CRUNCHY);
+    /** Parse LEGACY prefix to CRUNCHY, since they act the same for this type of key */
+    result.put(OutputPrefixType.LEGACY, AesSivParameters.Variant.CRUNCHY);
+    return Collections.unmodifiableMap(result);
+  }
+
+  // This map is constructed using Collections.unmodifiableMap
+  @SuppressWarnings("Immutable")
+  private static final Map<AesSivParameters.Variant, OutputPrefixType> variantsToOutputPrefixMap =
+      createVariantToOutputPrefixMap();
+
+  // This map is constructed using Collections.unmodifiableMap
+  @SuppressWarnings("Immutable")
+  private static final Map<OutputPrefixType, AesSivParameters.Variant> outputPrefixToVariantMap =
+      createOutputPrefixToVariantMap();
+
+  private static OutputPrefixType toProtoOutputPrefixType(AesSivParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (variantsToOutputPrefixMap.containsKey(variant)) {
+      return variantsToOutputPrefixMap.get(variant);
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static AesSivParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    if (outputPrefixToVariantMap.containsKey(outputPrefixType)) {
+      return outputPrefixToVariantMap.get(outputPrefixType);
+    }
+    throw new GeneralSecurityException(
+        "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+  }
+
+  private static ProtoParametersSerialization serializeParameters(AesSivParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder()
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(AesSivKey key, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesSivKey.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesSivParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesSivParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesSivKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesSivKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (format.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesSivParameters failed: ", e);
+    }
+    return AesSivParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException") // Prevents leaking key material
+  private static AesSivKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesSivParameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.AesSivKey protoKey =
+          com.google.crypto.tink.proto.AesSivKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      AesSivParameters parameters =
+          AesSivParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return AesSivKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesSivKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesSivProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/daead/BUILD.bazel
index f77915c..ccbd7b6 100644
--- a/java_src/src/main/java/com/google/crypto/tink/daead/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/BUILD.bazel
@@ -30,7 +30,6 @@
     deps = [
         ":deterministic_aead_wrapper",
         "//src/main/java/com/google/crypto/tink:deterministic_aead",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -41,7 +40,6 @@
     deps = [
         ":deterministic_aead_wrapper-android",
         "//src/main/java/com/google/crypto/tink:deterministic_aead-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -108,6 +106,7 @@
     name = "aes_siv_key_manager",
     srcs = ["AesSivKeyManager.java"],
     deps = [
+        ":aes_siv_proto_serialization",
         "//proto:aes_siv_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:deterministic_aead",
@@ -118,7 +117,32 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_siv",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "aes_siv_proto_serialization",
+    srcs = ["AesSivProtoSerialization.java"],
+    deps = [
+        ":aes_siv_key",
+        ":aes_siv_parameters",
+        "//proto:aes_siv_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -126,6 +150,7 @@
     name = "aes_siv_key_manager-android",
     srcs = ["AesSivKeyManager.java"],
     deps = [
+        ":aes_siv_proto_serialization-android",
         "//proto:aes_siv_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:deterministic_aead-android",
@@ -136,6 +161,139 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_siv-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "deterministic_aead_key",
+    srcs = ["DeterministicAeadKey.java"],
+    deps = [
+        ":deterministic_aead_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+    ],
+)
+
+android_library(
+    name = "deterministic_aead_key-android",
+    srcs = ["DeterministicAeadKey.java"],
+    deps = [
+        ":deterministic_aead_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+    ],
+)
+
+java_library(
+    name = "deterministic_aead_parameters",
+    srcs = ["DeterministicAeadParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "deterministic_aead_parameters-android",
+    srcs = ["DeterministicAeadParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_siv_parameters",
+    srcs = ["AesSivParameters.java"],
+    deps = [
+        ":deterministic_aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_siv_parameters-android",
+    srcs = ["AesSivParameters.java"],
+    deps = [
+        ":deterministic_aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_siv_key",
+    srcs = ["AesSivKey.java"],
+    deps = [
+        ":aes_siv_parameters",
+        ":deterministic_aead_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_siv_key-android",
+    srcs = ["AesSivKey.java"],
+    deps = [
+        ":aes_siv_parameters-android",
+        ":deterministic_aead_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_siv_proto_serialization-android",
+    srcs = ["AesSivProtoSerialization.java"],
+    deps = [
+        ":aes_siv_key-android",
+        ":aes_siv_parameters-android",
+        "//proto:aes_siv_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "predefined_deterministic_aead_parameters-android",
+    srcs = ["PredefinedDeterministicAeadParameters.java"],
+    deps = [
+        ":aes_siv_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+    ],
+)
+
+java_library(
+    name = "predefined_deterministic_aead_parameters",
+    srcs = ["PredefinedDeterministicAeadParameters.java"],
+    deps = [
+        ":aes_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
     ],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadConfig.java b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadConfig.java
index d9bb031..93b26df 100644
--- a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadConfig.java
@@ -40,14 +40,18 @@
 public final class DeterministicAeadConfig {
   public static final String AES_SIV_TYPE_URL = new AesSivKeyManager().getKeyType();
 
-  /** @deprecated use {@link #register} */
-  @Deprecated public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
+  /**
+   * @deprecated use {@link #register}
+   */
+  @Deprecated
+  public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
 
   /**
    * @deprecated use {@link #register}
    * @since 1.2.0
    */
-  @Deprecated public static final RegistryConfig LATEST = RegistryConfig.getDefaultInstance();
+  @Deprecated
+  public static final RegistryConfig LATEST = RegistryConfig.getDefaultInstance();
 
   static {
     try {
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadFactory.java b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadFactory.java
index 44cc71b..22e2c62 100644
--- a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadFactory.java
@@ -18,7 +18,6 @@
 
 import com.google.crypto.tink.DeterministicAead;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -46,7 +45,7 @@
   @Deprecated
   public static DeterministicAead getPrimitive(KeysetHandle keysetHandle)
       throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new DeterministicAeadWrapper());
+    DeterministicAeadWrapper.register();
     return keysetHandle.getPrimitive(DeterministicAead.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKey.java b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKey.java
new file mode 100644
index 0000000..1577933
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKey.java
@@ -0,0 +1,29 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.Bytes;
+
+/** Represents functions to encrypt and decrypt data deterministically using AEAD. */
+public abstract class DeterministicAeadKey extends Key {
+  /** Returns a {@link Bytes} instance which is prefixed to the ciphertext. */
+  public abstract Bytes getOutputPrefix();
+  /** Returns the parameters of this key. */
+  @Override
+  public abstract DeterministicAeadParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplates.java
index 3bdefc7..28f992b 100644
--- a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplates.java
@@ -21,9 +21,25 @@
 import com.google.crypto.tink.proto.OutputPrefixType;
 
 /**
- * Pre-generated {@code KeyTemplate} for {@code DeterministicAead} keys. One can use these templates
- * to generate new {@code Keyset} with {@code KeysetHandle}. To generate a new keyset that contains
- * a single {@code AesSivKey}, one can do:
+ * Pre-generated {@code KeyTemplate} for {@code DeterministicAead} keys.
+ *
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
+ * <p>One can use these templates to generate new {@code Keyset} with {@code KeysetHandle}. To
+ * generate a new keyset that contains a single {@code AesSivKey}, one can do:
  *
  * <pre>
  *   Config.register(DeterministicAeadConfig.TINK_1_1_0);
@@ -32,8 +48,7 @@
  * </pre>
  *
  * @since 1.1.0
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("AES256_SIV")
+ * @deprecated Use {@link PredefinedDeterministicAeadParameters} instead.
  */
 @Deprecated
 public final class DeterministicAeadKeyTemplates {
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadParameters.java
new file mode 100644
index 0000000..ef854a2
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadParameters.java
@@ -0,0 +1,24 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import com.google.crypto.tink.Parameters;
+import com.google.errorprone.annotations.Immutable;
+
+/** Represents a description of a {@link DeterministicAeadKey} */
+@Immutable
+public abstract class DeterministicAeadParameters extends Parameters {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadWrapper.java b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadWrapper.java
index 473b4a1..ae2d908 100644
--- a/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/DeterministicAeadWrapper.java
@@ -42,7 +42,9 @@
  */
 public class DeterministicAeadWrapper
     implements PrimitiveWrapper<DeterministicAead, DeterministicAead> {
+
   private static final Logger logger = Logger.getLogger(DeterministicAeadWrapper.class.getName());
+  private static final DeterministicAeadWrapper WRAPPER = new DeterministicAeadWrapper();
 
   private static class WrappedDeterministicAead implements DeterministicAead {
     private final PrimitiveSet<DeterministicAead> primitives;
@@ -138,6 +140,6 @@
   }
 
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new DeterministicAeadWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/daead/PredefinedDeterministicAeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/daead/PredefinedDeterministicAeadParameters.java
new file mode 100644
index 0000000..09b8b92
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/daead/PredefinedDeterministicAeadParameters.java
@@ -0,0 +1,37 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+/**
+ * Pre-generated {@link KeyTemplate} for {@link com.google.crypto.tink.DeterministicAead} keys.
+ *
+ * <p>Note: if you want to keep dependencies small, consider inlining the constants here.
+ */
+public final class PredefinedDeterministicAeadParameters {
+  /** A {@code KeyTemplate} that generates new instances of {@code AesSivKey} with a 64-byte key. */
+  public static final AesSivParameters AES256_SIV =
+      exceptionIsBug(
+          () ->
+              AesSivParameters.builder()
+                  .setKeySizeBytes(64)
+                  .setVariant(AesSivParameters.Variant.TINK)
+                  .build());
+
+  private PredefinedDeterministicAeadParameters() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/hybrid/BUILD.bazel
index d53394c..60c65ec 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/BUILD.bazel
@@ -21,6 +21,10 @@
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper",
         "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/internal:monitoring_util",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
     ],
 )
@@ -34,6 +38,10 @@
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper",
         "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/internal:monitoring_util",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
     ],
 )
 
@@ -52,7 +60,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_encrypt",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -73,7 +81,7 @@
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/hybrid/subtle:aead_or_daead",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_dem_helper",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -88,6 +96,7 @@
         "//proto:config_java_proto",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/hybrid/internal:hpke_private_key_manager",
     ],
 )
@@ -101,7 +110,7 @@
         "//proto:ecies_aead_hkdf_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -128,7 +137,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_decrypt",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -149,7 +158,6 @@
     deps = [
         ":hybrid_decrypt_wrapper",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -160,7 +168,6 @@
     deps = [
         ":hybrid_encrypt_wrapper",
         "//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -174,6 +181,116 @@
     ],
 )
 
+java_library(
+    name = "hybrid_parameters",
+    srcs = ["HybridParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hybrid_private_key",
+    srcs = ["HybridPrivateKey.java"],
+    deps = [
+        ":hybrid_parameters",
+        ":hybrid_public_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:private_key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hybrid_public_key",
+    srcs = ["HybridPublicKey.java"],
+    deps = [
+        ":hybrid_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hpke_parameters",
+    srcs = ["HpkeParameters.java"],
+    deps = [
+        ":hybrid_parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hpke_public_key",
+    srcs = ["HpkePublicKey.java"],
+    deps = [
+        ":hpke_parameters",
+        ":hybrid_public_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hpke_private_key",
+    srcs = ["HpkePrivateKey.java"],
+    deps = [
+        ":hpke_parameters",
+        ":hpke_public_key",
+        ":hybrid_private_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
+        "//src/main/java/com/google/crypto/tink/subtle:x25519",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hpke_proto_serialization",
+    srcs = ["HpkeProtoSerialization.java"],
+    deps = [
+        ":hpke_parameters",
+        ":hpke_private_key",
+        ":hpke_public_key",
+        "//proto:hpke_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
 # Android libraries
 
 android_library(
@@ -193,6 +310,10 @@
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
         "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:monitoring_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry-android",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client-android",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info-android",
         "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
     ],
 )
@@ -206,6 +327,10 @@
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
         "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:monitoring_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry-android",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client-android",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info-android",
     ],
 )
 
@@ -224,7 +349,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_encrypt-android",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -245,7 +370,7 @@
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config-android",
         "//src/main/java/com/google/crypto/tink/hybrid/subtle:aead_or_daead-android",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_dem_helper-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -260,6 +385,7 @@
         "//proto:config_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/aead:aead_config-android",
         "//src/main/java/com/google/crypto/tink/config:tink_fips-android",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config-android",
         "//src/main/java/com/google/crypto/tink/hybrid/internal:hpke_private_key_manager-android",
     ],
 )
@@ -273,7 +399,7 @@
         "//proto:ecies_aead_hkdf_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -300,7 +426,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_decrypt-android",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -321,7 +447,6 @@
     deps = [
         ":hybrid_decrypt_wrapper-android",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -332,7 +457,6 @@
     deps = [
         ":hybrid_encrypt_wrapper-android",
         "//src/main/java/com/google/crypto/tink:hybrid_encrypt-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -345,3 +469,113 @@
         "//src/main/java/com/google/crypto/tink:config-android",
     ],
 )
+
+android_library(
+    name = "hybrid_parameters-android",
+    srcs = ["HybridParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hybrid_private_key-android",
+    srcs = ["HybridPrivateKey.java"],
+    deps = [
+        ":hybrid_parameters-android",
+        ":hybrid_public_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:private_key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hybrid_public_key-android",
+    srcs = ["HybridPublicKey.java"],
+    deps = [
+        ":hybrid_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hpke_parameters-android",
+    srcs = ["HpkeParameters.java"],
+    deps = [
+        ":hybrid_parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hpke_public_key-android",
+    srcs = ["HpkePublicKey.java"],
+    deps = [
+        ":hpke_parameters-android",
+        ":hybrid_public_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hpke_private_key-android",
+    srcs = ["HpkePrivateKey.java"],
+    deps = [
+        ":hpke_parameters-android",
+        ":hpke_public_key-android",
+        ":hybrid_private_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
+        "//src/main/java/com/google/crypto/tink/subtle:x25519-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hpke_proto_serialization-android",
+    srcs = ["HpkeProtoSerialization.java"],
+    deps = [
+        ":hpke_parameters-android",
+        ":hpke_private_key-android",
+        ":hpke_public_key-android",
+        "//proto:hpke_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManager.java
index a618e6b..d5cca83 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManager.java
@@ -281,9 +281,7 @@
    *     <p>Unlike other key templates that use AES-GCM, the instances of {@link HybridDecrypt}
    *     generated by this key template has no limitation on Android KitKat (API level 19). They
    *     might not work in older versions though.
-   * @deprecated use {@code KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM")}
    */
-  @Deprecated
   public static final KeyTemplate eciesP256HkdfHmacSha256Aes128GcmTemplate() {
     return createKeyTemplate(
         EllipticCurveType.NIST_P256,
@@ -307,10 +305,7 @@
    *     <p>Unlike other key templates that use AES-GCM, the instances of {@link HybridDecrypt}
    *     generated by this key template has no limitation on Android KitKat (API level 19). They
    *     might not work in older versions though.
-   * @deprecated use {@code
-   *     KeyTemplates.get("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawEciesP256HkdfHmacSha256Aes128GcmCompressedTemplate() {
     return createKeyTemplate(
         EllipticCurveType.NIST_P256,
@@ -337,10 +332,7 @@
    *       <li>EC Point Format: Uncompressed
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256")}
    */
-  @Deprecated
   public static final KeyTemplate eciesP256HkdfHmacSha256Aes128CtrHmacSha256Template() {
     return createKeyTemplate(
         EllipticCurveType.NIST_P256,
@@ -367,11 +359,7 @@
    *       <li>EC Point Format: Compressed
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW} (no prefix)
    *     </ul>
-   *
-   * @deprecated use {@code
-   *     KeyTemplates.get("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate
       rawEciesP256HkdfHmacSha256Aes128CtrHmacSha256CompressedTemplate() {
     return createKeyTemplate(
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkeParameters.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkeParameters.java
new file mode 100644
index 0000000..3a28714
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkeParameters.java
@@ -0,0 +1,223 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+
+/** Description of the parameters for an {@link HpkePublicKey} or {@link HpkePrivateKey}. */
+public final class HpkeParameters extends HybridParameters {
+  /** Description of the output prefix prepended to the ciphertext. */
+  @Immutable
+  public static final class Variant {
+    /** {@code TINK}: Leading 0x01-byte followed by 4-byte key id (big endian format). */
+    public static final Variant TINK = new Variant("TINK");
+    /** {@code CRUNCHY}: Leading 0x00-byte followed by 4-byte key id (big endian format). */
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    /** {@code NO_PREFIX}: Empty prefix. */
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /**
+   * HPKE algorithm identifiers.
+   *
+   * <p>See https://www.rfc-editor.org/rfc/rfc9180.html#section-7.
+   */
+  @Immutable
+  private static class AlgorithmIdentifier {
+    protected final String name;
+    protected final int value;
+
+    private AlgorithmIdentifier(String name, int value) {
+      this.name = name;
+      this.value = value;
+    }
+
+    public int getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("%s(0x%04x)", name, value);
+    }
+  }
+
+  /**
+   * HPKE KEM identifiers.
+   *
+   * <p>See https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.
+   */
+  @Immutable
+  public static final class KemId extends AlgorithmIdentifier {
+    public static final KemId DHKEM_P256_HKDF_SHA256 = new KemId("DHKEM_P256_HKDF_SHA256", 0x10);
+    public static final KemId DHKEM_P384_HKDF_SHA384 = new KemId("DHKEM_P384_HKDF_SHA384", 0x11);
+    public static final KemId DHKEM_P521_HKDF_SHA512 = new KemId("DHKEM_P521_HKDF_SHA512", 0x12);
+    public static final KemId DHKEM_X25519_HKDF_SHA256 =
+        new KemId("DHKEM_X25519_HKDF_SHA256", 0x20);
+
+    private KemId(String name, int value) {
+      super(name, value);
+    }
+  }
+
+  /**
+   * HPKE KDF identifiers.
+   *
+   * <p>See https://www.rfc-editor.org/rfc/rfc9180.html#section-7.2.
+   */
+  @Immutable
+  public static final class KdfId extends AlgorithmIdentifier {
+    public static final KdfId HKDF_SHA256 = new KdfId("HKDF_SHA256", 0x01);
+    public static final KdfId HKDF_SHA384 = new KdfId("HKDF_SHA384", 0x02);
+    public static final KdfId HKDF_SHA512 = new KdfId("HKDF_SHA512", 0x03);
+
+    private KdfId(String name, int value) {
+      super(name, value);
+    }
+  }
+
+  /**
+   * HPKE AEAD identifiers.
+   *
+   * <p>See https://www.rfc-editor.org/rfc/rfc9180.html#section-7.3.
+   */
+  @Immutable
+  public static final class AeadId extends AlgorithmIdentifier {
+    public static final AeadId AES_128_GCM = new AeadId("AES_128_GCM", 0x01);
+    public static final AeadId AES_256_GCM = new AeadId("AES_256_GCM", 0x02);
+    public static final AeadId CHACHA20_POLY1305 = new AeadId("CHACHA20_POLY1305", 0x03);
+
+    private AeadId(String name, int value) {
+      super(name, value);
+    }
+  }
+
+  /** Builds a new {@link HpkeParameters} instance. */
+  public static final class Builder {
+    private KemId kem = null;
+    private KdfId kdf = null;
+    private AeadId aead = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setKemId(KemId kem) {
+      this.kem = kem;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKdfId(KdfId kdf) {
+      this.kdf = kdf;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setAeadId(AeadId aead) {
+      this.aead = aead;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public HpkeParameters build() throws GeneralSecurityException {
+      if (kem == null) {
+        throw new GeneralSecurityException("HPKE KEM parameter is not set");
+      }
+      if (kdf == null) {
+        throw new GeneralSecurityException("HPKE KDF parameter is not set");
+      }
+      if (aead == null) {
+        throw new GeneralSecurityException("HPKE AEAD parameter is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("HPKE variant is not set");
+      }
+      return new HpkeParameters(kem, kdf, aead, variant);
+    }
+  }
+
+  private final KemId kem;
+  private final KdfId kdf;
+  private final AeadId aead;
+  private final Variant variant;
+
+  private HpkeParameters(KemId kem, KdfId kdf, AeadId aead, Variant variant) {
+    this.kem = kem;
+    this.kdf = kdf;
+    this.aead = aead;
+    this.variant = variant;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public KemId getKemId() {
+    return kem;
+  }
+
+  public KdfId getKdfId() {
+    return kdf;
+  }
+
+  public AeadId getAeadId() {
+    return aead;
+  }
+
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof HpkeParameters)) {
+      return false;
+    }
+    HpkeParameters other = (HpkeParameters) o;
+    return kem == other.kem && kdf == other.kdf && aead == other.aead && variant == other.variant;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(HpkeParameters.class, kem, kdf, aead, variant);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkePrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkePrivateKey.java
new file mode 100644
index 0000000..06f49ef
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkePrivateKey.java
@@ -0,0 +1,191 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.subtle.EllipticCurves;
+import com.google.crypto.tink.subtle.X25519;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.util.Arrays;
+
+/** Representation of the decryption function for an HPKE hybrid encryption primitive. */
+@Alpha
+@Immutable
+public final class HpkePrivateKey extends HybridPrivateKey {
+  private final HpkePublicKey publicKey;
+  private final SecretBytes privateKeyBytes;
+
+  private HpkePrivateKey(HpkePublicKey publicKey, SecretBytes privateKeyBytes) {
+    this.publicKey = publicKey;
+    this.privateKeyBytes = privateKeyBytes;
+  }
+
+  private static void validatePrivateKeyByteLength(
+      HpkeParameters.KemId kemId, SecretBytes privateKeyBytes) throws GeneralSecurityException {
+    // Key lengths from 'Nsk' column in https://www.rfc-editor.org/rfc/rfc9180.html#table-2.
+    int keyLengthInBytes = privateKeyBytes.size();
+    String parameterizedErrorMessage =
+        "Encoded private key byte length for " + kemId + " must be %d, not " + keyLengthInBytes;
+    if (kemId == HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256) {
+      if (keyLengthInBytes != 32) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 32));
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384) {
+      if (keyLengthInBytes != 48) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 48));
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512) {
+      if (keyLengthInBytes != 66) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 66));
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256) {
+      if (keyLengthInBytes != 32) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 32));
+      }
+      return;
+    }
+    throw new GeneralSecurityException("Unable to validate private key length for " + kemId);
+  }
+
+  private static boolean isNistKem(HpkeParameters.KemId kemId) {
+    return kemId == HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256
+        || kemId == HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384
+        || kemId == HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512;
+  }
+
+  private static ECParameterSpec getNistCurveParams(HpkeParameters.KemId kemId) {
+    if (kemId == HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256) {
+      return EllipticCurves.getNistP256Params();
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384) {
+      return EllipticCurves.getNistP384Params();
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512) {
+      return EllipticCurves.getNistP521Params();
+    }
+    throw new IllegalArgumentException("Unable to determine NIST curve params for " + kemId);
+  }
+
+  /**
+   * Confirms that the private key encoded as {@code privateKeyBytes} corresponds to the public key
+   * encoded as {@code publicKeyBytes} given the HPKE {@code kemId}.
+   */
+  private static void validateKeyPair(
+      HpkeParameters.KemId kemId, byte[] publicKeyBytes, byte[] privateKeyBytes)
+      throws GeneralSecurityException {
+    if (isNistKem(kemId)) {
+      ECParameterSpec spec = getNistCurveParams(kemId);
+      BigInteger order = spec.getOrder();
+      BigInteger privateKey = BigIntegerEncoding.fromUnsignedBigEndianBytes(privateKeyBytes);
+      if ((privateKey.signum() <= 0) || (privateKey.compareTo(order) >= 0)) {
+        throw new GeneralSecurityException("Invalid private key.");
+      }
+      ECPoint expectedPoint = EllipticCurvesUtil.multiplyByGenerator(privateKey, spec);
+      ECPoint publicPoint =
+          EllipticCurves.pointDecode(
+              spec.getCurve(), EllipticCurves.PointFormatType.UNCOMPRESSED, publicKeyBytes);
+      if (!expectedPoint.equals(publicPoint)) {
+        throw new GeneralSecurityException("Invalid private key for public key.");
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256) {
+      byte[] expectedPublicKeyBytes = X25519.publicFromPrivate(privateKeyBytes);
+      if (!Arrays.equals(expectedPublicKeyBytes, publicKeyBytes)) {
+        throw new GeneralSecurityException("Invalid private key for public key.");
+      }
+      return;
+    }
+    throw new IllegalArgumentException("Unable to validate key pair for " + kemId);
+  }
+
+  /**
+   * Creates a new HPKE private key.
+   *
+   * @param publicKey Corresponding HPKE public key for this private key
+   * @param privateKeyBytes Private key encoded according to
+   *     https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.2
+   */
+  @AccessesPartialKey
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static HpkePrivateKey create(HpkePublicKey publicKey, SecretBytes privateKeyBytes)
+      throws GeneralSecurityException {
+    if (publicKey == null) {
+      throw new GeneralSecurityException(
+          "HPKE private key cannot be constructed without an HPKE public key");
+    }
+    if (privateKeyBytes == null) {
+      throw new GeneralSecurityException("HPKE private key cannot be constructed without secret");
+    }
+    validatePrivateKeyByteLength(publicKey.getParameters().getKemId(), privateKeyBytes);
+    validateKeyPair(
+        publicKey.getParameters().getKemId(),
+        publicKey.getPublicKeyBytes().toByteArray(),
+        privateKeyBytes.toByteArray(InsecureSecretKeyAccess.get()));
+    return new HpkePrivateKey(publicKey, privateKeyBytes);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getPrivateKeyBytes() {
+    return privateKeyBytes;
+  }
+
+  @Override
+  public HpkeParameters getParameters() {
+    return publicKey.getParameters();
+  }
+
+  @Override
+  public HpkePublicKey getPublicKey() {
+    return publicKey;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof HpkePrivateKey)) {
+      return false;
+    }
+    HpkePrivateKey other = (HpkePrivateKey) o;
+    return publicKey.equalsKey(other.publicKey)
+        && privateKeyBytes.equalsSecretBytes(other.privateKeyBytes);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkeProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkeProtoSerialization.java
new file mode 100644
index 0000000..5e570a9
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkeProtoSerialization.java
@@ -0,0 +1,308 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.EnumTypeProtoConverter;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HpkeAead;
+import com.google.crypto.tink.proto.HpkeKdf;
+import com.google.crypto.tink.proto.HpkeKem;
+import com.google.crypto.tink.proto.HpkeKeyFormat;
+import com.google.crypto.tink.proto.HpkeParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link HpkePrivateKey}, {@link HpkePublicKey}, and {@link
+ * HpkeParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class HpkeProtoSerialization {
+  private static final int VERSION = 0;
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.HpkePrivateKey";
+  private static final Bytes PRIVATE_TYPE_URL_BYTES = toBytesFromPrintableAscii(PRIVATE_TYPE_URL);
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.HpkePublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
+  private static final ParametersSerializer<HpkeParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              HpkeProtoSerialization::serializeParameters,
+              HpkeParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          HpkeProtoSerialization::parseParameters,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<HpkePublicKey, ProtoKeySerialization> PUBLIC_KEY_SERIALIZER =
+      KeySerializer.create(
+          HpkeProtoSerialization::serializePublicKey,
+          HpkePublicKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          HpkeProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final KeySerializer<HpkePrivateKey, ProtoKeySerialization> PRIVATE_KEY_SERIALIZER =
+      KeySerializer.create(
+          HpkeProtoSerialization::serializePrivateKey,
+          HpkePrivateKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PRIVATE_KEY_PARSER =
+      KeyParser.create(
+          HpkeProtoSerialization::parsePrivateKey,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final EnumTypeProtoConverter<OutputPrefixType, HpkeParameters.Variant>
+      VARIANT_TYPE_CONVERTER =
+          EnumTypeProtoConverter.<OutputPrefixType, HpkeParameters.Variant>builder()
+              .add(OutputPrefixType.RAW, HpkeParameters.Variant.NO_PREFIX)
+              .add(OutputPrefixType.TINK, HpkeParameters.Variant.TINK)
+              .add(OutputPrefixType.CRUNCHY, HpkeParameters.Variant.CRUNCHY)
+              .build();
+
+  private static final EnumTypeProtoConverter<HpkeKem, HpkeParameters.KemId> KEM_TYPE_CONVERTER =
+      EnumTypeProtoConverter.<HpkeKem, HpkeParameters.KemId>builder()
+          .add(HpkeKem.DHKEM_P256_HKDF_SHA256, HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+          .add(HpkeKem.DHKEM_P384_HKDF_SHA384, HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384)
+          .add(HpkeKem.DHKEM_P521_HKDF_SHA512, HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512)
+          .add(HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+          .build();
+
+  private static final EnumTypeProtoConverter<HpkeKdf, HpkeParameters.KdfId> KDF_TYPE_CONVERTER =
+      EnumTypeProtoConverter.<HpkeKdf, HpkeParameters.KdfId>builder()
+          .add(HpkeKdf.HKDF_SHA256, HpkeParameters.KdfId.HKDF_SHA256)
+          .add(HpkeKdf.HKDF_SHA384, HpkeParameters.KdfId.HKDF_SHA384)
+          .add(HpkeKdf.HKDF_SHA512, HpkeParameters.KdfId.HKDF_SHA512)
+          .build();
+
+  private static final EnumTypeProtoConverter<HpkeAead, HpkeParameters.AeadId> AEAD_TYPE_CONVERTER =
+      EnumTypeProtoConverter.<HpkeAead, HpkeParameters.AeadId>builder()
+          .add(HpkeAead.AES_128_GCM, HpkeParameters.AeadId.AES_128_GCM)
+          .add(HpkeAead.AES_256_GCM, HpkeParameters.AeadId.AES_256_GCM)
+          .add(HpkeAead.CHACHA20_POLY1305, HpkeParameters.AeadId.CHACHA20_POLY1305)
+          .build();
+
+  /**
+   * Registers previously defined parser/serializer objects into a global, mutable registry.
+   * Registration is public to enable custom configurations.
+   */
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  /** Registers previously defined parser/serializer objects into a given registry. */
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
+    registry.registerKeySerializer(PRIVATE_KEY_SERIALIZER);
+    registry.registerKeyParser(PRIVATE_KEY_PARSER);
+  }
+
+  private static com.google.crypto.tink.proto.HpkeParams toProtoParameters(HpkeParameters params)
+      throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.HpkeParams.newBuilder()
+        .setKem(KEM_TYPE_CONVERTER.toProtoEnum(params.getKemId()))
+        .setKdf(KDF_TYPE_CONVERTER.toProtoEnum(params.getKdfId()))
+        .setAead(AEAD_TYPE_CONVERTER.toProtoEnum(params.getAeadId()))
+        .build();
+  }
+
+  private static com.google.crypto.tink.proto.HpkePublicKey toProtoPublicKey(HpkePublicKey key)
+      throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.HpkePublicKey.newBuilder()
+        .setVersion(VERSION)
+        .setParams(toProtoParameters(key.getParameters()))
+        .setPublicKey(ByteString.copyFrom(key.getPublicKeyBytes().toByteArray()))
+        .build();
+  }
+
+  private static com.google.crypto.tink.proto.HpkePrivateKey toProtoPrivateKey(
+      HpkePrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.HpkePrivateKey.newBuilder()
+        .setVersion(VERSION)
+        .setPublicKey(toProtoPublicKey(key.getPublicKey()))
+        .setPrivateKey(
+            ByteString.copyFrom(
+                key.getPrivateKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+        .build();
+  }
+
+  private static HpkeParameters fromProtoParameters(
+      OutputPrefixType outputPrefixType, HpkeParams protoParams) throws GeneralSecurityException {
+    return HpkeParameters.builder()
+        .setVariant(VARIANT_TYPE_CONVERTER.fromProtoEnum(outputPrefixType))
+        .setKemId(KEM_TYPE_CONVERTER.fromProtoEnum(protoParams.getKem()))
+        .setKdfId(KDF_TYPE_CONVERTER.fromProtoEnum(protoParams.getKdf()))
+        .setAeadId(AEAD_TYPE_CONVERTER.fromProtoEnum(protoParams.getAead()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(HpkeParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(PRIVATE_TYPE_URL)
+            .setValue(
+                HpkeKeyFormat.newBuilder()
+                    .setParams(toProtoParameters(parameters))
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(VARIANT_TYPE_CONVERTER.toProtoEnum(parameters.getVariant()))
+            .build());
+  }
+
+  /**
+   * Returns the proto serialization of a {@link HpkePublicKey}.
+   *
+   * @param access may be null for public key material
+   * @throws GeneralSecurityException if the key cannot be serialized (e.g. unknown variant)
+   */
+  private static ProtoKeySerialization serializePublicKey(
+      HpkePublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        toProtoPublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        VARIANT_TYPE_CONVERTER.toProtoEnum(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static ProtoKeySerialization serializePrivateKey(
+      HpkePrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PRIVATE_TYPE_URL,
+        toProtoPrivateKey(key, access).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PRIVATE,
+        VARIANT_TYPE_CONVERTER.toProtoEnum(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static HpkeParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HpkeProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    HpkeKeyFormat format;
+    try {
+      format =
+          HpkeKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HpkeParameters failed: ", e);
+    }
+    return fromProtoParameters(
+        serialization.getKeyTemplate().getOutputPrefixType(), format.getParams());
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static HpkePublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HpkeProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.HpkePublicKey protoKey =
+          com.google.crypto.tink.proto.HpkePublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != VERSION) {
+        throw new GeneralSecurityException("Only version " + VERSION + " keys are accepted");
+      }
+
+      HpkeParameters params =
+          fromProtoParameters(serialization.getOutputPrefixType(), protoKey.getParams());
+      return HpkePublicKey.create(
+          params,
+          Bytes.copyFrom(protoKey.getPublicKey().toByteArray()),
+          serialization.getIdRequirementOrNull());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HpkePublicKey failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException") // Prevents leaking key material
+  private static HpkePrivateKey parsePrivateKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HpkeProtoSerialization.parsePrivateKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.HpkePrivateKey protoKey =
+          com.google.crypto.tink.proto.HpkePrivateKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != VERSION) {
+        throw new GeneralSecurityException("Only version " + VERSION + " keys are accepted");
+      }
+      com.google.crypto.tink.proto.HpkePublicKey protoPublicKey = protoKey.getPublicKey();
+      HpkeParameters params =
+          fromProtoParameters(serialization.getOutputPrefixType(), protoPublicKey.getParams());
+      HpkePublicKey publicKey =
+          HpkePublicKey.create(
+              params,
+              Bytes.copyFrom(protoPublicKey.getPublicKey().toByteArray()),
+              serialization.getIdRequirementOrNull());
+      return HpkePrivateKey.create(
+          publicKey,
+          SecretBytes.copyFrom(
+              protoKey.getPrivateKey().toByteArray(), SecretKeyAccess.requireAccess(access)));
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HpkePrivateKey failed");
+    }
+  }
+
+  private HpkeProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkePublicKey.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkePublicKey.java
new file mode 100644
index 0000000..bf602bb
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HpkePublicKey.java
@@ -0,0 +1,219 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.subtle.EllipticCurves;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Representation of the encryption function for an HPKE hybrid encryption primitive. */
+@Alpha
+@Immutable
+public final class HpkePublicKey extends HybridPublicKey {
+  private final HpkeParameters parameters;
+  private final Bytes publicKeyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  private HpkePublicKey(
+      HpkeParameters parameters,
+      Bytes publicKeyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.publicKeyBytes = publicKeyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  private static void validateIdRequirement(
+      HpkeParameters.Variant variant, @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+    if (!variant.equals(HpkeParameters.Variant.NO_PREFIX) && idRequirement == null) {
+      throw new GeneralSecurityException(
+          "'idRequirement' must be non-null for " + variant + " variant.");
+    }
+    if (variant.equals(HpkeParameters.Variant.NO_PREFIX) && idRequirement != null) {
+      throw new GeneralSecurityException("'idRequirement' must be null for NO_PREFIX variant.");
+    }
+  }
+
+  private static void validatePublicKeyByteLength(HpkeParameters.KemId kemId, Bytes publicKeyBytes)
+      throws GeneralSecurityException {
+    // Key lengths from 'Npk' column in https://www.rfc-editor.org/rfc/rfc9180.html#table-2.
+    int keyLengthInBytes = publicKeyBytes.size();
+    String parameterizedErrorMessage =
+        "Encoded public key byte length for " + kemId + " must be %d, not " + keyLengthInBytes;
+    if (kemId == HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256) {
+      if (keyLengthInBytes != 65) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 65));
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384) {
+      if (keyLengthInBytes != 97) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 97));
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512) {
+      if (keyLengthInBytes != 133) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 133));
+      }
+      return;
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256) {
+      if (keyLengthInBytes != 32) {
+        throw new GeneralSecurityException(String.format(parameterizedErrorMessage, 32));
+      }
+      return;
+    }
+    throw new GeneralSecurityException("Unable to validate public key length for " + kemId);
+  }
+
+  private static boolean isNistKem(HpkeParameters.KemId kemId) {
+    return kemId == HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256
+        || kemId == HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384
+        || kemId == HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512;
+  }
+
+  private static EllipticCurve getNistCurve(HpkeParameters.KemId kemId) {
+    if (kemId == HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256) {
+      return EllipticCurves.getNistP256Params().getCurve();
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384) {
+      return EllipticCurves.getNistP384Params().getCurve();
+    }
+    if (kemId == HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512) {
+      return EllipticCurves.getNistP521Params().getCurve();
+    }
+    throw new IllegalArgumentException("Unable to determine NIST curve type for " + kemId);
+  }
+
+  private static void validatePublicKeyOnCurve(HpkeParameters.KemId kemId, Bytes publicKeyBytes)
+      throws GeneralSecurityException {
+    if (!isNistKem(kemId)) {
+      return;
+    }
+    EllipticCurve curve = getNistCurve(kemId);
+    ECPoint point =
+        EllipticCurves.pointDecode(
+            curve, EllipticCurves.PointFormatType.UNCOMPRESSED, publicKeyBytes.toByteArray());
+    EllipticCurvesUtil.checkPointOnCurve(point, curve);
+  }
+
+  /**
+   * Validate public key according to https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.4.
+   *
+   * <p>Specifically, validate public key lengths and NIST KEM public key values according to
+   * Section 5.6.2.3.4 of https://doi.org/10.6028/nist.sp.800-56ar3.
+   */
+  private static void validatePublicKey(HpkeParameters.KemId kemId, Bytes publicKeyBytes)
+      throws GeneralSecurityException {
+    validatePublicKeyByteLength(kemId, publicKeyBytes);
+    validatePublicKeyOnCurve(kemId, publicKeyBytes);
+  }
+
+  private static Bytes createOutputPrefix(
+      HpkeParameters.Variant variant, @Nullable Integer idRequirement) {
+    if (variant == HpkeParameters.Variant.NO_PREFIX) {
+      return Bytes.copyFrom(new byte[] {});
+    }
+    if (idRequirement == null) {
+      throw new IllegalStateException(
+          "idRequirement must be non-null for HpkeParameters.Variant " + variant);
+    }
+    if (variant == HpkeParameters.Variant.CRUNCHY) {
+      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+    }
+    if (variant == HpkeParameters.Variant.TINK) {
+      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+    }
+    throw new IllegalStateException("Unknown HpkeParameters.Variant: " + variant);
+  }
+
+  /**
+   * Creates a new HPKE public key.
+   *
+   * @param parameters HPKE parameters for the public key
+   * @param publicKeyBytes Public key encoded according to
+   *     https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.1
+   * @param idRequirement Key id requirement, which must be null for {@code NO_PREFIX} variant and
+   *     non-null for all other variants
+   */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static HpkePublicKey create(
+      HpkeParameters parameters, Bytes publicKeyBytes, @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+    validateIdRequirement(parameters.getVariant(), idRequirement);
+    validatePublicKey(parameters.getKemId(), publicKeyBytes);
+    Bytes prefix = createOutputPrefix(parameters.getVariant(), idRequirement);
+    return new HpkePublicKey(parameters, publicKeyBytes, prefix, idRequirement);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public Bytes getPublicKeyBytes() {
+    return publicKeyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public HpkeParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof HpkePublicKey)) {
+      return false;
+    }
+    HpkePublicKey other = (HpkePublicKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return parameters.equals(other.parameters)
+        && publicKeyBytes.equals(other.publicKeyBytes)
+        && Objects.equals(idRequirement, other.idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridConfig.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridConfig.java
index 3bfdf13..0ad6b16 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridConfig.java
@@ -18,6 +18,7 @@
 
 import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.config.TinkFips;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
 import com.google.crypto.tink.hybrid.internal.HpkePrivateKeyManager;
 import com.google.crypto.tink.proto.RegistryConfig;
 import java.security.GeneralSecurityException;
@@ -45,19 +46,24 @@
   public static final String ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE_URL =
       new EciesAeadHkdfPrivateKeyManager().getKeyType();
 
-  /** @deprecated */
-  @Deprecated public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
+  /**
+   * @deprecated
+   */
+  @Deprecated
+  public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
   /**
    * @deprecated
    * @since 1.1.0
    */
-  @Deprecated public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
+  @Deprecated
+  public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
 
   /**
    * @deprecated
    * @since 1.2.0
    */
-  @Deprecated public static final RegistryConfig LATEST = RegistryConfig.getDefaultInstance();
+  @Deprecated
+  public static final RegistryConfig LATEST = RegistryConfig.getDefaultInstance();
 
   static {
     try {
@@ -99,6 +105,7 @@
     HybridEncryptWrapper.register();
 
     AeadConfig.register();
+    DeterministicAeadConfig.register();
 
     if (TinkFips.useOnlyFips()) {
       // If Tink is built in FIPS-mode do not register algorithms which are not compatible.
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptFactory.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptFactory.java
index 76278f5..baac3e9 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptFactory.java
@@ -17,7 +17,6 @@
 
 import com.google.crypto.tink.HybridDecrypt;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -45,7 +44,7 @@
   @Deprecated
   public static HybridDecrypt getPrimitive(KeysetHandle keysetHandle)
       throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new HybridDecryptWrapper());
+    HybridDecryptWrapper.register();
     return keysetHandle.getPrimitive(HybridDecrypt.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptWrapper.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptWrapper.java
index cd71e48..28507f4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridDecryptWrapper.java
@@ -20,6 +20,10 @@
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.PrimitiveWrapper;
 import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.internal.MonitoringUtil;
+import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.monitoring.MonitoringClient;
+import com.google.crypto.tink.monitoring.MonitoringKeysetInfo;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
 import java.util.List;
@@ -34,13 +38,24 @@
  * com.google.crypto.tink.proto.OutputPrefixType#RAW}.
  */
 public class HybridDecryptWrapper implements PrimitiveWrapper<HybridDecrypt, HybridDecrypt> {
+
   private static final Logger logger = Logger.getLogger(HybridDecryptWrapper.class.getName());
+  private static final HybridDecryptWrapper WRAPPER = new HybridDecryptWrapper();
 
   private static class WrappedHybridDecrypt implements HybridDecrypt {
     private final PrimitiveSet<HybridDecrypt> primitives;
 
+    private final MonitoringClient.Logger decLogger;
+
     public WrappedHybridDecrypt(final PrimitiveSet<HybridDecrypt> primitives) {
       this.primitives = primitives;
+      if (primitives.hasAnnotations()) {
+        MonitoringClient client = MutableMonitoringRegistry.globalInstance().getMonitoringClient();
+        MonitoringKeysetInfo keysetInfo = MonitoringUtil.getMonitoringKeysetInfo(primitives);
+        this.decLogger = client.createLogger(keysetInfo, "hybrid_decrypt", "decrypt");
+      } else {
+        this.decLogger = MonitoringUtil.DO_NOTHING_LOGGER;
+      }
     }
 
     @Override
@@ -53,7 +68,9 @@
         List<PrimitiveSet.Entry<HybridDecrypt>> entries = primitives.getPrimitive(prefix);
         for (PrimitiveSet.Entry<HybridDecrypt> entry : entries) {
           try {
-            return entry.getPrimitive().decrypt(ciphertextNoPrefix, contextInfo);
+            byte[] output = entry.getPrimitive().decrypt(ciphertextNoPrefix, contextInfo);
+            decLogger.log(entry.getKeyId(), ciphertextNoPrefix.length);
+            return output;
           } catch (GeneralSecurityException e) {
             logger.info("ciphertext prefix matches a key, but cannot decrypt: " + e.toString());
             continue;
@@ -64,12 +81,15 @@
       List<PrimitiveSet.Entry<HybridDecrypt>> entries = primitives.getRawPrimitives();
       for (PrimitiveSet.Entry<HybridDecrypt> entry : entries) {
         try {
-          return entry.getPrimitive().decrypt(ciphertext, contextInfo);
+          byte[] output = entry.getPrimitive().decrypt(ciphertext, contextInfo);
+          decLogger.log(entry.getKeyId(), ciphertext.length);
+          return output;
         } catch (GeneralSecurityException e) {
           continue;
         }
       }
       // nothing works.
+      decLogger.logFailure();
       throw new GeneralSecurityException("decryption failed");
     }
   }
@@ -98,6 +118,6 @@
    * argument.
    */
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new HybridDecryptWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptFactory.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptFactory.java
index 45e72b7..2b7623c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptFactory.java
@@ -17,7 +17,6 @@
 
 import com.google.crypto.tink.HybridEncrypt;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -46,7 +45,7 @@
   @Deprecated
   public static HybridEncrypt getPrimitive(KeysetHandle keysetHandle)
       throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new HybridEncryptWrapper());
+    HybridEncryptWrapper.register();
     return keysetHandle.getPrimitive(HybridEncrypt.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptWrapper.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptWrapper.java
index a3a7377..8cf86cd 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridEncryptWrapper.java
@@ -19,6 +19,10 @@
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.PrimitiveWrapper;
 import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.internal.MonitoringUtil;
+import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.monitoring.MonitoringClient;
+import com.google.crypto.tink.monitoring.MonitoringKeysetInfo;
 import com.google.crypto.tink.subtle.Bytes;
 import java.security.GeneralSecurityException;
 
@@ -30,22 +34,43 @@
  * with the primary key.
  */
 public class HybridEncryptWrapper implements PrimitiveWrapper<HybridEncrypt, HybridEncrypt> {
+
+  private static final HybridEncryptWrapper WRAPPER = new HybridEncryptWrapper();
+
   private static class WrappedHybridEncrypt implements HybridEncrypt {
     final PrimitiveSet<HybridEncrypt> primitives;
 
+    private final MonitoringClient.Logger encLogger;
+
     public WrappedHybridEncrypt(final PrimitiveSet<HybridEncrypt> primitives) {
       this.primitives = primitives;
+      if (primitives.hasAnnotations()) {
+        MonitoringClient client = MutableMonitoringRegistry.globalInstance().getMonitoringClient();
+        MonitoringKeysetInfo keysetInfo = MonitoringUtil.getMonitoringKeysetInfo(primitives);
+        this.encLogger = client.createLogger(keysetInfo, "hybrid_encrypt", "encrypt");
+      } else {
+        this.encLogger = MonitoringUtil.DO_NOTHING_LOGGER;
+      }
     }
 
     @Override
     public byte[] encrypt(final byte[] plaintext, final byte[] contextInfo)
         throws GeneralSecurityException {
       if (primitives.getPrimary() == null) {
+        encLogger.logFailure();
         throw new GeneralSecurityException("keyset without primary key");
       }
-      return Bytes.concat(
-          primitives.getPrimary().getIdentifier(),
-          primitives.getPrimary().getPrimitive().encrypt(plaintext, contextInfo));
+      try {
+        byte[] output =
+            Bytes.concat(
+                primitives.getPrimary().getIdentifier(),
+                primitives.getPrimary().getPrimitive().encrypt(plaintext, contextInfo));
+        encLogger.log(primitives.getPrimary().getKeyId(), plaintext.length);
+        return output;
+      } catch (GeneralSecurityException e) {
+        encLogger.logFailure();
+        throw e;
+      }
     }
   }
 
@@ -73,6 +98,6 @@
    * argument.
    */
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new HybridEncryptWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridKeyTemplates.java
index 9bfa542..974ba6b 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridKeyTemplates.java
@@ -31,6 +31,21 @@
 /**
  * Pre-generated {@link KeyTemplate} for {@link HybridDecrypt} and {@link HybridEncrypt} primitives.
  *
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
  * <p>One can use these templates to generate new {@link com.google.crypto.tink.proto.Keyset} with
  * {@link KeysetHandle#generateNew}. To generate a new keyset that contains a single {@link
  * com.google.crypto.tink.proto.EciesAeadHkdfPrivateKey}, one can do:
@@ -44,10 +59,7 @@
  * }</pre>
  *
  * @since 1.0.0
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM")
  */
-@Deprecated
 public final class HybridKeyTemplates {
   private static final byte[] EMPTY_SALT = new byte[0];
   /**
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridParameters.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridParameters.java
new file mode 100644
index 0000000..d0f0c4b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridParameters.java
@@ -0,0 +1,29 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.Immutable;
+
+/**
+ * Represents a description of a {@link HybridPrivateKey} and the corresponding {@link
+ * HybridPublicKey} excluding the randomly chosen key material.
+ */
+@Immutable
+@Alpha
+public abstract class HybridParameters extends Parameters {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridPrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridPrivateKey.java
new file mode 100644
index 0000000..cb97108
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridPrivateKey.java
@@ -0,0 +1,56 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrivateKey;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+import javax.annotation.Nullable;
+
+/**
+ * Representation of the decryption function for a hybrid encryption primitive.
+ *
+ * <p>The encryption function is available via {@link #getPublicKey}.
+ */
+@Immutable
+@Alpha
+public abstract class HybridPrivateKey extends Key implements PrivateKey {
+  @Override
+  public abstract HybridPublicKey getPublicKey();
+
+  /**
+   * Returns a {@link Bytes} instance, which is prefixed to every ciphertext.
+   *
+   * <p>Returns the same as {@code getPublicKey().getOutputPrefix()}.
+   */
+  public final Bytes getOutputPrefix() {
+    return getPublicKey().getOutputPrefix();
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return getPublicKey().getIdRequirementOrNull();
+  }
+
+  @Override
+  public HybridParameters getParameters() {
+    return getPublicKey().getParameters();
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridPublicKey.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridPublicKey.java
new file mode 100644
index 0000000..e67ea61
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridPublicKey.java
@@ -0,0 +1,37 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+
+/**
+ * Representation of the encryption function for a hybrid encryption primitive.
+ */
+@Immutable
+@Alpha
+public abstract class HybridPublicKey extends Key {
+  /**
+   * Returns a {@link Bytes} instance, which is prefixed to every ciphertext.
+   */
+  public abstract Bytes getOutputPrefix();
+
+  @Override
+  public abstract HybridParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridUtil.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridUtil.java
index a5bae81..2b4a46e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/HybridUtil.java
@@ -33,13 +33,14 @@
    * @throws GeneralSecurityException iff it's invalid.
    */
   public static void validate(EciesAeadHkdfParams params) throws GeneralSecurityException {
-    EllipticCurves.getCurveSpec(HybridUtil.toCurveType(params.getKemParams().getCurveType()));
-    HybridUtil.toHmacAlgo(params.getKemParams().getHkdfHashType());
+    Object unused =
+        EllipticCurves.getCurveSpec(HybridUtil.toCurveType(params.getKemParams().getCurveType()));
+    unused = HybridUtil.toHmacAlgo(params.getKemParams().getHkdfHashType());
     if (params.getEcPointFormat() == EcPointFormat.UNKNOWN_FORMAT) {
       throw new GeneralSecurityException("unknown EC point format");
     }
     // Check that we can generate new keys from the DEM AEAD key format.
-    Registry.newKeyData(params.getDemParams().getAeadDem());
+    unused = Registry.newKeyData(params.getDemParams().getAeadDem());
   }
 
   /**
@@ -65,7 +66,7 @@
     }
   }
 
-  /** Converts protobuf enum {@code EllipticCurveType} to raw Java enum {code CurveType}. */
+  /** Converts protobuf enum {@code EllipticCurveType} to raw Java enum {@code CurveType}. */
   public static EllipticCurves.CurveType toCurveType(EllipticCurveType type)
       throws GeneralSecurityException {
     switch (type) {
@@ -80,7 +81,7 @@
     }
   }
 
-  /** Converts protobuf enum {@code EcPointFormat} to raw Java enum {code PointFormatType}. */
+  /** Converts protobuf enum {@code EcPointFormat} to raw Java enum {@code PointFormatType}. */
   public static EllipticCurves.PointFormatType toPointFormatType(EcPointFormat format)
       throws GeneralSecurityException {
     switch (format) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelper.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelper.java
index 7573ba1..c98643a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelper.java
@@ -64,7 +64,10 @@
         AesGcmKeyFormat gcmKeyFormat =
             AesGcmKeyFormat.parseFrom(
                 demTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-        this.aesGcmKey = (AesGcmKey) Registry.newKey(demTemplate);
+        this.aesGcmKey =
+            AesGcmKey.parseFrom(
+                Registry.newKeyData(demTemplate).getValue(),
+                ExtensionRegistryLite.getEmptyRegistry());
         this.symmetricKeySize = gcmKeyFormat.getKeySize();
       } catch (InvalidProtocolBufferException e) {
         throw new GeneralSecurityException(
@@ -75,7 +78,10 @@
         AesCtrHmacAeadKeyFormat aesCtrHmacAeadKeyFormat =
             AesCtrHmacAeadKeyFormat.parseFrom(
                 demTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-        this.aesCtrHmacAeadKey = (AesCtrHmacAeadKey) Registry.newKey(demTemplate);
+        this.aesCtrHmacAeadKey =
+            AesCtrHmacAeadKey.parseFrom(
+                Registry.newKeyData(demTemplate).getValue(),
+                ExtensionRegistryLite.getEmptyRegistry());
         this.aesCtrKeySize = aesCtrHmacAeadKeyFormat.getAesCtrKeyFormat().getKeySize();
         int hmacKeySize = aesCtrHmacAeadKeyFormat.getHmacKeyFormat().getKeySize();
         this.symmetricKeySize = aesCtrKeySize + hmacKeySize;
@@ -88,7 +94,10 @@
         AesSivKeyFormat aesSivKeyFormat =
             AesSivKeyFormat.parseFrom(
                 demTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-        this.aesSivKey = (AesSivKey) Registry.newKey(demTemplate);
+        this.aesSivKey =
+            AesSivKey.parseFrom(
+                Registry.newKeyData(demTemplate).getValue(),
+                ExtensionRegistryLite.getEmptyRegistry());
         this.symmetricKeySize = aesSivKeyFormat.getKeySize();
       } catch (InvalidProtocolBufferException e) {
         throw new GeneralSecurityException(
@@ -115,7 +124,8 @@
           .mergeFrom(aesGcmKey)
           .setKeyValue(ByteString.copyFrom(symmetricKeyValue, 0, symmetricKeySize))
           .build();
-      return new AeadOrDaead(Registry.getPrimitive(demKeyTypeUrl, aeadKey, Aead.class));
+      return new AeadOrDaead(
+          Registry.getPrimitive(demKeyTypeUrl, aeadKey.toByteString(), Aead.class));
     } else if (demKeyTypeUrl.equals(AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL)) {
       byte[] aesCtrKeyValue = Arrays.copyOfRange(symmetricKeyValue, 0, aesCtrKeySize);
       byte[] hmacKeyValue = Arrays.copyOfRange(symmetricKeyValue, aesCtrKeySize, symmetricKeySize);
@@ -135,7 +145,8 @@
               .setAesCtrKey(aesCtrKey)
               .setHmacKey(hmacKey)
               .build();
-      return new AeadOrDaead(Registry.getPrimitive(demKeyTypeUrl, aeadKey, Aead.class));
+      return new AeadOrDaead(
+          Registry.getPrimitive(demKeyTypeUrl, aeadKey.toByteString(), Aead.class));
     } else if (demKeyTypeUrl.equals(DeterministicAeadConfig.AES_SIV_TYPE_URL)) {
       AesSivKey daeadKey =
           AesSivKey.newBuilder()
@@ -143,7 +154,7 @@
               .setKeyValue(ByteString.copyFrom(symmetricKeyValue, 0, symmetricKeySize))
               .build();
       return new AeadOrDaead(
-          Registry.getPrimitive(demKeyTypeUrl, daeadKey, DeterministicAead.class));
+          Registry.getPrimitive(demKeyTypeUrl, daeadKey.toByteString(), DeterministicAead.class));
     } else {
       throw new GeneralSecurityException("unknown DEM key type");
     }
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel
index 2808b12..136bfac 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel
@@ -54,8 +54,8 @@
         ":hpke_kem_private_key",
         ":hpke_util",
         "//proto:hpke_java_proto",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "@maven//:com_google_code_findbugs_jsr305",
     ],
 )
@@ -119,10 +119,10 @@
     srcs = ["HpkeKemKeyFactory.java"],
     deps = [
         ":hpke_kem_private_key",
+        ":hpke_util",
         ":nist_curves_hpke_kem_private_key",
         ":x25519_hpke_kem_private_key",
         "//proto:hpke_java_proto",
-        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
     ],
 )
 
@@ -168,9 +168,10 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
         "//src/main/java/com/google/crypto/tink/subtle:x25519",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -186,7 +187,7 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -195,7 +196,9 @@
     srcs = ["HpkeUtil.java"],
     deps = [
         "//proto:hpke_java_proto",
+        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
     ],
 )
 
@@ -303,8 +306,8 @@
         ":hpke_kem_private_key-android",
         ":hpke_util-android",
         "//proto:hpke_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
         "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
-        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "@maven//:com_google_code_findbugs_jsr305",
     ],
 )
@@ -368,10 +371,10 @@
     srcs = ["HpkeKemKeyFactory.java"],
     deps = [
         ":hpke_kem_private_key-android",
+        ":hpke_util-android",
         ":nist_curves_hpke_kem_private_key-android",
         ":x25519_hpke_kem_private_key-android",
         "//proto:hpke_java_proto_lite",
-        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
     ],
 )
 
@@ -417,9 +420,10 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:x25519-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -435,7 +439,7 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -444,7 +448,9 @@
     srcs = ["HpkeUtil.java"],
     deps = [
         "//proto:hpke_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
     ],
 )
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeContext.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeContext.java
index 3e449a5..4d5b35a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeContext.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeContext.java
@@ -16,9 +16,9 @@
 
 package com.google.crypto.tink.hybrid.internal;
 
+import com.google.crypto.tink.internal.BigIntegerEncoding;
 import com.google.crypto.tink.proto.HpkePublicKey;
 import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.SubtleUtil;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import javax.annotation.concurrent.GuardedBy;
@@ -137,7 +137,9 @@
   /** ComputeNonce() from https://www.rfc-editor.org/rfc/rfc9180.html#section-5.2-11. */
   @GuardedBy("this")
   private byte[] computeNonce() throws GeneralSecurityException {
-    return Bytes.xor(baseNonce, SubtleUtil.integer2Bytes(sequenceNumber, aead.getNonceLength()));
+    return Bytes.xor(
+        baseNonce,
+        BigIntegerEncoding.toBigEndianBytesOfFixedLength(sequenceNumber, aead.getNonceLength()));
   }
 
   /** Returns the next nonce to use for seal/open. Also, increments the sequence number. */
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeDecrypt.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeDecrypt.java
index 8e53cf1..7889256 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeDecrypt.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeDecrypt.java
@@ -61,6 +61,12 @@
     switch (kemProtoEnum) {
       case DHKEM_X25519_HKDF_SHA256:
         return 32;
+      case DHKEM_P256_HKDF_SHA256:
+        return 65;
+      case DHKEM_P384_HKDF_SHA384:
+        return 97;
+      case DHKEM_P521_HKDF_SHA512:
+        return 133;
       default:
         throw new IllegalArgumentException(
             "Unable to determine KEM-encoding length for " + kemProtoEnum.name());
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeKemKeyFactory.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeKemKeyFactory.java
index 1b0c68c..1b9cfee 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeKemKeyFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeKemKeyFactory.java
@@ -16,28 +16,12 @@
 
 package com.google.crypto.tink.hybrid.internal;
 
-import com.google.crypto.tink.proto.HpkeKem;
 import com.google.crypto.tink.proto.HpkePrivateKey;
-import com.google.crypto.tink.subtle.EllipticCurves;
 import java.security.GeneralSecurityException;
 
 /** Helper class for creating HPKE KEM asymmetric keys. */
 final class HpkeKemKeyFactory {
 
-  private static EllipticCurves.CurveType nistHpkeKemToCurve(HpkeKem kem)
-      throws GeneralSecurityException {
-    switch (kem) {
-      case DHKEM_P256_HKDF_SHA256:
-        return EllipticCurves.CurveType.NIST_P256;
-      case DHKEM_P384_HKDF_SHA384:
-        return EllipticCurves.CurveType.NIST_P384;
-      case DHKEM_P521_HKDF_SHA512:
-        return EllipticCurves.CurveType.NIST_P521;
-      default:
-        throw new GeneralSecurityException("Unrecognized NIST HPKE KEM identifier");
-    }
-  }
-
   static HpkeKemPrivateKey createPrivate(HpkePrivateKey privateKey)
       throws GeneralSecurityException {
     switch (privateKey.getPublicKey().getParams().getKem()) {
@@ -49,7 +33,7 @@
         return NistCurvesHpkeKemPrivateKey.fromBytes(
             privateKey.getPrivateKey().toByteArray(),
             privateKey.getPublicKey().getPublicKey().toByteArray(),
-            nistHpkeKemToCurve(privateKey.getPublicKey().getParams().getKem()));
+            HpkeUtil.nistHpkeKemToCurve(privateKey.getPublicKey().getParams().getKem()));
       default:
         throw new GeneralSecurityException("Unrecognized HPKE KEM identifier");
     }
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManager.java
index 5171d3d..77cb832 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManager.java
@@ -30,12 +30,16 @@
 import com.google.crypto.tink.proto.HpkePrivateKey;
 import com.google.crypto.tink.proto.HpkePublicKey;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.subtle.EllipticCurves;
 import com.google.crypto.tink.subtle.Validators;
 import com.google.crypto.tink.subtle.X25519;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
 import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -121,8 +125,30 @@
 
       @Override
       public HpkePrivateKey createKey(HpkeKeyFormat keyFormat) throws GeneralSecurityException {
-        byte[] privateKeyBytes = X25519.generatePrivateKey();
-        byte[] publicKeyBytes = X25519.publicFromPrivate(privateKeyBytes);
+        byte[] privateKeyBytes;
+        byte[] publicKeyBytes;
+
+        switch (keyFormat.getParams().getKem()) {
+          case DHKEM_X25519_HKDF_SHA256:
+            privateKeyBytes = X25519.generatePrivateKey();
+            publicKeyBytes = X25519.publicFromPrivate(privateKeyBytes);
+            break;
+          case DHKEM_P256_HKDF_SHA256:
+          case DHKEM_P384_HKDF_SHA384:
+          case DHKEM_P521_HKDF_SHA512:
+            EllipticCurves.CurveType curveType =
+                HpkeUtil.nistHpkeKemToCurve(keyFormat.getParams().getKem());
+            KeyPair keyPair = EllipticCurves.generateKeyPair(curveType);
+            publicKeyBytes =
+                EllipticCurves.pointEncode(
+                    curveType,
+                    EllipticCurves.PointFormatType.UNCOMPRESSED,
+                    ((ECPublicKey) keyPair.getPublic()).getW());
+            privateKeyBytes = ((ECPrivateKey) keyPair.getPrivate()).getS().toByteArray();
+            break;
+          default:
+            throw new GeneralSecurityException("Invalid KEM");
+        }
 
         HpkePublicKey publicKey =
             HpkePublicKey.newBuilder()
diff --git a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeUtil.java b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeUtil.java
index 535b287..7639422 100644
--- a/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/hybrid/internal/HpkeUtil.java
@@ -16,13 +16,14 @@
 
 package com.google.crypto.tink.hybrid.internal;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.crypto.tink.internal.Util.UTF_8;
 
 import com.google.crypto.tink.proto.HpkeAead;
 import com.google.crypto.tink.proto.HpkeKdf;
 import com.google.crypto.tink.proto.HpkeKem;
 import com.google.crypto.tink.proto.HpkeParams;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.EllipticCurves;
 import java.security.GeneralSecurityException;
 
 /** Collection of helper functions for HPKE. */
@@ -129,5 +130,18 @@
     }
   }
 
+  static EllipticCurves.CurveType nistHpkeKemToCurve(HpkeKem kem) throws GeneralSecurityException {
+    switch (kem) {
+      case DHKEM_P256_HKDF_SHA256:
+        return EllipticCurves.CurveType.NIST_P256;
+      case DHKEM_P384_HKDF_SHA384:
+        return EllipticCurves.CurveType.NIST_P384;
+      case DHKEM_P521_HKDF_SHA512:
+        return EllipticCurves.CurveType.NIST_P521;
+      default:
+        throw new GeneralSecurityException("Unrecognized NIST HPKE KEM identifier");
+    }
+  }
+
   private HpkeUtil() {}
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java
index 1cc4c39..786fa38 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java
@@ -17,22 +17,26 @@
 package com.google.crypto.tink.integration.android;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.os.Build;
+import android.preference.PreferenceManager;
 import android.util.Log;
 import androidx.annotation.ChecksSdkIntAtLeast;
+import androidx.annotation.Nullable;
 import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.BinaryKeysetReader;
 import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KeysetManager;
-import com.google.crypto.tink.KeysetReader;
 import com.google.crypto.tink.KeysetWriter;
 import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.protobuf.InvalidProtocolBufferException;
-import java.io.FileNotFoundException;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.InlineMe;
+import java.io.CharConversionException;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
-import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.ProviderException;
 import javax.annotation.concurrent.GuardedBy;
@@ -53,7 +57,7 @@
  * // later use.
  * AndroidKeysetManager manager = AndroidKeysetManager.Builder()
  *    .withSharedPref(getApplicationContext(), "my_keyset_name", "my_pref_file_name")
- *    .withKeyTemplate(AesGcmHkfStreamingKeyManager.aes128GcmHkdf4KBTemplate())
+ *    .withKeyTemplate(KeyTemplates.get("AES128_GCM_HKDF_4KB"))
  *    .build();
  * StreamingAead streamingAead = manager.getKeysetHandle().getPrimitive(StreamingAead.class);
  * }</pre>
@@ -63,23 +67,24 @@
  * preferences file.
  *
  * <ul>
- *   <li>If a keyset is found, but it is invalid, an {@link IOException} is thrown. The most common
- *       cause is when you decrypted a keyset with a wrong master key. In this case, an {@link
- *       InvalidProtocolBufferException} would be thrown. This is an irrecoverable error. You'd have
- *       to delete the keyset in Shared Preferences and all existing data encrypted with it.
+ *   <li>If a keyset is found, but cannot be read, either an {@link IOException} or a {@link
+ *       GeneralSecurityException} is thrown. The most common cause is that the master key is
+ *       missing or the wrong master key is used. In this case, a {@link GeneralSecurityException}
+ *       would be thrown. This is an irrecoverable error. You'd have to delete the keyset in Shared
+ *       Preferences and all existing data encrypted with it.
  *   <li>If a keyset is not found, and a {@link KeyTemplate} is set with {@link
  *       AndroidKeysetManager.Builder#withKeyTemplate(com.google.crypto.tink.KeyTemplate)}, a fresh
  *       keyset is generated and is written to the {@code my_keyset_name} preference of the {@code
  *       my_pref_file_name} shared preferences file.
  * </ul>
  *
- * <h3>Key rotation</h3>
+ * <h3>Adding a new key</h3>
  *
  * <p>The resulting manager supports all operations supported by {@link KeysetManager}. For example
- * to rotate the keyset, you can do:
+ * to add a key to the keyset, you can do:
  *
  * <pre>{@code
- * manager.rotate(AesGcmHkfStreamingKeyManager.aes128GcmHkdf1MBTemplate());
+ * manager.add(KeyTemplates.get("AES128_GCM_HKDF_4KB"));
  * }</pre>
  *
  * <p>All operations that manipulate the keyset would automatically persist the new keyset to
@@ -119,16 +124,18 @@
  * @since 1.0.0
  */
 public final class AndroidKeysetManager {
+  private static final Object lock = new Object();
+
   private static final String TAG = AndroidKeysetManager.class.getSimpleName();
   private final KeysetWriter writer;
-  private final Aead masterKey;
+  private final Aead masterAead;
 
   @GuardedBy("this")
   private KeysetManager keysetManager;
 
-  private AndroidKeysetManager(Builder builder) throws GeneralSecurityException, IOException {
-    writer = builder.writer;
-    masterKey = builder.masterKey;
+  private AndroidKeysetManager(Builder builder) {
+    writer = new SharedPrefKeysetWriter(builder.context, builder.keysetName, builder.prefFileName);
+    masterAead = builder.masterAead;
     keysetManager = builder.keysetManager;
   }
 
@@ -138,13 +145,14 @@
    * <p>This class is thread-safe.
    */
   public static final class Builder {
-    private KeysetReader reader = null;
-    private KeysetWriter writer = null;
+    private Context context = null;
+    private String keysetName = null;
+    private String prefFileName = null;
+
     private String masterKeyUri = null;
-    private Aead masterKey = null;
+    private Aead masterAead = null;
     private boolean useKeystore = true;
     private KeyTemplate keyTemplate = null;
-    private KeyStore keyStore = null;
 
     @GuardedBy("this")
     private KeysetManager keysetManager;
@@ -152,6 +160,7 @@
     public Builder() {}
 
     /** Reads and writes the keyset from shared preferences. */
+    @CanIgnoreReturnValue
     public Builder withSharedPref(Context context, String keysetName, String prefFileName)
         throws IOException {
       if (context == null) {
@@ -160,17 +169,24 @@
       if (keysetName == null) {
         throw new IllegalArgumentException("need a keyset name");
       }
-      reader = new SharedPrefKeysetReader(context, keysetName, prefFileName);
-      writer = new SharedPrefKeysetWriter(context, keysetName, prefFileName);
+      this.context = context;
+      this.keysetName = keysetName;
+      this.prefFileName = prefFileName;
+
       return this;
     }
 
     /**
-     * Sets the master key URI.
+     * Sets the master key URI that references the key in Android Keystore with which the keyset
+     * gets encrypted.
      *
      * <p>Only master keys stored in Android Keystore is supported. The URI must start with {@code
      * android-keystore://}.
+     *
+     * <p>Android Keystore is only supported on Android M (API level 23) and later. On older
+     * version, calling this method works but doesn't do anything.
      */
+    @CanIgnoreReturnValue
     public Builder withMasterKeyUri(String val) {
       if (!val.startsWith(AndroidKeystoreKmsClient.PREFIX)) {
         throw new IllegalArgumentException(
@@ -186,11 +202,8 @@
 
     /**
      * If the keyset is not found or valid, generates a new one using {@code val}.
-     *
-     * @deprecated This method takes a KeyTemplate proto, which is an internal implementation
-     *     detail. Please use the withKeyTemplate method that takes a {@link KeyTemplate} POJO.
      */
-    @Deprecated
+    @CanIgnoreReturnValue
     public Builder withKeyTemplate(com.google.crypto.tink.proto.KeyTemplate val) {
       keyTemplate =
           KeyTemplate.create(
@@ -199,6 +212,7 @@
     }
 
     /** If the keyset is not found or valid, generates a new one using {@code val}. */
+    @CanIgnoreReturnValue
     public Builder withKeyTemplate(KeyTemplate val) {
       keyTemplate = val;
       return this;
@@ -210,8 +224,10 @@
      * <p><b>Warning:</b> When Android Keystore is disabled, keys are stored in cleartext. This
      * should be safe because they are stored in private preferences.
      *
-     * @deprecated Android Keystore can be disabled by not setting a master key URI.
+     * @deprecated Please do not use this function. Instead, do not call {#code withMasterKeyUri}
+     *     which has the same effect.
      */
+    @CanIgnoreReturnValue
     @Deprecated
     public Builder doNotUseKeystore() {
       masterKeyUri = null;
@@ -219,10 +235,39 @@
       return this;
     }
 
-    /** This is for testing only */
-    Builder withKeyStore(KeyStore val) {
-      this.keyStore = val;
-      return this;
+    /** Returns the serialized keyset if it exist or null. */
+    @Nullable
+    @SuppressWarnings("UnusedException")
+    private static byte[] readKeysetFromPrefs(
+        Context context, String keysetName, String prefFileName) throws IOException {
+      if (keysetName == null) {
+        throw new IllegalArgumentException("keysetName cannot be null");
+      }
+      Context appContext = context.getApplicationContext();
+      SharedPreferences sharedPreferences;
+      if (prefFileName == null) {
+        sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext);
+      } else {
+        sharedPreferences = appContext.getSharedPreferences(prefFileName, Context.MODE_PRIVATE);
+      }
+      try {
+        String keysetHex = sharedPreferences.getString(keysetName, /* defValue= */ null);
+        if (keysetHex == null) {
+          return null;
+        }
+        return Hex.decode(keysetHex);
+      } catch (ClassCastException | IllegalArgumentException ex) {
+        // The original exception is swallowed to prevent leaked key material.
+        throw new CharConversionException(
+            String.format(
+                "can't read keyset; the pref value %s is not a valid hex string", keysetName));
+      }
+    }
+
+    private KeysetManager readKeysetInCleartext(byte[] serializedKeyset)
+        throws GeneralSecurityException, IOException {
+      return KeysetManager.withKeysetHandle(
+          CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(serializedKeyset)));
     }
 
     /**
@@ -233,35 +278,46 @@
      * @throws GeneralSecurityException If cannot read an existing keyset or generate a new one.
      */
     public synchronized AndroidKeysetManager build() throws GeneralSecurityException, IOException {
-      if (masterKeyUri != null) {
-        masterKey = readOrGenerateNewMasterKey();
+      if (keysetName == null) {
+        throw new IllegalArgumentException("keysetName cannot be null");
       }
-      this.keysetManager = readOrGenerateNewKeyset();
-
-      return new AndroidKeysetManager(this);
+      // readKeysetFromPrefs(), readOrGenerateNewMasterKey() and generateNewKeyset() involve shared
+      // pref filesystem operations. To control access to this global state in multi-threaded
+      // contexts we need to ensure mutual exclusion of these functions.
+      synchronized (lock) {
+        byte[] serializedKeyset = readKeysetFromPrefs(context, keysetName, prefFileName);
+        if (serializedKeyset == null) {
+          if (masterKeyUri != null) {
+            masterAead = readOrGenerateNewMasterKey();
+          }
+          this.keysetManager = generateKeysetAndWriteToPrefs();
+        } else {
+          if (masterKeyUri == null || !isAtLeastM()) {
+            this.keysetManager = readKeysetInCleartext(serializedKeyset);
+          } else {
+            this.keysetManager = readMasterkeyDecryptAndParseKeyset(serializedKeyset);
+          }
+        }
+        return new AndroidKeysetManager(this);
+      }
     }
 
+    @Nullable
     private Aead readOrGenerateNewMasterKey() throws GeneralSecurityException {
       if (!isAtLeastM()) {
         Log.w(TAG, "Android Keystore requires at least Android M");
         return null;
       }
 
-      AndroidKeystoreKmsClient client;
-      if (keyStore != null) {
-        client = new AndroidKeystoreKmsClient.Builder().setKeyStore(keyStore).build();
-      } else {
-        client = new AndroidKeystoreKmsClient();
-      }
+      AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
 
-      boolean existed = client.hasKey(masterKeyUri);
-      if (!existed) {
-        try {
-          AndroidKeystoreKmsClient.generateNewAeadKey(masterKeyUri);
-        } catch (GeneralSecurityException | ProviderException ex) {
-          Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
-          return null;
-        }
+      boolean generated;
+      try {
+        // Note that this function does not use the keyStore instance set with withKeyStore.
+        generated = AndroidKeystoreKmsClient.generateKeyIfNotExist(masterKeyUri);
+      } catch (GeneralSecurityException | ProviderException ex) {
+        Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
+        return null;
       }
 
       try {
@@ -269,9 +325,10 @@
       } catch (GeneralSecurityException | ProviderException ex) {
         // Throw the exception if the key exists but is unusable. We can't recover by generating a
         // new key because there might be existing encrypted data under the unusable key.
-        // Users can provide a master key that is stored in StrongBox, which may throw a
-        // ProviderException if there's any problem with it.
-        if (existed) {
+        // Users can provide a master key that is stored in StrongBox (see
+        // https://developer.android.com/about/versions/pie/android-9.0#hardware-security-module),
+        // which may throw a ProviderException if there's any problem with it.
+        if (!generated) {
           throw new KeyStoreException(
               String.format("the master key %s exists but is unusable", masterKeyUri), ex);
         }
@@ -283,59 +340,68 @@
       return null;
     }
 
-    private KeysetManager readOrGenerateNewKeyset() throws GeneralSecurityException, IOException {
-      try {
-        return read();
-      } catch (FileNotFoundException ex) {
-        // Not found, handle below.
-        if (Log.isLoggable(TAG, Log.INFO)) {
-          Log.i(
-              TAG, String.format("keyset not found, will generate a new one. %s", ex.getMessage()));
-        }
+    private KeysetManager generateKeysetAndWriteToPrefs()
+        throws GeneralSecurityException, IOException {
+      if (keyTemplate == null) {
+        throw new GeneralSecurityException("cannot read or generate keyset");
       }
 
-      // Not found.
-      if (keyTemplate != null) {
-        KeysetManager manager = KeysetManager.withEmptyKeyset().add(keyTemplate);
-        int keyId = manager.getKeysetHandle().getKeysetInfo().getKeyInfo(0).getKeyId();
-        manager = manager.setPrimary(keyId);
-        if (masterKey != null) {
-          manager.getKeysetHandle().write(writer, masterKey);
-        } else {
-          CleartextKeysetHandle.write(manager.getKeysetHandle(), writer);
-        }
-        return manager;
+      KeysetManager manager = KeysetManager.withEmptyKeyset().add(keyTemplate);
+      int keyId = manager.getKeysetHandle().getKeysetInfo().getKeyInfo(0).getKeyId();
+      manager = manager.setPrimary(keyId);
+      KeysetWriter writer = new SharedPrefKeysetWriter(context, keysetName, prefFileName);
+      if (masterAead != null) {
+        manager.getKeysetHandle().write(writer, masterAead);
+      } else {
+        CleartextKeysetHandle.write(manager.getKeysetHandle(), writer);
       }
-      throw new GeneralSecurityException("cannot read or generate keyset");
+      return manager;
     }
 
-    private KeysetManager read() throws GeneralSecurityException, IOException {
-      if (masterKey != null) {
+    @SuppressWarnings("UnusedException")
+    private KeysetManager readMasterkeyDecryptAndParseKeyset(byte[] serializedKeyset)
+        throws GeneralSecurityException, IOException {
+      // We expect that the keyset is encrypted. Try to get masterAead.
+      try {
+        masterAead = new AndroidKeystoreKmsClient().getAead(masterKeyUri);
+      } catch (GeneralSecurityException | ProviderException keystoreException) {
+        // Getting masterAead failed. Attempt to read the keyset in cleartext.
         try {
-          return KeysetManager.withKeysetHandle(KeysetHandle.read(reader, masterKey));
-        } catch (InvalidProtocolBufferException | GeneralSecurityException ex) {
-          // Swallow the exception and attempt to read the keyset in cleartext.
-          // This edge case may happen when either
-          //   - the keyset was generated on a pre M phone which is then upgraded to M or newer, or
-          //   - the keyset was generated with Keystore being disabled, then Keystore is enabled.
-          // By ignoring the security failure here, an adversary with write access to private
-          // preferences can replace an encrypted keyset (that it cannot read or write) with a
-          // cleartext value that it controls. This does not introduce new security risks because to
-          // overwrite the encrypted keyset in private preferences of an app, said adversaries must
-          // have the same privilege as the app, thus they can call Android Keystore to read or
-          // write
-          // the encrypted keyset in the first place.
-          Log.w(TAG, "cannot decrypt keyset: ", ex);
+          KeysetManager manager = readKeysetInCleartext(serializedKeyset);
+          Log.w(TAG, "cannot use Android Keystore, it'll be disabled", keystoreException);
+          return manager;
+        } catch (IOException unused) {
+          // Keyset is encrypted, throw error about master key encryption
+          throw keystoreException;
         }
       }
-
-      return KeysetManager.withKeysetHandle(CleartextKeysetHandle.read(reader));
+      // Got masterAead successfully.
+      try {
+        // Decrypt and parse the keyset using masterAead.
+        return KeysetManager.withKeysetHandle(
+            KeysetHandle.read(BinaryKeysetReader.withBytes(serializedKeyset), masterAead));
+      } catch (IOException | GeneralSecurityException ex) {
+        // Attempt to read the keyset in cleartext.
+        // This edge case may happen when either
+        //   - the keyset was generated on a pre M phone which was upgraded to M or newer, or
+        //   - the keyset was generated with Keystore being disabled, then Keystore is enabled.
+        // By ignoring the security failure here, an adversary with write access to private
+        // preferences can replace an encrypted keyset (that it cannot read or write) with a
+        // cleartext value that it controls. This does not introduce new security risks because to
+        // overwrite the encrypted keyset in private preferences of an app, said adversaries
+        // must have the same privilege as the app, thus they can call Android Keystore to read or
+        // write the encrypted keyset in the first place.
+        try {
+          return readKeysetInCleartext(serializedKeyset);
+        } catch (IOException unused) {
+          // Parsing failed because the keyset is encrypted but we were not able to decrypt it.
+          throw ex;
+        }
+      }
     }
   }
 
-  /**
-   * @return a {@link KeysetHandle} of the managed keyset
-   */
+  /** Returns a {@link KeysetHandle} of the managed keyset. */
   public synchronized KeysetHandle getKeysetHandle() throws GeneralSecurityException {
     return keysetManager.getKeysetHandle();
   }
@@ -350,6 +416,7 @@
    *     primary. However, when you do keyset rotation, you almost never want to make the new key
    *     primary, because old binaries don't know the new key yet.
    */
+  @CanIgnoreReturnValue
   @Deprecated
   public synchronized AndroidKeysetManager rotate(
       com.google.crypto.tink.proto.KeyTemplate keyTemplate) throws GeneralSecurityException {
@@ -363,11 +430,9 @@
    *
    * @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
    *     keyTemplate}
-   * @deprecated This method takes a KeyTemplate proto, which is an internal implementation detail.
-   *     Please use the add method that takes a {@link KeyTemplate} POJO.
    */
+  @CanIgnoreReturnValue
   @GuardedBy("this")
-  @Deprecated
   public synchronized AndroidKeysetManager add(com.google.crypto.tink.proto.KeyTemplate keyTemplate)
       throws GeneralSecurityException {
     keysetManager = keysetManager.add(keyTemplate);
@@ -381,6 +446,7 @@
    * @throws GeneralSecurityException if cannot find any {@link KeyManager} that can handle {@code
    *     keyTemplate}
    */
+  @CanIgnoreReturnValue
   @GuardedBy("this")
   public synchronized AndroidKeysetManager add(KeyTemplate keyTemplate)
       throws GeneralSecurityException {
@@ -394,6 +460,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or not enabled
    */
+  @CanIgnoreReturnValue
   public synchronized AndroidKeysetManager setPrimary(int keyId) throws GeneralSecurityException {
     keysetManager = keysetManager.setPrimary(keyId);
     write(keysetManager);
@@ -406,8 +473,11 @@
    * @throws GeneralSecurityException if the key is not found or not enabled
    * @deprecated use {@link setPrimary}
    */
+  @InlineMe(replacement = "this.setPrimary(keyId)")
+  @CanIgnoreReturnValue
   @Deprecated
-  public synchronized AndroidKeysetManager promote(int keyId) throws GeneralSecurityException {
+  public synchronized AndroidKeysetManager promote(int keyId)
+      throws GeneralSecurityException {
     return setPrimary(keyId);
   }
 
@@ -416,6 +486,7 @@
    *
    * @throws GeneralSecurityException if the key is not found
    */
+  @CanIgnoreReturnValue
   public synchronized AndroidKeysetManager enable(int keyId) throws GeneralSecurityException {
     keysetManager = keysetManager.enable(keyId);
     write(keysetManager);
@@ -427,6 +498,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or it is the primary key
    */
+  @CanIgnoreReturnValue
   public synchronized AndroidKeysetManager disable(int keyId) throws GeneralSecurityException {
     keysetManager = keysetManager.disable(keyId);
     write(keysetManager);
@@ -438,6 +510,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or it is the primary key
    */
+  @CanIgnoreReturnValue
   public synchronized AndroidKeysetManager delete(int keyId) throws GeneralSecurityException {
     keysetManager = keysetManager.delete(keyId);
     write(keysetManager);
@@ -449,6 +522,7 @@
    *
    * @throws GeneralSecurityException if the key is not found or it is the primary key
    */
+  @CanIgnoreReturnValue
   public synchronized AndroidKeysetManager destroy(int keyId) throws GeneralSecurityException {
     keysetManager = keysetManager.destroy(keyId);
     write(keysetManager);
@@ -463,7 +537,7 @@
   private void write(KeysetManager manager) throws GeneralSecurityException {
     try {
       if (shouldUseKeystore()) {
-        manager.getKeysetHandle().write(writer, masterKey);
+        manager.getKeysetHandle().write(writer, masterAead);
       } else {
         CleartextKeysetHandle.write(manager.getKeysetHandle(), writer);
       }
@@ -474,7 +548,7 @@
 
   @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
   private boolean shouldUseKeystore() {
-    return masterKey != null && isAtLeastM();
+    return masterAead != null && isAtLeastM();
   }
 
   private static KeyTemplate.OutputPrefixType fromProto(OutputPrefixType outputPrefixType) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreAesGcm.java b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreAesGcm.java
index 1fa3fe7..f0453a3 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreAesGcm.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreAesGcm.java
@@ -23,6 +23,7 @@
 import java.security.InvalidKeyException;
 import java.security.KeyStore;
 import java.security.ProviderException;
+import javax.crypto.AEADBadTagException;
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.GCMParameterSpec;
@@ -62,18 +63,18 @@
   }
 
   @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] aad)
+  public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
       throws GeneralSecurityException {
     try {
-      return encryptInternal(plaintext, aad);
+      return encryptInternal(plaintext, associatedData);
     } catch (ProviderException | GeneralSecurityException ex) {
       Log.w(TAG, "encountered a potentially transient KeyStore error, will wait and retry", ex);
-      sleep();
-      return encryptInternal(plaintext, aad);
+      sleepRandomAmount();
+      return encryptInternal(plaintext, associatedData);
     }
   }
 
-  private byte[] encryptInternal(final byte[] plaintext, final byte[] aad)
+  private byte[] encryptInternal(final byte[] plaintext, final byte[] associatedData)
       throws GeneralSecurityException {
     // Check that ciphertext is not longer than the max. size of a Java array.
     if (plaintext.length > Integer.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) {
@@ -82,7 +83,7 @@
     byte[] ciphertext = new byte[IV_SIZE_IN_BYTES + plaintext.length + TAG_SIZE_IN_BYTES];
     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
     cipher.init(Cipher.ENCRYPT_MODE, key);
-    cipher.updateAAD(aad);
+    cipher.updateAAD(associatedData);
     int unusedWritten =
         cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, IV_SIZE_IN_BYTES);
     // Copy the IV that is randomly generated by Android Keystore.
@@ -91,31 +92,33 @@
   }
 
   @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] aad)
-      throws GeneralSecurityException {
-    try {
-      return decryptInternal(ciphertext, aad);
-    } catch (ProviderException | GeneralSecurityException ex) {
-      Log.w(TAG, "encountered a potentially transient KeyStore error, will wait and retry", ex);
-      sleep();
-      return decryptInternal(ciphertext, aad);
-    }
-  }
-
-  private byte[] decryptInternal(final byte[] ciphertext, final byte[] aad)
+  public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
       throws GeneralSecurityException {
     if (ciphertext.length < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) {
       throw new GeneralSecurityException("ciphertext too short");
     }
+    try {
+      return decryptInternal(ciphertext, associatedData);
+    } catch (AEADBadTagException ex) {
+      throw ex;
+    } catch (ProviderException | GeneralSecurityException ex) {
+      Log.w(TAG, "encountered a potentially transient KeyStore error, will wait and retry", ex);
+      sleepRandomAmount();
+      return decryptInternal(ciphertext, associatedData);
+    }
+  }
+
+  private byte[] decryptInternal(final byte[] ciphertext, final byte[] associatedData)
+      throws GeneralSecurityException {
     GCMParameterSpec params =
         new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, ciphertext, 0, IV_SIZE_IN_BYTES);
     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
     cipher.init(Cipher.DECRYPT_MODE, key, params);
-    cipher.updateAAD(aad);
+    cipher.updateAAD(associatedData);
     return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.length - IV_SIZE_IN_BYTES);
   }
 
-  private static void sleep() {
+  private static void sleepRandomAmount() {
     int waitTimeMillis = (int) (Math.random() * MAX_WAIT_TIME_MILLISECONDS_BEFORE_RETRY);
     try {
       Thread.sleep(waitTimeMillis);
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreKmsClient.java b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreKmsClient.java
index 6cc1592..bf94a6d 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreKmsClient.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystoreKmsClient.java
@@ -26,6 +26,7 @@
 import com.google.crypto.tink.KmsClient;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.Validators;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
@@ -44,8 +45,10 @@
  * @since 1.0.0
  */
 public final class AndroidKeystoreKmsClient implements KmsClient {
+  private static final Object keyCreationLock = new Object();
+
   private static final String TAG = AndroidKeystoreKmsClient.class.getSimpleName();
-  private static final int WAIT_TIME_MILLISECONDS_BEFORE_RETRY = 20;
+  private static final int MAX_WAIT_TIME_MILLISECONDS_BEFORE_RETRY = 40;
 
   /** The prefix of all keys stored in Android Keystore. */
   public static final String PREFIX = "android-keystore://";
@@ -66,8 +69,8 @@
    *
    * @deprecated use {@link AndroidKeystoreKmsClient.Builder}.
    */
-  @Deprecated
   @RequiresApi(23)
+  @Deprecated
   public AndroidKeystoreKmsClient(String uri) {
     this(new Builder().setKeyUri(uri));
   }
@@ -96,6 +99,7 @@
       }
     }
 
+    @CanIgnoreReturnValue
     @RequiresApi(23)
     public Builder setKeyUri(String val) {
       if (val == null || !val.toLowerCase(Locale.US).startsWith(PREFIX)) {
@@ -106,6 +110,7 @@
     }
 
     /** This is for testing only */
+    @CanIgnoreReturnValue
     @RequiresApi(23)
     public Builder setKeyStore(KeyStore val) {
       if (val == null) {
@@ -187,35 +192,40 @@
     try {
       return this.keyStore.containsAlias(keyId);
     } catch (NullPointerException ex1) {
-      // TODO(b/167402931): figure out how to test this.
-      Log.w(
-          TAG,
-          "Keystore is temporarily unavailable, wait 20ms, reinitialize Keystore and try again.");
+      Log.w(TAG, "Keystore is temporarily unavailable, wait, reinitialize Keystore and try again.");
       try {
-        Thread.sleep(WAIT_TIME_MILLISECONDS_BEFORE_RETRY);
+        sleepRandomAmount();
         this.keyStore = KeyStore.getInstance("AndroidKeyStore");
         this.keyStore.load(/* param= */ null);
       } catch (IOException ex2) {
         throw new GeneralSecurityException(ex2);
-      } catch (InterruptedException ex) {
-        // Ignored.
       }
       return this.keyStore.containsAlias(keyId);
     }
   }
 
+  private static void sleepRandomAmount() {
+    int waitTimeMillis = (int) (Math.random() * MAX_WAIT_TIME_MILLISECONDS_BEFORE_RETRY);
+    try {
+      Thread.sleep(waitTimeMillis);
+    } catch (InterruptedException ex) {
+      // Ignored.
+    }
+  }
+
   /**
    * Generates a new key in Android Keystore, if it doesn't exist.
    *
-   * <p>At the moment it can generate only AES256-GCM keys.
+   * <p>Generates AES256-GCM keys.
    */
   @RequiresApi(Build.VERSION_CODES.M)
   public static Aead getOrGenerateNewAeadKey(String keyUri)
       throws GeneralSecurityException, IOException {
     AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
-    if (!client.hasKey(keyUri)) {
-      Log.i(TAG, String.format("key URI %s doesn't exist, generating a new one", keyUri));
-      generateNewAeadKey(keyUri);
+    synchronized (keyCreationLock) {
+      if (!client.hasKey(keyUri)) {
+        generateNewAesGcmKeyWithoutExistenceCheck(keyUri);
+      }
     }
     return client.getAead(keyUri);
   }
@@ -223,19 +233,31 @@
   /**
    * Generates a new key in Android Keystore.
    *
-   * <p>At the moment it can generate only AES256-GCM keys.
+   * <p>Generates AES256-GCM keys.
    */
   @RequiresApi(Build.VERSION_CODES.M)
   public static void generateNewAeadKey(String keyUri) throws GeneralSecurityException {
     AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
-    if (client.hasKey(keyUri)) {
-      throw new IllegalArgumentException(
-          String.format(
-              "cannot generate a new key %s because it already exists; please delete it with"
-                  + " deleteKey() and try again",
-              keyUri));
+    synchronized (keyCreationLock) {
+      if (client.hasKey(keyUri)) {
+        throw new IllegalArgumentException(
+            String.format(
+                "cannot generate a new key %s because it already exists; please delete it with"
+                    + " deleteKey() and try again",
+                keyUri));
+      }
+      generateNewAesGcmKeyWithoutExistenceCheck(keyUri);
     }
+  }
 
+  /**
+   * Generates a new AES256-GCM key in Android Keystore.
+   *
+   * <p>This function does not check if the key already exists, and will overwrite any existing key.
+   */
+  @RequiresApi(Build.VERSION_CODES.M)
+  static void generateNewAesGcmKeyWithoutExistenceCheck(String keyUri)
+      throws GeneralSecurityException {
     String keyId = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, keyUri);
     KeyGenerator keyGenerator =
         KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
@@ -250,6 +272,23 @@
     keyGenerator.generateKey();
   }
 
+  /**
+   * Checks if the key exists, and generates a new one if it does not yet exist.
+   *
+   * <p>Returns true if a new key was generated.
+   */
+  @RequiresApi(Build.VERSION_CODES.M)
+  static boolean generateKeyIfNotExist(String keyUri) throws GeneralSecurityException {
+    AndroidKeystoreKmsClient client = new AndroidKeystoreKmsClient();
+    synchronized (keyCreationLock) {
+      if (!client.hasKey(keyUri)) {
+        generateNewAesGcmKeyWithoutExistenceCheck(keyUri);
+        return true;
+      }
+      return false;
+    }
+  }
+
   /** Does a self-test to verify whether we can rely on Android Keystore */
   private static Aead validateAead(Aead aead) throws GeneralSecurityException {
     // Non-empty message and empty aad.
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/android/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/integration/android/BUILD.bazel
index c6360c4..2486f3f 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/android/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/android/BUILD.bazel
@@ -9,21 +9,20 @@
     srcs = ["AndroidKeystoreKmsClient.java"],
     deps = [
         ":android_keystore_aes_gcm",
-        "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink:kms_client",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/subtle:validators",
+        "//src/main/java/com/google/crypto/tink:aead-android",
+        "//src/main/java/com/google/crypto/tink:kms_client-android",
+        "//src/main/java/com/google/crypto/tink/subtle:random-android",
+        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
         "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
 android_library(
     name = "android_keystore_aes_gcm",
     srcs = ["AndroidKeystoreAesGcm.java"],
-    deps = [
-        "//src/main/java/com/google/crypto/tink:aead",
-    ],
+    deps = ["//src/main/java/com/google/crypto/tink:aead-android"],
 )
 
 android_library(
@@ -32,7 +31,7 @@
     deps = [
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:keyset_writer-android",
-        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:hex-android",
     ],
 )
 
@@ -42,8 +41,8 @@
     deps = [
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:keyset_reader-android",
-        "//src/main/java/com/google/crypto/tink/subtle:hex",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/subtle:hex-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -52,17 +51,18 @@
     srcs = ["AndroidKeysetManager.java"],
     deps = [
         ":android_keystore_kms_client",
-        ":shared_pref_keyset_reader",
         ":shared_pref_keyset_writer",
         "//proto:tink_java_proto_lite",
-        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:aead-android",
+        "//src/main/java/com/google/crypto/tink:binary_keyset_reader-android",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle-android",
         "//src/main/java/com/google/crypto/tink:key_template-android",
-        "//src/main/java/com/google/crypto/tink:keyset_reader-android",
         "//src/main/java/com/google/crypto/tink:keyset_writer-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/subtle:hex-android",
         "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/android/internal/FakeAndroidKeystoreProvider.java b/java_src/src/main/java/com/google/crypto/tink/integration/android/internal/FakeAndroidKeystoreProvider.java
new file mode 100644
index 0000000..1b3d821
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/android/internal/FakeAndroidKeystoreProvider.java
@@ -0,0 +1,450 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.integration.android.internal;
+
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import com.google.crypto.tink.subtle.Random;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.ProviderException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import javax.crypto.KeyGeneratorSpi;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Fake implementations of AndroidKeystore that provide all the function needed by Tink's android
+ * integration package.
+ */
+public final class FakeAndroidKeystoreProvider {
+
+  /** A partial fake implementation of KeyStoreSpi. */
+  public static class FakeKeyStoreSpi extends KeyStoreSpi {
+
+    public FakeKeyStoreSpi() {}
+
+    protected static HashMap<String, SecretKey> keys;
+
+    public static void setKeysMapRef(HashMap<String, SecretKey> keys) {
+      FakeKeyStoreSpi.keys = keys;
+    }
+
+    @Override
+    public boolean engineContainsAlias(String alias) {
+      return keys.containsKey(alias);
+    }
+
+    @Override
+    public Key engineGetKey(String keyId, char[] password)
+        throws NoSuchAlgorithmException, UnrecoverableKeyException {
+      return keys.get(keyId);
+    }
+
+    @Override
+    public void engineDeleteEntry(String alias) throws KeyStoreException {
+      keys.remove(alias);
+    }
+
+    @Override
+    public void engineLoad(KeyStore.LoadStoreParameter parameter)
+        throws CertificateException, NoSuchAlgorithmException, IOException {}
+
+    @Override
+    public void engineLoad(InputStream inputStream, char[] password)
+        throws IOException, NoSuchAlgorithmException, CertificateException {}
+
+    @Override
+    public Certificate[] engineGetCertificateChain(String alias) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Certificate engineGetCertificate(String s) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Date engineGetCreationDate(String s) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
+        throws KeyStoreException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void engineSetKeyEntry(String alias, byte[] keyBytes, Certificate[] chain)
+        throws KeyStoreException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void engineSetCertificateEntry(String alias, Certificate certificate)
+        throws KeyStoreException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Enumeration<String> engineAliases() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int engineSize() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean engineIsKeyEntry(String alias) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean engineIsCertificateEntry(String alias) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String engineGetCertificateAlias(Certificate certificate) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void engineStore(KeyStore.LoadStoreParameter parameter)
+        throws CertificateException, NoSuchAlgorithmException, IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void engineStore(OutputStream outputStream, char[] password)
+        throws IOException, NoSuchAlgorithmException, CertificateException {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  /** A partial fake implementation of KeyGeneratorSpi. */
+  public static class FakeKeyGeneratorSpi extends KeyGeneratorSpi {
+    public FakeKeyGeneratorSpi() {}
+
+    private KeyGenParameterSpec spec;
+
+    protected static HashMap<String, SecretKey> keys;
+
+    public static void setKeysMapRef(HashMap<String, SecretKey> keys) {
+      FakeKeyGeneratorSpi.keys = keys;
+    }
+
+    @Override
+    public SecretKey engineGenerateKey() {
+      SecretKey newKey = new SecretKeySpec(Random.randBytes(32), "AES");
+      keys.put(spec.getKeystoreAlias(), newKey);
+      return newKey;
+    }
+
+    @Override
+    public void engineInit(AlgorithmParameterSpec params, SecureRandom random) {
+      if (!(params instanceof KeyGenParameterSpec)) {
+        throw new UnsupportedOperationException("unsupported params");
+      }
+      KeyGenParameterSpec keyGenParams = (KeyGenParameterSpec) params;
+      if (keyGenParams.getPurposes()
+          != (KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)) {
+        throw new UnsupportedOperationException("unsupported purposes");
+      }
+      if (keyGenParams.getKeySize() != 256) {
+        throw new UnsupportedOperationException("unsupported key size");
+      }
+      if (keyGenParams.getBlockModes().length != 1) {
+        throw new UnsupportedOperationException("unsupported block modes length");
+      }
+      if (!keyGenParams.getBlockModes()[0].equals(KeyProperties.BLOCK_MODE_GCM)) {
+        throw new UnsupportedOperationException("unsupported block mode");
+      }
+      if (keyGenParams.getEncryptionPaddings().length != 1) {
+        throw new UnsupportedOperationException("unsupported encryption paddings length");
+      }
+      if (!keyGenParams.getEncryptionPaddings()[0].equals(KeyProperties.ENCRYPTION_PADDING_NONE)) {
+        throw new UnsupportedOperationException("unsupported encryption padding");
+      }
+      spec = keyGenParams;
+    }
+
+    @Override
+    public void engineInit(int keysize, SecureRandom random) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void engineInit(SecureRandom random) {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class FakeProvider extends Provider {
+    FakeProvider() {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore");
+
+      // The keys map is shared by FakeKeyStoreSpi and FakeKeyGeneratorSpi.
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeKeyGeneratorSpi.setKeysMapRef(keys);
+
+      put("KeyStore.AndroidKeyStore", FakeKeyStoreSpi.class.getName());
+      put("KeyGenerator.AES", FakeKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /** Returns a new fake Provider for AndroidKeystore. */
+  public static Provider newProvider() {
+    return new FakeProvider();
+  }
+
+  /**
+   * A fake KeyStoreSpi implementation that may raise a NullPointerException in engineContainsAlias.
+   *
+   * <p>This is added because of b/167402931.
+   */
+  public static class UnreliableFakeKeyStoreSpi
+      extends FakeAndroidKeystoreProvider.FakeKeyStoreSpi {
+
+    public UnreliableFakeKeyStoreSpi() {}
+
+    public static int failuresInARow;
+    public static int failuresLeft;
+
+    public static void setFailuresInARow(int failuresInARow) {
+      UnreliableFakeKeyStoreSpi.failuresInARow = failuresInARow;
+      failuresLeft = failuresInARow;
+    }
+
+    @Override
+    public boolean engineContainsAlias(String alias) {
+      if (failuresLeft > 0) {
+        failuresLeft = failuresLeft - 1;
+        throw new NullPointerException("something went wrong");
+      }
+      failuresLeft = failuresInARow;
+      return keys.containsKey(alias);
+    }
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class UnreliableFakeProvider extends Provider {
+    UnreliableFakeProvider(int failuresInARow) {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore with a bad containsAlias implementation");
+
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeAndroidKeystoreProvider.FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.setKeysMapRef(keys);
+      UnreliableFakeKeyStoreSpi.setFailuresInARow(failuresInARow);
+
+      this.setProperty("KeyStore.AndroidKeyStore", UnreliableFakeKeyStoreSpi.class.getName());
+      this.setProperty(
+          "KeyGenerator.AES", FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /** Returns a new fake Provider for AndroidKeystore. */
+  public static Provider newUnreliableProvider(int failuresInARow) {
+    return new UnreliableFakeProvider(failuresInARow);
+  }
+
+  /**
+   * A partial fake implementation of KeyStoreSpi where engineGetKey always throws an exception.
+   *
+   * <p>This is added because of b/151893419.
+   */
+  public static class FakeKeyStoreSpiWithUnrecoverableKeys extends FakeKeyStoreSpi {
+
+    public FakeKeyStoreSpiWithUnrecoverableKeys() {}
+
+    @Override
+    public Key engineGetKey(String keyId, char[] password)
+        throws NoSuchAlgorithmException, UnrecoverableKeyException {
+      Key key = super.engineGetKey(keyId, password);
+      if (key == null) {
+        return null;
+      }
+      throw new UnrecoverableKeyException("Failed to obtain information about key");
+    }
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class FakeProviderWithUnrecoverableKeys extends Provider {
+    FakeProviderWithUnrecoverableKeys() {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore that returns null keys");
+
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeAndroidKeystoreProvider.FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.setKeysMapRef(keys);
+
+      put("KeyStore.AndroidKeyStore", FakeKeyStoreSpiWithUnrecoverableKeys.class.getName());
+      put("KeyGenerator.AES", FakeKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /** Returns a new fake Provider for AndroidKeystore. */
+  public static Provider newProviderWithUnrecoverableKeys() {
+    return new FakeProviderWithUnrecoverableKeys();
+  }
+
+  /**
+   * A fake implementation of KeyStoreSpi where engineGetKey always throws a ProviderException.
+   *
+   * <p>If the key is stored in StrongBox, it is possible that a ProviderException is thrown if
+   * there's any problem with it.
+   */
+  public static class FakeKeyStoreSpiWithProviderException extends FakeKeyStoreSpi {
+
+    public FakeKeyStoreSpiWithProviderException() {}
+
+    @Override
+    public Key engineGetKey(String keyId, char[] password)
+        throws NoSuchAlgorithmException, UnrecoverableKeyException {
+      throw new ProviderException("Something is wrong with the provider");
+    }
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class FakeProviderWithProviderException extends Provider {
+    FakeProviderWithProviderException() {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore that throws ProviderException");
+
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeAndroidKeystoreProvider.FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.setKeysMapRef(keys);
+
+      put("KeyStore.AndroidKeyStore", FakeKeyStoreSpiWithProviderException.class.getName());
+      put("KeyGenerator.AES", FakeKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /** Returns a new fake provider where getKey always throws ProviderException. */
+  public static Provider newProviderWithProviderException() {
+    return new FakeProviderWithProviderException();
+  }
+
+  /** A fake implementation of KeyStoreSpi where engineGetKey always returns null. */
+  public static class FakeKeyStoreSpiWithNullKeys extends FakeKeyStoreSpi {
+
+    public FakeKeyStoreSpiWithNullKeys() {}
+
+    @Override
+    public Key engineGetKey(String keyId, char[] password)
+        throws NoSuchAlgorithmException, UnrecoverableKeyException {
+      return null;
+    }
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class FakeProviderWithNullKeys extends Provider {
+    FakeProviderWithNullKeys() {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore that returns null keys");
+
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeAndroidKeystoreProvider.FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.setKeysMapRef(keys);
+
+      put("KeyStore.AndroidKeyStore", FakeKeyStoreSpiWithNullKeys.class.getName());
+      put("KeyGenerator.AES", FakeKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /** Returns a new fake Provider for AndroidKeystore where getKey always returns null. */
+  public static Provider newProviderWithNullKeys() {
+    return new FakeProviderWithNullKeys();
+  }
+
+  /** An implementation of KeyGeneratorSpi that doesn't generate keys. */
+  public static class NoKeyGeneratorSpi extends FakeKeyGeneratorSpi {
+
+    public NoKeyGeneratorSpi() {}
+
+    @Override
+    public SecretKey engineGenerateKey() {
+      return null;
+    }
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class FakeProviderWithoutKeyGeneration extends Provider {
+    FakeProviderWithoutKeyGeneration() {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore that returns null keys");
+
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeAndroidKeystoreProvider.FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.setKeysMapRef(keys);
+
+      put("KeyStore.AndroidKeyStore", FakeKeyStoreSpiWithNullKeys.class.getName());
+      put("KeyGenerator.AES", NoKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /** Returns a new fake Provider for AndroidKeystore that doesn't generate keys. */
+  public static Provider newProviderWithoutKeyGeneration() {
+    return new FakeProviderWithoutKeyGeneration();
+  }
+
+  @SuppressWarnings(
+      "deprecation") // We need to use the old constructor to support older Java versions.
+  private static class BadProvider extends Provider {
+    BadProvider() {
+      super("AndroidKeyStore", 1.0, "Fake AndroidKeyStore that throws ProviderException");
+
+      HashMap<String, SecretKey> keys = new HashMap<>();
+      FakeAndroidKeystoreProvider.FakeKeyStoreSpi.setKeysMapRef(keys);
+      FakeAndroidKeystoreProvider.FakeKeyGeneratorSpi.setKeysMapRef(keys);
+
+      put("KeyStore.AndroidKeyStore", FakeKeyStoreSpiWithProviderException.class.getName());
+      put("KeyGenerator.AES", NoKeyGeneratorSpi.class.getName());
+    }
+  }
+
+  /**
+   * Returns a new fake Provider that doesn't generate keys and where getKey always throws
+   * ProviderException.
+   */
+  public static Provider newBadProvider() {
+    return new BadProvider();
+  }
+
+  private FakeAndroidKeystoreProvider() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/awskms/AwsKmsClient.java b/java_src/src/main/java/com/google/crypto/tink/integration/awskms/AwsKmsClient.java
index 0b6ca4e..452c480 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/awskms/AwsKmsClient.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/awskms/AwsKmsClient.java
@@ -28,11 +28,12 @@
 import com.google.crypto.tink.Aead;
 import com.google.crypto.tink.KmsClient;
 import com.google.crypto.tink.KmsClients;
-import com.google.crypto.tink.subtle.Validators;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.security.GeneralSecurityException;
 import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import javax.annotation.Nullable;
 
 /**
  * An implementation of {@link KmsClient} for <a href="https://aws.amazon.com/kms/">AWS KMS</a>.
@@ -44,23 +45,22 @@
   /** The prefix of all keys stored in AWS KMS. */
   public static final String PREFIX = "aws-kms://";
 
-  private String keyUri;
-  private AWSCredentialsProvider provider;
+  @Nullable private AWSKMS awsKms;
+  @Nullable private String keyUri;
+  @Nullable private AWSCredentialsProvider provider;
 
   /**
    * Constructs a generic AwsKmsClient that is not bound to any specific key.
    *
-   * @deprecated use {@link #register}
+   * This constructor should not be used. We recommend to register the client instead.
    */
-  @Deprecated
   public AwsKmsClient() {}
 
   /**
    * Constructs a specific AwsKmsClient that is bound to a single key identified by {@code uri}.
    *
-   * @deprecated use {@link #register}
+   * This constructor should not be used. We recommend to register the client instead.
    */
-  @Deprecated
   public AwsKmsClient(String uri) {
     if (!uri.toLowerCase(Locale.US).startsWith(PREFIX)) {
       throw new IllegalArgumentException("key URI must starts with " + PREFIX);
@@ -69,7 +69,7 @@
   }
 
   /**
-   * @return @return true either if this client is a generic one and uri starts with {@link
+   * @return true either if this client is a generic one and uri starts with {@link
    *     AwsKmsClient#PREFIX}, or the client is a specific one that is bound to the key identified
    *     by {@code uri}.
    */
@@ -90,6 +90,7 @@
    * @throws GeneralSecurityException if the client initialization fails
    */
   @Override
+  @CanIgnoreReturnValue
   public KmsClient withCredentials(String credentialPath) throws GeneralSecurityException {
     try {
       if (credentialPath == null) {
@@ -116,6 +117,7 @@
    * @throws GeneralSecurityException if the client initialization fails
    */
   @Override
+  @CanIgnoreReturnValue
   public KmsClient withDefaultCredentials() throws GeneralSecurityException {
     try {
       return withCredentialsProvider(new DefaultAWSCredentialsProviderChain());
@@ -125,12 +127,31 @@
   }
 
   /** Loads AWS credentials from a provider. */
+  @CanIgnoreReturnValue
   public KmsClient withCredentialsProvider(AWSCredentialsProvider provider)
       throws GeneralSecurityException {
     this.provider = provider;
     return this;
   }
 
+  /**
+   * Specifies the {@link com.amazonaws.services.kms.AWSKMS} object to be used. Only used for
+   * testing.
+   */
+  @CanIgnoreReturnValue
+  KmsClient withAwsKms(@Nullable AWSKMS awsKms) {
+    this.awsKms = awsKms;
+    return this;
+  }
+
+  private static String removePrefix(String expectedPrefix, String kmsKeyUri) {
+    if (!kmsKeyUri.toLowerCase(Locale.US).startsWith(expectedPrefix)) {
+      throw new IllegalArgumentException(
+          String.format("key URI must start with %s", expectedPrefix));
+    }
+    return kmsKeyUri.substring(expectedPrefix.length());
+  }
+
   @Override
   public Aead getAead(String uri) throws GeneralSecurityException {
     if (this.keyUri != null && !this.keyUri.equals(uri)) {
@@ -140,14 +161,21 @@
     }
 
     try {
-      String keyUri = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, uri);
-      List<String> tokens = Splitter.on(':').splitToList(keyUri);
-      AWSKMS client =
-          AWSKMSClientBuilder.standard()
-              .withCredentials(provider)
-              .withRegion(Regions.fromName(tokens.get(3)))
-              .build();
-      return new AwsKmsAead(client, keyUri);
+      String keyId = removePrefix(PREFIX, uri);
+      AWSKMS client = awsKms;
+      List<String> tokens = Splitter.on(':').splitToList(keyId);
+      if (tokens.size() < 4) {
+        throw new IllegalArgumentException("invalid key URI");
+      }
+      String regionName = tokens.get(3);
+      if (client == null) {
+        client =
+            AWSKMSClientBuilder.standard()
+                .withCredentials(provider)
+                .withRegion(Regions.fromName(regionName))
+                .build();
+      }
+      return new AwsKmsAead(client, keyId);
     } catch (AmazonServiceException e) {
       throw new GeneralSecurityException("cannot load credentials from provider", e);
     }
@@ -164,6 +192,16 @@
    */
   public static void register(Optional<String> keyUri, Optional<String> credentialPath)
       throws GeneralSecurityException {
+    registerWithAwsKms(keyUri, credentialPath, null);
+  }
+
+  /**
+   * Does the same as {@link #register}, but with an additional {@code awsKms} argument. Only used
+   * for testing.
+   */
+  static void registerWithAwsKms(
+      Optional<String> keyUri, Optional<String> credentialPath, @Nullable AWSKMS awsKms)
+      throws GeneralSecurityException {
     AwsKmsClient client;
     if (keyUri.isPresent()) {
       client = new AwsKmsClient(keyUri.get());
@@ -175,6 +213,7 @@
     } else {
       client.withDefaultCredentials();
     }
+    client.withAwsKms(awsKms);
     KmsClients.add(client);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/awskms/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/integration/awskms/BUILD.bazel
index 254703f..a166764 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/awskms/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/awskms/BUILD.bazel
@@ -16,22 +16,34 @@
 java_library(
     name = "aws_kms_client",
     srcs = ["AwsKmsClient.java"],
-    plugins = [
-        ":auto_service_plugin",
-    ],
+    plugins = [":auto_service_plugin"],
     deps = [
         ":aws_kms_aead",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:kms_client",
         "//src/main/java/com/google/crypto/tink:kms_clients",
-        "//src/main/java/com/google/crypto/tink/subtle:validators",
         "@maven//:com_amazonaws_aws_java_sdk_core",
         "@maven//:com_amazonaws_aws_java_sdk_kms",
         "@maven//:com_google_auto_service_auto_service_annotations",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_guava_guava",
     ],
 )
 
+java_library(
+    name = "fake_aws_kms",
+    testonly = 1,
+    srcs = ["FakeAwsKms.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@maven//:com_amazonaws_aws_java_sdk_core",
+        "@maven//:com_amazonaws_aws_java_sdk_kms",
+    ],
+)
+
 java_plugin(
     name = "auto_service_plugin",
     processor_class = "com.google.auto.service.processor.AutoServiceProcessor",
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/awskms/FakeAwsKms.java b/java_src/src/main/java/com/google/crypto/tink/integration/awskms/FakeAwsKms.java
new file mode 100644
index 0000000..8b466e9
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/awskms/FakeAwsKms.java
@@ -0,0 +1,96 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package com.google.crypto.tink.integration.awskms;
+
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.services.kms.AbstractAWSKMS;
+import com.amazonaws.services.kms.model.DecryptRequest;
+import com.amazonaws.services.kms.model.DecryptResult;
+import com.amazonaws.services.kms.model.EncryptRequest;
+import com.amazonaws.services.kms.model.EncryptResult;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A partial, fake implementation of AWSKMS that only supports encrypt and decrypt.
+ *
+ * <p>It creates a new AEAD for every valid key ID. It can encrypt message for these valid key IDs,
+ * but fails for all other key IDs. On decrypt, it tries out all its AEADs and returns the plaintext
+ * and the key ID of the AEAD that can successfully decrypt it.
+ */
+final class FakeAwsKms extends AbstractAWSKMS {
+  private static final Charset UTF_8 = Charset.forName("UTF-8");
+  private final Map<String, Aead> aeads = new HashMap<>();
+
+  private static byte[] serializeContext(Map<String, String> encryptionContext) {
+    TreeMap<String, String> ordered = new TreeMap<>(encryptionContext);
+    return ordered.toString().getBytes(UTF_8);
+  }
+
+  public FakeAwsKms(List<String> validKeyIds) throws GeneralSecurityException {
+    for (String keyId : validKeyIds) {
+      Aead aead = KeysetHandle.generateNew(KeyTemplates.get("AES128_GCM")).getPrimitive(Aead.class);
+      aeads.put(keyId, aead);
+    }
+  }
+
+  @Override
+  public EncryptResult encrypt(EncryptRequest request) {
+    if (!aeads.containsKey(request.getKeyId())) {
+      throw new AmazonServiceException(
+          "Unknown key ID : " + request.getKeyId() + " is not in " + aeads.keySet());
+    }
+    try {
+      Aead aead = aeads.get(request.getKeyId());
+      byte[] ciphertext =
+          aead.encrypt(
+              request.getPlaintext().array(), serializeContext(request.getEncryptionContext()));
+      return new EncryptResult()
+          .withKeyId(request.getKeyId())
+          .withCiphertextBlob(ByteBuffer.wrap(ciphertext));
+    } catch (GeneralSecurityException e) {
+      throw new AmazonServiceException(e.getMessage());
+    }
+  }
+
+  @Override
+  public DecryptResult decrypt(DecryptRequest request) {
+    for (Map.Entry<String, Aead> entry : aeads.entrySet()) {
+      try {
+        byte[] plaintext =
+            entry
+                .getValue()
+                .decrypt(
+                    request.getCiphertextBlob().array(),
+                    serializeContext(request.getEncryptionContext()));
+        return new DecryptResult()
+            .withKeyId(entry.getKey())
+            .withPlaintext(ByteBuffer.wrap(plaintext));
+      } catch (GeneralSecurityException e) {
+        // try next key
+      }
+    }
+    throw new AmazonServiceException("unable to decrypt");
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel
index de49309..3b719d0 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel
@@ -5,9 +5,7 @@
 java_library(
     name = "gcp_kms_client",
     srcs = ["GcpKmsClient.java"],
-    plugins = [
-        ":auto_service_plugin",
-    ],
+    plugins = [":auto_service_plugin"],
     deps = [
         ":gcp_kms_aead",
         "//src/main/java/com/google/crypto/tink:aead",
@@ -19,6 +17,8 @@
         "@maven//:com_google_apis_google_api_services_cloudkms",
         "@maven//:com_google_auth_google_auth_library_oauth2_http",
         "@maven//:com_google_auto_service_auto_service_annotations",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_http_client_google_http_client",
         "@maven//:com_google_http_client_google_http_client_gson",
         "@maven//:com_google_oauth_client_google_oauth_client",
@@ -42,3 +42,18 @@
         "@maven//:com_google_auto_service_auto_service",
     ],
 )
+
+java_library(
+    name = "fake_cloud_kms",
+    testonly = 1,
+    srcs = ["FakeCloudKms.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@maven//:com_google_api_client_google_api_client",
+        "@maven//:com_google_apis_google_api_services_cloudkms",
+        "@maven//:com_google_http_client_google_http_client",
+        "@maven//:com_google_http_client_google_http_client_gson",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/FakeCloudKms.java b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/FakeCloudKms.java
new file mode 100644
index 0000000..d55e7a9
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/FakeCloudKms.java
@@ -0,0 +1,169 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package com.google.crypto.tink.integration.gcpkms;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.services.cloudkms.v1.CloudKMS;
+import com.google.api.services.cloudkms.v1.model.DecryptRequest;
+import com.google.api.services.cloudkms.v1.model.DecryptResponse;
+import com.google.api.services.cloudkms.v1.model.EncryptRequest;
+import com.google.api.services.cloudkms.v1.model.EncryptResponse;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A partial, fake implementation of {@link com.google.api.services.cloudkms.v1.CloudKMS}.
+ *
+ * <p>It creates a new AEAD for every valid key ID. CryptoKeys use them to encrypt and decrypt.
+ */
+final class FakeCloudKms extends CloudKMS {
+  private final Map<String, Aead> aeads = new HashMap<>();
+
+  public FakeCloudKms(List<String> validKeyIds)
+      throws GeneralSecurityException {
+    super(
+        new HttpTransport() {
+          @Override
+          protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+            throw new IOException("Test should not have interacted with HttpTransport.");
+          }
+        },
+        new GsonFactory(),
+        new GoogleCredential());
+    for (String keyId : validKeyIds) {
+      Aead aead = KeysetHandle.generateNew(KeyTemplates.get("AES128_GCM")).getPrimitive(Aead.class);
+      aeads.put(keyId, aead);
+    }
+  }
+
+  private final Projects projects = new Projects();
+
+  @Override
+  public Projects projects() {
+    return projects;
+  }
+
+  final class Projects extends CloudKMS.Projects {
+
+    private final Locations locations = new Locations();
+
+    @Override
+    public Locations locations() {
+      return locations;
+    }
+
+    final class Locations extends CloudKMS.Projects.Locations {
+
+      private final KeyRings keyRings = new KeyRings();
+
+      @Override
+      public KeyRings keyRings() {
+        return keyRings;
+      }
+
+      final class KeyRings extends CloudKMS.Projects.Locations.KeyRings {
+
+        private final CryptoKeys cryptoKeys = new CryptoKeys();
+
+        @Override
+        public CryptoKeys cryptoKeys() {
+          return cryptoKeys;
+        }
+
+        final class CryptoKeys extends CloudKMS.Projects.Locations.KeyRings.CryptoKeys {
+          @Override
+          public Encrypt encrypt(String name, EncryptRequest request) {
+            return new Encrypt(name, request);
+          }
+
+          @Override
+          public Decrypt decrypt(String name, DecryptRequest request) {
+            return new Decrypt(name, request);
+          }
+
+          final class Encrypt extends CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Encrypt {
+            String name;
+            EncryptRequest request;
+
+            Encrypt(String name, EncryptRequest request) {
+              super(name, request);
+              this.name = name;
+              this.request = request;
+            }
+
+            @Override
+            public EncryptResponse execute() throws IOException {
+              if (!aeads.containsKey(name)) {
+                throw new IOException(
+                    "Unknown key ID : " + name + " is not in " + aeads.keySet());
+              }
+              try {
+                Aead aead = aeads.get(name);
+                byte[] ciphertext =
+                    aead.encrypt(
+                        request.decodePlaintext(), request.decodeAdditionalAuthenticatedData());
+                return new EncryptResponse().encodeCiphertext(ciphertext);
+              } catch (GeneralSecurityException e) {
+                throw new IOException(e.getMessage());
+              }
+            }
+          }
+
+          final class Decrypt extends CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Decrypt {
+            String name;
+            DecryptRequest request;
+
+            Decrypt(String name, DecryptRequest request) {
+              super(name, request);
+              this.name = name;
+              this.request = request;
+            }
+
+            @Override
+            public DecryptResponse execute() throws IOException {
+              if (!aeads.containsKey(name)) {
+                throw new IOException("Unknown key ID : " + name + " is not in " + aeads.keySet());
+              }
+              try {
+                Aead aead = aeads.get(name);
+                byte[] plaintext =
+                    aead.decrypt(
+                        request.decodeCiphertext(), request.decodeAdditionalAuthenticatedData());
+                if (plaintext.length == 0) {
+                  // The real CloudKMS also returns null in this case.
+                  return new DecryptResponse().encodePlaintext(null);
+                } else {
+                  return new DecryptResponse().encodePlaintext(plaintext);
+                }
+              } catch (GeneralSecurityException e) {
+                throw new IOException(e.getMessage());
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAead.java b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAead.java
index 91f575a..56127cb 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAead.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAead.java
@@ -41,48 +41,64 @@
   // The location of a CryptoKey in Google Cloud KMS.
   // Valid values have this format: projects/*/locations/*/keyRings/*/cryptoKeys/*.
   // See https://cloud.google.com/kms/docs/object-hierarchy.
-  private final String kmsKeyUri;
+  private final String keyName;
 
-  public GcpKmsAead(CloudKMS kmsClient, String keyUri) throws GeneralSecurityException {
+  public GcpKmsAead(CloudKMS kmsClient, String keyName) {
     this.kmsClient = kmsClient;
-    this.kmsKeyUri = keyUri;
+    this.keyName = keyName;
   }
 
   @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] aad) throws GeneralSecurityException {
+  public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
+      throws GeneralSecurityException {
     try {
       EncryptRequest request =
-          new EncryptRequest().encodePlaintext(plaintext).encodeAdditionalAuthenticatedData(aad);
+          new EncryptRequest()
+              .encodePlaintext(plaintext)
+              .encodeAdditionalAuthenticatedData(associatedData);
       EncryptResponse response =
           this.kmsClient
               .projects()
               .locations()
               .keyRings()
               .cryptoKeys()
-              .encrypt(this.kmsKeyUri, request)
+              .encrypt(this.keyName, request)
               .execute();
-      return response.decodeCiphertext();
+      return toNonNullableByteArray(response.decodeCiphertext());
     } catch (IOException e) {
       throw new GeneralSecurityException("encryption failed", e);
     }
   }
 
   @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] aad) throws GeneralSecurityException {
+  public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
+      throws GeneralSecurityException {
     try {
       DecryptRequest request =
-          new DecryptRequest().encodeCiphertext(ciphertext).encodeAdditionalAuthenticatedData(aad);
+          new DecryptRequest()
+              .encodeCiphertext(ciphertext)
+              .encodeAdditionalAuthenticatedData(associatedData);
       DecryptResponse response =
           this.kmsClient
               .projects()
               .locations()
               .keyRings()
               .cryptoKeys()
-              .decrypt(this.kmsKeyUri, request)
+              .decrypt(this.keyName, request)
               .execute();
-      return response.decodePlaintext();
+      return toNonNullableByteArray(response.decodePlaintext());
     } catch (IOException e) {
       throw new GeneralSecurityException("decryption failed", e);
     }
   }
+
+  private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+  private static byte[] toNonNullableByteArray(byte[] data) {
+    if (data == null) {
+      return EMPTY_BYTE_ARRAY;
+    } else {
+      return data;
+    }
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClient.java b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClient.java
index 632b279..5917aa4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClient.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClient.java
@@ -30,12 +30,14 @@
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.Version;
 import com.google.crypto.tink.subtle.Validators;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.Locale;
 import java.util.Optional;
+import javax.annotation.Nullable;
 
 /**
  * An implementation of {@code KmsClient} for <a href="https://cloud.google.com/kms/">Google Cloud
@@ -51,23 +53,21 @@
   private static final String APPLICATION_NAME =
       "Tink/" + Version.TINK_VERSION + " Java/" + System.getProperty("java.version");
 
-  private CloudKMS client;
-  private String keyUri;
+  @Nullable private CloudKMS cloudKms;
+  @Nullable private String keyUri;
 
   /**
    * Constructs a generic GcpKmsClient that is not bound to any specific key.
    *
-   * @deprecated use {@link #register}
+   * We recommend users to instead register this object by calling {@link #register}.
    */
-  @Deprecated
   public GcpKmsClient() {}
 
   /**
    * Constructs a specific GcpKmsClient that is bound to a single key identified by {@code uri}.
    *
-   * @deprecated use {@link register}
+   * We recommend users to instead register this object by calling {@link #register}.
    */
-  @Deprecated
   public GcpKmsClient(String uri) {
     if (!uri.toLowerCase(Locale.US).startsWith(PREFIX)) {
       throw new IllegalArgumentException("key URI must starts with " + PREFIX);
@@ -95,6 +95,7 @@
    * href="https://developers.google.com/accounts/docs/application-default-credentials" default
    * Google Cloud credentials</a>.
    */
+  @CanIgnoreReturnValue
   @Override
   public KmsClient withCredentials(String credentialPath) throws GeneralSecurityException {
     if (credentialPath == null) {
@@ -110,11 +111,12 @@
   }
 
   /** Loads the provided credential with {@code GoogleCredential}. */
+  @CanIgnoreReturnValue
   public KmsClient withCredentials(GoogleCredential credential) {
     if (credential.createScopedRequired()) {
       credential = credential.createScoped(CloudKMSScopes.all());
     }
-    this.client =
+    this.cloudKms =
         new CloudKMS.Builder(new NetHttpTransport(), new GsonFactory(), credential)
             .setApplicationName(APPLICATION_NAME)
             .build();
@@ -122,12 +124,13 @@
   }
 
   /** Loads the provided credentials with {@code GoogleCredentials}. */
+  @CanIgnoreReturnValue
   public KmsClient withCredentials(GoogleCredentials credentials) throws GeneralSecurityException {
     if (credentials.createScopedRequired()) {
       credentials = credentials.createScoped(CloudKMSScopes.all());
     }
     try {
-      this.client =
+      this.cloudKms =
           new CloudKMS.Builder(
                   GoogleNetHttpTransport.newTrustedTransport(),
                   new GsonFactory(),
@@ -144,6 +147,7 @@
    * Loads <a href="https://developers.google.com/accounts/docs/application-default-credentials"
    * default Google Cloud credentials</a>.
    */
+  @CanIgnoreReturnValue
   @Override
   public KmsClient withDefaultCredentials() throws GeneralSecurityException {
     try {
@@ -154,6 +158,16 @@
     }
   }
 
+  /**
+   * Specifies the {@link com.google.api.services.cloudkms.v1.CloudKMS} object to be used. Only used
+   * for testing.
+   */
+  @CanIgnoreReturnValue
+  KmsClient withCloudKms(CloudKMS cloudKms) {
+      this.cloudKms = cloudKms;
+      return this;
+  }
+
   @Override
   public Aead getAead(String uri) throws GeneralSecurityException {
     if (this.keyUri != null && !this.keyUri.equals(uri)) {
@@ -161,7 +175,7 @@
           String.format("this client is bound to %s, cannot load keys bound to %s",
               this.keyUri, uri));
     }
-    return new GcpKmsAead(client, Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, uri));
+    return new GcpKmsAead(cloudKms, Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, uri));
   }
 
   /**
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/internal/BUILD.bazel
index 9556ed6..91beaec 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/BUILD.bazel
@@ -11,9 +11,13 @@
     srcs = ["KeyTemplateProtoConverter.java"],
     visibility = ["//visibility:public"],
     deps = [
+        ":legacy_proto_parameters",
+        ":mutable_serialization_registry",
+        ":proto_parameters_serialization",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:key_template",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -66,7 +70,7 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -79,7 +83,7 @@
         "//src/main/java/com/google/crypto/tink:key_template-android",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -90,7 +94,7 @@
         ":key_type_manager",
         ":primitive_factory",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -101,7 +105,7 @@
         ":key_type_manager-android",
         ":primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -111,6 +115,7 @@
     srcs = ["KeyTester.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink:key",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_truth_truth",
     ],
 )
@@ -121,6 +126,7 @@
     srcs = ["KeyTester.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink:key-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_truth_truth",
     ],
 )
@@ -128,22 +134,26 @@
 java_library(
     name = "primitive_factory",
     srcs = ["PrimitiveFactory.java"],
-    deps = ["@com_google_protobuf//:protobuf_javalite"],
+    deps = ["@maven//:com_google_protobuf_protobuf_java"],
 )
 
 android_library(
     name = "primitive_factory-android",
     srcs = ["PrimitiveFactory.java"],
-    deps = ["@com_google_protobuf//:protobuf_javalite"],
+    deps = ["@maven//:com_google_protobuf_protobuf_javalite"],
 )
 
 android_library(
     name = "key_template_proto_converter-android",
     srcs = ["KeyTemplateProtoConverter.java"],
     deps = [
+        ":legacy_proto_parameters-android",
+        ":mutable_serialization_registry-android",
+        ":proto_parameters_serialization-android",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:key_template-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -219,8 +229,8 @@
         ":util",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/util:bytes",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -232,9 +242,9 @@
         ":util",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/util:bytes",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -246,8 +256,8 @@
         ":util-android",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -259,9 +269,9 @@
         ":util-android",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -279,6 +289,7 @@
         "//src/main/java/com/google/crypto/tink:secret_key_access",
         "//src/main/java/com/google/crypto/tink/util:bytes",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -296,6 +307,7 @@
         "//src/main/java/com/google/crypto/tink:secret_key_access-android",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -494,3 +506,235 @@
     srcs = ["BuildDispatchedCode.java"],
     deps = ["@maven//:com_google_code_findbugs_jsr305"],
 )
+
+java_library(
+    name = "json_parser",
+    srcs = ["JsonParser.java"],
+    deps = [
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_code_gson_gson",
+    ],
+)
+
+android_library(
+    name = "json_parser-android",
+    srcs = ["JsonParser.java"],
+    deps = [
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_code_gson_gson",
+    ],
+)
+
+java_library(
+    name = "primitive_registry",
+    srcs = ["PrimitiveRegistry.java"],
+    deps = [
+        ":primitive_constructor",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "primitive_registry-android",
+    srcs = ["PrimitiveRegistry.java"],
+    deps = [
+        ":primitive_constructor-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:primitive_set-android",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "primitive_constructor",
+    srcs = ["PrimitiveConstructor.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:key"],
+)
+
+android_library(
+    name = "primitive_constructor-android",
+    srcs = ["PrimitiveConstructor.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:key-android"],
+)
+
+android_library(
+    name = "mutable_primitive_registry-android",
+    srcs = ["MutablePrimitiveRegistry.java"],
+    deps = [
+        ":primitive_constructor-android",
+        ":primitive_registry-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:primitive_set-android",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
+    ],
+)
+
+java_library(
+    name = "mutable_primitive_registry",
+    srcs = ["MutablePrimitiveRegistry.java"],
+    deps = [
+        ":primitive_constructor",
+        ":primitive_registry",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+    ],
+)
+
+java_library(
+    name = "elliptic_curves_util",
+    srcs = ["EllipticCurvesUtil.java"],
+    deps = ["//src/main/java/com/google/crypto/tink/subtle:random"],
+)
+
+android_library(
+    name = "elliptic_curves_util-android",
+    srcs = ["EllipticCurvesUtil.java"],
+    deps = ["//src/main/java/com/google/crypto/tink/subtle:random-android"],
+)
+
+java_library(
+    name = "big_integer_encoding",
+    srcs = ["BigIntegerEncoding.java"],
+)
+
+android_library(
+    name = "big_integer_encoding-android",
+    srcs = ["BigIntegerEncoding.java"],
+)
+
+java_library(
+    name = "curve25519",
+    srcs = ["Curve25519.java"],
+    deps = [
+        ":field25519",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+    ],
+)
+
+android_library(
+    name = "curve25519-android",
+    srcs = ["Curve25519.java"],
+    deps = [
+        ":field25519-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
+        "//src/main/java/com/google/crypto/tink/subtle:hex-android",
+    ],
+)
+
+java_library(
+    name = "ed25519_cluster",
+    srcs = [
+        "Ed25519.java",
+        "Ed25519Constants.java",
+    ],
+    deps = [
+        ":curve25519",
+        ":field25519",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ed25519_cluster-android",
+    srcs = [
+        "Ed25519.java",
+        "Ed25519Constants.java",
+    ],
+    deps = [
+        ":curve25519-android",
+        ":field25519-android",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "field25519",
+    srcs = ["Field25519.java"],
+    deps = ["//src/main/java/com/google/crypto/tink/annotations:alpha"],
+)
+
+android_library(
+    name = "field25519-android",
+    srcs = ["Field25519.java"],
+    deps = ["//src/main/java/com/google/crypto/tink/annotations:alpha-android"],
+)
+
+java_library(
+    name = "enum_type_proto_converter",
+    srcs = ["EnumTypeProtoConverter.java"],
+    deps = ["@maven//:com_google_errorprone_error_prone_annotations"],
+)
+
+android_library(
+    name = "enum_type_proto_converter-android",
+    srcs = ["EnumTypeProtoConverter.java"],
+    deps = ["@maven//:com_google_errorprone_error_prone_annotations"],
+)
+
+java_library(
+    name = "internal_configuration",
+    srcs = ["InternalConfiguration.java"],
+    deps = [
+        ":primitive_registry",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:configuration",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+    ],
+)
+
+android_library(
+    name = "internal_configuration-android",
+    srcs = ["InternalConfiguration.java"],
+    deps = [
+        ":primitive_registry-android",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:configuration-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:primitive_set-android",
+    ],
+)
+
+android_library(
+    name = "registry_configuration-android",
+    srcs = ["RegistryConfiguration.java"],
+    deps = [
+        ":internal_configuration-android",
+        ":mutable_primitive_registry-android",
+        ":primitive_registry-android",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:primitive_set-android",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "registry_configuration",
+    srcs = ["RegistryConfiguration.java"],
+    deps = [
+        ":internal_configuration",
+        ":mutable_primitive_registry",
+        ":primitive_registry",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/BigIntegerEncoding.java b/java_src/src/main/java/com/google/crypto/tink/internal/BigIntegerEncoding.java
new file mode 100644
index 0000000..30c0047
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/BigIntegerEncoding.java
@@ -0,0 +1,88 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * Helper class with functions that encode and decode non-negative {@link java.math.BigInteger} to
+ * and from {@code byte[]}.
+ */
+public final class BigIntegerEncoding {
+
+  /**
+   * Encodes a non-negative {@link java.math.BigInteger} into the minimal two's-complement
+   * representation in big-endian byte-order.
+   *
+   * <p>The most significant bit of the first byte is the sign bit, which is always 0 because the
+   * input number is non-negative. Because of that, the output is at the same time also an unsigned
+   * big-endian encoding that may have an additional zero byte at the beginning, and can be parsed
+   * with {@link #fromUnsignedBigEndianBytes}.
+   */
+  public static byte[] toBigEndianBytes(BigInteger n) {
+    if (n.signum() == -1) {
+      throw new IllegalArgumentException("n must not be negative");
+    }
+    return n.toByteArray();
+  }
+
+  /**
+   * Encodes a non-negative {@link java.math.BigInteger} into a byte array of a specified length,
+   * using big-endian byte-order.
+   *
+   * <p>See also <a href="https://www.rfc-editor.org/rfc/rfc8017#section-4.2">RFC 8017, Sec. 4.2</a>
+   *
+   * <p>throws a GeneralSecurityException if the number is negative or length is too short.
+   */
+  public static byte[] toBigEndianBytesOfFixedLength(BigInteger n, int length)
+      throws GeneralSecurityException {
+    if (n.signum() == -1) {
+      throw new IllegalArgumentException("integer must be nonnegative");
+    }
+    byte[] b = n.toByteArray();
+    if (b.length == length) {
+      return b;
+    }
+    if (b.length > length + 1 /* potential leading zero */) {
+      throw new GeneralSecurityException("integer too large");
+    }
+    if (b.length == length + 1) {
+      if (b[0] == 0 /* leading zero */) {
+        return Arrays.copyOfRange(b, 1, b.length);
+      } else {
+        throw new GeneralSecurityException("integer too large");
+      }
+    }
+    // Left zero pad b.
+    byte[] res = new byte[length];
+    System.arraycopy(b, 0, res, length - b.length, b.length);
+    return res;
+  }
+
+  /**
+   * Parses a {@link BigInteger} from a byte array using unsigned big-endian encoding.
+   *
+   * <p>See also <a href="https://www.rfc-editor.org/rfc/rfc8017#section-4.2">RFC 8017, Sec. 4.2</a>
+   */
+  public static BigInteger fromUnsignedBigEndianBytes(byte[] bytes) {
+    return new BigInteger(1, bytes);
+  }
+
+  private BigIntegerEncoding() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/Curve25519.java b/java_src/src/main/java/com/google/crypto/tink/internal/Curve25519.java
new file mode 100644
index 0000000..7c1ca5b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/Curve25519.java
@@ -0,0 +1,434 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+
+/**
+ * This class implements point arithmetic on the elliptic curve <a
+ * href="https://cr.yp.to/ecdh/curve25519-20060209.pdf">Curve25519</a>.
+ *
+ * <p>This class only implements point arithmetic, if you want to use the ECDH Curve25519 function,
+ * please checkout {@link com.google.crypto.tink.subtle.X25519}.
+ *
+ * <p>This implementation is based on <a
+ * href="https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c">curve255-donna C
+ * implementation</a>.
+ */
+@Alpha
+public final class Curve25519 {
+  // https://cr.yp.to/ecdh.html#validate doesn't recommend validating peer's public key. However,
+  // validating public key doesn't harm security and in certain cases, prevents unwanted edge
+  // cases.
+  // As we clear the most significant bit of peer's public key, we don't have to include public keys
+  // that are larger than 2^255.
+  static final byte[][] BANNED_PUBLIC_KEYS =
+      new byte[][] {
+        // 0
+        new byte[] {
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
+        },
+        // 1
+        new byte[] {
+          (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        },
+        // 325606250916557431795983626356110631294008115727848805560023387167927233504
+        new byte[] {
+          (byte) 0xe0, (byte) 0xeb, (byte) 0x7a, (byte) 0x7c,
+          (byte) 0x3b, (byte) 0x41, (byte) 0xb8, (byte) 0xae,
+          (byte) 0x16, (byte) 0x56, (byte) 0xe3, (byte) 0xfa,
+          (byte) 0xf1, (byte) 0x9f, (byte) 0xc4, (byte) 0x6a,
+          (byte) 0xda, (byte) 0x09, (byte) 0x8d, (byte) 0xeb,
+          (byte) 0x9c, (byte) 0x32, (byte) 0xb1, (byte) 0xfd,
+          (byte) 0x86, (byte) 0x62, (byte) 0x05, (byte) 0x16,
+          (byte) 0x5f, (byte) 0x49, (byte) 0xb8, (byte) 0x00,
+        },
+        // 39382357235489614581723060781553021112529911719440698176882885853963445705823
+        new byte[] {
+          (byte) 0x5f, (byte) 0x9c, (byte) 0x95, (byte) 0xbc,
+          (byte) 0xa3, (byte) 0x50, (byte) 0x8c, (byte) 0x24,
+          (byte) 0xb1, (byte) 0xd0, (byte) 0xb1, (byte) 0x55,
+          (byte) 0x9c, (byte) 0x83, (byte) 0xef, (byte) 0x5b,
+          (byte) 0x04, (byte) 0x44, (byte) 0x5c, (byte) 0xc4,
+          (byte) 0x58, (byte) 0x1c, (byte) 0x8e, (byte) 0x86,
+          (byte) 0xd8, (byte) 0x22, (byte) 0x4e, (byte) 0xdd,
+          (byte) 0xd0, (byte) 0x9f, (byte) 0x11, (byte) 0x57
+        },
+        // 2^255 - 19 - 1
+        new byte[] {
+          (byte) 0xec, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f,
+        },
+        // 2^255 - 19
+        new byte[] {
+          (byte) 0xed, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
+        },
+        // 2^255 - 19 + 1
+        new byte[] {
+          (byte) 0xee, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
+        }
+      };
+  /**
+   * Computes Montgomery's double-and-add formulas.
+   *
+   * <p>On entry and exit, the absolute value of the limbs of all inputs and outputs are < 2^26.
+   *
+   * @param x2 x projective coordinate of output 2Q, long form
+   * @param z2 z projective coordinate of output 2Q, long form
+   * @param x3 x projective coordinate of output Q + Q', long form
+   * @param z3 z projective coordinate of output Q + Q', long form
+   * @param x x projective coordinate of input Q, short form, destroyed
+   * @param z z projective coordinate of input Q, short form, destroyed
+   * @param xprime x projective coordinate of input Q', short form, destroyed
+   * @param zprime z projective coordinate of input Q', short form, destroyed
+   * @param qmqp input Q - Q', short form, preserved
+   */
+  private static void monty(
+      long[] x2,
+      long[] z2,
+      long[] x3,
+      long[] z3,
+      long[] x,
+      long[] z,
+      long[] xprime,
+      long[] zprime,
+      long[] qmqp) {
+    long[] origx = Arrays.copyOf(x, Field25519.LIMB_CNT);
+    long[] zzz = new long[19];
+    long[] xx = new long[19];
+    long[] zz = new long[19];
+    long[] xxprime = new long[19];
+    long[] zzprime = new long[19];
+    long[] zzzprime = new long[19];
+    long[] xxxprime = new long[19];
+
+    Field25519.sum(x, z);
+    // |x[i]| < 2^27
+    Field25519.sub(z, origx); // does x - z
+    // |z[i]| < 2^27
+
+    long[] origxprime = Arrays.copyOf(xprime, Field25519.LIMB_CNT);
+    Field25519.sum(xprime, zprime);
+    // |xprime[i]| < 2^27
+    Field25519.sub(zprime, origxprime);
+    // |zprime[i]| < 2^27
+    Field25519.product(xxprime, xprime, z);
+    // |xxprime[i]| < 14*2^54: the largest product of two limbs will be < 2^(27+27) and {@ref
+    // Field25519#product} adds together, at most, 14 of those products. (Approximating that to
+    // 2^58 doesn't work out.)
+    Field25519.product(zzprime, x, zprime);
+    // |zzprime[i]| < 14*2^54
+    Field25519.reduceSizeByModularReduction(xxprime);
+    Field25519.reduceCoefficients(xxprime);
+    // |xxprime[i]| < 2^26
+    Field25519.reduceSizeByModularReduction(zzprime);
+    Field25519.reduceCoefficients(zzprime);
+    // |zzprime[i]| < 2^26
+    System.arraycopy(xxprime, 0, origxprime, 0, Field25519.LIMB_CNT);
+    Field25519.sum(xxprime, zzprime);
+    // |xxprime[i]| < 2^27
+    Field25519.sub(zzprime, origxprime);
+    // |zzprime[i]| < 2^27
+    Field25519.square(xxxprime, xxprime);
+    // |xxxprime[i]| < 2^26
+    Field25519.square(zzzprime, zzprime);
+    // |zzzprime[i]| < 2^26
+    Field25519.product(zzprime, zzzprime, qmqp);
+    // |zzprime[i]| < 14*2^52
+    Field25519.reduceSizeByModularReduction(zzprime);
+    Field25519.reduceCoefficients(zzprime);
+    // |zzprime[i]| < 2^26
+    System.arraycopy(xxxprime, 0, x3, 0, Field25519.LIMB_CNT);
+    System.arraycopy(zzprime, 0, z3, 0, Field25519.LIMB_CNT);
+
+    Field25519.square(xx, x);
+    // |xx[i]| < 2^26
+    Field25519.square(zz, z);
+    // |zz[i]| < 2^26
+    Field25519.product(x2, xx, zz);
+    // |x2[i]| < 14*2^52
+    Field25519.reduceSizeByModularReduction(x2);
+    Field25519.reduceCoefficients(x2);
+    // |x2[i]| < 2^26
+    Field25519.sub(zz, xx); // does zz = xx - zz
+    // |zz[i]| < 2^27
+    Arrays.fill(zzz, Field25519.LIMB_CNT, zzz.length - 1, 0);
+    Field25519.scalarProduct(zzz, zz, 121665);
+    // |zzz[i]| < 2^(27+17)
+    // No need to call reduceSizeByModularReduction here: scalarProduct doesn't increase the degree
+    // of its input.
+    Field25519.reduceCoefficients(zzz);
+    // |zzz[i]| < 2^26
+    Field25519.sum(zzz, xx);
+    // |zzz[i]| < 2^27
+    Field25519.product(z2, zz, zzz);
+    // |z2[i]| < 14*2^(26+27)
+    Field25519.reduceSizeByModularReduction(z2);
+    Field25519.reduceCoefficients(z2);
+    // |z2|i| < 2^26
+  }
+
+  /**
+   * Conditionally swap two reduced-form limb arrays if {@code iswap} is 1, but leave them unchanged
+   * if {@code iswap} is 0. Runs in data-invariant time to avoid side-channel attacks.
+   *
+   * <p>NOTE that this function requires that {@code iswap} be 1 or 0; other values give wrong
+   * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
+   * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
+   * have magnitude less than Integer.MAX_VALUE.
+   */
+  static void swapConditional(long[] a, long[] b, int iswap) {
+    int swap = -iswap;
+    for (int i = 0; i < Field25519.LIMB_CNT; i++) {
+      int x = swap & (((int) a[i]) ^ ((int) b[i]));
+      a[i] = ((int) a[i]) ^ x;
+      b[i] = ((int) b[i]) ^ x;
+    }
+  }
+
+  /**
+   * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1,
+   * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
+   * side-channel attacks.
+   *
+   * <p>NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong
+   * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
+   * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
+   * have magnitude less than Integer.MAX_VALUE.
+   */
+  static void copyConditional(long[] a, long[] b, int icopy) {
+    int copy = -icopy;
+    for (int i = 0; i < Field25519.LIMB_CNT; i++) {
+      int x = copy & (((int) a[i]) ^ ((int) b[i]));
+      a[i] = ((int) a[i]) ^ x;
+    }
+  }
+
+  /**
+   * Calculates nQ where Q is the x-coordinate of a point on the curve.
+   *
+   * @param resultx the x projective coordinate of the resulting curve point (short form).
+   * @param n a little endian, 32-byte number.
+   * @param qBytes a little endian, 32-byte number representing the public point' x coordinate.
+   * @throws InvalidKeyException iff the public key is in the banned list or its length is not
+   *     32-byte.
+   * @throws IllegalStateException iff there is arithmetic error.
+   */
+  public static void curveMult(long[] resultx, byte[] n, byte[] qBytes) throws InvalidKeyException {
+    byte[] qBytesWithoutMsb = validatePubKeyAndClearMsb(qBytes);
+
+    long[] q = Field25519.expand(qBytesWithoutMsb);
+    long[] nqpqx = new long[19];
+    long[] nqpqz = new long[19];
+    nqpqz[0] = 1;
+    long[] nqx = new long[19];
+    nqx[0] = 1;
+    long[] nqz = new long[19];
+    long[] nqpqx2 = new long[19];
+    long[] nqpqz2 = new long[19];
+    nqpqz2[0] = 1;
+    long[] nqx2 = new long[19];
+    long[] nqz2 = new long[19];
+    nqz2[0] = 1;
+    long[] t = new long[19];
+
+    System.arraycopy(q, 0, nqpqx, 0, Field25519.LIMB_CNT);
+
+    for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+      int b = n[Field25519.FIELD_LEN - i - 1] & 0xff;
+      for (int j = 0; j < 8; j++) {
+        int bit = (b >> (7 - j)) & 1;
+
+        swapConditional(nqx, nqpqx, bit);
+        swapConditional(nqz, nqpqz, bit);
+        monty(nqx2, nqz2, nqpqx2, nqpqz2, nqx, nqz, nqpqx, nqpqz, q);
+        swapConditional(nqx2, nqpqx2, bit);
+        swapConditional(nqz2, nqpqz2, bit);
+
+        t = nqx;
+        nqx = nqx2;
+        nqx2 = t;
+        t = nqz;
+        nqz = nqz2;
+        nqz2 = t;
+        t = nqpqx;
+        nqpqx = nqpqx2;
+        nqpqx2 = t;
+        t = nqpqz;
+        nqpqz = nqpqz2;
+        nqpqz2 = t;
+      }
+    }
+
+    // Computes nqx/nqz.
+    long[] zmone = new long[Field25519.LIMB_CNT];
+    Field25519.inverse(zmone, nqz);
+    Field25519.mult(resultx, nqx, zmone);
+
+    // Nowadays it should be standard to protect public key crypto against flaws. I.e. if there is a
+    // computation error through a faulty CPU or if the implementation contains a bug, then if
+    // possible this should be detected at run time.
+    //
+    // The situation is a bit more tricky for X25519 where for example the implementation
+    // proposed in https://tools.ietf.org/html/rfc7748 only uses the x-coordinate. However, a
+    // verification is still possible, but depends on the actual computation.
+    //
+    // Tink's Java implementation is equivalent to RFC7748. We will use the loop invariant in the
+    // Montgomery ladder to detect fault computation. In particular, we use the following invariant:
+    // q, resultx, nqpqx/nqpqx  are x coordinates of 3 collinear points q, n*q, (n + 1)*q.
+    if (!isCollinear(q, resultx, nqpqx, nqpqz)) {
+      throw new IllegalStateException(
+          "Arithmetic error in curve multiplication with the public key: " + Hex.encode(qBytes));
+    }
+  }
+
+  /**
+   * Validates public key and clear its most significant bit.
+   *
+   * @throws InvalidKeyException iff the {@code pubKey} is in the banned list or its length is not
+   *     32-byte.
+   */
+  private static byte[] validatePubKeyAndClearMsb(byte[] pubKey) throws InvalidKeyException {
+    if (pubKey.length != 32) {
+      throw new InvalidKeyException("Public key length is not 32-byte");
+    }
+    // Clears the most significant bit as in the method decodeUCoordinate() of RFC7748.
+    byte[] pubKeyWithoutMsb = Arrays.copyOf(pubKey, pubKey.length);
+    pubKeyWithoutMsb[31] &= (byte) 0x7f;
+
+    for (int i = 0; i < BANNED_PUBLIC_KEYS.length; i++) {
+      if (Bytes.equal(BANNED_PUBLIC_KEYS[i], pubKeyWithoutMsb)) {
+        throw new InvalidKeyException("Banned public key: " + Hex.encode(BANNED_PUBLIC_KEYS[i]));
+      }
+    }
+    return pubKeyWithoutMsb;
+  }
+
+  /**
+   * Checks whether there are three collinear points with x coordinate x1, x2, x3/z3.
+   *
+   * @return true if three collinear points with x coordianate x1, x2, x3/z3 are collinear.
+   */
+  private static boolean isCollinear(long[] x1, long[] x2, long[] x3, long[] z3) {
+    // If x1, x2, x3 (in this method x3 is represented as x3/z3) are the x-coordinates of three
+    // collinear points on a curve, then they satisfy the equation
+    //   y^2 = x^3 + ax^2 + x
+    // They also satisfy the equation
+    //   0 = (x - x1)(x - x2)(x - x3)
+    //     = x^3 + Ax^2 + Bx + C
+    // where
+    //   A = - x1 - x2 - x3
+    //   B = x1*x2 + x2*x3 + x3*x1
+    //   C = - x1*x2*x3
+    // Hence, the three points also satisfy
+    //   y^2 = (a - A)x^2 + (1 - B)x - C
+    // This is a quadratic curve. Three distinct collinear points can only be on a quadratic
+    // curve if the quadratic curve has a line as component. And if a quadratic curve has a line
+    // as component then its discriminant is 0.
+    // Therefore, discriminant((a - A)x^2 + (1-B)x - C) = 0.
+    // In particular:
+    //   a = 486662
+    //   lhs = 4 * ((x1 + x2 + a) * z3 + x3) * (x1 * x2 * x3)
+    //   rhs = ((x1 * x2 - 1) * z3 + x3 * (x1 + x2))**2
+    //   assert (lhs - rhs)  == 0
+    //
+    // There are 2 cases that we haven't discussed:
+    //
+    //   * If x1 and x2 are both points with y-coordinate 0 then the argument doesn't hold.
+    //   However, our ECDH computation doesn't allow points of low order (see {@code
+    //   validatePublicKey}). Therefore, this edge case never happen.
+    //
+    //   * x1, x2 or x3/y3 may be points on the twist. If so, they satisfy the equation
+    //     2y^2 = x^3 + ax^2 + x
+    //   Hence, the three points also satisfy
+    //     2y^2 = (a - A)x^2 + (1 - B)x - C
+    //   Thus, this collinear check should work for this case too.
+    long[] x1multx2 = new long[Field25519.LIMB_CNT];
+    long[] x1addx2 = new long[Field25519.LIMB_CNT];
+    long[] lhs = new long[Field25519.LIMB_CNT + 1];
+    long[] t = new long[Field25519.LIMB_CNT + 1];
+    long[] t2 = new long[Field25519.LIMB_CNT + 1];
+    Field25519.mult(x1multx2, x1, x2);
+    Field25519.sum(x1addx2, x1, x2);
+    long[] a = new long[Field25519.LIMB_CNT];
+    a[0] = 486662;
+    // t = x1 + x2 + a
+    Field25519.sum(t, x1addx2, a);
+    // t = (x1 + x2 + a) * z3
+    Field25519.mult(t, t, z3);
+    // t = (x1 + x2 + a) * z3 + x3
+    Field25519.sum(t, x3);
+    // t = ((x1 + x2 + a) * z3 + x3) * x1 * x2
+    Field25519.mult(t, t, x1multx2);
+    // t = ((x1 + x2 + a) * z3 + x3) * (x1 * x2 * x3)
+    Field25519.mult(t, t, x3);
+    Field25519.scalarProduct(lhs, t, 4);
+    Field25519.reduceCoefficients(lhs);
+
+    // t = x1 * x2 * z3
+    Field25519.mult(t, x1multx2, z3);
+    // t = x1 * x2 * z3 - z3
+    Field25519.sub(t, t, z3);
+    // t2 = (x1 + x2) * x3
+    Field25519.mult(t2, x1addx2, x3);
+    // t = x1 * x2 * z3 - z3 + (x1 + x2) * x3
+    Field25519.sum(t, t, t2);
+    // t = (x1 * x2 * z3 - z3 + (x1 + x2) * x3)^2
+    Field25519.square(t, t);
+    return Bytes.equal(Field25519.contract(lhs), Field25519.contract(t));
+  }
+
+  private Curve25519() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/Ed25519.java b/java_src/src/main/java/com/google/crypto/tink/internal/Ed25519.java
new file mode 100644
index 0000000..19d2fda
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/Ed25519.java
@@ -0,0 +1,1634 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.EngineFactory;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * This implementation is based on the ed25519/ref10 implementation in NaCl.
+ *
+ * <p>It implements this twisted Edwards curve:
+ *
+ * <pre>
+ * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
+ * </pre>
+ *
+ * @see <a href="https://eprint.iacr.org/2008/013.pdf">Bernstein D.J., Birkner P., Joye M., Lange
+ *     T., Peters C. (2008) Twisted Edwards Curves</a>
+ * @see <a href="https://eprint.iacr.org/2008/522.pdf">Hisil H., Wong K.KH., Carter G., Dawson E.
+ *     (2008) Twisted Edwards Curves Revisited</a>
+ */
+public final class Ed25519 {
+
+  public static final int SECRET_KEY_LEN = Field25519.FIELD_LEN;
+  public static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN;
+  public static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2;
+
+  // (x = 0, y = 1) point
+  private static final CachedXYT CACHED_NEUTRAL = new CachedXYT(
+      new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+      new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+      new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+  private static final PartialXYZT NEUTRAL = new PartialXYZT(
+      new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+          new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+          new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
+      new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+
+  /**
+   * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z
+   *
+   * Note that this is referred as ge_p2 in ref10 impl.
+   * Also note that x = X, y = Y and z = Z below following Java coding style.
+   *
+   * See
+   * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary
+   * Window Method.
+   *
+   * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html
+   */
+  private static class XYZ {
+
+    final long[] x;
+    final long[] y;
+    final long[] z;
+
+    XYZ() {
+      this(
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT]);
+    }
+
+    XYZ(long[] x, long[] y, long[] z) {
+      this.x = x;
+      this.y = y;
+      this.z = z;
+    }
+
+    XYZ(XYZ xyz) {
+      x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT);
+      y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT);
+      z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT);
+    }
+
+    XYZ(PartialXYZT partialXYZT) {
+      this();
+      fromPartialXYZT(this, partialXYZT);
+    }
+
+    /**
+     * ge_p1p1_to_p2.c
+     */
+    @CanIgnoreReturnValue
+    static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) {
+      Field25519.mult(out.x, in.xyz.x, in.t);
+      Field25519.mult(out.y, in.xyz.y, in.xyz.z);
+      Field25519.mult(out.z, in.xyz.z, in.t);
+      return out;
+    }
+
+    /**
+     * Encodes this point to bytes.
+     */
+    byte[] toBytes() {
+      long[] recip = new long[Field25519.LIMB_CNT];
+      long[] x = new long[Field25519.LIMB_CNT];
+      long[] y = new long[Field25519.LIMB_CNT];
+      Field25519.inverse(recip, z);
+      Field25519.mult(x, this.x, recip);
+      Field25519.mult(y, this.y, recip);
+      byte[] s = Field25519.contract(y);
+      s[31] = (byte) (s[31] ^ (getLsb(x) << 7));
+      return s;
+    }
+
+    /** Checks that the point is on curve */
+    boolean isOnCurve() {
+      long[] x2 = new long[Field25519.LIMB_CNT];
+      Field25519.square(x2, x);
+      long[] y2 = new long[Field25519.LIMB_CNT];
+      Field25519.square(y2, y);
+      long[] z2 = new long[Field25519.LIMB_CNT];
+      Field25519.square(z2, z);
+      long[] z4 = new long[Field25519.LIMB_CNT];
+      Field25519.square(z4, z2);
+      long[] lhs = new long[Field25519.LIMB_CNT];
+      // lhs = y^2 - x^2
+      Field25519.sub(lhs, y2, x2);
+      // lhs = z^2 * (y2 - x2)
+      Field25519.mult(lhs, lhs, z2);
+      long[] rhs = new long[Field25519.LIMB_CNT];
+      // rhs = x^2 * y^2
+      Field25519.mult(rhs, x2, y2);
+      // rhs = D * x^2 * y^2
+      Field25519.mult(rhs, rhs, Ed25519Constants.D);
+      // rhs = z^4 + D * x^2 * y^2
+      Field25519.sum(rhs, z4);
+      // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually
+      // reduce it here.
+      Field25519.reduce(rhs, rhs);
+      // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2
+      return Bytes.equal(Field25519.contract(lhs), Field25519.contract(rhs));
+    }
+  }
+
+  /**
+   * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z,
+   * XY = ZT
+   *
+   * Note that this is referred as ge_p3 in ref10 impl.
+   * Also note that t = T below following Java coding style.
+   *
+   * See
+   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+   *
+   * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
+   */
+  private static class XYZT {
+
+    final XYZ xyz;
+    final long[] t;
+
+    XYZT() {
+      this(new XYZ(), new long[Field25519.LIMB_CNT]);
+    }
+
+    XYZT(XYZ xyz, long[] t) {
+      this.xyz = xyz;
+      this.t = t;
+    }
+
+    XYZT(PartialXYZT partialXYZT) {
+      this();
+      fromPartialXYZT(this, partialXYZT);
+    }
+
+    /**
+     * ge_p1p1_to_p2.c
+     */
+    @CanIgnoreReturnValue
+    private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) {
+      Field25519.mult(out.xyz.x, in.xyz.x, in.t);
+      Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z);
+      Field25519.mult(out.xyz.z, in.xyz.z, in.t);
+      Field25519.mult(out.t, in.xyz.x, in.xyz.y);
+      return out;
+    }
+
+    /**
+     * Decodes {@code s} into an extented projective point.
+     * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3
+     */
+    private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException {
+      long[] x = new long[Field25519.LIMB_CNT];
+      long[] y = Field25519.expand(s);
+      long[] z = new long[Field25519.LIMB_CNT];
+      z[0] = 1;
+      long[] t = new long[Field25519.LIMB_CNT];
+      long[] u = new long[Field25519.LIMB_CNT];
+      long[] v = new long[Field25519.LIMB_CNT];
+      long[] vxx = new long[Field25519.LIMB_CNT];
+      long[] check = new long[Field25519.LIMB_CNT];
+      Field25519.square(u, y);
+      Field25519.mult(v, u, Ed25519Constants.D);
+      Field25519.sub(u, u, z); // u = y^2 - 1
+      Field25519.sum(v, v, z); // v = dy^2 + 1
+
+      long[] v3 = new long[Field25519.LIMB_CNT];
+      Field25519.square(v3, v);
+      Field25519.mult(v3, v3, v); // v3 = v^3
+      Field25519.square(x, v3);
+      Field25519.mult(x, x, v);
+      Field25519.mult(x, x, u); // x = uv^7
+
+      pow2252m3(x, x); // x = (uv^7)^((q-5)/8)
+      Field25519.mult(x, x, v3);
+      Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8)
+
+      Field25519.square(vxx, x);
+      Field25519.mult(vxx, vxx, v);
+      Field25519.sub(check, vxx, u); // vx^2-u
+      if (isNonZeroVarTime(check)) {
+        Field25519.sum(check, vxx, u); // vx^2+u
+        if (isNonZeroVarTime(check)) {
+          throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
+              + "coordinates. No square root exists for modulo 2^255-19");
+        }
+        Field25519.mult(x, x, Ed25519Constants.SQRTM1);
+      }
+
+      if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) {
+        throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
+            + "coordinates. Computed x is zero and encoded x's least significant bit is not zero");
+      }
+      if (getLsb(x) == ((s[31] & 0xff) >> 7)) {
+        neg(x, x);
+      }
+
+      Field25519.mult(t, x, y);
+      return new XYZT(new XYZ(x, y, z), t);
+    }
+  }
+
+  /**
+   * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
+   *
+   * Note that this is referred as complete form in the original ref10 impl (ge_p1p1).
+   * Also note that t = T below following Java coding style.
+   *
+   * Although this has the same types as XYZT, it is redefined to have its own type so that it is
+   * readable and 1:1 corresponds to ref10 impl.
+   *
+   * Can be converted to XYZT as follows:
+   * X1 = X * T = x * Z * T = x * Z1
+   * Y1 = Y * Z = y * T * Z = y * Z1
+   * Z1 = Z * T = Z * T
+   * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1
+   */
+  private static class PartialXYZT {
+
+    final XYZ xyz;
+    final long[] t;
+
+    PartialXYZT() {
+      this(new XYZ(), new long[Field25519.LIMB_CNT]);
+    }
+
+    PartialXYZT(XYZ xyz, long[] t) {
+      this.xyz = xyz;
+      this.t = t;
+    }
+
+    PartialXYZT(PartialXYZT other) {
+      xyz = new XYZ(other.xyz);
+      t = Arrays.copyOf(other.t, Field25519.LIMB_CNT);
+    }
+  }
+
+  /**
+   * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of
+   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+   * with Z = 1.
+   */
+  static class CachedXYT {
+
+    final long[] yPlusX;
+    final long[] yMinusX;
+    final long[] t2d;
+
+    CachedXYT() {
+      this(
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT]);
+    }
+
+    /**
+     * Creates a cached XYZT with Z = 1
+     *
+     * @param yPlusX y + x
+     * @param yMinusX y - x
+     * @param t2d 2d * xy
+     */
+    CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) {
+      this.yPlusX = yPlusX;
+      this.yMinusX = yMinusX;
+      this.t2d = t2d;
+    }
+
+    CachedXYT(CachedXYT other) {
+      yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT);
+      yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT);
+      t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT);
+    }
+
+    // z is one implicitly, so this just copies {@code in} to {@code output}.
+    void multByZ(long[] output, long[] in) {
+      System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT);
+    }
+
+    /**
+     * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value.
+     */
+    void copyConditional(CachedXYT other, int icopy) {
+      Curve25519.copyConditional(yPlusX, other.yPlusX, icopy);
+      Curve25519.copyConditional(yMinusX, other.yMinusX, icopy);
+      Curve25519.copyConditional(t2d, other.t2d, icopy);
+    }
+  }
+
+  private static class CachedXYZT extends CachedXYT {
+
+    private final long[] z;
+
+    CachedXYZT() {
+      this(
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT],
+          new long[Field25519.LIMB_CNT]);
+    }
+
+    /**
+     * ge_p3_to_cached.c
+     */
+    CachedXYZT(XYZT xyzt) {
+      this();
+      Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x);
+      Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x);
+      System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT);
+      Field25519.mult(t2d, xyzt.t, Ed25519Constants.D2);
+    }
+
+    /**
+     * Creates a cached XYZT
+     *
+     * @param yPlusX Y + X
+     * @param yMinusX Y - X
+     * @param z Z
+     * @param t2d 2d * (XY/Z)
+     */
+    CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) {
+      super(yPlusX, yMinusX, t2d);
+      this.z = z;
+    }
+
+    @Override
+    public void multByZ(long[] output, long[] in) {
+      Field25519.mult(output, in, z);
+    }
+  }
+
+  /**
+   * Addition defined in Section 3.1 of
+   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+   *
+   * Please note that this is a partial of the operation listed there leaving out the final
+   * conversion from PartialXYZT to XYZT.
+   *
+   * @param extended extended projective point input
+   * @param cached cached projective point input
+   */
+  private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
+    long[] t = new long[Field25519.LIMB_CNT];
+
+    // Y1 + X1
+    Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
+
+    // Y1 - X1
+    Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
+
+    // A = (Y1 - X1) * (Y2 - X2)
+    Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX);
+
+    // B = (Y1 + X1) * (Y2 + X2)
+    Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX);
+
+    // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
+    Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
+
+    // Z1 * Z2
+    cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
+
+    // D = 2 * Z1 * Z2
+    Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
+
+    // X3 = B - A
+    Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+    // Y3 = B + A
+    Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+    // Z3 = D + C
+    Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t);
+
+    // T3 = D - C
+    Field25519.sub(partialXYZT.t, t, partialXYZT.t);
+  }
+
+  /**
+   * Based on the addition defined in Section 3.1 of
+   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+   *
+   * Please note that this is a partial of the operation listed there leaving out the final
+   * conversion from PartialXYZT to XYZT.
+   *
+   * @param extended extended projective point input
+   * @param cached cached projective point input
+   */
+  private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
+    long[] t = new long[Field25519.LIMB_CNT];
+
+    // Y1 + X1
+    Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
+
+    // Y1 - X1
+    Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
+
+    // A = (Y1 - X1) * (Y2 + X2)
+    Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX);
+
+    // B = (Y1 + X1) * (Y2 - X2)
+    Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX);
+
+    // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
+    Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
+
+    // Z1 * Z2
+    cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
+
+    // D = 2 * Z1 * Z2
+    Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
+
+    // X3 = B - A
+    Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+    // Y3 = B + A
+    Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+    // Z3 = D - C
+    Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t);
+
+    // T3 = D + C
+    Field25519.sum(partialXYZT.t, t, partialXYZT.t);
+  }
+
+  /**
+   * Doubles {@code p} and puts the result into this PartialXYZT.
+   *
+   * This is based on the addition defined in formula 7 in Section 3.3 of
+   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+   *
+   * Please note that this is a partial of the operation listed there leaving out the final
+   * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in
+   * the paper, H should be replaced with A+B.
+   */
+  private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) {
+    long[] t0 = new long[Field25519.LIMB_CNT];
+
+    // XX = X1^2
+    Field25519.square(partialXYZT.xyz.x, p.x);
+
+    // YY = Y1^2
+    Field25519.square(partialXYZT.xyz.z, p.y);
+
+    // B' = Z1^2
+    Field25519.square(partialXYZT.t, p.z);
+
+    // B = 2 * B'
+    Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t);
+
+    // A = X1 + Y1
+    Field25519.sum(partialXYZT.xyz.y, p.x, p.y);
+
+    // AA = A^2
+    Field25519.square(t0, partialXYZT.xyz.y);
+
+    // Y3 = YY + XX
+    Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x);
+
+    // Z3 = YY - XX
+    Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x);
+
+    // X3 = AA - Y3
+    Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y);
+
+    // T3 = B - Z3
+    Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z);
+  }
+
+  /**
+   * Doubles {@code p} and puts the result into this PartialXYZT.
+   */
+  private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) {
+    doubleXYZ(partialXYZT, p.xyz);
+  }
+
+  /**
+   * Compares two byte values in constant time.
+   *
+   * Please note that this doesn't reuse {@link Curve25519#eq} method since the below inputs are
+   * byte values.
+   */
+  private static int eq(int a, int b) {
+    int r = ~(a ^ b) & 0xff;
+    r &= r << 4;
+    r &= r << 2;
+    r &= r << 1;
+    return (r >> 7) & 1;
+  }
+
+  /**
+   * This is a constant time operation where point b*B*256^pos is stored in {@code t}.
+   * When b is 0, t remains the same (i.e., neutral point).
+   *
+   * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select
+   * method negates the corresponding point if b is negative (which is straight forward in elliptic
+   * curves by just negating y coordinate). Therefore we can get multiples of B with the half of
+   * memory requirements.
+   *
+   * @param t neutral element (i.e., point 0), also serves as output.
+   * @param pos in B[pos][j] = (j+1)*B*256^pos
+   * @param b value in [-8, 8] range.
+   */
+  private static void select(CachedXYT t, int pos, byte b) {
+    int bnegative = (b & 0xff) >> 7;
+    int babs = b - (((-bnegative) & b) << 1);
+
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][0], eq(babs, 1));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][1], eq(babs, 2));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][2], eq(babs, 3));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][3], eq(babs, 4));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][4], eq(babs, 5));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][5], eq(babs, 6));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][6], eq(babs, 7));
+    t.copyConditional(Ed25519Constants.B_TABLE[pos][7], eq(babs, 8));
+
+    long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT);
+    long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT);
+    long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT);
+    neg(t2d, t2d);
+    CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d);
+    t.copyConditional(minust, bnegative);
+  }
+
+  /**
+   * Computes {@code a}*B
+   * where a = a[0]+256*a[1]+...+256^31 a[31] and
+   * B is the Ed25519 base point (x,4/5) with x positive.
+   *
+   * Preconditions:
+   * a[31] <= 127
+   * @throws IllegalStateException iff there is arithmetic error.
+   */
+  @SuppressWarnings("NarrowingCompoundAssignment")
+  private static XYZ scalarMultWithBase(byte[] a) {
+    byte[] e = new byte[2 * Field25519.FIELD_LEN];
+    for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+      e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf);
+      e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf);
+    }
+    // each e[i] is between 0 and 15
+    // e[63] is between 0 and 7
+
+    // Rewrite e in a way that each e[i] is in [-8, 8].
+    // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte
+    // a[63] can be at most 1.
+    int carry = 0;
+    for (int i = 0; i < e.length - 1; i++) {
+      e[i] += carry;
+      carry = e[i] + 8;
+      carry >>= 4;
+      e[i] -= carry << 4;
+    }
+    e[e.length - 1] += carry;
+
+    PartialXYZT ret = new PartialXYZT(NEUTRAL);
+    XYZT xyzt = new XYZT();
+    // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64
+    // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd
+    // indices. After the result, we can double the result point 4 times to shift the multiplication
+    // scalar by 4 bits.
+    for (int i = 1; i < e.length; i += 2) {
+      CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
+      select(t, i / 2, e[i]);
+      add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
+    }
+
+    // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result
+    // for the odd indices in e.
+    XYZ xyz = new XYZ();
+    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+
+    // Add multiples of B for even indices of e.
+    for (int i = 0; i < e.length; i += 2) {
+      CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
+      select(t, i / 2, e[i]);
+      add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
+    }
+
+    // This check is to protect against flaws, i.e. if there is a computation error through a
+    // faulty CPU or if the implementation contains a bug.
+    XYZ result = new XYZ(ret);
+    if (!result.isOnCurve()) {
+      throw new IllegalStateException("arithmetic error in scalar multiplication");
+    }
+    return result;
+  }
+
+  /**
+   * Computes {@code a}*B
+   * where a = a[0]+256*a[1]+...+256^31 a[31] and
+   * B is the Ed25519 base point (x,4/5) with x positive.
+   *
+   * Preconditions:
+   * a[31] <= 127
+   */
+  public static byte[] scalarMultWithBaseToBytes(byte[] a) {
+    return scalarMultWithBase(a).toBytes();
+  }
+
+  @SuppressWarnings("NarrowingCompoundAssignment")
+  private static byte[] slide(byte[] a) {
+    byte[] r = new byte[256];
+    // Writes each bit in a[0..31] into r[0..255]:
+    // a = a[0]+256*a[1]+...+256^31*a[31] is equal to
+    // r = r[0]+2*r[1]+...+2^255*r[255]
+    for (int i = 0; i < 256; i++) {
+      r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7)));
+    }
+
+    // Transforms r[i] as odd values in [-15, 15]
+    for (int i = 0; i < 256; i++) {
+      if (r[i] != 0) {
+        for (int b = 1; b <= 6 && i + b < 256; b++) {
+          if (r[i + b] != 0) {
+            if (r[i] + (r[i + b] << b) <= 15) {
+              r[i] += r[i + b] << b;
+              r[i + b] = 0;
+            } else if (r[i] - (r[i + b] << b) >= -15) {
+              r[i] -= r[i + b] << b;
+              for (int k = i + b; k < 256; k++) {
+                if (r[k] == 0) {
+                  r[k] = 1;
+                  break;
+                }
+                r[k] = 0;
+              }
+            } else {
+              break;
+            }
+          }
+        }
+      }
+    }
+    return r;
+  }
+
+  /**
+   * Computes {@code a}*{@code pointA}+{@code b}*B
+   * where a = a[0]+256*a[1]+...+256^31*a[31].
+   * and b = b[0]+256*b[1]+...+256^31*b[31].
+   * B is the Ed25519 base point (x,4/5) with x positive.
+   *
+   * Note that execution time varies based on the input since this will only be used in verification
+   * of signatures.
+   */
+  private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) {
+    // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA
+    CachedXYZT[] pointAArray = new CachedXYZT[8];
+    pointAArray[0] = new CachedXYZT(pointA);
+    PartialXYZT t = new PartialXYZT();
+    doubleXYZT(t, pointA);
+    XYZT doubleA = new XYZT(t);
+    for (int i = 1; i < pointAArray.length; i++) {
+      add(t, doubleA, pointAArray[i - 1]);
+      pointAArray[i] = new CachedXYZT(new XYZT(t));
+    }
+
+    byte[] aSlide = slide(a);
+    byte[] bSlide = slide(b);
+    t = new PartialXYZT(NEUTRAL);
+    XYZT u = new XYZT();
+    int i = 255;
+    for (; i >= 0; i--) {
+      if (aSlide[i] != 0 || bSlide[i] != 0) {
+        break;
+      }
+    }
+    for (; i >= 0; i--) {
+      doubleXYZ(t, new XYZ(t));
+      if (aSlide[i] > 0) {
+        add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]);
+      } else if (aSlide[i] < 0) {
+        sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]);
+      }
+      if (bSlide[i] > 0) {
+        add(t, XYZT.fromPartialXYZT(u, t), Ed25519Constants.B2[bSlide[i] / 2]);
+      } else if (bSlide[i] < 0) {
+        sub(t, XYZT.fromPartialXYZT(u, t), Ed25519Constants.B2[-bSlide[i] / 2]);
+      }
+    }
+
+    return new XYZ(t);
+  }
+
+  /**
+   * Returns true if {@code in} is nonzero.
+   *
+   * Note that execution time might depend on the input {@code in}.
+   */
+  private static boolean isNonZeroVarTime(long[] in) {
+    long[] inCopy = new long[in.length + 1];
+    System.arraycopy(in, 0, inCopy, 0, in.length);
+    Field25519.reduceCoefficients(inCopy);
+    byte[] bytes = Field25519.contract(inCopy);
+    for (byte b : bytes) {
+      if (b != 0) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns the least significant bit of {@code in}.
+   */
+  private static int getLsb(long[] in) {
+    return Field25519.contract(in)[0] & 1;
+  }
+
+  /**
+   * Negates all values in {@code in} and store it in {@code out}.
+   */
+  private static void neg(long[] out, long[] in) {
+    for (int i = 0; i < in.length; i++) {
+      out[i] = -in[i];
+    }
+  }
+
+  /**
+   * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}.
+   */
+  private static void pow2252m3(long[] out, long[] in) {
+    long[] t0 = new long[Field25519.LIMB_CNT];
+    long[] t1 = new long[Field25519.LIMB_CNT];
+    long[] t2 = new long[Field25519.LIMB_CNT];
+
+    // z2 = z1^2^1
+    Field25519.square(t0, in);
+
+    // z8 = z2^2^2
+    Field25519.square(t1, t0);
+    for (int i = 1; i < 2; i++) {
+      Field25519.square(t1, t1);
+    }
+
+    // z9 = z1*z8
+    Field25519.mult(t1, in, t1);
+
+    // z11 = z2*z9
+    Field25519.mult(t0, t0, t1);
+
+    // z22 = z11^2^1
+    Field25519.square(t0, t0);
+
+    // z_5_0 = z9*z22
+    Field25519.mult(t0, t1, t0);
+
+    // z_10_5 = z_5_0^2^5
+    Field25519.square(t1, t0);
+    for (int i = 1; i < 5; i++) {
+      Field25519.square(t1, t1);
+    }
+
+    // z_10_0 = z_10_5*z_5_0
+    Field25519.mult(t0, t1, t0);
+
+    // z_20_10 = z_10_0^2^10
+    Field25519.square(t1, t0);
+    for (int i = 1; i < 10; i++) {
+      Field25519.square(t1, t1);
+    }
+
+    // z_20_0 = z_20_10*z_10_0
+    Field25519.mult(t1, t1, t0);
+
+    // z_40_20 = z_20_0^2^20
+    Field25519.square(t2, t1);
+    for (int i = 1; i < 20; i++) {
+      Field25519.square(t2, t2);
+    }
+
+    // z_40_0 = z_40_20*z_20_0
+    Field25519.mult(t1, t2, t1);
+
+    // z_50_10 = z_40_0^2^10
+    Field25519.square(t1, t1);
+    for (int i = 1; i < 10; i++) {
+      Field25519.square(t1, t1);
+    }
+
+    // z_50_0 = z_50_10*z_10_0
+    Field25519.mult(t0, t1, t0);
+
+    // z_100_50 = z_50_0^2^50
+    Field25519.square(t1, t0);
+    for (int i = 1; i < 50; i++) {
+      Field25519.square(t1, t1);
+    }
+
+    // z_100_0 = z_100_50*z_50_0
+    Field25519.mult(t1, t1, t0);
+
+    // z_200_100 = z_100_0^2^100
+    Field25519.square(t2, t1);
+    for (int i = 1; i < 100; i++) {
+      Field25519.square(t2, t2);
+    }
+
+    // z_200_0 = z_200_100*z_100_0
+    Field25519.mult(t1, t2, t1);
+
+    // z_250_50 = z_200_0^2^50
+    Field25519.square(t1, t1);
+    for (int i = 1; i < 50; i++) {
+      Field25519.square(t1, t1);
+    }
+
+    // z_250_0 = z_250_50*z_50_0
+    Field25519.mult(t0, t1, t0);
+
+    // z_252_2 = z_250_0^2^2
+    Field25519.square(t0, t0);
+    for (int i = 1; i < 2; i++) {
+      Field25519.square(t0, t0);
+    }
+
+    // z_252_3 = z_252_2*z1
+    Field25519.mult(out, t0, in);
+  }
+
+  /**
+   * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format.
+   */
+  private static long load3(byte[] in, int idx) {
+    long result;
+    result = (long) in[idx] & 0xff;
+    result |= (long) (in[idx + 1] & 0xff) << 8;
+    result |= (long) (in[idx + 2] & 0xff) << 16;
+    return result;
+  }
+
+  /**
+   * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format.
+   */
+  private static long load4(byte[] in, int idx) {
+    long result = load3(in, idx);
+    result |= (long) (in[idx + 3] & 0xff) << 24;
+    return result;
+  }
+
+  /**
+   * Input:
+   * s[0]+256*s[1]+...+256^63*s[63] = s
+   *
+   * Output:
+   * s[0]+256*s[1]+...+256^31*s[31] = s mod l
+   * where l = 2^252 + 27742317777372353535851937790883648493.
+   * Overwrites s in place.
+   */
+  private static void reduce(byte[] s) {
+    // Observation:
+    // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l
+    // Let m = -27742317777372353535851937790883648493
+    // Thus a*2^252+b mod l is equivalent to a*m+b mod l
+    //
+    // First s is divided into chunks of 21 bits as follows:
+    // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63]
+    long s0 = 2097151 & load3(s, 0);
+    long s1 = 2097151 & (load4(s, 2) >> 5);
+    long s2 = 2097151 & (load3(s, 5) >> 2);
+    long s3 = 2097151 & (load4(s, 7) >> 7);
+    long s4 = 2097151 & (load4(s, 10) >> 4);
+    long s5 = 2097151 & (load3(s, 13) >> 1);
+    long s6 = 2097151 & (load4(s, 15) >> 6);
+    long s7 = 2097151 & (load3(s, 18) >> 3);
+    long s8 = 2097151 & load3(s, 21);
+    long s9 = 2097151 & (load4(s, 23) >> 5);
+    long s10 = 2097151 & (load3(s, 26) >> 2);
+    long s11 = 2097151 & (load4(s, 28) >> 7);
+    long s12 = 2097151 & (load4(s, 31) >> 4);
+    long s13 = 2097151 & (load3(s, 34) >> 1);
+    long s14 = 2097151 & (load4(s, 36) >> 6);
+    long s15 = 2097151 & (load3(s, 39) >> 3);
+    long s16 = 2097151 & load3(s, 42);
+    long s17 = 2097151 & (load4(s, 44) >> 5);
+    long s18 = 2097151 & (load3(s, 47) >> 2);
+    long s19 = 2097151 & (load4(s, 49) >> 7);
+    long s20 = 2097151 & (load4(s, 52) >> 4);
+    long s21 = 2097151 & (load3(s, 55) >> 1);
+    long s22 = 2097151 & (load4(s, 57) >> 6);
+    long s23 = (load4(s, 60) >> 3);
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+    long carry10;
+    long carry11;
+    long carry12;
+    long carry13;
+    long carry14;
+    long carry15;
+    long carry16;
+
+    // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l
+    // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6)
+    // starting from s11 (s11*2^210)
+    // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs
+    s11 += s23 * 666643;
+    s12 += s23 * 470296;
+    s13 += s23 * 654183;
+    s14 -= s23 * 997805;
+    s15 += s23 * 136657;
+    s16 -= s23 * 683901;
+    // s23 = 0;
+
+    s10 += s22 * 666643;
+    s11 += s22 * 470296;
+    s12 += s22 * 654183;
+    s13 -= s22 * 997805;
+    s14 += s22 * 136657;
+    s15 -= s22 * 683901;
+    // s22 = 0;
+
+    s9 += s21 * 666643;
+    s10 += s21 * 470296;
+    s11 += s21 * 654183;
+    s12 -= s21 * 997805;
+    s13 += s21 * 136657;
+    s14 -= s21 * 683901;
+    // s21 = 0;
+
+    s8 += s20 * 666643;
+    s9 += s20 * 470296;
+    s10 += s20 * 654183;
+    s11 -= s20 * 997805;
+    s12 += s20 * 136657;
+    s13 -= s20 * 683901;
+    // s20 = 0;
+
+    s7 += s19 * 666643;
+    s8 += s19 * 470296;
+    s9 += s19 * 654183;
+    s10 -= s19 * 997805;
+    s11 += s19 * 136657;
+    s12 -= s19 * 683901;
+    // s19 = 0;
+
+    s6 += s18 * 666643;
+    s7 += s18 * 470296;
+    s8 += s18 * 654183;
+    s9 -= s18 * 997805;
+    s10 += s18 * 136657;
+    s11 -= s18 * 683901;
+    // s18 = 0;
+
+    // Reduce the bit length of limbs from s6 to s15 to 21-bits.
+    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
+    carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21;
+    carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21;
+    carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21;
+
+    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21;
+    carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21;
+
+    // Resume reduction where we left off.
+    s5 += s17 * 666643;
+    s6 += s17 * 470296;
+    s7 += s17 * 654183;
+    s8 -= s17 * 997805;
+    s9 += s17 * 136657;
+    s10 -= s17 * 683901;
+    // s17 = 0;
+
+    s4 += s16 * 666643;
+    s5 += s16 * 470296;
+    s6 += s16 * 654183;
+    s7 -= s16 * 997805;
+    s8 += s16 * 136657;
+    s9 -= s16 * 683901;
+    // s16 = 0;
+
+    s3 += s15 * 666643;
+    s4 += s15 * 470296;
+    s5 += s15 * 654183;
+    s6 -= s15 * 997805;
+    s7 += s15 * 136657;
+    s8 -= s15 * 683901;
+    // s15 = 0;
+
+    s2 += s14 * 666643;
+    s3 += s14 * 470296;
+    s4 += s14 * 654183;
+    s5 -= s14 * 997805;
+    s6 += s14 * 136657;
+    s7 -= s14 * 683901;
+    // s14 = 0;
+
+    s1 += s13 * 666643;
+    s2 += s13 * 470296;
+    s3 += s13 * 654183;
+    s4 -= s13 * 997805;
+    s5 += s13 * 136657;
+    s6 -= s13 * 683901;
+    // s13 = 0;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    s12 = 0;
+
+    // Reduce the range of limbs from s0 to s11 to 21-bits.
+    carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
+
+    carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    s12 = 0;
+
+    // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs.
+    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
+    carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
+
+    // Do one last reduction as s12 might be 1.
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // s12 = 0;
+
+    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
+
+    // Serialize the result into the s.
+    s[0] = (byte) s0;
+    s[1] = (byte) (s0 >> 8);
+    s[2] = (byte) ((s0 >> 16) | (s1 << 5));
+    s[3] = (byte) (s1 >> 3);
+    s[4] = (byte) (s1 >> 11);
+    s[5] = (byte) ((s1 >> 19) | (s2 << 2));
+    s[6] = (byte) (s2 >> 6);
+    s[7] = (byte) ((s2 >> 14) | (s3 << 7));
+    s[8] = (byte) (s3 >> 1);
+    s[9] = (byte) (s3 >> 9);
+    s[10] = (byte) ((s3 >> 17) | (s4 << 4));
+    s[11] = (byte) (s4 >> 4);
+    s[12] = (byte) (s4 >> 12);
+    s[13] = (byte) ((s4 >> 20) | (s5 << 1));
+    s[14] = (byte) (s5 >> 7);
+    s[15] = (byte) ((s5 >> 15) | (s6 << 6));
+    s[16] = (byte) (s6 >> 2);
+    s[17] = (byte) (s6 >> 10);
+    s[18] = (byte) ((s6 >> 18) | (s7 << 3));
+    s[19] = (byte) (s7 >> 5);
+    s[20] = (byte) (s7 >> 13);
+    s[21] = (byte) s8;
+    s[22] = (byte) (s8 >> 8);
+    s[23] = (byte) ((s8 >> 16) | (s9 << 5));
+    s[24] = (byte) (s9 >> 3);
+    s[25] = (byte) (s9 >> 11);
+    s[26] = (byte) ((s9 >> 19) | (s10 << 2));
+    s[27] = (byte) (s10 >> 6);
+    s[28] = (byte) ((s10 >> 14) | (s11 << 7));
+    s[29] = (byte) (s11 >> 1);
+    s[30] = (byte) (s11 >> 9);
+    s[31] = (byte) (s11 >> 17);
+  }
+
+  /**
+   * Input:
+   * a[0]+256*a[1]+...+256^31*a[31] = a
+   * b[0]+256*b[1]+...+256^31*b[31] = b
+   * c[0]+256*c[1]+...+256^31*c[31] = c
+   *
+   * Output:
+   * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l
+   * where l = 2^252 + 27742317777372353535851937790883648493.
+   */
+  private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) {
+    // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c
+    // See Ed25519.reduce for related comments.
+    long a0 = 2097151 & load3(a, 0);
+    long a1 = 2097151 & (load4(a, 2) >> 5);
+    long a2 = 2097151 & (load3(a, 5) >> 2);
+    long a3 = 2097151 & (load4(a, 7) >> 7);
+    long a4 = 2097151 & (load4(a, 10) >> 4);
+    long a5 = 2097151 & (load3(a, 13) >> 1);
+    long a6 = 2097151 & (load4(a, 15) >> 6);
+    long a7 = 2097151 & (load3(a, 18) >> 3);
+    long a8 = 2097151 & load3(a, 21);
+    long a9 = 2097151 & (load4(a, 23) >> 5);
+    long a10 = 2097151 & (load3(a, 26) >> 2);
+    long a11 = (load4(a, 28) >> 7);
+    long b0 = 2097151 & load3(b, 0);
+    long b1 = 2097151 & (load4(b, 2) >> 5);
+    long b2 = 2097151 & (load3(b, 5) >> 2);
+    long b3 = 2097151 & (load4(b, 7) >> 7);
+    long b4 = 2097151 & (load4(b, 10) >> 4);
+    long b5 = 2097151 & (load3(b, 13) >> 1);
+    long b6 = 2097151 & (load4(b, 15) >> 6);
+    long b7 = 2097151 & (load3(b, 18) >> 3);
+    long b8 = 2097151 & load3(b, 21);
+    long b9 = 2097151 & (load4(b, 23) >> 5);
+    long b10 = 2097151 & (load3(b, 26) >> 2);
+    long b11 = (load4(b, 28) >> 7);
+    long c0 = 2097151 & load3(c, 0);
+    long c1 = 2097151 & (load4(c, 2) >> 5);
+    long c2 = 2097151 & (load3(c, 5) >> 2);
+    long c3 = 2097151 & (load4(c, 7) >> 7);
+    long c4 = 2097151 & (load4(c, 10) >> 4);
+    long c5 = 2097151 & (load3(c, 13) >> 1);
+    long c6 = 2097151 & (load4(c, 15) >> 6);
+    long c7 = 2097151 & (load3(c, 18) >> 3);
+    long c8 = 2097151 & load3(c, 21);
+    long c9 = 2097151 & (load4(c, 23) >> 5);
+    long c10 = 2097151 & (load3(c, 26) >> 2);
+    long c11 = (load4(c, 28) >> 7);
+    long s0;
+    long s1;
+    long s2;
+    long s3;
+    long s4;
+    long s5;
+    long s6;
+    long s7;
+    long s8;
+    long s9;
+    long s10;
+    long s11;
+    long s12;
+    long s13;
+    long s14;
+    long s15;
+    long s16;
+    long s17;
+    long s18;
+    long s19;
+    long s20;
+    long s21;
+    long s22;
+    long s23;
+    long carry0;
+    long carry1;
+    long carry2;
+    long carry3;
+    long carry4;
+    long carry5;
+    long carry6;
+    long carry7;
+    long carry8;
+    long carry9;
+    long carry10;
+    long carry11;
+    long carry12;
+    long carry13;
+    long carry14;
+    long carry15;
+    long carry16;
+    long carry17;
+    long carry18;
+    long carry19;
+    long carry20;
+    long carry21;
+    long carry22;
+
+    s0 = c0 + a0 * b0;
+    s1 = c1 + a0 * b1 + a1 * b0;
+    s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
+    s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
+    s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
+    s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
+    s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
+    s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
+    s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1
+        + a8 * b0;
+    s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2
+        + a8 * b1 + a9 * b0;
+    s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3
+        + a8 * b2 + a9 * b1 + a10 * b0;
+    s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4
+        + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
+    s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3
+        + a10 * b2 + a11 * b1;
+    s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3
+        + a11 * b2;
+    s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4
+        + a11 * b3;
+    s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
+    s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
+    s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
+    s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
+    s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
+    s20 = a9 * b11 + a10 * b10 + a11 * b9;
+    s21 = a10 * b11 + a11 * b10;
+    s22 = a11 * b11;
+    s23 = 0;
+
+    carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
+    carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21;
+    carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21;
+    carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21;
+    carry18 = (s18 + (1 << 20)) >> 21; s19 += carry18; s18 -= carry18 << 21;
+    carry20 = (s20 + (1 << 20)) >> 21; s21 += carry20; s20 -= carry20 << 21;
+    carry22 = (s22 + (1 << 20)) >> 21; s23 += carry22; s22 -= carry22 << 21;
+
+    carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21;
+    carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21;
+    carry17 = (s17 + (1 << 20)) >> 21; s18 += carry17; s17 -= carry17 << 21;
+    carry19 = (s19 + (1 << 20)) >> 21; s20 += carry19; s19 -= carry19 << 21;
+    carry21 = (s21 + (1 << 20)) >> 21; s22 += carry21; s21 -= carry21 << 21;
+
+    s11 += s23 * 666643;
+    s12 += s23 * 470296;
+    s13 += s23 * 654183;
+    s14 -= s23 * 997805;
+    s15 += s23 * 136657;
+    s16 -= s23 * 683901;
+    // s23 = 0;
+
+    s10 += s22 * 666643;
+    s11 += s22 * 470296;
+    s12 += s22 * 654183;
+    s13 -= s22 * 997805;
+    s14 += s22 * 136657;
+    s15 -= s22 * 683901;
+    // s22 = 0;
+
+    s9 += s21 * 666643;
+    s10 += s21 * 470296;
+    s11 += s21 * 654183;
+    s12 -= s21 * 997805;
+    s13 += s21 * 136657;
+    s14 -= s21 * 683901;
+    // s21 = 0;
+
+    s8 += s20 * 666643;
+    s9 += s20 * 470296;
+    s10 += s20 * 654183;
+    s11 -= s20 * 997805;
+    s12 += s20 * 136657;
+    s13 -= s20 * 683901;
+    // s20 = 0;
+
+    s7 += s19 * 666643;
+    s8 += s19 * 470296;
+    s9 += s19 * 654183;
+    s10 -= s19 * 997805;
+    s11 += s19 * 136657;
+    s12 -= s19 * 683901;
+    // s19 = 0;
+
+    s6 += s18 * 666643;
+    s7 += s18 * 470296;
+    s8 += s18 * 654183;
+    s9 -= s18 * 997805;
+    s10 += s18 * 136657;
+    s11 -= s18 * 683901;
+    // s18 = 0;
+
+    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
+    carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21;
+    carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21;
+    carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21;
+
+    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+    carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21;
+    carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21;
+
+    s5 += s17 * 666643;
+    s6 += s17 * 470296;
+    s7 += s17 * 654183;
+    s8 -= s17 * 997805;
+    s9 += s17 * 136657;
+    s10 -= s17 * 683901;
+    // s17 = 0;
+
+    s4 += s16 * 666643;
+    s5 += s16 * 470296;
+    s6 += s16 * 654183;
+    s7 -= s16 * 997805;
+    s8 += s16 * 136657;
+    s9 -= s16 * 683901;
+    // s16 = 0;
+
+    s3 += s15 * 666643;
+    s4 += s15 * 470296;
+    s5 += s15 * 654183;
+    s6 -= s15 * 997805;
+    s7 += s15 * 136657;
+    s8 -= s15 * 683901;
+    // s15 = 0;
+
+    s2 += s14 * 666643;
+    s3 += s14 * 470296;
+    s4 += s14 * 654183;
+    s5 -= s14 * 997805;
+    s6 += s14 * 136657;
+    s7 -= s14 * 683901;
+    // s14 = 0;
+
+    s1 += s13 * 666643;
+    s2 += s13 * 470296;
+    s3 += s13 * 654183;
+    s4 -= s13 * 997805;
+    s5 += s13 * 136657;
+    s6 -= s13 * 683901;
+    // s13 = 0;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    s12 = 0;
+
+    carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
+
+    carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    s12 = 0;
+
+    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
+    carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
+
+    s0 += s12 * 666643;
+    s1 += s12 * 470296;
+    s2 += s12 * 654183;
+    s3 -= s12 * 997805;
+    s4 += s12 * 136657;
+    s5 -= s12 * 683901;
+    // s12 = 0;
+
+    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
+    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
+    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
+    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
+    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
+    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
+    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
+    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
+    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
+    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
+    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
+
+    s[0] = (byte) s0;
+    s[1] = (byte) (s0 >> 8);
+    s[2] = (byte) ((s0 >> 16) | (s1 << 5));
+    s[3] = (byte) (s1 >> 3);
+    s[4] = (byte) (s1 >> 11);
+    s[5] = (byte) ((s1 >> 19) | (s2 << 2));
+    s[6] = (byte) (s2 >> 6);
+    s[7] = (byte) ((s2 >> 14) | (s3 << 7));
+    s[8] = (byte) (s3 >> 1);
+    s[9] = (byte) (s3 >> 9);
+    s[10] = (byte) ((s3 >> 17) | (s4 << 4));
+    s[11] = (byte) (s4 >> 4);
+    s[12] = (byte) (s4 >> 12);
+    s[13] = (byte) ((s4 >> 20) | (s5 << 1));
+    s[14] = (byte) (s5 >> 7);
+    s[15] = (byte) ((s5 >> 15) | (s6 << 6));
+    s[16] = (byte) (s6 >> 2);
+    s[17] = (byte) (s6 >> 10);
+    s[18] = (byte) ((s6 >> 18) | (s7 << 3));
+    s[19] = (byte) (s7 >> 5);
+    s[20] = (byte) (s7 >> 13);
+    s[21] = (byte) s8;
+    s[22] = (byte) (s8 >> 8);
+    s[23] = (byte) ((s8 >> 16) | (s9 << 5));
+    s[24] = (byte) (s9 >> 3);
+    s[25] = (byte) (s9 >> 11);
+    s[26] = (byte) ((s9 >> 19) | (s10 << 2));
+    s[27] = (byte) (s10 >> 6);
+    s[28] = (byte) ((s10 >> 14) | (s11 << 7));
+    s[29] = (byte) (s11 >> 1);
+    s[30] = (byte) (s11 >> 9);
+    s[31] = (byte) (s11 >> 17);
+  }
+
+  public static byte[] getHashedScalar(final byte[] privateKey) throws GeneralSecurityException {
+    MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512");
+    digest.update(privateKey, 0, Field25519.FIELD_LEN);
+    byte[] h = digest.digest();
+    // https://tools.ietf.org/html/rfc8032#section-5.1.2.
+    // Clear the lowest three bits of the first octet.
+    h[0] = (byte) (h[0] & 248);
+    // Clear the highest bit of the last octet.
+    h[31] = (byte) (h[31] & 127);
+    // Set the second highest bit if the last octet.
+    h[31] = (byte) (h[31] | 64);
+    return h;
+  }
+
+  /**
+   * Returns the EdDSA signature for the {@code message} based on the {@code hashedPrivateKey}.
+   *
+   * @param message to sign
+   * @param publicKey {@link Ed25519#scalarMultWithBaseToBytes(byte[])} of {@code hashedPrivateKey}
+   * @param hashedPrivateKey {@link Ed25519#getHashedScalar(byte[])} of the private key
+   * @return signature for the {@code message}.
+   * @throws GeneralSecurityException if there is no SHA-512 algorithm defined in {@link
+   *     EngineFactory}.MESSAGE_DIGEST.
+   */
+  public static byte[] sign(
+      final byte[] message, final byte[] publicKey, final byte[] hashedPrivateKey)
+      throws GeneralSecurityException {
+    // Copying the message to make it thread-safe. Otherwise, if the caller modifies the message
+    // between the first and the second hash then it might leak the private key.
+    byte[] messageCopy = Arrays.copyOfRange(message, 0, message.length);
+    MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512");
+    digest.update(hashedPrivateKey, Field25519.FIELD_LEN, Field25519.FIELD_LEN);
+    digest.update(messageCopy);
+    byte[] r = digest.digest();
+    reduce(r);
+
+    byte[] rB = Arrays.copyOfRange(scalarMultWithBase(r).toBytes(), 0, Field25519.FIELD_LEN);
+    digest.reset();
+    digest.update(rB);
+    digest.update(publicKey);
+    digest.update(messageCopy);
+    byte[] hram = digest.digest();
+    reduce(hram);
+    byte[] s = new byte[Field25519.FIELD_LEN];
+    mulAdd(s, hram, hashedPrivateKey, r);
+    return Bytes.concat(rB, s);
+  }
+
+
+  // The order of the generator as unsigned bytes in little endian order.
+  // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748)
+  static final byte[] GROUP_ORDER = new byte[] {
+     (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c,
+     (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
+     (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2,
+     (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
+     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10};
+
+  // Checks whether s represents an integer smaller than the order of the group.
+  // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check
+  // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.)
+  // @param s an integer in little-endian order.
+  private static boolean isSmallerThanGroupOrder(byte[] s) {
+    for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) {
+      // compare unsigned bytes
+      int a = s[j] & 0xff;
+      int b = GROUP_ORDER[j] & 0xff;
+      if (a != b) {
+        return a < b;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with {@code
+   * publicKey}.
+   *
+   * @throws GeneralSecurityException if there is no SHA-512 algorithm defined in {@link
+   *     EngineFactory}.MESSAGE_DIGEST.
+   */
+  public static boolean verify(final byte[] message, final byte[] signature, final byte[] publicKey)
+      throws GeneralSecurityException {
+    if (signature.length != SIGNATURE_LEN) {
+      return false;
+    }
+    byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN);
+    if (!isSmallerThanGroupOrder(s)) {
+      return false;
+    }
+    MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512");
+    digest.update(signature, 0, Field25519.FIELD_LEN);
+    digest.update(publicKey);
+    digest.update(message);
+    byte[] h = digest.digest();
+    reduce(h);
+
+    XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey);
+    XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s);
+    byte[] expectedR = xyz.toBytes();
+    for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+      if (expectedR[i] != signature[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /** Initializes Ed25519 if not yet initialized. */
+  public static void init() {
+    // We access one of the constants to make sure that the static initialization is called.
+    if (Ed25519Constants.D == null) {
+      throw new IllegalStateException("Could not initialize Ed25519.");
+    }
+  }
+
+  private Ed25519() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/Ed25519Constants.java b/java_src/src/main/java/com/google/crypto/tink/internal/Ed25519Constants.java
new file mode 100644
index 0000000..9b73a6d
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/Ed25519Constants.java
@@ -0,0 +1,133 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import java.math.BigInteger;
+
+/** Constants used in {@link Ed25519}. */
+final class Ed25519Constants {
+
+ // d = -121665 / 121666 mod 2^255-19
+  static final long[] D;
+  // 2d
+  static final long[] D2;
+  // 2^((p-1)/4) mod p where p = 2^255-19
+  static final long[] SQRTM1;
+
+  /**
+   * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] =
+   * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0]
+   *
+   * <p>See {@link Ed25519ConstantsGenerator}.
+   */
+  static final Ed25519.CachedXYT[][] B_TABLE;
+  static final Ed25519.CachedXYT[] B2;
+
+  private static final BigInteger P_BI =
+      BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
+  private static final BigInteger D_BI =
+      BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI);
+  private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI);
+  private static final BigInteger SQRTM1_BI =
+      BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI);
+
+  private static class Point {
+    private BigInteger x;
+    private BigInteger y;
+  }
+
+  private static BigInteger recoverX(BigInteger y) {
+    // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19
+    BigInteger xx =
+        y.pow(2)
+            .subtract(BigInteger.ONE)
+            .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI));
+    BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI);
+    if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) {
+      x = x.multiply(SQRTM1_BI).mod(P_BI);
+    }
+    if (x.testBit(0)) {
+      x = P_BI.subtract(x);
+    }
+    return x;
+  }
+
+  private static Point edwards(Point a, Point b) {
+    Point o = new Point();
+    BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI);
+    o.x =
+        (a.x.multiply(b.y).add(b.x.multiply(a.y)))
+            .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI))
+            .mod(P_BI);
+    o.y =
+        (a.y.multiply(b.y).add(a.x.multiply(b.x)))
+            .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI))
+            .mod(P_BI);
+    return o;
+  }
+
+  private static byte[] toLittleEndian(BigInteger n) {
+    byte[] b = new byte[32];
+    byte[] nBytes = n.toByteArray();
+    System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
+    for (int i = 0; i < b.length / 2; i++) {
+      byte t = b[i];
+      b[i] = b[b.length - i - 1];
+      b[b.length - i - 1] = t;
+    }
+    return b;
+  }
+
+  private static Ed25519.CachedXYT getCachedXYT(Point p) {
+    return new Ed25519.CachedXYT(
+        Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))),
+        Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))),
+        Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI))));
+  }
+
+  static {
+    Point b = new Point();
+    b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI);
+    b.x = recoverX(b.y);
+
+    D = Field25519.expand(toLittleEndian(D_BI));
+    D2 = Field25519.expand(toLittleEndian(D2_BI));
+    SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI));
+
+    Point bi = b;
+    B_TABLE = new Ed25519.CachedXYT[32][8];
+    for (int i = 0; i < 32; i++) {
+      Point bij = bi;
+      for (int j = 0; j < 8; j++) {
+        B_TABLE[i][j] = getCachedXYT(bij);
+        bij = edwards(bij, bi);
+      }
+      for (int j = 0; j < 8; j++) {
+        bi = edwards(bi, bi);
+      }
+    }
+    bi = b;
+    Point b2 = edwards(b, b);
+    B2 = new Ed25519.CachedXYT[8];
+    for (int i = 0; i < 8; i++) {
+      B2[i] = getCachedXYT(bi);
+      bi = edwards(bi, b2);
+    }
+  }
+
+  private Ed25519Constants() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/EllipticCurvesUtil.java b/java_src/src/main/java/com/google/crypto/tink/internal/EllipticCurvesUtil.java
new file mode 100644
index 0000000..cf5f623
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/EllipticCurvesUtil.java
@@ -0,0 +1,319 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.subtle.Random;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECField;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+
+/** Utility functions for elliptic curve crypto, used in ECDSA and ECDH. */
+public final class EllipticCurvesUtil {
+
+  public static final ECParameterSpec NIST_P256_PARAMS = getNistP256Params();
+  public static final ECParameterSpec NIST_P384_PARAMS = getNistP384Params();
+  public static final ECParameterSpec NIST_P521_PARAMS = getNistP521Params();
+
+  private static ECParameterSpec getNistP256Params() {
+    return getNistCurveSpec(
+        "115792089210356248762697446949407573530086143415290314195533631308867097853951",
+        "115792089210356248762697446949407573529996955224135760342422259061068512044369",
+        "5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b",
+        "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
+        "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5");
+  }
+
+  private static ECParameterSpec getNistP384Params() {
+    return getNistCurveSpec(
+        "3940200619639447921227904010014361380507973927046544666794829340"
+            + "4245721771496870329047266088258938001861606973112319",
+        "3940200619639447921227904010014361380507973927046544666794690527"
+            + "9627659399113263569398956308152294913554433653942643",
+        "b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875a"
+            + "c656398d8a2ed19d2a85c8edd3ec2aef",
+        "aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a38"
+            + "5502f25dbf55296c3a545e3872760ab7",
+        "3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c0"
+            + "0a60b1ce1d7e819d7a431d7c90ea0e5f");
+  }
+
+  private static ECParameterSpec getNistP521Params() {
+    return getNistCurveSpec(
+        "6864797660130609714981900799081393217269435300143305409394463459"
+            + "18554318339765605212255964066145455497729631139148085803712198"
+            + "7999716643812574028291115057151",
+        "6864797660130609714981900799081393217269435300143305409394463459"
+            + "18554318339765539424505774633321719753296399637136332111386476"
+            + "8612440380340372808892707005449",
+        "051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef10"
+            + "9e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00",
+        "c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3d"
+            + "baa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66",
+        "11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e6"
+            + "62c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650");
+  }
+
+  /**
+   * Checks that a point is on a given elliptic curve.
+   *
+   * <p>This method implements the partial public key validation routine from Section 5.6.2.6 of <a
+   * href="http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf">NIST SP
+   * 800-56A</a>. A partial public key validation is sufficient for curves with cofactor 1. See
+   * Section B.3 of http://www.nsa.gov/ia/_files/SuiteB_Implementer_G-113808.pdf.
+   *
+   * <p>The point validations above are taken from recommendations for ECDH, because parameter
+   * checks in ECDH are much more important than for the case of ECDSA. Performing this test for
+   * ECDSA keys is mainly a sanity check.
+   *
+   * @param point the point that needs verification
+   * @param ec the elliptic curve. This must be a curve over a prime order field.
+   * @throws GeneralSecurityException if the field is binary or if the point is not on the curve.
+   */
+  public static void checkPointOnCurve(ECPoint point, EllipticCurve ec)
+      throws GeneralSecurityException {
+    BigInteger p = getModulus(ec);
+    BigInteger x = point.getAffineX();
+    BigInteger y = point.getAffineY();
+    if (x == null || y == null) {
+      throw new GeneralSecurityException("point is at infinity");
+    }
+    // Check 0 <= x < p and 0 <= y < p.
+    if (x.signum() == -1 || x.compareTo(p) >= 0) {
+      throw new GeneralSecurityException("x is out of range");
+    }
+    if (y.signum() == -1 || y.compareTo(p) >= 0) {
+      throw new GeneralSecurityException("y is out of range");
+    }
+    // Check y^2 == x^3 + a x + b (mod p)
+    BigInteger lhs = y.multiply(y).mod(p);
+    BigInteger rhs = x.multiply(x).add(ec.getA()).multiply(x).add(ec.getB()).mod(p);
+    if (!lhs.equals(rhs)) {
+      throw new GeneralSecurityException("Point is not on curve");
+    }
+  }
+
+  /** Returns whether {@code spec} is a {@link ECParameterSpec} of one of the NIST curves. */
+  public static boolean isNistEcParameterSpec(ECParameterSpec spec) {
+    return isSameEcParameterSpec(spec, NIST_P256_PARAMS)
+        || isSameEcParameterSpec(spec, NIST_P384_PARAMS)
+        || isSameEcParameterSpec(spec, NIST_P521_PARAMS);
+  }
+
+  /** Returns whether {@code one} is the same {@link ECParameterSpec} as {@code two}. */
+  public static boolean isSameEcParameterSpec(ECParameterSpec one, ECParameterSpec two) {
+    return one.getCurve().equals(two.getCurve())
+        && one.getGenerator().equals(two.getGenerator())
+        && one.getOrder().equals(two.getOrder())
+        && one.getCofactor() == two.getCofactor();
+  }
+
+  /**
+   * Returns the modulus of the field used by the curve specified in ecParams.
+   *
+   * @param curve must be a prime order elliptic curve
+   * @return the order of the finite field over which curve is defined.
+   */
+  public static BigInteger getModulus(EllipticCurve curve) throws GeneralSecurityException {
+    ECField field = curve.getField();
+    if (field instanceof ECFieldFp) {
+      return ((ECFieldFp) field).getP();
+    } else {
+      throw new GeneralSecurityException("Only curves over prime order fields are supported");
+    }
+  }
+
+  private static ECParameterSpec getNistCurveSpec(
+      String decimalP, String decimalN, String hexB, String hexGX, String hexGY) {
+    final BigInteger p = new BigInteger(decimalP);
+    final BigInteger n = new BigInteger(decimalN);
+    final BigInteger three = new BigInteger("3");
+    final BigInteger a = p.subtract(three);
+    final BigInteger b = new BigInteger(hexB, 16);
+    final BigInteger gx = new BigInteger(hexGX, 16);
+    final BigInteger gy = new BigInteger(hexGY, 16);
+    final int h = 1;
+    ECFieldFp fp = new ECFieldFp(p);
+    EllipticCurve curveSpec = new EllipticCurve(fp, a, b);
+    ECPoint g = new ECPoint(gx, gy);
+    ECParameterSpec ecSpec = new ECParameterSpec(curveSpec, g, n, h);
+    return ecSpec;
+  }
+
+  /**
+   * Calculates x times the generator of the give elliptic curve spec using the Montgomery ladder.
+   *
+   * <p>This should only be used to validate keys, and not to sign or verify messages.
+   *
+   * <p>See: <a href="https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication">Elliptic
+   * curve point multiplication</a>.
+   *
+   * @param x must be larger than 0 and smaller than the order of the generator.
+   * @return the ECPoint that is x times the generator.
+   */
+  public static ECPoint multiplyByGenerator(BigInteger x, ECParameterSpec spec)
+      throws GeneralSecurityException {
+    if (!EllipticCurvesUtil.isNistEcParameterSpec(spec)) {
+      throw new GeneralSecurityException("spec must be NIST P256, P384 or P521");
+    }
+    if (x.signum() != 1) {
+      throw new GeneralSecurityException("k must be positive");
+    }
+    if (x.compareTo(spec.getOrder()) >= 0) {
+      throw new GeneralSecurityException("k must be smaller than the order of the generator");
+    }
+    EllipticCurve curve = spec.getCurve();
+    ECPoint generator = spec.getGenerator();
+    checkPointOnCurve(generator, curve);
+    BigInteger a = spec.getCurve().getA();
+    BigInteger modulus = getModulus(curve);
+
+    JacobianEcPoint r0 = toJacobianEcPoint(ECPoint.POINT_INFINITY, modulus);
+    JacobianEcPoint r1 = toJacobianEcPoint(generator, modulus);
+    for (int i = x.bitLength(); i >= 0; i--) {
+      if (x.testBit(i)) {
+        r0 = addJacobianPoints(r0, r1, a, modulus);
+        r1 = doubleJacobianPoint(r1, a, modulus);
+      } else {
+        r1 = addJacobianPoints(r0, r1, a, modulus);
+        r0 = doubleJacobianPoint(r0, a, modulus);
+      }
+    }
+    ECPoint output = r0.toECPoint(modulus);
+    checkPointOnCurve(output, curve);
+    return output;
+  }
+
+  private static final BigInteger TWO = BigInteger.valueOf(2);
+  private static final BigInteger THREE = BigInteger.valueOf(3);
+  private static final BigInteger FOUR = BigInteger.valueOf(4);
+  private static final BigInteger EIGHT = BigInteger.valueOf(8);
+
+  /**
+   * Jacobian representation of elliptic curve points.
+   *
+   * <p>The point (X, Y) is represented by a triple (x, y, z), where X = x/z^2 and Y = y/z^3.
+   */
+  static class JacobianEcPoint {
+    BigInteger x;
+    BigInteger y;
+    BigInteger z;
+
+    JacobianEcPoint(BigInteger x, BigInteger y, BigInteger z) {
+      this.x = x;
+      this.y = y;
+      this.z = z;
+    }
+
+    boolean isInfinity() {
+      return this.z.equals(BigInteger.ZERO);
+    }
+
+    ECPoint toECPoint(BigInteger modulus) {
+      if (isInfinity()) {
+        return ECPoint.POINT_INFINITY;
+      }
+      BigInteger zInv = z.modInverse(modulus);
+      BigInteger zInv2 = zInv.multiply(zInv).mod(modulus);
+      return new ECPoint(
+          x.multiply(zInv2).mod(modulus),
+          y.multiply(zInv2).mod(modulus).multiply(zInv).mod(modulus));
+    }
+
+    static final JacobianEcPoint INFINITY =
+        new JacobianEcPoint(BigInteger.ONE, BigInteger.ONE, BigInteger.ZERO);
+  }
+
+  static JacobianEcPoint toJacobianEcPoint(ECPoint p, BigInteger modulus) {
+    if (p.equals(ECPoint.POINT_INFINITY)) {
+      return JacobianEcPoint.INFINITY;
+    }
+    // Randomize the coordinates to get some protection against timing side channels.
+    // Note that this randomization does not protect against all attacks, since it does not
+    // randomize the value 0. A paper exploiting this is "Zero-Value Point Attacks on Elliptic Curve
+    // Cryptosystem" by T. Akishita and T. Takagi
+    // https://download.hrz.tu-darmstadt.de/pub/FB20/Dekanat/Publikationen/CDC/TI-03-01.zvp.pdf
+    // A consequence of this paper is that this implementation should not be used for ECDH.
+    BigInteger z = new BigInteger(1, Random.randBytes((modulus.bitLength() + 8) / 8)).mod(modulus);
+    BigInteger zz = z.multiply(z).mod(modulus);
+    BigInteger zzz = zz.multiply(z).mod(modulus);
+    return new JacobianEcPoint(
+        p.getAffineX().multiply(zz).mod(modulus), p.getAffineY().multiply(zzz).mod(modulus), z);
+  }
+
+  static JacobianEcPoint doubleJacobianPoint(JacobianEcPoint p, BigInteger a, BigInteger modulus) {
+    // http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl
+    if (p.y.equals(BigInteger.ZERO)) {
+      return JacobianEcPoint.INFINITY;
+    }
+    BigInteger xx = p.x.multiply(p.x).mod(modulus);
+    BigInteger yy = p.y.multiply(p.y).mod(modulus);
+    BigInteger yyyy = yy.multiply(yy).mod(modulus);
+    BigInteger zz = p.z.multiply(p.z).mod(modulus);
+    BigInteger x1yy = p.x.add(yy);
+    BigInteger s = x1yy.multiply(x1yy).mod(modulus).subtract(xx).subtract(yyyy).multiply(TWO);
+    BigInteger m = xx.multiply(THREE).add(a.multiply(zz).multiply(zz).mod(modulus));
+    BigInteger t = m.multiply(m).mod(modulus).subtract(s.multiply(TWO)).mod(modulus);
+    BigInteger x3 = t;
+    BigInteger y3 =
+        m.multiply(s.subtract(t)).mod(modulus).subtract(yyyy.multiply(EIGHT)).mod(modulus);
+    BigInteger y1z1 = p.y.add(p.z);
+    BigInteger z3 = y1z1.multiply(y1z1).mod(modulus).subtract(yy).subtract(zz).mod(modulus);
+    return new JacobianEcPoint(x3, y3, z3);
+  }
+
+  static JacobianEcPoint addJacobianPoints(
+      JacobianEcPoint p1, JacobianEcPoint p2, BigInteger a, BigInteger modulus) {
+    // See http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-2007-bl
+    // and https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates
+    if (p1.isInfinity()) {
+      return p2;
+    }
+    if (p2.isInfinity()) {
+      return p1;
+    }
+    BigInteger z1z1 = p1.z.multiply(p1.z).mod(modulus);
+    BigInteger z2z2 = p2.z.multiply(p2.z).mod(modulus);
+    BigInteger u1 = p1.x.multiply(z2z2).mod(modulus);
+    BigInteger u2 = p2.x.multiply(z1z1).mod(modulus);
+    BigInteger s1 = p1.y.multiply(p2.z).mod(modulus).multiply(z2z2).mod(modulus);
+    BigInteger s2 = p2.y.multiply(p1.z).mod(modulus).multiply(z1z1).mod(modulus);
+    if (u1.equals(u2)) {
+      if (!s1.equals(s2)) {
+        return JacobianEcPoint.INFINITY;
+      } else {
+        return doubleJacobianPoint(p1, a, modulus);
+      }
+    }
+    BigInteger h = u2.subtract(u1).mod(modulus);
+    BigInteger i = h.multiply(FOUR).multiply(h).mod(modulus);
+    BigInteger j = h.multiply(i).mod(modulus);
+    BigInteger r = s2.subtract(s1).multiply(TWO).mod(modulus);
+    BigInteger v = u1.multiply(i).mod(modulus);
+    BigInteger x3 = r.multiply(r).mod(modulus).subtract(j).subtract(v.multiply(TWO)).mod(modulus);
+    BigInteger y3 = r.multiply(v.subtract(x3)).subtract(s1.multiply(TWO).multiply(j)).mod(modulus);
+    BigInteger z12 = p1.z.add(p2.z);
+    BigInteger z3 =
+        z12.multiply(z12).mod(modulus).subtract(z1z1).subtract(z2z2).multiply(h).mod(modulus);
+    return new JacobianEcPoint(x3, y3, z3);
+  }
+
+  private EllipticCurvesUtil() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/EnumTypeProtoConverter.java b/java_src/src/main/java/com/google/crypto/tink/internal/EnumTypeProtoConverter.java
new file mode 100644
index 0000000..d16aae1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/EnumTypeProtoConverter.java
@@ -0,0 +1,86 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Utility class for bidirectional conversion to and from proto enum types. */
+@Immutable
+public final class EnumTypeProtoConverter<E extends Enum<E>, O> {
+
+  /** {@code fromProtoEnumMap} built from {@link java.util.Collections#unmodifiableMap}. */
+  @SuppressWarnings("Immutable")
+  private final Map<E, O> fromProtoEnumMap;
+
+  /** {@code toProtoEnumMap} built from {@link java.util.Collections#unmodifiableMap}. */
+  @SuppressWarnings("Immutable")
+  private final Map<O, E> toProtoEnumMap;
+
+  private EnumTypeProtoConverter(Map<E, O> fromProtoEnumMap, Map<O, E> toProtoEnumMap) {
+    this.fromProtoEnumMap = fromProtoEnumMap;
+    this.toProtoEnumMap = toProtoEnumMap;
+  }
+
+  /** Builds instances of {@link EnumTypeProtoConverter}. */
+  public static final class Builder<E extends Enum<E>, O> {
+    Map<E, O> fromProtoEnumMap = new HashMap<>();
+    Map<O, E> toProtoEnumMap = new HashMap<>();
+
+    private Builder() {}
+
+    /** Adds bidirectional conversion mapping between {@code protoEnum} and {@code objectEnum}. */
+    @CanIgnoreReturnValue
+    public Builder<E, O> add(E protoEnum, O objectEnum) {
+      fromProtoEnumMap.put(protoEnum, objectEnum);
+      toProtoEnumMap.put(objectEnum, protoEnum);
+      return this;
+    }
+
+    public EnumTypeProtoConverter<E, O> build() {
+      return new EnumTypeProtoConverter<>(
+          Collections.unmodifiableMap(fromProtoEnumMap),
+          Collections.unmodifiableMap(toProtoEnumMap));
+    }
+  }
+
+  public static <E extends Enum<E>, O> Builder<E, O> builder() {
+    return new Builder<>();
+  }
+
+  /** Converts {@code objectEnum} to the equivalent proto enum. */
+  public E toProtoEnum(O objectEnum) throws GeneralSecurityException {
+    E protoEnum = toProtoEnumMap.get(objectEnum);
+    if (protoEnum == null) {
+      throw new GeneralSecurityException("Unable to convert object enum: " + objectEnum);
+    }
+    return protoEnum;
+  }
+
+  /** Converts {@code protoEnum} to the equivalent object enum. */
+  public O fromProtoEnum(E protoEnum) throws GeneralSecurityException {
+    O objectEnum = fromProtoEnumMap.get(protoEnum);
+    if (objectEnum == null) {
+      throw new GeneralSecurityException("Unable to convert proto enum: " + protoEnum);
+    }
+    return objectEnum;
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/Field25519.java b/java_src/src/main/java/com/google/crypto/tink/internal/Field25519.java
new file mode 100644
index 0000000..641b2fd
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/Field25519.java
@@ -0,0 +1,595 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.annotations.Alpha;
+import java.util.Arrays;
+
+/**
+ * Defines field 25519 function based on <a
+ * href="https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c">curve25519-donna C
+ * implementation</a> (mostly identical).
+ *
+ * <p>Field elements are written as an array of signed, 64-bit limbs (an array of longs), least
+ * significant first. The value of the field element is:
+ *
+ * <pre>
+ * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
+ * 2^204·x[8] + 2^230·x[9],
+ * </pre>
+ *
+ * <p>i.e. the limbs are 26, 25, 26, 25, ... bits wide.
+ */
+@Alpha
+public final class Field25519 {
+  /**
+   * During Field25519 computation, the mixed radix representation may be in different forms:
+   *
+   * <ul>
+   *   <li>Reduced-size form: the array has size at most 10.
+   *   <li>Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most
+   *       19.
+   * </ul>
+   *
+   * TODO(quannguyen):
+   *
+   * <ul>
+   *   <li>Clarify ill-defined terminologies.
+   *   <li>The reduction procedure is different from DJB's paper
+   *       (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree
+   *       and reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should
+   *       check to see what's going on.
+   *   <li>Consider using method mult() everywhere and making product() private.
+   * </ul>
+   */
+  public static final int FIELD_LEN = 32;
+
+  public static final int LIMB_CNT = 10;
+  private static final long TWO_TO_25 = 1 << 25;
+  private static final long TWO_TO_26 = TWO_TO_25 << 1;
+
+  private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28};
+  private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6};
+  private static final int[] MASK = {0x3ffffff, 0x1ffffff};
+  private static final int[] SHIFT = {26, 25};
+
+  /**
+   * Sums two numbers: output = in1 + in2
+   *
+   * <p>On entry: in1, in2 are in reduced-size form.
+   */
+  static void sum(long[] output, long[] in1, long[] in2) {
+    for (int i = 0; i < LIMB_CNT; i++) {
+      output[i] = in1[i] + in2[i];
+    }
+  }
+
+  /**
+   * Sums two numbers: output += in
+   *
+   * <p>On entry: in is in reduced-size form.
+   */
+  static void sum(long[] output, long[] in) {
+    sum(output, output, in);
+  }
+
+  /**
+   * Find the difference of two numbers: output = in1 - in2 (note the order of the arguments!).
+   *
+   * <p>On entry: in1, in2 are in reduced-size form.
+   */
+  static void sub(long[] output, long[] in1, long[] in2) {
+    for (int i = 0; i < LIMB_CNT; i++) {
+      output[i] = in1[i] - in2[i];
+    }
+  }
+
+  /**
+   * Find the difference of two numbers: output = in - output (note the order of the arguments!).
+   *
+   * <p>On entry: in, output are in reduced-size form.
+   */
+  static void sub(long[] output, long[] in) {
+    sub(output, in, output);
+  }
+
+  /** Multiply a number by a scalar: output = in * scalar */
+  static void scalarProduct(long[] output, long[] in, long scalar) {
+    for (int i = 0; i < LIMB_CNT; i++) {
+      output[i] = in[i] * scalar;
+    }
+  }
+
+  /**
+   * Multiply two numbers: out = in2 * in
+   *
+   * <p>output must be distinct to both inputs. The inputs are reduced coefficient form, the output
+   * is not.
+   *
+   * <p>out[x] <= 14 * the largest product of the input limbs.
+   */
+  static void product(long[] out, long[] in2, long[] in) {
+    out[0] = in2[0] * in[0];
+    out[1] = in2[0] * in[1] + in2[1] * in[0];
+    out[2] = 2 * in2[1] * in[1] + in2[0] * in[2] + in2[2] * in[0];
+    out[3] = in2[1] * in[2] + in2[2] * in[1] + in2[0] * in[3] + in2[3] * in[0];
+    out[4] =
+        in2[2] * in[2] + 2 * (in2[1] * in[3] + in2[3] * in[1]) + in2[0] * in[4] + in2[4] * in[0];
+    out[5] =
+        in2[2] * in[3]
+            + in2[3] * in[2]
+            + in2[1] * in[4]
+            + in2[4] * in[1]
+            + in2[0] * in[5]
+            + in2[5] * in[0];
+    out[6] =
+        2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1])
+            + in2[2] * in[4]
+            + in2[4] * in[2]
+            + in2[0] * in[6]
+            + in2[6] * in[0];
+    out[7] =
+        in2[3] * in[4]
+            + in2[4] * in[3]
+            + in2[2] * in[5]
+            + in2[5] * in[2]
+            + in2[1] * in[6]
+            + in2[6] * in[1]
+            + in2[0] * in[7]
+            + in2[7] * in[0];
+    out[8] =
+        in2[4] * in[4]
+            + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1])
+            + in2[2] * in[6]
+            + in2[6] * in[2]
+            + in2[0] * in[8]
+            + in2[8] * in[0];
+    out[9] =
+        in2[4] * in[5]
+            + in2[5] * in[4]
+            + in2[3] * in[6]
+            + in2[6] * in[3]
+            + in2[2] * in[7]
+            + in2[7] * in[2]
+            + in2[1] * in[8]
+            + in2[8] * in[1]
+            + in2[0] * in[9]
+            + in2[9] * in[0];
+    out[10] =
+        2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1])
+            + in2[4] * in[6]
+            + in2[6] * in[4]
+            + in2[2] * in[8]
+            + in2[8] * in[2];
+    out[11] =
+        in2[5] * in[6]
+            + in2[6] * in[5]
+            + in2[4] * in[7]
+            + in2[7] * in[4]
+            + in2[3] * in[8]
+            + in2[8] * in[3]
+            + in2[2] * in[9]
+            + in2[9] * in[2];
+    out[12] =
+        in2[6] * in[6]
+            + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3])
+            + in2[4] * in[8]
+            + in2[8] * in[4];
+    out[13] =
+        in2[6] * in[7]
+            + in2[7] * in[6]
+            + in2[5] * in[8]
+            + in2[8] * in[5]
+            + in2[4] * in[9]
+            + in2[9] * in[4];
+    out[14] =
+        2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5]) + in2[6] * in[8] + in2[8] * in[6];
+    out[15] = in2[7] * in[8] + in2[8] * in[7] + in2[6] * in[9] + in2[9] * in[6];
+    out[16] = in2[8] * in[8] + 2 * (in2[7] * in[9] + in2[9] * in[7]);
+    out[17] = in2[8] * in[9] + in2[9] * in[8];
+    out[18] = 2 * in2[9] * in[9];
+  }
+
+  /**
+   * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients.
+   *
+   * @param input An input array of any length. If the array has 19 elements, it will be used as
+   *     temporary buffer and its contents changed.
+   * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold.
+   */
+  static void reduce(long[] input, long[] output) {
+    long[] tmp;
+    if (input.length == 19) {
+      tmp = input;
+    } else {
+      tmp = new long[19];
+      System.arraycopy(input, 0, tmp, 0, input.length);
+    }
+    reduceSizeByModularReduction(tmp);
+    reduceCoefficients(tmp);
+    System.arraycopy(tmp, 0, output, 0, LIMB_CNT);
+  }
+
+  /**
+   * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19.
+   *
+   * <p>On entry: |output[i]| < 14*2^54 On exit: |output[0..8]| < 280*2^54
+   */
+  static void reduceSizeByModularReduction(long[] output) {
+    // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19.
+    // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8].
+    //
+    // Each of these shifts and adds ends up multiplying the value by 19.
+    //
+    // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus,
+    // on exit, |output[0..8]| < 280*2^54.
+    output[8] += output[18] << 4;
+    output[8] += output[18] << 1;
+    output[8] += output[18];
+    output[7] += output[17] << 4;
+    output[7] += output[17] << 1;
+    output[7] += output[17];
+    output[6] += output[16] << 4;
+    output[6] += output[16] << 1;
+    output[6] += output[16];
+    output[5] += output[15] << 4;
+    output[5] += output[15] << 1;
+    output[5] += output[15];
+    output[4] += output[14] << 4;
+    output[4] += output[14] << 1;
+    output[4] += output[14];
+    output[3] += output[13] << 4;
+    output[3] += output[13] << 1;
+    output[3] += output[13];
+    output[2] += output[12] << 4;
+    output[2] += output[12] << 1;
+    output[2] += output[12];
+    output[1] += output[11] << 4;
+    output[1] += output[11] << 1;
+    output[1] += output[11];
+    output[0] += output[10] << 4;
+    output[0] += output[10] << 1;
+    output[0] += output[10];
+  }
+
+  /**
+   * Reduce all coefficients of the short form input so that |x| < 2^26.
+   *
+   * <p>On entry: |output[i]| < 280*2^54
+   */
+  static void reduceCoefficients(long[] output) {
+    output[10] = 0;
+
+    for (int i = 0; i < LIMB_CNT; i += 2) {
+      long over = output[i] / TWO_TO_26;
+      // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in
+      // the first iteration of this loop. This is added to the next limb and we can approximate the
+      // resulting bound of that limb by 281*2^54.
+      output[i] -= over << 26;
+      output[i + 1] += over;
+
+      // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is
+      // added to the next limb, the resulting bound can be approximated as 281*2^54.
+      //
+      // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no
+      // overflow occurs.
+      over = output[i + 1] / TWO_TO_25;
+      output[i + 1] -= over << 25;
+      output[i + 2] += over;
+    }
+    // Now |output[10]| < 281*2^29 and all other coefficients are reduced.
+    output[0] += output[10] << 4;
+    output[0] += output[10] << 1;
+    output[0] += output[10];
+
+    output[10] = 0;
+    // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more
+    // than 2^16.
+    long over = output[0] / TWO_TO_26;
+    output[0] -= over << 26;
+    output[1] += over;
+    // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on
+    // |output[1]| is sufficient to meet our needs.
+  }
+
+  /**
+   * A helpful wrapper around {@ref Field25519#product}: output = in * in2.
+   *
+   * <p>On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.
+   *
+   * <p>The output is reduced degree (indeed, one need only provide storage for 10 limbs) and
+   * |output[i]| < 2^26.
+   */
+  static void mult(long[] output, long[] in, long[] in2) {
+    long[] t = new long[19];
+    product(t, in, in2);
+    // |t[i]| < 2^26
+    reduce(t, output);
+  }
+
+  /**
+   * Square a number: out = in**2
+   *
+   * <p>output must be distinct from the input. The inputs are reduced coefficient form, the output
+   * is not.
+   *
+   * <p>out[x] <= 14 * the largest product of the input limbs.
+   */
+  private static void squareInner(long[] out, long[] in) {
+    out[0] = in[0] * in[0];
+    out[1] = 2 * in[0] * in[1];
+    out[2] = 2 * (in[1] * in[1] + in[0] * in[2]);
+    out[3] = 2 * (in[1] * in[2] + in[0] * in[3]);
+    out[4] = in[2] * in[2] + 4 * in[1] * in[3] + 2 * in[0] * in[4];
+    out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]);
+    out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]);
+    out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]);
+    out[8] =
+        in[4] * in[4] + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5]));
+    out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]);
+    out[10] =
+        2 * (in[5] * in[5] + in[4] * in[6] + in[2] * in[8] + 2 * (in[3] * in[7] + in[1] * in[9]));
+    out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]);
+    out[12] = in[6] * in[6] + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9]));
+    out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]);
+    out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]);
+    out[15] = 2 * (in[7] * in[8] + in[6] * in[9]);
+    out[16] = in[8] * in[8] + 4 * in[7] * in[9];
+    out[17] = 2 * in[8] * in[9];
+    out[18] = 2 * in[9] * in[9];
+  }
+
+  /**
+   * Returns in^2.
+   *
+   * <p>On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27.
+   *
+   * <p>On exit: The |output| argument is in reduced coefficients form (indeed, one need only
+   * provide storage for 10 limbs) and |out[i]| < 2^26.
+   */
+  static void square(long[] output, long[] in) {
+    long[] t = new long[19];
+    squareInner(t, in);
+    // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner
+    // adds together, at most, 14 of those products.
+    reduce(t, output);
+  }
+
+  /** Takes a little-endian, 32-byte number and expands it into mixed radix form. */
+  static long[] expand(byte[] input) {
+    long[] output = new long[LIMB_CNT];
+    for (int i = 0; i < LIMB_CNT; i++) {
+      output[i] =
+          ((((long) (input[EXPAND_START[i]] & 0xff))
+                      | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8
+                      | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16
+                      | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24)
+                  >> EXPAND_SHIFT[i])
+              & MASK[i & 1];
+    }
+    return output;
+  }
+
+  /**
+   * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte
+   * array.
+   *
+   * <p>On entry: |input_limbs[i]| < 2^26
+   */
+  @SuppressWarnings("NarrowingCompoundAssignment")
+  public static byte[] contract(long[] inputLimbs) {
+    long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT);
+    for (int j = 0; j < 2; j++) {
+      for (int i = 0; i < 9; i++) {
+        // This calculation is a time-invariant way to make input[i] non-negative by borrowing
+        // from the next-larger limb.
+        int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]);
+        input[i] = input[i] + (carry << SHIFT[i & 1]);
+        input[i + 1] -= carry;
+      }
+
+      // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow
+      // from input[0], which is valid mod 2^255-19.
+      {
+        int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25);
+        input[9] += (carry << 25);
+        input[0] -= (carry * 19L);
+      }
+
+      // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits,
+      // depending on position. However, input[0] may be negative.
+    }
+
+    // The first borrow-propagation pass above ended with every limb except (possibly) input[0]
+    // non-negative.
+    //
+    // If input[0] was negative after the first pass, then it was because of a carry from input[9].
+    // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus
+    // input[0] >= -19.
+    //
+    // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation
+    // pass could only have wrapped around to decrease input[0] again if the first pass left
+    // input[0] negative *and* input[1] through input[9] were all zero.  In that case, input[1] is
+    // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative.
+    {
+      int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26);
+      input[0] += (carry << 26);
+      input[1] -= carry;
+    }
+
+    // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a
+    // limb which is, nominally, 25 bits wide.
+    for (int j = 0; j < 2; j++) {
+      for (int i = 0; i < 9; i++) {
+        int carry = (int) (input[i] >> SHIFT[i & 1]);
+        input[i] &= MASK[i & 1];
+        input[i + 1] += carry;
+      }
+    }
+
+    {
+      int carry = (int) (input[9] >> 25);
+      input[9] &= 0x1ffffff;
+      input[0] += 19L * carry;
+    }
+
+    // If the first carry-chain pass, just above, ended up with a carry from input[9], and that
+    // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was,
+    // at most, two.
+    //
+    // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] ->
+    // input[0] carry didn't push input[0] out of bounds.
+
+    // It still remains the case that input might be between 2^255-19 and 2^255. In this case,
+    // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff,
+    // which is 0x3ffffed.
+    int mask = gte((int) input[0], 0x3ffffed);
+    for (int i = 1; i < LIMB_CNT; i++) {
+      mask &= eq((int) input[i], MASK[i & 1]);
+    }
+
+    // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally
+    // subtracts 2^255-19.
+    input[0] -= mask & 0x3ffffed;
+    input[1] -= mask & 0x1ffffff;
+    for (int i = 2; i < LIMB_CNT; i += 2) {
+      input[i] -= mask & 0x3ffffff;
+      input[i + 1] -= mask & 0x1ffffff;
+    }
+
+    for (int i = 0; i < LIMB_CNT; i++) {
+      input[i] <<= EXPAND_SHIFT[i];
+    }
+    byte[] output = new byte[FIELD_LEN];
+    for (int i = 0; i < LIMB_CNT; i++) {
+      output[EXPAND_START[i]] |= input[i] & 0xff;
+      output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff;
+      output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff;
+      output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff;
+    }
+    return output;
+  }
+
+  /**
+   * Computes inverse of z = z(2^255 - 21)
+   *
+   * <p>Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the
+   * comment format and the variable namings are different from those.
+   */
+  static void inverse(long[] out, long[] z) {
+    long[] z2 = new long[Field25519.LIMB_CNT];
+    long[] z9 = new long[Field25519.LIMB_CNT];
+    long[] z11 = new long[Field25519.LIMB_CNT];
+    long[] z2To5Minus1 = new long[Field25519.LIMB_CNT];
+    long[] z2To10Minus1 = new long[Field25519.LIMB_CNT];
+    long[] z2To20Minus1 = new long[Field25519.LIMB_CNT];
+    long[] z2To50Minus1 = new long[Field25519.LIMB_CNT];
+    long[] z2To100Minus1 = new long[Field25519.LIMB_CNT];
+    long[] t0 = new long[Field25519.LIMB_CNT];
+    long[] t1 = new long[Field25519.LIMB_CNT];
+
+    square(z2, z); // 2
+    square(t1, z2); // 4
+    square(t0, t1); // 8
+    mult(z9, t0, z); // 9
+    mult(z11, z9, z2); // 11
+    square(t0, z11); // 22
+    mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31
+
+    square(t0, z2To5Minus1); // 2^6 - 2^1
+    square(t1, t0); // 2^7 - 2^2
+    square(t0, t1); // 2^8 - 2^3
+    square(t1, t0); // 2^9 - 2^4
+    square(t0, t1); // 2^10 - 2^5
+    mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0
+
+    square(t0, z2To10Minus1); // 2^11 - 2^1
+    square(t1, t0); // 2^12 - 2^2
+    for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10
+      square(t0, t1);
+      square(t1, t0);
+    }
+    mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0
+
+    square(t0, z2To20Minus1); // 2^21 - 2^1
+    square(t1, t0); // 2^22 - 2^2
+    for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20
+      square(t0, t1);
+      square(t1, t0);
+    }
+    mult(t0, t1, z2To20Minus1); // 2^40 - 2^0
+
+    square(t1, t0); // 2^41 - 2^1
+    square(t0, t1); // 2^42 - 2^2
+    for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10
+      square(t1, t0);
+      square(t0, t1);
+    }
+    mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0
+
+    square(t0, z2To50Minus1); // 2^51 - 2^1
+    square(t1, t0); // 2^52 - 2^2
+    for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50
+      square(t0, t1);
+      square(t1, t0);
+    }
+    mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0
+
+    square(t1, z2To100Minus1); // 2^101 - 2^1
+    square(t0, t1); // 2^102 - 2^2
+    for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100
+      square(t1, t0);
+      square(t0, t1);
+    }
+    mult(t1, t0, z2To100Minus1); // 2^200 - 2^0
+
+    square(t0, t1); // 2^201 - 2^1
+    square(t1, t0); // 2^202 - 2^2
+    for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50
+      square(t0, t1);
+      square(t1, t0);
+    }
+    mult(t0, t1, z2To50Minus1); // 2^250 - 2^0
+
+    square(t1, t0); // 2^251 - 2^1
+    square(t0, t1); // 2^252 - 2^2
+    square(t1, t0); // 2^253 - 2^3
+    square(t0, t1); // 2^254 - 2^4
+    square(t1, t0); // 2^255 - 2^5
+    mult(out, t1, z11); // 2^255 - 21
+  }
+
+  /** Returns 0xffffffff iff a == b and zero otherwise. */
+  private static int eq(int a, int b) {
+    a = ~(a ^ b);
+    a &= a << 16;
+    a &= a << 8;
+    a &= a << 4;
+    a &= a << 2;
+    a &= a << 1;
+    return a >> 31;
+  }
+
+  /** returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative. */
+  private static int gte(int a, int b) {
+    a -= b;
+    // a >= 0 iff a >= b.
+    return ~(a >> 31);
+  }
+
+  private Field25519() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/InternalConfiguration.java b/java_src/src/main/java/com/google/crypto/tink/internal/InternalConfiguration.java
new file mode 100644
index 0000000..be5c422
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/InternalConfiguration.java
@@ -0,0 +1,99 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.Configuration;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.proto.KeyData;
+import java.security.GeneralSecurityException;
+
+/**
+ * Abstract class representing the real configuration API, i.e. all algorithms that Tink
+ * understands. Internal. Users should not access these methods since the operations are to be used
+ * by internal KeysetHandle operations only.
+ */
+public abstract class InternalConfiguration extends Configuration {
+  /**
+   * Creates a primitive from a key in the old (proto) format.
+   */
+  public abstract <P> P getLegacyPrimitive(KeyData keyData, Class<P> primitiveClass)
+      throws GeneralSecurityException;
+
+  /**
+   * Given a key and a desired primitive class, creates the required primitive.
+   */
+  public abstract <P> P getPrimitive(Key key, Class<P> primitiveClass)
+      throws GeneralSecurityException;
+
+  /**
+   * Wraps the primitives in the primitive set into the provided class.
+   *
+   * @throws GeneralSecurityException if the wrapper for the provided pair
+   * (input class, wrapped class) is not registered
+   */
+  public abstract <B, P> P wrap(PrimitiveSet<B> primitiveSet, Class<P> clazz)
+      throws GeneralSecurityException;
+
+  /**
+   * Given the target class, reveals primitive set of what type should be provided to the
+   * {@link InternalConfiguration.wrap} method in order to get a wrapped object of the target class.
+   */
+  public abstract Class<?> getInputPrimitiveClass(Class<?> wrapperClassObject)
+      throws GeneralSecurityException;
+
+  public static InternalConfiguration createFromPrimitiveRegistry(PrimitiveRegistry registry) {
+    return new InternalConfigurationImpl(registry);
+  }
+
+  /**
+   * Implementation of the configuration API.
+   */
+  private static class InternalConfigurationImpl extends InternalConfiguration {
+    /**
+     * Immutable registry instance.
+     */
+    private final PrimitiveRegistry registry;
+
+    private InternalConfigurationImpl(PrimitiveRegistry registry) {
+      this.registry = registry;
+    }
+
+    @Override
+    public <P> P getLegacyPrimitive(KeyData keyData, Class<P> primitiveClass)
+        throws GeneralSecurityException {
+      throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public <P> P getPrimitive(Key key, Class<P> primitiveClass) throws GeneralSecurityException {
+      return registry.getPrimitive(key, primitiveClass);
+    }
+
+    @Override
+    public Class<?> getInputPrimitiveClass(Class<?> wrapperClassObject)
+        throws GeneralSecurityException {
+      return registry.getInputPrimitiveClass(wrapperClassObject);
+    }
+
+    @Override
+    public <B, P> P wrap(PrimitiveSet<B> primitiveSet, Class<P> clazz)
+        throws GeneralSecurityException {
+      return registry.wrap(primitiveSet, clazz);
+    }
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/JsonParser.java b/java_src/src/main/java/com/google/crypto/tink/internal/JsonParser.java
new file mode 100644
index 0000000..6d5d138
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/JsonParser.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2011 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.crypto.tink.internal;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import javax.annotation.Nullable;
+
+/**
+ * A JSON Parser based on the GSON JsonReader.
+ *
+ * <p>The parsing is almost identical to the normal parser provided by GSON with these changes: it
+ * never uses "lenient" mode, it rejects duplicated map keys and it rejects strings with invalid
+ * UTF16 characters.
+ *
+ * <p>The implementation is adapted from almost identical to GSON's TypeAdapters.JSON_ELEMENT.
+ */
+public final class JsonParser {
+
+  public static boolean isValidString(String s) {
+    int length = s.length();
+    int i = 0;
+    while (true) {
+      char ch;
+      do {
+        if (i == length) {
+          return true;
+        }
+        ch = s.charAt(i);
+        i++;
+      } while (!Character.isSurrogate(ch));
+      if (Character.isLowSurrogate(ch) || i == length || !Character.isLowSurrogate(s.charAt(i))) {
+        return false;
+      }
+      i++;
+    }
+  }
+
+  /** This is a modified copy of Gson's internal LazilyParsedNumber. */
+  @SuppressWarnings("serial") // Serialization is not supported. Throws NotSerializableException
+  private static final class LazilyParsedNumber extends Number {
+    private final String value;
+
+    public LazilyParsedNumber(String value) {
+      this.value = value;
+    }
+
+    @Override
+    public int intValue() {
+      try {
+        return Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        try {
+          return (int) Long.parseLong(value);
+        } catch (NumberFormatException nfe) {
+          return new BigDecimal(value).intValue();
+        }
+      }
+    }
+
+    @Override
+    public long longValue() {
+      try {
+        return Long.parseLong(value);
+      } catch (NumberFormatException e) {
+        return new BigDecimal(value).longValue();
+      }
+    }
+
+    @Override
+    public float floatValue() {
+      return Float.parseFloat(value);
+    }
+
+    @Override
+    public double doubleValue() {
+      return Double.parseDouble(value);
+    }
+
+    @Override
+    public String toString() {
+      return value;
+    }
+
+    private Object writeReplace() throws NotSerializableException {
+      throw new NotSerializableException("serialization is not supported");
+    }
+
+    private void readObject(ObjectInputStream in) throws NotSerializableException {
+      throw new NotSerializableException("serialization is not supported");
+    }
+
+    @Override
+    public int hashCode() {
+      return value.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj instanceof LazilyParsedNumber) {
+        LazilyParsedNumber other = (LazilyParsedNumber) obj;
+        return value.equals(other.value);
+      }
+      return false;
+    }
+  }
+
+  private static final class JsonElementTypeAdapter extends TypeAdapter<JsonElement> {
+
+    private static final int RECURSION_LIMIT = 100;
+
+    /**
+     * Tries to begin reading a JSON array or JSON object, returning {@code null} if the next
+     * element is neither of those.
+     */
+    @Nullable
+    private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
+      switch (peeked) {
+        case BEGIN_ARRAY:
+          in.beginArray();
+          return new JsonArray();
+        case BEGIN_OBJECT:
+          in.beginObject();
+          return new JsonObject();
+        default:
+          return null;
+      }
+    }
+
+    /** Reads a {@link JsonElement} which cannot have any nested elements */
+    private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException {
+      switch (peeked) {
+        case STRING:
+          String value = in.nextString();
+          if (!isValidString(value)) {
+            throw new IOException("illegal characters in string");
+          }
+          return new JsonPrimitive(value);
+        case NUMBER:
+          String number = in.nextString();
+          return new JsonPrimitive(new LazilyParsedNumber(number));
+        case BOOLEAN:
+          return new JsonPrimitive(in.nextBoolean());
+        case NULL:
+          in.nextNull();
+          return JsonNull.INSTANCE;
+        default:
+          // When read(JsonReader) is called with JsonReader in invalid state
+          throw new IllegalStateException("Unexpected token: " + peeked);
+      }
+    }
+
+    @Override
+    public JsonElement read(JsonReader in) throws IOException {
+      // Either JsonArray or JsonObject
+      JsonElement current;
+      JsonToken peeked = in.peek();
+
+      current = tryBeginNesting(in, peeked);
+      if (current == null) {
+        return readTerminal(in, peeked);
+      }
+
+      Deque<JsonElement> stack = new ArrayDeque<>();
+
+      while (true) {
+        while (in.hasNext()) {
+          String name = null;
+          // Name is only used for JSON object members
+          if (current instanceof JsonObject) {
+            name = in.nextName();
+            if (!isValidString(name)) {
+              throw new IOException("illegal characters in string");
+            }
+          }
+
+          peeked = in.peek();
+          JsonElement value = tryBeginNesting(in, peeked);
+          boolean isNesting = value != null;
+
+          if (value == null) {
+            value = readTerminal(in, peeked);
+          }
+
+          if (current instanceof JsonArray) {
+            ((JsonArray) current).add(value);
+          } else {
+            if (((JsonObject) current).has(name)) {
+              throw new IOException("duplicate key: " + name);
+            }
+            ((JsonObject) current).add(name, value);
+          }
+
+          if (isNesting) {
+            stack.addLast(current);
+            if (stack.size() > RECURSION_LIMIT) {
+               throw new IOException("too many recursions");
+            }
+            current = value;
+          }
+        }
+
+        // End current element
+        if (current instanceof JsonArray) {
+          in.endArray();
+        } else {
+          in.endObject();
+        }
+
+        if (stack.isEmpty()) {
+          return current;
+        } else {
+          // Continue with enclosing element
+          current = stack.removeLast();
+        }
+      }
+    }
+
+    @Override
+    public void write(JsonWriter out, JsonElement value) {
+      throw new UnsupportedOperationException("write is not supported");
+    }
+  }
+
+  private static final JsonElementTypeAdapter JSON_ELEMENT = new JsonElementTypeAdapter();
+
+  public static JsonElement parse(String json) throws IOException {
+    try {
+      JsonReader jsonReader = new JsonReader(new StringReader(json));
+      jsonReader.setLenient(false);
+      return JSON_ELEMENT.read(jsonReader);
+    } catch (NumberFormatException e) {
+      throw new IOException(e);
+    }
+  }
+
+  /*
+   * Converts a parsed {@link JsonElement} into a long if it contains a valid long value.
+   *
+   * <p>Requires that {@code element} is part of a output produced by {@link #parse}.
+   *
+   * @throws NumberFormatException if {@code element} does not contain a valid long value.
+   *
+   */
+  public static long getParsedNumberAsLongOrThrow(JsonElement element) {
+    Number num = element.getAsNumber();
+    if (!(num instanceof LazilyParsedNumber)) {
+      // We restrict this function to LazilyParsedNumber because then we know that "toString" will
+      // return the unparsed number. For other implementations of Number interface, it is not
+      // clearly defined what toString will return.
+      throw new IllegalArgumentException("does not contain a parsed number.");
+    }
+    return Long.parseLong(element.getAsNumber().toString());
+  }
+
+  private JsonParser() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/KeyTemplateProtoConverter.java b/java_src/src/main/java/com/google/crypto/tink/internal/KeyTemplateProtoConverter.java
index dbd5a31..a130fed 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/KeyTemplateProtoConverter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/KeyTemplateProtoConverter.java
@@ -17,8 +17,8 @@
 package com.google.crypto.tink.internal;
 
 import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.Parameters;
 import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
 import java.security.GeneralSecurityException;
@@ -42,28 +42,16 @@
     }
   }
 
-  private static OutputPrefixType prefixToProto(KeyTemplate.OutputPrefixType outputPrefixType)
-      throws GeneralSecurityException {
-    switch (outputPrefixType) {
-      case TINK:
-        return OutputPrefixType.TINK;
-      case LEGACY:
-        return OutputPrefixType.LEGACY;
-      case RAW:
-        return OutputPrefixType.RAW;
-      case CRUNCHY:
-        return OutputPrefixType.CRUNCHY;
-    }
-    throw new GeneralSecurityException("Unknown output prefix type");
-  }
-
   public static com.google.crypto.tink.proto.KeyTemplate toProto(KeyTemplate keyTemplate)
       throws GeneralSecurityException {
-    return com.google.crypto.tink.proto.KeyTemplate.newBuilder()
-        .setTypeUrl(keyTemplate.getTypeUrl())
-        .setValue(ByteString.copyFrom(keyTemplate.getValue()))
-        .setOutputPrefixType(prefixToProto(keyTemplate.getOutputPrefixType()))
-        .build();
+    Parameters parameters = keyTemplate.toParameters();
+    if (parameters instanceof LegacyProtoParameters) {
+      return ((LegacyProtoParameters) parameters).getSerialization().getKeyTemplate();
+    }
+    ProtoParametersSerialization s =
+        MutableSerializationRegistry.globalInstance()
+            .serializeParameters(parameters, ProtoParametersSerialization.class);
+    return s.getKeyTemplate();
   }
 
   public static byte[] toByteArray(KeyTemplate keyTemplate) throws GeneralSecurityException {
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/KeyTester.java b/java_src/src/main/java/com/google/crypto/tink/internal/KeyTester.java
index d984cb9..b77f451 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/KeyTester.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/KeyTester.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.crypto.tink.Key;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -48,6 +49,7 @@
 public final class KeyTester {
   private final Map<String, List<Key>> equivalenceGroups = new HashMap<>();
 
+  @CanIgnoreReturnValue
   public KeyTester addEqualityGroup(String name, Key... keys) {
     if (equivalenceGroups.containsKey(name)) {
       throw new AssertionError("Group with name " + name + " already present");
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/KeyTypeManager.java b/java_src/src/main/java/com/google/crypto/tink/internal/KeyTypeManager.java
index d10133b..c86a18c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/KeyTypeManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/KeyTypeManager.java
@@ -23,6 +23,7 @@
 import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.MessageLite;
+import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Collections;
@@ -241,6 +242,29 @@
     public Map<String, KeyFormat<KeyFormatProtoT>> keyFormats() throws GeneralSecurityException {
       return Collections.emptyMap();
     }
+
+    /**
+     * Reads {@code output.length} number of bytes of (pseudo)randomness from the {@code input}
+     * stream into the provided {@code output} buffer.
+     *
+     * Note that this method will not close the {@code input} stream.
+     *
+     * @throws GeneralSecurityException when not enough randomness was provided in the {@code input}
+     *     stream.
+     */
+    protected static void readFully(InputStream input, byte[] output)
+        throws IOException, GeneralSecurityException {
+      int len = output.length;
+      int read;
+      int readTotal = 0;
+      while (readTotal < len) {
+        read = input.read(output, readTotal, len - readTotal);
+        if (read == -1) {
+          throw new GeneralSecurityException("Not enough pseudorandomness provided");
+        }
+        readTotal += read;
+      }
+    }
   }
 
   /**
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/MonitoringUtil.java b/java_src/src/main/java/com/google/crypto/tink/internal/MonitoringUtil.java
index 03ff136..cad27da 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/MonitoringUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/MonitoringUtil.java
@@ -51,12 +51,25 @@
     }
   }
 
+  private static final String TYPE_URL_PREFIX = "type.googleapis.com/google.crypto.";
+
+  private static String parseKeyTypeUrl(String keyTypeUrl) {
+    if (!keyTypeUrl.startsWith(TYPE_URL_PREFIX)) {
+      return keyTypeUrl;
+    }
+    return keyTypeUrl.substring(TYPE_URL_PREFIX.length());
+  }
+
   public static <P> MonitoringKeysetInfo getMonitoringKeysetInfo(PrimitiveSet<P> primitiveSet) {
     MonitoringKeysetInfo.Builder builder = MonitoringKeysetInfo.newBuilder();
     builder.setAnnotations(primitiveSet.getAnnotations());
     for (List<PrimitiveSet.Entry<P>> entries : primitiveSet.getAll()) {
       for (PrimitiveSet.Entry<P> entry : entries) {
-        builder.addEntry(parseStatus(entry.getStatus()), entry.getKeyId(), entry.getParameters());
+        builder.addEntry(
+            parseStatus(entry.getStatus()),
+            entry.getKeyId(),
+            parseKeyTypeUrl(entry.getKeyType()),
+            entry.getOutputPrefixType().name());
       }
     }
     @Nullable PrimitiveSet.Entry<P> primary = primitiveSet.getPrimary();
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/MutablePrimitiveRegistry.java b/java_src/src/main/java/com/google/crypto/tink/internal/MutablePrimitiveRegistry.java
new file mode 100644
index 0000000..50e3d4e
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/MutablePrimitiveRegistry.java
@@ -0,0 +1,100 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveWrapper;
+import java.security.GeneralSecurityException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A Mutable version of the {@link PrimitiveRegistry}.
+ *
+ * <p>This class probably shouldn't exist; it would be better if we had only the
+ * PrimitiveRegistry. However, at the moment, we need this, since a call to e.g.
+ *
+ * <pre> AesCmacKeyManager.register() </pre>
+ *
+ * should register such an object into a global, mutable registry.
+ */
+public final class MutablePrimitiveRegistry {
+  private static MutablePrimitiveRegistry globalInstance =
+      new MutablePrimitiveRegistry();
+
+  public static MutablePrimitiveRegistry globalInstance() {
+    return globalInstance;
+  }
+
+  public static void resetGlobalInstanceTestOnly() {
+    globalInstance = new MutablePrimitiveRegistry();
+  }
+
+  private final AtomicReference<PrimitiveRegistry> registry =
+      new AtomicReference<>(PrimitiveRegistry.builder().build());
+
+  MutablePrimitiveRegistry() {}
+
+  /**
+   * Registers a key primitive constructor for later use in {@link #getPrimitive}.
+   *
+   * <p>This registers a primitive constructor which can later be used to create a primitive by
+   * calling {@link #getPrimitive}. If a constructor for the pair {@code (KeyT, PrimitiveT)} has
+   * already been registered and is the same, then the call is ignored; otherwise, an exception is
+   * thrown.
+   */
+  public synchronized <KeyT extends Key, PrimitiveT> void registerPrimitiveConstructor(
+      PrimitiveConstructor<KeyT, PrimitiveT> constructor) throws GeneralSecurityException {
+    PrimitiveRegistry newRegistry =
+        PrimitiveRegistry.builder(registry.get())
+            .registerPrimitiveConstructor(constructor)
+            .build();
+    registry.set(newRegistry);
+  }
+
+  public synchronized <InputPrimitiveT, WrapperPrimitiveT> void registerPrimitiveWrapper(
+      PrimitiveWrapper<InputPrimitiveT, WrapperPrimitiveT> wrapper)
+      throws GeneralSecurityException {
+    PrimitiveRegistry newRegistry =
+        PrimitiveRegistry.builder(registry.get()).registerPrimitiveWrapper(wrapper).build();
+    registry.set(newRegistry);
+  }
+
+  /**
+   * Creates a primitive from a given key.
+   *
+   * <p>This will look up a previously registered constructor for the given pair of {@code (KeyT,
+   * PrimitiveT)}, and, if successful, use the registered PrimitiveConstructor object to create the
+   * requested primitive. Throws if the required constructor has not been registered, or if the
+   * primitive construction threw.
+   */
+  public <KeyT extends Key, PrimitiveT> PrimitiveT getPrimitive(
+      KeyT key, Class<PrimitiveT> primitiveClass) throws GeneralSecurityException {
+    return registry.get().getPrimitive(key, primitiveClass);
+  }
+
+  public <WrapperPrimitiveT> Class<?> getInputPrimitiveClass(
+      Class<WrapperPrimitiveT> wrapperClassObject) throws GeneralSecurityException {
+    return registry.get().getInputPrimitiveClass(wrapperClassObject);
+  }
+
+  public <InputPrimitiveT, WrapperPrimitiveT> WrapperPrimitiveT wrap(
+      PrimitiveSet<InputPrimitiveT> primitives, Class<WrapperPrimitiveT> wrapperClassObject)
+      throws GeneralSecurityException {
+    return registry.get().wrap(primitives, wrapperClassObject);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/MutableSerializationRegistry.java b/java_src/src/main/java/com/google/crypto/tink/internal/MutableSerializationRegistry.java
index 6709ebd..7a569e8 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/MutableSerializationRegistry.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/MutableSerializationRegistry.java
@@ -16,6 +16,8 @@
 
 package com.google.crypto.tink.internal;
 
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
 import com.google.crypto.tink.Key;
 import com.google.crypto.tink.Parameters;
 import com.google.crypto.tink.SecretKeyAccess;
@@ -34,8 +36,17 @@
  * should register such an object into a global, mutable registry.
  */
 public final class MutableSerializationRegistry {
+  private static MutableSerializationRegistry createGlobalInstance()
+      throws GeneralSecurityException {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    registry.registerKeySerializer(
+        KeySerializer.create(
+            LegacyProtoKey::getSerialization, LegacyProtoKey.class, ProtoKeySerialization.class));
+    return registry;
+  }
+
   private static final MutableSerializationRegistry GLOBAL_INSTANCE =
-      new MutableSerializationRegistry();
+      exceptionIsBug(MutableSerializationRegistry::createGlobalInstance);
 
   public static MutableSerializationRegistry globalInstance() {
     return GLOBAL_INSTANCE;
@@ -111,6 +122,12 @@
     registry.set(newRegistry);
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <SerializationT extends Serialization> boolean hasParserForKey(
+      SerializationT serializedKey) {
+    return registry.get().hasParserForKey(serializedKey);
+  }
+
   /**
    * Parses the given serialization into a Key.
    *
@@ -131,24 +148,23 @@
    * types for which we did not yet register a parser; in this case we simply fall back to return a
    * LegacyProtoKey.
    *
-   * <p>This always requires SecretKeyAccess. This guarantees that it cannot fail (every
-   * ProtoKeySerialization can be parsed into a LegacyProtoKey).
+   * @throws GeneralSecurityException if a parser is registered but parsing fails or required
+   *     SecretKeyAccess is missing
    */
   public Key parseKeyWithLegacyFallback(
-      ProtoKeySerialization protoKeySerialization, SecretKeyAccess access) {
-    if (access == null) {
-      throw new NullPointerException("access cannot be null");
+      ProtoKeySerialization protoKeySerialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!hasParserForKey(protoKeySerialization)) {
+      return new LegacyProtoKey(protoKeySerialization, access);
     }
-    try {
-      return parseKey(protoKeySerialization, access);
-    } catch (GeneralSecurityException e) {
-      try {
-        return new LegacyProtoKey(protoKeySerialization, access);
-      } catch (GeneralSecurityException e2) {
-        // Cannot happen -- this only throws if we have no access.
-        throw new TinkBugException("Creating a LegacyProtoKey failed", e2);
-      }
-    }
+    return parseKey(protoKeySerialization, access);
+  }
+
+
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <KeyT extends Key, SerializationT extends Serialization> boolean hasSerializerForKey(
+      KeyT key, Class<SerializationT> serializationClass) {
+    return registry.get().hasSerializerForKey(key, serializationClass);
   }
 
   /**
@@ -163,6 +179,12 @@
     return registry.get().serializeKey(key, serializationClass, access);
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <SerializationT extends Serialization> boolean hasParserForParameters(
+      SerializationT serializedParameters) {
+    return registry.get().hasParserForParameters(serializedParameters);
+  }
+
   /**
    * Parses the given serialization into a Parameters object.
    *
@@ -183,14 +205,21 @@
    * registered (e.g. for when we create a Key from a key template/parameters name object).
    */
   public Parameters parseParametersWithLegacyFallback(
-      ProtoParametersSerialization protoParametersSerialization) {
-    try {
-      return parseParameters(protoParametersSerialization);
-    } catch (GeneralSecurityException e) {
+      ProtoParametersSerialization protoParametersSerialization) throws GeneralSecurityException {
+    if (!hasParserForParameters(protoParametersSerialization)) {
       return new LegacyProtoParameters(protoParametersSerialization);
+    } else {
+      return parseParameters(protoParametersSerialization);
     }
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <ParametersT extends Parameters, SerializationT extends Serialization>
+      boolean hasSerializerForParameters(
+          ParametersT parameters, Class<SerializationT> serializationClass) {
+    return registry.get().hasSerializerForParameters(parameters, serializationClass);
+  }
+
   /**
    * Serializes a given Parameters object into a "SerializationT" object.
    *
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/PrimitiveConstructor.java b/java_src/src/main/java/com/google/crypto/tink/internal/PrimitiveConstructor.java
new file mode 100644
index 0000000..fb5f4f6
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/PrimitiveConstructor.java
@@ -0,0 +1,104 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.Key;
+import java.security.GeneralSecurityException;
+
+/**
+ * Create Primitive objects from {@code Key} objects of a certain kind.
+ *
+ * <p>This class should eventually be in Tinks public API -- however, it might still change before
+ * that.
+ * Note: Before making this public, the class name should be reconsidered. (Currently the desirable
+ * option "PrimitiveFactory" is unavailable because such a class already exists.)
+ */
+//TODO(lizatretyakova): reconsider the name before this class becomes public.
+public abstract class PrimitiveConstructor<KeyT extends Key, PrimitiveT> {
+  /**
+   * A function which creates a Primitive object.
+   *
+   * <p>This interface exists only so we have a type we can reference in {@link #create}. Users
+   * should not use this directly; see the explanation in {@link #create}.
+   */
+  public interface PrimitiveConstructionFunction<
+      KeyT extends Key, PrimitiveT> {
+    PrimitiveT constructPrimitive(KeyT key) throws GeneralSecurityException;
+  }
+
+  private final Class<KeyT> keyClass;
+  private final Class<PrimitiveT> primitiveClass;
+
+  private PrimitiveConstructor(
+      Class<KeyT> keyClass, Class<PrimitiveT> primitiveClass) {
+    this.keyClass = keyClass;
+    this.primitiveClass = primitiveClass;
+  }
+
+  public abstract PrimitiveT constructPrimitive(KeyT key)
+      throws GeneralSecurityException;
+
+  public Class<KeyT> getKeyClass() {
+    return keyClass;
+  }
+
+  public Class<PrimitiveT> getPrimitiveClass() {
+    return primitiveClass;
+  }
+
+  /**
+   * Creates a PrimitiveConstructor object.
+   *
+   * <p>This function should only be used by Primitives authors, that is, end users should almost
+   * certainly avoid using this function directly, and instead use their respective Primitive's
+   * {@code register()} method.
+   *
+   * <p>That being said, one typically creates a PrimitiveConstructor object by writing a function
+   *
+   * <pre>{@code
+   * class MyClass {
+   *   private static MyPrimitive getPrimitive(MyKey key)
+   *             throws GeneralSecurityException {
+   *     ...
+   *   }
+   * }
+   * }</pre>
+   *
+   * This function can then be used to create a {@code PrimitiveConstructor}:
+   *
+   * <pre>{@code
+   * PrimitiveConstructor<MyKey, MyPrimitive> serializer =
+   *       PrimitiveConstructor.create(MyClass::getPrimitive, MyKey.class,
+   *                                  MyPrimitive.class);
+   * }</pre>
+   *
+   * -- and the resulting {@code PrimitiveConstructor} object can in turn be registered in a
+   * {@code PrimitiveRegistry}.
+   */
+  public static <KeyT extends Key, PrimitiveT>
+  PrimitiveConstructor<KeyT, PrimitiveT> create(
+      PrimitiveConstructionFunction<KeyT, PrimitiveT> function,
+      Class<KeyT> keyClass,
+      Class<PrimitiveT> primitiveClass) {
+    return new PrimitiveConstructor<KeyT, PrimitiveT>(keyClass, primitiveClass) {
+      @Override
+      public PrimitiveT constructPrimitive(KeyT key) throws GeneralSecurityException {
+        return function.constructPrimitive(key);
+      }
+    };
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/PrimitiveRegistry.java b/java_src/src/main/java/com/google/crypto/tink/internal/PrimitiveRegistry.java
new file mode 100644
index 0000000..4be8c05
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/PrimitiveRegistry.java
@@ -0,0 +1,206 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveWrapper;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Allows registering {@code PrimitiveConstructor} objects, and creating primitives with those
+ * objects.
+ */
+public class PrimitiveRegistry {
+  private final Map<PrimitiveConstructorIndex, PrimitiveConstructor<?, ?>> primitiveConstructorMap;
+  private final Map<Class<?>, PrimitiveWrapper<?, ?>> primitiveWrapperMap;
+
+  /** Allows building PrimitiveRegistry objects. */
+  public static final class Builder {
+    private final Map<PrimitiveConstructorIndex, PrimitiveConstructor<?, ?>>
+        primitiveConstructorMap;
+    private final Map<Class<?>, PrimitiveWrapper<?, ?>> primitiveWrapperMap;
+
+    private Builder() {
+      primitiveConstructorMap = new HashMap<>();
+      primitiveWrapperMap = new HashMap<>();
+    }
+
+    private Builder(PrimitiveRegistry registry) {
+      primitiveConstructorMap = new HashMap<>(registry.primitiveConstructorMap);
+      primitiveWrapperMap = new HashMap<>(registry.primitiveWrapperMap);
+    }
+
+    /**
+     * Registers a primitive constructor for later use in {@link #getPrimitive}.
+     *
+     * <p>This registers a primitive constructor which can later be used to create a primitive
+     * by calling {@link #getPrimitive}. If a constructor for the pair {@code (KeyT, PrimitiveT)}
+     * has already been registered, this checks if they are the same. If they are, the call is
+     * ignored, otherwise an exception is thrown.
+     */
+    @CanIgnoreReturnValue
+    public <KeyT extends Key, PrimitiveT> Builder registerPrimitiveConstructor(
+        PrimitiveConstructor<KeyT, PrimitiveT> primitiveConstructor)
+        throws GeneralSecurityException {
+      if (primitiveConstructor == null) {
+        throw new NullPointerException("primitive constructor must be non-null");
+      }
+      PrimitiveConstructorIndex index =
+          new PrimitiveConstructorIndex(
+              primitiveConstructor.getKeyClass(), primitiveConstructor.getPrimitiveClass());
+      if (primitiveConstructorMap.containsKey(index)) {
+        PrimitiveConstructor<?, ?> existingConstructor = primitiveConstructorMap.get(index);
+        if (!existingConstructor.equals(primitiveConstructor)
+            || !primitiveConstructor.equals(existingConstructor)) {
+          throw new GeneralSecurityException(
+              "Attempt to register non-equal PrimitiveConstructor object for already existing"
+                  + " object of type: "
+                  + index);
+        }
+      } else {
+        primitiveConstructorMap.put(index, primitiveConstructor);
+      }
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public <InputPrimitiveT, WrapperPrimitiveT> Builder registerPrimitiveWrapper(
+        PrimitiveWrapper<InputPrimitiveT, WrapperPrimitiveT> wrapper)
+        throws GeneralSecurityException {
+      if (wrapper == null) {
+        throw new NullPointerException("wrapper must be non-null");
+      }
+      Class<WrapperPrimitiveT> wrapperClassObject = wrapper.getPrimitiveClass();
+      if (primitiveWrapperMap.containsKey(wrapperClassObject)) {
+        PrimitiveWrapper<?, ?> existingPrimitiveWrapper =
+            primitiveWrapperMap.get(wrapperClassObject);
+        if (!existingPrimitiveWrapper.equals(wrapper)
+            || !wrapper.equals(existingPrimitiveWrapper)) {
+          throw new GeneralSecurityException(
+              "Attempt to register non-equal PrimitiveWrapper object or input class object for"
+                  + " already existing object of type"
+                  + wrapperClassObject);
+        }
+      } else {
+        primitiveWrapperMap.put(wrapperClassObject, wrapper);
+      }
+      return this;
+    }
+
+    public PrimitiveRegistry build() {
+      return new PrimitiveRegistry(this);
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static Builder builder(PrimitiveRegistry registry) {
+    return new Builder(registry);
+  }
+
+  private PrimitiveRegistry(Builder builder) {
+    primitiveConstructorMap = new HashMap<>(builder.primitiveConstructorMap);
+    primitiveWrapperMap = new HashMap<>(builder.primitiveWrapperMap);
+  }
+
+  /**
+   * Creates a primitive from a given key.
+   *
+   * <p>This will look up a previously registered constructor for the given pair of {@code (KeyT,
+   * PrimitiveT)}, and, if successful, use the registered PrimitiveConstructor object to create the
+   * requested primitive. Throws on a failed lookup, or if the primitive construction threw.
+   */
+  public <KeyT extends Key, PrimitiveT> PrimitiveT getPrimitive(
+      KeyT key, Class<PrimitiveT> primitiveClass) throws GeneralSecurityException {
+    PrimitiveConstructorIndex index = new PrimitiveConstructorIndex(key.getClass(), primitiveClass);
+    if (!primitiveConstructorMap.containsKey(index)) {
+      throw new GeneralSecurityException("No PrimitiveConstructor for " + index + " available");
+    }
+    @SuppressWarnings("unchecked") // We know we only insert like this.
+    PrimitiveConstructor<KeyT, PrimitiveT> primitiveConstructor =
+        (PrimitiveConstructor<KeyT, PrimitiveT>) primitiveConstructorMap.get(index);
+    return primitiveConstructor.constructPrimitive(key);
+  }
+
+  public Class<?> getInputPrimitiveClass(Class<?> wrapperClassObject)
+      throws GeneralSecurityException {
+    if (!primitiveWrapperMap.containsKey(wrapperClassObject)) {
+      throw new GeneralSecurityException(
+          "No input primitive class for " + wrapperClassObject + " available");
+    }
+    return primitiveWrapperMap.get(wrapperClassObject).getInputPrimitiveClass();
+  }
+
+  public <InputPrimitiveT, WrapperPrimitiveT> WrapperPrimitiveT wrap(
+      PrimitiveSet<InputPrimitiveT> primitives, Class<WrapperPrimitiveT> wrapperClassObject)
+      throws GeneralSecurityException {
+    if (!primitiveWrapperMap.containsKey(wrapperClassObject)) {
+      throw new GeneralSecurityException(
+          "No wrapper found for " + wrapperClassObject);
+    }
+    @SuppressWarnings("unchecked") // We know this is how this map is organized.
+    PrimitiveWrapper<?, WrapperPrimitiveT> wrapper =
+        (PrimitiveWrapper<?, WrapperPrimitiveT>)
+            primitiveWrapperMap.get(wrapperClassObject);
+    if (!primitives.getPrimitiveClass().equals(wrapper.getInputPrimitiveClass())
+        || !wrapper.getInputPrimitiveClass().equals(primitives.getPrimitiveClass())) {
+      throw new GeneralSecurityException(
+          "Input primitive type of the wrapper doesn't match the type of primitives in the provided"
+              + " PrimitiveSet");
+    }
+    @SuppressWarnings("unchecked") // The check above ensured this.
+    PrimitiveWrapper<InputPrimitiveT, WrapperPrimitiveT> typedWrapper =
+        (PrimitiveWrapper<InputPrimitiveT, WrapperPrimitiveT>) wrapper;
+    return typedWrapper.wrap(primitives);
+  }
+
+  private static final class PrimitiveConstructorIndex {
+    private final Class<?> keyClass;
+    private final Class<?> primitiveClass;
+
+    private PrimitiveConstructorIndex(Class<?> keyClass, Class<?> primitiveClass) {
+      this.keyClass = keyClass;
+      this.primitiveClass = primitiveClass;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof PrimitiveConstructorIndex)) {
+        return false;
+      }
+      PrimitiveConstructorIndex other = (PrimitiveConstructorIndex) o;
+      return other.keyClass.equals(keyClass) && other.primitiveClass.equals(primitiveClass);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(keyClass, primitiveClass);
+    }
+
+    @Override
+    public String toString() {
+      return keyClass.getSimpleName() + " with primitive type: " + primitiveClass.getSimpleName();
+    }
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/RegistryConfiguration.java b/java_src/src/main/java/com/google/crypto/tink/internal/RegistryConfiguration.java
new file mode 100644
index 0000000..210dd9d
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/RegistryConfiguration.java
@@ -0,0 +1,83 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.errorprone.annotations.DoNotCall;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the configuration as currently specified by the registry. That is, this configuration
+ * is just a thin layer forwarding calls to the global {@link com.google.crypto.tink.Registry}.
+ *
+ * <p>Because the global {@link com.google.crypto.tink.Registry} changes when user code adds to it,
+ * using this class is not recommended.
+ */
+public final class RegistryConfiguration extends InternalConfiguration {
+  // Returns the singleton instance of RegistryConfiguration.
+  public static RegistryConfiguration get() {
+    return CONFIG;
+  }
+
+  private static final RegistryConfiguration CONFIG = new RegistryConfiguration();
+
+  private RegistryConfiguration() {}
+
+  @Override
+  public <P> P getLegacyPrimitive(KeyData keyData, Class<P> primitiveClass)
+      throws GeneralSecurityException {
+    return Registry.getPrimitive(keyData, primitiveClass);
+  }
+
+  @Override
+  public <P> P getPrimitive(Key key, Class<P> primitiveClass) throws GeneralSecurityException {
+    /* Here we call {@link MutablePrimitiveRegistry} directly and not through the global
+     * {@link com.google.crypto.tink.Registry} because the corresponding method
+     * {@link com.google.crypto.tink.Registry#getFullPrimitive} is package-private.
+     */
+    return MutablePrimitiveRegistry.globalInstance().getPrimitive(key, primitiveClass);
+  }
+
+  @Override
+  public <B, P> P wrap(PrimitiveSet<B> primitiveSet, Class<P> clazz)
+      throws GeneralSecurityException {
+    return Registry.wrap(primitiveSet, clazz);
+  }
+
+  @Override
+  @Nullable
+  public Class<?> getInputPrimitiveClass(Class<?> wrapperClassObject) {
+    return Registry.getInputPrimitive(wrapperClassObject);
+  }
+
+  /**
+   * Do not call.
+   *
+   * <p>We shadow the function {@code createFromPrimitiveRegistry} here so that one cannot invoke
+   * the static function in the superclass by writing {@code
+   * RegistryConfiguration.createFromPrimitiveRegistry}.
+   */
+  @DoNotCall
+  public static InternalConfiguration createFromPrimitiveRegistry(PrimitiveRegistry registry) {
+    throw new UnsupportedOperationException(
+        "Cannot create RegistryConfiguration from a PrimitiveRegistry");
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/SerializationRegistry.java b/java_src/src/main/java/com/google/crypto/tink/internal/SerializationRegistry.java
index 8404fed..96f065b 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/SerializationRegistry.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/SerializationRegistry.java
@@ -20,6 +20,7 @@
 import com.google.crypto.tink.Parameters;
 import com.google.crypto.tink.SecretKeyAccess;
 import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.security.GeneralSecurityException;
 import java.util.HashMap;
 import java.util.Map;
@@ -28,7 +29,7 @@
 
 /**
  * Allows registering {@code KeySerializer}, {@code KeyParser}, {@code ParametersSerializer}, and
- * {@ParametersParser} objects, and parsing/serializing keys and key formats with such objects.
+ * {@link ParametersParser} objects, and parsing/serializing keys and key formats with such objects.
  */
 public final class SerializationRegistry {
   private final Map<SerializerIndex, KeySerializer<?, ?>> keySerializerMap;
@@ -65,6 +66,7 @@
      * already been registered, this checks if they are the same. If they are, the call is ignored,
      * otherwise an exception is thrown.
      */
+    @CanIgnoreReturnValue
     public <KeyT extends Key, SerializationT extends Serialization> Builder registerKeySerializer(
         KeySerializer<KeyT, SerializationT> serializer) throws GeneralSecurityException {
       SerializerIndex index =
@@ -90,6 +92,7 @@
      * parser.getObjectIdentifier())} has already been registered, this checks if they are the same.
      * If they are, the call is ignored, otherwise an exception is thrown.
      */
+    @CanIgnoreReturnValue
     public <SerializationT extends Serialization> Builder registerKeyParser(
         KeyParser<SerializationT> parser) throws GeneralSecurityException {
       ParserIndex index =
@@ -114,6 +117,7 @@
      * already been registered, this checks if they are the same. If they are, the call is ignored,
      * otherwise an exception is thrown.
      */
+    @CanIgnoreReturnValue
     public <ParametersT extends Parameters, SerializationT extends Serialization>
         Builder registerParametersSerializer(
             ParametersSerializer<ParametersT, SerializationT> serializer)
@@ -141,6 +145,7 @@
      * parser.getObjectIdentifier())} has already been registered, this checks if they are the same.
      * If they are, the call is ignored, otherwise an exception is thrown.
      */
+    @CanIgnoreReturnValue
     public <SerializationT extends Serialization> Builder registerParametersParser(
         ParametersParser<SerializationT> parser) throws GeneralSecurityException {
       ParserIndex index =
@@ -235,6 +240,14 @@
     }
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <SerializationT extends Serialization> boolean hasParserForKey(
+      SerializationT serializedKey) {
+    ParserIndex index =
+        new ParserIndex(serializedKey.getClass(), serializedKey.getObjectIdentifier());
+    return keyParserMap.containsKey(index);
+  }
+
   /**
    * Parses the given serialization into a Key.
    *
@@ -257,6 +270,13 @@
     return parser.parseKey(serializedKey, access);
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <KeyT extends Key, SerializationT extends Serialization> boolean hasSerializerForKey(
+      KeyT key, Class<SerializationT> serializationClass) {
+    SerializerIndex index = new SerializerIndex(key.getClass(), serializationClass);
+    return keySerializerMap.containsKey(index);
+  }
+
   /**
    * Serializes a given Key into a "SerializationT" object.
    *
@@ -276,6 +296,15 @@
     return serializer.serializeKey(key, access);
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <SerializationT extends Serialization> boolean hasParserForParameters(
+      SerializationT serializedParameters) {
+    ParserIndex index =
+        new ParserIndex(
+            serializedParameters.getClass(), serializedParameters.getObjectIdentifier());
+    return parametersParserMap.containsKey(index);
+  }
+
   /**
    * Parses the given serialization into a Parameters.
    *
@@ -299,6 +328,14 @@
     return parser.parseParameters(serializedParameters);
   }
 
+  /** Returns true if a parser for this {@code serializedKey} has been registered. */
+  public <ParametersT extends Parameters, SerializationT extends Serialization>
+      boolean hasSerializerForParameters(
+          ParametersT parameters, Class<SerializationT> serializationClass) {
+    SerializerIndex index = new SerializerIndex(parameters.getClass(), serializationClass);
+    return parametersSerializerMap.containsKey(index);
+  }
+
   /**
    * Serializes a given Parameters object into a "SerializationT" object.
    *
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/TinkBugException.java b/java_src/src/main/java/com/google/crypto/tink/internal/TinkBugException.java
index 1fab855..79b62cc 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/TinkBugException.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/TinkBugException.java
@@ -37,4 +37,40 @@
   public TinkBugException(Throwable cause) {
     super(cause);
   }
+
+  /**
+   * A function which returns void and may throw an Exception.
+   *
+   * <p>Named after java.lang.Runnable.
+   */
+  public interface ThrowingRunnable {
+    void run() throws Exception;
+  }
+
+  /**
+   * A function which produces a T and may throw an Exception.
+   *
+   * <p>Named after java.util.function.Supplier.
+   */
+  public interface ThrowingSupplier<T> {
+    T get() throws Exception;
+  }
+
+  /** Swallows an exception and translates it into a TinkBugException. */
+  public static <T> T exceptionIsBug(ThrowingSupplier<T> t) {
+    try {
+      return t.get();
+    } catch (Exception e) {
+      throw new TinkBugException(e);
+    }
+  }
+
+  /** Swallows an exception and translates it into a TinkBugException. */
+  public static void exceptionIsBug(ThrowingRunnable v) {
+    try {
+      v.run();
+    } catch (Exception e) {
+      throw new TinkBugException(e);
+    }
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/Util.java b/java_src/src/main/java/com/google/crypto/tink/internal/Util.java
index 2615967..0b707b2 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/Util.java
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/Util.java
@@ -17,11 +17,15 @@
 package com.google.crypto.tink.internal;
 
 import com.google.crypto.tink.util.Bytes;
+import java.nio.charset.Charset;
 import java.security.SecureRandom;
+import java.util.Objects;
 import javax.annotation.Nullable;
 
 /** Helper functions used throughout Tink, for Tink internal use only. */
 public final class Util {
+  /** Android 18-compatible alternative to {@link java.nio.charset.StandardCharsets#UTF_8}. */
+  public static final Charset UTF_8 = Charset.forName("UTF-8");
 
   /** Returns a positive random int which can be used as a key ID in a keyset. */
   public static int randKeyId() {
@@ -63,9 +67,27 @@
     return Bytes.copyFrom(result);
   }
 
-  /** Returns the current Andrdoid API level as integer or null if we do not run on Android. */
+  /**
+   * Best-effort checks that this is Android.
+   *
+   * <p>Note: this is more tricky than it seems. For example, using reflection to see if
+   * android.os.Build.SDK_INT exists might fail because proguard might break the
+   * reflection part. Using build dispatching can also fail if there are issues in the build graph
+   * (see cl/510374081).
+   *
+   * @return true if running on Android.
+   */
+  public static boolean isAndroid() {
+    // https://developer.android.com/reference/java/lang/System#getProperties%28%29
+    return Objects.equals(System.getProperty("java.vendor"), "The Android Project");
+  }
+
+  /** Returns the current Android API level as integer or null if we do not run on Android. */
   @Nullable
   public static Integer getAndroidApiLevel() {
+    if (!isAndroid()) {
+      return null;
+    }
     return BuildDispatchedCode.getApiLevel();
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/internal/testing/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/internal/testing/BUILD.bazel
index 935f96f..4a2b5cc 100644
--- a/java_src/src/main/java/com/google/crypto/tink/internal/testing/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/internal/testing/BUILD.bazel
@@ -67,7 +67,7 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
     ],
 )
@@ -78,7 +78,7 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
         "@maven//:com_google_truth_truth",
     ],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel
index 981d52e..98c8cd1 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel
@@ -113,6 +113,7 @@
     deps = [
         ":json_util",
         ":jwt_format",
+        ":jwt_hmac_proto_serialization",
         ":jwt_invalid_exception",
         ":jwt_mac_internal",
         ":jwt_validator",
@@ -128,9 +129,9 @@
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -180,8 +181,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pss_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -206,7 +207,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -228,8 +229,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -251,8 +252,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:enums-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -260,6 +261,7 @@
     name = "jwt_ecdsa_sign_key_manager",
     srcs = ["JwtEcdsaSignKeyManager.java"],
     deps = [
+        ":jwt_ecdsa_proto_serialization",
         ":jwt_ecdsa_verify_key_manager",
         ":jwt_format",
         ":jwt_invalid_exception",
@@ -277,7 +279,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:enums",
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -285,6 +287,7 @@
     name = "jwt_ecdsa_sign_key_manager-android",
     srcs = ["JwtEcdsaSignKeyManager.java"],
     deps = [
+        ":jwt_ecdsa_proto_serialization-android",
         ":jwt_ecdsa_verify_key_manager-android",
         ":jwt_format-android",
         ":jwt_invalid_exception-android",
@@ -302,7 +305,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:enums-android",
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -324,8 +327,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -350,7 +353,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -362,6 +365,7 @@
         ":jwt_names",
         ":raw_jwt",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
         "@maven//:com_google_code_gson_gson",
     ],
@@ -371,21 +375,21 @@
     name = "jwk_set_converter",
     srcs = ["JwkSetConverter.java"],
     deps = [
-        ":jwt_format",
         "//proto:jwt_ecdsa_java_proto",
         "//proto:jwt_rsa_ssa_pkcs1_java_proto",
         "//proto:jwt_rsa_ssa_pss_java_proto",
         "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:key_template",
-        "//src/main/java/com/google/crypto/tink:keyset_writer",
+        "//src/main/java/com/google/crypto/tink:key_status",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_access",
-        "//src/main/java/com/google/crypto/tink/tinkkey:key_handle",
-        "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -393,21 +397,21 @@
     name = "jwk_set_converter-android",
     srcs = ["JwkSetConverter.java"],
     deps = [
-        ":jwt_format-android",
         "//proto:jwt_ecdsa_java_proto_lite",
         "//proto:jwt_rsa_ssa_pkcs1_java_proto_lite",
         "//proto:jwt_rsa_ssa_pss_java_proto_lite",
         "//proto:tink_java_proto_lite",
-        "//src/main/java/com/google/crypto/tink:key_template-android",
-        "//src/main/java/com/google/crypto/tink:keyset_writer-android",
+        "//src/main/java/com/google/crypto/tink:key_status-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
         "//src/main/java/com/google/crypto/tink/subtle:base64-android",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_access-android",
-        "//src/main/java/com/google/crypto/tink/tinkkey:key_handle-android",
-        "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -500,6 +504,7 @@
     srcs = ["JsonUtil.java"],
     deps = [
         ":jwt_invalid_exception",
+        "//src/main/java/com/google/crypto/tink/internal:json_parser",
         "@maven//:com_google_code_gson_gson",
     ],
 )
@@ -509,6 +514,7 @@
     srcs = ["JsonUtil.java"],
     deps = [
         ":jwt_invalid_exception-android",
+        "//src/main/java/com/google/crypto/tink/internal:json_parser-android",
         "@maven//:com_google_code_gson_gson",
     ],
 )
@@ -590,6 +596,7 @@
     deps = [
         ":json_util-android",
         ":jwt_format-android",
+        ":jwt_hmac_proto_serialization-android",
         ":jwt_invalid_exception-android",
         ":jwt_mac_internal-android",
         ":jwt_validator-android",
@@ -605,9 +612,9 @@
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -698,7 +705,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -720,8 +727,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_verify_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -746,7 +753,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -768,8 +775,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pss_verify_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -781,6 +788,7 @@
         ":jwt_names-android",
         ":raw_jwt-android",
         "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/subtle:base64-android",
         "@maven//:com_google_code_gson_gson",
     ],
@@ -795,3 +803,325 @@
     name = "jwt_names-android",
     srcs = ["JwtNames.java"],
 )
+
+android_library(
+    name = "jwt_mac_key-android",
+    srcs = ["JwtMacKey.java"],
+    deps = [
+        ":jwt_mac_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+    ],
+)
+
+android_library(
+    name = "jwt_mac_parameters-android",
+    srcs = ["JwtMacParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters-android"],
+)
+
+java_library(
+    name = "jwt_mac_key",
+    srcs = ["JwtMacKey.java"],
+    deps = [
+        ":jwt_mac_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+    ],
+)
+
+java_library(
+    name = "jwt_mac_parameters",
+    srcs = ["JwtMacParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters"],
+)
+
+java_library(
+    name = "jwt_hmac_parameters",
+    srcs = ["JwtHmacParameters.java"],
+    deps = [
+        ":jwt_mac_parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_hmac_parameters-android",
+    srcs = ["JwtHmacParameters.java"],
+    deps = [
+        ":jwt_mac_parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_hmac_key-android",
+    srcs = ["JwtHmacKey.java"],
+    deps = [
+        ":jwt_hmac_parameters-android",
+        ":jwt_mac_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/subtle:base64-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "jwt_hmac_key",
+    srcs = ["JwtHmacKey.java"],
+    deps = [
+        ":jwt_hmac_parameters",
+        ":jwt_mac_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_hmac_proto_serialization-android",
+    srcs = ["JwtHmacProtoSerialization.java"],
+    deps = [
+        ":jwt_hmac_key-android",
+        ":jwt_hmac_parameters-android",
+        "//proto:jwt_hmac_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "jwt_hmac_proto_serialization",
+    srcs = ["JwtHmacProtoSerialization.java"],
+    deps = [
+        ":jwt_hmac_key",
+        ":jwt_hmac_parameters",
+        "//proto:jwt_hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "jwt_signature_parameters-android",
+    srcs = ["JwtSignatureParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters-android"],
+)
+
+android_library(
+    name = "jwt_signature_private_key-android",
+    srcs = ["JwtSignaturePrivateKey.java"],
+    deps = [
+        ":jwt_signature_parameters-android",
+        ":jwt_signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:private_key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_signature_public_key-android",
+    srcs = ["JwtSignaturePublicKey.java"],
+    deps = [
+        ":jwt_signature_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+    ],
+)
+
+java_library(
+    name = "jwt_signature_parameters",
+    srcs = ["JwtSignatureParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters"],
+)
+
+java_library(
+    name = "jwt_signature_private_key",
+    srcs = ["JwtSignaturePrivateKey.java"],
+    deps = [
+        ":jwt_signature_parameters",
+        ":jwt_signature_public_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:private_key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "jwt_signature_public_key",
+    srcs = ["JwtSignaturePublicKey.java"],
+    deps = [
+        ":jwt_signature_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+    ],
+)
+
+android_library(
+    name = "jwt_ecdsa_parameters-android",
+    srcs = ["JwtEcdsaParameters.java"],
+    deps = [
+        ":jwt_signature_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "jwt_ecdsa_parameters",
+    srcs = ["JwtEcdsaParameters.java"],
+    deps = [
+        ":jwt_signature_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_ecdsa_public_key-android",
+    srcs = ["JwtEcdsaPublicKey.java"],
+    deps = [
+        ":jwt_ecdsa_parameters-android",
+        ":jwt_signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/subtle:base64-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "jwt_ecdsa_public_key",
+    srcs = ["JwtEcdsaPublicKey.java"],
+    deps = [
+        ":jwt_ecdsa_parameters",
+        ":jwt_signature_public_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_ecdsa_private_key-android",
+    srcs = ["JwtEcdsaPrivateKey.java"],
+    deps = [
+        ":jwt_ecdsa_parameters-android",
+        ":jwt_ecdsa_public_key-android",
+        ":jwt_signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "jwt_ecdsa_private_key",
+    srcs = ["JwtEcdsaPrivateKey.java"],
+    deps = [
+        ":jwt_ecdsa_parameters",
+        ":jwt_ecdsa_public_key",
+        ":jwt_signature_private_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "jwt_ecdsa_proto_serialization-android",
+    srcs = ["JwtEcdsaProtoSerialization.java"],
+    deps = [
+        ":jwt_ecdsa_parameters-android",
+        ":jwt_ecdsa_private_key-android",
+        ":jwt_ecdsa_public_key-android",
+        "//proto:jwt_ecdsa_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "jwt_ecdsa_proto_serialization",
+    srcs = ["JwtEcdsaProtoSerialization.java"],
+    deps = [
+        ":jwt_ecdsa_parameters",
+        ":jwt_ecdsa_private_key",
+        ":jwt_ecdsa_public_key",
+        "//proto:jwt_ecdsa_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JsonUtil.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JsonUtil.java
index b2fc98f..b76c2aa 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JsonUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JsonUtil.java
@@ -16,14 +16,11 @@
 
 package com.google.crypto.tink.jwt;
 
+import com.google.crypto.tink.internal.JsonParser;
 import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonReader;
-import java.io.StringReader;
-import java.util.Map;
+import java.io.IOException;
 
 /**
  * Helper functions to parse JSON strings, and validate strings.
@@ -31,74 +28,22 @@
 final class JsonUtil {
 
   static boolean isValidString(String s) {
-    int length = s.length();
-    int i = 0;
-    while (true) {
-      char ch;
-      do {
-        if (i == length) {
-          return true;
-        }
-        ch = s.charAt(i);
-        i++;
-      } while (!Character.isSurrogate(ch));
-      if (Character.isLowSurrogate(ch) || i == length || !Character.isLowSurrogate(s.charAt(i))) {
-        return false;
-      }
-      i++;
-    }
+    return JsonParser.isValidString(s);
   }
 
-  private static void validateAllStringsInJsonObject(JsonObject jsonObject)
-      throws JwtInvalidException {
-    for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
-      if (!isValidString(entry.getKey())) {
-        throw new JwtInvalidException("JSON string contains character");
-      }
-      validateAllStringsInJsonElement(entry.getValue());
-    }
-  }
-
-  private static void validateAllStringsInJsonElement(JsonElement element)
-      throws JwtInvalidException {
-    if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
-      if (!isValidString(element.getAsJsonPrimitive().getAsString())) {
-        throw new JwtInvalidException("JSON string contains invalid character");
-      }
-    } else if (element.isJsonObject()) {
-      validateAllStringsInJsonObject(element.getAsJsonObject());
-    } else if (element.isJsonArray()) {
-      validateAllStringsInJsonArray(element.getAsJsonArray());
-    }
-  }
-
-  private static void validateAllStringsInJsonArray(JsonArray jsonArray)
-      throws JwtInvalidException {
-    for (JsonElement element : jsonArray) {
-      validateAllStringsInJsonElement(element);
-    }
-  }
 
   static JsonObject parseJson(String jsonString) throws JwtInvalidException {
     try {
-      JsonReader jsonReader = new JsonReader(new StringReader(jsonString));
-      jsonReader.setLenient(false);
-      JsonObject output = Streams.parse(jsonReader).getAsJsonObject();
-      validateAllStringsInJsonObject(output);
-      return output;
-    } catch (IllegalStateException | JsonParseException | StackOverflowError ex) {
+      return JsonParser.parse(jsonString).getAsJsonObject();
+    } catch (IllegalStateException | JsonParseException  | IOException ex) {
       throw new JwtInvalidException("invalid JSON: " + ex);
     }
   }
 
   static JsonArray parseJsonArray(String jsonString) throws JwtInvalidException {
     try {
-      JsonReader jsonReader = new JsonReader(new StringReader(jsonString));
-      jsonReader.setLenient(false);
-      JsonArray output = Streams.parse(jsonReader).getAsJsonArray();
-      validateAllStringsInJsonArray(output);
-      return output;
-    } catch (IllegalStateException | JsonParseException | StackOverflowError ex) {
+      return JsonParser.parse(jsonString).getAsJsonArray();
+    } catch (IllegalStateException | JsonParseException | IOException ex) {
       throw new JwtInvalidException("invalid JSON: " + ex);
     }
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwkSetConverter.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwkSetConverter.java
index 60f8fe8..41e7bfb 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwkSetConverter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwkSetConverter.java
@@ -16,28 +16,21 @@
 
 package com.google.crypto.tink.jwt;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
+import com.google.crypto.tink.KeyStatus;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetManager;
-import com.google.crypto.tink.KeysetWriter;
-import com.google.crypto.tink.proto.EncryptedKeyset;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
 import com.google.crypto.tink.proto.JwtEcdsaAlgorithm;
 import com.google.crypto.tink.proto.JwtEcdsaPublicKey;
 import com.google.crypto.tink.proto.JwtRsaSsaPkcs1Algorithm;
 import com.google.crypto.tink.proto.JwtRsaSsaPkcs1PublicKey;
 import com.google.crypto.tink.proto.JwtRsaSsaPssAlgorithm;
 import com.google.crypto.tink.proto.JwtRsaSsaPssPublicKey;
-import com.google.crypto.tink.proto.KeyData;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset;
-import com.google.crypto.tink.proto.KeysetInfo;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Base64;
 import com.google.crypto.tink.tinkkey.KeyAccess;
-import com.google.crypto.tink.tinkkey.KeyHandle;
-import com.google.crypto.tink.tinkkey.internal.ProtoKey;
 import com.google.errorprone.annotations.InlineMe;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
@@ -47,12 +40,14 @@
 import com.google.gson.stream.JsonReader;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
-import java.io.ByteArrayOutputStream;
+import com.google.protobuf.InvalidProtocolBufferException;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.io.StringReader;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import java.util.Optional;
+import javax.annotation.Nullable;
 
 /**
  * Provides functions to import and export public Json Web Key (JWK) sets.
@@ -70,9 +65,45 @@
    */
   public static String fromPublicKeysetHandle(KeysetHandle handle)
       throws IOException, GeneralSecurityException {
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    handle.writeNoSecret(new JwkSetWriter(outputStream));
-    return outputStream.toString();
+    // Check validity of the keyset handle before calling "getAt".
+    // See comments in {@link KeysetHandle#Entry#getAt}.
+    handle = KeysetHandle.newBuilder(handle).build();
+    // We never throw a IOException anymore, but keep it in the interface for compatibility.
+    JsonArray keys = new JsonArray();
+    for (int i = 0; i < handle.size(); i++) {
+      KeysetHandle.Entry entry = handle.getAt(i);
+      if (entry.getStatus() != KeyStatus.ENABLED) {
+        continue;
+      }
+      ProtoKeySerialization protoKeySerialization =
+          MutableSerializationRegistry.globalInstance()
+              .serializeKey(entry.getKey(), ProtoKeySerialization.class, /* access= */ null);
+
+      if ((protoKeySerialization.getOutputPrefixType() != OutputPrefixType.RAW)
+          && (protoKeySerialization.getOutputPrefixType() != OutputPrefixType.TINK)) {
+        throw new GeneralSecurityException("only OutputPrefixType RAW and TINK are supported");
+      }
+      if (protoKeySerialization.getKeyMaterialType() != KeyMaterialType.ASYMMETRIC_PUBLIC) {
+        throw new GeneralSecurityException("only public keys can be converted");
+      }
+      switch (protoKeySerialization.getTypeUrl()) {
+        case JWT_ECDSA_PUBLIC_KEY_URL:
+          keys.add(convertJwtEcdsaKey(protoKeySerialization));
+          break;
+        case JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL:
+          keys.add(convertJwtRsaSsaPkcs1(protoKeySerialization));
+          break;
+        case JWT_RSA_SSA_PSS_PUBLIC_KEY_URL:
+          keys.add(convertJwtRsaSsaPss(protoKeySerialization));
+          break;
+        default:
+          throw new GeneralSecurityException(
+              String.format("key type %s is not supported", protoKeySerialization.getTypeUrl()));
+      }
+    }
+    JsonObject jwkSet = new JsonObject();
+    jwkSet.add("keys", keys);
+    return jwkSet.toString();
   }
 
   /**
@@ -84,44 +115,46 @@
    */
   public static KeysetHandle toPublicKeysetHandle(String jwkSet)
       throws IOException, GeneralSecurityException {
+    // We never throw a IOException anymore, but keep it in the interface for compatibility.
     JsonObject jsonKeyset;
     try {
       JsonReader jsonReader = new JsonReader(new StringReader(jwkSet));
       jsonReader.setLenient(false);
       jsonKeyset = Streams.parse(jsonReader).getAsJsonObject();
     } catch (IllegalStateException | JsonParseException | StackOverflowError ex) {
-      throw new IOException("JWK set is invalid JSON", ex);
+      throw new GeneralSecurityException("JWK set is invalid JSON", ex);
     }
-    KeysetManager manager = KeysetManager.withEmptyKeyset();
+    KeysetHandle.Builder builder = KeysetHandle.newBuilder();
     JsonArray jsonKeys = jsonKeyset.get("keys").getAsJsonArray();
     for (JsonElement element : jsonKeys) {
       JsonObject jsonKey = element.getAsJsonObject();
       String algPrefix = getStringItem(jsonKey, "alg").substring(0, 2);
-      KeyData keyData;
+      ProtoKeySerialization keySerialization;
       switch (algPrefix) {
         case "RS":
-          keyData = convertToRsaSsaPkcs1Key(jsonKey);
+          keySerialization = convertToRsaSsaPkcs1Key(jsonKey);
           break;
         case "PS":
-          keyData = convertToRsaSsaPssKey(jsonKey);
+          keySerialization = convertToRsaSsaPssKey(jsonKey);
           break;
         case "ES":
-          keyData = convertToEcdsaKey(jsonKey);
+          keySerialization = convertToEcdsaKey(jsonKey);
           break;
         default:
-          throw new IOException("unexpected alg value: " + getStringItem(jsonKey, "alg"));
+          throw new GeneralSecurityException(
+              "unexpected alg value: " + getStringItem(jsonKey, "alg"));
       }
-      manager.add(
-          KeyHandle.createFromKey(
-              new ProtoKey(keyData, com.google.crypto.tink.KeyTemplate.OutputPrefixType.RAW),
-              KeyAccess.publicAccess()));
+      builder.addEntry(
+          KeysetHandle.importKey(
+                  MutableSerializationRegistry.globalInstance()
+                      .parseKeyWithLegacyFallback(keySerialization, null))
+              .withRandomId());
     }
-    KeysetInfo info = manager.getKeysetHandle().getKeysetInfo();
-    if (info.getKeyInfoCount() <= 0) {
-      throw new IOException("empty keyset");
+    if (builder.size() <= 0) {
+      throw new GeneralSecurityException("empty keyset");
     }
-    manager.setPrimary(info.getKeyInfo(0).getKeyId());
-    return manager.getKeysetHandle();
+    builder.getAt(0).makePrimary();
+    return builder.build();
   }
 
   private static final String JWT_ECDSA_PUBLIC_KEY_URL =
@@ -131,226 +164,205 @@
   private static final String JWT_RSA_SSA_PSS_PUBLIC_KEY_URL =
       "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey";
 
-  private static final class JwkSetWriter implements KeysetWriter {
-
-    private final OutputStream outputStream;
-
-    private JwkSetWriter(OutputStream outputStream) {
-      this.outputStream = outputStream;
+  private static Optional<String> getKid(@Nullable Integer idRequirement) {
+    if (idRequirement == null) {
+      return Optional.empty();
     }
-
-    @Override
-    public void write(Keyset keyset) throws IOException {
-      JsonObject jwkSet;
-      try {
-        jwkSet = convertKeyset(keyset);
-      } catch (GeneralSecurityException exception) {
-        throw new IOException(exception);
-      }
-      outputStream.write(jwkSet.toString().getBytes(UTF_8));
-    }
-
-    @Override
-    public void write(EncryptedKeyset keyset) {
-      throw new UnsupportedOperationException("EncryptedKeyset are not implemented");
-    }
-
-    private static JsonObject convertKeyset(Keyset keyset)
-        throws IOException, GeneralSecurityException {
-      JsonArray keys = new JsonArray();
-      for (Keyset.Key key : keyset.getKeyList()) {
-        if (key.getStatus() != KeyStatusType.ENABLED) {
-          continue;
-        }
-        if (key.getKeyData().getKeyMaterialType() != KeyMaterialType.ASYMMETRIC_PUBLIC) {
-          throw new GeneralSecurityException("only public keys can be converted");
-        }
-        if ((key.getOutputPrefixType() != OutputPrefixType.RAW)
-            && (key.getOutputPrefixType() != OutputPrefixType.TINK)) {
-          throw new GeneralSecurityException("only OutputPrefixType RAW and TINK are supported");
-        }
-        switch (key.getKeyData().getTypeUrl()) {
-          case JWT_ECDSA_PUBLIC_KEY_URL:
-            keys.add(convertJwtEcdsaKey(key));
-            break;
-          case JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL:
-            keys.add(convertJwtRsaSsaPkcs1(key));
-            break;
-          case JWT_RSA_SSA_PSS_PUBLIC_KEY_URL:
-            keys.add(convertJwtRsaSsaPss(key));
-            break;
-          default:
-            throw new GeneralSecurityException(
-                String.format("key type %s is not supported", key.getKeyData().getTypeUrl()));
-        }
-      }
-      JsonObject jwkSet = new JsonObject();
-      jwkSet.add("keys", keys);
-      return jwkSet;
-    }
-
-    private static JsonObject convertJwtEcdsaKey(Keyset.Key key)
-        throws IOException, GeneralSecurityException {
-      JwtEcdsaPublicKey jwtEcdsaPublicKey =
-          JwtEcdsaPublicKey.parseFrom(
-              key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-      String alg;
-      String crv;
-      switch (jwtEcdsaPublicKey.getAlgorithm()) {
-        case ES256:
-          alg = "ES256";
-          crv = "P-256";
-          break;
-        case ES384:
-          alg = "ES384";
-          crv = "P-384";
-          break;
-        case ES512:
-          alg = "ES512";
-          crv = "P-521";
-          break;
-        default:
-          throw new GeneralSecurityException("unknown algorithm");
-      }
-      JsonObject jsonKey = new JsonObject();
-      jsonKey.addProperty("kty", "EC");
-      jsonKey.addProperty("crv", crv);
-      jsonKey.addProperty("x", Base64.urlSafeEncode(jwtEcdsaPublicKey.getX().toByteArray()));
-      jsonKey.addProperty("y", Base64.urlSafeEncode(jwtEcdsaPublicKey.getY().toByteArray()));
-      jsonKey.addProperty("use", "sig");
-      jsonKey.addProperty("alg", alg);
-      JsonArray keyOps = new JsonArray();
-      keyOps.add("verify");
-      jsonKey.add("key_ops", keyOps);
-      Optional<String> kid = JwtFormat.getKid(key.getKeyId(), key.getOutputPrefixType());
-      if (kid.isPresent()) {
-        jsonKey.addProperty("kid", kid.get());
-      } else if (jwtEcdsaPublicKey.hasCustomKid()) {
-        jsonKey.addProperty("kid", jwtEcdsaPublicKey.getCustomKid().getValue());
-      }
-      return jsonKey;
-    }
-
-    private static JsonObject convertJwtRsaSsaPkcs1(Keyset.Key key)
-        throws IOException, GeneralSecurityException {
-      JwtRsaSsaPkcs1PublicKey jwtRsaSsaPkcs1PublicKey =
-          JwtRsaSsaPkcs1PublicKey.parseFrom(
-              key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-      String alg;
-      switch (jwtRsaSsaPkcs1PublicKey.getAlgorithm()) {
-        case RS256:
-          alg = "RS256";
-          break;
-        case RS384:
-          alg = "RS384";
-          break;
-        case RS512:
-          alg = "RS512";
-          break;
-        default:
-          throw new GeneralSecurityException("unknown algorithm");
-      }
-      JsonObject jsonKey = new JsonObject();
-      jsonKey.addProperty("kty", "RSA");
-      jsonKey.addProperty("n", Base64.urlSafeEncode(jwtRsaSsaPkcs1PublicKey.getN().toByteArray()));
-      jsonKey.addProperty("e", Base64.urlSafeEncode(jwtRsaSsaPkcs1PublicKey.getE().toByteArray()));
-      jsonKey.addProperty("use", "sig");
-      jsonKey.addProperty("alg", alg);
-      JsonArray keyOps = new JsonArray();
-      keyOps.add("verify");
-      jsonKey.add("key_ops", keyOps);
-      Optional<String> kid = JwtFormat.getKid(key.getKeyId(), key.getOutputPrefixType());
-      if (kid.isPresent()) {
-        jsonKey.addProperty("kid", kid.get());
-      } else if (jwtRsaSsaPkcs1PublicKey.hasCustomKid()) {
-        jsonKey.addProperty("kid", jwtRsaSsaPkcs1PublicKey.getCustomKid().getValue());
-      }
-      return jsonKey;
-    }
-
-    private static JsonObject convertJwtRsaSsaPss(Keyset.Key key)
-        throws IOException, GeneralSecurityException {
-      JwtRsaSsaPssPublicKey jwtRsaSsaPssPublicKey =
-          JwtRsaSsaPssPublicKey.parseFrom(
-              key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-      String alg;
-      switch (jwtRsaSsaPssPublicKey.getAlgorithm()) {
-        case PS256:
-          alg = "PS256";
-          break;
-        case PS384:
-          alg = "PS384";
-          break;
-        case PS512:
-          alg = "PS512";
-          break;
-        default:
-          throw new GeneralSecurityException("unknown algorithm");
-      }
-      JsonObject jsonKey = new JsonObject();
-      jsonKey.addProperty("kty", "RSA");
-      jsonKey.addProperty("n", Base64.urlSafeEncode(jwtRsaSsaPssPublicKey.getN().toByteArray()));
-      jsonKey.addProperty("e", Base64.urlSafeEncode(jwtRsaSsaPssPublicKey.getE().toByteArray()));
-      jsonKey.addProperty("use", "sig");
-      jsonKey.addProperty("alg", alg);
-      JsonArray keyOps = new JsonArray();
-      keyOps.add("verify");
-      jsonKey.add("key_ops", keyOps);
-      Optional<String> kid = JwtFormat.getKid(key.getKeyId(), key.getOutputPrefixType());
-      if (kid.isPresent()) {
-        jsonKey.addProperty("kid", kid.get());
-      } else if (jwtRsaSsaPssPublicKey.hasCustomKid()) {
-        jsonKey.addProperty("kid", jwtRsaSsaPssPublicKey.getCustomKid().getValue());
-      }
-      return jsonKey;
-    }
+    byte[] bigEndianKeyId = ByteBuffer.allocate(4).putInt(idRequirement).array();
+    return Optional.of(Base64.urlSafeEncode(bigEndianKeyId));
   }
 
-  private static String getStringItem(JsonObject obj, String name) throws IOException {
+  private static JsonObject convertJwtEcdsaKey(ProtoKeySerialization protoKeySerialization)
+      throws GeneralSecurityException {
+    JwtEcdsaPublicKey jwtEcdsaPublicKey;
+    try {
+      jwtEcdsaPublicKey =
+        JwtEcdsaPublicKey.parseFrom(
+            protoKeySerialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("failed to parse value as JwtEcdsaPublicKey proto", e);
+    }
+    String alg;
+    String crv;
+    // We currently encode with one extra 0 byte at the beginning, to make sure
+    // that parsing is correct even if passing of a two's complement encoding is used.
+    // See also b/264525021.
+    int encLength;
+    switch (jwtEcdsaPublicKey.getAlgorithm()) {
+      case ES256:
+        alg = "ES256";
+        crv = "P-256";
+        encLength = 33;
+        break;
+      case ES384:
+        alg = "ES384";
+        crv = "P-384";
+        encLength = 49;
+        break;
+      case ES512:
+        alg = "ES512";
+        crv = "P-521";
+        encLength = 67;
+        break;
+      default:
+        throw new GeneralSecurityException("unknown algorithm");
+    }
+    JsonObject jsonKey = new JsonObject();
+    jsonKey.addProperty("kty", "EC");
+    jsonKey.addProperty("crv", crv);
+    BigInteger x =
+        BigIntegerEncoding.fromUnsignedBigEndianBytes(jwtEcdsaPublicKey.getX().toByteArray());
+    BigInteger y =
+        BigIntegerEncoding.fromUnsignedBigEndianBytes(jwtEcdsaPublicKey.getY().toByteArray());
+    jsonKey.addProperty(
+        "x", Base64.urlSafeEncode(BigIntegerEncoding.toBigEndianBytesOfFixedLength(x, encLength)));
+    jsonKey.addProperty(
+        "y", Base64.urlSafeEncode(BigIntegerEncoding.toBigEndianBytesOfFixedLength(y, encLength)));
+    jsonKey.addProperty("use", "sig");
+    jsonKey.addProperty("alg", alg);
+    JsonArray keyOps = new JsonArray();
+    keyOps.add("verify");
+    jsonKey.add("key_ops", keyOps);
+    Optional<String> kid = getKid(protoKeySerialization.getIdRequirementOrNull());
+    if (kid.isPresent()) {
+      jsonKey.addProperty("kid", kid.get());
+    } else if (jwtEcdsaPublicKey.hasCustomKid()) {
+      jsonKey.addProperty("kid", jwtEcdsaPublicKey.getCustomKid().getValue());
+    }
+    return jsonKey;
+  }
+
+  private static JsonObject convertJwtRsaSsaPkcs1(ProtoKeySerialization protoKeySerialization)
+      throws GeneralSecurityException {
+    JwtRsaSsaPkcs1PublicKey jwtRsaSsaPkcs1PublicKey;
+    try {
+      jwtRsaSsaPkcs1PublicKey =
+        JwtRsaSsaPkcs1PublicKey.parseFrom(
+            protoKeySerialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException(
+          "failed to parse value as JwtRsaSsaPkcs1PublicKey proto", e);
+    }
+    String alg;
+    switch (jwtRsaSsaPkcs1PublicKey.getAlgorithm()) {
+      case RS256:
+        alg = "RS256";
+        break;
+      case RS384:
+        alg = "RS384";
+        break;
+      case RS512:
+        alg = "RS512";
+        break;
+      default:
+        throw new GeneralSecurityException("unknown algorithm");
+    }
+    JsonObject jsonKey = new JsonObject();
+    jsonKey.addProperty("kty", "RSA");
+    jsonKey.addProperty("n", Base64.urlSafeEncode(jwtRsaSsaPkcs1PublicKey.getN().toByteArray()));
+    jsonKey.addProperty("e", Base64.urlSafeEncode(jwtRsaSsaPkcs1PublicKey.getE().toByteArray()));
+    jsonKey.addProperty("use", "sig");
+    jsonKey.addProperty("alg", alg);
+    JsonArray keyOps = new JsonArray();
+    keyOps.add("verify");
+    jsonKey.add("key_ops", keyOps);
+    Optional<String> kid = getKid(protoKeySerialization.getIdRequirementOrNull());
+    if (kid.isPresent()) {
+      jsonKey.addProperty("kid", kid.get());
+    } else if (jwtRsaSsaPkcs1PublicKey.hasCustomKid()) {
+      jsonKey.addProperty("kid", jwtRsaSsaPkcs1PublicKey.getCustomKid().getValue());
+    }
+    return jsonKey;
+  }
+
+  private static JsonObject convertJwtRsaSsaPss(ProtoKeySerialization protoKeySerialization)
+      throws GeneralSecurityException {
+    JwtRsaSsaPssPublicKey jwtRsaSsaPssPublicKey;
+    try {
+      jwtRsaSsaPssPublicKey =
+        JwtRsaSsaPssPublicKey.parseFrom(
+            protoKeySerialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("failed to parse value as JwtRsaSsaPssPublicKey proto", e);
+    }
+    String alg;
+    switch (jwtRsaSsaPssPublicKey.getAlgorithm()) {
+      case PS256:
+        alg = "PS256";
+        break;
+      case PS384:
+        alg = "PS384";
+        break;
+      case PS512:
+        alg = "PS512";
+        break;
+      default:
+        throw new GeneralSecurityException("unknown algorithm");
+    }
+    JsonObject jsonKey = new JsonObject();
+    jsonKey.addProperty("kty", "RSA");
+    jsonKey.addProperty("n", Base64.urlSafeEncode(jwtRsaSsaPssPublicKey.getN().toByteArray()));
+    jsonKey.addProperty("e", Base64.urlSafeEncode(jwtRsaSsaPssPublicKey.getE().toByteArray()));
+    jsonKey.addProperty("use", "sig");
+    jsonKey.addProperty("alg", alg);
+    JsonArray keyOps = new JsonArray();
+    keyOps.add("verify");
+    jsonKey.add("key_ops", keyOps);
+    Optional<String> kid = getKid(protoKeySerialization.getIdRequirementOrNull());
+    if (kid.isPresent()) {
+      jsonKey.addProperty("kid", kid.get());
+    } else if (jwtRsaSsaPssPublicKey.hasCustomKid()) {
+      jsonKey.addProperty("kid", jwtRsaSsaPssPublicKey.getCustomKid().getValue());
+    }
+    return jsonKey;
+  }
+
+  private static String getStringItem(JsonObject obj, String name) throws GeneralSecurityException {
     if (!obj.has(name)) {
-      throw new IOException(name + " not found");
+      throw new GeneralSecurityException(name + " not found");
     }
     if (!obj.get(name).isJsonPrimitive() || !obj.get(name).getAsJsonPrimitive().isString()) {
-      throw new IOException(name + " is not a string");
+      throw new GeneralSecurityException(name + " is not a string");
     }
     return obj.get(name).getAsString();
   }
 
   private static void expectStringItem(JsonObject obj, String name, String expectedValue)
-      throws IOException {
+      throws GeneralSecurityException {
     String value = getStringItem(obj, name);
     if (!value.equals(expectedValue)) {
-      throw new IOException("unexpected " + name + " value: " + value);
+      throw new GeneralSecurityException("unexpected " + name + " value: " + value);
     }
   }
 
-  private static void validateUseIsSig(JsonObject jsonKey) throws IOException {
+  private static void validateUseIsSig(JsonObject jsonKey) throws GeneralSecurityException {
     if (!jsonKey.has("use")) {
       return;
     }
     expectStringItem(jsonKey, "use", "sig");
   }
 
-  private static void validateKeyOpsIsVerify(JsonObject jsonKey) throws IOException {
+  private static void validateKeyOpsIsVerify(JsonObject jsonKey) throws GeneralSecurityException {
     if (!jsonKey.has("key_ops")) {
       return;
     }
     if (!jsonKey.get("key_ops").isJsonArray()) {
-      throw new IOException("key_ops is not an array");
+      throw new GeneralSecurityException("key_ops is not an array");
     }
     JsonArray keyOps = jsonKey.get("key_ops").getAsJsonArray();
     if (keyOps.size() != 1) {
-      throw new IOException("key_ops must contain exactly one element");
+      throw new GeneralSecurityException("key_ops must contain exactly one element");
     }
     if (!keyOps.get(0).isJsonPrimitive() || !keyOps.get(0).getAsJsonPrimitive().isString()) {
-      throw new IOException("key_ops is not a string");
+      throw new GeneralSecurityException("key_ops is not a string");
     }
     if (!keyOps.get(0).getAsString().equals("verify")) {
-      throw new IOException("unexpected keyOps value: " + keyOps.get(0).getAsString());
+      throw new GeneralSecurityException("unexpected keyOps value: " + keyOps.get(0).getAsString());
     }
   }
 
-  private static KeyData convertToRsaSsaPkcs1Key(JsonObject jsonKey) throws IOException {
+  private static ProtoKeySerialization convertToRsaSsaPkcs1Key(JsonObject jsonKey)
+      throws GeneralSecurityException {
     JwtRsaSsaPkcs1Algorithm algorithm;
     switch (getStringItem(jsonKey, "alg")) {
       case "RS256":
@@ -363,7 +375,8 @@
         algorithm = JwtRsaSsaPkcs1Algorithm.RS512;
         break;
       default:
-        throw new IOException("Unknown Rsa Algorithm: " + getStringItem(jsonKey, "alg"));
+        throw new GeneralSecurityException(
+            "Unknown Rsa Algorithm: " + getStringItem(jsonKey, "alg"));
     }
     if (jsonKey.has("p")
         || jsonKey.has("q")
@@ -388,14 +401,16 @@
               .setValue(getStringItem(jsonKey, "kid"))
               .build());
     }
-    return KeyData.newBuilder()
-        .setTypeUrl(JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL)
-        .setValue(pkcs1PubKeyBuilder.build().toByteString())
-        .setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
-        .build();
+    return ProtoKeySerialization.create(
+        JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL,
+        pkcs1PubKeyBuilder.build().toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        OutputPrefixType.RAW,
+        null);
   }
 
-  private static KeyData convertToRsaSsaPssKey(JsonObject jsonKey) throws IOException {
+  private static ProtoKeySerialization convertToRsaSsaPssKey(JsonObject jsonKey)
+      throws GeneralSecurityException {
     JwtRsaSsaPssAlgorithm algorithm;
     switch (getStringItem(jsonKey, "alg")) {
       case "PS256":
@@ -408,7 +423,8 @@
         algorithm = JwtRsaSsaPssAlgorithm.PS512;
         break;
       default:
-        throw new IOException("Unknown Rsa Algorithm: " + getStringItem(jsonKey, "alg"));
+        throw new GeneralSecurityException(
+            "Unknown Rsa Algorithm: " + getStringItem(jsonKey, "alg"));
     }
     if (jsonKey.has("p")
         || jsonKey.has("q")
@@ -433,14 +449,16 @@
               .setValue(getStringItem(jsonKey, "kid"))
               .build());
     }
-    return KeyData.newBuilder()
-        .setTypeUrl(JWT_RSA_SSA_PSS_PUBLIC_KEY_URL)
-        .setValue(pssPubKeyBuilder.build().toByteString())
-        .setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
-        .build();
+    return ProtoKeySerialization.create(
+        JWT_RSA_SSA_PSS_PUBLIC_KEY_URL,
+        pssPubKeyBuilder.build().toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        OutputPrefixType.RAW,
+        null);
   }
 
-  private static KeyData convertToEcdsaKey(JsonObject jsonKey) throws IOException {
+  private static ProtoKeySerialization convertToEcdsaKey(JsonObject jsonKey)
+      throws GeneralSecurityException {
     JwtEcdsaAlgorithm algorithm;
     switch (getStringItem(jsonKey, "alg")) {
       case "ES256":
@@ -456,7 +474,8 @@
         algorithm = JwtEcdsaAlgorithm.ES512;
         break;
       default:
-        throw new IOException("Unknown Ecdsa Algorithm: " + getStringItem(jsonKey, "alg"));
+        throw new GeneralSecurityException(
+            "Unknown Ecdsa Algorithm: " + getStringItem(jsonKey, "alg"));
     }
     if (jsonKey.has("d")) {
       throw new UnsupportedOperationException("importing ECDSA private keys is not implemented");
@@ -474,14 +493,17 @@
       ecdsaPubKeyBuilder.setCustomKid(
           JwtEcdsaPublicKey.CustomKid.newBuilder().setValue(getStringItem(jsonKey, "kid")).build());
     }
-    return KeyData.newBuilder()
-        .setTypeUrl(JWT_ECDSA_PUBLIC_KEY_URL)
-        .setValue(ecdsaPubKeyBuilder.build().toByteString())
-        .setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
-        .build();
+    return ProtoKeySerialization.create(
+        JWT_ECDSA_PUBLIC_KEY_URL,
+        ecdsaPubKeyBuilder.build().toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        OutputPrefixType.RAW,
+        null);
   }
 
-  /** @deprecated Use JwkSetConverter.fromPublicKeysetHandle(handle) instead. */
+  /**
+   * @deprecated Use JwkSetConverter.fromPublicKeysetHandle(handle) instead.
+   */
   @InlineMe(
       replacement = "JwkSetConverter.fromPublicKeysetHandle(handle)",
       imports = "com.google.crypto.tink.jwt.JwkSetConverter")
@@ -491,7 +513,9 @@
     return fromPublicKeysetHandle(handle);
   }
 
-  /** @deprecated Use JwkSetConverter.toPublicKeysetHandle(jwkSet) instead. */
+  /**
+   * @deprecated Use JwkSetConverter.toPublicKeysetHandle(jwkSet) instead.
+   */
   @InlineMe(
       replacement = "JwkSetConverter.toPublicKeysetHandle(jwkSet)",
       imports = "com.google.crypto.tink.jwt.JwkSetConverter")
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaParameters.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaParameters.java
new file mode 100644
index 0000000..ce50d54
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaParameters.java
@@ -0,0 +1,196 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECParameterSpec;
+import java.util.Objects;
+import java.util.Optional;
+
+/** Describes the parameters of a {@code JwtEcdsaPrivateKey} or a {@code JwtEcdsaPublicKey}. */
+public final class JwtEcdsaParameters extends JwtSignatureParameters {
+  /** Specifies how the "kid" header is handled. */
+  @Immutable
+  public static final class KidStrategy {
+    /**
+     * The "kid" is the URL safe (RFC 4648 Section 5) base64-encoded big-endian key_id in the
+     * keyset.
+     *
+     * <p>In {@code PublicKeySign#signAndEncode} Tink always adds the KID.
+     *
+     * <p>In {@code PublicKeyVerify#verifyAndDecode} Tink checks that the kid is present and equal
+     * to this value.
+     *
+     * <p>This strategy is recommended by Tink.
+     */
+    public static final KidStrategy BASE64_ENCODED_KEY_ID =
+        new KidStrategy("BASE64_ENCODED_KEY_ID");
+
+    /**
+     * The "kid" header is ignored.
+     *
+     * <p>In {@code PublicKeySign#signAndEncode} Tink does not write a "kid" header.
+     *
+     * <p>In {@code PublicKeyVerify#verifyAndDecode} Tink ignores the "kid" header.
+     */
+    public static final KidStrategy IGNORED = new KidStrategy("IGNORED");
+
+    /**
+     * The "kid" is fixed. It can be obtained from {@code parameters.getCustomKid()}.
+     *
+     * <p>In {@code PublicKeySign#signAndEncode} Tink writes the "kid" header to the value given by
+     * {@code parameters.getCustomKid()}.
+     *
+     * <p>In {@code PublicKeyVerify#verifyAndDecode}, if the kid is present, it needs to match
+     * {@code parameters.getCustomKid()}. If the kid is absent, it will be accepted.
+     *
+     * <p>Note: Tink does not allow to randomly generate new {@link JwtEcdsaKey} objects from
+     * parameters objects with {@code KidStrategy} equals to {@code CUSTOM}.
+     */
+    public static final KidStrategy CUSTOM = new KidStrategy("CUSTOM");
+
+    private final String name;
+
+    private KidStrategy(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The algorithm to be used for the signature computation. */
+  @Immutable
+  public static final class Algorithm {
+    /** ECDSA using P-256 and SHA-256 */
+    public static final Algorithm ES256 =
+        new Algorithm("ES256", EllipticCurvesUtil.NIST_P256_PARAMS);
+    /** ECDSA using P-384 and SHA-384 */
+    public static final Algorithm ES384 =
+        new Algorithm("ES384", EllipticCurvesUtil.NIST_P384_PARAMS);
+    /** ECDSA using P-521 and SHA-512 */
+    public static final Algorithm ES512 =
+        new Algorithm("ES512", EllipticCurvesUtil.NIST_P521_PARAMS);
+
+    private final String name;
+
+    @SuppressWarnings("Immutable") // ECParameterSpec is immutable
+    private final ECParameterSpec ecParameterSpec;
+
+    private Algorithm(String name, ECParameterSpec ecParameterSpec) {
+      this.name = name;
+      this.ecParameterSpec = ecParameterSpec;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+
+    public String getStandardName() {
+      return name;
+    }
+
+    ECParameterSpec getECParameterSpec() {
+      return ecParameterSpec;
+    }
+  }
+
+  /** Helps creating a {@code JwtEcdsaParameters} object. */
+  public static final class Builder {
+    Optional<KidStrategy> kidStrategy = Optional.empty();
+    Optional<Algorithm> algorithm = Optional.empty();
+
+    @CanIgnoreReturnValue
+    public Builder setKidStrategy(KidStrategy kidStrategy) {
+      this.kidStrategy = Optional.of(kidStrategy);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setAlgorithm(Algorithm algorithm) {
+      this.algorithm = Optional.of(algorithm);
+      return this;
+    }
+
+    public JwtEcdsaParameters build() throws GeneralSecurityException {
+      if (!algorithm.isPresent()) {
+        throw new GeneralSecurityException("Algorithm must be set");
+      }
+      if (!kidStrategy.isPresent()) {
+        throw new GeneralSecurityException("KidStrategy must be set");
+      }
+      return new JwtEcdsaParameters(kidStrategy.get(), algorithm.get());
+    }
+
+    private Builder() {}
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private JwtEcdsaParameters(KidStrategy kidStrategy, Algorithm algorithm) {
+    this.kidStrategy = kidStrategy;
+    this.algorithm = algorithm;
+  }
+
+  private final KidStrategy kidStrategy;
+  private final Algorithm algorithm;
+
+  public KidStrategy getKidStrategy() {
+    return kidStrategy;
+  }
+
+  public Algorithm getAlgorithm() {
+    return algorithm;
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return kidStrategy.equals(KidStrategy.BASE64_ENCODED_KEY_ID);
+  }
+
+  @Override
+  public boolean allowKidAbsent() {
+    return kidStrategy.equals(KidStrategy.CUSTOM) || kidStrategy.equals(KidStrategy.IGNORED);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof JwtEcdsaParameters)) {
+      return false;
+    }
+    JwtEcdsaParameters that = (JwtEcdsaParameters) o;
+    return that.kidStrategy.equals(kidStrategy) && that.algorithm.equals(algorithm);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(JwtEcdsaParameters.class, kidStrategy, algorithm);
+  }
+
+  @Override
+  public String toString() {
+    return "JWT ECDSA Parameters (kidStrategy: " + kidStrategy + ", Algorithm " + algorithm + ")";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaPrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaPrivateKey.java
new file mode 100644
index 0000000..32dd4a1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaPrivateKey.java
@@ -0,0 +1,102 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+
+/**
+ * Represents a key for computing JWT ECDSA signatures (ES256, ES384, ES512).
+ *
+ * <p>See https://datatracker.ietf.org/doc/html/rfc7518 for more information.
+ */
+@Immutable
+public final class JwtEcdsaPrivateKey extends JwtSignaturePrivateKey {
+  public final JwtEcdsaPublicKey publicKey;
+  public final SecretBigInteger privateKeyValue;
+
+  private static void validatePrivateValue(
+      BigInteger privateValue, ECPoint publicPoint, JwtEcdsaParameters.Algorithm algorithm)
+      throws GeneralSecurityException {
+    BigInteger order = algorithm.getECParameterSpec().getOrder();
+    if ((privateValue.signum() <= 0) || (privateValue.compareTo(order) >= 0)) {
+      throw new GeneralSecurityException("Invalid private value");
+    }
+    ECPoint p =
+        EllipticCurvesUtil.multiplyByGenerator(privateValue, algorithm.getECParameterSpec());
+    if (!p.equals(publicPoint)) {
+      throw new GeneralSecurityException("Invalid private value");
+    }
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  @AccessesPartialKey
+  public static JwtEcdsaPrivateKey create(
+      JwtEcdsaPublicKey publicKey, SecretBigInteger privateValue) throws GeneralSecurityException {
+    validatePrivateValue(
+        privateValue.getBigInteger(InsecureSecretKeyAccess.get()),
+        publicKey.getPublicPoint(),
+        publicKey.getParameters().getAlgorithm());
+    return new JwtEcdsaPrivateKey(publicKey, privateValue);
+  }
+
+  private JwtEcdsaPrivateKey(JwtEcdsaPublicKey publicKey, SecretBigInteger privateKeyValue) {
+    this.publicKey = publicKey;
+    this.privateKeyValue = privateKeyValue;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBigInteger getPrivateValue() {
+    return privateKeyValue;
+  }
+
+  @Override
+  public JwtEcdsaParameters getParameters() {
+    return publicKey.getParameters();
+  }
+
+  @Override
+  public JwtEcdsaPublicKey getPublicKey() {
+    return publicKey;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof JwtEcdsaPrivateKey)) {
+      return false;
+    }
+    JwtEcdsaPrivateKey that = (JwtEcdsaPrivateKey) o;
+    return that.publicKey.equalsKey(publicKey)
+        && privateKeyValue.equalsSecretBigInteger(that.privateKeyValue);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java
new file mode 100644
index 0000000..ad07f8e
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java
@@ -0,0 +1,374 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.JwtEcdsaAlgorithm;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link JwtEcdsaPrivateKey}, {@link JwtEcdsaPublicKey}, and {@link
+ * JwtEcdsaParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class JwtEcdsaProtoSerialization {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
+  private static final ParametersSerializer<JwtEcdsaParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              JwtEcdsaProtoSerialization::serializeParameters,
+              JwtEcdsaParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          JwtEcdsaProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<JwtEcdsaPublicKey, ProtoKeySerialization>
+      PUBLIC_KEY_SERIALIZER =
+          KeySerializer.create(
+              JwtEcdsaProtoSerialization::serializePublicKey,
+              JwtEcdsaPublicKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          JwtEcdsaProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final KeySerializer<JwtEcdsaPrivateKey, ProtoKeySerialization>
+      PRIVATE_KEY_SERIALIZER =
+          KeySerializer.create(
+              JwtEcdsaProtoSerialization::serializePrivateKey,
+              JwtEcdsaPrivateKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PRIVATE_KEY_PARSER =
+      KeyParser.create(
+          JwtEcdsaProtoSerialization::parsePrivateKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static JwtEcdsaAlgorithm toProtoAlgorithm(JwtEcdsaParameters.Algorithm algorithm)
+      throws GeneralSecurityException {
+    if (JwtEcdsaParameters.Algorithm.ES256.equals(algorithm)) {
+      return JwtEcdsaAlgorithm.ES256;
+    }
+    if (JwtEcdsaParameters.Algorithm.ES384.equals(algorithm)) {
+      return JwtEcdsaAlgorithm.ES384;
+    }
+    if (JwtEcdsaParameters.Algorithm.ES512.equals(algorithm)) {
+      return JwtEcdsaAlgorithm.ES512;
+    }
+    throw new GeneralSecurityException("Unable to serialize algorithm: " + algorithm);
+  }
+
+  private static JwtEcdsaParameters.Algorithm toAlgorithm(JwtEcdsaAlgorithm algorithm)
+      throws GeneralSecurityException {
+    switch (algorithm) {
+      case ES256:
+        return JwtEcdsaParameters.Algorithm.ES256;
+      case ES384:
+        return JwtEcdsaParameters.Algorithm.ES384;
+      case ES512:
+        return JwtEcdsaParameters.Algorithm.ES512;
+      default:
+        throw new GeneralSecurityException("Unable to parse algorithm: " + algorithm.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.JwtEcdsaKeyFormat serializeToJwtEcdsaKeyFormat(
+      JwtEcdsaParameters parameters) throws GeneralSecurityException {
+    if (!parameters.getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.IGNORED)
+        && !parameters
+            .getKidStrategy()
+            .equals(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)) {
+      throw new GeneralSecurityException(
+          "Unable to serialize Parameters object with KidStrategy " + parameters.getKidStrategy());
+    }
+    return com.google.crypto.tink.proto.JwtEcdsaKeyFormat.newBuilder()
+        .setVersion(0)
+        .setAlgorithm(toProtoAlgorithm(parameters.getAlgorithm()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(JwtEcdsaParameters parameters)
+      throws GeneralSecurityException {
+    OutputPrefixType outputPrefixType = OutputPrefixType.TINK;
+    if (parameters.getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.IGNORED)) {
+      outputPrefixType = OutputPrefixType.RAW;
+    }
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(serializeToJwtEcdsaKeyFormat(parameters).toByteString())
+            .setOutputPrefixType(outputPrefixType)
+            .build());
+  }
+
+  private static JwtEcdsaParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to JwtEcdsaParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.JwtEcdsaKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.JwtEcdsaKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing JwtEcdsaKeyFormat failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException(
+          "Parsing HmacParameters failed: unknown Version " + format.getVersion());
+    }
+    JwtEcdsaParameters.KidStrategy kidStrategy = null;
+    if (serialization.getKeyTemplate().getOutputPrefixType().equals(OutputPrefixType.TINK)) {
+      kidStrategy = JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID;
+    }
+    if (serialization.getKeyTemplate().getOutputPrefixType().equals(OutputPrefixType.RAW)) {
+      kidStrategy = JwtEcdsaParameters.KidStrategy.IGNORED;
+    }
+    if (kidStrategy == null) {
+      throw new GeneralSecurityException("Invalid OutputPrefixType for JwtHmacKeyFormat");
+    }
+    return JwtEcdsaParameters.builder()
+        .setAlgorithm(toAlgorithm(format.getAlgorithm()))
+        .setKidStrategy(kidStrategy)
+        .build();
+  }
+
+  private static int getEncodingLength(JwtEcdsaParameters.Algorithm algorithm)
+      throws GeneralSecurityException {
+    // We currently encode with one extra 0 byte at the beginning, to make sure
+    // that parsing is correct even if passing of a two's complement encoding is used.
+    // We want to prevent bugs similar to b/264525021
+    if (algorithm.equals(JwtEcdsaParameters.Algorithm.ES256)) {
+      return 33;
+    }
+    if (algorithm.equals(JwtEcdsaParameters.Algorithm.ES384)) {
+      return 49;
+    }
+    if (algorithm.equals(JwtEcdsaParameters.Algorithm.ES512)) {
+      return 67;
+    }
+    throw new GeneralSecurityException("Unknown algorithm: " + algorithm);
+  }
+
+  private static OutputPrefixType toProtoOutputPrefixType(JwtEcdsaParameters parameters) {
+    if (parameters.getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)) {
+      return OutputPrefixType.TINK;
+    }
+    return OutputPrefixType.RAW;
+  }
+
+  private static com.google.crypto.tink.proto.JwtEcdsaPublicKey serializePublicKey(
+      JwtEcdsaPublicKey key) throws GeneralSecurityException {
+    int encLength = getEncodingLength(key.getParameters().getAlgorithm());
+    ECPoint publicPoint = key.getPublicPoint();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey.Builder builder =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(toProtoAlgorithm(key.getParameters().getAlgorithm()))
+            .setX(
+                ByteString.copyFrom(
+                    BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                        publicPoint.getAffineX(), encLength)))
+            .setY(
+                ByteString.copyFrom(
+                    BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                        publicPoint.getAffineY(), encLength)));
+    if (key.getParameters().getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.CUSTOM)) {
+      builder.setCustomKid(
+          com.google.crypto.tink.proto.JwtEcdsaPublicKey.CustomKid.newBuilder()
+              .setValue(key.getKid().get())
+              .build());
+    }
+    return builder.build();
+  }
+
+  private static ProtoKeySerialization serializePublicKey(
+      JwtEcdsaPublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        serializePublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        toProtoOutputPrefixType(key.getParameters()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static JwtEcdsaPublicKey parsePublicKeyFromProto(
+      com.google.crypto.tink.proto.JwtEcdsaPublicKey protoKey,
+      OutputPrefixType outputPrefixType,
+      @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+
+      JwtEcdsaParameters.Builder parametersBuilder = JwtEcdsaParameters.builder();
+      JwtEcdsaPublicKey.Builder keyBuilder = JwtEcdsaPublicKey.builder();
+
+    if (outputPrefixType.equals(OutputPrefixType.TINK)) {
+        if (protoKey.hasCustomKid()) {
+          throw new GeneralSecurityException(
+              "Keys serialized with OutputPrefixType TINK should not have a custom kid");
+      }
+        if (idRequirement == null) {
+          throw new GeneralSecurityException(
+              "Keys serialized with OutputPrefixType TINK need an ID Requirement");
+        }
+        parametersBuilder.setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID);
+        keyBuilder.setIdRequirement(idRequirement);
+    } else if (outputPrefixType.equals(OutputPrefixType.RAW)) {
+        if (protoKey.hasCustomKid()) {
+          parametersBuilder.setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM);
+          keyBuilder.setCustomKid(protoKey.getCustomKid().getValue());
+        } else {
+          parametersBuilder.setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED);
+        }
+      }
+      parametersBuilder.setAlgorithm(toAlgorithm(protoKey.getAlgorithm()));
+      keyBuilder.setPublicPoint(
+          new ECPoint(
+              BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getX().toByteArray()),
+              BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getY().toByteArray())));
+      return keyBuilder.setParameters(parametersBuilder.build()).build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static JwtEcdsaPublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to EcdsaProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.JwtEcdsaPublicKey protoKey =
+          com.google.crypto.tink.proto.JwtEcdsaPublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      return parsePublicKeyFromProto(
+          protoKey, serialization.getOutputPrefixType(), serialization.getIdRequirementOrNull());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing EcdsaPublicKey failed");
+    }
+  }
+
+  private static com.google.crypto.tink.proto.JwtEcdsaPrivateKey serializePrivateKeyToProto(
+      JwtEcdsaPrivateKey key, SecretKeyAccess access) throws GeneralSecurityException {
+    int encLength = getEncodingLength(key.getParameters().getAlgorithm());
+    return com.google.crypto.tink.proto.JwtEcdsaPrivateKey.newBuilder()
+        .setPublicKey(serializePublicKey(key.getPublicKey()))
+        .setKeyValue(
+            ByteString.copyFrom(
+                BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                    key.getPrivateValue().getBigInteger(access), encLength)))
+        .build();
+  }
+
+  private static ProtoKeySerialization serializePrivateKey(
+      JwtEcdsaPrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        serializePrivateKeyToProto(key, SecretKeyAccess.requireAccess(access)).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PRIVATE,
+        toProtoOutputPrefixType(key.getParameters()),
+        key.getIdRequirementOrNull());
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static JwtEcdsaPrivateKey parsePrivateKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to EcdsaProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.JwtEcdsaPrivateKey protoKey =
+          com.google.crypto.tink.proto.JwtEcdsaPrivateKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      JwtEcdsaPublicKey publicKey =
+          parsePublicKeyFromProto(
+              protoKey.getPublicKey(),
+              serialization.getOutputPrefixType(),
+              serialization.getIdRequirementOrNull());
+      return JwtEcdsaPrivateKey.create(
+          publicKey,
+          SecretBigInteger.fromBigInteger(
+              BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getKeyValue().toByteArray()),
+              SecretKeyAccess.requireAccess(access)));
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing EcdsaPrivateKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
+    registry.registerKeySerializer(PRIVATE_KEY_SERIALIZER);
+    registry.registerKeyParser(PRIVATE_KEY_PARSER);
+  }
+
+  private JwtEcdsaProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaPublicKey.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaPublicKey.java
new file mode 100644
index 0000000..bc1ca03
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaPublicKey.java
@@ -0,0 +1,180 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/** JwtEcdsaPublicKey represents the public portion of JWT ECDSA keys. */
+@Immutable
+public final class JwtEcdsaPublicKey extends JwtSignaturePublicKey {
+  private final JwtEcdsaParameters parameters;
+
+  @SuppressWarnings("Immutable") // ECPoint is immutable
+  private final ECPoint publicPoint;
+
+  private final Optional<String> kid;
+  private final Optional<Integer> idRequirement;
+
+  /** Builder for EcdsaPublicKey. */
+  public static class Builder {
+    private Optional<JwtEcdsaParameters> parameters = Optional.empty();
+    private Optional<ECPoint> publicPoint = Optional.empty();
+    private Optional<Integer> idRequirement = Optional.empty();
+    private Optional<String> customKid = Optional.empty();
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(JwtEcdsaParameters parameters) {
+      this.parameters = Optional.of(parameters);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setPublicPoint(ECPoint publicPoint) {
+      this.publicPoint = Optional.of(publicPoint);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(Integer idRequirement) {
+      this.idRequirement = Optional.of(idRequirement);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setCustomKid(String customKid) {
+      this.customKid = Optional.of(customKid);
+      return this;
+    }
+
+    private Optional<String> computeKid() throws GeneralSecurityException {
+      if (parameters
+          .get()
+          .getKidStrategy()
+          .equals(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)) {
+        if (customKid.isPresent()) {
+          throw new GeneralSecurityException(
+              "customKid must not be set for KidStrategy BASE64_ENCODED_KEY_ID");
+        }
+        byte[] bigEndianKeyId = ByteBuffer.allocate(4).putInt(idRequirement.get()).array();
+        return Optional.of(Base64.urlSafeEncode(bigEndianKeyId));
+      }
+      if (parameters.get().getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.CUSTOM)) {
+        if (!customKid.isPresent()) {
+          throw new GeneralSecurityException("customKid needs to be set for KidStrategy CUSTOM");
+        }
+        return customKid;
+      }
+      if (parameters.get().getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.IGNORED)) {
+        if (customKid.isPresent()) {
+          throw new GeneralSecurityException("customKid must not be set for KidStrategy IGNORED");
+        }
+        return Optional.empty();
+      }
+      throw new IllegalStateException("Unknown kid strategy");
+    }
+
+    public JwtEcdsaPublicKey build() throws GeneralSecurityException {
+      if (!parameters.isPresent()) {
+        throw new GeneralSecurityException("Cannot build without parameters");
+      }
+      if (!publicPoint.isPresent()) {
+        throw new GeneralSecurityException("Cannot build without public point");
+      }
+      EllipticCurvesUtil.checkPointOnCurve(
+          publicPoint.get(), parameters.get().getAlgorithm().getECParameterSpec().getCurve());
+      if (parameters.get().hasIdRequirement() && !idRequirement.isPresent()) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+      if (!parameters.get().hasIdRequirement() && idRequirement.isPresent()) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      return new JwtEcdsaPublicKey(
+          parameters.get(), publicPoint.get(), computeKid(), idRequirement);
+    }
+  }
+
+  private JwtEcdsaPublicKey(
+      JwtEcdsaParameters parameters,
+      ECPoint publicPoint,
+      Optional<String> kid,
+      Optional<Integer> idRequirement) {
+    this.parameters = parameters;
+    this.publicPoint = publicPoint;
+    this.kid = kid;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public ECPoint getPublicPoint() {
+    return publicPoint;
+  }
+
+  @Override
+  public Optional<String> getKid() {
+    return kid;
+  }
+
+  @Nullable
+  @Override
+  public Integer getIdRequirementOrNull() {
+    return idRequirement.orElse(null);
+  }
+
+  @Override
+  public JwtEcdsaParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof JwtEcdsaPublicKey)) {
+      return false;
+    }
+    JwtEcdsaPublicKey that = (JwtEcdsaPublicKey) o;
+    return that.parameters.equals(parameters)
+        && that.publicPoint.equals(publicPoint)
+        && that.kid.equals(kid);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManager.java
index 1c21dcf..9735a70 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManager.java
@@ -231,6 +231,7 @@
   public static void registerPair(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerAsymmetricKeyManagers(
         new JwtEcdsaSignKeyManager(), new JwtEcdsaVerifyKeyManager(), newKeyAllowed);
+    JwtEcdsaProtoSerialization.register();
   }
 
   private static KeyFactory.KeyFormat<JwtEcdsaKeyFormat> createKeyFormat(
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManager.java
index ccd5597..4b1de63 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManager.java
@@ -72,7 +72,7 @@
   static final void validateEcdsaAlgorithm(JwtEcdsaAlgorithm algorithm)
       throws GeneralSecurityException {
     // Purposely ignore the result. This function will throw if the algorithm is invalid.
-    hashForEcdsaAlgorithm(algorithm);
+    Object unused = hashForEcdsaAlgorithm(algorithm);
   }
 
   private static class JwtPublicKeyVerifyFactory
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtFormat.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtFormat.java
index 727310b..715db85 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtFormat.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtFormat.java
@@ -16,7 +16,7 @@
 
 package com.google.crypto.tink.jwt;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.crypto.tink.internal.Util.UTF_8;
 
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Base64;
@@ -79,7 +79,7 @@
     }
   }
 
-  private static String validateAlgorithm(String algo) throws InvalidAlgorithmParameterException {
+  private static void validateAlgorithm(String algo) throws InvalidAlgorithmParameterException {
     switch (algo) {
       case "HS256":
       case "HS384":
@@ -93,7 +93,7 @@
       case "PS256":
       case "PS384":
       case "PS512":
-        return algo;
+        return;
       default:
         throw new InvalidAlgorithmParameterException("invalid algorithm: " + algo);
     }
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKey.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKey.java
new file mode 100644
index 0000000..6fb92d9
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKey.java
@@ -0,0 +1,188 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/** Represents a JWT HMAC key to create and verify JWT using HMAC. */
+public class JwtHmacKey extends JwtMacKey {
+  private final JwtHmacParameters parameters;
+  private final SecretBytes key;
+  private final Optional<Integer> idRequirement;
+  private final Optional<String> kid;
+
+  /** Helps creating new {@code JwtHmacKey} objects. */
+  public static class Builder {
+    private Optional<JwtHmacParameters> parameters = Optional.empty();
+    private Optional<SecretBytes> keyBytes = Optional.empty();
+    private Optional<Integer> idRequirement = Optional.empty();
+    private Optional<String> customKid = Optional.empty();
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(JwtHmacParameters parameters) {
+      this.parameters = Optional.of(parameters);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = Optional.of(keyBytes);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(int idRequirement) {
+      this.idRequirement = Optional.of(idRequirement);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setCustomKid(String customKid) {
+      this.customKid = Optional.of(customKid);
+      return this;
+    }
+
+    private Optional<String> computeKid() throws GeneralSecurityException {
+      if (parameters
+          .get()
+          .getKidStrategy()
+          .equals(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)) {
+        byte[] bigEndianKeyId = ByteBuffer.allocate(4).putInt(idRequirement.get()).array();
+        if (customKid.isPresent()) {
+          throw new GeneralSecurityException(
+              "customKid must not be set for KidStrategy BASE64_ENCODED_KEY_ID");
+        }
+        return Optional.of(Base64.urlSafeEncode(bigEndianKeyId));
+      }
+      if (parameters.get().getKidStrategy().equals(JwtHmacParameters.KidStrategy.CUSTOM)) {
+        if (!customKid.isPresent()) {
+          throw new GeneralSecurityException("customKid needs to be set for KidStrategy CUSTOM");
+        }
+        return customKid;
+      }
+      if (parameters.get().getKidStrategy().equals(JwtHmacParameters.KidStrategy.IGNORED)) {
+        if (customKid.isPresent()) {
+          throw new GeneralSecurityException("customKid must not be set for KidStrategy IGNORED");
+        }
+        return Optional.empty();
+      }
+      throw new IllegalStateException("Unknown kid strategy");
+    }
+
+    public JwtHmacKey build() throws GeneralSecurityException {
+      if (!parameters.isPresent()) {
+        throw new GeneralSecurityException("Parameters are required");
+      }
+      if (!keyBytes.isPresent()) {
+        throw new GeneralSecurityException("KeyBytes are required");
+      }
+
+      if (parameters.get().getKeySizeBytes() != keyBytes.get().size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.get().hasIdRequirement() && !idRequirement.isPresent()) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.get().hasIdRequirement() && idRequirement.isPresent()) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+
+      return new JwtHmacKey(parameters.get(), keyBytes.get(), idRequirement, computeKid());
+    }
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private JwtHmacKey(
+      JwtHmacParameters parameters,
+      SecretBytes key,
+      Optional<Integer> idRequirement,
+      Optional<String> kid) {
+    this.parameters = parameters;
+    this.key = key;
+    this.idRequirement = idRequirement;
+    this.kid = kid;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return key;
+  }
+
+  /**
+   * Returns the "kid" to be used for this key.
+   *
+   * <p>If present, this kid will be written into the {@code kid} header during {@code
+   * computeMacAndEncode}. If absent, no kid will be written.
+   *
+   * <p>If present, and the {@code kid} header is present, the contents of the {@code kid} header
+   * needs to match the return value of this function.
+   */
+  @Override
+  public Optional<String> getKid() {
+    return kid;
+  }
+
+  @Nullable
+  @Override
+  public Integer getIdRequirementOrNull() {
+    return idRequirement.orElse(null);
+  }
+
+  @Override
+  public JwtHmacParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof JwtHmacKey)) {
+      return false;
+    }
+    JwtHmacKey that = (JwtHmacKey) o;
+    return that.parameters.equals(parameters)
+        && that.key.equalsSecretBytes(key)
+        && that.kid.equals(kid)
+        && that.idRequirement.equals(idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKeyManager.java
index 6c7d7c6..5f87915 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacKeyManager.java
@@ -242,6 +242,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new JwtHmacKeyManager(), newKeyAllowed);
+    JwtHmacProtoSerialization.register();
   }
 
   /** Returns a {@link KeyTemplate} that generates new instances of HS256 256-bit keys. */
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacParameters.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacParameters.java
new file mode 100644
index 0000000..01dcb31
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacParameters.java
@@ -0,0 +1,207 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import java.util.Optional;
+
+/** Describes the parameters of a {@code JwtHmacKey}. */
+public class JwtHmacParameters extends JwtMacParameters {
+  /** Specifies how the "kid" header is handled. */
+  @Immutable
+  public static final class KidStrategy {
+    /**
+     * The "kid" is the URL safe (RFC 4648 Section 5) base64-encoded big-endian key_id in the
+     * keyset.
+     *
+     * <p>In {@code computeMacAndEncode} Tink always adds the KID.
+     *
+     * <p>In {@code verifyMacAndDecode} Tink checks that the kid is present and equal to this value.
+     *
+     * <p>This strategy is recommended by Tink.
+     */
+    public static final KidStrategy BASE64_ENCODED_KEY_ID =
+        new KidStrategy("BASE64_ENCODED_KEY_ID");
+
+    /**
+     * The "kid" header is ignored.
+     *
+     * <p>In {@code computeMacAndEncode} Tink does not write a "kid" header.
+     *
+     * <p>In {@code verifyMacAndDecode} Tink ignores the "kid" header.
+     */
+    public static final KidStrategy IGNORED = new KidStrategy("IGNORED");
+
+    /**
+     * The "kid" is fixed. It can be obtained from {@code parameters.getCustomKid()}.
+     *
+     * <p>In {@code computeMacAndEncode} Tink writes the "kid" header to the value given by {@code
+     * parameters.getCustomKid()}.
+     *
+     * <p>In {@code verifyMacAndDecode} If the kid is present, it needs to match {@code
+     * parameters.getCustomKid()}. If the kid is absent, it will be accepted.
+     *
+     * <p>Note: Tink does not allow to randomly generate new {@link JwtHmacKey} objects from
+     * parameters objects with {@code KidStrategy} equals to {@code CUSTOM}.
+     */
+    public static final KidStrategy CUSTOM = new KidStrategy("CUSTOM");
+
+    private final String name;
+
+    private KidStrategy(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The algorithm to be used for the mac computation. */
+  @Immutable
+  public static final class Algorithm {
+    public static final Algorithm HS256 = new Algorithm("HS256");
+    public static final Algorithm HS384 = new Algorithm("HS384");
+    public static final Algorithm HS512 = new Algorithm("HS512");
+
+    private final String name;
+
+    private Algorithm(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+
+    public String getStandardName() {
+      return name;
+    }
+  }
+
+  /** Helps creating a {@code JwtHmacParameters} object. */
+  public static final class Builder {
+    Optional<Integer> keySizeBytes = Optional.empty();
+    Optional<KidStrategy> kidStrategy = Optional.empty();
+    Optional<Algorithm> algorithm = Optional.empty();
+
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) {
+      this.keySizeBytes = Optional.of(keySizeBytes);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKidStrategy(KidStrategy kidStrategy) {
+      this.kidStrategy = Optional.of(kidStrategy);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setAlgorithm(Algorithm algorithm) {
+      this.algorithm = Optional.of(algorithm);
+      return this;
+    }
+
+    public JwtHmacParameters build() throws GeneralSecurityException {
+      if (!keySizeBytes.isPresent()) {
+        throw new GeneralSecurityException("Key Size must be set");
+      }
+      if (!algorithm.isPresent()) {
+        throw new GeneralSecurityException("Algorithm must be set");
+      }
+      if (!kidStrategy.isPresent()) {
+        throw new GeneralSecurityException("KidStrategy must be set");
+      }
+      if (keySizeBytes.get() < 16) {
+        throw new GeneralSecurityException("Key size must be at least 16 bytes");
+      }
+      return new JwtHmacParameters(keySizeBytes.get(), kidStrategy.get(), algorithm.get());
+    }
+
+    private Builder() {}
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private JwtHmacParameters(int keySizeBytes, KidStrategy kidStrategy, Algorithm algorithm) {
+    this.keySizeBytes = keySizeBytes;
+    this.kidStrategy = kidStrategy;
+    this.algorithm = algorithm;
+  }
+
+  private final int keySizeBytes;
+  private final KidStrategy kidStrategy;
+  private final Algorithm algorithm;
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  public KidStrategy getKidStrategy() {
+    return kidStrategy;
+  }
+
+  public Algorithm getAlgorithm() {
+    return algorithm;
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return kidStrategy.equals(KidStrategy.BASE64_ENCODED_KEY_ID);
+  }
+
+  @Override
+  public boolean allowKidAbsent() {
+    return kidStrategy.equals(KidStrategy.CUSTOM)
+        || kidStrategy.equals(KidStrategy.IGNORED);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof JwtHmacParameters)) {
+      return false;
+    }
+    JwtHmacParameters that = (JwtHmacParameters) o;
+    return that.keySizeBytes == keySizeBytes
+        && that.kidStrategy.equals(kidStrategy)
+        && that.algorithm.equals(algorithm);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(JwtHmacParameters.class, keySizeBytes, kidStrategy, algorithm);
+  }
+
+  @Override
+  public String toString() {
+    return "JWT HMAC Parameters (kidStrategy: "
+        + kidStrategy
+        + ", Algorithm "
+        + algorithm
+        + ", and "
+        + keySizeBytes
+        + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacProtoSerialization.java
new file mode 100644
index 0000000..d98cefe
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtHmacProtoSerialization.java
@@ -0,0 +1,263 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.JwtHmacAlgorithm;
+import com.google.crypto.tink.proto.JwtHmacKey.CustomKid;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link JwtHmacKey} objects and {@link JwtHmacParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class JwtHmacProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.JwtHmacKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<JwtHmacParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              JwtHmacProtoSerialization::serializeParameters,
+              JwtHmacParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          JwtHmacProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<JwtHmacKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          JwtHmacProtoSerialization::serializeKey, JwtHmacKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          JwtHmacProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static JwtHmacAlgorithm toProtoAlgorithm(JwtHmacParameters.Algorithm hashType)
+      throws GeneralSecurityException {
+    if (JwtHmacParameters.Algorithm.HS256.equals(hashType)) {
+      return JwtHmacAlgorithm.HS256;
+    }
+    if (JwtHmacParameters.Algorithm.HS384.equals(hashType)) {
+      return JwtHmacAlgorithm.HS384;
+    }
+    if (JwtHmacParameters.Algorithm.HS512.equals(hashType)) {
+      return JwtHmacAlgorithm.HS512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static JwtHmacParameters.Algorithm toAlgorithm(JwtHmacAlgorithm hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case HS256:
+        return JwtHmacParameters.Algorithm.HS256;
+      case HS384:
+        return JwtHmacParameters.Algorithm.HS384;
+      case HS512:
+        return JwtHmacParameters.Algorithm.HS512;
+      default:
+        throw new GeneralSecurityException("Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.JwtHmacKeyFormat serializeToJwtHmacKeyFormat(
+      JwtHmacParameters parameters) throws GeneralSecurityException {
+    if (parameters.getKidStrategy().equals(JwtHmacParameters.KidStrategy.CUSTOM)) {
+      throw new GeneralSecurityException(
+          "Unable to serialize Parameters object with KidStrategy CUSTOM");
+    }
+    return com.google.crypto.tink.proto.JwtHmacKeyFormat.newBuilder()
+        .setVersion(0)
+        .setAlgorithm(toProtoAlgorithm(parameters.getAlgorithm()))
+        .setKeySize(parameters.getKeySizeBytes())
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(JwtHmacParameters parameters)
+      throws GeneralSecurityException {
+    OutputPrefixType outputPrefixType = OutputPrefixType.TINK;
+    if (parameters.getKidStrategy().equals(JwtHmacParameters.KidStrategy.IGNORED)) {
+      outputPrefixType = OutputPrefixType.RAW;
+    }
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(serializeToJwtHmacKeyFormat(parameters).toByteString())
+            .setOutputPrefixType(outputPrefixType)
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      JwtHmacKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    com.google.crypto.tink.proto.JwtHmacKey.Builder protoKeyBuilder =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder();
+    protoKeyBuilder
+        .setVersion(0)
+        .setAlgorithm(toProtoAlgorithm(key.getParameters().getAlgorithm()))
+        .setKeyValue(
+            ByteString.copyFrom(
+                key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))));
+    OutputPrefixType outputPrefixType = null;
+    if (key.getParameters().getKidStrategy().equals(JwtHmacParameters.KidStrategy.CUSTOM)) {
+      protoKeyBuilder.setCustomKid(CustomKid.newBuilder().setValue(key.getKid().get()));
+      outputPrefixType = OutputPrefixType.RAW;
+    }
+    if (key.getParameters().getKidStrategy().equals(JwtHmacParameters.KidStrategy.IGNORED)) {
+      outputPrefixType = OutputPrefixType.RAW;
+    }
+    if (key.getParameters()
+        .getKidStrategy()
+        .equals(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)) {
+      outputPrefixType = OutputPrefixType.TINK;
+    }
+    if (outputPrefixType == null) {
+      throw new GeneralSecurityException(
+          "Unknown KID Strategy in " + key.getParameters().getKidStrategy());
+    }
+
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        protoKeyBuilder.build().toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        outputPrefixType,
+        key.getIdRequirementOrNull());
+  }
+
+  private static JwtHmacParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to JwtHmacProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.JwtHmacKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.JwtHmacKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HmacParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException(
+          "Parsing HmacParameters failed: unknown Version " + format.getVersion());
+    }
+    JwtHmacParameters.KidStrategy kidStrategy = null;
+    if (serialization.getKeyTemplate().getOutputPrefixType().equals(OutputPrefixType.TINK)) {
+      kidStrategy = JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID;
+    }
+    if (serialization.getKeyTemplate().getOutputPrefixType().equals(OutputPrefixType.RAW)) {
+      kidStrategy = JwtHmacParameters.KidStrategy.IGNORED;
+    }
+    if (kidStrategy == null) {
+      throw new GeneralSecurityException("Invalid OutputPrefixType for JwtHmacKeyFormat");
+    }
+    return JwtHmacParameters.builder()
+        .setAlgorithm(toAlgorithm(format.getAlgorithm()))
+        .setKeySizeBytes(format.getKeySize())
+        .setKidStrategy(kidStrategy)
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static JwtHmacKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HmacProtoSerialization.parseKey");
+    }
+    try {
+      com.google.crypto.tink.proto.JwtHmacKey protoKey =
+          com.google.crypto.tink.proto.JwtHmacKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      JwtHmacParameters.Builder parametersBuilder = JwtHmacParameters.builder();
+      JwtHmacKey.Builder keyBuilder = JwtHmacKey.builder();
+      if (serialization.getOutputPrefixType().equals(OutputPrefixType.TINK)) {
+        if (protoKey.hasCustomKid()) {
+          throw new GeneralSecurityException(
+              "Keys serialized with OutputPrefixType TINK should not have a custom kid");
+        }
+        @Nullable Integer idRequirement = serialization.getIdRequirementOrNull();
+        if (idRequirement == null) {
+          throw new GeneralSecurityException(
+              "Keys serialized with OutputPrefixType TINK need an ID Requirement");
+        }
+        parametersBuilder.setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID);
+        keyBuilder.setIdRequirement(idRequirement);
+      } else if (serialization.getOutputPrefixType().equals(OutputPrefixType.RAW)) {
+        if (protoKey.hasCustomKid()) {
+          parametersBuilder.setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM);
+          keyBuilder.setCustomKid(protoKey.getCustomKid().getValue());
+        } else {
+          parametersBuilder.setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED);
+        }
+      }
+      parametersBuilder.setAlgorithm(toAlgorithm(protoKey.getAlgorithm()));
+      parametersBuilder.setKeySizeBytes(protoKey.getKeyValue().size());
+      return keyBuilder
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setParameters(parametersBuilder.build())
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing HmacKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private JwtHmacProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacKey.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacKey.java
new file mode 100644
index 0000000..6c4dabe
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacKey.java
@@ -0,0 +1,46 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.Key;
+import java.util.Optional;
+
+/**
+ * Represents a key to compute JWT using symmetric cryptography (i.e., using the {@link JwtMac}
+ * interface).
+ */
+public abstract class JwtMacKey extends Key {
+  /**
+   * Returns the "kid" to be used for this key (https://www.rfc-editor.org/rfc/rfc7517#section-4.5).
+   *
+   * <p>Note that the "kid" is not necessarily related to Tink's "Key ID" in the keyset.
+   *
+   * <p>If present, this kid will be written into the {@code kid} header during {@code
+   * computeMacAndEncode}. If absent, no kid will be written.
+   *
+   * <p>If present, and the {@code kid} header is present, the contents of the {@code kid} header
+   * needs to match the return value of this function.
+   *
+   * <p>Note that {@code getParameters.allowKidAbsent()} specifies if omitting the {@code kid}
+   * header is allowed. Of course, if {@code getParameters.allowKidAbsent()} is true, then {@code
+   * getKid} must not return an empty {@link Optional}.
+   */
+  public abstract Optional<String> getKid();
+
+  @Override
+  public abstract JwtMacParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacParameters.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacParameters.java
new file mode 100644
index 0000000..7484b64
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacParameters.java
@@ -0,0 +1,25 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.Parameters;
+
+/** Represents a description of a {@link JwtMacKey} excluding the randomly chosen key material. */
+public abstract class JwtMacParameters extends Parameters {
+  /** If true, tokens without {@code "kid"} header are allowed when verifying a token. */
+  public abstract boolean allowKidAbsent();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacWrapper.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacWrapper.java
index 973915c..e66c170 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtMacWrapper.java
@@ -29,6 +29,9 @@
  * JwtMacWrapper is the implementation of {@link PrimitiveWrapper} for the {@link JwtMac} primitive.
  */
 class JwtMacWrapper implements PrimitiveWrapper<JwtMacInternal, JwtMac> {
+
+  private static final JwtMacWrapper WRAPPER = new JwtMacWrapper();
+
   private static void validate(PrimitiveSet<JwtMacInternal> primitiveSet)
       throws GeneralSecurityException {
     if (primitiveSet.getPrimary() == null) {
@@ -105,6 +108,6 @@
   }
 
  public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new JwtMacWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtNames.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtNames.java
index ee7cbac..b892610 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtNames.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtNames.java
@@ -39,8 +39,7 @@
   static final String HEADER_TYPE = "typ";
   static final String HEADER_CRITICAL = "crit";
 
-
-  static String validate(String name) {
+  static void validate(String name) {
     if (isRegisteredName(name)) {
       throw new IllegalArgumentException(
           String.format(
@@ -48,7 +47,6 @@
                   + " setter method.",
               name));
     }
-    return name;
   }
 
   static boolean isRegisteredName(String name) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeySignWrapper.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeySignWrapper.java
index 02e13af..241d7b4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeySignWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeySignWrapper.java
@@ -35,6 +35,8 @@
 class JwtPublicKeySignWrapper
     implements PrimitiveWrapper<JwtPublicKeySignInternal, JwtPublicKeySign> {
 
+  private static final JwtPublicKeySignWrapper WRAPPER = new JwtPublicKeySignWrapper();
+
   private static void validate(PrimitiveSet<JwtPublicKeySignInternal> primitiveSet)
       throws GeneralSecurityException {
     if (primitiveSet.getPrimary() == null) {
@@ -94,6 +96,6 @@
    * argument.
    */
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new JwtPublicKeySignWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeyVerifyWrapper.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeyVerifyWrapper.java
index 92b1a48..47c696a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeyVerifyWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtPublicKeyVerifyWrapper.java
@@ -29,6 +29,8 @@
 class JwtPublicKeyVerifyWrapper
     implements PrimitiveWrapper<JwtPublicKeyVerifyInternal, JwtPublicKeyVerify> {
 
+  private static final JwtPublicKeyVerifyWrapper WRAPPER = new JwtPublicKeyVerifyWrapper();
+
   private static void validate(PrimitiveSet<JwtPublicKeyVerifyInternal> primitiveSet)
       throws GeneralSecurityException {
     for (List<PrimitiveSet.Entry<JwtPublicKeyVerifyInternal>> entries : primitiveSet.getAll()) {
@@ -100,6 +102,6 @@
    * argument.
    */
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new JwtPublicKeyVerifyWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignatureParameters.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignatureParameters.java
new file mode 100644
index 0000000..9dcc05f
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignatureParameters.java
@@ -0,0 +1,27 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.Parameters;
+
+/**
+ * Represents a description of a {@link JwtSignatureKey} excluding the randomly chosen key material.
+ */
+public abstract class JwtSignatureParameters extends Parameters {
+  /** If true, tokens without {@code "kid"} header are allowed when verifying a token. */
+  public abstract boolean allowKidAbsent();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignaturePrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignaturePrivateKey.java
new file mode 100644
index 0000000..d0eab83
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignaturePrivateKey.java
@@ -0,0 +1,63 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrivateKey;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.Immutable;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a key to compute JWT using asymmetric cryptography (i.e., using the {@link
+ * JwtPublicKeySign} interface).
+ */
+@Immutable
+@Alpha
+public abstract class JwtSignaturePrivateKey extends Key implements PrivateKey {
+  @Override
+  public abstract JwtSignaturePublicKey getPublicKey();
+
+  /**
+   * Returns the "kid" to be used for this key (https://www.rfc-editor.org/rfc/rfc7517#section-4.5).
+   *
+   * <p>Note that the "kid" is not necessarily related to Tink's "Key ID" in the keyset.
+   *
+   * <p>If present, this kid will be written into the {@code kid} header during {@code
+   * computeMacAndEncode}. If absent, no kid will be written.
+   *
+   * <p>If present, and the {@code kid} header is present, the contents of the {@code kid} header
+   * needs to match the return value of this function.
+   *
+   * <p>Note that {@code getParameters.allowKidAbsent()} specifies if omitting the {@code kid}
+   * header is allowed. Of course, if {@code getParameters.allowKidAbsent()} is true, then {@code
+   * getKid} must not return an empty {@link Optional}.
+   */
+  public Optional<String> getKid() {
+    return getPublicKey().getKid();
+  }
+
+  @Override
+  public abstract JwtSignatureParameters getParameters();
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return getPublicKey().getIdRequirementOrNull();
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignaturePublicKey.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignaturePublicKey.java
new file mode 100644
index 0000000..cd125e5
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtSignaturePublicKey.java
@@ -0,0 +1,47 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import com.google.crypto.tink.Key;
+import java.util.Optional;
+
+/**
+ * Represents a key to verify JWT using asymmetric cryptography (i.e., using the {@link
+ * JwtPublicKeyVerify} interface).
+ */
+public abstract class JwtSignaturePublicKey extends Key {
+  /**
+   * Returns the "kid" to be used for this key (https://www.rfc-editor.org/rfc/rfc7517#section-4.5).
+   *
+   * <p>Note that the "kid" is not necessarily related to Tink's "Key ID" in the keyset.
+   *
+   * <p>If present, this kid will be written into the {@code kid} header during {@code
+   * computeMacAndEncode}. If absent, no kid will be written.
+   *
+   * <p>If present, and the {@code kid} header is present, the contents of the {@code kid} header
+   * needs to match the return value of this function.
+   *
+   * <p>Note that {@code getParameters.allowKidAbsent()} specifies if omitting the {@code kid}
+   * header is allowed. Of course, if {@code getParameters.allowKidAbsent()} is true, then {@code
+   * getKid} must not return an empty {@link Optional}.
+   */
+  public abstract Optional<String> getKid();
+
+  /** Returns the parameters of this key. */
+  @Override
+  public abstract JwtSignatureParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtValidator.java b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtValidator.java
index fd3859b..92c58f5 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/JwtValidator.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/JwtValidator.java
@@ -16,6 +16,7 @@
 
 package com.google.crypto.tink.jwt;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import java.time.Clock;
 import java.time.Duration;
@@ -104,6 +105,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.1
      */
+    @CanIgnoreReturnValue
     public Builder expectTypeHeader(String value) {
       if (value == null) {
         throw new NullPointerException("typ header cannot be null");
@@ -113,6 +115,7 @@
     }
 
     /** Lets the validator ignore the {@code typ} header. */
+    @CanIgnoreReturnValue
     public Builder ignoreTypeHeader() {
       this.ignoreTypeHeader = true;
       return this;
@@ -128,6 +131,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.1
      */
+    @CanIgnoreReturnValue
     public Builder expectIssuer(String value) {
       if (value == null) {
         throw new NullPointerException("issuer cannot be null");
@@ -137,6 +141,7 @@
     }
 
     /** Lets the validator ignore the {@code iss} claim. */
+    @CanIgnoreReturnValue
     public Builder ignoreIssuer() {
       this.ignoreIssuer = true;
       return this;
@@ -152,6 +157,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3
      */
+    @CanIgnoreReturnValue
     public Builder expectAudience(String value) {
       if (value == null) {
         throw new NullPointerException("audience cannot be null");
@@ -161,18 +167,21 @@
     }
 
     /** Lets the validator ignore the {@code aud} claim. */
+    @CanIgnoreReturnValue
     public Builder ignoreAudiences() {
       this.ignoreAudiences = true;
       return this;
     }
 
-    /** Checks that the {@code iat} claim is in the past.*/
+    /** Checks that the {@code iat} claim is in the past. */
+    @CanIgnoreReturnValue
     public Builder expectIssuedInThePast() {
       this.expectIssuedInThePast = true;
       return this;
     }
 
     /** Sets the clock used to verify timestamp claims. */
+    @CanIgnoreReturnValue
     public Builder setClock(Clock clock) {
       if (clock == null) {
         throw new NullPointerException("clock cannot be null");
@@ -188,6 +197,7 @@
      * <p>As recommended by https://tools.ietf.org/html/rfc7519, the clock skew should usually be no
      * more than a few minutes. In this implementation, the maximum value is 10 minutes.
      */
+    @CanIgnoreReturnValue
     public Builder setClockSkew(Duration clockSkew) {
       if (clockSkew.compareTo(MAX_CLOCK_SKEW) > 0) {
         throw new IllegalArgumentException("Clock skew too large, max is 10 minutes");
@@ -202,6 +212,7 @@
      * <p>In most cases, tokens should always have an expiration, so this option should rarely be
      * used.
      */
+    @CanIgnoreReturnValue
     public Builder allowMissingExpiration() {
       this.allowMissingExpiration = true;
       return this;
diff --git a/java_src/src/main/java/com/google/crypto/tink/jwt/RawJwt.java b/java_src/src/main/java/com/google/crypto/tink/jwt/RawJwt.java
index 1cfa64d..9c2fe94 100644
--- a/java_src/src/main/java/com/google/crypto/tink/jwt/RawJwt.java
+++ b/java_src/src/main/java/com/google/crypto/tink/jwt/RawJwt.java
@@ -16,6 +16,7 @@
 
 package com.google.crypto.tink.jwt;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
@@ -149,6 +150,7 @@
      * https://tools.ietf.org/html/rfc7519#section-5.1 and
      * https://tools.ietf.org/html/rfc8725#section-3.11
      */
+    @CanIgnoreReturnValue
     public Builder setTypeHeader(String value) {
       typeHeader = Optional.of(value);
       return this;
@@ -159,6 +161,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.1
      */
+    @CanIgnoreReturnValue
     public Builder setIssuer(String value) {
       if (!JsonUtil.isValidString(value)) {
         throw new IllegalArgumentException();
@@ -172,6 +175,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.2
      */
+    @CanIgnoreReturnValue
     public Builder setSubject(String value) {
       if (!JsonUtil.isValidString(value)) {
         throw new IllegalArgumentException();
@@ -183,11 +187,12 @@
     /**
      * Sets the audience that the JWT is intended for.
      *
-     * Sets the {@code aud} claim as a string. This method can't be used
-     * together with {@code setAudiences} or {@code addAudience}.
+     * <p>Sets the {@code aud} claim as a string. This method can't be used together with {@code
+     * setAudiences} or {@code addAudience}.
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3
      */
+    @CanIgnoreReturnValue
     public Builder setAudience(String value) {
       if (payload.has(JwtNames.CLAIM_AUDIENCE)
           && payload.get(JwtNames.CLAIM_AUDIENCE).isJsonArray()) {
@@ -204,11 +209,12 @@
     /**
      * Sets the audiences that the JWT is intended for.
      *
-     * Sets the {@code aud} claim as an array of strings. This method can't be used
-     * together with {@code setAudience}.
+     * <p>Sets the {@code aud} claim as an array of strings. This method can't be used together with
+     * {@code setAudience}.
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3
      */
+    @CanIgnoreReturnValue
     public Builder setAudiences(List<String> values) {
       if (payload.has(JwtNames.CLAIM_AUDIENCE)
               && !payload.get(JwtNames.CLAIM_AUDIENCE).isJsonArray()) {
@@ -231,11 +237,12 @@
     /**
      * Adds an audience that the JWT is intended for.
      *
-     * The {@code aud} claim will always be encoded as an array of strings. This method
-     * can't be used together with {@code setAudience}.
+     * <p>The {@code aud} claim will always be encoded as an array of strings. This method can't be
+     * used together with {@code setAudience}.
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3
      */
+    @CanIgnoreReturnValue
     public Builder addAudience(String value) {
       if (!JsonUtil.isValidString(value)) {
         throw new IllegalArgumentException("invalid string");
@@ -261,6 +268,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.7
      */
+    @CanIgnoreReturnValue
     public Builder setJwtId(String value) {
       if (!JsonUtil.isValidString(value)) {
         throw new IllegalArgumentException();
@@ -289,6 +297,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.4
      */
+    @CanIgnoreReturnValue
     public Builder setExpiration(Instant value) {
       setTimestampClaim(JwtNames.CLAIM_EXPIRATION, value);
       return this;
@@ -301,6 +310,7 @@
      * that this is not forgotten, by requiring to user to explicitly state that no expiration
      * should be set.
      */
+    @CanIgnoreReturnValue
     public Builder withoutExpiration() {
       this.withoutExpiration = true;
       return this;
@@ -316,6 +326,7 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.5
      */
+    @CanIgnoreReturnValue
     public Builder setNotBefore(Instant value) {
       setTimestampClaim(JwtNames.CLAIM_NOT_BEFORE, value);
       return this;
@@ -330,19 +341,30 @@
      *
      * <p>https://tools.ietf.org/html/rfc7519#section-4.1.6
      */
+    @CanIgnoreReturnValue
     public Builder setIssuedAt(Instant value) {
       setTimestampClaim(JwtNames.CLAIM_ISSUED_AT, value);
       return this;
     }
 
     /** Adds a custom claim of type {@code boolean} to the JWT. */
+    @CanIgnoreReturnValue
     public Builder addBooleanClaim(String name, boolean value) {
       JwtNames.validate(name);
       payload.add(name, new JsonPrimitive(value));
       return this;
     }
 
+    /** Adds a custom claim of type {@code long} to the JWT. */
+    @CanIgnoreReturnValue
+    public Builder addNumberClaim(String name, long value) {
+      JwtNames.validate(name);
+      payload.add(name, new JsonPrimitive(value));
+      return this;
+    }
+
     /** Adds a custom claim of type {@code double} to the JWT. */
+    @CanIgnoreReturnValue
     public Builder addNumberClaim(String name, double value) {
       JwtNames.validate(name);
       payload.add(name, new JsonPrimitive(value));
@@ -350,6 +372,7 @@
     }
 
     /** Adds a custom claim of type {@code String} to the JWT. */
+    @CanIgnoreReturnValue
     public Builder addStringClaim(String name, String value) {
       if (!JsonUtil.isValidString(value)) {
         throw new IllegalArgumentException();
@@ -360,6 +383,7 @@
     }
 
     /** Adds a custom claim with value null. */
+    @CanIgnoreReturnValue
     public Builder addNullClaim(String name) {
       JwtNames.validate(name);
       payload.add(name, JsonNull.INSTANCE);
@@ -367,6 +391,7 @@
     }
 
     /** Adds a custom claim encoded in a JSON {@code String} to the JWT. */
+    @CanIgnoreReturnValue
     public Builder addJsonObjectClaim(String name, String encodedJsonObject)
         throws JwtInvalidException {
       JwtNames.validate(name);
@@ -375,6 +400,7 @@
     }
 
     /** Adds a custom claim encoded in a JSON {@code String} to the JWT. */
+    @CanIgnoreReturnValue
     public Builder addJsonArrayClaim(String name, String encodedJsonArray)
         throws JwtInvalidException {
       JwtNames.validate(name);
@@ -387,7 +413,7 @@
     }
   }
 
-  String getJsonPayload() {
+  public String getJsonPayload() {
     return payload.toString();
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/keyderivation/BUILD.bazel
new file mode 100644
index 0000000..2a9caba
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/BUILD.bazel
@@ -0,0 +1,187 @@
+load("@build_bazel_rules_android//android:rules.bzl", "android_library")
+
+licenses(["notice"])
+
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "key_derivation_config",
+    srcs = ["KeyDerivationConfig.java"],
+    deps = [
+        ":keyset_deriver_wrapper",
+        "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager",
+    ],
+)
+
+java_library(
+    name = "key_derivation_key_templates",
+    srcs = ["KeyDerivationKeyTemplates.java"],
+    deps = [
+        "//proto:prf_based_deriver_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "keyset_deriver",
+    srcs = ["KeysetDeriver.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "keyset_deriver_wrapper",
+    srcs = ["KeysetDeriverWrapper.java"],
+    deps = [
+        ":keyset_deriver",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "key_derivation_config-android",
+    srcs = ["KeyDerivationConfig.java"],
+    deps = [
+        ":keyset_deriver_wrapper-android",
+        "//src/main/java/com/google/crypto/tink/config:tink_fips-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager-android",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager-android",
+    ],
+)
+
+android_library(
+    name = "key_derivation_key_templates-android",
+    srcs = ["KeyDerivationKeyTemplates.java"],
+    deps = [
+        "//proto:prf_based_deriver_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:key_template-android",
+        "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "keyset_deriver-android",
+    srcs = ["KeysetDeriver.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "keyset_deriver_wrapper-android",
+    srcs = ["KeysetDeriverWrapper.java"],
+    deps = [
+        ":keyset_deriver-android",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:primitive_set-android",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "key_derivation_key-android",
+    srcs = ["KeyDerivationKey.java"],
+    deps = [
+        ":key_derivation_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+    ],
+)
+
+android_library(
+    name = "key_derivation_parameters-android",
+    srcs = ["KeyDerivationParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters-android"],
+)
+
+java_library(
+    name = "key_derivation_key",
+    srcs = ["KeyDerivationKey.java"],
+    deps = [
+        ":key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+    ],
+)
+
+java_library(
+    name = "key_derivation_parameters",
+    srcs = ["KeyDerivationParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters"],
+)
+
+java_library(
+    name = "prf_based_key_derivation_parameters",
+    srcs = ["PrfBasedKeyDerivationParameters.java"],
+    deps = [
+        ":key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "prf_based_key_derivation_parameters-android",
+    srcs = ["PrfBasedKeyDerivationParameters.java"],
+    deps = [
+        ":key_derivation_parameters-android",
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "prf_based_key_derivation_key-android",
+    srcs = ["PrfBasedKeyDerivationKey.java"],
+    deps = [
+        ":key_derivation_key-android",
+        ":prf_based_key_derivation_parameters-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/prf:prf_key-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "prf_based_key_derivation_key",
+    srcs = ["PrfBasedKeyDerivationKey.java"],
+    deps = [
+        ":key_derivation_key",
+        ":prf_based_key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/prf:prf_key",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationConfig.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationConfig.java
new file mode 100644
index 0000000..43db697
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationConfig.java
@@ -0,0 +1,62 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.config.TinkFips;
+import com.google.crypto.tink.keyderivation.internal.PrfBasedDeriverKeyManager;
+import com.google.crypto.tink.prf.HkdfPrfKeyManager;
+import java.security.GeneralSecurityException;
+
+/**
+ * Static methods and constants for registering with the {@link com.google.crypto.tink.Registry} all
+ * instances of {@link KeysetDeriver} key types supported in a particular release of Tink.
+ *
+ * <p>To register all {@link KeysetDeriver} key types provided in the latest Tink version one can
+ * do:
+ *
+ * <pre>{@code
+ * KeyDerivationConfig.register();
+ * }</pre>
+ *
+ * <p>For more information on how to obtain and use instances of {@link KeysetDeriver}, see {@link
+ * com.google.crypto.tink.KeysetHandle#getPrimitive}.
+ */
+public final class KeyDerivationConfig {
+  /**
+   * Tries to register with the {@link com.google.crypto.tink.Registry} all instances of {@link
+   * com.google.crypto.tink.KeyManager} needed to handle KeysetDeriver key types supported in Tink.
+   */
+  public static void register() throws GeneralSecurityException {
+    // Register primitive wrappers.
+    KeysetDeriverWrapper.register();
+
+    if (TinkFips.useOnlyFips()) {
+      // If Tink is built in FIPS-mode do not register algorithms which are not compatible.
+      // Currently there are no FIPS-compliant key derivation primitives available, therefore no
+      // key manager will be registered.
+      return;
+    }
+
+    // Register required key manager for PrfBasedDeriverKeyManager.
+    HkdfPrfKeyManager.register(/* newKeyAllowed= */ true);
+
+    // Register key managers.
+    PrfBasedDeriverKeyManager.register(/* newKeyAllowed= */ true);
+  }
+
+  private KeyDerivationConfig() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationKey.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationKey.java
new file mode 100644
index 0000000..cbe0d02
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationKey.java
@@ -0,0 +1,33 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.Key;
+
+/**
+ * Represents a function to derive a key.
+ *
+ * <p>Tink Key Derivation is given by the primitive which maps a {@code byte[] salt} to a Keyset.
+ * For each key, a {@link KeyDerivationKey} maps a {@code byte[] salt} to a new "derived" {@code
+ * Key}. For a Keyset containing multiple derivation keys, the derived keyset is obtained by mapping
+ * each key according to this map (for the same {@code byte[] salt}), and inserting them into a new
+ * keyset.
+ */
+public abstract class KeyDerivationKey extends Key {
+  @Override
+  public abstract KeyDerivationParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationKeyTemplates.java
new file mode 100644
index 0000000..a398389
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationKeyTemplates.java
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.keyderivation.internal.PrfBasedDeriverKeyManager;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.proto.PrfBasedDeriverKeyFormat;
+import com.google.crypto.tink.proto.PrfBasedDeriverParams;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+
+/**
+ * Generates {@link com.google.crypto.tink.KeyTemplate} for the {@link KeysetDeriver} primitive.
+ *
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
+ * <p>One can use these templates to generate new {@link com.google.crypto.tink.proto.Keyset} with
+ * {@link KeysetHandle#generateNew}. To generate a new keyset that uses the HKDF_SHA256 PRF to
+ * derive a AES256_GCM keyset, one can do:
+ *
+ * <pre>{@code
+ * KeyTemplate template = KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+ *     KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES256_GCM"));
+ * KeysetHandle handle = KeysetHandle.generateNew(template);
+ * KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+ * }</pre>
+ *
+ * @since 1.0.0
+ */
+public final class KeyDerivationKeyTemplates {
+  /**
+   * Returns a {@link KeyTemplate} containing a {@link PrfBasedDeriverKeyFormat}.
+   *
+   * <p>Creates a key template for key derivation that uses a PRF to derive a key that adheres to
+   * {@code derivedKeyTemplate}. The following must be true:
+   *
+   * <ol>
+   *   <li>{@code prfKeyTemplate} is a PRF key template, i.e. {@code
+   *       handle.getPrimitive(StreamingPrf.class)} works.
+   *   <li>{@code derivedKeyTemplate} describes a key type that supports derivation.
+   * </ol>
+   *
+   * <p>The output prefix type of the derived key will match the output prefix type of {@code
+   * derivedKeyTemplate}. This function verifies the newly created key template by creating a
+   * KeysetDeriver primitive from it. This requires both the {@code prfKeyTemplate} and {@code
+   * derivedKeyTemplate} key types to be in the registry. It also attempts to derive a key,
+   * returning an error on failure.
+   */
+  public static KeyTemplate createPrfBasedKeyTemplate(
+      KeyTemplate prfKeyTemplate, KeyTemplate derivedKeyTemplate) throws GeneralSecurityException {
+    PrfBasedDeriverKeyFormat format =
+        PrfBasedDeriverKeyFormat.newBuilder()
+            .setPrfKeyTemplate(toKeyTemplateProto(prfKeyTemplate))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(toKeyTemplateProto(derivedKeyTemplate)))
+            .build();
+    KeyTemplate template =
+        KeyTemplate.create(
+            new PrfBasedDeriverKeyManager().getKeyType(),
+            format.toByteArray(),
+            derivedKeyTemplate.getOutputPrefixType());
+    // Verify {@code template} is derivable.
+    KeysetHandle unused = KeysetHandle.generateNew(template);
+    return template;
+  }
+
+  private static com.google.crypto.tink.proto.KeyTemplate toKeyTemplateProto(
+      KeyTemplate keyTemplate) {
+    return com.google.crypto.tink.proto.KeyTemplate.newBuilder()
+        .setTypeUrl(keyTemplate.getTypeUrl())
+        .setValue(ByteString.copyFrom(keyTemplate.getValue()))
+        .setOutputPrefixType(toOutputPrefixProto(keyTemplate.getOutputPrefixType()))
+        .build();
+  }
+
+  private static OutputPrefixType toOutputPrefixProto(
+      KeyTemplate.OutputPrefixType outputPrefixType) {
+    switch (outputPrefixType) {
+      case TINK:
+        return OutputPrefixType.TINK;
+      case LEGACY:
+        return OutputPrefixType.LEGACY;
+      case RAW:
+        return OutputPrefixType.RAW;
+      case CRUNCHY:
+        return OutputPrefixType.CRUNCHY;
+    }
+    throw new IllegalArgumentException("Unknown output prefix type");
+  }
+
+  private KeyDerivationKeyTemplates() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationParameters.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationParameters.java
new file mode 100644
index 0000000..e68c1f1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeyDerivationParameters.java
@@ -0,0 +1,36 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.Parameters;
+
+/** The public part of a {@link KeyDerivationKey}. */
+public abstract class KeyDerivationParameters extends Parameters {
+  public abstract Parameters getDerivedKeyParameters();
+
+  /**
+   * Whether this key needs a specific ID when it is in a Keyset.
+   *
+   * <p>Tink Keyset derivation always copies the ID of a key to the derived key. Hence,a {@link
+   * KeyDerivationKey} needs a specific ID if and only if the derived key needs a specific ID, and
+   * so this is always equal to {@code getDerivedParameters().hasIdRequirement()}.
+   */
+  @Override
+  public boolean hasIdRequirement() {
+    return getDerivedKeyParameters().hasIdRequirement();
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeysetDeriver.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeysetDeriver.java
new file mode 100644
index 0000000..aa8bc71
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeysetDeriver.java
@@ -0,0 +1,33 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// //////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.KeysetHandle;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+
+/**
+ * KeysetDeriver is the interface used to derive new keysets based on an additional input, the salt.
+ *
+ * <p>The {@code salt} is used to create the keyset using a pseudorandom function. Implementations
+ * must be indistinguishable from ideal KeysetDerivers, which, for every salt, generates a new
+ * random keyset and caches it.
+ */
+@Immutable
+public interface KeysetDeriver {
+  KeysetHandle deriveKeyset(byte[] salt) throws GeneralSecurityException;
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeysetDeriverWrapper.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeysetDeriverWrapper.java
new file mode 100644
index 0000000..74f2e8d
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/KeysetDeriverWrapper.java
@@ -0,0 +1,109 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveWrapper;
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+
+/**
+ * KeysetDeriverWrapper is the implementation of PrimitiveWrapper for the KeysetDeriver primitive.
+ *
+ * <p>The wrapper derives a key from each key in a keyset, and returns the resulting keys as a new
+ * keyset. Each of the derived keys inherits key_id, status, and output_prefix_type from the key
+ * from which it was derived.
+ */
+public class KeysetDeriverWrapper implements PrimitiveWrapper<KeysetDeriver, KeysetDeriver> {
+
+  private static final KeysetDeriverWrapper WRAPPER = new KeysetDeriverWrapper();
+
+  private static void validate(PrimitiveSet<KeysetDeriver> primitiveSet)
+      throws GeneralSecurityException {
+    if (primitiveSet.getPrimary() == null) {
+      throw new GeneralSecurityException("Primitive set has no primary.");
+    }
+  }
+
+  @Immutable
+  private static class WrappedKeysetDeriver implements KeysetDeriver {
+    @SuppressWarnings("Immutable")
+    private final PrimitiveSet<KeysetDeriver> primitiveSet;
+
+    private WrappedKeysetDeriver(PrimitiveSet<KeysetDeriver> primitiveSet) {
+      this.primitiveSet = primitiveSet;
+    }
+
+    private static KeyData deriveAndGetKeyData(byte[] salt, KeysetDeriver deriver)
+        throws GeneralSecurityException {
+      KeysetHandle keysetHandle = deriver.deriveKeyset(salt);
+      Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
+      if (keyset.getKeyCount() != 1) {
+        throw new GeneralSecurityException(
+            "Wrapped Deriver must create a keyset with exactly one KeyData");
+      }
+      return keyset.getKey(0).getKeyData();
+    }
+
+    @Override
+    public KeysetHandle deriveKeyset(byte[] salt) throws GeneralSecurityException {
+      Keyset.Builder builder = Keyset.newBuilder();
+      for (PrimitiveSet.Entry<KeysetDeriver> entry : primitiveSet.getAllInKeysetOrder()) {
+        builder.addKey(
+            Keyset.Key.newBuilder()
+                .setKeyData(deriveAndGetKeyData(salt, entry.getPrimitive()))
+                .setStatus(entry.getStatus())
+                .setOutputPrefixType(entry.getOutputPrefixType())
+                .setKeyId(entry.getKeyId()));
+      }
+      builder.setPrimaryKeyId(primitiveSet.getPrimary().getKeyId());
+      return TinkProtoKeysetFormat.parseKeyset(
+          builder.build().toByteArray(), InsecureSecretKeyAccess.get());
+    }
+  }
+
+  KeysetDeriverWrapper() {}
+
+  @Override
+  public KeysetDeriver wrap(final PrimitiveSet<KeysetDeriver> primitiveSet)
+      throws GeneralSecurityException {
+    validate(primitiveSet);
+    return new WrappedKeysetDeriver(primitiveSet);
+  }
+
+  @Override
+  public Class<KeysetDeriver> getPrimitiveClass() {
+    return KeysetDeriver.class;
+  }
+
+  @Override
+  public Class<KeysetDeriver> getInputPrimitiveClass() {
+    return KeysetDeriver.class;
+  }
+
+  /** Registers this wrapper with Tink, allowing to use the primitive. */
+  public static void register() throws GeneralSecurityException {
+    Registry.registerPrimitiveWrapper(WRAPPER);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationKey.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationKey.java
new file mode 100644
index 0000000..cce4c4b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationKey.java
@@ -0,0 +1,110 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.prf.PrfKey;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a Derivation key which is based on a PRF.
+ *
+ * <p>An object of this class represents the map which 1) uses the given PRF (as specified by {@link
+ * #getPrfKey}) to get sufficient key material from the salt, then 2) creates a key for the
+ * parameters as specified in {@code getParameters().getDerivedKeyParameters()}.
+ */
+public final class PrfBasedKeyDerivationKey extends KeyDerivationKey {
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static PrfBasedKeyDerivationKey create(
+      PrfBasedKeyDerivationParameters parameters, PrfKey prfKey, @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+    if (!parameters.getPrfParameters().equals(prfKey.getParameters())) {
+      throw new GeneralSecurityException(
+          "PrfParameters of passed in PrfBasedKeyDerivationParameters and passed in prfKey"
+              + " parameters object must match. DerivationParameters gave: "
+              + parameters.getPrfParameters()
+              + ", key gives: "
+              + prfKey.getParameters());
+    }
+    if (parameters.getDerivedKeyParameters().hasIdRequirement()) {
+      if (idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Derived key has an ID requirement, but no idRequirement was passed in on creation of"
+                + " this key");
+      }
+    }
+    if (!parameters.getDerivedKeyParameters().hasIdRequirement()) {
+      if (idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Derived key has no ID requirement, but idRequirement was passed in on creation of"
+                + " this key");
+      }
+    }
+    return new PrfBasedKeyDerivationKey(parameters, prfKey, idRequirement);
+  }
+
+  private final PrfBasedKeyDerivationParameters parameters;
+  private final PrfKey prfKey;
+  private final Integer idRequirementOrNull;
+
+  private PrfBasedKeyDerivationKey(
+      PrfBasedKeyDerivationParameters parameters, PrfKey prfKey, @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.prfKey = prfKey;
+    this.idRequirementOrNull = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public PrfKey getPrfKey() {
+    return prfKey;
+  }
+
+  @Override
+  public PrfBasedKeyDerivationParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirementOrNull;
+  }
+
+  @Override
+  public boolean equalsKey(Key other) {
+    if (!(other instanceof PrfBasedKeyDerivationKey)) {
+      return false;
+    }
+
+    PrfBasedKeyDerivationKey otherKey = (PrfBasedKeyDerivationKey) other;
+    return otherKey.getParameters().equals(getParameters())
+        && otherKey.prfKey.equalsKey(prfKey)
+        && Objects.equals(otherKey.idRequirementOrNull, idRequirementOrNull);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationParameters.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationParameters.java
new file mode 100644
index 0000000..e98669b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationParameters.java
@@ -0,0 +1,108 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.prf.PrfParameters;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Represents the parameters needed in a {@link PrfBasedKeyDerivationKey}. */
+@Immutable
+public final class PrfBasedKeyDerivationParameters extends KeyDerivationParameters {
+  /** Builds a new PrfBasedKeyDerivationParameters instance. */
+  public static class Builder {
+    @Nullable private PrfParameters prfParameters = null;
+
+    @Nullable private Parameters derivedKeyParameters = null;
+
+    /** Sets the parameters of the PRF used to create randomness from the salt. */
+    public Builder setPrfParameters(PrfParameters prfParameters) {
+      this.prfParameters = prfParameters;
+      return this;
+    }
+
+    /**
+     * The parameters of the keys which are in the result keyset when the user calls {@code
+     * KeysetDeriver.deriveKeyset()}.
+     */
+    public Builder setDerivedKeyParameters(Parameters derivedKeyParameters) {
+      this.derivedKeyParameters = derivedKeyParameters;
+      return this;
+    }
+
+    public PrfBasedKeyDerivationParameters build() throws GeneralSecurityException {
+      if (prfParameters == null) {
+        throw new GeneralSecurityException("PrfParameters must be set.");
+      }
+      if (derivedKeyParameters == null) {
+        throw new GeneralSecurityException("DerivedKeyParameters must be set.");
+      }
+      return new PrfBasedKeyDerivationParameters(prfParameters, derivedKeyParameters);
+    }
+  }
+
+  private final PrfParameters prfParameters;
+  private final Parameters derivedKeyParameters;
+
+  private PrfBasedKeyDerivationParameters(
+      PrfParameters prfParameters, Parameters derivedKeyParameters) {
+    this.prfParameters = prfParameters;
+    this.derivedKeyParameters = derivedKeyParameters;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** The parameters of the PRF used to create randomness from the salt. */
+  public PrfParameters getPrfParameters() {
+    return prfParameters;
+  }
+
+  /**
+   * The parameters of the keys which are in the result keyset when the user calls {@code
+   * KeysetDeriver.deriveKeyset()}.
+   */
+  @Override
+  public Parameters getDerivedKeyParameters() {
+    return derivedKeyParameters;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof PrfBasedKeyDerivationParameters)) {
+      return false;
+    }
+    PrfBasedKeyDerivationParameters that = (PrfBasedKeyDerivationParameters) o;
+    return that.getPrfParameters().equals(getPrfParameters())
+        && that.getDerivedKeyParameters().equals(getDerivedKeyParameters());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(PrfBasedKeyDerivationParameters.class, prfParameters, derivedKeyParameters);
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "PrfBasedKeyDerivationParameters(%s, %s)", prfParameters, derivedKeyParameters);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/BUILD.bazel
new file mode 100644
index 0000000..4542e26
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/BUILD.bazel
@@ -0,0 +1,71 @@
+load("@build_bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+java_library(
+    name = "prf_based_deriver",
+    srcs = ["PrfBasedDeriver.java"],
+    deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:privileged_registry",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/subtle/prf:streaming_prf",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "prf_based_deriver-android",
+    srcs = ["PrfBasedDeriver.java"],
+    deps = [
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:privileged_registry-android",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver-android",
+        "//src/main/java/com/google/crypto/tink/subtle/prf:streaming_prf-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "prf_based_deriver_key_manager",
+    srcs = ["PrfBasedDeriverKeyManager.java"],
+    deps = [
+        ":prf_based_deriver",
+        "//proto:prf_based_deriver_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/subtle:validators",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "prf_based_deriver_key_manager-android",
+    srcs = ["PrfBasedDeriverKeyManager.java"],
+    deps = [
+        ":prf_based_deriver-android",
+        "//proto:prf_based_deriver_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver-android",
+        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriver.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriver.java
new file mode 100644
index 0000000..31f4a48
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriver.java
@@ -0,0 +1,77 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation.internal;
+
+import static com.google.crypto.tink.internal.Util.UTF_8;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.PrivilegedRegistry;
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.keyderivation.KeysetDeriver;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.prf.StreamingPrf;
+import com.google.errorprone.annotations.Immutable;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+
+/**
+ * An implementation of {@link KeysetDeriver}, which uses a PRF and the Tink registry to derive a
+ * {@link com.google.crypto.tink.KeysetHandle}.
+ */
+@Immutable
+public class PrfBasedDeriver implements KeysetDeriver {
+  private PrfBasedDeriver(KeyData streamingPrfKey, KeyTemplate derivedKeyTemplate) {
+    this.streamingPrfKey = streamingPrfKey;
+    this.derivedKeyTemplate = derivedKeyTemplate;
+  }
+
+  public static PrfBasedDeriver create(KeyData streamingPrfKey, KeyTemplate derivedKeyTemplate)
+      throws GeneralSecurityException {
+    // Validate {@code streamingPrfKey} and {@code derivedKeyTemplate}.
+    StreamingPrf prf = Registry.getPrimitive(streamingPrfKey, StreamingPrf.class);
+    KeyData unused =
+        PrivilegedRegistry.deriveKey(derivedKeyTemplate, prf.computePrf("s".getBytes(UTF_8)));
+
+    return new PrfBasedDeriver(streamingPrfKey, derivedKeyTemplate);
+  }
+
+  private final KeyData streamingPrfKey;
+  private final KeyTemplate derivedKeyTemplate;
+
+  @Override
+  public KeysetHandle deriveKeyset(byte[] salt) throws GeneralSecurityException {
+    StreamingPrf prf = Registry.getPrimitive(streamingPrfKey, StreamingPrf.class);
+    InputStream randomness = prf.computePrf(salt);
+    KeyData keyData = PrivilegedRegistry.deriveKey(derivedKeyTemplate, randomness);
+    Keyset.Key key =
+        Keyset.Key.newBuilder()
+            .setKeyData(keyData)
+            .setStatus(KeyStatusType.UNKNOWN_STATUS)
+            .setKeyId(0)
+            .setOutputPrefixType(OutputPrefixType.UNKNOWN_PREFIX)
+            .build();
+    return TinkProtoKeysetFormat.parseKeyset(
+        Keyset.newBuilder().addKey(key).setPrimaryKeyId(0).build().toByteArray(),
+        InsecureSecretKeyAccess.get());
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverKeyManager.java
new file mode 100644
index 0000000..c06024e
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverKeyManager.java
@@ -0,0 +1,119 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation.internal;
+
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.internal.PrimitiveFactory;
+import com.google.crypto.tink.keyderivation.KeysetDeriver;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.PrfBasedDeriverKey;
+import com.google.crypto.tink.proto.PrfBasedDeriverKeyFormat;
+import com.google.crypto.tink.subtle.Validators;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+
+/** {@link com.google.crypto.tink.internal.KeyTypeManager} for {@link PrfBasedDeriverKey}. */
+public final class PrfBasedDeriverKeyManager extends KeyTypeManager<PrfBasedDeriverKey> {
+  public PrfBasedDeriverKeyManager() {
+    super(
+        PrfBasedDeriverKey.class,
+        new PrimitiveFactory<KeysetDeriver, PrfBasedDeriverKey>(KeysetDeriver.class) {
+          @Override
+          public KeysetDeriver getPrimitive(PrfBasedDeriverKey key)
+              throws GeneralSecurityException {
+            return PrfBasedDeriver.create(key.getPrfKey(), key.getParams().getDerivedKeyTemplate());
+          }
+        });
+  }
+
+  @Override
+  public String getKeyType() {
+    return "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey";
+  }
+
+  @Override
+  public int getVersion() {
+    return 0;
+  }
+
+  @Override
+  public KeyMaterialType keyMaterialType() {
+    return KeyMaterialType.SYMMETRIC;
+  }
+
+  @Override
+  public void validateKey(PrfBasedDeriverKey key) throws GeneralSecurityException {
+    Validators.validateVersion(key.getVersion(), getVersion());
+    if (!key.hasPrfKey()) {
+      throw new GeneralSecurityException("key.prf_key must be set");
+    }
+    if (!key.getParams().hasDerivedKeyTemplate()) {
+      throw new GeneralSecurityException("key.params.derived_key_template must be set");
+    }
+  }
+
+  @Override
+  public PrfBasedDeriverKey parseKey(ByteString byteString) throws InvalidProtocolBufferException {
+    return PrfBasedDeriverKey.parseFrom(byteString, ExtensionRegistryLite.getEmptyRegistry());
+  }
+
+  @Override
+  public KeyFactory<PrfBasedDeriverKeyFormat, PrfBasedDeriverKey> keyFactory() {
+    return new KeyFactory<PrfBasedDeriverKeyFormat, PrfBasedDeriverKey>(
+        PrfBasedDeriverKeyFormat.class) {
+      @Override
+      public void validateKeyFormat(PrfBasedDeriverKeyFormat format)
+          throws GeneralSecurityException {
+        if (!format.hasPrfKeyTemplate()) {
+          throw new GeneralSecurityException("format.params.prf_key_template must be set");
+        }
+        if (!format.getParams().hasDerivedKeyTemplate()) {
+          throw new GeneralSecurityException("format.params.derived_key_template must be set");
+        }
+      }
+
+      @Override
+      public PrfBasedDeriverKeyFormat parseKeyFormat(ByteString byteString)
+          throws InvalidProtocolBufferException {
+        return PrfBasedDeriverKeyFormat.parseFrom(
+            byteString, ExtensionRegistryLite.getEmptyRegistry());
+      }
+
+      @Override
+      public PrfBasedDeriverKey createKey(PrfBasedDeriverKeyFormat format)
+          throws GeneralSecurityException {
+        KeyData prfKey = Registry.newKeyData(format.getPrfKeyTemplate());
+        // Verify {@code format} is derivable.
+        PrfBasedDeriver unused =
+            PrfBasedDeriver.create(prfKey, format.getParams().getDerivedKeyTemplate());
+        return PrfBasedDeriverKey.newBuilder()
+            .setVersion(getVersion())
+            .setParams(format.getParams())
+            .setPrfKey(prfKey)
+            .build();
+      }
+    };
+  }
+
+  public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
+    Registry.registerKeyManager(new PrfBasedDeriverKeyManager(), newKeyAllowed);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKey.java b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKey.java
index fc3443d..534b615 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKey.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKey.java
@@ -20,6 +20,8 @@
 import com.google.crypto.tink.Key;
 import com.google.crypto.tink.util.Bytes;
 import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
 import com.google.errorprone.annotations.RestrictedApi;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
@@ -32,68 +34,102 @@
  * <p>AES-CMAC is specified in RFC 4493. Tink supports AES-CMAC with keys of length 32 bytes (256
  * bits) only.
  */
+@Immutable
 public final class AesCmacKey extends MacKey {
   private final AesCmacParameters parameters;
   private final SecretBytes aesKeyBytes;
+  private final Bytes outputPrefix;
   @Nullable private final Integer idRequirement;
 
+  /**
+   * Builder for AesCmacKey.
+   */
+  public static class Builder {
+    @Nullable private AesCmacParameters parameters = null;
+    @Nullable private SecretBytes aesKeyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(AesCmacParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setAesKeyBytes(SecretBytes aesKeyBytes) throws GeneralSecurityException {
+      this.aesKeyBytes = aesKeyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == AesCmacParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == AesCmacParameters.Variant.LEGACY
+          || parameters.getVariant() == AesCmacParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == AesCmacParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown AesCmacParametersParameters.Variant: " + parameters.getVariant());
+    }
+
+    public AesCmacKey build() throws GeneralSecurityException {
+      if (parameters == null || aesKeyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != aesKeyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new AesCmacKey(parameters, aesKeyBytes, outputPrefix, idRequirement);
+    }
+  }
+
   private AesCmacKey(
-      AesCmacParameters parameters, SecretBytes aesKeyBytes, @Nullable Integer idRequirement) {
+      AesCmacParameters parameters,
+      SecretBytes aesKeyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
     this.parameters = parameters;
     this.aesKeyBytes = aesKeyBytes;
+    this.outputPrefix = outputPrefix;
     this.idRequirement = idRequirement;
   }
 
-  /** Creates a new AES-CMAC key with an empty prefix. */
   @RestrictedApi(
-      explanation = "Accessing parts of keys can produce unexpected incompatibilities",
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
       link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
       allowedOnPath = ".*Test\\.java",
       allowlistAnnotations = {AccessesPartialKey.class})
-  public static AesCmacKey create(AesCmacParameters parameters, SecretBytes aesKey)
-      throws GeneralSecurityException {
-    if (aesKey.size() != 32) {
-      throw new GeneralSecurityException("Invalid key size");
-    }
-    if (parameters.hasIdRequirement()) {
-      throw new GeneralSecurityException(
-          "Must use createForKeyset for parameters with ID requirement");
-    }
-    return new AesCmacKey(parameters, aesKey, null);
-  }
-
-  /**
-   * Creates a new AES-CMAC key for use in a keyset.
-   *
-   * <p>If the format specifies a variant which uses a prefix, the id is used to compute this
-   * prefix.
-   */
-  @RestrictedApi(
-      explanation = "Accessing parts of keys can produce unexpected incompatibilities",
-      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
-      allowedOnPath = ".*Test\\.java",
-      allowlistAnnotations = {AccessesPartialKey.class})
-  public static AesCmacKey createForKeyset(
-      AesCmacParameters parameters, SecretBytes aesKeyBytes, @Nullable Integer idRequirement)
-      throws GeneralSecurityException {
-    if (aesKeyBytes.size() != 32) {
-      throw new GeneralSecurityException("Invalid key size");
-    }
-    if (parameters.hasIdRequirement() && idRequirement == null) {
-      throw new GeneralSecurityException(
-          "Cannot create key without ID requirement with format with ID requirement");
-    }
-    if (!parameters.hasIdRequirement() && idRequirement != null) {
-      throw new GeneralSecurityException(
-          "Cannot create key with ID requirement with format without ID requirement");
-    }
-
-    return new AesCmacKey(parameters, aesKeyBytes, idRequirement);
+  public static Builder builder() {
+    return new Builder();
   }
 
   /** Returns the underlying AES key. */
   @RestrictedApi(
-      explanation = "Accessing parts of keys can produce unexpected incompatibilities",
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
       link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
       allowedOnPath = ".*Test\\.java",
       allowlistAnnotations = {AccessesPartialKey.class})
@@ -103,18 +139,7 @@
 
   @Override
   public Bytes getOutputPrefix() {
-    if (parameters.getVariant() == AesCmacParameters.Variant.NO_PREFIX) {
-      return Bytes.copyFrom(new byte[] {});
-    }
-    if (parameters.getVariant() == AesCmacParameters.Variant.LEGACY
-        || parameters.getVariant() == AesCmacParameters.Variant.CRUNCHY) {
-      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
-    }
-    if (parameters.getVariant() == AesCmacParameters.Variant.TINK) {
-      return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
-    }
-    throw new IllegalStateException(
-        "Unknown AesCmacParameters.Variant: " + parameters.getVariant());
+    return outputPrefix;
   }
 
   @Override
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKeyManager.java
index 5f1e943..3407f90 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacKeyManager.java
@@ -20,7 +20,10 @@
 import com.google.crypto.tink.Mac;
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.internal.MutablePrimitiveRegistry;
+import com.google.crypto.tink.internal.PrimitiveConstructor;
 import com.google.crypto.tink.internal.PrimitiveFactory;
+import com.google.crypto.tink.mac.internal.ChunkedAesCmacImpl;
 import com.google.crypto.tink.proto.AesCmacKey;
 import com.google.crypto.tink.proto.AesCmacKeyFormat;
 import com.google.crypto.tink.proto.AesCmacParams;
@@ -58,6 +61,12 @@
   private static final int KEY_SIZE_IN_BYTES = 32;
   private static final int MIN_TAG_SIZE_IN_BYTES = 10;
   private static final int MAX_TAG_SIZE_IN_BYTES = 16;
+  private static final PrimitiveConstructor<com.google.crypto.tink.mac.AesCmacKey, ChunkedMac>
+      CHUNKED_MAC_PRIMITIVE_CONSTRUCTOR =
+          PrimitiveConstructor.create(
+              ChunkedAesCmacImpl::new,
+              com.google.crypto.tink.mac.AesCmacKey.class,
+              ChunkedMac.class);
 
   @Override
   public String getKeyType() {
@@ -161,6 +170,8 @@
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesCmacKeyManager(), newKeyAllowed);
     AesCmacProtoSerialization.register();
+    MutablePrimitiveRegistry.globalInstance()
+        .registerPrimitiveConstructor(CHUNKED_MAC_PRIMITIVE_CONSTRUCTOR);
   }
 
   /**
@@ -171,10 +182,7 @@
    *       <li>Tag size: 16 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_CMAC")}
    */
-  @Deprecated
   public static final KeyTemplate aes256CmacTemplate() {
     AesCmacKeyFormat format =
         AesCmacKeyFormat.newBuilder()
@@ -195,10 +203,7 @@
    *       <li>Tag size: 16 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW} (no prefix)
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_CMAC_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawAes256CmacTemplate() {
     AesCmacKeyFormat format =
         AesCmacKeyFormat.newBuilder()
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacParameters.java b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacParameters.java
index 194c3c3..0bd8add 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacParameters.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacParameters.java
@@ -16,17 +16,20 @@
 
 package com.google.crypto.tink.mac;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
 import java.util.Objects;
+import javax.annotation.Nullable;
 
 /** Describes the parameters of an {@link AesCmacKey}. */
 public final class AesCmacParameters extends MacParameters {
   /**
    * Describes details of the mac computation.
    *
-   * <p>The usual AES CMAC key is used for variant "NO_PREFIX". Other variants slightly change how
-   * the mac is computed, or add a prefix to every computation depending on the key id.
+   * <p>The standard AES CMAC key is used for variant "NO_PREFIX". Other variants slightly change
+   * how the mac is computed, or add a prefix to every computation depending on the key id.
    */
   @Immutable
   public static final class Variant {
@@ -47,30 +50,74 @@
     }
   }
 
+  /**
+   * Builds a new AesCmacParameters instance.
+   */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    @Nullable private Integer tagSizeBytes = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes != 16 && keySizeBytes != 32) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 128-bit and 256-bit AES keys are supported",
+                keySizeBytes * 8));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setTagSizeBytes(int tagSizeBytes) throws GeneralSecurityException {
+      if (tagSizeBytes < 10 || 16 < tagSizeBytes) {
+        throw new GeneralSecurityException(
+            "Invalid tag size for AesCmacParameters: " + tagSizeBytes);
+      }
+      this.tagSizeBytes = tagSizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public AesCmacParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("key size not set");
+      }
+      if (tagSizeBytes == null) {
+        throw new GeneralSecurityException("tag size not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("variant not set");
+      }
+      return new AesCmacParameters(keySizeBytes, tagSizeBytes, variant);
+    }
+  }
+
+  private final int keySizeBytes;
   private final int tagSizeBytes;
   private final Variant variant;
 
-  private AesCmacParameters(int tagSizeBytes, Variant variant) {
+  private AesCmacParameters(int keySizeBytes, int tagSizeBytes, Variant variant) {
+    this.keySizeBytes = keySizeBytes;
     this.tagSizeBytes = tagSizeBytes;
     this.variant = variant;
   }
 
-  /** Equivalent to {@code createForKeysetWithCryptographicTagSize(tagSize, Variant.NO_PREFIX);} */
-  public static AesCmacParameters create(int tagSize) throws GeneralSecurityException {
-    return createForKeysetWithCryptographicTagSize(tagSize, Variant.NO_PREFIX);
+  public static Builder builder() {
+    return new Builder();
   }
 
-  /**
-   * Creates a new parameters object.
-   *
-   * @throws GeneralSecurityException if tagSizeBytes not in {10, …, 16}.
-   */
-  public static AesCmacParameters createForKeysetWithCryptographicTagSize(
-      int tagSizeBytes, Variant variant) throws GeneralSecurityException {
-    if (tagSizeBytes < 10 || 16 < tagSizeBytes) {
-      throw new GeneralSecurityException("Invalid tag size for AesCmacParameters: " + tagSizeBytes);
-    }
-    return new AesCmacParameters(tagSizeBytes, variant);
+  public int getKeySizeBytes() {
+    return keySizeBytes;
   }
 
   /**
@@ -114,13 +161,14 @@
       return false;
     }
     AesCmacParameters that = (AesCmacParameters) o;
-    return that.getTotalTagSizeBytes() == getTotalTagSizeBytes()
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getTotalTagSizeBytes() == getTotalTagSizeBytes()
         && that.getVariant() == getVariant();
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(tagSizeBytes, variant);
+    return Objects.hash(AesCmacParameters.class, keySizeBytes, tagSizeBytes, variant);
   }
 
   @Override
@@ -130,6 +178,12 @@
 
   @Override
   public String toString() {
-    return "AES-CMAC Parameters (variant: " + variant + ", " + tagSizeBytes + "-byte tags)";
+    return "AES-CMAC Parameters (variant: "
+        + variant
+        + ", "
+        + tagSizeBytes
+        + "-byte tags, and "
+        + keySizeBytes
+        + "-byte key)";
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacProtoSerialization.java
index 477c266..86f1858 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacProtoSerialization.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/AesCmacProtoSerialization.java
@@ -117,7 +117,7 @@
             .setValue(
                 com.google.crypto.tink.proto.AesCmacKeyFormat.newBuilder()
                     .setParams(getProtoParams(parameters))
-                    .setKeySize(32)
+                    .setKeySize(parameters.getKeySizeBytes())
                     .build()
                     .toByteString())
             .setOutputPrefixType(toOutputPrefixType(parameters.getVariant()))
@@ -140,13 +140,6 @@
         key.getIdRequirementOrNull());
   }
 
-  private static AesCmacParameters parseParams(
-      com.google.crypto.tink.proto.AesCmacParams params, OutputPrefixType outputPrefixType)
-      throws GeneralSecurityException {
-    return AesCmacParameters.createForKeysetWithCryptographicTagSize(
-        params.getTagSize(), toVariant(outputPrefixType));
-  }
-
   private static AesCmacParameters parseParameters(ProtoParametersSerialization serialization)
       throws GeneralSecurityException {
     if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
@@ -163,7 +156,9 @@
       throw new GeneralSecurityException("Parsing AesCmacParameters failed: ", e);
     }
 
-    return parseParams(format.getParams(), serialization.getKeyTemplate().getOutputPrefixType());
+    return AesCmacParameters.builder().setKeySizeBytes(format.getKeySize()).setTagSizeBytes(
+        format.getParams().getTagSize()).setVariant(
+        toVariant(serialization.getKeyTemplate().getOutputPrefixType())).build();
   }
 
   @SuppressWarnings("UnusedException")
@@ -182,12 +177,15 @@
         throw new GeneralSecurityException("Only version 0 keys are accepted");
       }
       AesCmacParameters parameters =
-          parseParams(protoKey.getParams(), serialization.getOutputPrefixType());
-      return AesCmacKey.createForKeyset(
-          parameters,
-          SecretBytes.copyFrom(
-              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)),
-          serialization.getIdRequirementOrNull());
+          AesCmacParameters.builder().setKeySizeBytes(protoKey.getKeyValue().size())
+              .setTagSizeBytes(protoKey.getParams().getTagSize())
+              .setVariant(toVariant(serialization.getOutputPrefixType())).build();
+      return AesCmacKey.builder()
+          .setParameters(parameters)
+          .setAesKeyBytes(SecretBytes.copyFrom(
+                protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
     } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
       throw new GeneralSecurityException("Parsing AesCmacKey failed");
     }
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/mac/BUILD.bazel
index cf34544..4189716 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/BUILD.bazel
@@ -8,6 +8,9 @@
     name = "hmac_key_manager",
     srcs = ["HmacKeyManager.java"],
     deps = [
+        ":chunked_mac",
+        ":hmac_key",
+        ":hmac_proto_serialization",
         "//proto:common_java_proto",
         "//proto:hmac_java_proto",
         "//proto:tink_java_proto",
@@ -16,12 +19,15 @@
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_impl",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -29,6 +35,9 @@
     name = "hmac_key_manager-android",
     srcs = ["HmacKeyManager.java"],
     deps = [
+        ":chunked_mac-android",
+        ":hmac_key-android",
+        ":hmac_proto_serialization-android",
         "//proto:common_java_proto_lite",
         "//proto:hmac_java_proto_lite",
         "//proto:tink_java_proto_lite",
@@ -37,12 +46,15 @@
         "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_impl-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -52,7 +64,6 @@
     deps = [
         ":mac_wrapper",
         "//src/main/java/com/google/crypto/tink:mac",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -63,7 +74,6 @@
     deps = [
         ":mac_wrapper-android",
         "//src/main/java/com/google/crypto/tink:mac-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -139,6 +149,7 @@
     srcs = ["MacConfig.java"],
     deps = [
         ":aes_cmac_key_manager",
+        ":chunked_mac_wrapper",
         ":hmac_key_manager",
         ":mac_wrapper",
         "//proto:config_java_proto",
@@ -151,6 +162,7 @@
     srcs = ["MacConfig.java"],
     deps = [
         ":aes_cmac_key_manager-android",
+        ":chunked_mac_wrapper-android",
         ":hmac_key_manager-android",
         ":mac_wrapper-android",
         "//proto:config_java_proto_lite",
@@ -162,19 +174,24 @@
     name = "aes_cmac_key_manager",
     srcs = ["AesCmacKeyManager.java"],
     deps = [
+        ":aes_cmac_key",
         ":aes_cmac_proto_serialization",
+        ":chunked_mac",
         "//proto:aes_cmac_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_impl",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -182,19 +199,24 @@
     name = "aes_cmac_key_manager-android",
     srcs = ["AesCmacKeyManager.java"],
     deps = [
+        ":aes_cmac_key-android",
         ":aes_cmac_proto_serialization-android",
+        ":chunked_mac-android",
         "//proto:aes_cmac_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:key_template-android",
         "//src/main/java/com/google/crypto/tink:mac-android",
         "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor-android",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_impl-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac-android",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -271,6 +293,7 @@
     srcs = ["AesCmacParameters.java"],
     deps = [
         ":mac_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
@@ -280,6 +303,29 @@
     srcs = ["AesCmacParameters.java"],
     deps = [
         ":mac_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hmac_parameters",
+    srcs = ["HmacParameters.java"],
+    deps = [
+        ":mac_parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hmac_parameters-android",
+    srcs = ["HmacParameters.java"],
+    deps = [
+        ":mac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
@@ -344,8 +390,8 @@
         "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/util:bytes",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -369,7 +415,141 @@
         "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "hmac_key",
+    srcs = ["HmacKey.java"],
+    deps = [
+        ":hmac_parameters",
+        ":mac_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hmac_key-android",
+    srcs = ["HmacKey.java"],
+    deps = [
+        ":hmac_parameters-android",
+        ":mac_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hmac_proto_serialization",
+    srcs = ["HmacProtoSerialization.java"],
+    deps = [
+        ":hmac_key",
+        ":hmac_parameters",
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "hmac_proto_serialization-android",
+    srcs = ["HmacProtoSerialization.java"],
+    deps = [
+        ":hmac_key-android",
+        ":hmac_parameters-android",
+        "//proto:common_java_proto_lite",
+        "//proto:hmac_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "chunked_mac_wrapper",
+    srcs = ["ChunkedMacWrapper.java"],
+    deps = [
+        ":chunked_mac",
+        ":chunked_mac_computation",
+        ":chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink:crypto_format",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "chunked_mac_wrapper-android",
+    srcs = ["ChunkedMacWrapper.java"],
+    deps = [
+        ":chunked_mac-android",
+        ":chunked_mac_computation-android",
+        ":chunked_mac_verification-android",
+        "//src/main/java/com/google/crypto/tink:crypto_format-android",
+        "//src/main/java/com/google/crypto/tink:primitive_set-android",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "predefined_mac_parameters-android",
+    srcs = ["PredefinedMacParameters.java"],
+    deps = [
+        ":aes_cmac_parameters-android",
+        ":hmac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+    ],
+)
+
+java_library(
+    name = "predefined_mac_parameters",
+    srcs = ["PredefinedMacParameters.java"],
+    deps = [
+        ":aes_cmac_parameters",
+        ":hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
     ],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/ChunkedMacWrapper.java b/java_src/src/main/java/com/google/crypto/tink/mac/ChunkedMacWrapper.java
new file mode 100644
index 0000000..9d54fde
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/ChunkedMacWrapper.java
@@ -0,0 +1,158 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import com.google.crypto.tink.CryptoFormat;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveSet.Entry;
+import com.google.crypto.tink.PrimitiveWrapper;
+import com.google.crypto.tink.Registry;
+import com.google.errorprone.annotations.Immutable;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * ChunkedMacWrapper is the implementation of PrimitiveWrapper for the ChunkedMac primitive.
+ *
+ * <p>The returned primitive works with a keyset (rather than a single key). To compute a MAC tag,
+ * it uses the primary key in the keyset, and prepends to the tag a certain prefix associated with
+ * the primary key. To verify a tag, the primitive uses the prefix of the tag to efficiently select
+ * the right key in the set. If the keys associated with the prefix do not validate the tag, the
+ * primitive tries all keys with {@link com.google.crypto.tink.proto.OutputPrefixType#RAW}.
+ */
+public class ChunkedMacWrapper implements PrimitiveWrapper<ChunkedMac, ChunkedMac> {
+
+  private static final ChunkedMacWrapper WRAPPER = new ChunkedMacWrapper();
+
+  private static class WrappedChunkedMacVerification implements ChunkedMacVerification {
+    private final List<ChunkedMacVerification> verifications;
+
+    private WrappedChunkedMacVerification(List<ChunkedMacVerification> verificationEntries) {
+      this.verifications = verificationEntries;
+    }
+
+    @Override
+    public void update(ByteBuffer data) throws GeneralSecurityException {
+      // We will need to be `reset()`ting this buffer due to potentially multiple reads from the
+      // same data span in order to be consistent with the behaviour of ChunkedMacComputation
+      // wrapper. That is, after the execution, user's buffer's `mark` is left unchanged, and its
+      // `position` is equal to `limit` after we finished reading from the buffer. In order to
+      // achieve that we `duplicate()` the given `data` buffer here and set `mark()`s on the cloned
+      // buffer (note that the `duplicate()` method does not copy the underlying data).
+      ByteBuffer clonedData = data.duplicate();
+      clonedData.mark();
+      for (ChunkedMacVerification entry : verifications) {
+        clonedData.reset();
+        entry.update(clonedData);
+      }
+      data.position(data.limit());
+    }
+
+    @Override
+    public void verifyMac() throws GeneralSecurityException {
+      GeneralSecurityException errorSink =
+          new GeneralSecurityException("MAC verification failed for all suitable keys in keyset");
+      for (ChunkedMacVerification entry : verifications) {
+        try {
+          entry.verifyMac();
+          // If there is no exception, the MAC is valid and we can return.
+          return;
+        } catch (GeneralSecurityException e) {
+          // Ignored as we want to continue verification with the remaining keys.
+          errorSink.addSuppressed(e);
+        }
+      }
+      // nothing works.
+      throw errorSink;
+    }
+  }
+
+  @Immutable
+  private static class WrappedChunkedMac implements ChunkedMac {
+    @SuppressWarnings("Immutable") // We never change the primitives set.
+    private final PrimitiveSet<ChunkedMac> primitives;
+
+    private WrappedChunkedMac(PrimitiveSet<ChunkedMac> primitives) {
+      this.primitives = primitives;
+    }
+
+    @Override
+    public ChunkedMacComputation createComputation() throws GeneralSecurityException {
+      return getChunkedMac(primitives.getPrimary()).createComputation();
+    }
+
+    private ChunkedMac getChunkedMac(Entry<ChunkedMac> entry) {
+      return entry.getFullPrimitive();
+    }
+
+    @Override
+    public ChunkedMacVerification createVerification(final byte[] tag)
+        throws GeneralSecurityException {
+      byte[] prefix = Arrays.copyOf(tag, CryptoFormat.NON_RAW_PREFIX_SIZE);
+
+      // First add verifications with prefixed keys.
+      List<ChunkedMacVerification> verifications = new ArrayList<>();
+      for (PrimitiveSet.Entry<ChunkedMac> primitive : primitives.getPrimitive(prefix)) {
+        verifications.add(getChunkedMac(primitive).createVerification(tag));
+      }
+      // Also add verifications with non-prefixed keys.
+      for (PrimitiveSet.Entry<ChunkedMac> primitive : primitives.getRawPrimitives()) {
+        verifications.add(getChunkedMac(primitive).createVerification(tag));
+      }
+
+      return new WrappedChunkedMacVerification(verifications);
+    }
+  }
+
+  private ChunkedMacWrapper() {}
+
+  @Override
+  public ChunkedMac wrap(final PrimitiveSet<ChunkedMac> primitives)
+      throws GeneralSecurityException {
+    if (primitives == null) {
+      throw new GeneralSecurityException("primitive set must be non-null");
+    }
+    if (primitives.getPrimary() == null) {
+      throw new GeneralSecurityException("no primary in primitive set");
+    }
+    for (List<PrimitiveSet.Entry<ChunkedMac>> list : primitives.getAll()) {
+      for (PrimitiveSet.Entry<ChunkedMac> entry : list) {
+        // Ensure that all entries in the primitive set are present and valid (i.e. have
+        // `fullPrimitive` field set). Throws unchecked exceptions if it's not the case.
+        ChunkedMac unused = entry.getFullPrimitive();
+      }
+    }
+    return new WrappedChunkedMac(primitives);
+  }
+
+  @Override
+  public Class<ChunkedMac> getPrimitiveClass() {
+    return ChunkedMac.class;
+  }
+
+  @Override
+  public Class<ChunkedMac> getInputPrimitiveClass() {
+    return ChunkedMac.class;
+  }
+
+  static void register() throws GeneralSecurityException {
+    Registry.registerPrimitiveWrapper(WRAPPER);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/HmacKey.java b/java_src/src/main/java/com/google/crypto/tink/mac/HmacKey.java
new file mode 100644
index 0000000..585ac23
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/HmacKey.java
@@ -0,0 +1,166 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a key computing HMAC.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class HmacKey extends MacKey {
+  private final HmacParameters parameters;
+  private final SecretBytes keyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for HmacKey. */
+  public static class Builder {
+    @Nullable private HmacParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(HmacParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == HmacParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == HmacParameters.Variant.LEGACY
+          || parameters.getVariant() == HmacParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == HmacParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException("Unknown HmacParameters.Variant: " + parameters.getVariant());
+    }
+
+    public HmacKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new HmacKey(parameters, keyBytes, outputPrefix, idRequirement);
+    }
+  }
+
+  private HmacKey(
+      HmacParameters parameters,
+      SecretBytes keyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public HmacParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof HmacKey)) {
+      return false;
+    }
+    HmacKey that = (HmacKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.keyBytes.equalsSecretBytes(keyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/HmacKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/mac/HmacKeyManager.java
index 988dda7..b439a40 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/HmacKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/HmacKeyManager.java
@@ -21,7 +21,10 @@
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
 import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.internal.MutablePrimitiveRegistry;
+import com.google.crypto.tink.internal.PrimitiveConstructor;
 import com.google.crypto.tink.internal.PrimitiveFactory;
+import com.google.crypto.tink.mac.internal.ChunkedHmacImpl;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.HmacKey;
 import com.google.crypto.tink.proto.HmacKeyFormat;
@@ -81,6 +84,13 @@
   /** Minimum tag size in bytes. This provides minimum 80-bit security strength. */
   private static final int MIN_TAG_SIZE_IN_BYTES = 10;
 
+  private static final PrimitiveConstructor<com.google.crypto.tink.mac.HmacKey, ChunkedMac>
+      CHUNKED_MAC_PRIMITIVE_CONSTRUCTOR =
+          PrimitiveConstructor.create(
+              ChunkedHmacImpl::new,
+              com.google.crypto.tink.mac.HmacKey.class,
+              ChunkedMac.class);
+
   @Override
   public String getKeyType() {
     return "type.googleapis.com/google.crypto.tink.HmacKey";
@@ -177,10 +187,7 @@
         Validators.validateVersion(format.getVersion(), getVersion());
         byte[] pseudorandomness = new byte[format.getKeySize()];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != format.getKeySize()) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return HmacKey.newBuilder()
               .setVersion(getVersion())
               .setParams(format.getParams())
@@ -232,6 +239,9 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new HmacKeyManager(), newKeyAllowed);
+    HmacProtoSerialization.register();
+    MutablePrimitiveRegistry.globalInstance()
+        .registerPrimitiveConstructor(CHUNKED_MAC_PRIMITIVE_CONSTRUCTOR);
   }
 
   /**
@@ -243,10 +253,7 @@
    *       <li>Hash function: SHA256
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HMAC_SHA256_128BITTAG")}
    */
-  @Deprecated
   public static final KeyTemplate hmacSha256HalfDigestTemplate() {
     return createTemplate(32, 16, HashType.SHA256);
   }
@@ -260,10 +267,7 @@
    *       <li>Hash function: SHA256
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HMAC_SHA256_256BITTAG")}
    */
-  @Deprecated
   public static final KeyTemplate hmacSha256Template() {
     return createTemplate(32, 32, HashType.SHA256);
   }
@@ -277,10 +281,7 @@
    *       <li>Hash function: SHA512
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HMAC_SHA512_256BITTAG")}
    */
-  @Deprecated
   public static final KeyTemplate hmacSha512HalfDigestTemplate() {
     return createTemplate(64, 32, HashType.SHA512);
   }
@@ -294,10 +295,7 @@
    *       <li>Hash function: SHA512
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HMAC_SHA512_512BITTAG")}
    */
-  @Deprecated
   public static final KeyTemplate hmacSha512Template() {
     return createTemplate(64, 64, HashType.SHA512);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/HmacParameters.java b/java_src/src/main/java/com/google/crypto/tink/mac/HmacParameters.java
new file mode 100644
index 0000000..582030c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/HmacParameters.java
@@ -0,0 +1,283 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Describes the parameters of an {@link HmacKey}.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+public final class HmacParameters extends MacParameters {
+  /**
+   * Describes details of the mac computation.
+   *
+   * <p>The standard HMAC key is used for variant "NO_PREFIX". Other variants slightly change how
+   * the mac is computed, or add a prefix to every computation depending on the key id.
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant LEGACY = new Variant("LEGACY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The Hash algorithm used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA1 = new HashType("SHA1");
+    public static final HashType SHA224 = new HashType("SHA224");
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builds a new HmacParameters instance. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    @Nullable private Integer tagSizeBytes = null;
+    private HashType hashType = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setTagSizeBytes(int tagSizeBytes) throws GeneralSecurityException {
+      this.tagSizeBytes = tagSizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHashType(HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    private static void validateTagSizeBytes(int tagSizeBytes, HashType hashType)
+        throws GeneralSecurityException {
+      if (tagSizeBytes < 10) {
+        throw new GeneralSecurityException(
+            String.format("Invalid tag size in bytes %d; must be at least 10 bytes", tagSizeBytes));
+      }
+      if (hashType == HashType.SHA1) {
+        if (tagSizeBytes > 20) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 20 bytes for SHA1", tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA224) {
+        if (tagSizeBytes > 28) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 28 bytes for SHA224",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA256) {
+        if (tagSizeBytes > 32) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 32 bytes for SHA256",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA384) {
+        if (tagSizeBytes > 48) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 48 bytes for SHA384",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      if (hashType == HashType.SHA512) {
+        if (tagSizeBytes > 64) {
+          throw new GeneralSecurityException(
+              String.format(
+                  "Invalid tag size in bytes %d; can be at most 64 bytes for SHA512",
+                  tagSizeBytes));
+        }
+        return;
+      }
+      throw new GeneralSecurityException("unknown hash type; must be SHA256, SHA384 or SHA512");
+    }
+
+    public HmacParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("key size is not set");
+      }
+      if (tagSizeBytes == null) {
+        throw new GeneralSecurityException("tag size is not set");
+      }
+      if (hashType == null) {
+        throw new GeneralSecurityException("hash type is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("variant is not set");
+      }
+      if (keySizeBytes < 16) {
+        throw new InvalidAlgorithmParameterException(
+            String.format("Invalid key size in bytes %d; must be at least 16 bytes", keySizeBytes));
+      }
+      validateTagSizeBytes(tagSizeBytes, hashType);
+      return new HmacParameters(keySizeBytes, tagSizeBytes, variant, hashType);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final int tagSizeBytes;
+  private final Variant variant;
+  private final HashType hashType;
+
+  private HmacParameters(int keySizeBytes, int tagSizeBytes, Variant variant, HashType hashType) {
+    this.keySizeBytes = keySizeBytes;
+    this.tagSizeBytes = tagSizeBytes;
+    this.variant = variant;
+    this.hashType = hashType;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  /**
+   * Returns the size of the tag which is computed cryptographically from the message.
+   *
+   * <p>This may differ from the total size of the tag, as for some keys, Tink prefixes the tag with
+   * a key dependent output prefix.
+   */
+  public int getCryptographicTagSizeBytes() {
+    return tagSizeBytes;
+  }
+
+  /**
+   * Returns the size of the security relevant tag plus the size of the prefix with which this key
+   * prefixes every tag.
+   */
+  public int getTotalTagSizeBytes() {
+    if (variant == Variant.NO_PREFIX) {
+      return getCryptographicTagSizeBytes();
+    }
+    if (variant == Variant.TINK) {
+      return getCryptographicTagSizeBytes() + 5;
+    }
+    if (variant == Variant.CRUNCHY) {
+      return getCryptographicTagSizeBytes() + 5;
+    }
+    if (variant == Variant.LEGACY) {
+      return getCryptographicTagSizeBytes() + 5;
+    }
+    throw new IllegalStateException("Unknown variant");
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  /** Returns a hash type object. */
+  public HashType getHashType() {
+    return hashType;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof HmacParameters)) {
+      return false;
+    }
+    HmacParameters that = (HmacParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getTotalTagSizeBytes() == getTotalTagSizeBytes()
+        && that.getVariant() == getVariant()
+        && that.getHashType() == getHashType();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(HmacParameters.class, keySizeBytes, tagSizeBytes, variant, hashType);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "HMAC Parameters (variant: "
+        + variant
+        + ", hashType: "
+        + hashType
+        + ", "
+        + tagSizeBytes
+        + "-byte tags, and "
+        + keySizeBytes
+        + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/HmacProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/mac/HmacProtoSerialization.java
new file mode 100644
index 0000000..32303a4
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/HmacProtoSerialization.java
@@ -0,0 +1,256 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/** Methods to serialize and parse {@link HmacKey} objects and {@link HmacParameters} objects. */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class HmacProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.HmacKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<HmacParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              HmacProtoSerialization::serializeParameters,
+              HmacParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          HmacProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<HmacKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          HmacProtoSerialization::serializeKey, HmacKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          HmacProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(HmacParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (HmacParameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (HmacParameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (HmacParameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    if (HmacParameters.Variant.LEGACY.equals(variant)) {
+      return OutputPrefixType.LEGACY;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static HashType toProtoHashType(HmacParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (HmacParameters.HashType.SHA1.equals(hashType)) {
+      return HashType.SHA1;
+    }
+    if (HmacParameters.HashType.SHA224.equals(hashType)) {
+      return HashType.SHA224;
+    }
+    if (HmacParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (HmacParameters.HashType.SHA384.equals(hashType)) {
+      return HashType.SHA384;
+    }
+    if (HmacParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static HmacParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA1:
+        return HmacParameters.HashType.SHA1;
+      case SHA224:
+        return HmacParameters.HashType.SHA224;
+      case SHA256:
+        return HmacParameters.HashType.SHA256;
+      case SHA384:
+        return HmacParameters.HashType.SHA384;
+      case SHA512:
+        return HmacParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static HmacParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return HmacParameters.Variant.TINK;
+      case CRUNCHY:
+        return HmacParameters.Variant.CRUNCHY;
+      case LEGACY:
+        return HmacParameters.Variant.LEGACY;
+      case RAW:
+        return HmacParameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.HmacParams getProtoParams(HmacParameters parameters)
+      throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.HmacParams.newBuilder()
+        .setTagSize(parameters.getCryptographicTagSizeBytes())
+        .setHash(toProtoHashType(parameters.getHashType()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(HmacParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(HmacKey key, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setParams(getProtoParams(key.getParameters()))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static HmacParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HmacProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.HmacKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.HmacKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HmacParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException(
+          "Parsing HmacParameters failed: unknown Version " + format.getVersion());
+    }
+    return HmacParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        .setTagSizeBytes(format.getParams().getTagSize())
+        .setHashType(toHashType(format.getParams().getHash()))
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static HmacKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HmacProtoSerialization.parseKey");
+    }
+    try {
+      com.google.crypto.tink.proto.HmacKey protoKey =
+          com.google.crypto.tink.proto.HmacKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      HmacParameters parameters =
+          HmacParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setTagSizeBytes(protoKey.getParams().getTagSize())
+              .setHashType(toHashType(protoKey.getParams().getHash()))
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return HmacKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing HmacKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private HmacProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/MacConfig.java b/java_src/src/main/java/com/google/crypto/tink/mac/MacConfig.java
index afa5a47..b4caaae 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/MacConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/MacConfig.java
@@ -39,20 +39,25 @@
 public final class MacConfig {
   public static final String HMAC_TYPE_URL = new HmacKeyManager().getKeyType();
 
-  /** @deprecated use {@link #register} */
-  @Deprecated public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
+  /**
+   * @deprecated use {@link #register}
+   */
+  @Deprecated
+  public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
 
   /**
    * @deprecated use {@link #register}
    * @since 1.1.0
    */
-  @Deprecated public static final RegistryConfig TINK_1_1_0 = TINK_1_0_0;
+  @Deprecated
+  public static final RegistryConfig TINK_1_1_0 = TINK_1_0_0;
 
   /**
    * @deprecated use {@link #register}
    * @since 1.2.0
    */
-  @Deprecated public static final RegistryConfig LATEST = TINK_1_0_0;
+  @Deprecated
+  public static final RegistryConfig LATEST = TINK_1_0_0;
 
   static {
     try {
@@ -83,6 +88,7 @@
    */
   public static void register() throws GeneralSecurityException {
     MacWrapper.register();
+    ChunkedMacWrapper.register();
     HmacKeyManager.register(true);
 
     if (TinkFips.useOnlyFips()) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/MacFactory.java b/java_src/src/main/java/com/google/crypto/tink/mac/MacFactory.java
index 9361446..8d4dfc3 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/MacFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/MacFactory.java
@@ -18,7 +18,6 @@
 
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.Mac;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -46,8 +45,9 @@
    *     MacWrapper} instead.
    */
   @Deprecated
-  public static Mac getPrimitive(KeysetHandle keysetHandle) throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new MacWrapper());
+  public static Mac getPrimitive(KeysetHandle keysetHandle)
+      throws GeneralSecurityException {
+    MacWrapper.register();
     return keysetHandle.getPrimitive(Mac.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/MacKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/mac/MacKeyTemplates.java
index d397a11..2838982 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/MacKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/MacKeyTemplates.java
@@ -27,6 +27,21 @@
 /**
  * Pre-generated {@link KeyTemplate} for {@link com.google.crypto.tink.Mac}.
  *
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
  * <p>One can use these templates to generate new {@link com.google.crypto.tink.proto.Keyset} with
  * {@link com.google.crypto.tink.KeysetHandle}. To generate a new keyset that contains a single
  * {@link com.google.crypto.tink.proto.HmacKey}, one can do:
@@ -38,8 +53,7 @@
  * }</pre>
  *
  * @since 1.0.0
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("HMAC_SHA256_128BITTAG")
+ * @deprecated Use PredefinedMacParameters instead.
  */
 @Deprecated
 public final class MacKeyTemplates {
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/MacWrapper.java b/java_src/src/main/java/com/google/crypto/tink/mac/MacWrapper.java
index 62718cc..f0ab5bb 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/MacWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/MacWrapper.java
@@ -45,6 +45,7 @@
   private static final Logger logger = Logger.getLogger(MacWrapper.class.getName());
 
   private static final byte[] FORMAT_VERSION = new byte[] {0};
+  private static final MacWrapper WRAPPER = new MacWrapper();
 
   private static class WrappedMac implements Mac {
     private final PrimitiveSet<Mac> primitives;
@@ -169,6 +170,6 @@
   }
 
  public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new MacWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/PredefinedMacParameters.java b/java_src/src/main/java/com/google/crypto/tink/mac/PredefinedMacParameters.java
new file mode 100644
index 0000000..317471c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/PredefinedMacParameters.java
@@ -0,0 +1,131 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+/**
+ * Pre-defined {@link Parameter} objects for {@link com.google.crypto.tink.Mac}.
+ *
+ * <p>Note: if you want to keep dependencies small, consider inlining the constants here.
+ */
+public final class PredefinedMacParameters {
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HmacKey} with the following
+   * parameters:
+   *
+   * <ul>
+   *   <li>Key size: 32 bytes
+   *   <li>Tag size: 16 bytes
+   *   <li>Hash function: SHA256
+   *   <li>OutputPrefixType: TINK
+   * </ul>
+   */
+  public static final HmacParameters HMAC_SHA256_128BITTAG =
+      exceptionIsBug(
+          () ->
+              HmacParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setTagSizeBytes(16)
+                  .setVariant(HmacParameters.Variant.TINK)
+                  .setHashType(HmacParameters.HashType.SHA256)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HmacKey} with the following
+   * parameters:
+   *
+   * <ul>
+   *   <li>Key size: 32 bytes
+   *   <li>Tag size: 32 bytes
+   *   <li>Hash function: SHA256
+   *   <li>OutputPrefixType: TINK
+   * </ul>
+   */
+  public static final HmacParameters HMAC_SHA256_256BITTAG =
+      exceptionIsBug(
+          () ->
+              HmacParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setTagSizeBytes(32)
+                  .setVariant(HmacParameters.Variant.TINK)
+                  .setHashType(HmacParameters.HashType.SHA256)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HmacKey} with the following
+   * parameters:
+   *
+   * <ul>
+   *   <li>Key size: 64 bytes
+   *   <li>Tag size: 32 bytes
+   *   <li>Hash function: SHA512
+   *   <li>OutputPrefixType: TINK
+   * </ul>
+   */
+  public static final HmacParameters HMAC_SHA512_256BITTAG =
+      exceptionIsBug(
+          () ->
+              HmacParameters.builder()
+                  .setKeySizeBytes(64)
+                  .setTagSizeBytes(32)
+                  .setVariant(HmacParameters.Variant.TINK)
+                  .setHashType(HmacParameters.HashType.SHA512)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HmacKey} with the following
+   * parameters:
+   *
+   * <ul>
+   *   <li>Key size: 64 bytes
+   *   <li>Tag size: 64 bytes
+   *   <li>Hash function: SHA512
+   *   <li>OutputPrefixType: TINK
+   * </ul>
+   */
+  public static final HmacParameters HMAC_SHA512_512BITTAG =
+      exceptionIsBug(
+          () ->
+              HmacParameters.builder()
+                  .setKeySizeBytes(64)
+                  .setTagSizeBytes(64)
+                  .setVariant(HmacParameters.Variant.TINK)
+                  .setHashType(HmacParameters.HashType.SHA512)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link CmacKey} with the following
+   * parameters:
+   *
+   * <ul>
+   *   <li>Key size: 32 bytes
+   *   <li>Tag size: 16 bytes
+   *   <li>OutputPrefixType: TINK
+   * </ul>
+   */
+  public static final AesCmacParameters AES_CMAC =
+      exceptionIsBug(
+          () ->
+              AesCmacParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setTagSizeBytes(16)
+                  .setVariant(AesCmacParameters.Variant.TINK)
+                  .build());
+
+  private PredefinedMacParameters() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/AesCmacTestUtil.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/AesCmacTestUtil.java
new file mode 100644
index 0000000..a25e4ad
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/AesCmacTestUtil.java
@@ -0,0 +1,238 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.TinkBugException;
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.AesCmacParameters;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Test vector class, reusable test vectors, and convenience functions for testing AesCmac
+ * implementations.
+ */
+@AccessesPartialKey
+public final class AesCmacTestUtil {
+
+  public static final AesCmacTestVector RFC_TEST_VECTOR_0 =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "2b7e151628aed2a6abf7158809cf4f3c",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "",
+          "bb1d6929e95937287fa37d129b756746");
+  public static final AesCmacTestVector RFC_TEST_VECTOR_1 =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "2b7e151628aed2a6abf7158809cf4f3c",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "6bc1bee22e409f96e93d7e117393172a"
+              + "ae2d8a571e03ac9c9eb76fac45af8e51"
+              + "30c81c46a35ce411",
+          "dfa66747de9ae63030ca32611497c827");
+  public static final AesCmacTestVector RFC_TEST_VECTOR_2 =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "2b7e151628aed2a6abf7158809cf4f3c",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "6bc1bee22e409f96e93d7e117393172a"
+              + "ae2d8a571e03ac9c9eb76fac45af8e51"
+              + "30c81c46a35ce411e5fbc1191a0a52ef"
+              + "f69f2445df4f9b17ad2b417be66c3710",
+          "51f0bebf7e3b9d92fc49741779363cfe");
+
+  public static final AesCmacTestVector NOT_OVERFLOWING_INTERNAL_STATE =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "aaaaaa",
+          "97268151a23fcd035a2dd0573d84e6ba");
+  public static final AesCmacTestVector FILL_UP_EXACTLY_INTERNAL_STATE =
+      new AesCmacTestVector(
+          // fill up exactly the internal state once
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+          "70e4648706483f8c5e8e2fab7b190c08");
+  public static final AesCmacTestVector FILL_UP_EXACTLY_INTERNAL_STATE_TWICE =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+              + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+          "219db2ebac5416dc2b0d8afcb666fb7a");
+  public static final AesCmacTestVector OVERFLOW_INTERNAL_STATE_ONCE =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "0336c9c4bf8f1bc219b017292af24358");
+  public static final AesCmacTestVector OVERFLOW_INTERNAL_STATE_TWICE =
+      new AesCmacTestVector(
+          // overflow the internal state twice
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.NO_PREFIX),
+              null),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "611a1ededd3dfff548ed80b7fd10c0ba");
+  public static final AesCmacTestVector SHORTER_TAG =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 15, Variant.NO_PREFIX),
+              null),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "611a1ededd3dfff548ed80b7fd10c0");
+  public static final AesCmacTestVector TAG_WITH_KEY_PREFIX_TYPE_LEGACY =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.LEGACY),
+              1877),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "00000007554816512e20d15db74f1de942d86a2f7b");
+  public static final AesCmacTestVector TAG_WITH_KEY_PREFIX_TYPE_TINK =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.TINK),
+              1877),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "0100000755611a1ededd3dfff548ed80b7fd10c0ba");
+  public static final AesCmacTestVector LONG_KEY_TEST_VECTOR =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
+              createAesCmacParameters(32, 16, Variant.NO_PREFIX),
+              null),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "139fce15a6f4a281ad22458d3d3cac26");
+
+  public static final AesCmacTestVector WRONG_PREFIX_TAG_LEGACY =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.LEGACY),
+              1877),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "0000611a1ededd3dfff548ed80b7fd10c0ba");
+  public static final AesCmacTestVector WRONG_PREFIX_TAG_TINK =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.TINK),
+              1877),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "0100000745611a1ededd3dfff548ed80b7fd10c0ba");
+  public static final AesCmacTestVector TAG_TOO_SHORT =
+      new AesCmacTestVector(
+          createAesCmacKey(
+              "00112233445566778899aabbccddeeff",
+              createAesCmacParameters(16, 16, Variant.TINK),
+              1877),
+          "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+              + "bbbbbb",
+          "c0ba");
+
+  /**
+   * Creates an {@link AesCmacKey} from the provided {@code keyMaterial}, {@code parameters}, and
+   * {@code idRequirement}. This is a convenience method that makes the tests code more concise and
+   * readable.
+   */
+  public static AesCmacKey createAesCmacKey(
+      String keyMaterial, AesCmacParameters parameters, @Nullable Integer idRequirement) {
+    try {
+      return AesCmacKey.builder()
+          .setAesKeyBytes(
+              SecretBytes.copyFrom(Hex.decode(keyMaterial), InsecureSecretKeyAccess.get()))
+          .setParameters(parameters)
+          .setIdRequirement(idRequirement)
+          .build();
+    } catch (GeneralSecurityException ex) {
+      throw new TinkBugException(ex);
+    }
+  }
+
+  /**
+   * Creates an {@link AesCmacParameters} object from the provided {@code keySizeBytes},
+   * {@code tagSizeBytes}, and {@code variant}. This is a convenience method that makes the tests
+   * code more concise and readable.
+   */
+  public static AesCmacParameters createAesCmacParameters(
+      int keySizeBytes, int tagSizeBytes, Variant variant) {
+    try {
+      return AesCmacParameters.builder()
+          .setKeySizeBytes(keySizeBytes)
+          .setVariant(variant)
+          .setTagSizeBytes(tagSizeBytes)
+          .build();
+    } catch (GeneralSecurityException ex) {
+      throw new TinkBugException(ex);
+    }
+  }
+
+  /**
+   * Represents a single AesCmac test vector.
+   */
+  public static final class AesCmacTestVector {
+    public final AesCmacKey key;
+    public final byte[] message;
+    public final byte[] tag;
+
+    public AesCmacTestVector(AesCmacKey key, String message, String tag) {
+      this.key = key;
+      this.message = Hex.decode(message);
+      this.tag = Hex.decode(tag);
+    }
+  }
+
+  private AesCmacTestUtil() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/mac/internal/BUILD.bazel
index d596cdb..510e532 100644
--- a/java_src/src/main/java/com/google/crypto/tink/mac/internal/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/BUILD.bazel
@@ -11,3 +11,201 @@
     name = "aes_util-android",
     srcs = ["AesUtil.java"],
 )
+
+java_library(
+    name = "chunked_aes_cmac_computation",
+    srcs = ["ChunkedAesCmacComputation.java"],
+    deps = [
+        ":aes_util",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+    ],
+)
+
+android_library(
+    name = "chunked_aes_cmac_computation-android",
+    srcs = ["ChunkedAesCmacComputation.java"],
+    deps = [
+        ":aes_util-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation-android",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
+    ],
+)
+
+java_library(
+    name = "chunked_aes_cmac_impl",
+    srcs = ["ChunkedAesCmacImpl.java"],
+    deps = [
+        ":chunked_aes_cmac_computation",
+        ":chunked_aes_cmac_verification",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "chunked_aes_cmac_impl-android",
+    srcs = ["ChunkedAesCmacImpl.java"],
+    deps = [
+        ":chunked_aes_cmac_computation-android",
+        ":chunked_aes_cmac_verification-android",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "chunked_aes_cmac_verification",
+    srcs = ["ChunkedAesCmacVerification.java"],
+    deps = [
+        ":chunked_aes_cmac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+    ],
+)
+
+android_library(
+    name = "chunked_aes_cmac_verification-android",
+    srcs = ["ChunkedAesCmacVerification.java"],
+    deps = [
+        ":chunked_aes_cmac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+    ],
+)
+
+java_library(
+    name = "chunked_hmac_verification",
+    srcs = ["ChunkedHmacVerification.java"],
+    deps = [
+        ":chunked_hmac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+    ],
+)
+
+android_library(
+    name = "chunked_hmac_verification-android",
+    srcs = ["ChunkedHmacVerification.java"],
+    deps = [
+        ":chunked_hmac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+    ],
+)
+
+java_library(
+    name = "chunked_hmac_impl",
+    srcs = ["ChunkedHmacImpl.java"],
+    deps = [
+        ":chunked_hmac_computation",
+        ":chunked_hmac_verification",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "chunked_hmac_impl-android",
+    srcs = ["ChunkedHmacImpl.java"],
+    deps = [
+        ":chunked_hmac_computation-android",
+        ":chunked_hmac_verification-android",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "chunked_hmac_computation",
+    srcs = ["ChunkedHmacComputation.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+    ],
+)
+
+android_library(
+    name = "chunked_hmac_computation-android",
+    srcs = ["ChunkedHmacComputation.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key-android",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/subtle:bytes-android",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
+    ],
+)
+
+java_library(
+    name = "aes_cmac_test_util",
+    testonly = 1,
+    srcs = ["AesCmacTestUtil.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+    ],
+)
+
+android_library(
+    name = "aes_cmac_test_util-android",
+    testonly = 1,
+    srcs = ["AesCmacTestUtil.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters-android",
+        "//src/main/java/com/google/crypto/tink/subtle:hex-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacComputation.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacComputation.java
new file mode 100644
index 0000000..70bc038
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacComputation.java
@@ -0,0 +1,156 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import static java.lang.Math.min;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.EngineFactory;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An implementation of streaming CMAC computation following
+ * tink/java_src/src/main/java/com/google/crypto/tink/subtle/PrfAesCmac.java's vision of <a
+ * href="https://tools.ietf.org/html/rfc4493">RFC 4493</a>.
+ */
+@AccessesPartialKey
+final class ChunkedAesCmacComputation implements ChunkedMacComputation {
+  // A single byte to be added to the plaintext for the legacy key type.
+  private static final byte[] FORMAT_VERSION = new byte[] {0};
+
+  private final Cipher aes;
+  private final AesCmacKey key;
+  // subKey1 and subKey2 are derived as in RFC 4493.
+  private final byte[] subKey1;
+  private final byte[] subKey2;
+  /**
+   * We need this AES-block sized buffer in order to account for the possibility of data not
+   * arriving in perfect blocks, and also because we never know which block is going to be the last,
+   * so we need to cache some data unprocessed between calls to update().
+   *
+   * Invariant: between calls we have: 0 <= localStash.position < 16, and localStash is a suffix of
+   * data so far, such that the rest is divisible by 16.
+   */
+  private final ByteBuffer localStash;
+  /* x and y contain the contents as in RFC 4493. */
+  private final ByteBuffer x;
+  private final ByteBuffer y;
+
+  private boolean finalized = false;
+
+  ChunkedAesCmacComputation(AesCmacKey key) throws GeneralSecurityException {
+    this.key = key;
+    aes = EngineFactory.CIPHER.getInstance("AES/ECB/NoPadding");
+    aes.init(
+        Cipher.ENCRYPT_MODE,
+        new SecretKeySpec(this.key.getAesKey().toByteArray(InsecureSecretKeyAccess.get()), "AES"));
+
+    // Generate subkeys; https://tools.ietf.org/html/rfc4493#section-2.3
+    byte[] zeroes = new byte[AesUtil.BLOCK_SIZE];
+    // As per documentation, the cipher is good to use after doFinal():
+    // https://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html#doFinal(byte[])
+    byte[] l = aes.doFinal(zeroes);
+    subKey1 = AesUtil.dbl(l);
+    subKey2 = AesUtil.dbl(subKey1);
+
+    localStash = ByteBuffer.allocate(AesUtil.BLOCK_SIZE);
+    x = ByteBuffer.allocate(AesUtil.BLOCK_SIZE);
+    y = ByteBuffer.allocate(AesUtil.BLOCK_SIZE);
+  }
+
+  private void munch(ByteBuffer data) throws GeneralSecurityException {
+    y.rewind();
+    x.rewind();
+    Bytes.xor(/* output= */ y, /* x= */ x, /* y= */ data, AesUtil.BLOCK_SIZE);
+
+    y.rewind();
+    x.rewind();
+    aes.doFinal(/* input= */ y, /* output= */ x);
+  }
+
+  @Override
+  public void update(ByteBuffer data) throws GeneralSecurityException {
+    if (finalized) {
+      throw new IllegalStateException(
+          "Can not update after computing the MAC tag. Please create a new object.");
+    }
+
+    if (localStash.remaining() != AesUtil.BLOCK_SIZE) {
+      // Only copy data into the stash if there are existing leftovers.
+      int bytesToCopy = min(localStash.remaining(), data.remaining());
+      for (int i = 0; i < bytesToCopy; i++) {
+        localStash.put(data.get());
+      }
+    }
+    if (localStash.remaining() == 0 && data.remaining() > 0) {
+      // Stash is full but there is more data.
+      localStash.rewind();
+      munch(localStash);
+      localStash.rewind();
+    }
+
+    // Now, "stash is empty" OR "data is empty".
+
+    // Now process directly from the rest of the input buffer.
+    // NOTE: if there are exactly block_size bytes left, don't process yet. (may be last block)
+    while (data.remaining() > AesUtil.BLOCK_SIZE) {
+      munch(data);
+    }
+
+    // There is now {0 .. block size} data left,
+    // stash is empty with the block size capacity
+    // => we can safely stuff everything into stash.
+    localStash.put(data);
+  }
+
+  @Override
+  public byte[] computeMac() throws GeneralSecurityException {
+    if (finalized) {
+      throw new IllegalStateException(
+          "Can not compute after computing the MAC tag. Please create a new object.");
+    }
+    if (key.getParameters().getVariant() == Variant.LEGACY) {
+      update(ByteBuffer.wrap(FORMAT_VERSION));
+    }
+    finalized = true;
+
+    byte[] mLast;
+    if (localStash.remaining() > 0) {
+      // An incomplete block or an empty input.
+      byte[] lastChunkToPad = Arrays.copyOf(localStash.array(), localStash.position());
+      mLast = Bytes.xor(AesUtil.cmacPad(lastChunkToPad), subKey2);
+    } else {
+      // Block is full (remaining() == 0).
+      mLast = Bytes.xor(localStash.array(), 0, subKey1, 0, AesUtil.BLOCK_SIZE);
+    }
+
+    return Bytes.concat(
+        key.getOutputPrefix().toByteArray(),
+        Arrays.copyOf(
+            aes.doFinal(Bytes.xor(mLast, x.array())),
+            key.getParameters().getCryptographicTagSizeBytes()));
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacImpl.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacImpl.java
new file mode 100644
index 0000000..8ca9b3c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacImpl.java
@@ -0,0 +1,60 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.ChunkedMac;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.ChunkedMacVerification;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+
+/** AES-CMAC implementation of the ChunkedMac interface. */
+@Immutable
+public final class ChunkedAesCmacImpl implements ChunkedMac {
+  private static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS =
+      TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_NOT_FIPS;
+
+  @SuppressWarnings("Immutable") // We never change the key.
+  private final AesCmacKey key;
+
+  public ChunkedAesCmacImpl(AesCmacKey key) throws GeneralSecurityException {
+    if (!FIPS.isCompatible()) {
+      throw new GeneralSecurityException("Can not use AES-CMAC in FIPS-mode.");
+    }
+    this.key = key;
+  }
+
+  @Override
+  public ChunkedMacComputation createComputation() throws GeneralSecurityException {
+    return new ChunkedAesCmacComputation(key);
+  }
+
+  @Override
+  public ChunkedMacVerification createVerification(final byte[] tag)
+      throws GeneralSecurityException {
+    if (tag.length < key.getOutputPrefix().size()) {
+      throw new GeneralSecurityException("Tag too short");
+    }
+    if (!key.getOutputPrefix().equals(Bytes.copyFrom(tag, 0, key.getOutputPrefix().size()))) {
+      throw new GeneralSecurityException("Wrong tag prefix");
+    }
+    return new ChunkedAesCmacVerification(key, tag);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacVerification.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacVerification.java
new file mode 100644
index 0000000..fa8ffe0
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacVerification.java
@@ -0,0 +1,54 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.ChunkedMacVerification;
+import com.google.crypto.tink.util.Bytes;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+
+/**
+ * An implementation of streaming CMAC verification. Uses ChunkedAesCmacComputation implementation
+ * under the hood.
+ */
+final class ChunkedAesCmacVerification implements ChunkedMacVerification {
+  private final Bytes tag;
+  private final ChunkedAesCmacComputation aesCmacComputation;
+
+  ChunkedAesCmacVerification(AesCmacKey key, byte[] tag)
+      throws GeneralSecurityException {
+    // Checks regarding tag and key sizes, as well as FIPS-compatibility, are performed by
+    // ChunkedAesCmacImpl.
+    aesCmacComputation = new ChunkedAesCmacComputation(key);
+    this.tag = Bytes.copyFrom(tag);
+  }
+
+  @Override
+  public void update(ByteBuffer data) throws GeneralSecurityException {
+    // No need to check state since the ChunkedAesCmacComputation already does this.
+    aesCmacComputation.update(data);
+  }
+
+  @Override
+  public void verifyMac() throws GeneralSecurityException {
+    byte[] other = aesCmacComputation.computeMac();
+    if (!tag.equals(Bytes.copyFrom(other))) {
+      throw new GeneralSecurityException("invalid MAC");
+    }
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacComputation.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacComputation.java
new file mode 100644
index 0000000..f61cf14
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacComputation.java
@@ -0,0 +1,80 @@
+// Copyright 2022 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacParameters.Variant;
+import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.EngineFactory;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An implementation of streaming HMAC computation. Not thread-safe, thread safety must be ensured
+ * by the caller if objects of this class are accessed concurrently.
+ */
+@AccessesPartialKey
+final class ChunkedHmacComputation implements ChunkedMacComputation {
+  private static final byte[] FORMAT_VERSION = new byte[] {0};
+
+  private final Mac mac;
+  private final HmacKey key;
+
+  private boolean finalized = false;
+
+  ChunkedHmacComputation(HmacKey key) throws GeneralSecurityException {
+    mac = EngineFactory.MAC.getInstance(composeAlgorithmName(key));
+    mac.init(
+        new SecretKeySpec(
+            key.getKeyBytes().toByteArray(InsecureSecretKeyAccess.get()), "HMAC"));
+    this.key = key;
+  }
+
+  @Override
+  public void update(ByteBuffer data) {
+    if (finalized) {
+      throw new IllegalStateException(
+          "Cannot update after computing the MAC tag. Please create a new object.");
+    }
+    mac.update(data);
+  }
+
+  @Override
+  public byte[] computeMac() throws GeneralSecurityException {
+    if (finalized) {
+      throw new IllegalStateException(
+          "Cannot compute after already computing the MAC tag. Please create a new object.");
+    }
+    if (key.getParameters().getVariant() == Variant.LEGACY) {
+      update(ByteBuffer.wrap(FORMAT_VERSION));
+    }
+    finalized = true;
+    return Bytes.concat(
+        key.getOutputPrefix().toByteArray(),
+        Arrays.copyOf(mac.doFinal(), key.getParameters().getCryptographicTagSizeBytes()));
+  }
+
+  private static String composeAlgorithmName(HmacKey key) {
+    return "HMAC" + key.getParameters().getHashType();
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacImpl.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacImpl.java
new file mode 100644
index 0000000..8f403bc
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacImpl.java
@@ -0,0 +1,61 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.mac.ChunkedMac;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.ChunkedMacVerification;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+
+/** Class that provides the functionality expressed by the ChunkedMac interface with HMAC. */
+@Immutable
+public final class ChunkedHmacImpl implements ChunkedMac {
+  private static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS =
+      TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_REQUIRES_BORINGCRYPTO;
+
+  @SuppressWarnings("Immutable") // We never change the key.
+  private final HmacKey key;
+
+  public ChunkedHmacImpl(HmacKey key) throws GeneralSecurityException {
+    if (!FIPS.isCompatible()) {
+      throw new GeneralSecurityException(
+          "Can not use HMAC in FIPS-mode, as BoringCrypto module is not available.");
+    }
+    this.key = key;
+  }
+
+  @Override
+  public ChunkedMacComputation createComputation() throws GeneralSecurityException {
+    return new ChunkedHmacComputation(key);
+  }
+
+  @Override
+  public ChunkedMacVerification createVerification(final byte[] tag)
+      throws GeneralSecurityException {
+    if (tag.length < key.getOutputPrefix().size()) {
+      throw new GeneralSecurityException("Tag too short");
+    }
+    if (!key.getOutputPrefix().equals(Bytes.copyFrom(tag, 0, key.getOutputPrefix().size()))) {
+      throw new GeneralSecurityException("Wrong tag prefix");
+    }
+    return new ChunkedHmacVerification(key, tag);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacVerification.java b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacVerification.java
new file mode 100644
index 0000000..b55a6d7
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacVerification.java
@@ -0,0 +1,52 @@
+// Copyright 2022 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import com.google.crypto.tink.mac.ChunkedMacVerification;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.util.Bytes;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+
+/**
+ * An implementation of streaming HMAC verification. Uses ChunkedHmacComputation implementation
+ * under the hood. Not thread-safe, thread safety must be ensured by the caller if objects of this
+ * class are accessed concurrently.
+ */
+final class ChunkedHmacVerification implements ChunkedMacVerification {
+  private final Bytes tag;
+  private final ChunkedHmacComputation hmacComputation;
+
+  ChunkedHmacVerification(HmacKey key, byte[] tag) throws GeneralSecurityException {
+    hmacComputation = new ChunkedHmacComputation(key);
+    this.tag = Bytes.copyFrom(tag);
+  }
+
+  @Override
+  public void update(ByteBuffer data) {
+    // No need to check state since the ChunkedHmacComputation already does this.
+    hmacComputation.update(data);
+  }
+
+  @Override
+  public void verifyMac() throws GeneralSecurityException {
+    byte[] other = hmacComputation.computeMac();
+    if (!tag.equals(Bytes.copyFrom(other))) {
+      throw new GeneralSecurityException("invalid MAC");
+    }
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/monitoring/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/monitoring/BUILD.bazel
index e3834cf..ab2c465 100644
--- a/java_src/src/main/java/com/google/crypto/tink/monitoring/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/monitoring/BUILD.bazel
@@ -10,7 +10,6 @@
     deps = [
         ":monitoring_annotations",
         "//src/main/java/com/google/crypto/tink:key_status",
-        "//src/main/java/com/google/crypto/tink:parameters",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
@@ -23,7 +22,6 @@
     deps = [
         ":monitoring_annotations-android",
         "//src/main/java/com/google/crypto/tink:key_status-android",
-        "//src/main/java/com/google/crypto/tink:parameters-android",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
diff --git a/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringAnnotations.java b/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringAnnotations.java
index d9efa7e..096353d 100644
--- a/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringAnnotations.java
+++ b/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringAnnotations.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.monitoring;
 
 import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import java.util.Collections;
 import java.util.HashMap;
@@ -37,6 +38,7 @@
   public static final class Builder {
     private HashMap<String, String> builderEntries = new HashMap<>();
 
+    @CanIgnoreReturnValue
     public Builder addAll(Map<String, String> newEntries) {
       if (builderEntries == null) {
         throw new IllegalStateException("addAll cannot be called after build()");
@@ -45,6 +47,7 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder add(String name, String value) {
       if (builderEntries == null) {
         throw new IllegalStateException("add cannot be called after build()");
diff --git a/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfo.java b/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfo.java
index 554c9d3..9f43a35 100644
--- a/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfo.java
+++ b/java_src/src/main/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfo.java
@@ -17,8 +17,8 @@
 package com.google.crypto.tink.monitoring;
 
 import com.google.crypto.tink.KeyStatus;
-import com.google.crypto.tink.Parameters;
 import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
@@ -42,7 +42,8 @@
   public static final class Entry {
     private final KeyStatus status;
     private final int keyId;
-    private final Parameters parameters;
+    private final String keyType;
+    private final String keyPrefix;
 
     public KeyStatus getStatus() {
       return status;
@@ -52,14 +53,19 @@
       return keyId;
     }
 
-    public Parameters getParameters() {
-      return parameters;
+    public String getKeyType() {
+      return keyType;
     }
 
-    private Entry(KeyStatus status, int keyId, Parameters parameters) {
+    public String getKeyPrefix() {
+      return keyPrefix;
+    }
+
+    private Entry(KeyStatus status, int keyId, String keyType, String keyPrefix) {
       this.status = status;
       this.keyId = keyId;
-      this.parameters = parameters;
+      this.keyType = keyType;
+      this.keyPrefix = keyPrefix;
     }
 
     @Override
@@ -70,18 +76,20 @@
       Entry entry = (Entry) obj;
       return this.status == entry.status
           && this.keyId == entry.keyId
-          && this.parameters.equals(entry.parameters);
+          && this.keyType.equals(entry.keyType)
+          && this.keyPrefix.equals(entry.keyPrefix);
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(status, keyId, parameters.hashCode());
+      return Objects.hash(status, keyId, keyType, keyPrefix);
     }
 
     @Override
     public String toString() {
       return String.format(
-          "(status=%s, keyId=%s, parameters='%s')", this.status, this.keyId, this.parameters);
+          "(status=%s, keyId=%s, keyType='%s', keyPrefix='%s')",
+          this.status, this.keyId, this.keyType, this.keyPrefix);
     }
   }
 
@@ -93,6 +101,7 @@
     private MonitoringAnnotations builderAnnotations = MonitoringAnnotations.EMPTY;
     @Nullable private Integer builderPrimaryKeyId = null;
 
+    @CanIgnoreReturnValue
     public Builder setAnnotations(MonitoringAnnotations annotations) {
       if (builderEntries == null) {
         throw new IllegalStateException("setAnnotations cannot be called after build()");
@@ -101,14 +110,16 @@
       return this;
     }
 
-    public Builder addEntry(KeyStatus status, int keyId, Parameters parameters) {
+    @CanIgnoreReturnValue
+    public Builder addEntry(KeyStatus status, int keyId, String keyType, String keyPrefix) {
       if (builderEntries == null) {
         throw new IllegalStateException("addEntry cannot be called after build()");
       }
-      builderEntries.add(new Entry(status, keyId, parameters));
+      builderEntries.add(new Entry(status, keyId, keyType, keyPrefix));
       return this;
     }
 
+    @CanIgnoreReturnValue
     public Builder setPrimaryKeyId(int primaryKeyId) {
       if (builderEntries == null) {
         throw new IllegalStateException("setPrimaryKeyId cannot be called after build()");
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKey.java b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKey.java
new file mode 100644
index 0000000..33f154b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKey.java
@@ -0,0 +1,87 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a key computing AES CMAC PRF.
+ *
+ * <p>This API is annotated with {@link com.google.crypto.tink.annotations.Alpha} because it is not
+ * yet stable and might change in the future.
+ */
+@Alpha
+@Immutable
+public final class AesCmacPrfKey extends PrfKey {
+  private final AesCmacPrfParameters parameters;
+  private final SecretBytes keyBytes;
+
+  private AesCmacPrfKey(AesCmacPrfParameters parameters, SecretBytes keyBytes) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static AesCmacPrfKey create(AesCmacPrfParameters parameters, SecretBytes keyBytes)
+      throws GeneralSecurityException {
+
+    if (parameters.getKeySizeBytes() != keyBytes.size()) {
+      throw new GeneralSecurityException("Key size mismatch");
+    }
+    return new AesCmacPrfKey(parameters, keyBytes);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public AesCmacPrfParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return null;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesCmacPrfKey)) {
+      return false;
+    }
+    AesCmacPrfKey that = (AesCmacPrfKey) o;
+    return that.parameters.equals(parameters) && that.keyBytes.equalsSecretBytes(keyBytes);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKeyManager.java
index 4a5198b..12a4e5c 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfKeyManager.java
@@ -130,6 +130,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesCmacPrfKeyManager(), newKeyAllowed);
+    AesCmacPrfProtoSerialization.register();
   }
 
   /**
@@ -141,7 +142,7 @@
    *   <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW}
    * </ul>
    *
-   * .
+   *
    *
    * @return A {@link KeyTemplate} that generates new instances of AES-CMAC keys with the following
    *     parameters:
@@ -149,10 +150,7 @@
    *       <li>Key size: 32 bytes
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW}
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_CMAC_PRF")}
    */
-  @Deprecated
   public static final KeyTemplate aes256CmacTemplate() {
     AesCmacPrfKeyFormat format = AesCmacPrfKeyFormat.newBuilder().setKeySize(32).build();
     return KeyTemplate.create(
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfParameters.java b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfParameters.java
new file mode 100644
index 0000000..f918b94
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfParameters.java
@@ -0,0 +1,74 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.annotations.Alpha;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+
+/**
+ * Describes the parameters of an {@link AesCmacPrfKey}.
+ *
+ * <p>This API is annotated with {@link com.google.crypto.tink.annotations.Alpha} because it is not
+ * yet stable and might change in the future.
+ */
+@Alpha
+public final class AesCmacPrfParameters extends PrfParameters {
+  public static AesCmacPrfParameters create(int keySizeBytes) throws GeneralSecurityException {
+    if (keySizeBytes != 16 && keySizeBytes != 32) {
+      throw new InvalidAlgorithmParameterException(
+          String.format(
+              "Invalid key size %d; only 128-bit and 256-bit are supported", keySizeBytes * 8));
+    }
+    return new AesCmacPrfParameters(keySizeBytes);
+  }
+
+  private final int keySizeBytes;
+
+  private AesCmacPrfParameters(int keySizeBytes) {
+    this.keySizeBytes = keySizeBytes;
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesCmacPrfParameters)) {
+      return false;
+    }
+    AesCmacPrfParameters that = (AesCmacPrfParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(AesCmacPrfParameters.class, keySizeBytes);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "AesCmac PRF Parameters (" + keySizeBytes + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfProtoSerialization.java
new file mode 100644
index 0000000..945fb8c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/AesCmacPrfProtoSerialization.java
@@ -0,0 +1,175 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link AesCmacPrfKey} objects and {@link AesCmacPrfParameters}
+ * objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesCmacPrfProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesCmacPrfKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<AesCmacPrfParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesCmacPrfProtoSerialization::serializeParameters,
+              AesCmacPrfParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesCmacPrfProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesCmacPrfKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesCmacPrfProtoSerialization::serializeKey,
+          AesCmacPrfKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesCmacPrfProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static ProtoParametersSerialization serializeParameters(AesCmacPrfParameters parameters) {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesCmacPrfKeyFormat.newBuilder()
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      AesCmacPrfKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        OutputPrefixType.RAW,
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesCmacPrfParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesCmacPrfProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesCmacPrfKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesCmacPrfKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesCmacPrfParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException(
+          "Parsing AesCmacPrfParameters failed: unknown Version " + format.getVersion());
+    }
+    if (serialization.getKeyTemplate().getOutputPrefixType() != OutputPrefixType.RAW) {
+      throw new GeneralSecurityException(
+          "Parsing AesCmacPrfParameters failed: only RAW output prefix type is accepted");
+    }
+    return AesCmacPrfParameters.create(format.getKeySize());
+  }
+
+  @SuppressWarnings("UnusedException") // Proto parsing error adds no relevant information
+  private static AesCmacPrfKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesCmacPrfProtoSerialization.parseKey");
+    }
+    try {
+      com.google.crypto.tink.proto.AesCmacPrfKey protoKey =
+          com.google.crypto.tink.proto.AesCmacPrfKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      if (serialization.getIdRequirementOrNull() != null) {
+        throw new GeneralSecurityException("ID requirement must be null.");
+      }
+      AesCmacPrfParameters parameters = AesCmacPrfParameters.create(protoKey.getKeyValue().size());
+      return AesCmacPrfKey.create(
+          parameters,
+          SecretBytes.copyFrom(
+              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)));
+
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesCmacPrfKey failed");
+    }
+  }
+
+  /**
+   * Registers previously defined parser/serializer objects into a global, mutable registry.
+   * Registration is public to enable custom configurations.
+   */
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  /** Registers previously defined parser/serializer objects into a given registry. */
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesCmacPrfProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/prf/BUILD.bazel
index c8e0527..4ba3398 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/BUILD.bazel
@@ -6,6 +6,7 @@
     name = "hkdf_prf_key_manager",
     srcs = ["HkdfPrfKeyManager.java"],
     deps = [
+        ":hkdf_prf_proto_serialization",
         ":prf_set",
         "//proto:common_java_proto",
         "//proto:hkdf_prf_java_proto",
@@ -20,7 +21,7 @@
         "//src/main/java/com/google/crypto/tink/subtle/prf:hkdf_streaming_prf",
         "//src/main/java/com/google/crypto/tink/subtle/prf:prf_impl",
         "//src/main/java/com/google/crypto/tink/subtle/prf:streaming_prf",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -72,14 +73,231 @@
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper",
         "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/internal:monitoring_util",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
+java_library(
+    name = "aes_cmac_prf_key_manager",
+    srcs = ["AesCmacPrfKeyManager.java"],
+    deps = [
+        ":aes_cmac_prf_proto_serialization",
+        ":prf_set",
+        "//proto:aes_cmac_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/subtle:validators",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "hmac_prf_key_manager",
+    srcs = ["HmacPrfKeyManager.java"],
+    deps = [
+        ":hmac_prf_proto_serialization",
+        ":prf_set",
+        "//proto:common_java_proto",
+        "//proto:hmac_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/subtle:validators",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "prf_parameters",
+    srcs = ["PrfParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "prf_key",
+    srcs = ["PrfKey.java"],
+    deps = [
+        ":prf_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+    ],
+)
+
+java_library(
+    name = "hkdf_prf_parameters",
+    srcs = ["HkdfPrfParameters.java"],
+    deps = [
+        ":prf_parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hkdf_prf_key",
+    srcs = ["HkdfPrfKey.java"],
+    deps = [
+        ":hkdf_prf_parameters",
+        ":prf_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hkdf_prf_proto_serialization",
+    srcs = ["HkdfPrfProtoSerialization.java"],
+    deps = [
+        ":hkdf_prf_key",
+        ":hkdf_prf_parameters",
+        "//proto:common_java_proto",
+        "//proto:hkdf_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "hmac_prf_parameters",
+    srcs = ["HmacPrfParameters.java"],
+    deps = [
+        ":prf_parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_cmac_prf_parameters",
+    srcs = ["AesCmacPrfParameters.java"],
+    deps = [
+        ":prf_parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+    ],
+)
+
+java_library(
+    name = "hmac_prf_key",
+    srcs = ["HmacPrfKey.java"],
+    deps = [
+        ":hmac_prf_parameters",
+        ":prf_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_cmac_prf_key",
+    srcs = ["AesCmacPrfKey.java"],
+    deps = [
+        ":aes_cmac_prf_parameters",
+        ":prf_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "hmac_prf_proto_serialization",
+    srcs = ["HmacPrfProtoSerialization.java"],
+    deps = [
+        ":hmac_prf_key",
+        ":hmac_prf_parameters",
+        "//proto:common_java_proto",
+        "//proto:hmac_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "aes_cmac_prf_proto_serialization",
+    srcs = ["AesCmacPrfProtoSerialization.java"],
+    deps = [
+        ":aes_cmac_prf_key",
+        ":aes_cmac_prf_parameters",
+        "//proto:aes_cmac_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
 android_library(
     name = "hkdf_prf_key_manager-android",
     srcs = ["HkdfPrfKeyManager.java"],
     deps = [
+        ":hkdf_prf_proto_serialization-android",
         ":prf_set-android",
         "//proto:common_java_proto_lite",
         "//proto:hkdf_prf_java_proto_lite",
@@ -94,83 +312,7 @@
         "//src/main/java/com/google/crypto/tink/subtle/prf:hkdf_streaming_prf-android",
         "//src/main/java/com/google/crypto/tink/subtle/prf:prf_impl-android",
         "//src/main/java/com/google/crypto/tink/subtle/prf:streaming_prf-android",
-        "@com_google_protobuf//:protobuf_javalite",
-    ],
-)
-
-java_library(
-    name = "aes_cmac_prf_key_manager",
-    srcs = ["AesCmacPrfKeyManager.java"],
-    deps = [
-        ":prf_set",
-        "//proto:aes_cmac_prf_java_proto",
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:key_template",
-        "//src/main/java/com/google/crypto/tink:registry",
-        "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
-        "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
-        "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
-    ],
-)
-
-android_library(
-    name = "aes_cmac_prf_key_manager-android",
-    srcs = ["AesCmacPrfKeyManager.java"],
-    deps = [
-        ":prf_set-android",
-        "//proto:aes_cmac_prf_java_proto_lite",
-        "//proto:tink_java_proto_lite",
-        "//src/main/java/com/google/crypto/tink:key_template-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
-        "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
-        "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
-        "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac-android",
-        "//src/main/java/com/google/crypto/tink/subtle:random-android",
-        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
-    ],
-)
-
-java_library(
-    name = "hmac_prf_key_manager",
-    srcs = ["HmacPrfKeyManager.java"],
-    deps = [
-        ":prf_set",
-        "//proto:common_java_proto",
-        "//proto:hmac_prf_java_proto",
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:key_template",
-        "//src/main/java/com/google/crypto/tink:registry",
-        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
-        "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
-        "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
-        "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
-    ],
-)
-
-android_library(
-    name = "hmac_prf_key_manager-android",
-    srcs = ["HmacPrfKeyManager.java"],
-    deps = [
-        ":prf_set-android",
-        "//proto:common_java_proto_lite",
-        "//proto:hmac_prf_java_proto_lite",
-        "//proto:tink_java_proto_lite",
-        "//src/main/java/com/google/crypto/tink:key_template-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
-        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
-        "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
-        "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
-        "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce-android",
-        "//src/main/java/com/google/crypto/tink/subtle:random-android",
-        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -210,6 +352,10 @@
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:primitive_wrapper-android",
         "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:monitoring_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry-android",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client-android",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info-android",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
@@ -223,3 +369,237 @@
     visibility = ["//visibility:public"],
     deps = ["@maven//:com_google_errorprone_error_prone_annotations"],
 )
+
+android_library(
+    name = "aes_cmac_prf_key_manager-android",
+    srcs = ["AesCmacPrfKeyManager.java"],
+    deps = [
+        ":aes_cmac_prf_proto_serialization-android",
+        ":prf_set-android",
+        "//proto:aes_cmac_prf_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:key_template-android",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac-android",
+        "//src/main/java/com/google/crypto/tink/subtle:random-android",
+        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "hmac_prf_key_manager-android",
+    srcs = ["HmacPrfKeyManager.java"],
+    deps = [
+        ":hmac_prf_proto_serialization-android",
+        ":prf_set-android",
+        "//proto:common_java_proto_lite",
+        "//proto:hmac_prf_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:key_template-android",
+        "//src/main/java/com/google/crypto/tink:registry-android",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce-android",
+        "//src/main/java/com/google/crypto/tink/subtle:random-android",
+        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "prf_parameters-android",
+    srcs = ["PrfParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "prf_key-android",
+    srcs = ["PrfKey.java"],
+    deps = [
+        ":prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+    ],
+)
+
+android_library(
+    name = "hkdf_prf_parameters-android",
+    srcs = ["HkdfPrfParameters.java"],
+    deps = [
+        ":prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hkdf_prf_key-android",
+    srcs = ["HkdfPrfKey.java"],
+    deps = [
+        ":hkdf_prf_parameters-android",
+        ":prf_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hkdf_prf_proto_serialization-android",
+    srcs = ["HkdfPrfProtoSerialization.java"],
+    deps = [
+        ":hkdf_prf_key-android",
+        ":hkdf_prf_parameters-android",
+        "//proto:common_java_proto_lite",
+        "//proto:hkdf_prf_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "hmac_prf_parameters-android",
+    srcs = ["HmacPrfParameters.java"],
+    deps = [
+        ":prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_cmac_prf_parameters-android",
+    srcs = ["AesCmacPrfParameters.java"],
+    deps = [
+        ":prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+    ],
+)
+
+android_library(
+    name = "hmac_prf_key-android",
+    srcs = ["HmacPrfKey.java"],
+    deps = [
+        ":hmac_prf_parameters-android",
+        ":prf_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_cmac_prf_key-android",
+    srcs = ["AesCmacPrfKey.java"],
+    deps = [
+        ":aes_cmac_prf_parameters-android",
+        ":prf_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "hmac_prf_proto_serialization-android",
+    srcs = ["HmacPrfProtoSerialization.java"],
+    deps = [
+        ":hmac_prf_key-android",
+        ":hmac_prf_parameters-android",
+        "//proto:common_java_proto_lite",
+        "//proto:hmac_prf_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "aes_cmac_prf_proto_serialization-android",
+    srcs = ["AesCmacPrfProtoSerialization.java"],
+    deps = [
+        ":aes_cmac_prf_key-android",
+        ":aes_cmac_prf_parameters-android",
+        "//proto:aes_cmac_prf_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "predefined_prf_parameters-android",
+    srcs = ["PredefinedPrfParameters.java"],
+    deps = [
+        ":aes_cmac_prf_parameters-android",
+        ":hkdf_prf_parameters-android",
+        ":hmac_prf_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+    ],
+)
+
+java_library(
+    name = "predefined_prf_parameters",
+    srcs = ["PredefinedPrfParameters.java"],
+    deps = [
+        ":aes_cmac_prf_parameters",
+        ":hkdf_prf_parameters",
+        ":hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKey.java b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKey.java
new file mode 100644
index 0000000..620c1de
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKey.java
@@ -0,0 +1,117 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a key computing HKDF PRF.
+ *
+ * <p>HKDF PRF is specified in RFC 5869.
+ *
+ * <p>This API is annotated with {@link com.google.crypto.tink.annotations.Alpha} because it is not
+ * yet stable and might change in the future.
+ */
+@Alpha
+@Immutable
+public final class HkdfPrfKey extends PrfKey {
+  private final HkdfPrfParameters parameters;
+  private final SecretBytes keyBytes;
+
+  /** Builder for HkdfPrfKey. */
+  public static final class Builder {
+    @Nullable private HkdfPrfParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(HkdfPrfParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    public HkdfPrfKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      return new HkdfPrfKey(parameters, keyBytes);
+    }
+  }
+
+  private HkdfPrfKey(HkdfPrfParameters parameters, SecretBytes keyBytes) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public HkdfPrfParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return null;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof HkdfPrfKey)) {
+      return false;
+    }
+    HkdfPrfKey that = (HkdfPrfKey) o;
+    return that.parameters.equals(parameters) && that.keyBytes.equalsSecretBytes(keyBytes);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKeyManager.java
index 0963f47..dd924bf 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfKeyManager.java
@@ -170,6 +170,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new HkdfPrfKeyManager(), newKeyAllowed);
+    HkdfPrfProtoSerialization.register();
   }
 
   public static String staticKeyType() {
@@ -184,10 +185,7 @@
    *   <li>HMAC key size: 32 bytes
    *   <li>Salt: empty
    * </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HKDF_SHA256")}
    */
-  @Deprecated
   public static final KeyTemplate hkdfSha256Template() {
     HkdfPrfKeyFormat format =
         HkdfPrfKeyFormat.newBuilder()
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfParameters.java b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfParameters.java
new file mode 100644
index 0000000..0ea2997
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfParameters.java
@@ -0,0 +1,168 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Describes the parameters of an {@link HkdfPrfKey}.
+ *
+ * <p>This API is annotated with {@link com.google.crypto.tink.annotations.Alpha} because it is not
+ * yet stable and might change in the future.
+ */
+@Alpha
+public final class HkdfPrfParameters extends PrfParameters {
+  private static final int MIN_KEY_SIZE = 16;
+
+  /** The Hash algorithm used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA1 = new HashType("SHA1");
+    public static final HashType SHA224 = new HashType("SHA224");
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builder for HkdfPrfParameters. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    @Nullable private HashType hashType = null;
+    @Nullable private Bytes salt = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes < MIN_KEY_SIZE) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 128-bit or larger are supported", keySizeBytes * 8));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHashType(HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setSalt(Bytes salt) {
+      if (salt.size() == 0) {
+        this.salt = null;
+        return this;
+      }
+      this.salt = salt;
+      return this;
+    }
+
+    public HkdfPrfParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("key size is not set");
+      }
+      if (hashType == null) {
+        throw new GeneralSecurityException("hash type is not set");
+      }
+      return new HkdfPrfParameters(keySizeBytes, hashType, salt);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final HashType hashType;
+  @Nullable private final Bytes salt;
+
+  private HkdfPrfParameters(int keySizeBytes, HashType hashType, Bytes salt) {
+    this.keySizeBytes = keySizeBytes;
+    this.hashType = hashType;
+    this.salt = salt;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  public HashType getHashType() {
+    return hashType;
+  }
+
+  /**
+   * Gets the salt value, which defaults to null if not set, as per RFC 5869. The HKDF PRF
+   * implementation must convert a null salt to a string of zeros that is the length of the hash
+   * function output.
+   */
+  @Nullable
+  public Bytes getSalt() {
+    return salt;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof HkdfPrfParameters)) {
+      return false;
+    }
+    HkdfPrfParameters that = (HkdfPrfParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getHashType() == getHashType()
+        && Objects.equals(that.getSalt(), getSalt());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(HkdfPrfParameters.class, keySizeBytes, hashType, salt);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "HKDF PRF Parameters (hashType: "
+        + hashType
+        + ", salt: "
+        + salt
+        + ", and "
+        + keySizeBytes
+        + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfProtoSerialization.java
new file mode 100644
index 0000000..5e36b7d
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HkdfPrfProtoSerialization.java
@@ -0,0 +1,231 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link HkdfPrfKey} objects and {@link HkdfPrfParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class HkdfPrfProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.HkdfPrfKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<HkdfPrfParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              HkdfPrfProtoSerialization::serializeParameters,
+              HkdfPrfParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          HkdfPrfProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<HkdfPrfKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          HkdfPrfProtoSerialization::serializeKey, HkdfPrfKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          HkdfPrfProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static HashType toProtoHashType(HkdfPrfParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (HkdfPrfParameters.HashType.SHA1.equals(hashType)) {
+      return HashType.SHA1;
+    }
+    if (HkdfPrfParameters.HashType.SHA224.equals(hashType)) {
+      return HashType.SHA224;
+    }
+    if (HkdfPrfParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (HkdfPrfParameters.HashType.SHA384.equals(hashType)) {
+      return HashType.SHA384;
+    }
+    if (HkdfPrfParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static HkdfPrfParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA1:
+        return HkdfPrfParameters.HashType.SHA1;
+      case SHA224:
+        return HkdfPrfParameters.HashType.SHA224;
+      case SHA256:
+        return HkdfPrfParameters.HashType.SHA256;
+      case SHA384:
+        return HkdfPrfParameters.HashType.SHA384;
+      case SHA512:
+        return HkdfPrfParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.HkdfPrfParams getProtoParams(
+      HkdfPrfParameters parameters) throws GeneralSecurityException {
+    com.google.crypto.tink.proto.HkdfPrfParams.Builder builder =
+        com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+            .setHash(toProtoHashType(parameters.getHashType()));
+    if (parameters.getSalt() != null && parameters.getSalt().size() > 0) {
+      builder.setSalt(ByteString.copyFrom(parameters.getSalt().toByteArray()));
+    }
+    return builder.build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(HkdfPrfParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.HkdfPrfKeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      HkdfPrfKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+            .setParams(getProtoParams(key.getParameters()))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        OutputPrefixType.RAW,
+        key.getIdRequirementOrNull());
+  }
+
+  private static HkdfPrfParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HkdfPrfProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.HkdfPrfKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.HkdfPrfKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HkdfPrfParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException(
+          "Parsing HkdfPrfParameters failed: unknown Version " + format.getVersion());
+    }
+    if (serialization.getKeyTemplate().getOutputPrefixType() != OutputPrefixType.RAW) {
+      throw new GeneralSecurityException(
+          "Parsing HkdfPrfParameters failed: only RAW output prefix type is accepted");
+    }
+    return HkdfPrfParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        .setHashType(toHashType(format.getParams().getHash()))
+        .setSalt(Bytes.copyFrom(format.getParams().getSalt().toByteArray()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static HkdfPrfKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HkdfPrfProtoSerialization.parseKey");
+    }
+    try {
+      com.google.crypto.tink.proto.HkdfPrfKey protoKey =
+          com.google.crypto.tink.proto.HkdfPrfKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      if (serialization.getIdRequirementOrNull() != null) {
+        throw new GeneralSecurityException("ID requirement must be null.");
+      }
+      HkdfPrfParameters parameters =
+          HkdfPrfParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setHashType(toHashType(protoKey.getParams().getHash()))
+              .setSalt(Bytes.copyFrom(protoKey.getParams().getSalt().toByteArray()))
+              .build();
+      return HkdfPrfKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing HkdfPrfKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private HkdfPrfProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKey.java b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKey.java
new file mode 100644
index 0000000..d05827a
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKey.java
@@ -0,0 +1,115 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a key computing HMAC PRF.
+ *
+ * <p>This API is annotated with {@link com.google.crypto.tink.annotations.Alpha} because it is not
+ * yet stable and might change in the future.
+ */
+@Alpha
+@Immutable
+public final class HmacPrfKey extends PrfKey {
+  private final HmacPrfParameters parameters;
+  private final SecretBytes keyBytes;
+
+  /** Builder for HmacPrfKey. */
+  public static final class Builder {
+    @Nullable private HmacPrfParameters parameters = null;
+    @Nullable private SecretBytes keyBytes = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(HmacPrfParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setKeyBytes(SecretBytes keyBytes) {
+      this.keyBytes = keyBytes;
+      return this;
+    }
+
+    public HmacPrfKey build() throws GeneralSecurityException {
+      if (parameters == null || keyBytes == null) {
+        throw new GeneralSecurityException("Cannot build without parameters and/or key material");
+      }
+
+      if (parameters.getKeySizeBytes() != keyBytes.size()) {
+        throw new GeneralSecurityException("Key size mismatch");
+      }
+
+      return new HmacPrfKey(parameters, keyBytes);
+    }
+  }
+
+  private HmacPrfKey(HmacPrfParameters parameters, SecretBytes keyBytes) {
+    this.parameters = parameters;
+    this.keyBytes = keyBytes;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getKeyBytes() {
+    return keyBytes;
+  }
+
+  @Override
+  public HmacPrfParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return null;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof HmacPrfKey)) {
+      return false;
+    }
+    HmacPrfKey that = (HmacPrfKey) o;
+    return that.parameters.equals(parameters) && that.keyBytes.equalsSecretBytes(keyBytes);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKeyManager.java
index 1d399cb..699e3aa 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfKeyManager.java
@@ -146,10 +146,7 @@
         Validators.validateVersion(format.getVersion(), getVersion());
         byte[] pseudorandomness = new byte[format.getKeySize()];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != format.getKeySize()) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return HmacPrfKey.newBuilder()
               .setVersion(getVersion())
               .setParams(format.getParams())
@@ -187,6 +184,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new HmacPrfKeyManager(), newKeyAllowed);
+    HmacPrfProtoSerialization.register();
   }
 
   @Override
@@ -203,10 +201,7 @@
    *   <li>Hash function: SHA256
    *   <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW}
    * </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HMAC_SHA256_PRF")}
    */
-  @Deprecated
   public static final KeyTemplate hmacSha256Template() {
     return createTemplate(32, HashType.SHA256);
   }
@@ -220,10 +215,7 @@
    *   <li>Hash function: SHA512
    *   <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW}
    * </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("HMAC_SHA512_PRF")}
    */
-  @Deprecated
   public static final KeyTemplate hmacSha512Template() {
     return createTemplate(64, HashType.SHA512);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfParameters.java b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfParameters.java
new file mode 100644
index 0000000..587d3e0
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfParameters.java
@@ -0,0 +1,136 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Describes the parameters of an {@link HmacPrfKey}.
+ *
+ * <p>This API is annotated with {@link com.google.crypto.tink.annotations.Alpha} because it is not
+ * yet stable and might change in the future.
+ */
+@Alpha
+public final class HmacPrfParameters extends PrfParameters {
+  private static final int MIN_KEY_SIZE = 16;
+
+  /** The Hash algorithm used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA1 = new HashType("SHA1");
+    public static final HashType SHA224 = new HashType("SHA224");
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builder for HmacPrfParameters. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+    @Nullable private HashType hashType = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) throws GeneralSecurityException {
+      if (keySizeBytes < MIN_KEY_SIZE) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size %d; only 128-bit or larger are supported", keySizeBytes * 8));
+      }
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHashType(HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    public HmacPrfParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("key size is not set");
+      }
+      if (hashType == null) {
+        throw new GeneralSecurityException("hash type is not set");
+      }
+      return new HmacPrfParameters(keySizeBytes, hashType);
+    }
+  }
+
+  private final int keySizeBytes;
+  private final HashType hashType;
+
+  private HmacPrfParameters(int keySizeBytes, HashType hashType) {
+    this.keySizeBytes = keySizeBytes;
+    this.hashType = hashType;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  public HashType getHashType() {
+    return hashType;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof HmacPrfParameters)) {
+      return false;
+    }
+    HmacPrfParameters that = (HmacPrfParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes() && that.getHashType() == getHashType();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(HmacPrfParameters.class, keySizeBytes, hashType);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "HMAC PRF Parameters (hashType: " + hashType + " and " + keySizeBytes + "-byte key)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfProtoSerialization.java
new file mode 100644
index 0000000..6e8c67f
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/HmacPrfProtoSerialization.java
@@ -0,0 +1,224 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link HmacPrfKey} objects and {@link HmacPrfParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class HmacPrfProtoSerialization {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.HmacPrfKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<HmacPrfParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              HmacPrfProtoSerialization::serializeParameters,
+              HmacPrfParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          HmacPrfProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<HmacPrfKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          HmacPrfProtoSerialization::serializeKey, HmacPrfKey.class, ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          HmacPrfProtoSerialization::parseKey, TYPE_URL_BYTES, ProtoKeySerialization.class);
+
+  private static HashType toProtoHashType(HmacPrfParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (HmacPrfParameters.HashType.SHA1.equals(hashType)) {
+      return HashType.SHA1;
+    }
+    if (HmacPrfParameters.HashType.SHA224.equals(hashType)) {
+      return HashType.SHA224;
+    }
+    if (HmacPrfParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (HmacPrfParameters.HashType.SHA384.equals(hashType)) {
+      return HashType.SHA384;
+    }
+    if (HmacPrfParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static HmacPrfParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA1:
+        return HmacPrfParameters.HashType.SHA1;
+      case SHA224:
+        return HmacPrfParameters.HashType.SHA224;
+      case SHA256:
+        return HmacPrfParameters.HashType.SHA256;
+      case SHA384:
+        return HmacPrfParameters.HashType.SHA384;
+      case SHA512:
+        return HmacPrfParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException("Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.HmacPrfParams getProtoParams(
+      HmacPrfParameters parameters) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+        .setHash(toProtoHashType(parameters.getHashType()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(HmacPrfParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.HmacPrfKeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      HmacPrfKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+            .setParams(getProtoParams(key.getParameters()))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        OutputPrefixType.RAW,
+        key.getIdRequirementOrNull());
+  }
+
+  private static HmacPrfParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HmacPrfProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.HmacPrfKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.HmacPrfKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HmacPrfParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException(
+          "Parsing HmacPrfParameters failed: unknown Version " + format.getVersion());
+    }
+    if (serialization.getKeyTemplate().getOutputPrefixType() != OutputPrefixType.RAW) {
+      throw new GeneralSecurityException(
+          "Parsing HmacPrfParameters failed: only RAW output prefix type is accepted");
+    }
+    return HmacPrfParameters.builder()
+        .setKeySizeBytes(format.getKeySize())
+        .setHashType(toHashType(format.getParams().getHash()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static HmacPrfKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to HmacPrfProtoSerialization.parseKey");
+    }
+    try {
+      com.google.crypto.tink.proto.HmacPrfKey protoKey =
+          com.google.crypto.tink.proto.HmacPrfKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      if (serialization.getIdRequirementOrNull() != null) {
+        throw new GeneralSecurityException("ID requirement must be null.");
+      }
+      HmacPrfParameters parameters =
+          HmacPrfParameters.builder()
+              .setKeySizeBytes(protoKey.getKeyValue().size())
+              .setHashType(toHashType(protoKey.getParams().getHash()))
+              .build();
+      return HmacPrfKey.builder()
+          .setParameters(parameters)
+          .setKeyBytes(
+              SecretBytes.copyFrom(
+                  protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)))
+          .build();
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing HmacPrfKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private HmacPrfProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/PredefinedPrfParameters.java b/java_src/src/main/java/com/google/crypto/tink/prf/PredefinedPrfParameters.java
new file mode 100644
index 0000000..a4caead
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/PredefinedPrfParameters.java
@@ -0,0 +1,92 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+/**
+ * Predefined {@link Parameters} for PRF-Keys.
+ *
+ * <p>Note: if you want to keep dependencies small, consider inlining the constants here.
+ */
+public final class PredefinedPrfParameters {
+
+  private PredefinedPrfParameters() {}
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HkdfPrfKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256
+   *   <li>HMAC key size: 32 bytes
+   *   <li>Salt: empty
+   * </ul>
+   */
+  public static final HkdfPrfParameters HKDF_SHA256 =
+      exceptionIsBug(
+          () ->
+              HkdfPrfParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setHashType(HkdfPrfParameters.HashType.SHA256)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HmacPrfKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256
+   *   <li>HMAC key size: 32 bytes
+   * </ul>
+   */
+  public static final HmacPrfParameters HMAC_SHA256_PRF =
+      exceptionIsBug(
+          () ->
+              HmacPrfParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setHashType(HmacPrfParameters.HashType.SHA256)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link HmacPrfKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA512
+   *   <li>HMAC key size: 64 bytes
+   * </ul>
+   */
+  public static final HmacPrfParameters HMAC_SHA512_PRF =
+      exceptionIsBug(
+          () ->
+              HmacPrfParameters.builder()
+                  .setKeySizeBytes(64)
+                  .setHashType(HmacPrfParameters.HashType.SHA512)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCmacKey} with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>HMAC key size: 32 bytes
+   * </ul>
+   */
+  public static final AesCmacPrfParameters AES_CMAC_PRF =
+      exceptionIsBug(() -> AesCmacPrfParameters.create(32));
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/PrfConfig.java b/java_src/src/main/java/com/google/crypto/tink/prf/PrfConfig.java
index dde0823..827da74 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/PrfConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/PrfConfig.java
@@ -25,6 +25,7 @@
  */
 public final class PrfConfig {
   public static final String PRF_TYPE_URL = new HkdfPrfKeyManager().getKeyType();
+  public static final String HMAC_PRF_TYPE_URL = new HmacPrfKeyManager().getKeyType();
 
   /**
    * Tries to register with the {@link Registry} all instances of {@link
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/PrfKey.java b/java_src/src/main/java/com/google/crypto/tink/prf/PrfKey.java
new file mode 100644
index 0000000..3dfa86b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/PrfKey.java
@@ -0,0 +1,26 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.Key;
+
+/** Represents functions to compute a PRF. */
+public abstract class PrfKey extends Key {
+  /** Returns the parameters of this key. */
+  @Override
+  public abstract PrfParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/PrfKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/prf/PrfKeyTemplates.java
index dc24f26..64bfe02 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/PrfKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/PrfKeyTemplates.java
@@ -28,8 +28,22 @@
 /**
  * Key templates for PRF-Keys.
  *
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("HKDF_SHA256")
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
+ * @deprecated Use PredefinedPrfParameters instead.
  */
 @Deprecated
 public final class PrfKeyTemplates {
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/PrfParameters.java b/java_src/src/main/java/com/google/crypto/tink/prf/PrfParameters.java
new file mode 100644
index 0000000..2f26722
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/PrfParameters.java
@@ -0,0 +1,24 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import com.google.crypto.tink.Parameters;
+import com.google.errorprone.annotations.Immutable;
+
+/** Represents a description of a {@link PrfKey} excluding the randomly chosen key material. */
+@Immutable
+public abstract class PrfParameters extends Parameters {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/prf/PrfSetWrapper.java b/java_src/src/main/java/com/google/crypto/tink/prf/PrfSetWrapper.java
index dc26890..3108300 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/PrfSetWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/PrfSetWrapper.java
@@ -18,6 +18,10 @@
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.PrimitiveWrapper;
 import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.internal.MonitoringUtil;
+import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.monitoring.MonitoringClient;
+import com.google.crypto.tink.monitoring.MonitoringKeysetInfo;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
@@ -34,6 +38,9 @@
  */
 @Immutable
 public class PrfSetWrapper implements PrimitiveWrapper<Prf, PrfSet> {
+
+  private static final PrfSetWrapper WRAPPER = new PrfSetWrapper();
+
   private static class WrappedPrfSet extends PrfSet {
     // This map is constructed using Collections.unmodifiableMap
     @SuppressWarnings("Immutable")
@@ -41,6 +48,33 @@
 
     private final int primaryKeyId;
 
+    @Immutable
+    private static class PrfWithMonitoring implements Prf {
+      private final Prf prf;
+      private final int keyId;
+
+      @SuppressWarnings("Immutable")
+      private final MonitoringClient.Logger logger;
+
+      @Override
+      public byte[] compute(byte[] input, int outputLength) throws GeneralSecurityException {
+        try {
+          byte[] output = prf.compute(input, outputLength);
+          logger.log(keyId, input.length);
+          return output;
+        } catch (GeneralSecurityException e) {
+          logger.logFailure();
+          throw e;
+        }
+      }
+
+      public PrfWithMonitoring(Prf prf, int keyId, MonitoringClient.Logger logger) {
+        this.prf = prf;
+        this.keyId = keyId;
+        this.logger = logger;
+      }
+    }
+
     private WrappedPrfSet(PrimitiveSet<Prf> primitives) throws GeneralSecurityException {
       if (primitives.getRawPrimitives().isEmpty()) {
         throw new GeneralSecurityException("No primitives provided.");
@@ -48,6 +82,14 @@
       if (primitives.getPrimary() == null) {
         throw new GeneralSecurityException("Primary key not set.");
       }
+      MonitoringClient.Logger logger;
+      if (primitives.hasAnnotations()) {
+        MonitoringClient client = MutableMonitoringRegistry.globalInstance().getMonitoringClient();
+        MonitoringKeysetInfo keysetInfo = MonitoringUtil.getMonitoringKeysetInfo(primitives);
+        logger = client.createLogger(keysetInfo, "prf", "compute");
+      } else {
+        logger = MonitoringUtil.DO_NOTHING_LOGGER;
+      }
 
       primaryKeyId = primitives.getPrimary().getKeyId();
       List<PrimitiveSet.Entry<Prf>> entries = primitives.getRawPrimitives();
@@ -58,7 +100,9 @@
               "Key " + entry.getKeyId() + " has non raw prefix type");
         }
         // Likewise, the key IDs of the PrfSet passed
-        mutablePrfMap.put(entry.getKeyId(), entry.getPrimitive());
+        mutablePrfMap.put(
+            entry.getKeyId(),
+            new PrfWithMonitoring(entry.getPrimitive(), entry.getKeyId(), logger));
       }
       keyIdToPrfMap = Collections.unmodifiableMap(mutablePrfMap);
     }
@@ -90,6 +134,6 @@
   }
 
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new PrfSetWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/signature/BUILD.bazel
index e6fc144..b51aede 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/BUILD.bazel
@@ -37,7 +37,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -57,6 +57,8 @@
         "//src/main/java/com/google/crypto/tink:pem_key_type",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util",
         "//src/main/java/com/google/crypto/tink/subtle:random",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -73,7 +75,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pss_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -81,6 +83,7 @@
     name = "ecdsa_sign_key_manager",
     srcs = ["EcdsaSignKeyManager.java"],
     deps = [
+        ":ecdsa_proto_serialization",
         ":ecdsa_verify_key_manager",
         "//proto:common_java_proto",
         "//proto:ecdsa_java_proto",
@@ -97,7 +100,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -140,7 +143,7 @@
         "//proto:rsa_ssa_pkcs1_java_proto",
         "//proto:rsa_ssa_pss_java_proto",
         "//proto:tink_java_proto",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -148,6 +151,7 @@
     name = "rsa_ssa_pkcs1_sign_key_manager",
     srcs = ["RsaSsaPkcs1SignKeyManager.java"],
     deps = [
+        ":rsa_ssa_pkcs1_proto_serialization",
         ":rsa_ssa_pkcs1_verify_key_manager",
         "//proto:common_java_proto",
         "//proto:rsa_ssa_pkcs1_java_proto",
@@ -164,7 +168,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -174,7 +178,6 @@
     deps = [
         ":public_key_sign_wrapper",
         "//src/main/java/com/google/crypto/tink:public_key_sign",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -210,6 +213,7 @@
     name = "rsa_ssa_pss_sign_key_manager",
     srcs = ["RsaSsaPssSignKeyManager.java"],
     deps = [
+        ":rsa_ssa_pss_proto_serialization",
         ":rsa_ssa_pss_verify_key_manager",
         "//proto:common_java_proto",
         "//proto:rsa_ssa_pss_java_proto",
@@ -225,7 +229,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -233,6 +237,7 @@
     name = "ed25519_private_key_manager",
     srcs = ["Ed25519PrivateKeyManager.java"],
     deps = [
+        ":ed25519_proto_serialization",
         ":ed25519_public_key_manager",
         "//proto:ed25519_java_proto",
         "//proto:tink_java_proto",
@@ -244,7 +249,7 @@
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_sign",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -262,7 +267,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -272,7 +277,6 @@
     deps = [
         ":public_key_verify_wrapper",
         "//src/main/java/com/google/crypto/tink:public_key_verify",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
 )
@@ -288,7 +292,324 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "signature_parameters",
+    srcs = ["SignatureParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "signature_private_key",
+    srcs = ["SignaturePrivateKey.java"],
+    deps = [
+        ":signature_parameters",
+        ":signature_public_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:private_key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "signature_public_key",
+    srcs = ["SignaturePublicKey.java"],
+    deps = [
+        ":signature_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ecdsa_parameters",
+    srcs = ["EcdsaParameters.java"],
+    deps = [
+        ":signature_parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ecdsa_public_key",
+    srcs = ["EcdsaPublicKey.java"],
+    deps = [
+        ":ecdsa_parameters",
+        ":signature_public_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ecdsa_private_key",
+    srcs = ["EcdsaPrivateKey.java"],
+    deps = [
+        ":ecdsa_parameters",
+        ":ecdsa_public_key",
+        ":signature_private_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ecdsa_proto_serialization",
+    srcs = ["EcdsaProtoSerialization.java"],
+    deps = [
+        ":ecdsa_parameters",
+        ":ecdsa_private_key",
+        ":ecdsa_public_key",
+        "//proto:common_java_proto",
+        "//proto:ecdsa_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pkcs1_parameters",
+    srcs = ["RsaSsaPkcs1Parameters.java"],
+    deps = [
+        ":signature_parameters",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pkcs1_public_key",
+    srcs = ["RsaSsaPkcs1PublicKey.java"],
+    deps = [
+        ":rsa_ssa_pkcs1_parameters",
+        ":signature_public_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pkcs1_private_key",
+    srcs = ["RsaSsaPkcs1PrivateKey.java"],
+    deps = [
+        ":rsa_ssa_pkcs1_parameters",
+        ":rsa_ssa_pkcs1_public_key",
+        ":signature_private_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pkcs1_proto_serialization",
+    srcs = ["RsaSsaPkcs1ProtoSerialization.java"],
+    deps = [
+        ":rsa_ssa_pkcs1_parameters",
+        ":rsa_ssa_pkcs1_private_key",
+        ":rsa_ssa_pkcs1_public_key",
+        "//proto:common_java_proto",
+        "//proto:rsa_ssa_pkcs1_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "ed25519_parameters",
+    srcs = ["Ed25519Parameters.java"],
+    deps = [
+        ":signature_parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ed25519_public_key",
+    srcs = ["Ed25519PublicKey.java"],
+    deps = [
+        ":ed25519_parameters",
+        ":signature_public_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ed25519_private_key",
+    srcs = ["Ed25519PrivateKey.java"],
+    deps = [
+        ":ed25519_parameters",
+        ":ed25519_public_key",
+        ":signature_private_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "ed25519_proto_serialization",
+    srcs = ["Ed25519ProtoSerialization.java"],
+    deps = [
+        ":ed25519_parameters",
+        ":ed25519_private_key",
+        ":ed25519_public_key",
+        "//proto:ed25519_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pss_parameters",
+    srcs = ["RsaSsaPssParameters.java"],
+    deps = [
+        ":signature_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pss_public_key",
+    srcs = ["RsaSsaPssPublicKey.java"],
+    deps = [
+        ":rsa_ssa_pss_parameters",
+        ":signature_public_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pss_private_key",
+    srcs = ["RsaSsaPssPrivateKey.java"],
+    deps = [
+        ":rsa_ssa_pss_parameters",
+        ":rsa_ssa_pss_public_key",
+        ":signature_private_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "rsa_ssa_pss_proto_serialization",
+    srcs = ["RsaSsaPssProtoSerialization.java"],
+    deps = [
+        ":rsa_ssa_pss_parameters",
+        ":rsa_ssa_pss_private_key",
+        ":rsa_ssa_pss_public_key",
+        "//proto:common_java_proto",
+        "//proto:rsa_ssa_pss_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -327,7 +648,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -347,6 +668,8 @@
         "//src/main/java/com/google/crypto/tink:pem_key_type-android",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -363,7 +686,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pss_verify_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -371,6 +694,7 @@
     name = "ecdsa_sign_key_manager-android",
     srcs = ["EcdsaSignKeyManager.java"],
     deps = [
+        ":ecdsa_proto_serialization-android",
         ":ecdsa_verify_key_manager-android",
         "//proto:common_java_proto_lite",
         "//proto:ecdsa_java_proto_lite",
@@ -387,7 +711,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -430,7 +754,7 @@
         "//proto:rsa_ssa_pkcs1_java_proto_lite",
         "//proto:rsa_ssa_pss_java_proto_lite",
         "//proto:tink_java_proto_lite",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -438,6 +762,7 @@
     name = "rsa_ssa_pkcs1_sign_key_manager-android",
     srcs = ["RsaSsaPkcs1SignKeyManager.java"],
     deps = [
+        ":rsa_ssa_pkcs1_proto_serialization-android",
         ":rsa_ssa_pkcs1_verify_key_manager-android",
         "//proto:common_java_proto_lite",
         "//proto:rsa_ssa_pkcs1_java_proto_lite",
@@ -454,7 +779,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -464,7 +789,6 @@
     deps = [
         ":public_key_sign_wrapper-android",
         "//src/main/java/com/google/crypto/tink:public_key_sign-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -500,6 +824,7 @@
     name = "rsa_ssa_pss_sign_key_manager-android",
     srcs = ["RsaSsaPssSignKeyManager.java"],
     deps = [
+        ":rsa_ssa_pss_proto_serialization-android",
         ":rsa_ssa_pss_verify_key_manager-android",
         "//proto:common_java_proto_lite",
         "//proto:rsa_ssa_pss_java_proto_lite",
@@ -515,7 +840,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:selfkeytests_validators-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -523,6 +848,7 @@
     name = "ed25519_private_key_manager-android",
     srcs = ["Ed25519PrivateKeyManager.java"],
     deps = [
+        ":ed25519_proto_serialization-android",
         ":ed25519_public_key_manager-android",
         "//proto:ed25519_java_proto_lite",
         "//proto:tink_java_proto_lite",
@@ -534,7 +860,7 @@
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager-android",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_sign-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -552,7 +878,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_verify_jce-android",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -562,7 +888,6 @@
     deps = [
         ":public_key_verify_wrapper-android",
         "//src/main/java/com/google/crypto/tink:public_key_verify-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
     ],
 )
@@ -578,6 +903,347 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory-android",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "signature_parameters-android",
+    srcs = ["SignatureParameters.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "signature_private_key-android",
+    srcs = ["SignaturePrivateKey.java"],
+    deps = [
+        ":signature_parameters-android",
+        ":signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink:private_key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "signature_public_key-android",
+    srcs = ["SignaturePublicKey.java"],
+    deps = [
+        ":signature_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ecdsa_parameters-android",
+    srcs = ["EcdsaParameters.java"],
+    deps = [
+        ":signature_parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ecdsa_public_key-android",
+    srcs = ["EcdsaPublicKey.java"],
+    deps = [
+        ":ecdsa_parameters-android",
+        ":signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ecdsa_private_key-android",
+    srcs = ["EcdsaPrivateKey.java"],
+    deps = [
+        ":ecdsa_parameters-android",
+        ":ecdsa_public_key-android",
+        ":signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ecdsa_proto_serialization-android",
+    srcs = ["EcdsaProtoSerialization.java"],
+    deps = [
+        ":ecdsa_parameters-android",
+        ":ecdsa_private_key-android",
+        ":ecdsa_public_key-android",
+        "//proto:common_java_proto_lite",
+        "//proto:ecdsa_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pkcs1_parameters-android",
+    srcs = ["RsaSsaPkcs1Parameters.java"],
+    deps = [
+        ":signature_parameters-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pkcs1_public_key-android",
+    srcs = ["RsaSsaPkcs1PublicKey.java"],
+    deps = [
+        ":rsa_ssa_pkcs1_parameters-android",
+        ":signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pkcs1_private_key-android",
+    srcs = ["RsaSsaPkcs1PrivateKey.java"],
+    deps = [
+        ":rsa_ssa_pkcs1_parameters-android",
+        ":rsa_ssa_pkcs1_public_key-android",
+        ":signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pkcs1_proto_serialization-android",
+    srcs = ["RsaSsaPkcs1ProtoSerialization.java"],
+    deps = [
+        ":rsa_ssa_pkcs1_parameters-android",
+        ":rsa_ssa_pkcs1_private_key-android",
+        ":rsa_ssa_pkcs1_public_key-android",
+        "//proto:common_java_proto_lite",
+        "//proto:rsa_ssa_pkcs1_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "ed25519_parameters-android",
+    srcs = ["Ed25519Parameters.java"],
+    deps = [
+        ":signature_parameters-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ed25519_public_key-android",
+    srcs = ["Ed25519PublicKey.java"],
+    deps = [
+        ":ed25519_parameters-android",
+        ":signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ed25519_private_key-android",
+    srcs = ["Ed25519PrivateKey.java"],
+    deps = [
+        ":ed25519_parameters-android",
+        ":ed25519_public_key-android",
+        ":signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "ed25519_proto_serialization-android",
+    srcs = ["Ed25519ProtoSerialization.java"],
+    deps = [
+        ":ed25519_parameters-android",
+        ":ed25519_private_key-android",
+        ":ed25519_public_key-android",
+        "//proto:ed25519_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pss_parameters-android",
+    srcs = ["RsaSsaPssParameters.java"],
+    deps = [
+        ":signature_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pss_public_key-android",
+    srcs = ["RsaSsaPssPublicKey.java"],
+    deps = [
+        ":rsa_ssa_pss_parameters-android",
+        ":signature_public_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pss_private_key-android",
+    srcs = ["RsaSsaPssPrivateKey.java"],
+    deps = [
+        ":rsa_ssa_pss_parameters-android",
+        ":rsa_ssa_pss_public_key-android",
+        ":signature_private_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "rsa_ssa_pss_proto_serialization-android",
+    srcs = ["RsaSsaPssProtoSerialization.java"],
+    deps = [
+        ":rsa_ssa_pss_parameters-android",
+        ":rsa_ssa_pss_private_key-android",
+        ":rsa_ssa_pss_public_key-android",
+        "//proto:common_java_proto_lite",
+        "//proto:rsa_ssa_pss_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "predefined_signature_parameters-android",
+    srcs = ["PredefinedSignatureParameters.java"],
+    deps = [
+        ":ecdsa_parameters-android",
+        ":ed25519_parameters-android",
+        ":rsa_ssa_pkcs1_parameters-android",
+        ":rsa_ssa_pss_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+    ],
+)
+
+java_library(
+    name = "predefined_signature_parameters",
+    srcs = ["PredefinedSignatureParameters.java"],
+    deps = [
+        ":ecdsa_parameters",
+        ":ed25519_parameters",
+        ":rsa_ssa_pkcs1_parameters",
+        ":rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
     ],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaParameters.java b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaParameters.java
new file mode 100644
index 0000000..12dcbb6
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaParameters.java
@@ -0,0 +1,276 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECParameterSpec;
+import java.util.Objects;
+
+/**
+ * Describes the parameters of an ECDSA signature primitive.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+public final class EcdsaParameters extends SignatureParameters {
+  /**
+   * Describes details of the ECDSA signature computation.
+   *
+   * <p>The standard ECDSA key is used for variant "NO_PREFIX". Other variants slightly change how
+   * the signature is computed, or add a prefix to every computation depending on the key id.
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant LEGACY = new Variant("LEGACY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The encoding used in the signature. */
+  @Immutable
+  public static final class SignatureEncoding {
+    public static final SignatureEncoding IEEE_P1363 = new SignatureEncoding("IEEE_P1363");
+    public static final SignatureEncoding DER = new SignatureEncoding("DER");
+
+    private final String name;
+
+    private SignatureEncoding(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The elliptic curve and its parameters. */
+  @Immutable
+  public static final class CurveType {
+    public static final CurveType NIST_P256 =
+        new CurveType("NIST_P256", EllipticCurvesUtil.NIST_P256_PARAMS);
+    public static final CurveType NIST_P384 =
+        new CurveType("NIST_P384", EllipticCurvesUtil.NIST_P384_PARAMS);
+    public static final CurveType NIST_P521 =
+        new CurveType("NIST_P521", EllipticCurvesUtil.NIST_P521_PARAMS);
+
+    private final String name;
+    @SuppressWarnings("Immutable") // ECParameterSpec is immutable
+    private final ECParameterSpec spec;
+
+    private CurveType(String name, ECParameterSpec spec) {
+      this.name = name;
+      this.spec = spec;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+
+    public ECParameterSpec toParameterSpec() {
+      return spec;
+    }
+
+    public static CurveType fromParameterSpec(ECParameterSpec spec)
+        throws GeneralSecurityException {
+      if (EllipticCurvesUtil.isSameEcParameterSpec(spec, NIST_P256.toParameterSpec())) {
+        return NIST_P256;
+      }
+      if (EllipticCurvesUtil.isSameEcParameterSpec(spec, NIST_P384.toParameterSpec())) {
+        return NIST_P384;
+      }
+      if (EllipticCurvesUtil.isSameEcParameterSpec(spec, NIST_P521.toParameterSpec())) {
+        return NIST_P521;
+      }
+      throw new GeneralSecurityException("unknown ECParameterSpec");
+    }
+  }
+
+  /** The Hash algorithm used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Builds a new EcdsaParameters instance. */
+  public static final class Builder {
+    private SignatureEncoding signatureEncoding = null;
+    private CurveType curveType = null;
+    private HashType hashType = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setSignatureEncoding(SignatureEncoding signatureEncoding) {
+      this.signatureEncoding = signatureEncoding;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setCurveType(CurveType curveType) {
+      this.curveType = curveType;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHashType(HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    public EcdsaParameters build() throws GeneralSecurityException {
+      if (signatureEncoding == null) {
+        throw new GeneralSecurityException("signature encoding is not set");
+      }
+      if (curveType == null) {
+        throw new GeneralSecurityException("EC curve type is not set");
+      }
+      if (hashType == null) {
+        throw new GeneralSecurityException("hash type is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("variant is not set");
+      }
+
+      if (curveType == CurveType.NIST_P256) {
+        if (hashType != HashType.SHA256) {
+          throw new GeneralSecurityException("NIST_P256 requires SHA256");
+        }
+      }
+      if (curveType == CurveType.NIST_P384) {
+        if (hashType != HashType.SHA384 && hashType != HashType.SHA512) {
+          throw new GeneralSecurityException("NIST_P384 requires SHA384 or SHA512");
+        }
+      }
+      if (curveType == CurveType.NIST_P521) {
+        if (hashType != HashType.SHA512) {
+          throw new GeneralSecurityException("NIST_P521 requires SHA512");
+        }
+      }
+      return new EcdsaParameters(signatureEncoding, curveType, hashType, variant);
+    }
+  }
+
+  private final SignatureEncoding signatureEncoding;
+  private final CurveType curveType;
+  private final HashType hashType;
+  private final Variant variant;
+
+  private EcdsaParameters(
+      SignatureEncoding signatureEncoding,
+      CurveType curveType,
+      HashType hashType,
+      Variant variant) {
+    this.signatureEncoding = signatureEncoding;
+    this.curveType = curveType;
+    this.hashType = hashType;
+    this.variant = variant;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public SignatureEncoding getSignatureEncoding() {
+    return signatureEncoding;
+  }
+
+  public CurveType getCurveType() {
+    return curveType;
+  }
+
+  public HashType getHashType() {
+    return hashType;
+  }
+
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof EcdsaParameters)) {
+      return false;
+    }
+    EcdsaParameters that = (EcdsaParameters) o;
+    return that.getSignatureEncoding() == getSignatureEncoding()
+        && that.getCurveType() == getCurveType()
+        && that.getHashType() == getHashType()
+        && that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(EcdsaParameters.class, signatureEncoding, curveType, hashType, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "ECDSA Parameters (variant: "
+        + variant
+        + ", hashType: "
+        + hashType
+        + ", encoding: "
+        + signatureEncoding
+        + ", curve: "
+        + curveType
+        + ")";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaPrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaPrivateKey.java
new file mode 100644
index 0000000..8319ebf
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaPrivateKey.java
@@ -0,0 +1,136 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+
+/**
+ * Represents a key for computing ECDSA signatures.
+ *
+ * <p>ECDSA is defined in http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, section 6.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class EcdsaPrivateKey extends SignaturePrivateKey {
+  private final EcdsaPublicKey publicKey;
+  private final SecretBigInteger privateValue;
+
+  /** Builder for EcdsaPrivateKey. */
+  public static class Builder {
+    private EcdsaPublicKey publicKey = null;
+    private SecretBigInteger privateValue = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setPublicKey(EcdsaPublicKey publicKey) {
+      this.publicKey = publicKey;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setPrivateValue(SecretBigInteger privateValue) {
+      this.privateValue = privateValue;
+      return this;
+    }
+
+    private static void validatePrivateValue(
+        BigInteger privateValue, ECPoint publicPoint, EcdsaParameters.CurveType curveType)
+        throws GeneralSecurityException {
+      BigInteger order = curveType.toParameterSpec().getOrder();
+      if ((privateValue.signum() <= 0) || (privateValue.compareTo(order) >= 0)) {
+        throw new GeneralSecurityException("Invalid private value");
+      }
+      ECPoint p = EllipticCurvesUtil.multiplyByGenerator(privateValue, curveType.toParameterSpec());
+      if (!p.equals(publicPoint)) {
+        throw new GeneralSecurityException("Invalid private value");
+      }
+    }
+
+    @AccessesPartialKey
+    public EcdsaPrivateKey build() throws GeneralSecurityException {
+      if (publicKey == null) {
+        throw new GeneralSecurityException("Cannot build without a ecdsa public key");
+      }
+      if (privateValue == null) {
+        throw new GeneralSecurityException("Cannot build without a private value");
+      }
+      validatePrivateValue(
+          privateValue.getBigInteger(InsecureSecretKeyAccess.get()),
+          publicKey.getPublicPoint(),
+          publicKey.getParameters().getCurveType());
+      return new EcdsaPrivateKey(publicKey, privateValue);
+    }
+  }
+
+  private EcdsaPrivateKey(EcdsaPublicKey publicKey, SecretBigInteger privateValue) {
+    this.publicKey = publicKey;
+    this.privateValue = privateValue;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public EcdsaParameters getParameters() {
+    return publicKey.getParameters();
+  }
+
+  @Override
+  public EcdsaPublicKey getPublicKey() {
+    return publicKey;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBigInteger getPrivateValue() {
+    return privateValue;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof EcdsaPrivateKey)) {
+      return false;
+    }
+    EcdsaPrivateKey that = (EcdsaPrivateKey) o;
+    return that.publicKey.equalsKey(publicKey)
+        && privateValue.equalsSecretBigInteger(that.privateValue);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaProtoSerialization.java
new file mode 100644
index 0000000..a0076d1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaProtoSerialization.java
@@ -0,0 +1,422 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.EcdsaSignatureEncoding;
+import com.google.crypto.tink.proto.EllipticCurveType;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link EcdsaPrivateKey} and {@link EcdsaPublicKey} objects and
+ * {@link EcdsaParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class EcdsaProtoSerialization {
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
+  private static final Bytes PRIVATE_TYPE_URL_BYTES = toBytesFromPrintableAscii(PRIVATE_TYPE_URL);
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.EcdsaPublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
+  private static final ParametersSerializer<EcdsaParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              EcdsaProtoSerialization::serializeParameters,
+              EcdsaParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          EcdsaProtoSerialization::parseParameters,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<EcdsaPublicKey, ProtoKeySerialization> PUBLIC_KEY_SERIALIZER =
+      KeySerializer.create(
+          EcdsaProtoSerialization::serializePublicKey,
+          EcdsaPublicKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          EcdsaProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final KeySerializer<EcdsaPrivateKey, ProtoKeySerialization>
+      PRIVATE_KEY_SERIALIZER =
+          KeySerializer.create(
+              EcdsaProtoSerialization::serializePrivateKey,
+              EcdsaPrivateKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PRIVATE_KEY_PARSER =
+      KeyParser.create(
+          EcdsaProtoSerialization::parsePrivateKey,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(EcdsaParameters.Variant variant)
+      throws GeneralSecurityException {
+    if (EcdsaParameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (EcdsaParameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (EcdsaParameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    if (EcdsaParameters.Variant.LEGACY.equals(variant)) {
+      return OutputPrefixType.LEGACY;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static HashType toProtoHashType(EcdsaParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (EcdsaParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (EcdsaParameters.HashType.SHA384.equals(hashType)) {
+      return HashType.SHA384;
+    }
+    if (EcdsaParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static EcdsaParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA256:
+        return EcdsaParameters.HashType.SHA256;
+      case SHA384:
+        return EcdsaParameters.HashType.SHA384;
+      case SHA512:
+        return EcdsaParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static EcdsaParameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return EcdsaParameters.Variant.TINK;
+      case CRUNCHY:
+        return EcdsaParameters.Variant.CRUNCHY;
+      case LEGACY:
+        return EcdsaParameters.Variant.LEGACY;
+      case RAW:
+        return EcdsaParameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static EllipticCurveType toProtoCurveType(EcdsaParameters.CurveType curveType)
+      throws GeneralSecurityException {
+    if (EcdsaParameters.CurveType.NIST_P256.equals(curveType)) {
+      return EllipticCurveType.NIST_P256;
+    }
+    if (EcdsaParameters.CurveType.NIST_P384.equals(curveType)) {
+      return EllipticCurveType.NIST_P384;
+    }
+    if (EcdsaParameters.CurveType.NIST_P521.equals(curveType)) {
+      return EllipticCurveType.NIST_P521;
+    }
+    throw new GeneralSecurityException("Unable to serialize CurveType " + curveType);
+  }
+
+  private static int getEncodingLength(EcdsaParameters.CurveType curveType)
+      throws GeneralSecurityException {
+    // We currently encode with one extra 0 byte at the beginning, to make sure
+    // that parsing is correct even if passing of a two's complement encoding is used.
+    // See also b/264525021.
+    if (EcdsaParameters.CurveType.NIST_P256.equals(curveType)) {
+      return 33;
+    }
+    if (EcdsaParameters.CurveType.NIST_P384.equals(curveType)) {
+      return 49;
+    }
+    if (EcdsaParameters.CurveType.NIST_P521.equals(curveType)) {
+      return 67;
+    }
+    throw new GeneralSecurityException("Unable to serialize CurveType " + curveType);
+  }
+
+  private static EcdsaParameters.CurveType toCurveType(EllipticCurveType protoCurveType)
+      throws GeneralSecurityException {
+    switch (protoCurveType) {
+      case NIST_P256:
+        return EcdsaParameters.CurveType.NIST_P256;
+      case NIST_P384:
+        return EcdsaParameters.CurveType.NIST_P384;
+      case NIST_P521:
+        return EcdsaParameters.CurveType.NIST_P521;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse EllipticCurveType: " + protoCurveType.getNumber());
+    }
+  }
+
+  private static EcdsaSignatureEncoding toProtoSignatureEncoding(
+      EcdsaParameters.SignatureEncoding encoding) throws GeneralSecurityException {
+    if (EcdsaParameters.SignatureEncoding.IEEE_P1363.equals(encoding)) {
+      return EcdsaSignatureEncoding.IEEE_P1363;
+    }
+    if (EcdsaParameters.SignatureEncoding.DER.equals(encoding)) {
+      return EcdsaSignatureEncoding.DER;
+    }
+    throw new GeneralSecurityException("Unable to serialize SignatureEncoding " + encoding);
+  }
+
+  private static EcdsaParameters.SignatureEncoding toSignatureEncoding(
+      EcdsaSignatureEncoding encoding) throws GeneralSecurityException {
+    switch (encoding) {
+      case IEEE_P1363:
+        return EcdsaParameters.SignatureEncoding.IEEE_P1363;
+      case DER:
+        return EcdsaParameters.SignatureEncoding.DER;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse EcdsaSignatureEncoding: " + encoding.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.EcdsaParams getProtoParams(EcdsaParameters parameters)
+      throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.EcdsaParams.newBuilder()
+        .setHashType(toProtoHashType(parameters.getHashType()))
+        .setCurve(toProtoCurveType(parameters.getCurveType()))
+        .setEncoding(toProtoSignatureEncoding(parameters.getSignatureEncoding()))
+        .build();
+  }
+
+  private static com.google.crypto.tink.proto.EcdsaPublicKey getProtoPublicKey(EcdsaPublicKey key)
+      throws GeneralSecurityException {
+    int encLength = getEncodingLength(key.getParameters().getCurveType());
+    ECPoint publicPoint = key.getPublicPoint();
+    return com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+        .setParams(getProtoParams(key.getParameters()))
+        .setX(
+            ByteString.copyFrom(
+                BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                    publicPoint.getAffineX(), encLength)))
+        .setY(
+            ByteString.copyFrom(
+                BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                    publicPoint.getAffineY(), encLength)))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(EcdsaParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(PRIVATE_TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializePublicKey(
+      EcdsaPublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        getProtoPublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static ProtoKeySerialization serializePrivateKey(
+      EcdsaPrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    int encLength = getEncodingLength(key.getParameters().getCurveType());
+    return ProtoKeySerialization.create(
+        PRIVATE_TYPE_URL,
+        com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+            .setPublicKey(getProtoPublicKey(key.getPublicKey()))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                        key.getPrivateValue().getBigInteger(SecretKeyAccess.requireAccess(access)),
+                        encLength)))
+            .build()
+            .toByteString(),
+        KeyMaterialType.ASYMMETRIC_PRIVATE,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static EcdsaParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to EcdsaProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.EcdsaKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.EcdsaKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing EcdsaParameters failed: ", e);
+    }
+    return EcdsaParameters.builder()
+        .setHashType(toHashType(format.getParams().getHashType()))
+        .setSignatureEncoding(toSignatureEncoding(format.getParams().getEncoding()))
+        .setCurveType(toCurveType(format.getParams().getCurve()))
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static EcdsaPublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to EcdsaProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.EcdsaPublicKey protoKey =
+          com.google.crypto.tink.proto.EcdsaPublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      EcdsaParameters parameters =
+          EcdsaParameters.builder()
+              .setHashType(toHashType(protoKey.getParams().getHashType()))
+              .setSignatureEncoding(toSignatureEncoding(protoKey.getParams().getEncoding()))
+              .setCurveType(toCurveType(protoKey.getParams().getCurve()))
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return EcdsaPublicKey.builder()
+          .setParameters(parameters)
+          .setPublicPoint(
+              new ECPoint(
+                  BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getX().toByteArray()),
+                  BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getY().toByteArray())))
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing EcdsaPublicKey failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static EcdsaPrivateKey parsePrivateKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to EcdsaProtoSerialization.parsePrivateKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.EcdsaPrivateKey protoKey =
+          com.google.crypto.tink.proto.EcdsaPrivateKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKey = protoKey.getPublicKey();
+      EcdsaParameters parameters =
+          EcdsaParameters.builder()
+              .setHashType(toHashType(protoPublicKey.getParams().getHashType()))
+              .setSignatureEncoding(toSignatureEncoding(protoPublicKey.getParams().getEncoding()))
+              .setCurveType(toCurveType(protoPublicKey.getParams().getCurve()))
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      EcdsaPublicKey publicKey =
+          EcdsaPublicKey.builder()
+              .setParameters(parameters)
+              .setPublicPoint(
+                  new ECPoint(
+                      BigIntegerEncoding.fromUnsignedBigEndianBytes(
+                          protoPublicKey.getX().toByteArray()),
+                      BigIntegerEncoding.fromUnsignedBigEndianBytes(
+                          protoPublicKey.getY().toByteArray())))
+              .setIdRequirement(serialization.getIdRequirementOrNull())
+              .build();
+      return EcdsaPrivateKey.builder()
+          .setPublicKey(publicKey)
+          .setPrivateValue(
+              SecretBigInteger.fromBigInteger(
+                  BigIntegerEncoding.fromUnsignedBigEndianBytes(
+                      protoKey.getKeyValue().toByteArray()),
+                  SecretKeyAccess.requireAccess(access)))
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing EcdsaPrivateKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
+    registry.registerKeySerializer(PRIVATE_KEY_SERIALIZER);
+    registry.registerKeyParser(PRIVATE_KEY_PARSER);
+  }
+
+  private EcdsaProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaPublicKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaPublicKey.java
new file mode 100644
index 0000000..0cb3ef1
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaPublicKey.java
@@ -0,0 +1,167 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * EcdsaPublicKey represents the public portion of ECDSA signature primitive.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class EcdsaPublicKey extends SignaturePublicKey {
+  private final EcdsaParameters parameters;
+  @SuppressWarnings("Immutable") // ECPoint is immutable
+  private final ECPoint publicPoint;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for EcdsaPublicKey. */
+  public static class Builder {
+    @Nullable private EcdsaParameters parameters = null;
+    @Nullable private ECPoint publicPoint = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(EcdsaParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setPublicPoint(ECPoint publicPoint) {
+      this.publicPoint = publicPoint;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == EcdsaParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == EcdsaParameters.Variant.LEGACY
+          || parameters.getVariant() == EcdsaParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == EcdsaParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown EcdsaParameters.Variant: " + parameters.getVariant());
+    }
+
+    public EcdsaPublicKey build() throws GeneralSecurityException {
+      if (parameters == null) {
+        throw new GeneralSecurityException("Cannot build without parameters");
+      }
+      if (publicPoint == null) {
+        throw new GeneralSecurityException("Cannot build without public point");
+      }
+      EllipticCurvesUtil.checkPointOnCurve(
+          publicPoint, parameters.getCurveType().toParameterSpec().getCurve());
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new EcdsaPublicKey(parameters, publicPoint, outputPrefix, idRequirement);
+    }
+  }
+
+  private EcdsaPublicKey(
+      EcdsaParameters parameters,
+      ECPoint publicPoint,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.publicPoint = publicPoint;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public ECPoint getPublicPoint() {
+    return publicPoint;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public EcdsaParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof EcdsaPublicKey)) {
+      return false;
+    }
+    EcdsaPublicKey that = (EcdsaPublicKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.publicPoint.equals(publicPoint)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaSignKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaSignKeyManager.java
index 51a2b52..6952876 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaSignKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/EcdsaSignKeyManager.java
@@ -259,6 +259,7 @@
   public static void registerPair(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerAsymmetricKeyManagers(
         new EcdsaSignKeyManager(), new EcdsaVerifyKeyManager(), newKeyAllowed);
+    EcdsaProtoSerialization.register();
   }
 
   /**
@@ -270,10 +271,7 @@
    *       <li>Signature encoding: DER (this is the encoding that Java uses).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}.
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("ECDSA_P256")}
    */
-  @Deprecated
   public static final KeyTemplate ecdsaP256Template() {
     return createKeyTemplate(
         HashType.SHA256,
@@ -293,9 +291,7 @@
    *     </ul>
    *     Keys generated from this template create raw signatures of exactly 64 bytes. It is
    *     compatible with JWS and most other libraries.
-   * @deprecated use {@code KeyTemplates.get("ECDSA_P256_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawEcdsaP256Template() {
     return createKeyTemplate(
         HashType.SHA256,
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519Parameters.java b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519Parameters.java
new file mode 100644
index 0000000..5ede453
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519Parameters.java
@@ -0,0 +1,93 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.errorprone.annotations.Immutable;
+import java.util.Objects;
+
+/** This class describes the parameters of an {@link Ed25519Key}. */
+public final class Ed25519Parameters extends SignatureParameters {
+  /**
+   * An enum-like class with constant instances, which explains how the prefix is computed.
+   *
+   * <p>The standard Ed25519 key is used for variant "NO_PREFIX". Other variants slightly change how
+   * the signature is computed, or add a prefix to every computation depending on the key id.
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant LEGACY = new Variant("LEGACY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Creates an instance with NO_PREFIX variant. */
+  public static Ed25519Parameters create() {
+    return new Ed25519Parameters(Variant.NO_PREFIX);
+  }
+
+  /** Creates an instance with given variant. */
+  public static Ed25519Parameters create(Variant variant) {
+    return new Ed25519Parameters(variant);
+  }
+
+  private final Variant variant;
+
+  private Ed25519Parameters(Variant variant) {
+    this.variant = variant;
+  }
+
+  /** Returns a variant object. */
+  public Variant getVariant() {
+    return variant;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof Ed25519Parameters)) {
+      return false;
+    }
+    Ed25519Parameters that = (Ed25519Parameters) o;
+    return that.getVariant() == getVariant();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(Ed25519Parameters.class, variant);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "Ed25519 Parameters (variant: " + variant + ")";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKey.java
new file mode 100644
index 0000000..60670b3
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKey.java
@@ -0,0 +1,106 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.Ed25519;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * The key for computing Ed25519 signatures.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class Ed25519PrivateKey extends SignaturePrivateKey {
+  private final Ed25519PublicKey publicKey;
+  private final SecretBytes privateKeyBytes;
+
+  private Ed25519PrivateKey(Ed25519PublicKey publicKey, SecretBytes privateKeyBytes) {
+    this.publicKey = publicKey;
+    this.privateKeyBytes = privateKeyBytes;
+  }
+
+  @AccessesPartialKey
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Ed25519PrivateKey create(Ed25519PublicKey publicKey, SecretBytes privateKeyBytes)
+      throws GeneralSecurityException {
+    if (publicKey == null) {
+      throw new GeneralSecurityException(
+          "Ed25519 key cannot be constructed without an Ed25519 public key");
+    }
+    if (privateKeyBytes.size() != 32) {
+      throw new GeneralSecurityException(
+          "Ed25519 key must be constructed with key of length 32 bytes, not "
+              + privateKeyBytes.size());
+    }
+
+    // Validate private key based on the public key bytes.
+    byte[] publicKeyBytes = publicKey.getPublicKeyBytes().toByteArray();
+    byte[] secretSeed = privateKeyBytes.toByteArray(InsecureSecretKeyAccess.get());
+    byte[] expectedPublicKeyBytes =
+        Ed25519.scalarMultWithBaseToBytes(Ed25519.getHashedScalar(secretSeed));
+
+    if (!Arrays.equals(publicKeyBytes, expectedPublicKeyBytes)) {
+      throw new GeneralSecurityException("Ed25519 keys mismatch");
+    }
+
+    return new Ed25519PrivateKey(publicKey, privateKeyBytes);
+  }
+
+  @Override
+  public Ed25519Parameters getParameters() {
+    return publicKey.getParameters();
+  }
+
+  @Override
+  public Ed25519PublicKey getPublicKey() {
+    return publicKey;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getPrivateKeyBytes() {
+    return privateKeyBytes;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof Ed25519PrivateKey)) {
+      return false;
+    }
+    Ed25519PrivateKey that = (Ed25519PrivateKey) o;
+    return that.publicKey.equalsKey(publicKey)
+        && privateKeyBytes.equalsSecretBytes(that.privateKeyBytes);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManager.java
index 08576ba..2ae7ea4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManager.java
@@ -126,10 +126,7 @@
 
         byte[] pseudorandomness = new byte[Ed25519Sign.SECRET_KEY_LEN];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != Ed25519Sign.SECRET_KEY_LEN) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           Ed25519Sign.KeyPair keyPair = Ed25519Sign.KeyPair.newKeyPairFromSeed(pseudorandomness);
           Ed25519PublicKey publicKey =
               Ed25519PublicKey.newBuilder()
@@ -177,13 +174,12 @@
   public static void registerPair(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerAsymmetricKeyManagers(
         new Ed25519PrivateKeyManager(), new Ed25519PublicKeyManager(), newKeyAllowed);
+    Ed25519ProtoSerialization.register();
   }
 
   /**
    * @return A {@link KeyTemplate} that generates new instances of ED25519 keys.
-   * @deprecated use {@code KeyTemplates.get("ED25519")}
    */
-  @Deprecated
   public static final KeyTemplate ed25519Template() {
     return KeyTemplate.create(
         new Ed25519PrivateKeyManager().getKeyType(),
@@ -195,9 +191,7 @@
    * @return A {@link KeyTemplate} that generates new instances of Ed25519 keys. Keys generated from
    *     this template creates raw signatures of exactly 64 bytes. It's compatible with most other
    *     libraries.
-   * @deprecated use {@code KeyTemplates.get("ED25519_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawEd25519Template() {
     return KeyTemplate.create(
         new Ed25519PrivateKeyManager().getKeyType(),
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519ProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519ProtoSerialization.java
new file mode 100644
index 0000000..091793c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519ProtoSerialization.java
@@ -0,0 +1,254 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.EnumTypeProtoConverter;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.Ed25519KeyFormat;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link Ed25519PrivateKey} and {@link Ed25519PublicKey} objects and
+ * {@link Ed25519Parameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class Ed25519ProtoSerialization {
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey";
+  private static final Bytes PRIVATE_TYPE_URL_BYTES = toBytesFromPrintableAscii(PRIVATE_TYPE_URL);
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.Ed25519PublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
+  private static final ParametersSerializer<Ed25519Parameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              Ed25519ProtoSerialization::serializeParameters,
+              Ed25519Parameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          Ed25519ProtoSerialization::parseParameters,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<Ed25519PublicKey, ProtoKeySerialization>
+      PUBLIC_KEY_SERIALIZER =
+          KeySerializer.create(
+              Ed25519ProtoSerialization::serializePublicKey,
+              Ed25519PublicKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          Ed25519ProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final KeySerializer<Ed25519PrivateKey, ProtoKeySerialization>
+      PRIVATE_KEY_SERIALIZER =
+          KeySerializer.create(
+              Ed25519ProtoSerialization::serializePrivateKey,
+              Ed25519PrivateKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PRIVATE_KEY_PARSER =
+      KeyParser.create(
+          Ed25519ProtoSerialization::parsePrivateKey,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final EnumTypeProtoConverter<OutputPrefixType, Ed25519Parameters.Variant>
+      VARIANT_CONVERTER =
+          EnumTypeProtoConverter.<OutputPrefixType, Ed25519Parameters.Variant>builder()
+              .add(OutputPrefixType.RAW, Ed25519Parameters.Variant.NO_PREFIX)
+              .add(OutputPrefixType.TINK, Ed25519Parameters.Variant.TINK)
+              .add(OutputPrefixType.CRUNCHY, Ed25519Parameters.Variant.CRUNCHY)
+              .add(OutputPrefixType.LEGACY, Ed25519Parameters.Variant.LEGACY)
+              .build();
+
+  /**
+   * Registers previously defined parser/serializer objects into a global, mutable registry.
+   * Registration is public to enable custom configurations.
+   */
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  /** Registers previously defined parser/serializer objects into a given registry. */
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
+    registry.registerKeySerializer(PRIVATE_KEY_SERIALIZER);
+    registry.registerKeyParser(PRIVATE_KEY_PARSER);
+  }
+
+  private static com.google.crypto.tink.proto.Ed25519PublicKey getProtoPublicKey(
+      Ed25519PublicKey key) {
+    return com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+        .setKeyValue(ByteString.copyFrom(key.getPublicKeyBytes().toByteArray()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(Ed25519Parameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(PRIVATE_TYPE_URL)
+            .setValue(Ed25519KeyFormat.getDefaultInstance().toByteString())
+            .setOutputPrefixType(VARIANT_CONVERTER.toProtoEnum(parameters.getVariant()))
+            .build());
+  }
+
+  /**
+   * Returns the proto serialization of a {@link Ed25519PublicKey}.
+   *
+   * @param access may be null for public key material
+   * @throws GeneralSecurityException if the key cannot be serialized (e.g. unknown variant)
+   */
+  private static ProtoKeySerialization serializePublicKey(
+      Ed25519PublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        getProtoPublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        VARIANT_CONVERTER.toProtoEnum(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static ProtoKeySerialization serializePrivateKey(
+      Ed25519PrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PRIVATE_TYPE_URL,
+        com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+            .setPublicKey(getProtoPublicKey(key.getPublicKey()))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getPrivateKeyBytes().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .build()
+            .toByteString(),
+        KeyMaterialType.ASYMMETRIC_PRIVATE,
+        VARIANT_CONVERTER.toProtoEnum(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static Ed25519Parameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to Ed25519ProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    // Check that serialization.getKeyTemplate().getValue() is a proto-encoded string of version 0.
+    try {
+      Ed25519KeyFormat format =
+          Ed25519KeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (format.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing Ed25519Parameters failed: ", e);
+    }
+    return Ed25519Parameters.create(
+        VARIANT_CONVERTER.fromProtoEnum(serialization.getKeyTemplate().getOutputPrefixType()));
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static Ed25519PublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to Ed25519ProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.Ed25519PublicKey protoKey =
+          com.google.crypto.tink.proto.Ed25519PublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+
+      return Ed25519PublicKey.create(
+          VARIANT_CONVERTER.fromProtoEnum(serialization.getOutputPrefixType()),
+          Bytes.copyFrom(protoKey.getKeyValue().toByteArray()),
+          serialization.getIdRequirementOrNull());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing Ed25519PublicKey failed");
+    }
+  }
+
+  @SuppressWarnings("UnusedException") // Prevents leaking key material
+  private static Ed25519PrivateKey parsePrivateKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to Ed25519ProtoSerialization.parsePrivateKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.Ed25519PrivateKey protoKey =
+          com.google.crypto.tink.proto.Ed25519PrivateKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      com.google.crypto.tink.proto.Ed25519PublicKey protoPublicKey = protoKey.getPublicKey();
+      Ed25519PublicKey publicKey =
+          Ed25519PublicKey.create(
+              VARIANT_CONVERTER.fromProtoEnum(serialization.getOutputPrefixType()),
+              Bytes.copyFrom(protoPublicKey.getKeyValue().toByteArray()),
+              serialization.getIdRequirementOrNull());
+
+      return Ed25519PrivateKey.create(
+          publicKey,
+          SecretBytes.copyFrom(
+              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)));
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing Ed25519PrivateKey failed");
+    }
+  }
+
+  private Ed25519ProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PublicKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PublicKey.java
new file mode 100644
index 0000000..6ac743b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/Ed25519PublicKey.java
@@ -0,0 +1,146 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Ed25519PublicKey represents the public portion of the Ed25519 signature primitive.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+@Immutable
+public final class Ed25519PublicKey extends SignaturePublicKey {
+  private final Ed25519Parameters parameters;
+  private final Bytes publicKeyBytes;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  private Ed25519PublicKey(
+      Ed25519Parameters parameters,
+      Bytes publicKeyBytes,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.publicKeyBytes = publicKeyBytes;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  private static Bytes createOutputPrefix(
+      Ed25519Parameters parameters, @Nullable Integer idRequirement) {
+    if (parameters.getVariant() == Ed25519Parameters.Variant.NO_PREFIX) {
+      return Bytes.copyFrom(new byte[] {});
+    }
+    if (parameters.getVariant() == Ed25519Parameters.Variant.CRUNCHY
+        || parameters.getVariant() == Ed25519Parameters.Variant.LEGACY) {
+      return Bytes.copyFrom(
+          ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement.intValue()).array());
+    }
+    if (parameters.getVariant() == Ed25519Parameters.Variant.TINK) {
+      return Bytes.copyFrom(
+          ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement.intValue()).array());
+    }
+    throw new IllegalStateException("Unknown Variant: " + parameters.getVariant());
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  @AccessesPartialKey
+  public static Ed25519PublicKey create(Bytes publicKeyBytes) throws GeneralSecurityException {
+    return create(Ed25519Parameters.Variant.NO_PREFIX, publicKeyBytes, null);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Ed25519PublicKey create(
+      Ed25519Parameters.Variant variant, Bytes publicKeyBytes, @Nullable Integer idRequirement)
+      throws GeneralSecurityException {
+    Ed25519Parameters parameters = Ed25519Parameters.create(variant);
+    if (!variant.equals(Ed25519Parameters.Variant.NO_PREFIX) && idRequirement == null) {
+      throw new GeneralSecurityException(
+          "For given Variant " + variant + " the value of idRequirement must be non-null");
+      }
+
+    if (variant.equals(Ed25519Parameters.Variant.NO_PREFIX) && idRequirement != null) {
+      throw new GeneralSecurityException(
+          "For given Variant NO_PREFIX the value of idRequirement must be null");
+    }
+    if (publicKeyBytes.size() != 32) {
+      throw new GeneralSecurityException(
+          "Ed25519 key must be constructed with key of length 32 bytes, not "
+              + publicKeyBytes.size());
+    }
+
+    return new Ed25519PublicKey(
+        parameters, publicKeyBytes, createOutputPrefix(parameters, idRequirement), idRequirement);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public Bytes getPublicKeyBytes() {
+    return publicKeyBytes;
+  }
+
+  @Override
+  public Ed25519Parameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof Ed25519PublicKey)) {
+      return false;
+    }
+    Ed25519PublicKey that = (Ed25519PublicKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.publicKeyBytes.equals(publicKeyBytes)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PredefinedSignatureParameters.java b/java_src/src/main/java/com/google/crypto/tink/signature/PredefinedSignatureParameters.java
new file mode 100644
index 0000000..5ac8d02
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PredefinedSignatureParameters.java
@@ -0,0 +1,309 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+/**
+ * Pre-generated {@link Parameter} objects for {@link com.google.crypto.tink.PublicKeySign} and
+ * {@link com.google.crypto.tink.PublicKeyVerify}. keys.
+ *
+ * <p>Note: if you want to keep dependencies small, consider inlining the constants here.
+ */
+public final class PredefinedSignatureParameters {
+  /**
+   * A {@link Parameters} object that generates new instances of {@link EcdsaPrivateKey} objects
+   * with the following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256
+   *   <li>Curve: NIST P-256
+   *   <li>Signature encoding: DER (this is the encoding that Java uses).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final EcdsaParameters ECDSA_P256 =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setHashType(EcdsaParameters.HashType.SHA256)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                  .setVariant(EcdsaParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link EcdsaPrivateKey} objects with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA512
+   *   <li>Curve: NIST P-384
+   *   <li>Signature encoding: DER (this is the encoding that Java uses).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final EcdsaParameters ECDSA_P384 =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setHashType(EcdsaParameters.HashType.SHA512)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                  .setVariant(EcdsaParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link EcdsaPrivateKey} objects with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA512
+   *   <li>Curve: NIST P-521
+   *   <li>Signature encoding: DER (this is the encoding that Java uses).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final EcdsaParameters ECDSA_P521 =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setHashType(EcdsaParameters.HashType.SHA512)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                  .setVariant(EcdsaParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link EcdsaPrivateKey} objects with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256
+   *   <li>Curve: NIST P-256
+   *   <li>Signature encoding: IEEE_P1363 (this is the encoding that JWS and WebCrypto use).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final EcdsaParameters ECDSA_P256_IEEE_P1363 =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                  .setHashType(EcdsaParameters.HashType.SHA256)
+                  .setVariant(EcdsaParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link EcdsaPrivateKey} objects with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA512
+   *   <li>Curve: NIST P-384
+   *   <li>Signature encoding: IEEE_P1363 (this is the encoding that JWS and WebCrypto use).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final EcdsaParameters ECDSA_P384_IEEE_P1363 =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                  .setHashType(EcdsaParameters.HashType.SHA512)
+                  .setVariant(EcdsaParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link EcdsaPrivateKey} objects with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256
+   *   <li>Curve: NIST P-256
+   *   <li>Signature encoding: DER (this is the encoding that Java uses).
+   *   <li>Prefix type: None
+   * </ul>
+   *
+   * The digital signature generated by this key would be 64 bytes exactly.
+   */
+  public static final EcdsaParameters ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                  .setHashType(EcdsaParameters.HashType.SHA256)
+                  .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link EcdsaPrivateKey} objects with the
+   * following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA512
+   *   <li>Curve: NIST P-521
+   *   <li>Signature encoding: IEEE_P1363 (this is the encoding that JWS and WebCrypto use).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final EcdsaParameters ECDSA_P521_IEEE_P1363 =
+      exceptionIsBug(
+          () ->
+              EcdsaParameters.builder()
+                  .setHashType(EcdsaParameters.HashType.SHA512)
+                  .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+                  .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                  .setVariant(EcdsaParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link Ed25519PrivateKey} objects.
+   *
+   * @since 1.1.0
+   */
+  public static final Ed25519Parameters ED25519 =
+      exceptionIsBug(() -> Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link ED25519PrivateKey}.
+   *
+   * <p>The difference between {@link ED25519WithRawOutput} and {@link ED25519} is the format of
+   * signatures generated. {@link ED25519WithRawOutput} generates signatures of {@link
+   * OutputPrefixType.RAW} format, which is 64 bytes long.
+   *
+   * @since 1.3.0
+   */
+  public static final Ed25519Parameters ED25519WithRawOutput =
+      exceptionIsBug(() -> Ed25519Parameters.create(Ed25519Parameters.Variant.NO_PREFIX));
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link RsaSsaPkcs1PrivateKey} objects with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256.
+   *   <li>Modulus size: 3072 bit.
+   *   <li>Public exponent: 65537 (aka F4).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final RsaSsaPkcs1Parameters RSA_SSA_PKCS1_3072_SHA256_F4 =
+      exceptionIsBug(
+          () ->
+              RsaSsaPkcs1Parameters.builder()
+                  .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                  .setModulusSizeBits(3072)
+                  .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                  .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link RsaSsaPkcs1PrivateKey} objects with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA256.
+   *   <li>Modulus size: 3072 bit.
+   *   <li>Public exponent: 65537 (aka F4).
+   *   <li>Prefix type: None
+   * </ul>
+   */
+  public static final RsaSsaPkcs1Parameters RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX =
+      exceptionIsBug(
+          () ->
+              RsaSsaPkcs1Parameters.builder()
+                  .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                  .setModulusSizeBits(3072)
+                  .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                  .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link RsaSsaPkcs1PrivateKey} objects with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Hash function: SHA512.
+   *   <li>Modulus size: 4096 bit.
+   *   <li>Public exponent: 65537 (aka F4).
+   *   <li>Prefix type: {@link OutputPrefixType.TINK}
+   * </ul>
+   */
+  public static final RsaSsaPkcs1Parameters RSA_SSA_PKCS1_4096_SHA512_F4 =
+      exceptionIsBug(
+          () ->
+              RsaSsaPkcs1Parameters.builder()
+                  .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+                  .setModulusSizeBits(4096)
+                  .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                  .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link RsaSsaPssPrivateKey} objects with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Signature hash: SHA256.
+   *   <li>MGF1 hash: SHA256.
+   *   <li>Salt length: 32 (i.e., SHA256's output length).
+   *   <li>Modulus size: 3072 bit.
+   *   <li>Public exponent: 65537 (aka F4).
+   * </ul>
+   */
+  public static final RsaSsaPssParameters RSA_SSA_PSS_3072_SHA256_SHA256_32_F4 =
+      exceptionIsBug(
+          () ->
+              RsaSsaPssParameters.builder()
+                  .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                  .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                  .setSaltLengthBytes(32)
+                  .setModulusSizeBits(3072)
+                  .setPublicExponent(RsaSsaPssParameters.F4)
+                  .setVariant(RsaSsaPssParameters.Variant.TINK)
+                  .build());
+
+  /**
+   * A {@link Parameters} that generates new instances of {@link RsaSsaPssPrivateKey} objects with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Signature hash: SHA512.
+   *   <li>MGF1 hash: SHA512.
+   *   <li>Salt length: 64 (i.e., SHA512's output length).
+   *   <li>Modulus size: 4096 bit.
+   *   <li>Public exponent: 65537 (aka F4).
+   * </ul>
+   */
+  public static final RsaSsaPssParameters RSA_SSA_PSS_4096_SHA512_SHA512_64_F4 =
+      exceptionIsBug(
+          () ->
+              RsaSsaPssParameters.builder()
+                  .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                  .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                  .setSaltLengthBytes(64)
+                  .setModulusSizeBits(4096)
+                  .setPublicExponent(RsaSsaPssParameters.F4)
+                  .setVariant(RsaSsaPssParameters.Variant.TINK)
+                  .build());
+
+  private PredefinedSignatureParameters() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignConfig.java b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignConfig.java
index fcf83c3..58ba749 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignConfig.java
@@ -47,7 +47,7 @@
    */
   @Deprecated
   public static void registerStandardKeyTypes() throws GeneralSecurityException {
-    Config.register(SignatureConfig.TINK_1_0_0);
+    SignatureConfig.register();
   }
 
   private PublicKeySignConfig() {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignFactory.java b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignFactory.java
index 85cbfe6..775f90d 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignFactory.java
@@ -18,7 +18,6 @@
 
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -44,9 +43,9 @@
    *     {@code PublicKeySignWrapper} instead.
    */
   @Deprecated
-  public static PublicKeySign getPrimitive(KeysetHandle keysetHandle)
+  public  static PublicKeySign getPrimitive(KeysetHandle keysetHandle)
       throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new PublicKeySignWrapper());
+    PublicKeySignWrapper.register();
     return keysetHandle.getPrimitive(PublicKeySign.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignWrapper.java b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignWrapper.java
index 5898f21..b1db7f6 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeySignWrapper.java
@@ -38,6 +38,7 @@
 public class PublicKeySignWrapper implements PrimitiveWrapper<PublicKeySign, PublicKeySign> {
 
   private static final byte[] FORMAT_VERSION = new byte[] {0};
+  private static final PublicKeySignWrapper WRAPPER = new PublicKeySignWrapper();
 
   private static class WrappedPublicKeySign implements PublicKeySign {
     private final PrimitiveSet<PublicKeySign> primitives;
@@ -99,6 +100,6 @@
    * argument.
    */
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new PublicKeySignWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyConfig.java b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyConfig.java
index 848d273..5e21247 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyConfig.java
@@ -47,7 +47,7 @@
    */
   @Deprecated
   public static void registerStandardKeyTypes() throws GeneralSecurityException {
-    Config.register(SignatureConfig.TINK_1_0_0);
+    SignatureConfig.register();
   }
 
   private PublicKeyVerifyConfig() {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyFactory.java b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyFactory.java
index 7e6ff62..05699f6 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyFactory.java
@@ -18,7 +18,6 @@
 
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PublicKeyVerify;
-import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 
 /**
@@ -44,9 +43,9 @@
    *     {@code PublicKeyVerifyWrapper} instead.
    */
   @Deprecated
-  public static PublicKeyVerify getPrimitive(KeysetHandle keysetHandle)
+  public  static PublicKeyVerify getPrimitive(KeysetHandle keysetHandle)
       throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new PublicKeyVerifyWrapper());
+    PublicKeyVerifyWrapper.register();
     return keysetHandle.getPrimitive(PublicKeyVerify.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapper.java b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapper.java
index e3a3bd1..520f2cd 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapper.java
@@ -43,9 +43,11 @@
  * @since 1.0.0
  */
 class PublicKeyVerifyWrapper implements PrimitiveWrapper<PublicKeyVerify, PublicKeyVerify> {
+
   private static final Logger logger = Logger.getLogger(PublicKeyVerifyWrapper.class.getName());
 
   private static final byte[] FORMAT_VERSION = new byte[] {0};
+  private static final PublicKeyVerifyWrapper WRAPPER = new PublicKeyVerifyWrapper();
 
   private static class WrappedPublicKeyVerify implements PublicKeyVerify {
     private final PrimitiveSet<PublicKeyVerify> primitives;
@@ -131,6 +133,6 @@
    * argument.
    */
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new PublicKeyVerifyWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1Parameters.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1Parameters.java
new file mode 100644
index 0000000..4b6141b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1Parameters.java
@@ -0,0 +1,235 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Describes the parameters of a {@link RsaSsaPkcs1PublicKey} and {@link RsaSsaPkcs1PrivateKey}.
+ *
+ * <p>Standard: https://www.rfc-editor.org/rfc/rfc8017.txt
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+public final class RsaSsaPkcs1Parameters extends SignatureParameters {
+  /**
+   * Describes details of the signature.
+   *
+   * <p>The usual key is used for variant "NO_PREFIX". Other variants slightly change how the
+   * signature is computed, or add a prefix to every computation depending on the key id.
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant LEGACY = new Variant("LEGACY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The Hash algorithm used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static final BigInteger F4 = BigInteger.valueOf(65537);
+
+  /** Builds a new RsaSsaPkcs1Parameters instance. */
+  public static final class Builder {
+    @Nullable private Integer modulusSizeBits = null;
+    @Nullable private BigInteger publicExponent = F4;
+    @Nullable private HashType hashType = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setModulusSizeBits(int modulusSizeBits) {
+      this.modulusSizeBits = modulusSizeBits;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setPublicExponent(BigInteger e) {
+      this.publicExponent = e;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setHashType(HashType hashType) {
+      this.hashType = hashType;
+      return this;
+    }
+
+    private static final BigInteger TWO = BigInteger.valueOf(2);
+    private static final BigInteger PUBLIC_EXPONENT_UPPER_BOUND = TWO.pow(256);
+
+    private void validatePublicExponent(BigInteger publicExponent)
+        throws InvalidAlgorithmParameterException {
+      // We use the validation of the public exponent as defined in
+      // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, B.3
+      int c = publicExponent.compareTo(F4);
+      if (c == 0) {
+        // publicExponent is F4.
+        return;
+      }
+      if (c < 0) {
+        // publicExponent is smaller than F4.
+        throw new InvalidAlgorithmParameterException("Public exponent must be at least 65537.");
+      }
+      if (publicExponent.mod(TWO).equals(BigInteger.ZERO)) {
+        // publicExponent is even. This is invalid since it is not co-prime to p-1.
+        throw new InvalidAlgorithmParameterException("Invalid public exponent");
+      }
+      if (publicExponent.compareTo(PUBLIC_EXPONENT_UPPER_BOUND) > 0) {
+        // publicExponent is larger than PUBLIC_EXPONENT_UPPER_BOUND.
+        throw new InvalidAlgorithmParameterException(
+            "Public exponent cannot be larger than 2^256.");
+      }
+    }
+
+    public RsaSsaPkcs1Parameters build() throws GeneralSecurityException {
+      if (modulusSizeBits == null) {
+        throw new GeneralSecurityException("key size is not set");
+      }
+      if (publicExponent == null) {
+        throw new GeneralSecurityException("publicExponent is not set");
+      }
+      if (hashType == null) {
+        throw new GeneralSecurityException("hash type is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("variant is not set");
+      }
+      if (modulusSizeBits < 2048) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size in bytes %d; must be at least 2048 bits", modulusSizeBits));
+      }
+      validatePublicExponent(publicExponent);
+      return new RsaSsaPkcs1Parameters(modulusSizeBits, publicExponent, variant, hashType);
+    }
+  }
+
+  private final int modulusSizeBits;
+  private final BigInteger publicExponent;
+  private final Variant variant;
+  private final HashType hashType;
+
+  private RsaSsaPkcs1Parameters(
+      int modulusSizeBits, BigInteger publicExponent, Variant variant, HashType hashType) {
+    this.modulusSizeBits = modulusSizeBits;
+    this.publicExponent = publicExponent;
+    this.variant = variant;
+    this.hashType = hashType;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getModulusSizeBits() {
+    return modulusSizeBits;
+  }
+
+  public BigInteger getPublicExponent() {
+    return publicExponent;
+  }
+
+  public Variant getVariant() {
+    return variant;
+  }
+
+  public HashType getHashType() {
+    return hashType;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof RsaSsaPkcs1Parameters)) {
+      return false;
+    }
+    RsaSsaPkcs1Parameters that = (RsaSsaPkcs1Parameters) o;
+    return that.getModulusSizeBits() == getModulusSizeBits()
+        && Objects.equals(that.getPublicExponent(), getPublicExponent())
+        && that.getVariant() == getVariant()
+        && that.getHashType() == getHashType();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        RsaSsaPkcs1Parameters.class, modulusSizeBits, publicExponent, variant, hashType);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "RSA SSA PKCS1 Parameters (variant: "
+        + variant
+        + ", hashType: "
+        + hashType
+        + ", publicExponent: "
+        + publicExponent
+        + ", and "
+        + modulusSizeBits
+        + "-bit modulus)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1PrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1PrivateKey.java
new file mode 100644
index 0000000..359bc97
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1PrivateKey.java
@@ -0,0 +1,272 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a private key for RSA SSA PKCS1 signatures.
+ *
+ * <p>Standard: https://www.rfc-editor.org/rfc/rfc8017#section-3.2.
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+public final class RsaSsaPkcs1PrivateKey extends SignaturePrivateKey {
+  private final RsaSsaPkcs1PublicKey publicKey;
+  private final SecretBigInteger d;
+  private final SecretBigInteger p;
+  private final SecretBigInteger q;
+  private final SecretBigInteger dP;
+  private final SecretBigInteger dQ;
+  private final SecretBigInteger qInv;
+
+  /** Builder for RsaSsaPkcs1PrivateKey. */
+  public static class Builder {
+    @Nullable private RsaSsaPkcs1PublicKey publicKey = null;
+    @Nullable private SecretBigInteger d = null;
+    @Nullable private SecretBigInteger p = null;
+    @Nullable private SecretBigInteger q = null;
+    @Nullable private SecretBigInteger dP = null;
+    @Nullable private SecretBigInteger dQ = null;
+    @Nullable private SecretBigInteger qInv = null;
+
+    private Builder() {}
+
+    /**
+     * Sets the public key, which includes the parameters.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPublicKey(RsaSsaPkcs1PublicKey publicKey) {
+      this.publicKey = publicKey;
+      return this;
+    }
+
+    /**
+     * Sets the prime factors p and q.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPrimes(SecretBigInteger p, SecretBigInteger q) {
+      this.p = p;
+      this.q = q;
+      return this;
+    }
+
+    /**
+     * Sets the private exponent d.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPrivateExponent(SecretBigInteger d) {
+      this.d = d;
+      return this;
+    }
+
+    /**
+     * Sets the prime exponents dP and dQ.
+     *
+     * <p>See https://www.rfc-editor.org/rfc/rfc8017#section-3.2.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPrimeExponents(SecretBigInteger dP, SecretBigInteger dQ) {
+      this.dP = dP;
+      this.dQ = dQ;
+      return this;
+    }
+
+    /**
+     * Sets the CRT coefficient qInv.
+     *
+     * <p>See https://www.rfc-editor.org/rfc/rfc8017#section-3.2.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setCrtCoefficient(SecretBigInteger qInv) {
+      this.qInv = qInv;
+      return this;
+    }
+
+    private static final int PRIME_CERTAINTY = 10;
+
+    @AccessesPartialKey
+    public RsaSsaPkcs1PrivateKey build() throws GeneralSecurityException {
+      if (publicKey == null) {
+        throw new GeneralSecurityException("Cannot build without a RSA SSA PKCS1 public key");
+      }
+      if (p == null || q == null) {
+        throw new GeneralSecurityException("Cannot build without prime factors");
+      }
+      if (d == null) {
+        throw new GeneralSecurityException("Cannot build without private exponent");
+      }
+      if (dP == null || dQ == null) {
+        throw new GeneralSecurityException("Cannot build without prime exponents");
+      }
+      if (qInv == null) {
+        throw new GeneralSecurityException("Cannot build without CRT coefficient");
+      }
+      BigInteger e = publicKey.getParameters().getPublicExponent();
+      BigInteger n = publicKey.getModulus();
+
+      BigInteger ip = this.p.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger iq = this.q.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger id = this.d.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger idP = this.dP.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger idQ = this.dQ.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger iqInv = this.qInv.getBigInteger(InsecureSecretKeyAccess.get());
+
+      if (!ip.isProbablePrime(PRIME_CERTAINTY)) {
+        throw new GeneralSecurityException("p is not a prime");
+      }
+      if (!iq.isProbablePrime(PRIME_CERTAINTY)) {
+        throw new GeneralSecurityException("q is not a prime");
+      }
+      if (!ip.multiply(iq).equals(n)) {
+        throw new GeneralSecurityException(
+            "Prime p times prime q is not equal to the public key's modulus");
+      }
+      // lambda = LCM(p-1, q-1)
+      BigInteger pMinusOne = ip.subtract(BigInteger.ONE);
+      BigInteger qMinusOne = iq.subtract(BigInteger.ONE);
+      BigInteger lambda = pMinusOne.divide(pMinusOne.gcd(qMinusOne)).multiply(qMinusOne);
+      if (!e.multiply(id).mod(lambda).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("D is invalid.");
+      }
+      if (!e.multiply(idP).mod(pMinusOne).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("dP is invalid.");
+      }
+      if (!e.multiply(idQ).mod(qMinusOne).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("dQ is invalid.");
+      }
+      if (!iq.multiply(iqInv).mod(ip).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("qInv is invalid.");
+      }
+      return new RsaSsaPkcs1PrivateKey(publicKey, p, q, d, dP, dQ, qInv);
+    }
+  }
+
+  private RsaSsaPkcs1PrivateKey(
+      RsaSsaPkcs1PublicKey publicKey,
+      SecretBigInteger p,
+      SecretBigInteger q,
+      SecretBigInteger d,
+      SecretBigInteger dP,
+      SecretBigInteger dQ,
+      SecretBigInteger qInv) {
+    this.publicKey = publicKey;
+    this.p = p;
+    this.q = q;
+    this.d = d;
+    this.dP = dP;
+    this.dQ = dQ;
+    this.qInv = qInv;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the key parameters. */
+  @Override
+  public RsaSsaPkcs1Parameters getParameters() {
+    return publicKey.getParameters();
+  }
+
+  /** Returns the public key. */
+  @Override
+  public RsaSsaPkcs1PublicKey getPublicKey() {
+    return publicKey;
+  }
+
+  /** Returns the prime factor p. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBigInteger getPrimeP() {
+    return p;
+  }
+
+  /** Returns the prime factor q. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBigInteger getPrimeQ() {
+    return q;
+  }
+
+  /** Returns the private exponent d. */
+  public SecretBigInteger getPrivateExponent() {
+    return d;
+  }
+
+  /** Returns the prime exponent dP. */
+  public SecretBigInteger getPrimeExponentP() {
+    return dP;
+  }
+
+  /** Returns the prime exponent dQ. */
+  public SecretBigInteger getPrimeExponentQ() {
+    return dQ;
+  }
+
+  /** Returns the CRT coefficient qInv. */
+  public SecretBigInteger getCrtCoefficient() {
+    return qInv;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof RsaSsaPkcs1PrivateKey)) {
+      return false;
+    }
+    RsaSsaPkcs1PrivateKey that = (RsaSsaPkcs1PrivateKey) o;
+    return that.publicKey.equalsKey(publicKey)
+        && p.equalsSecretBigInteger(that.p)
+        && q.equalsSecretBigInteger(that.q)
+        && d.equalsSecretBigInteger(that.d)
+        && dP.equalsSecretBigInteger(that.dP)
+        && dQ.equalsSecretBigInteger(that.dQ)
+        && qInv.equalsSecretBigInteger(that.qInv);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1ProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1ProtoSerialization.java
new file mode 100644
index 0000000..c879eda
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1ProtoSerialization.java
@@ -0,0 +1,370 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link RsaSsaPkcs1PrivateKey} and {@link RsaSsaPkcs1PublicKey}
+ * objects and {@link RsaSsaPkcs1Parameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class RsaSsaPkcs1ProtoSerialization {
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey";
+  private static final Bytes PRIVATE_TYPE_URL_BYTES = toBytesFromPrintableAscii(PRIVATE_TYPE_URL);
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
+  private static final ParametersSerializer<RsaSsaPkcs1Parameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              RsaSsaPkcs1ProtoSerialization::serializeParameters,
+              RsaSsaPkcs1Parameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          RsaSsaPkcs1ProtoSerialization::parseParameters,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<RsaSsaPkcs1PublicKey, ProtoKeySerialization>
+      PUBLIC_KEY_SERIALIZER =
+          KeySerializer.create(
+              RsaSsaPkcs1ProtoSerialization::serializePublicKey,
+              RsaSsaPkcs1PublicKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          RsaSsaPkcs1ProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final KeySerializer<RsaSsaPkcs1PrivateKey, ProtoKeySerialization>
+      PRIVATE_KEY_SERIALIZER =
+          KeySerializer.create(
+              RsaSsaPkcs1ProtoSerialization::serializePrivateKey,
+              RsaSsaPkcs1PrivateKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PRIVATE_KEY_PARSER =
+      KeyParser.create(
+          RsaSsaPkcs1ProtoSerialization::parsePrivateKey,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static OutputPrefixType toProtoOutputPrefixType(RsaSsaPkcs1Parameters.Variant variant)
+      throws GeneralSecurityException {
+    if (RsaSsaPkcs1Parameters.Variant.TINK.equals(variant)) {
+      return OutputPrefixType.TINK;
+    }
+    if (RsaSsaPkcs1Parameters.Variant.CRUNCHY.equals(variant)) {
+      return OutputPrefixType.CRUNCHY;
+    }
+    if (RsaSsaPkcs1Parameters.Variant.NO_PREFIX.equals(variant)) {
+      return OutputPrefixType.RAW;
+    }
+    if (RsaSsaPkcs1Parameters.Variant.LEGACY.equals(variant)) {
+      return OutputPrefixType.LEGACY;
+    }
+    throw new GeneralSecurityException("Unable to serialize variant: " + variant);
+  }
+
+  private static HashType toProtoHashType(RsaSsaPkcs1Parameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (RsaSsaPkcs1Parameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (RsaSsaPkcs1Parameters.HashType.SHA384.equals(hashType)) {
+      return HashType.SHA384;
+    }
+    if (RsaSsaPkcs1Parameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static RsaSsaPkcs1Parameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA256:
+        return RsaSsaPkcs1Parameters.HashType.SHA256;
+      case SHA384:
+        return RsaSsaPkcs1Parameters.HashType.SHA384;
+      case SHA512:
+        return RsaSsaPkcs1Parameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static RsaSsaPkcs1Parameters.Variant toVariant(OutputPrefixType outputPrefixType)
+      throws GeneralSecurityException {
+    switch (outputPrefixType) {
+      case TINK:
+        return RsaSsaPkcs1Parameters.Variant.TINK;
+      case CRUNCHY:
+        return RsaSsaPkcs1Parameters.Variant.CRUNCHY;
+      case LEGACY:
+        return RsaSsaPkcs1Parameters.Variant.LEGACY;
+      case RAW:
+        return RsaSsaPkcs1Parameters.Variant.NO_PREFIX;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse OutputPrefixType: " + outputPrefixType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.RsaSsaPkcs1Params getProtoParams(
+      RsaSsaPkcs1Parameters parameters) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.RsaSsaPkcs1Params.newBuilder()
+        .setHashType(toProtoHashType(parameters.getHashType()))
+        .build();
+  }
+
+  /** Encodes a BigInteger using a big-endian encoding. */
+  private static ByteString encodeBigInteger(BigInteger i) {
+    // Note that toBigEndianBytes() returns the minimal big-endian encoding using the two's
+    // complement representation. This means that the encoding may have a leading zero.
+    byte[] encoded = BigIntegerEncoding.toBigEndianBytes(i);
+    return ByteString.copyFrom(encoded);
+  }
+
+  private static com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey getProtoPublicKey(
+      RsaSsaPkcs1PublicKey key) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+        .setParams(getProtoParams(key.getParameters()))
+        .setN(encodeBigInteger(key.getModulus()))
+        .setE(encodeBigInteger(key.getParameters().getPublicExponent()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(RsaSsaPkcs1Parameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(PRIVATE_TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .setModulusSizeInBits(parameters.getModulusSizeBits())
+                    .setPublicExponent(encodeBigInteger(parameters.getPublicExponent()))
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(toProtoOutputPrefixType(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializePublicKey(
+      RsaSsaPkcs1PublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        getProtoPublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static ByteString encodeSecretBigInteger(SecretBigInteger i, SecretKeyAccess access) {
+    return encodeBigInteger(i.getBigInteger(access));
+  }
+
+  private static ProtoKeySerialization serializePrivateKey(
+      RsaSsaPkcs1PrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    SecretKeyAccess a = SecretKeyAccess.requireAccess(access);
+    com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(getProtoPublicKey(key.getPublicKey()))
+            .setD(encodeSecretBigInteger(key.getPrivateExponent(), a))
+            .setP(encodeSecretBigInteger(key.getPrimeP(), a))
+            .setQ(encodeSecretBigInteger(key.getPrimeQ(), a))
+            .setDp(encodeSecretBigInteger(key.getPrimeExponentP(), a))
+            .setDq(encodeSecretBigInteger(key.getPrimeExponentQ(), a))
+            .setCrt(encodeSecretBigInteger(key.getCrtCoefficient(), a))
+            .build();
+    return ProtoKeySerialization.create(
+        PRIVATE_TYPE_URL,
+        protoPrivateKey.toByteString(),
+        KeyMaterialType.ASYMMETRIC_PRIVATE,
+        toProtoOutputPrefixType(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static BigInteger decodeBigInteger(ByteString data) {
+    return BigIntegerEncoding.fromUnsignedBigEndianBytes(data.toByteArray());
+  }
+
+  private static RsaSsaPkcs1Parameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to RsaSsaPkcs1ProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing RsaSsaPkcs1Parameters failed: ", e);
+    }
+    return RsaSsaPkcs1Parameters.builder()
+        .setHashType(toHashType(format.getParams().getHashType()))
+        .setPublicExponent(decodeBigInteger(format.getPublicExponent()))
+        .setModulusSizeBits(format.getModulusSizeInBits())
+        .setVariant(toVariant(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static RsaSsaPkcs1PublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to RsaSsaPkcs1ProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey protoKey =
+          com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      BigInteger modulus = decodeBigInteger(protoKey.getN());
+      int modulusSizeInBits = modulus.bitLength();
+      RsaSsaPkcs1Parameters parameters =
+          RsaSsaPkcs1Parameters.builder()
+              .setHashType(toHashType(protoKey.getParams().getHashType()))
+              .setPublicExponent(decodeBigInteger(protoKey.getE()))
+              .setModulusSizeBits(modulusSizeInBits)
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      return RsaSsaPkcs1PublicKey.builder()
+          .setParameters(parameters)
+          .setModulus(modulus)
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing RsaSsaPkcs1PublicKey failed");
+    }
+  }
+
+  private static SecretBigInteger decodeSecretBigInteger(ByteString data, SecretKeyAccess access) {
+    return SecretBigInteger.fromBigInteger(
+        BigIntegerEncoding.fromUnsignedBigEndianBytes(data.toByteArray()), access);
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static RsaSsaPkcs1PrivateKey parsePrivateKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to RsaSsaPkcs1ProtoSerialization.parsePrivateKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey protoKey =
+          com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey protoPublicKey = protoKey.getPublicKey();
+
+      BigInteger modulus = decodeBigInteger(protoPublicKey.getN());
+      int modulusSizeInBits = modulus.bitLength();
+      BigInteger publicExponent = decodeBigInteger(protoPublicKey.getE());
+      RsaSsaPkcs1Parameters parameters =
+          RsaSsaPkcs1Parameters.builder()
+              .setHashType(toHashType(protoPublicKey.getParams().getHashType()))
+              .setPublicExponent(publicExponent)
+              .setModulusSizeBits(modulusSizeInBits)
+              .setVariant(toVariant(serialization.getOutputPrefixType()))
+              .build();
+      RsaSsaPkcs1PublicKey publicKey =
+          RsaSsaPkcs1PublicKey.builder()
+              .setParameters(parameters)
+              .setModulus(modulus)
+              .setIdRequirement(serialization.getIdRequirementOrNull())
+              .build();
+
+      SecretKeyAccess a = SecretKeyAccess.requireAccess(access);
+      return RsaSsaPkcs1PrivateKey.builder()
+          .setPublicKey(publicKey)
+          .setPrimes(
+              decodeSecretBigInteger(protoKey.getP(), a),
+              decodeSecretBigInteger(protoKey.getQ(), a))
+          .setPrivateExponent(decodeSecretBigInteger(protoKey.getD(), a))
+          .setPrimeExponents(
+              decodeSecretBigInteger(protoKey.getDp(), a),
+              decodeSecretBigInteger(protoKey.getDq(), a))
+          .setCrtCoefficient(decodeSecretBigInteger(protoKey.getCrt(), a))
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing RsaSsaPkcs1PrivateKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
+    registry.registerKeySerializer(PRIVATE_KEY_SERIALIZER);
+    registry.registerKeyParser(PRIVATE_KEY_PARSER);
+  }
+
+  private RsaSsaPkcs1ProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1PublicKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1PublicKey.java
new file mode 100644
index 0000000..3fb1c5d
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1PublicKey.java
@@ -0,0 +1,178 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a public key for the RSA SSA PKCS1 signature primitive.
+ *
+ * <p>Standard: https://www.rfc-editor.org/rfc/rfc8017.txt
+ *
+ * <p>This API is annotated with Alpha because it is not yet stable and might be changed in the
+ * future.
+ */
+@Alpha
+public final class RsaSsaPkcs1PublicKey extends SignaturePublicKey {
+  private final RsaSsaPkcs1Parameters parameters;
+  private final BigInteger modulus;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for RsaSsaPkcs1PublicKey. */
+  public static class Builder {
+    @Nullable private RsaSsaPkcs1Parameters parameters = null;
+    @Nullable private BigInteger modulus = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(RsaSsaPkcs1Parameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setModulus(BigInteger modulus) {
+      this.modulus = modulus;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == RsaSsaPkcs1Parameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == RsaSsaPkcs1Parameters.Variant.LEGACY
+          || parameters.getVariant() == RsaSsaPkcs1Parameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == RsaSsaPkcs1Parameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown RsaSsaPkcs1Parameters.Variant: " + parameters.getVariant());
+    }
+
+    public RsaSsaPkcs1PublicKey build() throws GeneralSecurityException {
+      if (parameters == null) {
+        throw new GeneralSecurityException("Cannot build without parameters");
+      }
+
+      if (modulus == null) {
+        throw new GeneralSecurityException("Cannot build without modulus");
+      }
+      int modulusSize = modulus.bitLength();
+      int paramModulusSize = parameters.getModulusSizeBits();
+      // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, B.3 requires p and q
+      // to be chosen such that 2^(paramModulusSize-1) < modulus < 2^paramModulusSize.
+      if (modulusSize != paramModulusSize) {
+        throw new GeneralSecurityException(
+            "Got modulus size "
+                + modulusSize
+                + ", but parameters requires modulus size "
+                + paramModulusSize);
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new RsaSsaPkcs1PublicKey(parameters, modulus, outputPrefix, idRequirement);
+    }
+  }
+
+  private RsaSsaPkcs1PublicKey(
+      RsaSsaPkcs1Parameters parameters,
+      BigInteger modulus,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.modulus = modulus;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public BigInteger getModulus() {
+    return modulus;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public RsaSsaPkcs1Parameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof RsaSsaPkcs1PublicKey)) {
+      return false;
+    }
+    RsaSsaPkcs1PublicKey that = (RsaSsaPkcs1PublicKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.modulus.equals(modulus)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManager.java
index 5cd710b..ffd91a6 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManager.java
@@ -233,6 +233,7 @@
   public static void registerPair(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerAsymmetricKeyManagers(
         new RsaSsaPkcs1SignKeyManager(), new RsaSsaPkcs1VerifyKeyManager(), newKeyAllowed);
+    RsaSsaPkcs1ProtoSerialization.register();
   }
 
   /**
@@ -244,10 +245,7 @@
    *       <li>Public exponent: 65537 (aka F4).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}.
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PKCS1_3072_SHA256_F4")}
    */
-  @Deprecated
   public static final KeyTemplate rsa3072SsaPkcs1Sha256F4Template() {
     return createKeyTemplate(
         HashType.SHA256,
@@ -265,10 +263,7 @@
    *       <li>Public exponent: 65537 (aka F4).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW} (no prefix).
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PKCS1_3072_SHA256_F4_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawRsa3072SsaPkcs1Sha256F4Template() {
     return createKeyTemplate(
         HashType.SHA256,
@@ -286,10 +281,7 @@
    *       <li>Public exponent: 65537 (aka F4).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}.
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PKCS1_4096_SHA512_F4")}
    */
-  @Deprecated
   public static final KeyTemplate rsa4096SsaPkcs1Sha512F4Template() {
     return createKeyTemplate(
         HashType.SHA512,
@@ -307,10 +299,7 @@
    *       <li>Public exponent: 65537 (aka F4).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#RAW} (no prefix).
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PKCS1_4096_SHA512_F4_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawRsa4096SsaPkcs1Sha512F4Template() {
     return createKeyTemplate(
         HashType.SHA512,
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssParameters.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssParameters.java
new file mode 100644
index 0000000..4f24d6a
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssParameters.java
@@ -0,0 +1,290 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Describes the parameters of a {@link RsaSsaPssPublicKey} and {@link RsaSsaPssPrivateKey}.
+ *
+ * <p>Standard: https://datatracker.ietf.org/doc/html/rfc8017#section-8.1
+ */
+public final class RsaSsaPssParameters extends SignatureParameters {
+  /**
+   * Describes details of the signature.
+   *
+   * <p>The usual key is used for variant "NO_PREFIX". Other variants slightly change how the
+   * signature is computed, or add a prefix to every computation depending on the key id.
+   */
+  @Immutable
+  public static final class Variant {
+    public static final Variant TINK = new Variant("TINK");
+    public static final Variant CRUNCHY = new Variant("CRUNCHY");
+    public static final Variant LEGACY = new Variant("LEGACY");
+    public static final Variant NO_PREFIX = new Variant("NO_PREFIX");
+
+    private final String name;
+
+    private Variant(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** The Hash algorithm used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA384 = new HashType("SHA384");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static final BigInteger F4 = BigInteger.valueOf(65537);
+
+  /** Builds a new RsaSsaPssParameters instance. */
+  public static final class Builder {
+    @Nullable private Integer modulusSizeBits = null;
+    @Nullable private BigInteger publicExponent = F4;
+    @Nullable private HashType sigHashType = null;
+    @Nullable private HashType mgf1HashType = null;
+    @Nullable private Integer saltLengthBytes = null;
+    private Variant variant = Variant.NO_PREFIX;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setModulusSizeBits(int modulusSizeBits) {
+      this.modulusSizeBits = modulusSizeBits;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setPublicExponent(BigInteger e) {
+      this.publicExponent = e;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setVariant(Variant variant) {
+      this.variant = variant;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setSigHashType(HashType sigHashType) {
+      this.sigHashType = sigHashType;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setMgf1HashType(HashType mgf1HashType) {
+      this.mgf1HashType = mgf1HashType;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setSaltLengthBytes(int saltLengthBytes) throws GeneralSecurityException {
+      if (saltLengthBytes < 0) {
+        throw new GeneralSecurityException(
+            String.format(
+                "Invalid salt length in bytes %d; salt length must be positive", saltLengthBytes));
+      }
+      this.saltLengthBytes = saltLengthBytes;
+      return this;
+    }
+
+    private static final BigInteger TWO = BigInteger.valueOf(2);
+    private static final BigInteger PUBLIC_EXPONENT_UPPER_BOUND = TWO.pow(256);
+    private static final int MIN_RSA_MODULUS_SIZE = 2048;
+
+    private void validatePublicExponent(BigInteger publicExponent)
+        throws InvalidAlgorithmParameterException {
+      // We use the validation of the public exponent as defined in
+      // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, B.3
+      int c = publicExponent.compareTo(F4);
+      if (c == 0) {
+        // publicExponent is F4.
+        return;
+      }
+      if (c < 0) {
+        // publicExponent is smaller than F4.
+        throw new InvalidAlgorithmParameterException("Public exponent must be at least 65537.");
+      }
+      if (publicExponent.mod(TWO).equals(BigInteger.ZERO)) {
+        // publicExponent is even. This is invalid since it is not co-prime to p-1.
+        throw new InvalidAlgorithmParameterException("Invalid public exponent");
+      }
+      if (publicExponent.compareTo(PUBLIC_EXPONENT_UPPER_BOUND) > 0) {
+        // publicExponent is larger than PUBLIC_EXPONENT_UPPER_BOUND.
+        throw new InvalidAlgorithmParameterException(
+            "Public exponent cannot be larger than 2^256.");
+      }
+    }
+
+    public RsaSsaPssParameters build() throws GeneralSecurityException {
+      if (modulusSizeBits == null) {
+        throw new GeneralSecurityException("key size is not set");
+      }
+      if (publicExponent == null) {
+        throw new GeneralSecurityException("publicExponent is not set");
+      }
+      if (sigHashType == null) {
+        throw new GeneralSecurityException("signature hash type is not set");
+      }
+      if (mgf1HashType == null) {
+        throw new GeneralSecurityException("mgf1 hash type is not set");
+      }
+      if (variant == null) {
+        throw new GeneralSecurityException("variant is not set");
+      }
+      if (saltLengthBytes == null) {
+        throw new GeneralSecurityException("salt length is not set");
+      }
+      if (modulusSizeBits < MIN_RSA_MODULUS_SIZE) {
+        throw new InvalidAlgorithmParameterException(
+            String.format(
+                "Invalid key size in bytes %d; must be at least %d bits",
+                modulusSizeBits, MIN_RSA_MODULUS_SIZE));
+      }
+      if (sigHashType != mgf1HashType) {
+        throw new GeneralSecurityException("MGF1 hash is different from signature hash");
+      }
+      validatePublicExponent(publicExponent);
+      return new RsaSsaPssParameters(
+          modulusSizeBits, publicExponent, variant, sigHashType, mgf1HashType, saltLengthBytes);
+    }
+  }
+
+  private final int modulusSizeBits;
+  private final BigInteger publicExponent;
+  private final Variant variant;
+  private final HashType sigHashType;
+  private final HashType mgf1HashType;
+  private final int saltLengthBytes;
+
+  private RsaSsaPssParameters(
+      int modulusSizeBits,
+      BigInteger publicExponent,
+      Variant variant,
+      HashType sigHashType,
+      HashType mgf1HashType,
+      int saltLengthBytes) {
+    this.modulusSizeBits = modulusSizeBits;
+    this.publicExponent = publicExponent;
+    this.variant = variant;
+    this.sigHashType = sigHashType;
+    this.mgf1HashType = mgf1HashType;
+    this.saltLengthBytes = saltLengthBytes;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public int getModulusSizeBits() {
+    return modulusSizeBits;
+  }
+
+  public BigInteger getPublicExponent() {
+    return publicExponent;
+  }
+
+  public Variant getVariant() {
+    return variant;
+  }
+
+  public HashType getSigHashType() {
+    return sigHashType;
+  }
+
+  public HashType getMgf1HashType() {
+    return mgf1HashType;
+  }
+
+  public int getSaltLengthBytes() {
+    return saltLengthBytes;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof RsaSsaPssParameters)) {
+      return false;
+    }
+    RsaSsaPssParameters that = (RsaSsaPssParameters) o;
+    return that.getModulusSizeBits() == getModulusSizeBits()
+        && Objects.equals(that.getPublicExponent(), getPublicExponent())
+        && Objects.equals(that.getVariant(), getVariant())
+        && Objects.equals(that.getSigHashType(), getSigHashType())
+        && Objects.equals(that.getMgf1HashType(), getMgf1HashType())
+        && that.getSaltLengthBytes() == getSaltLengthBytes();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        RsaSsaPssParameters.class,
+        modulusSizeBits,
+        publicExponent,
+        variant,
+        sigHashType,
+        mgf1HashType,
+        saltLengthBytes);
+  }
+
+  @Override
+  public boolean hasIdRequirement() {
+    return variant != Variant.NO_PREFIX;
+  }
+
+  @Override
+  public String toString() {
+    return "RSA SSA PSS Parameters (variant: "
+        + variant
+        + ", signature hashType: "
+        + sigHashType
+        + ", mgf1 hashType: "
+        + mgf1HashType
+        + ", saltLengthBytes: "
+        + saltLengthBytes
+        + ", publicExponent: "
+        + publicExponent
+        + ", and "
+        + modulusSizeBits
+        + "-bit modulus)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssPrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssPrivateKey.java
new file mode 100644
index 0000000..e7c3122
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssPrivateKey.java
@@ -0,0 +1,267 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a private key for RSA SSA PSS signatures.
+ *
+ * <p>Standard: https://www.rfc-editor.org/rfc/rfc8017#section-3.2.
+ */
+public final class RsaSsaPssPrivateKey extends SignaturePrivateKey {
+  private final RsaSsaPssPublicKey publicKey;
+  private final SecretBigInteger d;
+  private final SecretBigInteger p;
+  private final SecretBigInteger q;
+  private final SecretBigInteger dP;
+  private final SecretBigInteger dQ;
+  private final SecretBigInteger qInv;
+
+  /** Builder for RsaSsaPssPrivateKey. */
+  public static class Builder {
+    @Nullable private RsaSsaPssPublicKey publicKey = null;
+    @Nullable private SecretBigInteger d = null;
+    @Nullable private SecretBigInteger p = null;
+    @Nullable private SecretBigInteger q = null;
+    @Nullable private SecretBigInteger dP = null;
+    @Nullable private SecretBigInteger dQ = null;
+    @Nullable private SecretBigInteger qInv = null;
+
+    private Builder() {}
+
+    /**
+     * Sets the public key, which includes the parameters.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPublicKey(RsaSsaPssPublicKey publicKey) {
+      this.publicKey = publicKey;
+      return this;
+    }
+
+    /**
+     * Sets the prime factors p and q.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPrimes(SecretBigInteger p, SecretBigInteger q) {
+      this.p = p;
+      this.q = q;
+      return this;
+    }
+
+    /**
+     * Sets the private exponent d.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPrivateExponent(SecretBigInteger d) {
+      this.d = d;
+      return this;
+    }
+
+    /**
+     * Sets the prime exponents dP and dQ.
+     *
+     * <p>See https://www.rfc-editor.org/rfc/rfc8017#section-3.2.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setPrimeExponents(SecretBigInteger dP, SecretBigInteger dQ) {
+      this.dP = dP;
+      this.dQ = dQ;
+      return this;
+    }
+
+    /**
+     * Sets the CRT coefficient qInv.
+     *
+     * <p>See https://www.rfc-editor.org/rfc/rfc8017#section-3.2.
+     *
+     * <p>This is required.
+     */
+    @CanIgnoreReturnValue
+    public Builder setCrtCoefficient(SecretBigInteger qInv) {
+      this.qInv = qInv;
+      return this;
+    }
+
+    private static final int PRIME_CERTAINTY = 10;
+
+    @AccessesPartialKey
+    public RsaSsaPssPrivateKey build() throws GeneralSecurityException {
+      if (publicKey == null) {
+        throw new GeneralSecurityException("Cannot build without a RSA SSA PKCS1 public key");
+      }
+      if (p == null || q == null) {
+        throw new GeneralSecurityException("Cannot build without prime factors");
+      }
+      if (d == null) {
+        throw new GeneralSecurityException("Cannot build without private exponent");
+      }
+      if (dP == null || dQ == null) {
+        throw new GeneralSecurityException("Cannot build without prime exponents");
+      }
+      if (qInv == null) {
+        throw new GeneralSecurityException("Cannot build without CRT coefficient");
+      }
+      BigInteger e = publicKey.getParameters().getPublicExponent();
+      BigInteger n = publicKey.getModulus();
+
+      BigInteger ip = this.p.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger iq = this.q.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger id = this.d.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger idP = this.dP.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger idQ = this.dQ.getBigInteger(InsecureSecretKeyAccess.get());
+      BigInteger iqInv = this.qInv.getBigInteger(InsecureSecretKeyAccess.get());
+
+      if (!ip.isProbablePrime(PRIME_CERTAINTY)) {
+        throw new GeneralSecurityException("p is not a prime");
+      }
+      if (!iq.isProbablePrime(PRIME_CERTAINTY)) {
+        throw new GeneralSecurityException("q is not a prime");
+      }
+      if (!ip.multiply(iq).equals(n)) {
+        throw new GeneralSecurityException(
+            "Prime p times prime q is not equal to the public key's modulus");
+      }
+      // lambda = LCM(p-1, q-1)
+      BigInteger pMinusOne = ip.subtract(BigInteger.ONE);
+      BigInteger qMinusOne = iq.subtract(BigInteger.ONE);
+      BigInteger lambda = pMinusOne.divide(pMinusOne.gcd(qMinusOne)).multiply(qMinusOne);
+      if (!e.multiply(id).mod(lambda).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("D is invalid.");
+      }
+      if (!e.multiply(idP).mod(pMinusOne).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("dP is invalid.");
+      }
+      if (!e.multiply(idQ).mod(qMinusOne).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("dQ is invalid.");
+      }
+      if (!iq.multiply(iqInv).mod(ip).equals(BigInteger.ONE)) {
+        throw new GeneralSecurityException("qInv is invalid.");
+      }
+      return new RsaSsaPssPrivateKey(publicKey, p, q, d, dP, dQ, qInv);
+    }
+  }
+
+  private RsaSsaPssPrivateKey(
+      RsaSsaPssPublicKey publicKey,
+      SecretBigInteger p,
+      SecretBigInteger q,
+      SecretBigInteger d,
+      SecretBigInteger dP,
+      SecretBigInteger dQ,
+      SecretBigInteger qInv) {
+    this.publicKey = publicKey;
+    this.p = p;
+    this.q = q;
+    this.d = d;
+    this.dP = dP;
+    this.dQ = dQ;
+    this.qInv = qInv;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the key parameters. */
+  @Override
+  public RsaSsaPssParameters getParameters() {
+    return publicKey.getParameters();
+  }
+
+  /** Returns the public key. */
+  @Override
+  public RsaSsaPssPublicKey getPublicKey() {
+    return publicKey;
+  }
+
+  /** Returns the prime factor p. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBigInteger getPrimeP() {
+    return p;
+  }
+
+  /** Returns the prime factor q. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBigInteger getPrimeQ() {
+    return q;
+  }
+
+  /** Returns the private exponent d. */
+  public SecretBigInteger getPrivateExponent() {
+    return d;
+  }
+
+  /** Returns the prime exponent dP. */
+  public SecretBigInteger getPrimeExponentP() {
+    return dP;
+  }
+
+  /** Returns the prime exponent dQ. */
+  public SecretBigInteger getPrimeExponentQ() {
+    return dQ;
+  }
+
+  /** Returns the CRT coefficient qInv. */
+  public SecretBigInteger getCrtCoefficient() {
+    return qInv;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof RsaSsaPssPrivateKey)) {
+      return false;
+    }
+    RsaSsaPssPrivateKey that = (RsaSsaPssPrivateKey) o;
+    return that.publicKey.equalsKey(publicKey)
+        && p.equalsSecretBigInteger(that.p)
+        && q.equalsSecretBigInteger(that.q)
+        && d.equalsSecretBigInteger(that.d)
+        && dP.equalsSecretBigInteger(that.dP)
+        && dQ.equalsSecretBigInteger(that.dQ)
+        && qInv.equalsSecretBigInteger(that.qInv);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssProtoSerialization.java
new file mode 100644
index 0000000..f324d57
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssProtoSerialization.java
@@ -0,0 +1,338 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.EnumTypeProtoConverter;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link RsaSsaPssPrivateKey} and {@link RsaSsaPssPublicKey} objects
+ * and {@link RsaSsaPssParameters} objects.
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class RsaSsaPssProtoSerialization {
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey";
+  private static final Bytes PRIVATE_TYPE_URL_BYTES = toBytesFromPrintableAscii(PRIVATE_TYPE_URL);
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPssPublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
+  private static final ParametersSerializer<RsaSsaPssParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              RsaSsaPssProtoSerialization::serializeParameters,
+              RsaSsaPssParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          RsaSsaPssProtoSerialization::parseParameters,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<RsaSsaPssPublicKey, ProtoKeySerialization>
+      PUBLIC_KEY_SERIALIZER =
+          KeySerializer.create(
+              RsaSsaPssProtoSerialization::serializePublicKey,
+              RsaSsaPssPublicKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          RsaSsaPssProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final KeySerializer<RsaSsaPssPrivateKey, ProtoKeySerialization>
+      PRIVATE_KEY_SERIALIZER =
+          KeySerializer.create(
+              RsaSsaPssProtoSerialization::serializePrivateKey,
+              RsaSsaPssPrivateKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PRIVATE_KEY_PARSER =
+      KeyParser.create(
+          RsaSsaPssProtoSerialization::parsePrivateKey,
+          PRIVATE_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static final EnumTypeProtoConverter<OutputPrefixType, RsaSsaPssParameters.Variant>
+      VARIANT_CONVERTER =
+          EnumTypeProtoConverter.<OutputPrefixType, RsaSsaPssParameters.Variant>builder()
+              .add(OutputPrefixType.RAW, RsaSsaPssParameters.Variant.NO_PREFIX)
+              .add(OutputPrefixType.TINK, RsaSsaPssParameters.Variant.TINK)
+              .add(OutputPrefixType.CRUNCHY, RsaSsaPssParameters.Variant.CRUNCHY)
+              .add(OutputPrefixType.LEGACY, RsaSsaPssParameters.Variant.LEGACY)
+              .build();
+
+  private static final EnumTypeProtoConverter<HashType, RsaSsaPssParameters.HashType>
+      HASH_TYPE_CONVERTER =
+          EnumTypeProtoConverter.<HashType, RsaSsaPssParameters.HashType>builder()
+              .add(HashType.SHA256, RsaSsaPssParameters.HashType.SHA256)
+              .add(HashType.SHA384, RsaSsaPssParameters.HashType.SHA384)
+              .add(HashType.SHA512, RsaSsaPssParameters.HashType.SHA512)
+              .build();
+
+  private static com.google.crypto.tink.proto.RsaSsaPssParams getProtoParams(
+      RsaSsaPssParameters parameters) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.RsaSsaPssParams.newBuilder()
+        .setSigHash(HASH_TYPE_CONVERTER.toProtoEnum(parameters.getSigHashType()))
+        .setMgf1Hash(HASH_TYPE_CONVERTER.toProtoEnum(parameters.getMgf1HashType()))
+        .setSaltLength(parameters.getSaltLengthBytes())
+        .build();
+  }
+
+  /** Encodes a BigInteger using a big-endian encoding. */
+  private static ByteString encodeBigInteger(BigInteger i) {
+    // Note that toBigEndianBytes() returns the minimal big-endian encoding using the two's
+    // complement representation. This means that the encoding may have a leading zero.
+    byte[] encoded = BigIntegerEncoding.toBigEndianBytes(i);
+    return ByteString.copyFrom(encoded);
+  }
+
+  private static com.google.crypto.tink.proto.RsaSsaPssPublicKey getProtoPublicKey(
+      RsaSsaPssPublicKey key) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+        .setParams(getProtoParams(key.getParameters()))
+        .setN(encodeBigInteger(key.getModulus()))
+        .setE(encodeBigInteger(key.getParameters().getPublicExponent()))
+        .setVersion(0)
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(RsaSsaPssParameters parameters)
+      throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(PRIVATE_TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                    .setParams(getProtoParams(parameters))
+                    .setModulusSizeInBits(parameters.getModulusSizeBits())
+                    .setPublicExponent(encodeBigInteger(parameters.getPublicExponent()))
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(VARIANT_CONVERTER.toProtoEnum(parameters.getVariant()))
+            .build());
+  }
+
+  private static ProtoKeySerialization serializePublicKey(
+      RsaSsaPssPublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        getProtoPublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        VARIANT_CONVERTER.toProtoEnum(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static ByteString encodeSecretBigInteger(SecretBigInteger i, SecretKeyAccess access) {
+    return encodeBigInteger(i.getBigInteger(access));
+  }
+
+  private static ProtoKeySerialization serializePrivateKey(
+      RsaSsaPssPrivateKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    SecretKeyAccess a = SecretKeyAccess.requireAccess(access);
+    com.google.crypto.tink.proto.RsaSsaPssPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(getProtoPublicKey(key.getPublicKey()))
+            .setD(encodeSecretBigInteger(key.getPrivateExponent(), a))
+            .setP(encodeSecretBigInteger(key.getPrimeP(), a))
+            .setQ(encodeSecretBigInteger(key.getPrimeQ(), a))
+            .setDp(encodeSecretBigInteger(key.getPrimeExponentP(), a))
+            .setDq(encodeSecretBigInteger(key.getPrimeExponentQ(), a))
+            .setCrt(encodeSecretBigInteger(key.getCrtCoefficient(), a))
+            .build();
+    return ProtoKeySerialization.create(
+        PRIVATE_TYPE_URL,
+        protoPrivateKey.toByteString(),
+        KeyMaterialType.ASYMMETRIC_PRIVATE,
+        VARIANT_CONVERTER.toProtoEnum(key.getParameters().getVariant()),
+        key.getIdRequirementOrNull());
+  }
+
+  private static BigInteger decodeBigInteger(ByteString data) {
+    return BigIntegerEncoding.fromUnsignedBigEndianBytes(data.toByteArray());
+  }
+
+  private static RsaSsaPssParameters parseParameters(ProtoParametersSerialization serialization)
+      throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to RsaSsaPssProtoSerialization.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.RsaSsaPssKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.RsaSsaPssKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing RsaSsaPssParameters failed: ", e);
+    }
+    return RsaSsaPssParameters.builder()
+        .setSigHashType(HASH_TYPE_CONVERTER.fromProtoEnum(format.getParams().getSigHash()))
+        .setMgf1HashType(HASH_TYPE_CONVERTER.fromProtoEnum(format.getParams().getMgf1Hash()))
+        .setPublicExponent(decodeBigInteger(format.getPublicExponent()))
+        .setModulusSizeBits(format.getModulusSizeInBits())
+        .setSaltLengthBytes(format.getParams().getSaltLength())
+        .setVariant(
+            VARIANT_CONVERTER.fromProtoEnum(serialization.getKeyTemplate().getOutputPrefixType()))
+        .build();
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static RsaSsaPssPublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to RsaSsaPssProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.RsaSsaPssPublicKey protoKey =
+          com.google.crypto.tink.proto.RsaSsaPssPublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      BigInteger modulus = decodeBigInteger(protoKey.getN());
+      int modulusSizeInBits = modulus.bitLength();
+      RsaSsaPssParameters parameters =
+          RsaSsaPssParameters.builder()
+              .setSigHashType(HASH_TYPE_CONVERTER.fromProtoEnum(protoKey.getParams().getSigHash()))
+              .setMgf1HashType(
+                  HASH_TYPE_CONVERTER.fromProtoEnum(protoKey.getParams().getMgf1Hash()))
+              .setPublicExponent(decodeBigInteger(protoKey.getE()))
+              .setModulusSizeBits(modulusSizeInBits)
+              .setSaltLengthBytes(protoKey.getParams().getSaltLength())
+              .setVariant(VARIANT_CONVERTER.fromProtoEnum(serialization.getOutputPrefixType()))
+              .build();
+      return RsaSsaPssPublicKey.builder()
+          .setParameters(parameters)
+          .setModulus(modulus)
+          .setIdRequirement(serialization.getIdRequirementOrNull())
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing RsaSsaPssPublicKey failed");
+    }
+  }
+
+  private static SecretBigInteger decodeSecretBigInteger(ByteString data, SecretKeyAccess access) {
+    return SecretBigInteger.fromBigInteger(
+        BigIntegerEncoding.fromUnsignedBigEndianBytes(data.toByteArray()), access);
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static RsaSsaPssPrivateKey parsePrivateKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PRIVATE_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to RsaSsaPssProtoSerialization.parsePrivateKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.RsaSsaPssPrivateKey protoKey =
+          com.google.crypto.tink.proto.RsaSsaPssPrivateKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      com.google.crypto.tink.proto.RsaSsaPssPublicKey protoPublicKey = protoKey.getPublicKey();
+
+      BigInteger modulus = decodeBigInteger(protoPublicKey.getN());
+      int modulusSizeInBits = modulus.bitLength();
+      BigInteger publicExponent = decodeBigInteger(protoPublicKey.getE());
+      RsaSsaPssParameters parameters =
+          RsaSsaPssParameters.builder()
+              .setSigHashType(
+                  HASH_TYPE_CONVERTER.fromProtoEnum(protoPublicKey.getParams().getSigHash()))
+              .setMgf1HashType(
+                  HASH_TYPE_CONVERTER.fromProtoEnum(protoPublicKey.getParams().getMgf1Hash()))
+              .setPublicExponent(publicExponent)
+              .setModulusSizeBits(modulusSizeInBits)
+              .setSaltLengthBytes(protoPublicKey.getParams().getSaltLength())
+              .setVariant(VARIANT_CONVERTER.fromProtoEnum(serialization.getOutputPrefixType()))
+              .build();
+      RsaSsaPssPublicKey publicKey =
+          RsaSsaPssPublicKey.builder()
+              .setParameters(parameters)
+              .setModulus(modulus)
+              .setIdRequirement(serialization.getIdRequirementOrNull())
+              .build();
+
+      SecretKeyAccess a = SecretKeyAccess.requireAccess(access);
+      return RsaSsaPssPrivateKey.builder()
+          .setPublicKey(publicKey)
+          .setPrimes(
+              decodeSecretBigInteger(protoKey.getP(), a),
+              decodeSecretBigInteger(protoKey.getQ(), a))
+          .setPrivateExponent(decodeSecretBigInteger(protoKey.getD(), a))
+          .setPrimeExponents(
+              decodeSecretBigInteger(protoKey.getDp(), a),
+              decodeSecretBigInteger(protoKey.getDq(), a))
+          .setCrtCoefficient(decodeSecretBigInteger(protoKey.getCrt(), a))
+          .build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing RsaSsaPssPrivateKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
+    registry.registerKeySerializer(PRIVATE_KEY_SERIALIZER);
+    registry.registerKeyParser(PRIVATE_KEY_PARSER);
+  }
+
+  private RsaSsaPssProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssPublicKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssPublicKey.java
new file mode 100644
index 0000000..175964f
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssPublicKey.java
@@ -0,0 +1,174 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a public key for the RSA SSA PSS signature primitive.
+ *
+ * <p>Standard: https://www.rfc-editor.org/rfc/rfc8017.txt
+ */
+public final class RsaSsaPssPublicKey extends SignaturePublicKey {
+  private final RsaSsaPssParameters parameters;
+  private final BigInteger modulus;
+  private final Bytes outputPrefix;
+  @Nullable private final Integer idRequirement;
+
+  /** Builder for RsaSsaPssPublicKey. */
+  public static class Builder {
+    @Nullable private RsaSsaPssParameters parameters = null;
+    @Nullable private BigInteger modulus = null;
+    @Nullable private Integer idRequirement = null;
+
+    private Builder() {}
+
+    @CanIgnoreReturnValue
+    public Builder setParameters(RsaSsaPssParameters parameters) {
+      this.parameters = parameters;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setModulus(BigInteger modulus) {
+      this.modulus = modulus;
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setIdRequirement(@Nullable Integer idRequirement) {
+      this.idRequirement = idRequirement;
+      return this;
+    }
+
+    private Bytes getOutputPrefix() {
+      if (parameters.getVariant() == RsaSsaPssParameters.Variant.NO_PREFIX) {
+        return Bytes.copyFrom(new byte[] {});
+      }
+      if (parameters.getVariant() == RsaSsaPssParameters.Variant.LEGACY
+          || parameters.getVariant() == RsaSsaPssParameters.Variant.CRUNCHY) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 0).putInt(idRequirement).array());
+      }
+      if (parameters.getVariant() == RsaSsaPssParameters.Variant.TINK) {
+        return Bytes.copyFrom(ByteBuffer.allocate(5).put((byte) 1).putInt(idRequirement).array());
+      }
+      throw new IllegalStateException(
+          "Unknown RsaSsaPssParameters.Variant: " + parameters.getVariant());
+    }
+
+    public RsaSsaPssPublicKey build() throws GeneralSecurityException {
+      if (parameters == null) {
+        throw new GeneralSecurityException("Cannot build without parameters");
+      }
+
+      if (modulus == null) {
+        throw new GeneralSecurityException("Cannot build without modulus");
+      }
+
+      int modulusSize = modulus.bitLength();
+      int paramModulusSize = parameters.getModulusSizeBits();
+      // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, B.3 requires p and q
+      // to be chosen such that 2^(paramModulusSize-1) < modulus < 2^paramModulusSize.
+      if (modulusSize != paramModulusSize) {
+        throw new GeneralSecurityException(
+            "Got modulus size "
+                + modulusSize
+                + ", but parameters requires modulus size "
+                + paramModulusSize);
+      }
+
+      if (parameters.hasIdRequirement() && idRequirement == null) {
+        throw new GeneralSecurityException(
+            "Cannot create key without ID requirement with parameters with ID requirement");
+      }
+
+      if (!parameters.hasIdRequirement() && idRequirement != null) {
+        throw new GeneralSecurityException(
+            "Cannot create key with ID requirement with parameters without ID requirement");
+      }
+      Bytes outputPrefix = getOutputPrefix();
+      return new RsaSsaPssPublicKey(parameters, modulus, outputPrefix, idRequirement);
+    }
+  }
+
+  private RsaSsaPssPublicKey(
+      RsaSsaPssParameters parameters,
+      BigInteger modulus,
+      Bytes outputPrefix,
+      @Nullable Integer idRequirement) {
+    this.parameters = parameters;
+    this.modulus = modulus;
+    this.outputPrefix = outputPrefix;
+    this.idRequirement = idRequirement;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns the underlying key bytes. */
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public BigInteger getModulus() {
+    return modulus;
+  }
+
+  @Override
+  public Bytes getOutputPrefix() {
+    return outputPrefix;
+  }
+
+  @Override
+  public RsaSsaPssParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return idRequirement;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof RsaSsaPssPublicKey)) {
+      return false;
+    }
+    RsaSsaPssPublicKey that = (RsaSsaPssPublicKey) o;
+    // Since outputPrefix is a function of parameters, we can ignore it here.
+    return that.parameters.equals(parameters)
+        && that.modulus.equals(modulus)
+        && Objects.equals(that.idRequirement, idRequirement);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManager.java
index f41cd1d..ec423c6 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManager.java
@@ -274,6 +274,7 @@
   public static void registerPair(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerAsymmetricKeyManagers(
         new RsaSsaPssSignKeyManager(), new RsaSsaPssVerifyKeyManager(), newKeyAllowed);
+    RsaSsaPssProtoSerialization.register();
   }
 
   /**
@@ -287,9 +288,7 @@
    *       <li>Public exponent: 65537 (aka F4).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}.
    *     </ul>
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PSS_3072_SHA256_F4")}
    */
-  @Deprecated
   public static final KeyTemplate rsa3072PssSha256F4Template() {
     return createKeyTemplate(
         HashType.SHA256,
@@ -312,9 +311,7 @@
    *     </ul>
    *     <p>Keys generated from this template create signatures compatible with OpenSSL and other
    *     libraries.
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PSS_3072_SHA256_F4_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawRsa3072PssSha256F4Template() {
     return createKeyTemplate(
         HashType.SHA256,
@@ -336,9 +333,7 @@
    *       <li>Public exponent: 65537 (aka F4).
    *       <li>Prefix type: {@link KeyTemplate.OutputPrefixType#TINK}.
    *     </ul>
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PSS_4096_SHA512_F4")}
    */
-  @Deprecated
   public static final KeyTemplate rsa4096PssSha512F4Template() {
     return createKeyTemplate(
         HashType.SHA512,
@@ -362,9 +357,7 @@
    *     </ul>
    *     <p>Keys generated from this template create signatures compatible with OpenSSL and other
    *     libraries.
-   * @deprecated use {@code KeyTemplates.get("RSA_SSA_PSS_4096_SHA512_F4_RAW")}
    */
-  @Deprecated
   public static final KeyTemplate rawRsa4096PssSha512F4Template() {
     return createKeyTemplate(
         HashType.SHA512,
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/SignatureConfig.java b/java_src/src/main/java/com/google/crypto/tink/signature/SignatureConfig.java
index 94fa596..73dccc8 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/SignatureConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/SignatureConfig.java
@@ -53,15 +53,22 @@
   public static final String RSA_PSS_PUBLIC_KEY_TYPE_URL =
       new RsaSsaPssVerifyKeyManager().getKeyType();
 
-  /** @deprecated */
-  @Deprecated public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
   /**
-   * @deprecated
-   * @since 1.1.0
+   * @deprecated Call {@link #register} instead.
    */
-  @Deprecated public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
+  @Deprecated
+  public static final RegistryConfig TINK_1_0_0 = RegistryConfig.getDefaultInstance();
 
-  /** @since 1.2.0 */
+  /**
+   * @deprecated Call {@link #register} instead.
+   */
+  @Deprecated
+  public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
+
+  /**
+   * @deprecated Call {@link #register} instead.
+   */
+  @Deprecated
   public static final RegistryConfig LATEST = RegistryConfig.getDefaultInstance();
 
   static {
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/SignatureKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/signature/SignatureKeyTemplates.java
index 95b9721..991f04e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/SignatureKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/SignatureKeyTemplates.java
@@ -35,6 +35,21 @@
  * Pre-generated {@link KeyTemplate} for {@link com.google.crypto.tink.PublicKeySign} and {@link
  * com.google.crypto.tink.PublicKeyVerify}.
  *
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
  * <p>One can use these templates to generate new {@link com.google.crypto.tink.proto.Keyset} with
  * {@link com.google.crypto.tink.KeysetHandle}. To generate a new keyset that contains a single
  * {@code EcdsaPrivateKey}, one can do:
@@ -47,10 +62,7 @@
  * }</pre>
  *
  * @since 1.0.0
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("ECDSA_P256")
  */
-@Deprecated
 public final class SignatureKeyTemplates {
   /**
    * A {@link KeyTemplate} that generates new instances of {@link
@@ -211,7 +223,9 @@
   /**
    * @return a {@link KeyTemplate} containing a {@link EcdsaKeyFormat} with some specified
    *     parameters.
+   * @deprecated Use a corresponding {@link EcdsaParameters} object instead.
    */
+  @Deprecated
   public static KeyTemplate createEcdsaKeyTemplate(
       HashType hashType,
       EllipticCurveType curve,
@@ -279,7 +293,9 @@
   /**
    * @return a {@link KeyTemplate} containing a {@link RsaSsaPkcs1KeyFormat} with some specified
    *     parameters.
+   * @deprecated Use a corresponding {@link RsaSsaPkcs1Parameters} object instead
    */
+  @Deprecated
   public static KeyTemplate createRsaSsaPkcs1KeyTemplate(
       HashType hashType, int modulusSize, BigInteger publicExponent, OutputPrefixType prefixType) {
     RsaSsaPkcs1Params params = RsaSsaPkcs1Params.newBuilder().setHashType(hashType).build();
@@ -331,7 +347,9 @@
   /**
    * @return a {@link KeyTemplate} containing a {@link RsaSsaPssKeyFormat} with some specified
    *     parameters.
+   * @deprecated Use a corresponding {@link RsaSsaPssParameters} object instead.
    */
+  @Deprecated
   public static KeyTemplate createRsaSsaPssKeyTemplate(
       HashType sigHash,
       HashType mgf1Hash,
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/SignatureParameters.java b/java_src/src/main/java/com/google/crypto/tink/signature/SignatureParameters.java
new file mode 100644
index 0000000..0b47156
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/SignatureParameters.java
@@ -0,0 +1,29 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.Immutable;
+
+/**
+ * Represents a description of a {@link SignaturePrivateKey} and the coresponding {@link
+ * SignaturePublicKey} excluding the randomly chosen key material.
+ */
+@Immutable
+@Alpha
+public abstract class SignatureParameters extends Parameters {}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePemKeysetReader.java b/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePemKeysetReader.java
index 4082e72..4330dbe 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePemKeysetReader.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePemKeysetReader.java
@@ -34,6 +34,7 @@
 import com.google.crypto.tink.proto.RsaSsaPssPublicKey;
 import com.google.crypto.tink.signature.internal.SigUtil;
 import com.google.crypto.tink.subtle.Random;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
@@ -42,11 +43,16 @@
 import java.security.interfaces.RSAPublicKey;
 import java.util.ArrayList;
 import java.util.List;
+import javax.annotation.Nullable;
 
 /**
  * SignaturePemKeysetReader is a {@link KeysetReader} that can read digital signature keys in PEM
  * format (RFC 7468).
  *
+ * <p>Only supports public keys.
+ *
+ * <p>Private, unknown or invalid keys are ignored.
+ *
  * <h3>Usage</h3>
  *
  * <pre>{@code
@@ -64,7 +70,7 @@
     this.pemKeys = pemKeys;
   }
 
-  /** @return a {@link Builder} for {@link SignaturePemKeysetReader}. */
+  /** Returns a {@link Builder} for {@link SignaturePemKeysetReader}. */
   public static Builder newBuilder() {
     return new Builder();
   }
@@ -87,6 +93,7 @@
      *
      * <p>The first key in the first added PEM is the primary key.
      */
+    @CanIgnoreReturnValue
     public Builder addPem(String pem, PemKeyType keyType) {
       PemKey pemKey = new PemKey();
       pemKey.reader = new BufferedReader(new StringReader(pem));
@@ -126,6 +133,7 @@
   }
 
   /** Reads a single PEM key from {@code reader}. Invalid or unparsable PEM would be ignored */
+  @Nullable
   private static Keyset.Key readKey(BufferedReader reader, PemKeyType pemKeyType)
       throws IOException {
     Key key = pemKeyType.readKey(reader);
@@ -139,7 +147,7 @@
     } else if (key instanceof ECPublicKey) {
       keyData = convertEcPublicKey(pemKeyType, (ECPublicKey) key);
     } else {
-      // TODO(thaidn): support RSA and EC private keys.
+      // Private keys are ignored.
       return null;
     }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePrivateKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePrivateKey.java
new file mode 100644
index 0000000..7fa026b
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePrivateKey.java
@@ -0,0 +1,66 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.PrivateKey;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SignaturePrivateKey} represents a digital signature primitive, which consists of a sign
+ * and a verify function.
+ *
+ * <p>The verify function is only available indirectly, with {@link #getPublicKey}.
+ */
+@Immutable
+@Alpha
+public abstract class SignaturePrivateKey extends Key implements PrivateKey {
+  /**
+   * Returns the {@link SignaturePublicKey}, which contains the verify function of the digital
+   * signature primitive.
+   */
+  @Override
+  public abstract SignaturePublicKey getPublicKey();
+
+  /**
+   * Returns a {@link Bytes} instance which is prefixed to every signature.
+   *
+   * <p>Returns the same as {@code getPublicKey().getOutputPrefix()}.
+   */
+  public final Bytes getOutputPrefix() {
+    return getPublicKey().getOutputPrefix();
+  }
+
+  @Override
+  @Nullable
+  public Integer getIdRequirementOrNull() {
+    return getPublicKey().getIdRequirementOrNull();
+  }
+
+  /**
+   * Returns the parameters of this key.
+   *
+   * <p>Returns the same as {@code getPublicKey().getParameters()}.
+   */
+  @Override
+  public SignatureParameters getParameters() {
+    return getPublicKey().getParameters();
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePublicKey.java b/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePublicKey.java
new file mode 100644
index 0000000..34afe8c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/SignaturePublicKey.java
@@ -0,0 +1,40 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.util.Bytes;
+import com.google.errorprone.annotations.Immutable;
+
+/**
+ * A SignaturePublicKey represents the verification portion of a digital signature primitive.
+ */
+@Immutable
+@Alpha
+public abstract class SignaturePublicKey extends Key {
+  /**
+   * Returns a {@link Bytes} instance which is prefixed to every signature.
+   */
+  public abstract Bytes getOutputPrefix();
+
+  /**
+   * Returns the parameters of this key.
+   */
+  @Override
+  public abstract SignatureParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/signature/internal/BUILD.bazel
index a3d70d9..01bb270 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/internal/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/internal/BUILD.bazel
@@ -14,7 +14,7 @@
         "//proto:rsa_ssa_pss_java_proto",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -28,6 +28,6 @@
         "//proto:rsa_ssa_pss_java_proto_lite",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:enums-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
diff --git a/java_src/src/main/java/com/google/crypto/tink/signature/internal/SigUtil.java b/java_src/src/main/java/com/google/crypto/tink/signature/internal/SigUtil.java
index 21f488f..40fb0d4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/signature/internal/SigUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/signature/internal/SigUtil.java
@@ -82,7 +82,7 @@
    */
   public static void validateRsaSsaPkcs1Params(RsaSsaPkcs1Params params)
       throws GeneralSecurityException {
-    toHashType(params.getHashType());
+    Enums.HashType unused = toHashType(params.getHashType());
   }
 
   /**
@@ -99,7 +99,7 @@
    */
   public static void validateRsaSsaPssParams(RsaSsaPssParams params)
       throws GeneralSecurityException {
-    toHashType(params.getSigHash());
+    Object unused = toHashType(params.getSigHash());
     if (params.getSigHash() != params.getMgf1Hash()) {
       throw new GeneralSecurityException("MGF1 hash is different from signature hash");
     }
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKey.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKey.java
new file mode 100644
index 0000000..4cdd320
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKey.java
@@ -0,0 +1,78 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+
+/**
+ * Represents a StreamingAead functions.
+ *
+ * <p>See https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming.
+ */
+public final class AesCtrHmacStreamingKey extends StreamingAeadKey {
+  private final AesCtrHmacStreamingParameters parameters;
+  private final SecretBytes initialKeymaterial;
+
+  private AesCtrHmacStreamingKey(
+      AesCtrHmacStreamingParameters parameters, SecretBytes initialKeymaterial) {
+    this.parameters = parameters;
+    this.initialKeymaterial = initialKeymaterial;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static AesCtrHmacStreamingKey create(
+      AesCtrHmacStreamingParameters parameters, SecretBytes initialKeymaterial)
+      throws GeneralSecurityException {
+
+    if (parameters.getKeySizeBytes() != initialKeymaterial.size()) {
+      throw new GeneralSecurityException("Key size mismatch");
+    }
+    return new AesCtrHmacStreamingKey(parameters, initialKeymaterial);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getInitialKeyMaterial() {
+    return initialKeymaterial;
+  }
+
+  @Override
+  public AesCtrHmacStreamingParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesCtrHmacStreamingKey)) {
+      return false;
+    }
+    AesCtrHmacStreamingKey that = (AesCtrHmacStreamingKey) o;
+    return that.parameters.equals(parameters)
+        && that.initialKeymaterial.equalsSecretBytes(initialKeymaterial);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManager.java
index 4177850..9ba58ca 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManager.java
@@ -163,8 +163,11 @@
   private static void validateParams(AesCtrHmacStreamingParams params)
       throws GeneralSecurityException {
     Validators.validateAesKeySize(params.getDerivedKeySize());
-    if (params.getHkdfHashType() == HashType.UNKNOWN_HASH) {
-      throw new GeneralSecurityException("unknown HKDF hash type");
+    if (params.getHkdfHashType() != HashType.SHA1
+        && params.getHkdfHashType() != HashType.SHA256
+        && params.getHkdfHashType() != HashType.SHA512) {
+      throw new GeneralSecurityException(
+          "Invalid HKDF hash type: " + params.getHkdfHashType().getNumber());
     }
     if (params.getHmacParams().getHash() == HashType.UNKNOWN_HASH) {
       throw new GeneralSecurityException("unknown HMAC hash type");
@@ -209,6 +212,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesCtrHmacStreamingKeyManager(), newKeyAllowed);
+    AesCtrHmacStreamingProtoSerialization.register();
   }
 
   /**
@@ -222,10 +226,7 @@
    *       <li>Tag size: 32 bytes
    *       <li>Ciphertext segment size: 4096
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_CTR_HMAC_SHA256_4KB")}
    */
-  @Deprecated
   public static final KeyTemplate aes128CtrHmacSha2564KBTemplate() {
     return createKeyTemplate(16, HashType.SHA256, 16, HashType.SHA256, 32, 4096);
   }
@@ -241,10 +242,7 @@
    *       <li>Tag size: 32 bytes
    *       <li>Ciphertext segment size: 1MB
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_CTR_HMAC_SHA256_1MB")}
    */
-  @Deprecated
   public static final KeyTemplate aes128CtrHmacSha2561MBTemplate() {
     return createKeyTemplate(16, HashType.SHA256, 16, HashType.SHA256, 32, 1 << 20);
   }
@@ -260,10 +258,7 @@
    *       <li>Tag size: 32 bytes
    *       <li>Ciphertext segment size: 4096
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_CTR_HMAC_SHA256_4KB")}
    */
-  @Deprecated
   public static final KeyTemplate aes256CtrHmacSha2564KBTemplate() {
     return createKeyTemplate(32, HashType.SHA256, 32, HashType.SHA256, 32, 4096);
   }
@@ -279,10 +274,7 @@
    *       <li>Tag size: 32 bytes
    *       <li>Ciphertext segment size: 1MB
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_CTR_HMAC_SHA256_1MB")}
    */
-  @Deprecated
   public static final KeyTemplate aes256CtrHmacSha2561MBTemplate() {
     return createKeyTemplate(32, HashType.SHA256, 32, HashType.SHA256, 32, 1 << 20);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingParameters.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingParameters.java
new file mode 100644
index 0000000..184df92
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingParameters.java
@@ -0,0 +1,283 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the parameters of a {@link AesCtrHmacStreamingKey}.
+ *
+ * <p>We refer to https://developers.google.com/tink/streaming-aead/aes_ctr_hmac_streaming for a
+ * complete description of the values.
+ */
+public class AesCtrHmacStreamingParameters extends StreamingAeadParameters {
+  /** Represents the hash type used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA1 = new HashType("SHA1");
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Helps creating new {@link AesCtrHmacStreamingParameters} objects.. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+
+    @Nullable private Integer derivedKeySizeBytes = null;
+
+    @Nullable private HashType hkdfHashType = null;
+
+    @Nullable private HashType hmacHashType = null;
+    @Nullable private Integer hmacTagSizeBytes = null;
+
+    @Nullable private Integer ciphertextSegmentSizeBytes = null;
+
+    /**
+     * Sets the size of the initial key material (used as input to HKDF).
+     *
+     * <p>Must be at least 16, and at least equal to the value set in {@link
+     * #setDerivedKeySizeBytes}
+     */
+    @CanIgnoreReturnValue
+    public Builder setKeySizeBytes(int keySizeBytes) {
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    /**
+     * Sets the size of the AES GCM key which will internally be derived.
+     *
+     * <p>Must be 16 or 32.
+     */
+    @CanIgnoreReturnValue
+    public Builder setDerivedKeySizeBytes(int derivedKeySizeBytes) {
+      this.derivedKeySizeBytes = derivedKeySizeBytes;
+      return this;
+    }
+
+    /** Sets the type of the hash function used in HKDF. */
+    @CanIgnoreReturnValue
+    public Builder setHkdfHashType(HashType hkdfHashType) {
+      this.hkdfHashType = hkdfHashType;
+      return this;
+    }
+
+    /** Sets the type of the hash function used in the HMAC. */
+    @CanIgnoreReturnValue
+    public Builder setHmacHashType(HashType hmacHashType) {
+      this.hmacHashType = hmacHashType;
+      return this;
+    }
+
+    /** Sets the size of the Hmac tag used. */
+    @CanIgnoreReturnValue
+    public Builder setHmacTagSizeBytes(Integer hmacTagSizeBytes) {
+      this.hmacTagSizeBytes = hmacTagSizeBytes;
+      return this;
+    }
+    /**
+     * Sets the size of a segment.
+     *
+     * <p>Must be at least equal 24 plus the value set in {@link #setDerivedKeySizeBytes}, and less
+     * than 2^31.
+     */
+    @CanIgnoreReturnValue
+    public Builder setCiphertextSegmentSizeBytes(int ciphertextSegmentSizeBytes) {
+      this.ciphertextSegmentSizeBytes = ciphertextSegmentSizeBytes;
+      return this;
+    }
+
+    /** Checks restrictions as on the devsite */
+    public AesCtrHmacStreamingParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("keySizeBytes needs to be set");
+      }
+      if (derivedKeySizeBytes == null) {
+        throw new GeneralSecurityException("derivedKeySizeBytes needs to be set");
+      }
+      if (hkdfHashType == null) {
+        throw new GeneralSecurityException("hkdfHashType needs to be set");
+      }
+      if (hmacHashType == null) {
+        throw new GeneralSecurityException("hmacHashType needs to be set");
+      }
+      if (hmacTagSizeBytes == null) {
+        throw new GeneralSecurityException("hmacTagSizeBytes needs to be set");
+      }
+      if (ciphertextSegmentSizeBytes == null) {
+        throw new GeneralSecurityException("ciphertextSegmentSizeBytes needs to be set");
+      }
+
+      if (derivedKeySizeBytes != 16 && derivedKeySizeBytes != 32) {
+        throw new GeneralSecurityException(
+            "derivedKeySizeBytes needs to be 16 or 32, not " + derivedKeySizeBytes);
+      }
+      if (keySizeBytes < derivedKeySizeBytes) {
+        throw new GeneralSecurityException(
+            "keySizeBytes needs to be at least derivedKeySizeBytes, i.e., " + derivedKeySizeBytes);
+      }
+      if (ciphertextSegmentSizeBytes <= derivedKeySizeBytes + hmacTagSizeBytes + 8) {
+        throw new GeneralSecurityException(
+            "ciphertextSegmentSizeBytes needs to be at least derivedKeySizeBytes + hmacTagSizeBytes"
+                + " + 9, i.e., "
+                + (derivedKeySizeBytes + hmacTagSizeBytes + 9));
+      }
+
+      int hmacTagSizeLowerBound = 10;
+      int hmacTagSizeUpperBound = 0;
+      if (hmacHashType == HashType.SHA1) {
+        hmacTagSizeUpperBound = 20;
+      }
+      if (hmacHashType == HashType.SHA256) {
+        hmacTagSizeUpperBound = 32;
+      }
+      if (hmacHashType == HashType.SHA512) {
+        hmacTagSizeUpperBound = 64;
+      }
+      if (hmacTagSizeBytes < hmacTagSizeLowerBound || hmacTagSizeBytes > hmacTagSizeUpperBound) {
+        throw new GeneralSecurityException(
+            "hmacTagSize must be in range ["
+                + hmacTagSizeLowerBound
+                + ", "
+                + hmacTagSizeUpperBound
+                + "], but is "
+                + hmacTagSizeBytes);
+      }
+      return new AesCtrHmacStreamingParameters(
+          keySizeBytes,
+          derivedKeySizeBytes,
+          hkdfHashType,
+          hmacHashType,
+          hmacTagSizeBytes,
+          ciphertextSegmentSizeBytes);
+    }
+  }
+
+  private final Integer keySizeBytes;
+  private final Integer derivedKeySizeBytes;
+  private final HashType hkdfHashType;
+  private final HashType hmacHashType;
+  private final Integer hmacTagSizeBytes;
+  private final Integer ciphertextSegmentSizeBytes;
+
+  private AesCtrHmacStreamingParameters(
+      Integer keySizeBytes,
+      Integer derivedKeySizeBytes,
+      HashType hkdfHashType,
+      HashType hmacHashType,
+      Integer hmacTagSizeBytes,
+      Integer ciphertextSegmentSizeBytes) {
+    this.keySizeBytes = keySizeBytes;
+    this.derivedKeySizeBytes = derivedKeySizeBytes;
+    this.hkdfHashType = hkdfHashType;
+    this.hmacHashType = hmacHashType;
+    this.hmacTagSizeBytes = hmacTagSizeBytes;
+    this.ciphertextSegmentSizeBytes = ciphertextSegmentSizeBytes;
+  }
+
+  /** Returns the size of the initial key material. */
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  /*  * Returns the size of the AES GCM key which will internally be derived. */
+  public int getDerivedKeySizeBytes() {
+    return derivedKeySizeBytes;
+  }
+
+  /** Returns the type of the hash function used in HKDF. */
+  public HashType getHkdfHashType() {
+    return hkdfHashType;
+  }
+
+  /** Returns the type of the hash function used in HMAC. */
+  public HashType getHmacHashType() {
+    return hmacHashType;
+  }
+
+  /** Returns the number of bytes used in the HMAC tag. */
+  public int getHmacTagSizeBytes() {
+    return hmacTagSizeBytes;
+  }
+
+  /** Returns the size a ciphertext segment has. */
+  public int getCiphertextSegmentSizeBytes() {
+    return ciphertextSegmentSizeBytes;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesCtrHmacStreamingParameters)) {
+      return false;
+    }
+    AesCtrHmacStreamingParameters that = (AesCtrHmacStreamingParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getDerivedKeySizeBytes() == getDerivedKeySizeBytes()
+        && that.getHkdfHashType() == getHkdfHashType()
+        && that.getHmacHashType() == getHmacHashType()
+        && that.getHmacTagSizeBytes() == getHmacTagSizeBytes()
+        && that.getCiphertextSegmentSizeBytes() == getCiphertextSegmentSizeBytes();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        AesCtrHmacStreamingParameters.class,
+        keySizeBytes,
+        derivedKeySizeBytes,
+        hkdfHashType,
+        hmacHashType,
+        hmacTagSizeBytes,
+        ciphertextSegmentSizeBytes);
+  }
+
+  @Override
+  public String toString() {
+    return "AesCtrHmacStreaming Parameters (IKM size: "
+        + keySizeBytes
+        + ", "
+        + derivedKeySizeBytes
+        + "-byte AES key, "
+        + hkdfHashType
+        + " for HKDF, "
+        + hkdfHashType
+        + " for HMAC, "
+        + hmacTagSizeBytes
+        + "-byte tags, "
+        + ciphertextSegmentSizeBytes
+        + "-byte ciphertexts)";
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingProtoSerialization.java
new file mode 100644
index 0000000..acbb697
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingProtoSerialization.java
@@ -0,0 +1,224 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HmacParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link AesCtrHmacStreamingKey} objects and {@link
+ * AesCtrHmacStreamingParameters} objects
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesCtrHmacStreamingProtoSerialization {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<
+          AesCtrHmacStreamingParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesCtrHmacStreamingProtoSerialization::serializeParameters,
+              AesCtrHmacStreamingParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesCtrHmacStreamingProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesCtrHmacStreamingKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesCtrHmacStreamingProtoSerialization::serializeKey,
+          AesCtrHmacStreamingKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesCtrHmacStreamingProtoSerialization::parseKey,
+          TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static HashType toProtoHashType(AesCtrHmacStreamingParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (AesCtrHmacStreamingParameters.HashType.SHA1.equals(hashType)) {
+      return HashType.SHA1;
+    }
+    if (AesCtrHmacStreamingParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (AesCtrHmacStreamingParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static AesCtrHmacStreamingParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA1:
+        return AesCtrHmacStreamingParameters.HashType.SHA1;
+      case SHA256:
+        return AesCtrHmacStreamingParameters.HashType.SHA256;
+      case SHA512:
+        return AesCtrHmacStreamingParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.AesCtrHmacStreamingParams toProtoParams(
+      AesCtrHmacStreamingParameters parameters) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+        .setCiphertextSegmentSize(parameters.getCiphertextSegmentSizeBytes())
+        .setDerivedKeySize(parameters.getDerivedKeySizeBytes())
+        .setHkdfHashType(toProtoHashType(parameters.getHkdfHashType()))
+        .setHmacParams(
+            HmacParams.newBuilder()
+                .setHash(toProtoHashType(parameters.getHmacHashType()))
+                .setTagSize(parameters.getHmacTagSizeBytes()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(
+      AesCtrHmacStreamingParameters parameters) throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .setParams(toProtoParams(parameters))
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      AesCtrHmacStreamingKey key, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getInitialKeyMaterial().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .setParams(toProtoParams(key.getParameters()))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        OutputPrefixType.RAW,
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesCtrHmacStreamingParameters toParametersObject(
+      com.google.crypto.tink.proto.AesCtrHmacStreamingParams params, int keySize)
+      throws GeneralSecurityException {
+    return AesCtrHmacStreamingParameters.builder()
+        .setKeySizeBytes(keySize)
+        .setDerivedKeySizeBytes(params.getDerivedKeySize())
+        .setCiphertextSegmentSizeBytes(params.getCiphertextSegmentSize())
+        .setHkdfHashType(toHashType(params.getHkdfHashType()))
+        .setHmacHashType(toHashType(params.getHmacParams().getHash()))
+        .setHmacTagSizeBytes(params.getHmacParams().getTagSize())
+        .build();
+  }
+
+  private static AesCtrHmacStreamingParameters parseParameters(
+      ProtoParametersSerialization serialization) throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesCtrHmacStreamingParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesCtrHmacStreamingParameters failed: ", e);
+    }
+    return toParametersObject(format.getParams(), format.getKeySize());
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static AesCtrHmacStreamingKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesCtrHmacStreamingParameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+          com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      AesCtrHmacStreamingParameters parameters =
+          toParametersObject(protoKey.getParams(), protoKey.getKeyValue().size());
+      return AesCtrHmacStreamingKey.create(
+          parameters,
+          SecretBytes.copyFrom(
+              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)));
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesCtrHmacStreamingKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesCtrHmacStreamingProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKey.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKey.java
new file mode 100644
index 0000000..4974bba
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKey.java
@@ -0,0 +1,78 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.RestrictedApi;
+import java.security.GeneralSecurityException;
+
+/**
+ * Represents a StreamingAead functions.
+ *
+ * <p>See https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming.
+ */
+public final class AesGcmHkdfStreamingKey extends StreamingAeadKey {
+  private final AesGcmHkdfStreamingParameters parameters;
+  private final SecretBytes initialKeymaterial;
+
+  private AesGcmHkdfStreamingKey(
+      AesGcmHkdfStreamingParameters parameters, SecretBytes initialKeymaterial) {
+    this.parameters = parameters;
+    this.initialKeymaterial = initialKeymaterial;
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public static AesGcmHkdfStreamingKey create(
+      AesGcmHkdfStreamingParameters parameters, SecretBytes initialKeymaterial)
+      throws GeneralSecurityException {
+
+    if (parameters.getKeySizeBytes() != initialKeymaterial.size()) {
+      throw new GeneralSecurityException("Key size mismatch");
+    }
+    return new AesGcmHkdfStreamingKey(parameters, initialKeymaterial);
+  }
+
+  @RestrictedApi(
+      explanation = "Accessing parts of keys can produce unexpected incompatibilities, annotate the function with @AccessesPartialKey",
+      link = "https://developers.google.com/tink/design/access_control#accessing_partial_keys",
+      allowedOnPath = ".*Test\\.java",
+      allowlistAnnotations = {AccessesPartialKey.class})
+  public SecretBytes getInitialKeyMaterial() {
+    return initialKeymaterial;
+  }
+
+  @Override
+  public AesGcmHkdfStreamingParameters getParameters() {
+    return parameters;
+  }
+
+  @Override
+  public boolean equalsKey(Key o) {
+    if (!(o instanceof AesGcmHkdfStreamingKey)) {
+      return false;
+    }
+    AesGcmHkdfStreamingKey that = (AesGcmHkdfStreamingKey) o;
+    return that.parameters.equals(parameters)
+        && that.initialKeymaterial.equalsSecretBytes(initialKeymaterial);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManager.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManager.java
index ea81460..53b42fc 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManager.java
@@ -128,10 +128,7 @@
         Validators.validateVersion(format.getVersion(), getVersion());
         byte[] pseudorandomness = new byte[format.getKeySize()];
         try {
-          int read = inputStream.read(pseudorandomness);
-          if (read != format.getKeySize()) {
-            throw new GeneralSecurityException("Not enough pseudorandomness given");
-          }
+          readFully(inputStream, pseudorandomness);
           return AesGcmHkdfStreamingKey.newBuilder()
               .setKeyValue(ByteString.copyFrom(pseudorandomness))
               .setParams(format.getParams())
@@ -172,8 +169,10 @@
   private static void validateParams(AesGcmHkdfStreamingParams params)
       throws GeneralSecurityException {
     Validators.validateAesKeySize(params.getDerivedKeySize());
-    if (params.getHkdfHashType() == HashType.UNKNOWN_HASH) {
-      throw new GeneralSecurityException("unknown HKDF hash type");
+    if (params.getHkdfHashType() != HashType.SHA1
+        && params.getHkdfHashType() != HashType.SHA256
+        && params.getHkdfHashType() != HashType.SHA512) {
+      throw new GeneralSecurityException("Invalid HKDF hash type");
     }
     if (params.getCiphertextSegmentSize()
         < params.getDerivedKeySize() + NONCE_PREFIX_IN_BYTES + TAG_SIZE_IN_BYTES + 2) {
@@ -185,6 +184,7 @@
 
   public static void register(boolean newKeyAllowed) throws GeneralSecurityException {
     Registry.registerKeyManager(new AesGcmHkdfStreamingKeyManager(), newKeyAllowed);
+    AesGcmHkdfStreamingProtoSerialization.register();
   }
 
   /**
@@ -196,10 +196,7 @@
    *       <li>Size of AES-GCM derived keys: 16 bytes
    *       <li>Ciphertext segment size: 4096 bytes
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_GCM_HKDF_4KB")}
    */
-  @Deprecated
   public static final KeyTemplate aes128GcmHkdf4KBTemplate() {
     return createKeyTemplate(16, HashType.SHA256, 16, 4096);
   }
@@ -213,10 +210,7 @@
    *       <li>Size of AES-GCM derived keys: 16 bytes
    *       <li>Ciphertext segment size: 1MB
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES128_GCM_HKDF_1MB")}
    */
-  @Deprecated
   public static final KeyTemplate aes128GcmHkdf1MBTemplate() {
     return createKeyTemplate(16, HashType.SHA256, 16, 1 << 20);
   }
@@ -230,10 +224,7 @@
    *       <li>Size of AES-GCM derived keys: 32 bytes
    *       <li>Ciphertext segment size: 4096 bytes
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_GCM_HKDF_4KB")}
    */
-  @Deprecated
   public static final KeyTemplate aes256GcmHkdf4KBTemplate() {
     return createKeyTemplate(32, HashType.SHA256, 32, 4096);
   }
@@ -247,10 +238,7 @@
    *       <li>Size of AES-GCM derived keys: 32 bytes
    *       <li>Ciphertext segment size: 1MB
    *     </ul>
-   *
-   * @deprecated use {@code KeyTemplates.get("AES256_GCM_HKDF_1MB")}
    */
-  @Deprecated
   public static final KeyTemplate aes256GcmHkdf1MBTemplate() {
     return createKeyTemplate(32, HashType.SHA256, 32, 1 << 20);
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingParameters.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingParameters.java
new file mode 100644
index 0000000..5a0666e
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingParameters.java
@@ -0,0 +1,208 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents the parameters of a {@link AesGcmHkdfStreamingKey}.
+ *
+ * <p>We refer to https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming for a
+ * complete description of the values.
+ */
+public class AesGcmHkdfStreamingParameters extends StreamingAeadParameters {
+  /** Represents the hash type used. */
+  @Immutable
+  public static final class HashType {
+    public static final HashType SHA1 = new HashType("SHA1");
+    public static final HashType SHA256 = new HashType("SHA256");
+    public static final HashType SHA512 = new HashType("SHA512");
+
+    private final String name;
+
+    private HashType(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Helps creating new {@link AesGcmHkdfStreamingParameters} objects. */
+  public static final class Builder {
+    @Nullable private Integer keySizeBytes = null;
+
+    @Nullable private Integer derivedAesGcmKeySizeBytes = null;
+
+    @Nullable private HashType hkdfHashType = null;
+
+    @Nullable private Integer ciphertextSegmentSizeBytes = null;
+
+    /**
+     * Sets the size of the initial key material (used as input to HKDF).
+     *
+     * <p>Must be at least 16, and at least equal to the value set in {@link
+     * #setDerivedAesGcmKeySizeBytes}
+     */
+    public Builder setKeySizeBytes(int keySizeBytes) {
+      this.keySizeBytes = keySizeBytes;
+      return this;
+    }
+
+    /**
+     * Sets the size of the AES GCM key which will internally be derived.
+     *
+     * <p>Must be 16 or 32.
+     */
+    public Builder setDerivedAesGcmKeySizeBytes(int derivedAesGcmKeySizeBytes) {
+      this.derivedAesGcmKeySizeBytes = derivedAesGcmKeySizeBytes;
+      return this;
+    }
+
+    /** Sets the type of the hash function used in HKDF. */
+    public Builder setHkdfHashType(HashType hkdfHashType) {
+      this.hkdfHashType = hkdfHashType;
+      return this;
+    }
+
+    /**
+     * Sets the size of a segment.
+     *
+     * <p>Must be at least equal 24 plus the value set in {@link #setDerivedAesGcmKeySizeBytes}, and
+     * less than 2^31.
+     */
+    public Builder setCiphertextSegmentSizeBytes(int ciphertextSegmentSizeBytes) {
+      this.ciphertextSegmentSizeBytes = ciphertextSegmentSizeBytes;
+      return this;
+    }
+
+    /** Checks restrictions as on the devsite */
+    public AesGcmHkdfStreamingParameters build() throws GeneralSecurityException {
+      if (keySizeBytes == null) {
+        throw new GeneralSecurityException("keySizeBytes needs to be set");
+      }
+      if (derivedAesGcmKeySizeBytes == null) {
+        throw new GeneralSecurityException("derivedAesGcmKeySizeBytes needs to be set");
+      }
+      if (hkdfHashType == null) {
+        throw new GeneralSecurityException("hkdfHashType needs to be set");
+      }
+      if (ciphertextSegmentSizeBytes == null) {
+        throw new GeneralSecurityException("ciphertextSegmentSizeBytes needs to be set");
+      }
+
+      if (derivedAesGcmKeySizeBytes != 16 && derivedAesGcmKeySizeBytes != 32) {
+        throw new GeneralSecurityException(
+            "derivedAesGcmKeySizeBytes needs to be 16 or 32, not " + derivedAesGcmKeySizeBytes);
+      }
+      if (keySizeBytes < derivedAesGcmKeySizeBytes) {
+        throw new GeneralSecurityException(
+            "keySizeBytes needs to be at least derivedAesGcmKeySizeBytes, i.e., "
+                + derivedAesGcmKeySizeBytes);
+      }
+      if (ciphertextSegmentSizeBytes <= derivedAesGcmKeySizeBytes + 24) {
+        throw new GeneralSecurityException(
+            "ciphertextSegmentSizeBytes needs to be at least derivedAesGcmKeySizeBytes + 25, i.e., "
+                + (derivedAesGcmKeySizeBytes + 25));
+      }
+      return new AesGcmHkdfStreamingParameters(
+          keySizeBytes, derivedAesGcmKeySizeBytes, hkdfHashType, ciphertextSegmentSizeBytes);
+    }
+  }
+
+  private final Integer keySizeBytes;
+  private final Integer derivedAesGcmKeySizeBytes;
+  private final HashType hkdfHashType;
+  private final Integer ciphertextSegmentSizeBytes;
+
+  private AesGcmHkdfStreamingParameters(
+      Integer keySizeBytes,
+      Integer derivedAesGcmKeySizeBytes,
+      HashType hkdfHashType,
+      Integer ciphertextSegmentSizeBytes) {
+    this.keySizeBytes = keySizeBytes;
+    this.derivedAesGcmKeySizeBytes = derivedAesGcmKeySizeBytes;
+    this.hkdfHashType = hkdfHashType;
+    this.ciphertextSegmentSizeBytes = ciphertextSegmentSizeBytes;
+  }
+
+  /** Returns the size of the initial key material. */
+  public int getKeySizeBytes() {
+    return keySizeBytes;
+  }
+
+  /** Returns the size of the AES GCM key which will internally be derived. */
+  public int getDerivedAesGcmKeySizeBytes() {
+    return derivedAesGcmKeySizeBytes;
+  }
+
+  /** Returns the type of the hash function used in HKDF. */
+  public HashType getHkdfHashType() {
+    return hkdfHashType;
+  }
+
+  /** Returns the size a ciphertext segment has. */
+  public int getCiphertextSegmentSizeBytes() {
+    return ciphertextSegmentSizeBytes;
+  }
+
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AesGcmHkdfStreamingParameters)) {
+      return false;
+    }
+    AesGcmHkdfStreamingParameters that = (AesGcmHkdfStreamingParameters) o;
+    return that.getKeySizeBytes() == getKeySizeBytes()
+        && that.getDerivedAesGcmKeySizeBytes() == getDerivedAesGcmKeySizeBytes()
+        && that.getHkdfHashType() == getHkdfHashType()
+        && that.getCiphertextSegmentSizeBytes() == getCiphertextSegmentSizeBytes();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        AesGcmHkdfStreamingParameters.class,
+        keySizeBytes,
+        derivedAesGcmKeySizeBytes,
+        hkdfHashType,
+        ciphertextSegmentSizeBytes);
+  }
+
+  @Override
+  public String toString() {
+    return "AesGcmHkdfStreaming Parameters (IKM size: "
+        + keySizeBytes
+        + ", "
+        + derivedAesGcmKeySizeBytes
+        + "-byte AES GCM key, "
+        + hkdfHashType
+        + " for HKDF "
+        + ciphertextSegmentSizeBytes
+        + "-byte ciphertexts)";
+  }
+
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingProtoSerialization.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingProtoSerialization.java
new file mode 100644
index 0000000..3c9c11c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingProtoSerialization.java
@@ -0,0 +1,220 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
+
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ParametersParser;
+import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+
+/**
+ * Methods to serialize and parse {@link AesGcmHkdfStreamingKey} objects and {@link
+ * AesGcmHkdfStreamingParameters} objects
+ */
+@AccessesPartialKey
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+final class AesGcmHkdfStreamingProtoSerialization {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey";
+  private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
+
+  private static final ParametersSerializer<
+          AesGcmHkdfStreamingParameters, ProtoParametersSerialization>
+      PARAMETERS_SERIALIZER =
+          ParametersSerializer.create(
+              AesGcmHkdfStreamingProtoSerialization::serializeParameters,
+              AesGcmHkdfStreamingParameters.class,
+              ProtoParametersSerialization.class);
+
+  private static final ParametersParser<ProtoParametersSerialization> PARAMETERS_PARSER =
+      ParametersParser.create(
+          AesGcmHkdfStreamingProtoSerialization::parseParameters,
+          TYPE_URL_BYTES,
+          ProtoParametersSerialization.class);
+
+  private static final KeySerializer<AesGcmHkdfStreamingKey, ProtoKeySerialization> KEY_SERIALIZER =
+      KeySerializer.create(
+          AesGcmHkdfStreamingProtoSerialization::serializeKey,
+          AesGcmHkdfStreamingKey.class,
+          ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> KEY_PARSER =
+      KeyParser.create(
+          AesGcmHkdfStreamingProtoSerialization::parseKey,
+          TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
+  private static HashType toProtoHashType(AesGcmHkdfStreamingParameters.HashType hashType)
+      throws GeneralSecurityException {
+    if (AesGcmHkdfStreamingParameters.HashType.SHA1.equals(hashType)) {
+      return HashType.SHA1;
+    }
+    if (AesGcmHkdfStreamingParameters.HashType.SHA256.equals(hashType)) {
+      return HashType.SHA256;
+    }
+    if (AesGcmHkdfStreamingParameters.HashType.SHA512.equals(hashType)) {
+      return HashType.SHA512;
+    }
+    throw new GeneralSecurityException("Unable to serialize HashType " + hashType);
+  }
+
+  private static AesGcmHkdfStreamingParameters.HashType toHashType(HashType hashType)
+      throws GeneralSecurityException {
+    switch (hashType) {
+      case SHA1:
+        return AesGcmHkdfStreamingParameters.HashType.SHA1;
+      case SHA256:
+        return AesGcmHkdfStreamingParameters.HashType.SHA256;
+      case SHA512:
+        return AesGcmHkdfStreamingParameters.HashType.SHA512;
+      default:
+        throw new GeneralSecurityException(
+            "Unable to parse HashType: " + hashType.getNumber());
+    }
+  }
+
+  private static com.google.crypto.tink.proto.AesGcmHkdfStreamingParams toProtoParams(
+      AesGcmHkdfStreamingParameters parameters) throws GeneralSecurityException {
+    return com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+        .setCiphertextSegmentSize(parameters.getCiphertextSegmentSizeBytes())
+        .setDerivedKeySize(parameters.getDerivedAesGcmKeySizeBytes())
+        .setHkdfHashType(toProtoHashType(parameters.getHkdfHashType()))
+        .build();
+  }
+
+  private static ProtoParametersSerialization serializeParameters(
+      AesGcmHkdfStreamingParameters parameters) throws GeneralSecurityException {
+    return ProtoParametersSerialization.create(
+        KeyTemplate.newBuilder()
+            .setTypeUrl(TYPE_URL)
+            .setValue(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                    .setKeySize(parameters.getKeySizeBytes())
+                    .setParams(toProtoParams(parameters))
+                    .build()
+                    .toByteString())
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build());
+  }
+
+  private static ProtoKeySerialization serializeKey(
+      AesGcmHkdfStreamingKey key, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        TYPE_URL,
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setKeyValue(
+                ByteString.copyFrom(
+                    key.getInitialKeyMaterial().toByteArray(SecretKeyAccess.requireAccess(access))))
+            .setParams(toProtoParams(key.getParameters()))
+            .build()
+            .toByteString(),
+        KeyMaterialType.SYMMETRIC,
+        OutputPrefixType.RAW,
+        key.getIdRequirementOrNull());
+  }
+
+  private static AesGcmHkdfStreamingParameters toParametersObject(
+      com.google.crypto.tink.proto.AesGcmHkdfStreamingParams params, int keySize)
+      throws GeneralSecurityException {
+    return AesGcmHkdfStreamingParameters.builder()
+        .setKeySizeBytes(keySize)
+        .setDerivedAesGcmKeySizeBytes(params.getDerivedKeySize())
+        .setCiphertextSegmentSizeBytes(params.getCiphertextSegmentSize())
+        .setHkdfHashType(toHashType(params.getHkdfHashType()))
+        .build();
+  }
+
+  private static AesGcmHkdfStreamingParameters parseParameters(
+      ProtoParametersSerialization serialization) throws GeneralSecurityException {
+    if (!serialization.getKeyTemplate().getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesGcmHkdfStreamingParameters.parseParameters: "
+              + serialization.getKeyTemplate().getTypeUrl());
+    }
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat format;
+    try {
+      format =
+          com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.parseFrom(
+              serialization.getKeyTemplate().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesGcmHkdfStreamingParameters failed: ", e);
+    }
+    if (format.getVersion() != 0) {
+      throw new GeneralSecurityException("Only version 0 parameters are accepted");
+    }
+    return toParametersObject(format.getParams(), format.getKeySize());
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static AesGcmHkdfStreamingKey parseKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to AesGcmHkdfStreamingParameters.parseParameters");
+    }
+    try {
+      com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+          com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      AesGcmHkdfStreamingParameters parameters =
+          toParametersObject(protoKey.getParams(), protoKey.getKeyValue().size());
+      return AesGcmHkdfStreamingKey.create(
+          parameters,
+          SecretBytes.copyFrom(
+              protoKey.getKeyValue().toByteArray(), SecretKeyAccess.requireAccess(access)));
+    } catch (InvalidProtocolBufferException e) {
+      throw new GeneralSecurityException("Parsing AesGcmHkdfStreamingKey failed");
+    }
+  }
+
+  public static void register() throws GeneralSecurityException {
+    register(MutableSerializationRegistry.globalInstance());
+  }
+
+  public static void register(MutableSerializationRegistry registry)
+      throws GeneralSecurityException {
+    registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
+    registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(KEY_SERIALIZER);
+    registry.registerKeyParser(KEY_PARSER);
+  }
+
+  private AesGcmHkdfStreamingProtoSerialization() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/streamingaead/BUILD.bazel
index 12bba89..24453ca 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/BUILD.bazel
@@ -42,8 +42,8 @@
     deps = [
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
-        "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -66,7 +66,6 @@
         ":seekable_byte_channel_decrypter",
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
-        "@maven//:androidx_annotation_annotation",
     ],
 )
 
@@ -75,7 +74,6 @@
     srcs = ["StreamingAeadFactory.java"],
     deps = [
         ":streaming_aead_wrapper",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
     ],
@@ -85,6 +83,7 @@
     name = "aes_gcm_hkdf_streaming_key_manager",
     srcs = ["AesGcmHkdfStreamingKeyManager.java"],
     deps = [
+        ":aes_gcm_hkdf_streaming_proto_serialization",
         ":streaming_aead_util",
         "//proto:aes_gcm_hkdf_streaming_java_proto",
         "//proto:common_java_proto",
@@ -97,7 +96,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_hkdf_streaming",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -105,6 +104,7 @@
     name = "aes_ctr_hmac_streaming_key_manager",
     srcs = ["AesCtrHmacStreamingKeyManager.java"],
     deps = [
+        ":aes_ctr_hmac_streaming_proto_serialization",
         ":streaming_aead_util",
         "//proto:aes_ctr_hmac_streaming_java_proto",
         "//proto:common_java_proto",
@@ -118,7 +118,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:validators",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -145,7 +145,80 @@
     ],
 )
 
-# Android libraries
+java_library(
+    name = "streaming_aead_key",
+    srcs = ["StreamingAeadKey.java"],
+    deps = [
+        ":streaming_aead_parameters",
+        "//src/main/java/com/google/crypto/tink:key",
+        "@maven//:com_google_code_findbugs_jsr305",
+    ],
+)
+
+java_library(
+    name = "streaming_aead_parameters",
+    srcs = ["StreamingAeadParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters"],
+)
+
+java_library(
+    name = "aes_gcm_hkdf_streaming_parameters",
+    srcs = ["AesGcmHkdfStreamingParameters.java"],
+    deps = [
+        ":streaming_aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_hkdf_streaming_key",
+    srcs = ["AesGcmHkdfStreamingKey.java"],
+    deps = [
+        ":aes_gcm_hkdf_streaming_parameters",
+        ":streaming_aead_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_gcm_hkdf_streaming_proto_serialization",
+    srcs = ["AesGcmHkdfStreamingProtoSerialization.java"],
+    deps = [
+        ":aes_gcm_hkdf_streaming_key",
+        ":aes_gcm_hkdf_streaming_parameters",
+        "//proto:aes_gcm_hkdf_streaming_java_proto",
+        "//proto:common_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+java_library(
+    name = "aes_ctr_hmac_streaming_parameters",
+    srcs = ["AesCtrHmacStreamingParameters.java"],
+    deps = [
+        ":streaming_aead_parameters",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
 
 android_library(
     name = "streaming_aead_util-android",
@@ -185,8 +258,8 @@
     deps = [
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
-        "@maven//:androidx_annotation_annotation",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -209,7 +282,6 @@
         ":seekable_byte_channel_decrypter-android",
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
-        "@maven//:androidx_annotation_annotation",
     ],
 )
 
@@ -218,7 +290,6 @@
     srcs = ["StreamingAeadFactory.java"],
     deps = [
         ":streaming_aead_wrapper-android",
-        "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
     ],
@@ -228,6 +299,7 @@
     name = "aes_gcm_hkdf_streaming_key_manager-android",
     srcs = ["AesGcmHkdfStreamingKeyManager.java"],
     deps = [
+        ":aes_gcm_hkdf_streaming_proto_serialization-android",
         ":streaming_aead_util-android",
         "//proto:aes_gcm_hkdf_streaming_java_proto_lite",
         "//proto:common_java_proto_lite",
@@ -240,7 +312,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_hkdf_streaming-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -248,6 +320,7 @@
     name = "aes_ctr_hmac_streaming_key_manager-android",
     srcs = ["AesCtrHmacStreamingKeyManager.java"],
     deps = [
+        ":aes_ctr_hmac_streaming_proto_serialization-android",
         ":streaming_aead_util-android",
         "//proto:aes_ctr_hmac_streaming_java_proto_lite",
         "//proto:common_java_proto_lite",
@@ -261,7 +334,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
         "//src/main/java/com/google/crypto/tink/subtle:validators-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -287,3 +360,178 @@
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
     ],
 )
+
+android_library(
+    name = "streaming_aead_key-android",
+    srcs = ["StreamingAeadKey.java"],
+    deps = [
+        ":streaming_aead_parameters-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+    ],
+)
+
+android_library(
+    name = "streaming_aead_parameters-android",
+    srcs = ["StreamingAeadParameters.java"],
+    deps = ["//src/main/java/com/google/crypto/tink:parameters-android"],
+)
+
+android_library(
+    name = "aes_gcm_hkdf_streaming_parameters-android",
+    srcs = ["AesGcmHkdfStreamingParameters.java"],
+    deps = [
+        ":streaming_aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_hkdf_streaming_key-android",
+    srcs = ["AesGcmHkdfStreamingKey.java"],
+    deps = [
+        ":aes_gcm_hkdf_streaming_parameters-android",
+        ":streaming_aead_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_gcm_hkdf_streaming_proto_serialization-android",
+    srcs = ["AesGcmHkdfStreamingProtoSerialization.java"],
+    deps = [
+        ":aes_gcm_hkdf_streaming_key-android",
+        ":aes_gcm_hkdf_streaming_parameters-android",
+        "//proto:aes_gcm_hkdf_streaming_java_proto_lite",
+        "//proto:common_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+android_library(
+    name = "aes_ctr_hmac_streaming_parameters-android",
+    srcs = ["AesCtrHmacStreamingParameters.java"],
+    deps = [
+        ":streaming_aead_parameters-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_ctr_hmac_streaming_key-android",
+    srcs = ["AesCtrHmacStreamingKey.java"],
+    deps = [
+        ":aes_ctr_hmac_streaming_parameters-android",
+        ":streaming_aead_key-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:key-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+java_library(
+    name = "aes_ctr_hmac_streaming_key",
+    srcs = ["AesCtrHmacStreamingKey.java"],
+    deps = [
+        ":aes_ctr_hmac_streaming_parameters",
+        ":streaming_aead_key",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "aes_ctr_hmac_streaming_proto_serialization-android",
+    srcs = ["AesCtrHmacStreamingProtoSerialization.java"],
+    deps = [
+        ":aes_ctr_hmac_streaming_key-android",
+        ":aes_ctr_hmac_streaming_parameters-android",
+        "//proto:aes_ctr_hmac_streaming_java_proto_lite",
+        "//proto:common_java_proto_lite",
+        "//proto:hmac_java_proto_lite",
+        "//proto:tink_java_proto_lite",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
+        "//src/main/java/com/google/crypto/tink/internal:util-android",
+        "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_javalite",
+    ],
+)
+
+java_library(
+    name = "aes_ctr_hmac_streaming_proto_serialization",
+    srcs = ["AesCtrHmacStreamingProtoSerialization.java"],
+    deps = [
+        ":aes_ctr_hmac_streaming_key",
+        ":aes_ctr_hmac_streaming_parameters",
+        "//proto:aes_ctr_hmac_streaming_java_proto",
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
+        "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+    ],
+)
+
+android_library(
+    name = "predefined_streaming_aead_parameters-android",
+    srcs = ["PredefinedStreamingAeadParameters.java"],
+    deps = [
+        ":aes_ctr_hmac_streaming_parameters-android",
+        ":aes_gcm_hkdf_streaming_parameters-android",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception-android",
+    ],
+)
+
+java_library(
+    name = "predefined_streaming_aead_parameters",
+    srcs = ["PredefinedStreamingAeadParameters.java"],
+    deps = [
+        ":aes_ctr_hmac_streaming_parameters",
+        ":aes_gcm_hkdf_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/InputStreamDecrypter.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/InputStreamDecrypter.java
index 0ad3494..2778a33 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/InputStreamDecrypter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/InputStreamDecrypter.java
@@ -16,7 +16,6 @@
 
 package com.google.crypto.tink.streamingaead;
 
-import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.StreamingAead;
 import java.io.BufferedInputStream;
 import java.io.IOException;
@@ -36,27 +35,30 @@
   @GuardedBy("this")
   InputStream ciphertextStream;
 
-  PrimitiveSet<StreamingAead> primitives;
+  List<StreamingAead> primitives;
   byte[] associatedData;
 
   /**
    * Constructs a new decrypter for {@code ciphertextStream}.
    *
-   * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives},
-   * and uses it for decryption.  The matching happens as follows:
-   * upon first {@code read()}-call each candidate primitive reads an initial portion
-   * of the stream, until it can determine whether the stream matches the key of the primitive.
-   * If a canditate does not match, then the stream is reset to its initial position,
-   * and the next candiate can attempt matching.  The first successful candidate
-   * is then used exclusively on subsequent {@code read()}-calls.
+   * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives}, and
+   * uses it for decryption. The matching happens as follows: upon first {@code read()}-call each
+   * candidate primitive reads an initial portion of the stream, until it can determine whether the
+   * stream matches the key of the primitive. If a canditate does not match, then the stream is
+   * reset to its initial position, and the next candiate can attempt matching. The first successful
+   * candidate is then used exclusively on subsequent {@code read()}-calls.
    *
-   * <p> The matching process wraps {@code ciphertextStream} into a BufferedInputStream,
-   * unless ciphertextStream supports rewinding (i.e. ciphertextStream.markSupported() == true).
-   * Buffering of the ciphertext is disabled once a ciphertext block has been successfully
-   * decrypted.
+   * <p>The matching process wraps {@code ciphertextStream} into a BufferedInputStream, unless
+   * ciphertextStream supports rewinding (i.e. ciphertextStream.markSupported() == true). Buffering
+   * of the ciphertext is disabled once a ciphertext block has been successfully decrypted.
+   *
+   * @param primitives a list of possible {@link StreamingAeads} to try. Must not be mutateted by
+   * the caller. Will not be muteted by the {@code InputStreamDecrypter},
+   * @param ciphertextStream the stream with the ciphertext
+   * @param associatedData The assocated data with this encryption
    */
-  public InputStreamDecrypter(PrimitiveSet<StreamingAead> primitives,
-      InputStream ciphertextStream, final byte[] associatedData) {
+  public InputStreamDecrypter(
+      List<StreamingAead> primitives, InputStream ciphertextStream, final byte[] associatedData) {
     this.attemptedMatching = false;
     this.matchingStream = null;
     this.primitives = primitives;
@@ -143,11 +145,10 @@
         throw new IOException("No matching key found for the ciphertext in the stream.");
       }
       attemptedMatching = true;
-      List<PrimitiveSet.Entry<StreamingAead>> entries = primitives.getRawPrimitives();
-      for (PrimitiveSet.Entry<StreamingAead> entry : entries) {
+      for (StreamingAead streamingAead : primitives) {
         try {
           InputStream attemptedStream =
-              entry.getPrimitive().newDecryptingStream(ciphertextStream, associatedData);
+              streamingAead.newDecryptingStream(ciphertextStream, associatedData);
           int retValue = attemptedStream.read(b, offset, len);
           if (retValue == 0) {
             // Read should never return 0 when len > 0.
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/PredefinedStreamingAeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/PredefinedStreamingAeadParameters.java
new file mode 100644
index 0000000..9051f72
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/PredefinedStreamingAeadParameters.java
@@ -0,0 +1,212 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+/**
+ * Pre-generated {@link Parameter} objects for {@link com.google.crypto.tink.DeterministicAead}
+ * keys.
+ *
+ * <p>Note: if you want to keep dependencies small, consider inlining the constants here.
+ */
+public final class PredefinedStreamingAeadParameters {
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCtrHmacStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 16 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-CTR derived keys: 16 bytes
+   *   <li>Tag algo: HMAC-SHA256
+   *   <li>Tag size: 32 bytes
+   *   <li>Ciphertext segment size: 4096
+   * </ul>
+   */
+  public static final AesCtrHmacStreamingParameters AES128_CTR_HMAC_SHA256_4KB =
+      exceptionIsBug(
+          () ->
+              AesCtrHmacStreamingParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setDerivedKeySizeBytes(16)
+                  .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacTagSizeBytes(32)
+                  .setCiphertextSegmentSizeBytes(4096)
+                  .build());
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCtrHmacStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 16 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-CTR derived keys: 16 bytes
+   *   <li>Tag algo: HMAC-SHA256
+   *   <li>Tag size: 32 bytes
+   *   <li>Ciphertext segment size: 1048576 bytes (1 MB)
+   * </ul>
+   */
+  public static final AesCtrHmacStreamingParameters AES128_CTR_HMAC_SHA256_1MB =
+      exceptionIsBug(
+          () ->
+              AesCtrHmacStreamingParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setDerivedKeySizeBytes(16)
+                  .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacTagSizeBytes(32)
+                  .setCiphertextSegmentSizeBytes(1024 * 1024)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCtrHmacStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 32 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-CTR derived keys: 32 bytes
+   *   <li>Tag algo: HMAC-SHA256
+   *   <li>Tag size: 32 bytes
+   *   <li>Ciphertext segment size: 4096
+   * </ul>
+   */
+  public static final AesCtrHmacStreamingParameters AES256_CTR_HMAC_SHA256_4KB =
+      exceptionIsBug(
+          () ->
+              AesCtrHmacStreamingParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setDerivedKeySizeBytes(32)
+                  .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacTagSizeBytes(32)
+                  .setCiphertextSegmentSizeBytes(4096)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesCtrHmacStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 32 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-CTR derived keys: 32 bytes
+   *   <li>Tag algo: HMAC-SHA256
+   *   <li>Tag size: 32 bytes
+   *   <li>Ciphertext segment size: 1048576 bytes (1 MB)
+   * </ul>
+   */
+  public static final AesCtrHmacStreamingParameters AES256_CTR_HMAC_SHA256_1MB =
+      exceptionIsBug(
+          () ->
+              AesCtrHmacStreamingParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setDerivedKeySizeBytes(32)
+                  .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                  .setHmacTagSizeBytes(32)
+                  .setCiphertextSegmentSizeBytes(1024 * 1024)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesGcmHkdfStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 16 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-GCM derived keys: 16 bytes
+   *   <li>Ciphertext segment size: 4096 bytes
+   * </ul>
+   */
+  public static final AesGcmHkdfStreamingParameters AES128_GCM_HKDF_4KB =
+      exceptionIsBug(
+          () ->
+              AesGcmHkdfStreamingParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setDerivedAesGcmKeySizeBytes(16)
+                  .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                  .setCiphertextSegmentSizeBytes(4096)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesGcmHkdfStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 16 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-GCM derived keys: 16 bytes
+   *   <li>Ciphertext segment size: 1048576 bytes (1 MB)
+   * </ul>
+   */
+  public static final AesGcmHkdfStreamingParameters AES128_GCM_HKDF_1MB =
+      exceptionIsBug(
+          () ->
+              AesGcmHkdfStreamingParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setDerivedAesGcmKeySizeBytes(16)
+                  .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                  .setCiphertextSegmentSizeBytes(1024 * 1024)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesGcmHkdfStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 32 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-GCM derived keys: 32 bytes
+   *   <li>Ciphertext segment size: 4096 bytes (4 KB)
+   * </ul>
+   */
+  public static final AesGcmHkdfStreamingParameters AES256_GCM_HKDF_4KB =
+      exceptionIsBug(
+          () ->
+              AesGcmHkdfStreamingParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setDerivedAesGcmKeySizeBytes(32)
+                  .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                  .setCiphertextSegmentSizeBytes(4096)
+                  .build());
+
+  /**
+   * A {@link Parameters} object for generating new instances of {@link AesGcmHkdfStreamingKey} with
+   * the following parameters:
+   *
+   * <ul>
+   *   <li>Size of the main key: 32 bytes
+   *   <li>HKDF algo: HMAC-SHA256
+   *   <li>Size of AES-GCM derived keys: 32 bytes
+   *   <li>Ciphertext segment size: 1048576 bytes (1 MB)
+   * </ul>
+   */
+  public static final AesGcmHkdfStreamingParameters AES256_GCM_HKDF_1MB =
+      exceptionIsBug(
+          () ->
+              AesGcmHkdfStreamingParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setDerivedAesGcmKeySizeBytes(32)
+                  .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                  .setCiphertextSegmentSizeBytes(1024 * 1024)
+                  .build());
+
+  private PredefinedStreamingAeadParameters() {}
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/ReadableByteChannelDecrypter.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/ReadableByteChannelDecrypter.java
index 4d24677..3e7fd69 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/ReadableByteChannelDecrypter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/ReadableByteChannelDecrypter.java
@@ -16,7 +16,6 @@
 
 package com.google.crypto.tink.streamingaead;
 
-import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.StreamingAead;
 import com.google.crypto.tink.subtle.RewindableReadableByteChannel;
 import java.io.IOException;
@@ -25,6 +24,7 @@
 import java.security.GeneralSecurityException;
 import java.util.ArrayDeque;
 import java.util.Deque;
+import java.util.List;
 import javax.annotation.concurrent.GuardedBy;
 
 /**
@@ -42,24 +42,24 @@
   Deque<StreamingAead> remainingPrimitives;
   byte[] associatedData;
 
-
   /**
    * Constructs a new decrypter for {@code ciphertextChannel}.
    *
-   * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives},
-   * and uses it for decryption.  The matching happens as follows:
-   * upon first {@code read()}-call each candidate primitive reads an initial portion
-   * of the channel, until it can determine whether the channel matches the key of the primitive.
-   * If a canditate does not match, then the channel is reset to its initial position,
-   * and the next candiate can attempt matching.  The first successful candidate
-   * is then used exclusively on subsequent {@code read()}-calls.
+   * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives}, and
+   * uses it for decryption. The matching happens as follows: upon first {@code read()}-call each
+   * candidate primitive reads an initial portion of the channel, until it can determine whether the
+   * channel matches the key of the primitive. If a canditate does not match, then the channel is
+   * reset to its initial position, and the next candiate can attempt matching. The first successful
+   * candidate is then used exclusively on subsequent {@code read()}-calls.
    *
-   * <p> The matching process uses a buffering wrapper around {@code ciphertextChannel}
-   * to enable resetting of the channel to the initial position.  The buffering
-   * is removed once the matching is successful.
+   * <p>The matching process uses a buffering wrapper around {@code ciphertextChannel} to enable
+   * resetting of the channel to the initial position. The buffering is removed once the matching is
+   * successful.
    */
-  public ReadableByteChannelDecrypter(PrimitiveSet<StreamingAead> primitives,
-      ReadableByteChannel ciphertextChannel, final byte[] associatedData) {
+  public ReadableByteChannelDecrypter(
+      List<StreamingAead> allPrimitives,
+      ReadableByteChannel ciphertextChannel,
+      final byte[] associatedData) {
     // There are 3 phases:
     // 1) both matchingChannel and attemptingChannel are null. Rewind is enabled.
     // 2) attemptingChannel is non-null, matchingChannel is null. Rewind is enabled.
@@ -67,8 +67,8 @@
     this.attemptingChannel = null;
     this.matchingChannel = null;
     this.remainingPrimitives = new ArrayDeque<>();
-    for (PrimitiveSet.Entry<StreamingAead> entry : primitives.getRawPrimitives()) {
-      this.remainingPrimitives.add(entry.getPrimitive());
+    for (StreamingAead primitive : allPrimitives) {
+      this.remainingPrimitives.add(primitive);
     }
     this.ciphertextChannel = new RewindableReadableByteChannel(ciphertextChannel);
     this.associatedData = associatedData.clone();
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/SeekableByteChannelDecrypter.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/SeekableByteChannelDecrypter.java
index ac3a472..d21c92b 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/SeekableByteChannelDecrypter.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/SeekableByteChannelDecrypter.java
@@ -16,9 +16,8 @@
 
 package com.google.crypto.tink.streamingaead;
 
-import androidx.annotation.RequiresApi;
-import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.StreamingAead;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.NonWritableChannelException;
@@ -26,10 +25,10 @@
 import java.security.GeneralSecurityException;
 import java.util.ArrayDeque;
 import java.util.Deque;
+import java.util.List;
 import javax.annotation.concurrent.GuardedBy;
 
 /** A decrypter for ciphertext given in a {@link SeekableByteChannel}. */
-@RequiresApi(24) // https://developer.android.com/reference/java/nio/channels/SeekableByteChannel
 final class SeekableByteChannelDecrypter implements SeekableByteChannel {
   @GuardedBy("this")
   SeekableByteChannel attemptingChannel;
@@ -57,7 +56,7 @@
    * and the next candiate can attempt matching.  The first successful candidate
    * is then used exclusively on subsequent {@code read()}-calls.
    */
-  public SeekableByteChannelDecrypter(PrimitiveSet<StreamingAead> primitives,
+  public SeekableByteChannelDecrypter(List<StreamingAead> allPrimitives,
       SeekableByteChannel ciphertextChannel, final byte[] associatedData) throws IOException {
     // There are 3 phases:
     // 1) both matchingChannel and attemptingChannel are null.
@@ -66,8 +65,8 @@
     this.attemptingChannel = null;
     this.matchingChannel = null;
     this.remainingPrimitives = new ArrayDeque<>();
-    for (PrimitiveSet.Entry<StreamingAead> entry : primitives.getRawPrimitives()) {
-      this.remainingPrimitives.add(entry.getPrimitive());
+    for (StreamingAead primitive : allPrimitives) {
+      this.remainingPrimitives.add(primitive);
     }
     this.ciphertextChannel = ciphertextChannel;
     // In phase 1) and 2), cachedPosition is always equal to the last position value set.
@@ -132,6 +131,7 @@
     }
   }
 
+  @CanIgnoreReturnValue
   @Override
   @GuardedBy("this")
   public synchronized SeekableByteChannel position(long newPosition) throws IOException {
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadConfig.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadConfig.java
index 532e30a..b1c99f7 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadConfig.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadConfig.java
@@ -42,7 +42,9 @@
   public static final String AES_GCM_HKDF_STREAMINGAEAD_TYPE_URL =
       new AesGcmHkdfStreamingKeyManager().getKeyType();
 
-  /** @deprecated */
+  /**
+   * @deprecated
+   */
   @Deprecated
   public static final RegistryConfig TINK_1_1_0 = RegistryConfig.getDefaultInstance();
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadFactory.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadFactory.java
index a7a878d..828f2be 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadFactory.java
@@ -17,7 +17,6 @@
 package com.google.crypto.tink.streamingaead;
 
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.StreamingAead;
 import java.security.GeneralSecurityException;
 
@@ -43,7 +42,7 @@
    */
   public static StreamingAead getPrimitive(KeysetHandle keysetHandle)
       throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new StreamingAeadWrapper());
+    StreamingAeadWrapper.register();
     return keysetHandle.getPrimitive(StreamingAead.class);
   }
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadHelper.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadHelper.java
index 7bcbff5..18c4cd4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadHelper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadHelper.java
@@ -16,8 +16,6 @@
 
 package com.google.crypto.tink.streamingaead;
 
-import androidx.annotation.RequiresApi;
-import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.StreamingAead;
 import java.io.IOException;
 import java.io.InputStream;
@@ -26,12 +24,14 @@
 import java.nio.channels.SeekableByteChannel;
 import java.nio.channels.WritableByteChannel;
 import java.security.GeneralSecurityException;
+import java.util.List;
 
 /**
  * A helper for creating {@link StreamingAead}-primitives from keysets.
  */
 final class StreamingAeadHelper implements StreamingAead {
-  PrimitiveSet<StreamingAead> primitives;
+  private final List<StreamingAead> allPrimitives;
+  private final StreamingAead primary;
 
   /**
    * Creates a helper that uses the provided primitives for encryption
@@ -39,35 +39,31 @@
    * For encryption it uses the primitive corresponding to the primary key.
    * For decryption it uses an enabled primitive that matches the given ciphertext.
    */
-  public StreamingAeadHelper(PrimitiveSet<StreamingAead> primitives)
+  public StreamingAeadHelper(List<StreamingAead> allPrimitives, StreamingAead primary)
       throws GeneralSecurityException {
-    if (primitives.getPrimary() == null) {
-      throw new GeneralSecurityException("Missing primary primitive.");
-    }
-    this.primitives = primitives;
+    this.allPrimitives = allPrimitives;
+    this.primary = primary;
   }
 
   @Override
   public WritableByteChannel newEncryptingChannel(
       WritableByteChannel ciphertextDestination, byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    return primitives.getPrimary().getPrimitive()
-        .newEncryptingChannel(ciphertextDestination, associatedData);
+    return primary.newEncryptingChannel(ciphertextDestination, associatedData);
   }
 
   @Override
   public ReadableByteChannel newDecryptingChannel(
       ReadableByteChannel ciphertextChannel, byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    return new ReadableByteChannelDecrypter(primitives, ciphertextChannel, associatedData);
+    return new ReadableByteChannelDecrypter(allPrimitives, ciphertextChannel, associatedData);
   }
 
   @Override
-  @RequiresApi(24) // https://developer.android.com/reference/java/nio/channels/SeekableByteChannel
   public SeekableByteChannel newSeekableDecryptingChannel(
       SeekableByteChannel ciphertextChannel, byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    return new SeekableByteChannelDecrypter(primitives, ciphertextChannel, associatedData);
+    return new SeekableByteChannelDecrypter(allPrimitives, ciphertextChannel, associatedData);
   }
 
   @Override
@@ -75,14 +71,13 @@
       InputStream ciphertextStream,
       byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    return new InputStreamDecrypter(primitives, ciphertextStream, associatedData);
+    return new InputStreamDecrypter(allPrimitives, ciphertextStream, associatedData);
   }
 
   @Override
   public OutputStream newEncryptingStream(
       OutputStream ciphertext, byte[] associatedData)
       throws GeneralSecurityException, IOException {
-    return primitives.getPrimary().getPrimitive()
-        .newEncryptingStream(ciphertext, associatedData);
+    return primary.newEncryptingStream(ciphertext, associatedData);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKey.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKey.java
new file mode 100644
index 0000000..bfa7e98
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKey.java
@@ -0,0 +1,32 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import com.google.crypto.tink.Key;
+import javax.annotation.Nullable;
+
+/** Represents functions to encrypt and decrypt data using a StreamingAead. */
+public abstract class StreamingAeadKey extends Key {
+  @Override
+  @Nullable
+  public final Integer getIdRequirementOrNull() {
+    return null;
+  }
+
+  @Override
+  public abstract StreamingAeadParameters getParameters();
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplates.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplates.java
index a5d5de2..414f476 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplates.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplates.java
@@ -28,6 +28,21 @@
 /**
  * Pre-generated {@link KeyTemplate} for {@link com.google.crypto.tink.StreamingAead} keys.
  *
+ * <p>We recommend to avoid this class in order to keep dependencies small.
+ *
+ * <ul>
+ *   <li>Using this class adds a dependency on protobuf. We hope that eventually it is possible to
+ *       use Tink without a dependency on protobuf.
+ *   <li>Using this class adds a dependency on classes for all involved key types.
+ * </ul>
+ *
+ * These dependencies all come from static class member variables, which are initialized when the
+ * class is loaded. This implies that static analysis and code minimization tools (such as proguard)
+ * cannot remove the usages either.
+ *
+ * <p>Instead, we recommend to use {@code KeysetHandle.generateEntryFromParametersName} or {@code
+ * KeysetHandle.generateEntryFromParameters}.
+ *
  * <p>One can use these templates to generate new {@link com.google.crypto.tink.proto.Keyset} with
  * {@code KeysetHandle}. To generate a new keyset that contains a {@link AesGcmHkdfStreamingKey},
  * one can do:
@@ -38,9 +53,10 @@
  * StreamingAead ags = handle.getPrimitive(StreamingAead.class);
  * }</pre>
  *
+ * @deprecated Try using our refaster templates to replace them (see
+ *     https://github.com/tink-crypto/tink-java/tree/main/tools/refaster). If migration is unclear,
+ *     please file an issue on https://github.com/tink-crypto/tink-java.
  * @since 1.1.0
- * @deprecated use {@link com.google.crypto.tink.KeyTemplates#get}, e.g.,
- *     KeyTemplates.get("AES256_GCM_HKDF_1MB")
  */
 @Deprecated
 public final class StreamingAeadKeyTemplates {
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadParameters.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadParameters.java
new file mode 100644
index 0000000..a7931eb
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadParameters.java
@@ -0,0 +1,29 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import com.google.crypto.tink.Parameters;
+
+/**
+ * Represents a description of a {@link StreamingAeadKey}.
+ */
+public abstract class StreamingAeadParameters extends Parameters {
+  @Override
+  public final boolean hasIdRequirement() {
+    return false;
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapper.java b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapper.java
index ef5961c..8ccba3e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapper.java
+++ b/java_src/src/main/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapper.java
@@ -21,6 +21,8 @@
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.StreamingAead;
 import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * StreamingAeadWrapper is the implementation of PrimitiveWrapper for the StreamingAead primitive.
@@ -31,6 +33,9 @@
  * {@link com.google.crypto.tink.proto.OutputPrefixType#RAW}.
  */
 public class StreamingAeadWrapper implements PrimitiveWrapper<StreamingAead, StreamingAead> {
+
+  private static final StreamingAeadWrapper WRAPPER = new StreamingAeadWrapper();
+
   StreamingAeadWrapper() {}
 
   /**
@@ -40,7 +45,19 @@
   @Override
   public StreamingAead wrap(final PrimitiveSet<StreamingAead> primitives)
       throws GeneralSecurityException {
-    return new StreamingAeadHelper(primitives);
+    List<StreamingAead> allStreamingAeads = new ArrayList<>();
+    for (List<PrimitiveSet.Entry<StreamingAead>> entryList : primitives.getAll()) {
+      // For legacy reasons (Tink always encrypted with non-RAW keys) we use all
+      // primitives, even those which have output_prefix_type != RAW.
+      for (PrimitiveSet.Entry<StreamingAead> entry : entryList) {
+        allStreamingAeads.add(entry.getPrimitive());
+      }
+    }
+    PrimitiveSet.Entry<StreamingAead> primary = primitives.getPrimary();
+    if (primary == null || primary.getPrimitive() == null) {
+      throw new GeneralSecurityException("No primary set");
+    }
+    return new StreamingAeadHelper(allStreamingAeads, primary.getPrimitive());
   }
 
   @Override
@@ -54,6 +71,6 @@
   }
 
   public static void register() throws GeneralSecurityException {
-    Registry.registerPrimitiveWrapper(new StreamingAeadWrapper());
+    Registry.registerPrimitiveWrapper(WRAPPER);
   }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/subtle/BUILD.bazel
index 64d8893..f3c1c24 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/BUILD.bazel
@@ -9,8 +9,11 @@
         ":bytes",
         ":subtle_util_cluster",
         ":validators",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/mac/internal:aes_util",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
@@ -65,10 +68,10 @@
     name = "x25519",
     srcs = ["X25519.java"],
     deps = [
-        ":curve25519",
-        ":field25519",
         ":random",
         "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "//src/main/java/com/google/crypto/tink/internal:curve25519",
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
     ],
 )
 
@@ -130,7 +133,11 @@
 java_library(
     name = "elliptic_curves",
     srcs = ["EllipticCurves.java"],
-    deps = [":subtle_util_cluster"],
+    deps = [
+        ":subtle_util_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+    ],
 )
 
 java_library(
@@ -200,7 +207,7 @@
         ":rsa_ssa_pkcs1_verify_jce",
         ":rsa_ssa_pss_sign_jce",
         ":rsa_ssa_pss_verify_jce",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
 
@@ -237,7 +244,12 @@
     srcs = ["PrfMac.java"],
     deps = [
         ":bytes",
+        ":prf_aes_cmac",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
@@ -263,22 +275,6 @@
 )
 
 java_library(
-    name = "cha_cha20_base",
-    srcs = ["ChaCha20Base.java"],
-    deps = [
-        ":bytes",
-        ":ind_cpa_cipher",
-        ":random",
-    ],
-)
-
-java_library(
-    name = "poly1305",
-    srcs = ["Poly1305.java"],
-    deps = [":bytes"],
-)
-
-java_library(
     name = "aes_gcm_jce",
     srcs = ["AesGcmJce.java"],
     deps = [
@@ -298,11 +294,11 @@
     name = "ed25519_sign",
     srcs = ["Ed25519Sign.java"],
     deps = [
-        ":ed25519_cluster",
-        ":field25519",
         ":random",
         "//src/main/java/com/google/crypto/tink:public_key_sign",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
     ],
 )
 
@@ -336,26 +332,15 @@
     name = "ed25519_verify",
     srcs = ["Ed25519Verify.java"],
     deps = [
-        ":ed25519_cluster",
-        ":field25519",
         "//src/main/java/com/google/crypto/tink:public_key_verify",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
         "//src/main/java/com/google/crypto/tink/util:bytes",
     ],
 )
 
 java_library(
-    name = "cha_cha20_poly1305_base",
-    srcs = ["ChaCha20Poly1305Base.java"],
-    deps = [
-        ":cha_cha20_base",
-        ":poly1305",
-        "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
-    ],
-)
-
-java_library(
     name = "x_cha_cha20",
     srcs = ["XChaCha20.java"],
     deps = [
@@ -379,12 +364,6 @@
 )
 
 java_library(
-    name = "field25519",
-    srcs = ["Field25519.java"],
-    deps = ["//src/main/java/com/google/crypto/tink/annotations:alpha"],
-)
-
-java_library(
     name = "cha_cha20",
     srcs = ["ChaCha20.java"],
     deps = [
@@ -439,6 +418,7 @@
 java_library(
     name = "base64",
     srcs = ["Base64.java"],
+    deps = ["@maven//:com_google_errorprone_error_prone_annotations"],
 )
 
 java_library(
@@ -446,7 +426,10 @@
     srcs = ["PrfHmacJce.java"],
     deps = [
         ":subtle_util_cluster",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
@@ -486,17 +469,6 @@
 )
 
 java_library(
-    name = "curve25519",
-    srcs = ["Curve25519.java"],
-    deps = [
-        ":bytes",
-        ":field25519",
-        ":hex",
-        "//src/main/java/com/google/crypto/tink/annotations:alpha",
-    ],
-)
-
-java_library(
     name = "nonce_based_streaming_aead_cluster",
     srcs = [
         "NonceBasedStreamingAead.java",
@@ -510,7 +482,7 @@
         ":stream_segment_decrypter",
         ":stream_segment_encrypter",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
-        "@maven//:androidx_annotation_annotation",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -525,22 +497,10 @@
         ":enums",
         ":validators",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
         "//src/main/java/com/google/crypto/tink/internal:util",
         "@maven//:com_google_code_findbugs_jsr305",
-    ],
-)
-
-java_library(
-    name = "ed25519_cluster",
-    srcs = [
-        "Ed25519.java",
-        "Ed25519Constants.java",
-    ],
-    deps = [
-        ":bytes",
-        ":curve25519",
-        ":field25519",
-        ":subtle_util_cluster",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -618,6 +578,7 @@
 android_library(
     name = "base64-android",
     srcs = ["Base64.java"],
+    deps = ["@maven//:com_google_errorprone_error_prone_annotations"],
 )
 
 android_library(
@@ -636,16 +597,6 @@
 )
 
 android_library(
-    name = "cha_cha20_base-android",
-    srcs = ["ChaCha20Base.java"],
-    deps = [
-        ":bytes-android",
-        ":ind_cpa_cipher-android",
-        ":random-android",
-    ],
-)
-
-android_library(
     name = "cha_cha20_poly1305-android",
     srcs = ["ChaCha20Poly1305.java"],
     deps = [
@@ -658,28 +609,6 @@
 )
 
 android_library(
-    name = "cha_cha20_poly1305_base-android",
-    srcs = ["ChaCha20Poly1305Base.java"],
-    deps = [
-        ":cha_cha20_base-android",
-        ":poly1305-android",
-        "//src/main/java/com/google/crypto/tink:aead-android",
-        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
-    ],
-)
-
-android_library(
-    name = "curve25519-android",
-    srcs = ["Curve25519.java"],
-    deps = [
-        ":bytes-android",
-        ":field25519-android",
-        ":hex-android",
-        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
-    ],
-)
-
-android_library(
     name = "ecdsa_sign_jce-android",
     srcs = ["EcdsaSignJce.java"],
     deps = [
@@ -755,28 +684,14 @@
 )
 
 android_library(
-    name = "ed25519_cluster-android",
-    srcs = [
-        "Ed25519.java",
-        "Ed25519Constants.java",
-    ],
-    deps = [
-        ":bytes-android",
-        ":curve25519-android",
-        ":field25519-android",
-        ":subtle_util_cluster-android",
-    ],
-)
-
-android_library(
     name = "ed25519_sign-android",
     srcs = ["Ed25519Sign.java"],
     deps = [
-        ":ed25519_cluster-android",
-        ":field25519-android",
         ":random-android",
         "//src/main/java/com/google/crypto/tink:public_key_sign-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster-android",
+        "//src/main/java/com/google/crypto/tink/internal:field25519-android",
     ],
 )
 
@@ -796,10 +711,10 @@
     name = "ed25519_verify-android",
     srcs = ["Ed25519Verify.java"],
     deps = [
-        ":ed25519_cluster-android",
-        ":field25519-android",
         "//src/main/java/com/google/crypto/tink:public_key_verify-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster-android",
+        "//src/main/java/com/google/crypto/tink/internal:field25519-android",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
     ],
 )
@@ -807,7 +722,11 @@
 android_library(
     name = "elliptic_curves-android",
     srcs = ["EllipticCurves.java"],
-    deps = [":subtle_util_cluster-android"],
+    deps = [
+        ":subtle_util_cluster-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util-android",
+    ],
 )
 
 android_library(
@@ -830,12 +749,6 @@
 )
 
 android_library(
-    name = "field25519-android",
-    srcs = ["Field25519.java"],
-    deps = ["//src/main/java/com/google/crypto/tink/annotations:alpha-android"],
-)
-
-android_library(
     name = "hex-android",
     srcs = ["Hex.java"],
 )
@@ -877,7 +790,7 @@
         ":stream_segment_decrypter-android",
         ":stream_segment_encrypter-android",
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
-        "@maven//:androidx_annotation_annotation",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -894,20 +807,17 @@
 )
 
 android_library(
-    name = "poly1305-android",
-    srcs = ["Poly1305.java"],
-    deps = [":bytes-android"],
-)
-
-android_library(
     name = "prf_aes_cmac-android",
     srcs = ["PrfAesCmac.java"],
     deps = [
         ":bytes-android",
         ":subtle_util_cluster-android",
         ":validators-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
         "//src/main/java/com/google/crypto/tink/mac/internal:aes_util-android",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_set-android",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
@@ -918,7 +828,10 @@
     srcs = ["PrfHmacJce.java"],
     deps = [
         ":subtle_util_cluster-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_set-android",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
@@ -929,7 +842,12 @@
     srcs = ["PrfMac.java"],
     deps = [
         ":bytes-android",
+        ":prf_aes_cmac-android",
+        "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
         "//src/main/java/com/google/crypto/tink:mac-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key-android",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_set-android",
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
@@ -1014,7 +932,7 @@
         ":rsa_ssa_pkcs1_verify_jce-android",
         ":rsa_ssa_pss_sign_jce-android",
         ":rsa_ssa_pss_verify_jce-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
 
@@ -1039,8 +957,10 @@
         ":enums-android",
         ":validators-android",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
         "//src/main/java/com/google/crypto/tink/internal:util-android",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
 
@@ -1057,10 +977,10 @@
     name = "x25519-android",
     srcs = ["X25519.java"],
     deps = [
-        ":curve25519-android",
-        ":field25519-android",
         ":random-android",
         "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "//src/main/java/com/google/crypto/tink/internal:curve25519-android",
+        "//src/main/java/com/google/crypto/tink/internal:field25519-android",
     ],
 )
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Base64.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Base64.java
index 5ffbaab..31d6fa1 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Base64.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/Base64.java
@@ -16,6 +16,7 @@
 
 package com.google.crypto.tink.subtle;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
 
@@ -580,6 +581,13 @@
       return len * 8 / 5 + 10;
     }
 
+    /**
+     * Processes the input to encode it in base 64.
+     *
+     * <p>This function always returns true -- encoding can never fail. So if one knows that one has
+     * this class one can ignore the return value.
+     */
+    @CanIgnoreReturnValue
     @Override
     public boolean process(byte[] input, int offset, int len, boolean finish) {
       // Using local variables makes the encoder about 9% faster.
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Bytes.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Bytes.java
index e77f835..3f134f8 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Bytes.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/Bytes.java
@@ -18,6 +18,7 @@
 
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
 import java.util.Arrays;
 
 /**
@@ -32,17 +33,7 @@
    * @return true if two arrays are equal.
    */
   public static final boolean equal(final byte[] x, final byte[] y) {
-    if (x == null || y == null) {
-      return false;
-    }
-    if (x.length != y.length) {
-      return false;
-    }
-    int res = 0;
-    for (int i = 0; i < x.length; i++) {
-      res |= x[i] ^ y[i];
-    }
-    return res == 0;
+    return MessageDigest.isEqual(x, y);
   }
 
   /**
@@ -87,10 +78,8 @@
   }
 
   /**
-   * Computes the xor of two byte buffers, specifying the length to xor, and
-   * stores the result to {@code output}.
-   *
-   * @return a new byte[] of length len.
+   * Computes the xor of two byte buffers, specifying the length to xor, and stores the result to
+   * {@code output}.
    */
   public static final void xor(ByteBuffer output, ByteBuffer x, ByteBuffer y, int len) {
     if (len < 0 || x.remaining() < len || y.remaining() < len || output.remaining() < len) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/ChaCha20Base.java b/java_src/src/main/java/com/google/crypto/tink/subtle/ChaCha20Base.java
deleted file mode 100644
index 257f0fd..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/ChaCha20Base.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.IntBuffer;
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-
-/**
- * Abstract base class for ChaCha20 and XChaCha20.
- *
- * <p>ChaCha20 and XChaCha20 have two differences: the size of the nonce and the initial state of
- * the block function that produces a key stream block from a key, a nonce, and a counter.
- *
- * <p>Concrete implementations of this class are meant to be used to construct an {@link
- * com.google.crypto.tink.Aead} with {@link com.google.crypto.tink.subtle.Poly1305}.
- *
- * @deprecated replaced by {@link com.google.crypto.tink.aead.internal.ChaCha20Util} and {@link
- *     com.google.crypto.tink.aead.internal.InsecureNonceChaCha20Base}.
- */
-@Deprecated
-abstract class ChaCha20Base implements IndCpaCipher {
-  public static final int BLOCK_SIZE_IN_INTS = 16;
-  public static final int BLOCK_SIZE_IN_BYTES = BLOCK_SIZE_IN_INTS * 4;
-  public static final int KEY_SIZE_IN_INTS = 8;
-  public static final int KEY_SIZE_IN_BYTES = KEY_SIZE_IN_INTS * 4;
-  private static final int[] SIGMA =
-      toIntArray(
-          new byte[] {
-            'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k'
-          });
-  int[] key;
-  private final int initialCounter;
-
-  ChaCha20Base(final byte[] key, int initialCounter) throws InvalidKeyException {
-    if (key.length != KEY_SIZE_IN_BYTES) {
-      throw new InvalidKeyException("The key length in bytes must be 32.");
-    }
-    this.key = toIntArray(key);
-    this.initialCounter = initialCounter;
-  }
-
-  /** Returns the initial state from {@code nonce} and {@code counter}. */
-  abstract int[] createInitialState(final int[] nonce, int counter);
-
-  /**
-   * The size of the randomly generated nonces.
-   *
-   * <p>ChaCha20 uses 12-byte nonces, but XChaCha20 use 24-byte nonces.
-   */
-  abstract int nonceSizeInBytes();
-
-  @Override
-  public byte[] encrypt(final byte[] plaintext) throws GeneralSecurityException {
-    if (plaintext.length > Integer.MAX_VALUE - nonceSizeInBytes()) {
-      throw new GeneralSecurityException("plaintext too long");
-    }
-    ByteBuffer ciphertext = ByteBuffer.allocate(nonceSizeInBytes() + plaintext.length);
-    encrypt(ciphertext, plaintext);
-    return ciphertext.array();
-  }
-
-  void encrypt(ByteBuffer output, final byte[] plaintext) throws GeneralSecurityException {
-    if (output.remaining() - nonceSizeInBytes() < plaintext.length) {
-      throw new IllegalArgumentException("Given ByteBuffer output is too small");
-    }
-
-    byte[] nonce = Random.randBytes(nonceSizeInBytes());
-    output.put(nonce);
-    process(nonce, output, ByteBuffer.wrap(plaintext));
-  }
-
-  @Override
-  public byte[] decrypt(final byte[] ciphertext) throws GeneralSecurityException {
-    return decrypt(ByteBuffer.wrap(ciphertext));
-  }
-
-  byte[] decrypt(ByteBuffer ciphertext) throws GeneralSecurityException {
-    if (ciphertext.remaining() < nonceSizeInBytes()) {
-      throw new GeneralSecurityException("ciphertext too short");
-    }
-    byte[] nonce = new byte[nonceSizeInBytes()];
-    ciphertext.get(nonce);
-    ByteBuffer plaintext = ByteBuffer.allocate(ciphertext.remaining());
-    process(nonce, plaintext, ciphertext);
-    return plaintext.array();
-  }
-
-  private void process(final byte[] nonce, ByteBuffer output, ByteBuffer input)
-      throws GeneralSecurityException {
-    int length = input.remaining();
-    int numBlocks = (length / BLOCK_SIZE_IN_BYTES) + 1;
-    for (int i = 0; i < numBlocks; i++) {
-      ByteBuffer keyStreamBlock = chacha20Block(nonce, i + initialCounter);
-      if (i == numBlocks - 1) {
-        // last block
-        Bytes.xor(output, input, keyStreamBlock, length % BLOCK_SIZE_IN_BYTES);
-      } else {
-        Bytes.xor(output, input, keyStreamBlock, BLOCK_SIZE_IN_BYTES);
-      }
-    }
-  }
-
-  // https://tools.ietf.org/html/rfc8439#section-2.3.
-  ByteBuffer chacha20Block(final byte[] nonce, int counter) {
-    int[] state = createInitialState(toIntArray(nonce), counter);
-    int[] workingState = state.clone();
-    shuffleState(workingState);
-    for (int i = 0; i < state.length; i++) {
-      state[i] += workingState[i];
-    }
-    ByteBuffer out = ByteBuffer.allocate(BLOCK_SIZE_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN);
-    out.asIntBuffer().put(state, 0, BLOCK_SIZE_IN_INTS);
-    return out;
-  }
-
-  static void setSigmaAndKey(int[] state, final int[] key) {
-    System.arraycopy(SIGMA, 0, state, 0, SIGMA.length);
-    System.arraycopy(key, 0, state, SIGMA.length, KEY_SIZE_IN_INTS);
-  }
-
-  static void shuffleState(final int[] state) {
-    for (int i = 0; i < 10; i++) {
-      quarterRound(state, 0, 4, 8, 12);
-      quarterRound(state, 1, 5, 9, 13);
-      quarterRound(state, 2, 6, 10, 14);
-      quarterRound(state, 3, 7, 11, 15);
-      quarterRound(state, 0, 5, 10, 15);
-      quarterRound(state, 1, 6, 11, 12);
-      quarterRound(state, 2, 7, 8, 13);
-      quarterRound(state, 3, 4, 9, 14);
-    }
-  }
-
-  static void quarterRound(int[] x, int a, int b, int c, int d) {
-    x[a] += x[b];
-    x[d] = rotateLeft(x[d] ^ x[a], 16);
-    x[c] += x[d];
-    x[b] = rotateLeft(x[b] ^ x[c], 12);
-    x[a] += x[b];
-    x[d] = rotateLeft(x[d] ^ x[a], 8);
-    x[c] += x[d];
-    x[b] = rotateLeft(x[b] ^ x[c], 7);
-  }
-
-  static int[] toIntArray(final byte[] input) {
-    IntBuffer intBuffer = ByteBuffer.wrap(input).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
-    int[] ret = new int[intBuffer.remaining()];
-    intBuffer.get(ret);
-    return ret;
-  }
-
-  private static int rotateLeft(int x, int y) {
-    return (x << y) | (x >>> -y);
-  }
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Base.java b/java_src/src/main/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Base.java
deleted file mode 100644
index 1f86dc5..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Base.java
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import static com.google.crypto.tink.subtle.Poly1305.MAC_KEY_SIZE_IN_BYTES;
-import static com.google.crypto.tink.subtle.Poly1305.MAC_TAG_SIZE_IN_BYTES;
-
-import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.config.internal.TinkFipsUtil;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import javax.crypto.AEADBadTagException;
-
-/**
- * Abstract base class for class of ChaCha20Poly1305 and XChaCha20Poly1305, following RFC 8439
- * https://tools.ietf.org/html/rfc8439.
- *
- * <p>This implementation produces ciphertext with the following format: {@code nonce ||
- * actual_ciphertext || tag} and only decrypts the same format.
- *
- * @deprecated replaced by {@link
- *     com.google.crypto.tink.aead.internal.InsecureNonceChaCha20Poly1305Base}.
- */
-@Deprecated
-abstract class ChaCha20Poly1305Base implements Aead {
-  public static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS =
-      TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_NOT_FIPS;
-
-  private final ChaCha20Base chacha20;
-  private final ChaCha20Base macKeyChaCha20;
-
-  public ChaCha20Poly1305Base(final byte[] key)
-      throws GeneralSecurityException {
-    if (!FIPS.isCompatible()) {
-      throw new GeneralSecurityException("Can not use ChaCha20Poly1305 in FIPS-mode.");
-    }
-
-    this.chacha20 = newChaCha20Instance(key, 1);
-    this.macKeyChaCha20 = newChaCha20Instance(key, 0);
-  }
-
-  abstract ChaCha20Base newChaCha20Instance(final byte[] key, int initialCounter)
-      throws InvalidKeyException;
-
-  /**
-   * Encrypts the {@code plaintext} with Poly1305 authentication based on {@code associatedData}.
-   *
-   * <p>Please note that nonce is randomly generated hence keys need to be rotated after encrypting
-   * a certain number of messages depending on the nonce size of the underlying {@link
-   * ChaCha20Base}.
-   *
-   * @param plaintext data to encrypt
-   * @param associatedData associated authenticated data
-   * @return ciphertext with the following format {@code nonce || actual_ciphertext || tag}
-   */
-  @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
-      throws GeneralSecurityException {
-    if (plaintext.length
-        > Integer.MAX_VALUE - chacha20.nonceSizeInBytes() - MAC_TAG_SIZE_IN_BYTES) {
-      throw new GeneralSecurityException("plaintext too long");
-    }
-    ByteBuffer ciphertext =
-        ByteBuffer.allocate(plaintext.length + chacha20.nonceSizeInBytes() + MAC_TAG_SIZE_IN_BYTES);
-
-    encrypt(ciphertext, plaintext, associatedData);
-    return ciphertext.array();
-  }
-
-  private void encrypt(ByteBuffer output, final byte[] plaintext, final byte[] associatedData)
-      throws GeneralSecurityException {
-    if (output.remaining()
-        < plaintext.length + chacha20.nonceSizeInBytes() + MAC_TAG_SIZE_IN_BYTES) {
-      throw new IllegalArgumentException("Given ByteBuffer output is too small");
-    }
-    int firstPosition = output.position();
-    chacha20.encrypt(output, plaintext);
-    output.position(firstPosition);
-    byte[] nonce = new byte[chacha20.nonceSizeInBytes()];
-    output.get(nonce);
-    output.limit(output.limit() - MAC_TAG_SIZE_IN_BYTES);
-    byte[] aad = associatedData;
-    if (aad == null) {
-      aad = new byte[0];
-    }
-    byte[] tag = Poly1305.computeMac(getMacKey(nonce), macDataRfc8439(aad, output));
-    output.limit(output.limit() + MAC_TAG_SIZE_IN_BYTES);
-    output.put(tag);
-  }
-
-  /**
-   * Decryptes {@code ciphertext} with the following format: {@code nonce || actual_ciphertext ||
-   * tag}
-   *
-   * @param ciphertext with format {@code nonce || actual_ciphertext || tag}
-   * @param associatedData associated authenticated data
-   * @return plaintext if authentication is successful.
-   * @throws GeneralSecurityException when ciphertext is shorter than nonce size + tag size or when
-   *     computed tag based on {@code ciphertext} and {@code associatedData} does not match the tag
-   *     given in {@code ciphertext}.
-   */
-  @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
-      throws GeneralSecurityException {
-    return decrypt(ByteBuffer.wrap(ciphertext), associatedData);
-  }
-
-  /**
-   * Decryptes {@code ciphertext} with the following format: {@code nonce || actual_ciphertext ||
-   * tag}
-   *
-   * @param ciphertext with format {@code nonce || actual_ciphertext || tag}
-   * @param associatedData associated authenticated data
-   * @return plaintext if authentication is successful
-   * @throws GeneralSecurityException when ciphertext is shorter than nonce size + tag size
-   * @throws AEADBadTagException when the tag is invalid
-   */
-  private byte[] decrypt(ByteBuffer ciphertext, final byte[] associatedData)
-      throws GeneralSecurityException {
-    if (ciphertext.remaining() < chacha20.nonceSizeInBytes() + MAC_TAG_SIZE_IN_BYTES) {
-      throw new GeneralSecurityException("ciphertext too short");
-    }
-    int firstPosition = ciphertext.position();
-    byte[] tag = new byte[MAC_TAG_SIZE_IN_BYTES];
-    ciphertext.position(ciphertext.limit() - MAC_TAG_SIZE_IN_BYTES);
-    ciphertext.get(tag);
-    // rewind to read ciphertext and compute tag.
-    ciphertext.position(firstPosition);
-    ciphertext.limit(ciphertext.limit() - MAC_TAG_SIZE_IN_BYTES);
-    byte[] nonce = new byte[chacha20.nonceSizeInBytes()];
-    ciphertext.get(nonce);
-    byte[] aad = associatedData;
-    if (aad == null) {
-      aad = new byte[0];
-    }
-    try {
-      Poly1305.verifyMac(getMacKey(nonce), macDataRfc8439(aad, ciphertext), tag);
-    } catch (GeneralSecurityException ex) {
-      throw new AEADBadTagException(ex.toString());
-    }
-
-    // rewind to decrypt the ciphertext.
-    ciphertext.position(firstPosition);
-    return chacha20.decrypt(ciphertext);
-  }
-
-  /** The MAC key is the first 32 bytes of the first key stream block */
-  private byte[] getMacKey(final byte[] nonce) throws GeneralSecurityException {
-    ByteBuffer firstBlock = macKeyChaCha20.chacha20Block(nonce, 0 /* counter */);
-    byte[] result = new byte[MAC_KEY_SIZE_IN_BYTES];
-    firstBlock.get(result);
-    return result;
-  }
-
-  /** Prepares the input to MAC, following RFC 8439, section 2.8. */
-  private static byte[] macDataRfc8439(final byte[] aad, ByteBuffer ciphertext) {
-    int aadPaddedLen = (aad.length % 16 == 0) ? aad.length : (aad.length + 16 - aad.length % 16);
-    int ciphertextLen = ciphertext.remaining();
-    int ciphertextPaddedLen =
-        (ciphertextLen % 16 == 0) ? ciphertextLen : (ciphertextLen + 16 - ciphertextLen % 16);
-    ByteBuffer macData =
-        ByteBuffer.allocate(aadPaddedLen + ciphertextPaddedLen + 16).order(ByteOrder.LITTLE_ENDIAN);
-    macData.put(aad);
-    macData.position(aadPaddedLen);
-    macData.put(ciphertext);
-    macData.position(aadPaddedLen + ciphertextPaddedLen);
-    macData.putLong(aad.length);
-    macData.putLong(ciphertextLen);
-    return macData.array();
-  }
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Curve25519.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Curve25519.java
deleted file mode 100644
index 9073441..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Curve25519.java
+++ /dev/null
@@ -1,433 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import com.google.crypto.tink.annotations.Alpha;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-
-/**
- * This class implements point arithmetic on the elliptic curve <a
- * href="https://cr.yp.to/ecdh/curve25519-20060209.pdf">Curve25519</a>.
- *
- * <p>This class only implements point arithmetic, if you want to use the ECDH Curve25519 function,
- * please checkout {@link com.google.crypto.tink.subtle.X25519}.
- *
- * <p>This implementation is based on <a
- * href="https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c">curve255-donna C
- * implementation</a>.
- */
-@Alpha
-final class Curve25519 {
-  // https://cr.yp.to/ecdh.html#validate doesn't recommend validating peer's public key. However,
-  // validating public key doesn't harm security and in certain cases, prevents unwanted edge
-  // cases.
-  // As we clear the most significant bit of peer's public key, we don't have to include public keys
-  // that are larger than 2^255.
-  static final byte[][] BANNED_PUBLIC_KEYS =
-      new byte[][] {
-        // 0
-        new byte[] {
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
-        },
-        // 1
-        new byte[] {
-          (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-          (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-        },
-        // 325606250916557431795983626356110631294008115727848805560023387167927233504
-        new byte[] {
-          (byte) 0xe0, (byte) 0xeb, (byte) 0x7a, (byte) 0x7c,
-          (byte) 0x3b, (byte) 0x41, (byte) 0xb8, (byte) 0xae,
-          (byte) 0x16, (byte) 0x56, (byte) 0xe3, (byte) 0xfa,
-          (byte) 0xf1, (byte) 0x9f, (byte) 0xc4, (byte) 0x6a,
-          (byte) 0xda, (byte) 0x09, (byte) 0x8d, (byte) 0xeb,
-          (byte) 0x9c, (byte) 0x32, (byte) 0xb1, (byte) 0xfd,
-          (byte) 0x86, (byte) 0x62, (byte) 0x05, (byte) 0x16,
-          (byte) 0x5f, (byte) 0x49, (byte) 0xb8, (byte) 0x00,
-        },
-        // 39382357235489614581723060781553021112529911719440698176882885853963445705823
-        new byte[] {
-          (byte) 0x5f, (byte) 0x9c, (byte) 0x95, (byte) 0xbc,
-          (byte) 0xa3, (byte) 0x50, (byte) 0x8c, (byte) 0x24,
-          (byte) 0xb1, (byte) 0xd0, (byte) 0xb1, (byte) 0x55,
-          (byte) 0x9c, (byte) 0x83, (byte) 0xef, (byte) 0x5b,
-          (byte) 0x04, (byte) 0x44, (byte) 0x5c, (byte) 0xc4,
-          (byte) 0x58, (byte) 0x1c, (byte) 0x8e, (byte) 0x86,
-          (byte) 0xd8, (byte) 0x22, (byte) 0x4e, (byte) 0xdd,
-          (byte) 0xd0, (byte) 0x9f, (byte) 0x11, (byte) 0x57
-        },
-        // 2^255 - 19 - 1
-        new byte[] {
-          (byte) 0xec, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f,
-        },
-        // 2^255 - 19
-        new byte[] {
-          (byte) 0xed, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
-        },
-        // 2^255 - 19 + 1
-        new byte[] {
-          (byte) 0xee, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
-          (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
-        }
-      };
-  /**
-   * Computes Montgomery's double-and-add formulas.
-   *
-   * <p>On entry and exit, the absolute value of the limbs of all inputs and outputs are < 2^26.
-   *
-   * @param x2 x projective coordinate of output 2Q, long form
-   * @param z2 z projective coordinate of output 2Q, long form
-   * @param x3 x projective coordinate of output Q + Q', long form
-   * @param z3 z projective coordinate of output Q + Q', long form
-   * @param x x projective coordinate of input Q, short form, destroyed
-   * @param z z projective coordinate of input Q, short form, destroyed
-   * @param xprime x projective coordinate of input Q', short form, destroyed
-   * @param zprime z projective coordinate of input Q', short form, destroyed
-   * @param qmqp input Q - Q', short form, preserved
-   */
-  private static void monty(
-      long[] x2,
-      long[] z2,
-      long[] x3,
-      long[] z3,
-      long[] x,
-      long[] z,
-      long[] xprime,
-      long[] zprime,
-      long[] qmqp) {
-    long[] origx = Arrays.copyOf(x, Field25519.LIMB_CNT);
-    long[] zzz = new long[19];
-    long[] xx = new long[19];
-    long[] zz = new long[19];
-    long[] xxprime = new long[19];
-    long[] zzprime = new long[19];
-    long[] zzzprime = new long[19];
-    long[] xxxprime = new long[19];
-
-    Field25519.sum(x, z);
-    // |x[i]| < 2^27
-    Field25519.sub(z, origx); // does x - z
-    // |z[i]| < 2^27
-
-    long[] origxprime = Arrays.copyOf(xprime, Field25519.LIMB_CNT);
-    Field25519.sum(xprime, zprime);
-    // |xprime[i]| < 2^27
-    Field25519.sub(zprime, origxprime);
-    // |zprime[i]| < 2^27
-    Field25519.product(xxprime, xprime, z);
-    // |xxprime[i]| < 14*2^54: the largest product of two limbs will be < 2^(27+27) and {@ref
-    // Field25519#product} adds together, at most, 14 of those products. (Approximating that to
-    // 2^58 doesn't work out.)
-    Field25519.product(zzprime, x, zprime);
-    // |zzprime[i]| < 14*2^54
-    Field25519.reduceSizeByModularReduction(xxprime);
-    Field25519.reduceCoefficients(xxprime);
-    // |xxprime[i]| < 2^26
-    Field25519.reduceSizeByModularReduction(zzprime);
-    Field25519.reduceCoefficients(zzprime);
-    // |zzprime[i]| < 2^26
-    System.arraycopy(xxprime, 0, origxprime, 0, Field25519.LIMB_CNT);
-    Field25519.sum(xxprime, zzprime);
-    // |xxprime[i]| < 2^27
-    Field25519.sub(zzprime, origxprime);
-    // |zzprime[i]| < 2^27
-    Field25519.square(xxxprime, xxprime);
-    // |xxxprime[i]| < 2^26
-    Field25519.square(zzzprime, zzprime);
-    // |zzzprime[i]| < 2^26
-    Field25519.product(zzprime, zzzprime, qmqp);
-    // |zzprime[i]| < 14*2^52
-    Field25519.reduceSizeByModularReduction(zzprime);
-    Field25519.reduceCoefficients(zzprime);
-    // |zzprime[i]| < 2^26
-    System.arraycopy(xxxprime, 0, x3, 0, Field25519.LIMB_CNT);
-    System.arraycopy(zzprime, 0, z3, 0, Field25519.LIMB_CNT);
-
-    Field25519.square(xx, x);
-    // |xx[i]| < 2^26
-    Field25519.square(zz, z);
-    // |zz[i]| < 2^26
-    Field25519.product(x2, xx, zz);
-    // |x2[i]| < 14*2^52
-    Field25519.reduceSizeByModularReduction(x2);
-    Field25519.reduceCoefficients(x2);
-    // |x2[i]| < 2^26
-    Field25519.sub(zz, xx); // does zz = xx - zz
-    // |zz[i]| < 2^27
-    Arrays.fill(zzz, Field25519.LIMB_CNT, zzz.length - 1, 0);
-    Field25519.scalarProduct(zzz, zz, 121665);
-    // |zzz[i]| < 2^(27+17)
-    // No need to call reduceSizeByModularReduction here: scalarProduct doesn't increase the degree
-    // of its input.
-    Field25519.reduceCoefficients(zzz);
-    // |zzz[i]| < 2^26
-    Field25519.sum(zzz, xx);
-    // |zzz[i]| < 2^27
-    Field25519.product(z2, zz, zzz);
-    // |z2[i]| < 14*2^(26+27)
-    Field25519.reduceSizeByModularReduction(z2);
-    Field25519.reduceCoefficients(z2);
-    // |z2|i| < 2^26
-  }
-
-  /**
-   * Conditionally swap two reduced-form limb arrays if {@code iswap} is 1, but leave them unchanged
-   * if {@code iswap} is 0. Runs in data-invariant time to avoid side-channel attacks.
-   *
-   * <p>NOTE that this function requires that {@code iswap} be 1 or 0; other values give wrong
-   * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
-   * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
-   * have magnitude less than Integer.MAX_VALUE.
-   */
-  static void swapConditional(long[] a, long[] b, int iswap) {
-    int swap = -iswap;
-    for (int i = 0; i < Field25519.LIMB_CNT; i++) {
-      int x = swap & (((int) a[i]) ^ ((int) b[i]));
-      a[i] = ((int) a[i]) ^ x;
-      b[i] = ((int) b[i]) ^ x;
-    }
-  }
-
-  /**
-   * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1,
-   * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
-   * side-channel attacks.
-   *
-   * <p>NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong
-   * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
-   * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
-   * have magnitude less than Integer.MAX_VALUE.
-   */
-  static void copyConditional(long[] a, long[] b, int icopy) {
-    int copy = -icopy;
-    for (int i = 0; i < Field25519.LIMB_CNT; i++) {
-      int x = copy & (((int) a[i]) ^ ((int) b[i]));
-      a[i] = ((int) a[i]) ^ x;
-    }
-  }
-
-  /**
-   * Calculates nQ where Q is the x-coordinate of a point on the curve.
-   *
-   * @param resultx the x projective coordinate of the resulting curve point (short form).
-   * @param n a little endian, 32-byte number.
-   * @param qBytes a little endian, 32-byte number representing the public point' x coordinate.
-   * @throws InvalidKeyException iff the public key is in the banned list or its length is not
-   *     32-byte.
-   * @throws IllegalStateException iff there is arithmetic error.
-   */
-  static void curveMult(long[] resultx, byte[] n, byte[] qBytes) throws InvalidKeyException {
-    byte[] qBytesWithoutMsb = validatePubKeyAndClearMsb(qBytes);
-
-    long[] q = Field25519.expand(qBytesWithoutMsb);
-    long[] nqpqx = new long[19];
-    long[] nqpqz = new long[19];
-    nqpqz[0] = 1;
-    long[] nqx = new long[19];
-    nqx[0] = 1;
-    long[] nqz = new long[19];
-    long[] nqpqx2 = new long[19];
-    long[] nqpqz2 = new long[19];
-    nqpqz2[0] = 1;
-    long[] nqx2 = new long[19];
-    long[] nqz2 = new long[19];
-    nqz2[0] = 1;
-    long[] t = new long[19];
-
-    System.arraycopy(q, 0, nqpqx, 0, Field25519.LIMB_CNT);
-
-    for (int i = 0; i < Field25519.FIELD_LEN; i++) {
-      int b = n[Field25519.FIELD_LEN - i - 1] & 0xff;
-      for (int j = 0; j < 8; j++) {
-        int bit = (b >> (7 - j)) & 1;
-
-        swapConditional(nqx, nqpqx, bit);
-        swapConditional(nqz, nqpqz, bit);
-        monty(nqx2, nqz2, nqpqx2, nqpqz2, nqx, nqz, nqpqx, nqpqz, q);
-        swapConditional(nqx2, nqpqx2, bit);
-        swapConditional(nqz2, nqpqz2, bit);
-
-        t = nqx;
-        nqx = nqx2;
-        nqx2 = t;
-        t = nqz;
-        nqz = nqz2;
-        nqz2 = t;
-        t = nqpqx;
-        nqpqx = nqpqx2;
-        nqpqx2 = t;
-        t = nqpqz;
-        nqpqz = nqpqz2;
-        nqpqz2 = t;
-      }
-    }
-
-    // Computes nqx/nqz.
-    long[] zmone = new long[Field25519.LIMB_CNT];
-    Field25519.inverse(zmone, nqz);
-    Field25519.mult(resultx, nqx, zmone);
-
-    // Nowadays it should be standard to protect public key crypto against flaws. I.e. if there is a
-    // computation error through a faulty CPU or if the implementation contains a bug, then if
-    // possible this should be detected at run time.
-    //
-    // The situation is a bit more tricky for X25519 where for example the implementation
-    // proposed in https://tools.ietf.org/html/rfc7748 only uses the x-coordinate. However, a
-    // verification is still possible, but depends on the actual computation.
-    //
-    // Tink's Java implementation is equivalent to RFC7748. We will use the loop invariant in the
-    // Montgomery ladder to detect fault computation. In particular, we use the following invariant:
-    // q, resultx, nqpqx/nqpqx  are x coordinates of 3 collinear points q, n*q, (n + 1)*q.
-    if (!isCollinear(q, resultx, nqpqx, nqpqz)) {
-      throw new IllegalStateException(
-          "Arithmetic error in curve multiplication with the public key: "
-              + Hex.encode(qBytes));
-    }
-  }
-
-  /**
-   * Validates public key and clear its most significant bit.
-   *
-   * @throws InvalidKeyException iff the {@code pubKey} is in the banned list or its length is not
-   *     32-byte.
-   */
-  private static byte[] validatePubKeyAndClearMsb(byte[] pubKey) throws InvalidKeyException {
-    if (pubKey.length != 32) {
-      throw new InvalidKeyException("Public key length is not 32-byte");
-    }
-    // Clears the most significant bit as in the method decodeUCoordinate() of RFC7748.
-    byte[] pubKeyWithoutMsb = Arrays.copyOf(pubKey, pubKey.length);
-    pubKeyWithoutMsb[31] &= (byte) 0x7f;
-
-    for (int i = 0; i < BANNED_PUBLIC_KEYS.length; i++) {
-      if (Bytes.equal(BANNED_PUBLIC_KEYS[i], pubKeyWithoutMsb)) {
-        throw new InvalidKeyException("Banned public key: " + Hex.encode(BANNED_PUBLIC_KEYS[i]));
-      }
-    }
-    return pubKeyWithoutMsb;
-  }
-
-  /**
-   * Checks whether there are three collinear points with x coordinate x1, x2, x3/z3.
-   *
-   * @return true if three collinear points with x coordianate x1, x2, x3/z3 are collinear.
-   */
-  private static boolean isCollinear(long[] x1, long[] x2, long[] x3, long[] z3) {
-    // If x1, x2, x3 (in this method x3 is represented as x3/z3) are the x-coordinates of three
-    // collinear points on a curve, then they satisfy the equation
-    //   y^2 = x^3 + ax^2 + x
-    // They also satisfy the equation
-    //   0 = (x - x1)(x - x2)(x - x3)
-    //     = x^3 + Ax^2 + Bx + C
-    // where
-    //   A = - x1 - x2 - x3
-    //   B = x1*x2 + x2*x3 + x3*x1
-    //   C = - x1*x2*x3
-    // Hence, the three points also satisfy
-    //   y^2 = (a - A)x^2 + (1 - B)x - C
-    // This is a quadratic curve. Three distinct collinear points can only be on a quadratic
-    // curve if the quadratic curve has a line as component. And if a quadratic curve has a line
-    // as component then its discriminant is 0.
-    // Therefore, discriminant((a - A)x^2 + (1-B)x - C) = 0.
-    // In particular:
-    //   a = 486662
-    //   lhs = 4 * ((x1 + x2 + a) * z3 + x3) * (x1 * x2 * x3)
-    //   rhs = ((x1 * x2 - 1) * z3 + x3 * (x1 + x2))**2
-    //   assert (lhs - rhs)  == 0
-    //
-    // There are 2 cases that we haven't discussed:
-    //
-    //   * If x1 and x2 are both points with y-coordinate 0 then the argument doesn't hold.
-    //   However, our ECDH computation doesn't allow points of low order (see {@code
-    //   validatePublicKey}). Therefore, this edge case never happen.
-    //
-    //   * x1, x2 or x3/y3 may be points on the twist. If so, they satisfy the equation
-    //     2y^2 = x^3 + ax^2 + x
-    //   Hence, the three points also satisfy
-    //     2y^2 = (a - A)x^2 + (1 - B)x - C
-    //   Thus, this collinear check should work for this case too.
-    long[] x1multx2 = new long[Field25519.LIMB_CNT];
-    long[] x1addx2 = new long[Field25519.LIMB_CNT];
-    long[] lhs = new long[Field25519.LIMB_CNT + 1];
-    long[] t = new long[Field25519.LIMB_CNT + 1];
-    long[] t2 = new long[Field25519.LIMB_CNT + 1];
-    Field25519.mult(x1multx2, x1, x2);
-    Field25519.sum(x1addx2, x1, x2);
-    long[] a = new long[Field25519.LIMB_CNT];
-    a[0] = 486662;
-    // t = x1 + x2 + a
-    Field25519.sum(t, x1addx2, a);
-    // t = (x1 + x2 + a) * z3
-    Field25519.mult(t, t, z3);
-    // t = (x1 + x2 + a) * z3 + x3
-    Field25519.sum(t, x3);
-    // t = ((x1 + x2 + a) * z3 + x3) * x1 * x2
-    Field25519.mult(t, t, x1multx2);
-    // t = ((x1 + x2 + a) * z3 + x3) * (x1 * x2 * x3)
-    Field25519.mult(t, t, x3);
-    Field25519.scalarProduct(lhs, t, 4);
-    Field25519.reduceCoefficients(lhs);
-
-    // t = x1 * x2 * z3
-    Field25519.mult(t, x1multx2, z3);
-    // t = x1 * x2 * z3 - z3
-    Field25519.sub(t, t, z3);
-    // t2 = (x1 + x2) * x3
-    Field25519.mult(t2, x1addx2, x3);
-    // t = x1 * x2 * z3 - z3 + (x1 + x2) * x3
-    Field25519.sum(t, t, t2);
-    // t = (x1 * x2 * z3 - z3 + (x1 + x2) * x3)^2
-    Field25519.square(t, t);
-    return Bytes.equal(Field25519.contract(lhs), Field25519.contract(t));
-  }
-
-  private Curve25519() {}
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaSignJce.java b/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaSignJce.java
index f111346..5107463 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaSignJce.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaSignJce.java
@@ -22,9 +22,11 @@
 import com.google.crypto.tink.subtle.Enums.HashType;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
+import java.security.Provider;
 import java.security.Signature;
 import java.security.interfaces.ECPrivateKey;
 import java.security.spec.EllipticCurve;
+import java.util.List;
 
 /**
  * ECDSA signing with JCE.
@@ -56,7 +58,10 @@
 
   @Override
   public byte[] sign(final byte[] data) throws GeneralSecurityException {
-    Signature signer = EngineFactory.SIGNATURE.getInstance(signatureAlgorithm);
+    // Prefer Conscrypt over other providers if available.
+    List<Provider> preferredProviders =
+        EngineFactory.toProviderList("GmsCore_OpenSSL", "AndroidOpenSSL", "Conscrypt");
+    Signature signer = EngineFactory.SIGNATURE.getInstance(signatureAlgorithm, preferredProviders);
     signer.initSign(privateKey);
     signer.update(data);
     byte[] signature = signer.sign();
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaVerifyJce.java b/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaVerifyJce.java
index 982278d..0382100 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaVerifyJce.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/EcdsaVerifyJce.java
@@ -22,9 +22,11 @@
 import com.google.crypto.tink.subtle.Enums.HashType;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
+import java.security.Provider;
 import java.security.Signature;
 import java.security.interfaces.ECPublicKey;
 import java.security.spec.EllipticCurve;
+import java.util.List;
 
 /**
  * ECDSA verifying with JCE.
@@ -68,7 +70,10 @@
     if (!EllipticCurves.isValidDerEncoding(derSignature)) {
       throw new GeneralSecurityException("Invalid signature");
     }
-    Signature verifier = EngineFactory.SIGNATURE.getInstance(signatureAlgorithm);
+    List<Provider> preferredProviders =
+        EngineFactory.toProviderList("GmsCore_OpenSSL", "AndroidOpenSSL", "Conscrypt");
+    Signature verifier =
+        EngineFactory.SIGNATURE.getInstance(signatureAlgorithm, preferredProviders);
     verifier.initVerify(publicKey);
     verifier.update(data);
     boolean verified = false;
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519.java
deleted file mode 100644
index 9ef0503..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519.java
+++ /dev/null
@@ -1,1618 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import static com.google.crypto.tink.subtle.Ed25519Constants.B2;
-import static com.google.crypto.tink.subtle.Ed25519Constants.B_TABLE;
-import static com.google.crypto.tink.subtle.Ed25519Constants.D;
-import static com.google.crypto.tink.subtle.Ed25519Constants.D2;
-import static com.google.crypto.tink.subtle.Ed25519Constants.SQRTM1;
-import static com.google.crypto.tink.subtle.Field25519.FIELD_LEN;
-import static com.google.crypto.tink.subtle.Field25519.LIMB_CNT;
-
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.util.Arrays;
-
-/**
- * This implementation is based on the ed25519/ref10 implementation in NaCl.
- *
- * <p>It implements this twisted Edwards curve:
- *
- * <pre>
- * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
- * </pre>
- *
- * @see <a href="https://eprint.iacr.org/2008/013.pdf">Bernstein D.J., Birkner P., Joye M., Lange
- *     T., Peters C. (2008) Twisted Edwards Curves</a>
- * @see <a href="https://eprint.iacr.org/2008/522.pdf">Hisil H., Wong K.KH., Carter G., Dawson E.
- *     (2008) Twisted Edwards Curves Revisited</a>
- */
-final class Ed25519 {
-
-  public static final int SECRET_KEY_LEN = FIELD_LEN;
-  public static final int PUBLIC_KEY_LEN = FIELD_LEN;
-  public static final int SIGNATURE_LEN = FIELD_LEN * 2;
-
-  // (x = 0, y = 1) point
-  private static final CachedXYT CACHED_NEUTRAL = new CachedXYT(
-      new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-      new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-      new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
-  private static final PartialXYZT NEUTRAL = new PartialXYZT(
-      new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-          new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-          new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
-      new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
-
-  /**
-   * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z
-   *
-   * Note that this is referred as ge_p2 in ref10 impl.
-   * Also note that x = X, y = Y and z = Z below following Java coding style.
-   *
-   * See
-   * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary
-   * Window Method.
-   *
-   * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html
-   */
-  private static class XYZ {
-
-    final long[] x;
-    final long[] y;
-    final long[] z;
-
-    XYZ() {
-      this(new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT]);
-    }
-
-    XYZ(long[] x, long[] y, long[] z) {
-      this.x = x;
-      this.y = y;
-      this.z = z;
-    }
-
-    XYZ(XYZ xyz) {
-      x = Arrays.copyOf(xyz.x, LIMB_CNT);
-      y = Arrays.copyOf(xyz.y, LIMB_CNT);
-      z = Arrays.copyOf(xyz.z, LIMB_CNT);
-    }
-
-    XYZ(PartialXYZT partialXYZT) {
-      this();
-      fromPartialXYZT(this, partialXYZT);
-    }
-
-    /**
-     * ge_p1p1_to_p2.c
-     */
-    static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) {
-      Field25519.mult(out.x, in.xyz.x, in.t);
-      Field25519.mult(out.y, in.xyz.y, in.xyz.z);
-      Field25519.mult(out.z, in.xyz.z, in.t);
-      return out;
-    }
-
-    /**
-     * Encodes this point to bytes.
-     */
-    byte[] toBytes() {
-      long[] recip = new long[LIMB_CNT];
-      long[] x = new long[LIMB_CNT];
-      long[] y = new long[LIMB_CNT];
-      Field25519.inverse(recip, z);
-      Field25519.mult(x, this.x, recip);
-      Field25519.mult(y, this.y, recip);
-      byte[] s = Field25519.contract(y);
-      s[31] = (byte) (s[31] ^ (getLsb(x) << 7));
-      return s;
-    }
-
-    /** Checks that the point is on curve */
-    boolean isOnCurve() {
-      long[] x2 = new long[LIMB_CNT];
-      Field25519.square(x2, x);
-      long[] y2 = new long[LIMB_CNT];
-      Field25519.square(y2, y);
-      long[] z2 = new long[LIMB_CNT];
-      Field25519.square(z2, z);
-      long[] z4 = new long[LIMB_CNT];
-      Field25519.square(z4, z2);
-      long[] lhs = new long[LIMB_CNT];
-      // lhs = y^2 - x^2
-      Field25519.sub(lhs, y2, x2);
-      // lhs = z^2 * (y2 - x2)
-      Field25519.mult(lhs, lhs, z2);
-      long[] rhs = new long[LIMB_CNT];
-      // rhs = x^2 * y^2
-      Field25519.mult(rhs, x2, y2);
-      // rhs = D * x^2 * y^2
-      Field25519.mult(rhs, rhs, D);
-      // rhs = z^4 + D * x^2 * y^2
-      Field25519.sum(rhs, z4);
-      // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually
-      // reduce it here.
-      Field25519.reduce(rhs, rhs);
-      // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2
-      return Bytes.equal(Field25519.contract(lhs), Field25519.contract(rhs));
-    }
-  }
-
-  /**
-   * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z,
-   * XY = ZT
-   *
-   * Note that this is referred as ge_p3 in ref10 impl.
-   * Also note that t = T below following Java coding style.
-   *
-   * See
-   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
-   *
-   * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
-   */
-  private static class XYZT {
-
-    final XYZ xyz;
-    final long[] t;
-
-    XYZT() {
-      this(new XYZ(), new long[LIMB_CNT]);
-    }
-
-    XYZT(XYZ xyz, long[] t) {
-      this.xyz = xyz;
-      this.t = t;
-    }
-
-    XYZT(PartialXYZT partialXYZT) {
-      this();
-      fromPartialXYZT(this, partialXYZT);
-    }
-
-    /**
-     * ge_p1p1_to_p2.c
-     */
-    private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) {
-      Field25519.mult(out.xyz.x, in.xyz.x, in.t);
-      Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z);
-      Field25519.mult(out.xyz.z, in.xyz.z, in.t);
-      Field25519.mult(out.t, in.xyz.x, in.xyz.y);
-      return out;
-    }
-
-    /**
-     * Decodes {@code s} into an extented projective point.
-     * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3
-     */
-    private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException {
-      long[] x = new long[LIMB_CNT];
-      long[] y = Field25519.expand(s);
-      long[] z = new long[LIMB_CNT]; z[0] = 1;
-      long[] t = new long[LIMB_CNT];
-      long[] u = new long[LIMB_CNT];
-      long[] v = new long[LIMB_CNT];
-      long[] vxx = new long[LIMB_CNT];
-      long[] check = new long[LIMB_CNT];
-      Field25519.square(u, y);
-      Field25519.mult(v, u, D);
-      Field25519.sub(u, u, z); // u = y^2 - 1
-      Field25519.sum(v, v, z); // v = dy^2 + 1
-
-      long[] v3 = new long[LIMB_CNT];
-      Field25519.square(v3, v);
-      Field25519.mult(v3, v3, v); // v3 = v^3
-      Field25519.square(x, v3);
-      Field25519.mult(x, x, v);
-      Field25519.mult(x, x, u); // x = uv^7
-
-      pow2252m3(x, x); // x = (uv^7)^((q-5)/8)
-      Field25519.mult(x, x, v3);
-      Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8)
-
-      Field25519.square(vxx, x);
-      Field25519.mult(vxx, vxx, v);
-      Field25519.sub(check, vxx, u); // vx^2-u
-      if (isNonZeroVarTime(check)) {
-        Field25519.sum(check, vxx, u); // vx^2+u
-        if (isNonZeroVarTime(check)) {
-          throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
-              + "coordinates. No square root exists for modulo 2^255-19");
-        }
-        Field25519.mult(x, x, SQRTM1);
-      }
-
-      if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) {
-        throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
-            + "coordinates. Computed x is zero and encoded x's least significant bit is not zero");
-      }
-      if (getLsb(x) == ((s[31] & 0xff) >> 7)) {
-        neg(x, x);
-      }
-
-      Field25519.mult(t, x, y);
-      return new XYZT(new XYZ(x, y, z), t);
-    }
-  }
-
-  /**
-   * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
-   *
-   * Note that this is referred as complete form in the original ref10 impl (ge_p1p1).
-   * Also note that t = T below following Java coding style.
-   *
-   * Although this has the same types as XYZT, it is redefined to have its own type so that it is
-   * readable and 1:1 corresponds to ref10 impl.
-   *
-   * Can be converted to XYZT as follows:
-   * X1 = X * T = x * Z * T = x * Z1
-   * Y1 = Y * Z = y * T * Z = y * Z1
-   * Z1 = Z * T = Z * T
-   * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1
-   */
-  private static class PartialXYZT {
-
-    final XYZ xyz;
-    final long[] t;
-
-    PartialXYZT() {
-      this(new XYZ(), new long[LIMB_CNT]);
-    }
-
-    PartialXYZT(XYZ xyz, long[] t) {
-      this.xyz = xyz;
-      this.t = t;
-    }
-
-    PartialXYZT(PartialXYZT other) {
-      xyz = new XYZ(other.xyz);
-      t = Arrays.copyOf(other.t, LIMB_CNT);
-    }
-  }
-
-  /**
-   * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of
-   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
-   * with Z = 1.
-   */
-  static class CachedXYT {
-
-    final long[] yPlusX;
-    final long[] yMinusX;
-    final long[] t2d;
-
-    CachedXYT() {
-      this(new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT]);
-    }
-
-    /**
-     * Creates a cached XYZT with Z = 1
-     *
-     * @param yPlusX y + x
-     * @param yMinusX y - x
-     * @param t2d 2d * xy
-     */
-    CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) {
-      this.yPlusX = yPlusX;
-      this.yMinusX = yMinusX;
-      this.t2d = t2d;
-    }
-
-    CachedXYT(CachedXYT other) {
-      yPlusX = Arrays.copyOf(other.yPlusX, LIMB_CNT);
-      yMinusX = Arrays.copyOf(other.yMinusX, LIMB_CNT);
-      t2d = Arrays.copyOf(other.t2d, LIMB_CNT);
-    }
-
-    // z is one implicitly, so this just copies {@code in} to {@code output}.
-    void multByZ(long[] output, long[] in) {
-      System.arraycopy(in, 0, output, 0, LIMB_CNT);
-    }
-
-    /**
-     * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value.
-     */
-    void copyConditional(CachedXYT other, int icopy) {
-      Curve25519.copyConditional(yPlusX, other.yPlusX, icopy);
-      Curve25519.copyConditional(yMinusX, other.yMinusX, icopy);
-      Curve25519.copyConditional(t2d, other.t2d, icopy);
-    }
-  }
-
-  private static class CachedXYZT extends CachedXYT {
-
-    private final long[] z;
-
-    CachedXYZT() {
-      this(new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT]);
-    }
-
-    /**
-     * ge_p3_to_cached.c
-     */
-    CachedXYZT(XYZT xyzt) {
-      this();
-      Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x);
-      Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x);
-      System.arraycopy(xyzt.xyz.z, 0, z, 0, LIMB_CNT);
-      Field25519.mult(t2d, xyzt.t, D2);
-    }
-
-    /**
-     * Creates a cached XYZT
-     *
-     * @param yPlusX Y + X
-     * @param yMinusX Y - X
-     * @param z Z
-     * @param t2d 2d * (XY/Z)
-     */
-    CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) {
-      super(yPlusX, yMinusX, t2d);
-      this.z = z;
-    }
-
-    @Override
-    public void multByZ(long[] output, long[] in) {
-      Field25519.mult(output, in, z);
-    }
-  }
-
-  /**
-   * Addition defined in Section 3.1 of
-   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
-   *
-   * Please note that this is a partial of the operation listed there leaving out the final
-   * conversion from PartialXYZT to XYZT.
-   *
-   * @param extended extended projective point input
-   * @param cached cached projective point input
-   */
-  private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
-    long[] t = new long[LIMB_CNT];
-
-    // Y1 + X1
-    Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
-
-    // Y1 - X1
-    Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
-
-    // A = (Y1 - X1) * (Y2 - X2)
-    Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX);
-
-    // B = (Y1 + X1) * (Y2 + X2)
-    Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX);
-
-    // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
-    Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
-
-    // Z1 * Z2
-    cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
-
-    // D = 2 * Z1 * Z2
-    Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
-
-    // X3 = B - A
-    Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
-    // Y3 = B + A
-    Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
-    // Z3 = D + C
-    Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t);
-
-    // T3 = D - C
-    Field25519.sub(partialXYZT.t, t, partialXYZT.t);
-  }
-
-  /**
-   * Based on the addition defined in Section 3.1 of
-   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
-   *
-   * Please note that this is a partial of the operation listed there leaving out the final
-   * conversion from PartialXYZT to XYZT.
-   *
-   * @param extended extended projective point input
-   * @param cached cached projective point input
-   */
-  private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
-    long[] t = new long[LIMB_CNT];
-
-    // Y1 + X1
-    Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
-
-    // Y1 - X1
-    Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
-
-    // A = (Y1 - X1) * (Y2 + X2)
-    Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX);
-
-    // B = (Y1 + X1) * (Y2 - X2)
-    Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX);
-
-    // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
-    Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
-
-    // Z1 * Z2
-    cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
-
-    // D = 2 * Z1 * Z2
-    Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
-
-    // X3 = B - A
-    Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
-    // Y3 = B + A
-    Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
-    // Z3 = D - C
-    Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t);
-
-    // T3 = D + C
-    Field25519.sum(partialXYZT.t, t, partialXYZT.t);
-  }
-
-  /**
-   * Doubles {@code p} and puts the result into this PartialXYZT.
-   *
-   * This is based on the addition defined in formula 7 in Section 3.3 of
-   * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
-   *
-   * Please note that this is a partial of the operation listed there leaving out the final
-   * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in
-   * the paper, H should be replaced with A+B.
-   */
-  private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) {
-    long[] t0 = new long[LIMB_CNT];
-
-    // XX = X1^2
-    Field25519.square(partialXYZT.xyz.x, p.x);
-
-    // YY = Y1^2
-    Field25519.square(partialXYZT.xyz.z, p.y);
-
-    // B' = Z1^2
-    Field25519.square(partialXYZT.t, p.z);
-
-    // B = 2 * B'
-    Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t);
-
-    // A = X1 + Y1
-    Field25519.sum(partialXYZT.xyz.y, p.x, p.y);
-
-    // AA = A^2
-    Field25519.square(t0, partialXYZT.xyz.y);
-
-    // Y3 = YY + XX
-    Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x);
-
-    // Z3 = YY - XX
-    Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x);
-
-    // X3 = AA - Y3
-    Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y);
-
-    // T3 = B - Z3
-    Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z);
-  }
-
-  /**
-   * Doubles {@code p} and puts the result into this PartialXYZT.
-   */
-  private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) {
-    doubleXYZ(partialXYZT, p.xyz);
-  }
-
-  /**
-   * Compares two byte values in constant time.
-   *
-   * Please note that this doesn't reuse {@link Curve25519#eq} method since the below inputs are
-   * byte values.
-   */
-  private static int eq(int a, int b) {
-    int r = ~(a ^ b) & 0xff;
-    r &= r << 4;
-    r &= r << 2;
-    r &= r << 1;
-    return (r >> 7) & 1;
-  }
-
-  /**
-   * This is a constant time operation where point b*B*256^pos is stored in {@code t}.
-   * When b is 0, t remains the same (i.e., neutral point).
-   *
-   * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select
-   * method negates the corresponding point if b is negative (which is straight forward in elliptic
-   * curves by just negating y coordinate). Therefore we can get multiples of B with the half of
-   * memory requirements.
-   *
-   * @param t neutral element (i.e., point 0), also serves as output.
-   * @param pos in B[pos][j] = (j+1)*B*256^pos
-   * @param b value in [-8, 8] range.
-   */
-  private static void select(CachedXYT t, int pos, byte b) {
-    int bnegative = (b & 0xff) >> 7;
-    int babs = b - (((-bnegative) & b) << 1);
-
-    t.copyConditional(B_TABLE[pos][0], eq(babs, 1));
-    t.copyConditional(B_TABLE[pos][1], eq(babs, 2));
-    t.copyConditional(B_TABLE[pos][2], eq(babs, 3));
-    t.copyConditional(B_TABLE[pos][3], eq(babs, 4));
-    t.copyConditional(B_TABLE[pos][4], eq(babs, 5));
-    t.copyConditional(B_TABLE[pos][5], eq(babs, 6));
-    t.copyConditional(B_TABLE[pos][6], eq(babs, 7));
-    t.copyConditional(B_TABLE[pos][7], eq(babs, 8));
-
-    long[] yPlusX = Arrays.copyOf(t.yMinusX, LIMB_CNT);
-    long[] yMinusX = Arrays.copyOf(t.yPlusX, LIMB_CNT);
-    long[] t2d = Arrays.copyOf(t.t2d, LIMB_CNT);
-    neg(t2d, t2d);
-    CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d);
-    t.copyConditional(minust, bnegative);
-  }
-
-  /**
-   * Computes {@code a}*B
-   * where a = a[0]+256*a[1]+...+256^31 a[31] and
-   * B is the Ed25519 base point (x,4/5) with x positive.
-   *
-   * Preconditions:
-   * a[31] <= 127
-   * @throws IllegalStateException iff there is arithmetic error.
-   */
-  @SuppressWarnings("NarrowingCompoundAssignment")
-  private static XYZ scalarMultWithBase(byte[] a) {
-    byte[] e = new byte[2 * FIELD_LEN];
-    for (int i = 0; i < FIELD_LEN; i++) {
-      e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf);
-      e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf);
-    }
-    // each e[i] is between 0 and 15
-    // e[63] is between 0 and 7
-
-    // Rewrite e in a way that each e[i] is in [-8, 8].
-    // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte
-    // a[63] can be at most 1.
-    int carry = 0;
-    for (int i = 0; i < e.length - 1; i++) {
-      e[i] += carry;
-      carry = e[i] + 8;
-      carry >>= 4;
-      e[i] -= carry << 4;
-    }
-    e[e.length - 1] += carry;
-
-    PartialXYZT ret = new PartialXYZT(NEUTRAL);
-    XYZT xyzt = new XYZT();
-    // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64
-    // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd
-    // indices. After the result, we can double the result point 4 times to shift the multiplication
-    // scalar by 4 bits.
-    for (int i = 1; i < e.length; i += 2) {
-      CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
-      select(t, i / 2, e[i]);
-      add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
-    }
-
-    // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result
-    // for the odd indices in e.
-    XYZ xyz = new XYZ();
-    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
-    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
-    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
-    doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
-
-    // Add multiples of B for even indices of e.
-    for (int i = 0; i < e.length; i += 2) {
-      CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
-      select(t, i / 2, e[i]);
-      add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
-    }
-
-    // This check is to protect against flaws, i.e. if there is a computation error through a
-    // faulty CPU or if the implementation contains a bug.
-    XYZ result = new XYZ(ret);
-    if (!result.isOnCurve()) {
-      throw new IllegalStateException("arithmetic error in scalar multiplication");
-    }
-    return result;
-  }
-
-  /**
-   * Computes {@code a}*B
-   * where a = a[0]+256*a[1]+...+256^31 a[31] and
-   * B is the Ed25519 base point (x,4/5) with x positive.
-   *
-   * Preconditions:
-   * a[31] <= 127
-   */
-  static byte[] scalarMultWithBaseToBytes(byte[] a) {
-    return scalarMultWithBase(a).toBytes();
-  }
-
-  @SuppressWarnings("NarrowingCompoundAssignment")
-  private static byte[] slide(byte[] a) {
-    byte[] r = new byte[256];
-    // Writes each bit in a[0..31] into r[0..255]:
-    // a = a[0]+256*a[1]+...+256^31*a[31] is equal to
-    // r = r[0]+2*r[1]+...+2^255*r[255]
-    for (int i = 0; i < 256; i++) {
-      r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7)));
-    }
-
-    // Transforms r[i] as odd values in [-15, 15]
-    for (int i = 0; i < 256; i++) {
-      if (r[i] != 0) {
-        for (int b = 1; b <= 6 && i + b < 256; b++) {
-          if (r[i + b] != 0) {
-            if (r[i] + (r[i + b] << b) <= 15) {
-              r[i] += r[i + b] << b;
-              r[i + b] = 0;
-            } else if (r[i] - (r[i + b] << b) >= -15) {
-              r[i] -= r[i + b] << b;
-              for (int k = i + b; k < 256; k++) {
-                if (r[k] == 0) {
-                  r[k] = 1;
-                  break;
-                }
-                r[k] = 0;
-              }
-            } else {
-              break;
-            }
-          }
-        }
-      }
-    }
-    return r;
-  }
-
-  /**
-   * Computes {@code a}*{@code pointA}+{@code b}*B
-   * where a = a[0]+256*a[1]+...+256^31*a[31].
-   * and b = b[0]+256*b[1]+...+256^31*b[31].
-   * B is the Ed25519 base point (x,4/5) with x positive.
-   *
-   * Note that execution time varies based on the input since this will only be used in verification
-   * of signatures.
-   */
-  private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) {
-    // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA
-    CachedXYZT[] pointAArray = new CachedXYZT[8];
-    pointAArray[0] = new CachedXYZT(pointA);
-    PartialXYZT t = new PartialXYZT();
-    doubleXYZT(t, pointA);
-    XYZT doubleA = new XYZT(t);
-    for (int i = 1; i < pointAArray.length; i++) {
-      add(t, doubleA, pointAArray[i - 1]);
-      pointAArray[i] = new CachedXYZT(new XYZT(t));
-    }
-
-    byte[] aSlide = slide(a);
-    byte[] bSlide = slide(b);
-    t = new PartialXYZT(NEUTRAL);
-    XYZT u = new XYZT();
-    int i = 255;
-    for (; i >= 0; i--) {
-      if (aSlide[i] != 0 || bSlide[i] != 0) {
-        break;
-      }
-    }
-    for (; i >= 0; i--) {
-      doubleXYZ(t, new XYZ(t));
-      if (aSlide[i] > 0) {
-        add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]);
-      } else if (aSlide[i] < 0) {
-        sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]);
-      }
-      if (bSlide[i] > 0) {
-        add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]);
-      } else if (bSlide[i] < 0) {
-        sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]);
-      }
-    }
-
-    return new XYZ(t);
-  }
-
-  /**
-   * Returns true if {@code in} is nonzero.
-   *
-   * Note that execution time might depend on the input {@code in}.
-   */
-  private static boolean isNonZeroVarTime(long[] in) {
-    long[] inCopy = new long[in.length + 1];
-    System.arraycopy(in, 0, inCopy, 0, in.length);
-    Field25519.reduceCoefficients(inCopy);
-    byte[] bytes = Field25519.contract(inCopy);
-    for (byte b : bytes) {
-      if (b != 0) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Returns the least significant bit of {@code in}.
-   */
-  private static int getLsb(long[] in) {
-    return Field25519.contract(in)[0] & 1;
-  }
-
-  /**
-   * Negates all values in {@code in} and store it in {@code out}.
-   */
-  private static void neg(long[] out, long[] in) {
-    for (int i = 0; i < in.length; i++) {
-      out[i] = -in[i];
-    }
-  }
-
-  /**
-   * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}.
-   */
-  private static void pow2252m3(long[] out, long[] in) {
-    long[] t0 = new long[LIMB_CNT];
-    long[] t1 = new long[LIMB_CNT];
-    long[] t2 = new long[LIMB_CNT];
-
-    // z2 = z1^2^1
-    Field25519.square(t0, in);
-
-    // z8 = z2^2^2
-    Field25519.square(t1, t0);
-    for (int i = 1; i < 2; i++) {
-      Field25519.square(t1, t1);
-    }
-
-    // z9 = z1*z8
-    Field25519.mult(t1, in, t1);
-
-    // z11 = z2*z9
-    Field25519.mult(t0, t0, t1);
-
-    // z22 = z11^2^1
-    Field25519.square(t0, t0);
-
-    // z_5_0 = z9*z22
-    Field25519.mult(t0, t1, t0);
-
-    // z_10_5 = z_5_0^2^5
-    Field25519.square(t1, t0);
-    for (int i = 1; i < 5; i++) {
-      Field25519.square(t1, t1);
-    }
-
-    // z_10_0 = z_10_5*z_5_0
-    Field25519.mult(t0, t1, t0);
-
-    // z_20_10 = z_10_0^2^10
-    Field25519.square(t1, t0);
-    for (int i = 1; i < 10; i++) {
-      Field25519.square(t1, t1);
-    }
-
-    // z_20_0 = z_20_10*z_10_0
-    Field25519.mult(t1, t1, t0);
-
-    // z_40_20 = z_20_0^2^20
-    Field25519.square(t2, t1);
-    for (int i = 1; i < 20; i++) {
-      Field25519.square(t2, t2);
-    }
-
-    // z_40_0 = z_40_20*z_20_0
-    Field25519.mult(t1, t2, t1);
-
-    // z_50_10 = z_40_0^2^10
-    Field25519.square(t1, t1);
-    for (int i = 1; i < 10; i++) {
-      Field25519.square(t1, t1);
-    }
-
-    // z_50_0 = z_50_10*z_10_0
-    Field25519.mult(t0, t1, t0);
-
-    // z_100_50 = z_50_0^2^50
-    Field25519.square(t1, t0);
-    for (int i = 1; i < 50; i++) {
-      Field25519.square(t1, t1);
-    }
-
-    // z_100_0 = z_100_50*z_50_0
-    Field25519.mult(t1, t1, t0);
-
-    // z_200_100 = z_100_0^2^100
-    Field25519.square(t2, t1);
-    for (int i = 1; i < 100; i++) {
-      Field25519.square(t2, t2);
-    }
-
-    // z_200_0 = z_200_100*z_100_0
-    Field25519.mult(t1, t2, t1);
-
-    // z_250_50 = z_200_0^2^50
-    Field25519.square(t1, t1);
-    for (int i = 1; i < 50; i++) {
-      Field25519.square(t1, t1);
-    }
-
-    // z_250_0 = z_250_50*z_50_0
-    Field25519.mult(t0, t1, t0);
-
-    // z_252_2 = z_250_0^2^2
-    Field25519.square(t0, t0);
-    for (int i = 1; i < 2; i++) {
-      Field25519.square(t0, t0);
-    }
-
-    // z_252_3 = z_252_2*z1
-    Field25519.mult(out, t0, in);
-  }
-
-  /**
-   * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format.
-   */
-  private static long load3(byte[] in, int idx) {
-    long result;
-    result = (long) in[idx] & 0xff;
-    result |= (long) (in[idx + 1] & 0xff) << 8;
-    result |= (long) (in[idx + 2] & 0xff) << 16;
-    return result;
-  }
-
-  /**
-   * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format.
-   */
-  private static long load4(byte[] in, int idx) {
-    long result = load3(in, idx);
-    result |= (long) (in[idx + 3] & 0xff) << 24;
-    return result;
-  }
-
-  /**
-   * Input:
-   * s[0]+256*s[1]+...+256^63*s[63] = s
-   *
-   * Output:
-   * s[0]+256*s[1]+...+256^31*s[31] = s mod l
-   * where l = 2^252 + 27742317777372353535851937790883648493.
-   * Overwrites s in place.
-   */
-  private static void reduce(byte[] s) {
-    // Observation:
-    // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l
-    // Let m = -27742317777372353535851937790883648493
-    // Thus a*2^252+b mod l is equivalent to a*m+b mod l
-    //
-    // First s is divided into chunks of 21 bits as follows:
-    // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63]
-    long s0 = 2097151 & load3(s, 0);
-    long s1 = 2097151 & (load4(s, 2) >> 5);
-    long s2 = 2097151 & (load3(s, 5) >> 2);
-    long s3 = 2097151 & (load4(s, 7) >> 7);
-    long s4 = 2097151 & (load4(s, 10) >> 4);
-    long s5 = 2097151 & (load3(s, 13) >> 1);
-    long s6 = 2097151 & (load4(s, 15) >> 6);
-    long s7 = 2097151 & (load3(s, 18) >> 3);
-    long s8 = 2097151 & load3(s, 21);
-    long s9 = 2097151 & (load4(s, 23) >> 5);
-    long s10 = 2097151 & (load3(s, 26) >> 2);
-    long s11 = 2097151 & (load4(s, 28) >> 7);
-    long s12 = 2097151 & (load4(s, 31) >> 4);
-    long s13 = 2097151 & (load3(s, 34) >> 1);
-    long s14 = 2097151 & (load4(s, 36) >> 6);
-    long s15 = 2097151 & (load3(s, 39) >> 3);
-    long s16 = 2097151 & load3(s, 42);
-    long s17 = 2097151 & (load4(s, 44) >> 5);
-    long s18 = 2097151 & (load3(s, 47) >> 2);
-    long s19 = 2097151 & (load4(s, 49) >> 7);
-    long s20 = 2097151 & (load4(s, 52) >> 4);
-    long s21 = 2097151 & (load3(s, 55) >> 1);
-    long s22 = 2097151 & (load4(s, 57) >> 6);
-    long s23 = (load4(s, 60) >> 3);
-    long carry0;
-    long carry1;
-    long carry2;
-    long carry3;
-    long carry4;
-    long carry5;
-    long carry6;
-    long carry7;
-    long carry8;
-    long carry9;
-    long carry10;
-    long carry11;
-    long carry12;
-    long carry13;
-    long carry14;
-    long carry15;
-    long carry16;
-
-    // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l
-    // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6)
-    // starting from s11 (s11*2^210)
-    // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs
-    s11 += s23 * 666643;
-    s12 += s23 * 470296;
-    s13 += s23 * 654183;
-    s14 -= s23 * 997805;
-    s15 += s23 * 136657;
-    s16 -= s23 * 683901;
-    // s23 = 0;
-
-    s10 += s22 * 666643;
-    s11 += s22 * 470296;
-    s12 += s22 * 654183;
-    s13 -= s22 * 997805;
-    s14 += s22 * 136657;
-    s15 -= s22 * 683901;
-    // s22 = 0;
-
-    s9 += s21 * 666643;
-    s10 += s21 * 470296;
-    s11 += s21 * 654183;
-    s12 -= s21 * 997805;
-    s13 += s21 * 136657;
-    s14 -= s21 * 683901;
-    // s21 = 0;
-
-    s8 += s20 * 666643;
-    s9 += s20 * 470296;
-    s10 += s20 * 654183;
-    s11 -= s20 * 997805;
-    s12 += s20 * 136657;
-    s13 -= s20 * 683901;
-    // s20 = 0;
-
-    s7 += s19 * 666643;
-    s8 += s19 * 470296;
-    s9 += s19 * 654183;
-    s10 -= s19 * 997805;
-    s11 += s19 * 136657;
-    s12 -= s19 * 683901;
-    // s19 = 0;
-
-    s6 += s18 * 666643;
-    s7 += s18 * 470296;
-    s8 += s18 * 654183;
-    s9 -= s18 * 997805;
-    s10 += s18 * 136657;
-    s11 -= s18 * 683901;
-    // s18 = 0;
-
-    // Reduce the bit length of limbs from s6 to s15 to 21-bits.
-    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
-    carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21;
-    carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21;
-    carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21;
-
-    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
-    carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21;
-    carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21;
-
-    // Resume reduction where we left off.
-    s5 += s17 * 666643;
-    s6 += s17 * 470296;
-    s7 += s17 * 654183;
-    s8 -= s17 * 997805;
-    s9 += s17 * 136657;
-    s10 -= s17 * 683901;
-    // s17 = 0;
-
-    s4 += s16 * 666643;
-    s5 += s16 * 470296;
-    s6 += s16 * 654183;
-    s7 -= s16 * 997805;
-    s8 += s16 * 136657;
-    s9 -= s16 * 683901;
-    // s16 = 0;
-
-    s3 += s15 * 666643;
-    s4 += s15 * 470296;
-    s5 += s15 * 654183;
-    s6 -= s15 * 997805;
-    s7 += s15 * 136657;
-    s8 -= s15 * 683901;
-    // s15 = 0;
-
-    s2 += s14 * 666643;
-    s3 += s14 * 470296;
-    s4 += s14 * 654183;
-    s5 -= s14 * 997805;
-    s6 += s14 * 136657;
-    s7 -= s14 * 683901;
-    // s14 = 0;
-
-    s1 += s13 * 666643;
-    s2 += s13 * 470296;
-    s3 += s13 * 654183;
-    s4 -= s13 * 997805;
-    s5 += s13 * 136657;
-    s6 -= s13 * 683901;
-    // s13 = 0;
-
-    s0 += s12 * 666643;
-    s1 += s12 * 470296;
-    s2 += s12 * 654183;
-    s3 -= s12 * 997805;
-    s4 += s12 * 136657;
-    s5 -= s12 * 683901;
-    s12 = 0;
-
-    // Reduce the range of limbs from s0 to s11 to 21-bits.
-    carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
-
-    carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
-
-    s0 += s12 * 666643;
-    s1 += s12 * 470296;
-    s2 += s12 * 654183;
-    s3 -= s12 * 997805;
-    s4 += s12 * 136657;
-    s5 -= s12 * 683901;
-    s12 = 0;
-
-    // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs.
-    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
-    carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
-
-    // Do one last reduction as s12 might be 1.
-    s0 += s12 * 666643;
-    s1 += s12 * 470296;
-    s2 += s12 * 654183;
-    s3 -= s12 * 997805;
-    s4 += s12 * 136657;
-    s5 -= s12 * 683901;
-    // s12 = 0;
-
-    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
-
-    // Serialize the result into the s.
-    s[0] = (byte) s0;
-    s[1] = (byte) (s0 >> 8);
-    s[2] = (byte) ((s0 >> 16) | (s1 << 5));
-    s[3] = (byte) (s1 >> 3);
-    s[4] = (byte) (s1 >> 11);
-    s[5] = (byte) ((s1 >> 19) | (s2 << 2));
-    s[6] = (byte) (s2 >> 6);
-    s[7] = (byte) ((s2 >> 14) | (s3 << 7));
-    s[8] = (byte) (s3 >> 1);
-    s[9] = (byte) (s3 >> 9);
-    s[10] = (byte) ((s3 >> 17) | (s4 << 4));
-    s[11] = (byte) (s4 >> 4);
-    s[12] = (byte) (s4 >> 12);
-    s[13] = (byte) ((s4 >> 20) | (s5 << 1));
-    s[14] = (byte) (s5 >> 7);
-    s[15] = (byte) ((s5 >> 15) | (s6 << 6));
-    s[16] = (byte) (s6 >> 2);
-    s[17] = (byte) (s6 >> 10);
-    s[18] = (byte) ((s6 >> 18) | (s7 << 3));
-    s[19] = (byte) (s7 >> 5);
-    s[20] = (byte) (s7 >> 13);
-    s[21] = (byte) s8;
-    s[22] = (byte) (s8 >> 8);
-    s[23] = (byte) ((s8 >> 16) | (s9 << 5));
-    s[24] = (byte) (s9 >> 3);
-    s[25] = (byte) (s9 >> 11);
-    s[26] = (byte) ((s9 >> 19) | (s10 << 2));
-    s[27] = (byte) (s10 >> 6);
-    s[28] = (byte) ((s10 >> 14) | (s11 << 7));
-    s[29] = (byte) (s11 >> 1);
-    s[30] = (byte) (s11 >> 9);
-    s[31] = (byte) (s11 >> 17);
-  }
-
-  /**
-   * Input:
-   * a[0]+256*a[1]+...+256^31*a[31] = a
-   * b[0]+256*b[1]+...+256^31*b[31] = b
-   * c[0]+256*c[1]+...+256^31*c[31] = c
-   *
-   * Output:
-   * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l
-   * where l = 2^252 + 27742317777372353535851937790883648493.
-   */
-  private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) {
-    // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c
-    // See Ed25519.reduce for related comments.
-    long a0 = 2097151 & load3(a, 0);
-    long a1 = 2097151 & (load4(a, 2) >> 5);
-    long a2 = 2097151 & (load3(a, 5) >> 2);
-    long a3 = 2097151 & (load4(a, 7) >> 7);
-    long a4 = 2097151 & (load4(a, 10) >> 4);
-    long a5 = 2097151 & (load3(a, 13) >> 1);
-    long a6 = 2097151 & (load4(a, 15) >> 6);
-    long a7 = 2097151 & (load3(a, 18) >> 3);
-    long a8 = 2097151 & load3(a, 21);
-    long a9 = 2097151 & (load4(a, 23) >> 5);
-    long a10 = 2097151 & (load3(a, 26) >> 2);
-    long a11 = (load4(a, 28) >> 7);
-    long b0 = 2097151 & load3(b, 0);
-    long b1 = 2097151 & (load4(b, 2) >> 5);
-    long b2 = 2097151 & (load3(b, 5) >> 2);
-    long b3 = 2097151 & (load4(b, 7) >> 7);
-    long b4 = 2097151 & (load4(b, 10) >> 4);
-    long b5 = 2097151 & (load3(b, 13) >> 1);
-    long b6 = 2097151 & (load4(b, 15) >> 6);
-    long b7 = 2097151 & (load3(b, 18) >> 3);
-    long b8 = 2097151 & load3(b, 21);
-    long b9 = 2097151 & (load4(b, 23) >> 5);
-    long b10 = 2097151 & (load3(b, 26) >> 2);
-    long b11 = (load4(b, 28) >> 7);
-    long c0 = 2097151 & load3(c, 0);
-    long c1 = 2097151 & (load4(c, 2) >> 5);
-    long c2 = 2097151 & (load3(c, 5) >> 2);
-    long c3 = 2097151 & (load4(c, 7) >> 7);
-    long c4 = 2097151 & (load4(c, 10) >> 4);
-    long c5 = 2097151 & (load3(c, 13) >> 1);
-    long c6 = 2097151 & (load4(c, 15) >> 6);
-    long c7 = 2097151 & (load3(c, 18) >> 3);
-    long c8 = 2097151 & load3(c, 21);
-    long c9 = 2097151 & (load4(c, 23) >> 5);
-    long c10 = 2097151 & (load3(c, 26) >> 2);
-    long c11 = (load4(c, 28) >> 7);
-    long s0;
-    long s1;
-    long s2;
-    long s3;
-    long s4;
-    long s5;
-    long s6;
-    long s7;
-    long s8;
-    long s9;
-    long s10;
-    long s11;
-    long s12;
-    long s13;
-    long s14;
-    long s15;
-    long s16;
-    long s17;
-    long s18;
-    long s19;
-    long s20;
-    long s21;
-    long s22;
-    long s23;
-    long carry0;
-    long carry1;
-    long carry2;
-    long carry3;
-    long carry4;
-    long carry5;
-    long carry6;
-    long carry7;
-    long carry8;
-    long carry9;
-    long carry10;
-    long carry11;
-    long carry12;
-    long carry13;
-    long carry14;
-    long carry15;
-    long carry16;
-    long carry17;
-    long carry18;
-    long carry19;
-    long carry20;
-    long carry21;
-    long carry22;
-
-    s0 = c0 + a0 * b0;
-    s1 = c1 + a0 * b1 + a1 * b0;
-    s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
-    s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
-    s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
-    s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
-    s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
-    s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
-    s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1
-        + a8 * b0;
-    s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2
-        + a8 * b1 + a9 * b0;
-    s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3
-        + a8 * b2 + a9 * b1 + a10 * b0;
-    s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4
-        + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
-    s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3
-        + a10 * b2 + a11 * b1;
-    s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3
-        + a11 * b2;
-    s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4
-        + a11 * b3;
-    s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
-    s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
-    s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
-    s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
-    s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
-    s20 = a9 * b11 + a10 * b10 + a11 * b9;
-    s21 = a10 * b11 + a11 * b10;
-    s22 = a11 * b11;
-    s23 = 0;
-
-    carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
-    carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21;
-    carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21;
-    carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21;
-    carry18 = (s18 + (1 << 20)) >> 21; s19 += carry18; s18 -= carry18 << 21;
-    carry20 = (s20 + (1 << 20)) >> 21; s21 += carry20; s20 -= carry20 << 21;
-    carry22 = (s22 + (1 << 20)) >> 21; s23 += carry22; s22 -= carry22 << 21;
-
-    carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
-    carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21;
-    carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21;
-    carry17 = (s17 + (1 << 20)) >> 21; s18 += carry17; s17 -= carry17 << 21;
-    carry19 = (s19 + (1 << 20)) >> 21; s20 += carry19; s19 -= carry19 << 21;
-    carry21 = (s21 + (1 << 20)) >> 21; s22 += carry21; s21 -= carry21 << 21;
-
-    s11 += s23 * 666643;
-    s12 += s23 * 470296;
-    s13 += s23 * 654183;
-    s14 -= s23 * 997805;
-    s15 += s23 * 136657;
-    s16 -= s23 * 683901;
-    // s23 = 0;
-
-    s10 += s22 * 666643;
-    s11 += s22 * 470296;
-    s12 += s22 * 654183;
-    s13 -= s22 * 997805;
-    s14 += s22 * 136657;
-    s15 -= s22 * 683901;
-    // s22 = 0;
-
-    s9 += s21 * 666643;
-    s10 += s21 * 470296;
-    s11 += s21 * 654183;
-    s12 -= s21 * 997805;
-    s13 += s21 * 136657;
-    s14 -= s21 * 683901;
-    // s21 = 0;
-
-    s8 += s20 * 666643;
-    s9 += s20 * 470296;
-    s10 += s20 * 654183;
-    s11 -= s20 * 997805;
-    s12 += s20 * 136657;
-    s13 -= s20 * 683901;
-    // s20 = 0;
-
-    s7 += s19 * 666643;
-    s8 += s19 * 470296;
-    s9 += s19 * 654183;
-    s10 -= s19 * 997805;
-    s11 += s19 * 136657;
-    s12 -= s19 * 683901;
-    // s19 = 0;
-
-    s6 += s18 * 666643;
-    s7 += s18 * 470296;
-    s8 += s18 * 654183;
-    s9 -= s18 * 997805;
-    s10 += s18 * 136657;
-    s11 -= s18 * 683901;
-    // s18 = 0;
-
-    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
-    carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21;
-    carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21;
-    carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21;
-
-    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
-    carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21;
-    carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21;
-
-    s5 += s17 * 666643;
-    s6 += s17 * 470296;
-    s7 += s17 * 654183;
-    s8 -= s17 * 997805;
-    s9 += s17 * 136657;
-    s10 -= s17 * 683901;
-    // s17 = 0;
-
-    s4 += s16 * 666643;
-    s5 += s16 * 470296;
-    s6 += s16 * 654183;
-    s7 -= s16 * 997805;
-    s8 += s16 * 136657;
-    s9 -= s16 * 683901;
-    // s16 = 0;
-
-    s3 += s15 * 666643;
-    s4 += s15 * 470296;
-    s5 += s15 * 654183;
-    s6 -= s15 * 997805;
-    s7 += s15 * 136657;
-    s8 -= s15 * 683901;
-    // s15 = 0;
-
-    s2 += s14 * 666643;
-    s3 += s14 * 470296;
-    s4 += s14 * 654183;
-    s5 -= s14 * 997805;
-    s6 += s14 * 136657;
-    s7 -= s14 * 683901;
-    // s14 = 0;
-
-    s1 += s13 * 666643;
-    s2 += s13 * 470296;
-    s3 += s13 * 654183;
-    s4 -= s13 * 997805;
-    s5 += s13 * 136657;
-    s6 -= s13 * 683901;
-    // s13 = 0;
-
-    s0 += s12 * 666643;
-    s1 += s12 * 470296;
-    s2 += s12 * 654183;
-    s3 -= s12 * 997805;
-    s4 += s12 * 136657;
-    s5 -= s12 * 683901;
-    s12 = 0;
-
-    carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21;
-
-    carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
-
-    s0 += s12 * 666643;
-    s1 += s12 * 470296;
-    s2 += s12 * 654183;
-    s3 -= s12 * 997805;
-    s4 += s12 * 136657;
-    s5 -= s12 * 683901;
-    s12 = 0;
-
-    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
-    carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
-
-    s0 += s12 * 666643;
-    s1 += s12 * 470296;
-    s2 += s12 * 654183;
-    s3 -= s12 * 997805;
-    s4 += s12 * 136657;
-    s5 -= s12 * 683901;
-    // s12 = 0;
-
-    carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21;
-    carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21;
-    carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21;
-    carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21;
-    carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21;
-    carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21;
-    carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21;
-    carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21;
-    carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21;
-    carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21;
-    carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21;
-
-    s[0] = (byte) s0;
-    s[1] = (byte) (s0 >> 8);
-    s[2] = (byte) ((s0 >> 16) | (s1 << 5));
-    s[3] = (byte) (s1 >> 3);
-    s[4] = (byte) (s1 >> 11);
-    s[5] = (byte) ((s1 >> 19) | (s2 << 2));
-    s[6] = (byte) (s2 >> 6);
-    s[7] = (byte) ((s2 >> 14) | (s3 << 7));
-    s[8] = (byte) (s3 >> 1);
-    s[9] = (byte) (s3 >> 9);
-    s[10] = (byte) ((s3 >> 17) | (s4 << 4));
-    s[11] = (byte) (s4 >> 4);
-    s[12] = (byte) (s4 >> 12);
-    s[13] = (byte) ((s4 >> 20) | (s5 << 1));
-    s[14] = (byte) (s5 >> 7);
-    s[15] = (byte) ((s5 >> 15) | (s6 << 6));
-    s[16] = (byte) (s6 >> 2);
-    s[17] = (byte) (s6 >> 10);
-    s[18] = (byte) ((s6 >> 18) | (s7 << 3));
-    s[19] = (byte) (s7 >> 5);
-    s[20] = (byte) (s7 >> 13);
-    s[21] = (byte) s8;
-    s[22] = (byte) (s8 >> 8);
-    s[23] = (byte) ((s8 >> 16) | (s9 << 5));
-    s[24] = (byte) (s9 >> 3);
-    s[25] = (byte) (s9 >> 11);
-    s[26] = (byte) ((s9 >> 19) | (s10 << 2));
-    s[27] = (byte) (s10 >> 6);
-    s[28] = (byte) ((s10 >> 14) | (s11 << 7));
-    s[29] = (byte) (s11 >> 1);
-    s[30] = (byte) (s11 >> 9);
-    s[31] = (byte) (s11 >> 17);
-  }
-
-  static byte[] getHashedScalar(final byte[] privateKey)
-      throws GeneralSecurityException {
-    MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512");
-    digest.update(privateKey, 0, FIELD_LEN);
-    byte[] h = digest.digest();
-    // https://tools.ietf.org/html/rfc8032#section-5.1.2.
-    // Clear the lowest three bits of the first octet.
-    h[0] = (byte) (h[0] & 248);
-    // Clear the highest bit of the last octet.
-    h[31] = (byte) (h[31] & 127);
-    // Set the second highest bit if the last octet.
-    h[31] = (byte) (h[31] | 64);
-    return h;
-  }
-
-  /**
-   * Returns the EdDSA signature for the {@code message} based on the {@code hashedPrivateKey}.
-   *
-   * @param message to sign
-   * @param publicKey {@link Ed25519#scalarMultToBytes(byte[])} of {@code hashedPrivateKey}
-   * @param hashedPrivateKey {@link Ed25519#getHashedScalar(byte[])} of the private key
-   * @return signature for the {@code message}.
-   * @throws GeneralSecurityException if there is no SHA-512 algorithm defined in
-   * {@link EngineFactory}.MESSAGE_DIGEST.
-   */
-  static byte[] sign(final byte[] message, final byte[] publicKey, final byte[] hashedPrivateKey)
-      throws GeneralSecurityException {
-    // Copying the message to make it thread-safe. Otherwise, if the caller modifies the message
-    // between the first and the second hash then it might leak the private key.
-    byte[] messageCopy = Arrays.copyOfRange(message, 0, message.length);
-    MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512");
-    digest.update(hashedPrivateKey, FIELD_LEN, FIELD_LEN);
-    digest.update(messageCopy);
-    byte[] r = digest.digest();
-    reduce(r);
-
-    byte[] rB = Arrays.copyOfRange(scalarMultWithBase(r).toBytes(), 0, FIELD_LEN);
-    digest.reset();
-    digest.update(rB);
-    digest.update(publicKey);
-    digest.update(messageCopy);
-    byte[] hram = digest.digest();
-    reduce(hram);
-    byte[] s = new byte[FIELD_LEN];
-    mulAdd(s, hram, hashedPrivateKey, r);
-    return Bytes.concat(rB, s);
-  }
-
-
-  // The order of the generator as unsigned bytes in little endian order.
-  // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748)
-  static final byte[] GROUP_ORDER = new byte[] {
-     (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c,
-     (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
-     (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2,
-     (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
-     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
-     (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10};
-
-  // Checks whether s represents an integer smaller than the order of the group.
-  // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check
-  // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.)
-  // @param s an integer in little-endian order.
-  private static boolean isSmallerThanGroupOrder(byte[] s) {
-    for (int j = FIELD_LEN - 1; j >= 0; j--) {
-      // compare unsigned bytes
-      int a = s[j] & 0xff;
-      int b = GROUP_ORDER[j] & 0xff;
-      if (a != b) {
-        return a < b;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with
-   * {@code publicKey}.
-   *
-   * @throws GeneralSecurityException if there is no SHA-512 algorithm defined in
-   * {@link EngineFactory}.MESSAGE_DIGEST.
-   */
-  static boolean verify(final byte[] message, final byte[] signature,
-      final byte[] publicKey) throws GeneralSecurityException {
-    if (signature.length != SIGNATURE_LEN) {
-      return false;
-    }
-    byte[] s = Arrays.copyOfRange(signature, FIELD_LEN, SIGNATURE_LEN);
-    if (!isSmallerThanGroupOrder(s)) {
-      return false;
-    }
-    MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512");
-    digest.update(signature, 0, FIELD_LEN);
-    digest.update(publicKey);
-    digest.update(message);
-    byte[] h = digest.digest();
-    reduce(h);
-
-    XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey);
-    XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s);
-    byte[] expectedR = xyz.toBytes();
-    for (int i = 0; i < FIELD_LEN; i++) {
-      if (expectedR[i] != signature[i]) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private Ed25519() {}
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Constants.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Constants.java
deleted file mode 100644
index 4d11464..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Constants.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import java.math.BigInteger;
-
-/** Constants used in {@link Ed25519}. */
-final class Ed25519Constants {
-
- // d = -121665 / 121666 mod 2^255-19
-  static final long[] D;
-  // 2d
-  static final long[] D2;
-  // 2^((p-1)/4) mod p where p = 2^255-19
-  static final long[] SQRTM1;
-
-  /**
-   * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] =
-   * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0]
-   *
-   * <p>See {@link Ed25519ConstantsGenerator}.
-   */
-  static final Ed25519.CachedXYT[][] B_TABLE;
-  static final Ed25519.CachedXYT[] B2;
-
-  private static final BigInteger P_BI =
-      BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
-  private static final BigInteger D_BI =
-      BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI);
-  private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI);
-  private static final BigInteger SQRTM1_BI =
-      BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI);
-
-  private static class Point {
-    private BigInteger x;
-    private BigInteger y;
-  }
-
-  private static BigInteger recoverX(BigInteger y) {
-    // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19
-    BigInteger xx =
-        y.pow(2)
-            .subtract(BigInteger.ONE)
-            .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI));
-    BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI);
-    if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) {
-      x = x.multiply(SQRTM1_BI).mod(P_BI);
-    }
-    if (x.testBit(0)) {
-      x = P_BI.subtract(x);
-    }
-    return x;
-  }
-
-  private static Point edwards(Point a, Point b) {
-    Point o = new Point();
-    BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI);
-    o.x =
-        (a.x.multiply(b.y).add(b.x.multiply(a.y)))
-            .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI))
-            .mod(P_BI);
-    o.y =
-        (a.y.multiply(b.y).add(a.x.multiply(b.x)))
-            .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI))
-            .mod(P_BI);
-    return o;
-  }
-
-  private static byte[] toLittleEndian(BigInteger n) {
-    byte[] b = new byte[32];
-    byte[] nBytes = n.toByteArray();
-    System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
-    for (int i = 0; i < b.length / 2; i++) {
-      byte t = b[i];
-      b[i] = b[b.length - i - 1];
-      b[b.length - i - 1] = t;
-    }
-    return b;
-  }
-
-  private static Ed25519.CachedXYT getCachedXYT(Point p) {
-    return new Ed25519.CachedXYT(
-        Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))),
-        Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))),
-        Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI))));
-  }
-
-  static {
-    Point b = new Point();
-    b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI);
-    b.x = recoverX(b.y);
-
-    D = Field25519.expand(toLittleEndian(D_BI));
-    D2 = Field25519.expand(toLittleEndian(D2_BI));
-    SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI));
-
-    Point bi = b;
-    B_TABLE = new Ed25519.CachedXYT[32][8];
-    for (int i = 0; i < 32; i++) {
-      Point bij = bi;
-      for (int j = 0; j < 8; j++) {
-        B_TABLE[i][j] = getCachedXYT(bij);
-        bij = edwards(bij, bi);
-      }
-      for (int j = 0; j < 8; j++) {
-        bi = edwards(bi, bi);
-      }
-    }
-    bi = b;
-    Point b2 = edwards(b, b);
-    B2 = new Ed25519.CachedXYT[8];
-    for (int i = 0; i < 8; i++) {
-      B2[i] = getCachedXYT(bi);
-      bi = edwards(bi, b2);
-    }
-  }
-
-  private Ed25519Constants() {}
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Sign.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Sign.java
index 2465921..d722e69 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Sign.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Sign.java
@@ -18,6 +18,8 @@
 
 import com.google.crypto.tink.PublicKeySign;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.internal.Ed25519;
+import com.google.crypto.tink.internal.Field25519;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Verify.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Verify.java
index 75621d0..7cf04e2 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Verify.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/Ed25519Verify.java
@@ -18,12 +18,17 @@
 
 import com.google.crypto.tink.PublicKeyVerify;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.internal.Ed25519;
+import com.google.crypto.tink.internal.Field25519;
 import com.google.crypto.tink.util.Bytes;
 import java.security.GeneralSecurityException;
 
 /**
  * Ed25519 verifying.
  *
+ * <p>The first call to this function may take longer, because Ed25519Constants needs to be
+ * initialized.
+ *
  * <h3>Usage</h3>
  *
  * <pre>{@code
@@ -61,6 +66,7 @@
           String.format("Given public key's length is not %s.", PUBLIC_KEY_LEN));
     }
     this.publicKey = Bytes.copyFrom(publicKey);
+    Ed25519.init();
   }
 
   @Override
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/EllipticCurves.java b/java_src/src/main/java/com/google/crypto/tink/subtle/EllipticCurves.java
index f4291c4..b112ce6 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/EllipticCurves.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/EllipticCurves.java
@@ -16,6 +16,8 @@
 
 package com.google.crypto.tink.subtle;
 
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.InvalidAlgorithmParameterException;
@@ -26,7 +28,6 @@
 import java.security.PublicKey;
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECFieldFp;
 import java.security.spec.ECParameterSpec;
 import java.security.spec.ECPoint;
 import java.security.spec.ECPrivateKeySpec;
@@ -67,84 +68,17 @@
   }
 
   public static ECParameterSpec getNistP256Params() {
-    return getNistCurveSpec(
-        "115792089210356248762697446949407573530086143415290314195533631308867097853951",
-        "115792089210356248762697446949407573529996955224135760342422259061068512044369",
-        "5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b",
-        "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
-        "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5");
+    return EllipticCurvesUtil.NIST_P256_PARAMS;
   }
 
   public static ECParameterSpec getNistP384Params() {
-    return getNistCurveSpec(
-        "3940200619639447921227904010014361380507973927046544666794829340"
-            + "4245721771496870329047266088258938001861606973112319",
-        "3940200619639447921227904010014361380507973927046544666794690527"
-            + "9627659399113263569398956308152294913554433653942643",
-        "b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875a"
-            + "c656398d8a2ed19d2a85c8edd3ec2aef",
-        "aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a38"
-            + "5502f25dbf55296c3a545e3872760ab7",
-        "3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c0"
-            + "0a60b1ce1d7e819d7a431d7c90ea0e5f");
+    return EllipticCurvesUtil.NIST_P384_PARAMS;
   }
 
   public static ECParameterSpec getNistP521Params() {
-    return getNistCurveSpec(
-        "6864797660130609714981900799081393217269435300143305409394463459"
-            + "18554318339765605212255964066145455497729631139148085803712198"
-            + "7999716643812574028291115057151",
-        "6864797660130609714981900799081393217269435300143305409394463459"
-            + "18554318339765539424505774633321719753296399637136332111386476"
-            + "8612440380340372808892707005449",
-        "051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef10"
-            + "9e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00",
-        "c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3d"
-            + "baa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66",
-        "11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e6"
-            + "62c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650");
+    return EllipticCurvesUtil.NIST_P521_PARAMS;
   }
 
-  /**
-   * Checks that a point is on a given elliptic curve.
-   *
-   * <p><b>Warning:</b> Please use {@link #validatePublicKey} if you want to validate a public key
-   * to avoid invalid curve attacks or small subgroup attacks in ECDH.
-   *
-   * <p>This method implements the partial public key validation routine from Section 5.6.2.6 of <a
-   * href="http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf">NIST SP
-   * 800-56A</a>. A partial public key validation is sufficient for curves with cofactor 1. See
-   * Section B.3 of http://www.nsa.gov/ia/_files/SuiteB_Implementer_G-113808.pdf.
-   *
-   * <p>The point validations above are taken from recommendations for ECDH, because parameter
-   * checks in ECDH are much more important than for the case of ECDSA. Performing this test for
-   * ECDSA keys is mainly a sanity check.
-   *
-   * @param point the point that needs verification
-   * @param ec the elliptic curve. This must be a curve over a prime order field.
-   * @throws GeneralSecurityException if the field is binary or if the point is not on the curve.
-   */
-  static void checkPointOnCurve(ECPoint point, EllipticCurve ec) throws GeneralSecurityException {
-    BigInteger p = getModulus(ec);
-    BigInteger x = point.getAffineX();
-    BigInteger y = point.getAffineY();
-    if (x == null || y == null) {
-      throw new GeneralSecurityException("point is at infinity");
-    }
-    // Check 0 <= x < p and 0 <= y < p.
-    if (x.signum() == -1 || x.compareTo(p) >= 0) {
-      throw new GeneralSecurityException("x is out of range");
-    }
-    if (y.signum() == -1 || y.compareTo(p) >= 0) {
-      throw new GeneralSecurityException("y is out of range");
-    }
-    // Check y^2 == x^3 + a x + b (mod p)
-    BigInteger lhs = y.multiply(y).mod(p);
-    BigInteger rhs = x.multiply(x).add(ec.getA()).multiply(x).add(ec.getB()).mod(p);
-    if (!lhs.equals(rhs)) {
-      throw new GeneralSecurityException("Point is not on curve");
-    }
-  }
 
   /**
    * Checks that the point of the public key is on the curve of the public key.
@@ -161,22 +95,17 @@
    * @throws GeneralSecurityException if the key is not valid.
    */
   static void checkPublicKey(ECPublicKey key) throws GeneralSecurityException {
-    checkPointOnCurve(key.getW(), key.getParams().getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(key.getW(), key.getParams().getCurve());
   }
 
   /** Returns whether {@code spec} is a {@link ECParameterSpec} of one of the NIST curves. */
   public static boolean isNistEcParameterSpec(ECParameterSpec spec) {
-    return isSameEcParameterSpec(spec, getNistP256Params())
-        || isSameEcParameterSpec(spec, getNistP384Params())
-        || isSameEcParameterSpec(spec, getNistP521Params());
+    return EllipticCurvesUtil.isNistEcParameterSpec(spec);
   }
 
   /** Returns whether {@code one} is the same {@link ECParameterSpec} as {@code two}. */
   public static boolean isSameEcParameterSpec(ECParameterSpec one, ECParameterSpec two) {
-    return one.getCurve().equals(two.getCurve())
-        && one.getGenerator().equals(two.getGenerator())
-        && one.getOrder().equals(two.getOrder())
-        && one.getCofactor() == two.getCofactor();
+    return EllipticCurvesUtil.isSameEcParameterSpec(one, two);
   }
 
   /**
@@ -188,7 +117,7 @@
   public static void validatePublicKey(ECPublicKey publicKey, ECPrivateKey privateKey)
       throws GeneralSecurityException {
     validatePublicKeySpec(publicKey, privateKey);
-    checkPointOnCurve(publicKey.getW(), privateKey.getParams().getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(publicKey.getW(), privateKey.getParams().getCurve());
   }
 
   /** Checks that the public key's params spec is the same as the private key's params spec. */
@@ -214,12 +143,7 @@
    * @return the order of the finite field over which curve is defined.
    */
   public static BigInteger getModulus(EllipticCurve curve) throws GeneralSecurityException {
-    java.security.spec.ECField field = curve.getField();
-    if (field instanceof java.security.spec.ECFieldFp) {
-      return ((java.security.spec.ECFieldFp) field).getP();
-    } else {
-      throw new GeneralSecurityException("Only curves over prime order fields are supported");
-    }
+    return EllipticCurvesUtil.getModulus(curve);
   }
 
   /**
@@ -242,23 +166,6 @@
     return (fieldSizeInBits(curve) + 7) / 8;
   }
 
-  private static ECParameterSpec getNistCurveSpec(
-      String decimalP, String decimalN, String hexB, String hexGX, String hexGY) {
-    final BigInteger p = new BigInteger(decimalP);
-    final BigInteger n = new BigInteger(decimalN);
-    final BigInteger three = new BigInteger("3");
-    final BigInteger a = p.subtract(three);
-    final BigInteger b = new BigInteger(hexB, 16);
-    final BigInteger gx = new BigInteger(hexGX, 16);
-    final BigInteger gy = new BigInteger(hexGY, 16);
-    final int h = 1;
-    ECFieldFp fp = new ECFieldFp(p);
-    java.security.spec.EllipticCurve curveSpec = new java.security.spec.EllipticCurve(fp, a, b);
-    ECPoint g = new ECPoint(gx, gy);
-    ECParameterSpec ecSpec = new ECParameterSpec(curveSpec, g, n, h);
-    return ecSpec;
-  }
-
   /**
    * Computes a square root modulo an odd prime. Timing and exceptions can leak information about
    * the inputs. Therefore this method must only be used to decompress public keys.
@@ -646,9 +553,7 @@
    * @return the point
    * @throws GeneralSecurityException if the encoded point is invalid or if the curve or format are
    *     not supported.
-   * @deprecated use {#pointDecode}
    */
-  @Deprecated
   public static ECPoint ecPointDecode(EllipticCurve curve, PointFormatType format, byte[] encoded)
       throws GeneralSecurityException {
     return pointDecode(curve, format, encoded);
@@ -699,7 +604,7 @@
           BigInteger y =
               new BigInteger(1, Arrays.copyOfRange(encoded, coordinateSize + 1, encoded.length));
           ECPoint point = new ECPoint(x, y);
-          checkPointOnCurve(point, curve);
+          EllipticCurvesUtil.checkPointOnCurve(point, curve);
           return point;
         }
       case DO_NOT_USE_CRUNCHY_UNCOMPRESSED:
@@ -711,7 +616,7 @@
           BigInteger y =
               new BigInteger(1, Arrays.copyOfRange(encoded, coordinateSize, encoded.length));
           ECPoint point = new ECPoint(x, y);
-          checkPointOnCurve(point, curve);
+          EllipticCurvesUtil.checkPointOnCurve(point, curve);
           return point;
         }
       case COMPRESSED:
@@ -768,14 +673,14 @@
    */
   public static byte[] pointEncode(EllipticCurve curve, PointFormatType format, ECPoint point)
       throws GeneralSecurityException {
-    checkPointOnCurve(point, curve);
+    EllipticCurvesUtil.checkPointOnCurve(point, curve);
     int coordinateSize = fieldSizeInBytes(curve);
     switch (format) {
       case UNCOMPRESSED:
         {
           byte[] encoded = new byte[2 * coordinateSize + 1];
-          byte[] x = point.getAffineX().toByteArray();
-          byte[] y = point.getAffineY().toByteArray();
+          byte[] x = BigIntegerEncoding.toBigEndianBytes(point.getAffineX());
+          byte[] y = BigIntegerEncoding.toBigEndianBytes(point.getAffineY());
           // Order of System.arraycopy is important because x,y can have leading 0's.
           System.arraycopy(y, 0, encoded, 1 + 2 * coordinateSize - y.length, y.length);
           System.arraycopy(x, 0, encoded, 1 + coordinateSize - x.length, x.length);
@@ -785,12 +690,12 @@
       case DO_NOT_USE_CRUNCHY_UNCOMPRESSED:
         {
           byte[] encoded = new byte[2 * coordinateSize];
-          byte[] x = point.getAffineX().toByteArray();
+          byte[] x = BigIntegerEncoding.toBigEndianBytes(point.getAffineX());
           if (x.length > coordinateSize) {
             // x has leading 0's, strip them.
             x = Arrays.copyOfRange(x, x.length - coordinateSize, x.length);
           }
-          byte[] y = point.getAffineY().toByteArray();
+          byte[] y = BigIntegerEncoding.toBigEndianBytes(point.getAffineY());
           if (y.length > coordinateSize) {
             // y has leading 0's, strip them.
             y = Arrays.copyOfRange(y, y.length - coordinateSize, y.length);
@@ -802,7 +707,7 @@
       case COMPRESSED:
         {
           byte[] encoded = new byte[coordinateSize + 1];
-          byte[] x = point.getAffineX().toByteArray();
+          byte[] x = BigIntegerEncoding.toBigEndianBytes(point.getAffineX());
           System.arraycopy(x, 0, encoded, 1 + coordinateSize - x.length, x.length);
           encoded[0] = (byte) (point.getAffineY().testBit(0) ? 3 : 2);
           return encoded;
@@ -874,7 +779,7 @@
     BigInteger pubX = new BigInteger(1, x);
     BigInteger pubY = new BigInteger(1, y);
     ECPoint w = new ECPoint(pubX, pubY);
-    checkPointOnCurve(w, ecParams.getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(w, ecParams.getCurve());
     ECPublicKeySpec spec = new ECPublicKeySpec(w, ecParams);
     KeyFactory kf = EngineFactory.KEY_FACTORY.getInstance("EC");
     return (ECPublicKey) kf.generatePublic(spec);
@@ -897,7 +802,7 @@
   public static ECPrivateKey getEcPrivateKey(CurveType curve, final byte[] keyValue)
       throws GeneralSecurityException {
     ECParameterSpec ecParams = getCurveSpec(curve);
-    BigInteger privValue = new BigInteger(1, keyValue);
+    BigInteger privValue = BigIntegerEncoding.fromUnsignedBigEndianBytes(keyValue);
     ECPrivateKeySpec spec = new ECPrivateKeySpec(privValue, ecParams);
     KeyFactory kf = EngineFactory.KEY_FACTORY.getInstance("EC");
     return (ECPrivateKey) kf.generatePrivate(spec);
@@ -927,7 +832,7 @@
       throw new GeneralSecurityException("shared secret is out of range");
     }
     // This will throw if x is not a valid coordinate.
-    getY(x, true /* lsb, doesn't matter here */, privateKeyCurve);
+    Object unused = getY(x, true /* lsb, doesn't matter here */, privateKeyCurve);
   }
 
   /* Generates the DH shared secret using {@code myPrivateKey} and {@code peerPublicKey} */
@@ -944,7 +849,7 @@
    */
   public static byte[] computeSharedSecret(ECPrivateKey myPrivateKey, ECPoint publicPoint)
       throws GeneralSecurityException {
-    checkPointOnCurve(publicPoint, myPrivateKey.getParams().getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(publicPoint, myPrivateKey.getParams().getCurve());
     // Explicitly reconstruct the peer public key using private key's spec.
     ECParameterSpec privSpec = myPrivateKey.getParams();
     ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(publicPoint, privSpec);
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/EngineFactory.java b/java_src/src/main/java/com/google/crypto/tink/subtle/EngineFactory.java
index 9aa9053..205663e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/EngineFactory.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/EngineFactory.java
@@ -26,7 +26,6 @@
 import java.security.Signature;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.logging.Logger;
 import javax.crypto.Cipher;
 import javax.crypto.KeyAgreement;
 import javax.crypto.Mac;
@@ -41,30 +40,120 @@
  *
  * @since 1.0.0
  */
-public final class EngineFactory<T_WRAPPER extends EngineWrapper<T_ENGINE>, T_ENGINE> {
-  private static final Logger logger = Logger.getLogger(EngineFactory.class.getName());
-  private static final List<Provider> policy;
-  private static final boolean LET_FALLBACK;
+public final class EngineFactory<T_WRAPPER extends EngineWrapper<JcePrimitiveT>, JcePrimitiveT> {
+  private final Policy<JcePrimitiveT> policy;
 
-  // Warning: keep this above the initialization of static providers below. or you'll get null
-  // pointer errors (due to this policy not being initialized).
-  static {
-    if (TinkFipsUtil.useOnlyFips()) {
-      // The only accepted provider is Conscrypt in FIPS-mode and no fallback is allowed.
-      policy = toProviderList("GmsCore_OpenSSL", "AndroidOpenSSL", "Conscrypt");
-      LET_FALLBACK = false;
-    } else if (SubtleUtil.isAndroid()) {
-      // TODO(thaidn): test this when Android building and testing are supported.
-      // On Android prefer Conscrypt, but allow fallback to other providers.
-      policy =
-          toProviderList(
-              "GmsCore_OpenSSL" /* Conscrypt in GmsCore, updatable thus preferrable */,
-              "AndroidOpenSSL" /* Conscrypt in AOSP, not updatable but still better than BC */);
-      LET_FALLBACK = true;
-    } else {
-      policy = new ArrayList<>();
-      LET_FALLBACK = true;
+  /**
+   * A Policy provides a wrapper around the JCE engines, and defines how a cipher instance will be
+   * retrieved. A preferred list of providers can be passed, which the policy might use to
+   * prioritize certain providers. For details see the specific policies.
+   */
+  private static interface Policy<JcePrimitiveT> {
+    public JcePrimitiveT getInstance(String algorithm) throws GeneralSecurityException;
+
+    public JcePrimitiveT getInstance(String algorithm, List<Provider> preferredProviders)
+        throws GeneralSecurityException;
+  }
+
+  /**
+   * The default policy, which uses the JDK priority for providers. If a list of preferred providers
+   * is provided, then these will be used first in the order they are given.
+   */
+  private static class DefaultPolicy<JcePrimitiveT> implements Policy<JcePrimitiveT> {
+    private DefaultPolicy(EngineWrapper<JcePrimitiveT> jceFactory) {
+      this.jceFactory = jceFactory;
     }
+
+    @Override
+    public JcePrimitiveT getInstance(String algorithm) throws GeneralSecurityException {
+      return this.jceFactory.getInstance(algorithm, null);
+    }
+
+    @Override
+    public JcePrimitiveT getInstance(String algorithm, List<Provider> preferredProviders)
+        throws GeneralSecurityException {
+      for (Provider provider : preferredProviders) {
+        try {
+          return this.jceFactory.getInstance(algorithm, provider);
+        } catch (Exception e) {
+          // Provider failed to provide instance, but we can continue with other providers.
+        }
+      }
+      return getInstance(algorithm);
+    }
+
+    private final EngineWrapper<JcePrimitiveT> jceFactory;
+  }
+
+  /**
+   * The FIPS policy, only allows Conscrypt as a provider. No other provider will be used, and any
+   * preferred provider will be ignored.
+   */
+  private static class FipsPolicy<JcePrimitiveT> implements Policy<JcePrimitiveT> {
+    private FipsPolicy(EngineWrapper<JcePrimitiveT> jceFactory) {
+      this.jceFactory = jceFactory;
+    }
+
+    @Override
+    public JcePrimitiveT getInstance(String algorithm) throws GeneralSecurityException {
+      List<Provider> conscryptProviders =
+          toProviderList("GmsCore_OpenSSL", "AndroidOpenSSL", "Conscrypt");
+      Exception cause = null;
+      for (Provider provider : conscryptProviders) {
+        try {
+          return this.jceFactory.getInstance(algorithm, provider);
+        } catch (Exception e) {
+          if (cause == null) {
+            cause = e;
+          }
+        }
+      }
+      throw new GeneralSecurityException("No good Provider found.", cause);
+    }
+
+    @Override
+    public JcePrimitiveT getInstance(String algorithm, List<Provider> preferredProviders)
+        throws GeneralSecurityException {
+      // Ignores preferred provider, as we don't allow overruling the policy.
+      return getInstance(algorithm);
+    }
+
+    private final EngineWrapper<JcePrimitiveT> jceFactory;
+  }
+
+  /**
+   * The Android policy always prefer Conscrypt as a provider, but allows to fallback on the JDK
+   * behavior if these are not available. Preferred providers will be ignored.
+   */
+  private static class AndroidPolicy<JcePrimitiveT> implements Policy<JcePrimitiveT> {
+    private AndroidPolicy(EngineWrapper<JcePrimitiveT> jceFactory) {
+      this.jceFactory = jceFactory;
+    }
+
+    @Override
+    public JcePrimitiveT getInstance(String algorithm) throws GeneralSecurityException {
+      List<Provider> conscryptProviders = toProviderList("GmsCore_OpenSSL", "AndroidOpenSSL");
+      Exception cause = null;
+      for (Provider provider : conscryptProviders) {
+        try {
+          return this.jceFactory.getInstance(algorithm, provider);
+        } catch (Exception e) {
+          if (cause == null) {
+            cause = e;
+          }
+        }
+      }
+      return this.jceFactory.getInstance(algorithm, null);
+    }
+
+    @Override
+    public JcePrimitiveT getInstance(String algorithm, List<Provider> preferredProviders)
+        throws GeneralSecurityException {
+      // Ignores preferred provider, as we don't allow overruling the policy.
+      return getInstance(algorithm);
+    }
+
+    private final EngineWrapper<JcePrimitiveT> jceFactory;
   }
 
   public static final EngineFactory<EngineWrapper.TCipher, Cipher> CIPHER =
@@ -95,33 +184,27 @@
       Provider p = Security.getProvider(s);
       if (p != null) {
         providers.add(p);
-      } else {
-        logger.info(String.format("Provider %s not available", s));
       }
     }
     return providers;
   }
 
   public EngineFactory(T_WRAPPER instanceBuilder) {
-    this.instanceBuilder = instanceBuilder;
+    if (TinkFipsUtil.useOnlyFips()) {
+      policy = new FipsPolicy<>(instanceBuilder);
+    } else if (SubtleUtil.isAndroid()) {
+      policy = new AndroidPolicy<>(instanceBuilder);
+    } else {
+      policy = new DefaultPolicy<>(instanceBuilder);
+    }
   }
 
-  public T_ENGINE getInstance(String algorithm) throws GeneralSecurityException {
-    Exception cause = null;
-    for (Provider provider : policy) {
-      try {
-        return this.instanceBuilder.getInstance(algorithm, provider);
-      } catch (Exception e) {
-        if (cause == null) {
-          cause = e;
-        }
-      }
-    }
-    if (LET_FALLBACK) {
-      return this.instanceBuilder.getInstance(algorithm, null);
-    }
-    throw new GeneralSecurityException("No good Provider found.", cause);
+  public JcePrimitiveT getInstance(String algorithm) throws GeneralSecurityException {
+    return policy.getInstance(algorithm);
   }
 
-  private final T_WRAPPER instanceBuilder;
+  JcePrimitiveT getInstance(String algorithm, List<Provider> preferredProviders)
+      throws GeneralSecurityException {
+    return policy.getInstance(algorithm, preferredProviders);
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Field25519.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Field25519.java
deleted file mode 100644
index e49dcd6..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Field25519.java
+++ /dev/null
@@ -1,614 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import com.google.crypto.tink.annotations.Alpha;
-import java.util.Arrays;
-
-/**
- * Defines field 25519 function based on <a
- * href="https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c">curve25519-donna C
- * implementation</a> (mostly identical).
- *
- * <p>Field elements are written as an array of signed, 64-bit limbs (an array of longs), least
- * significant first. The value of the field element is:
- *
- * <pre>
- * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
- * 2^204·x[8] + 2^230·x[9],
- * </pre>
- *
- * <p>i.e. the limbs are 26, 25, 26, 25, ... bits wide.
- */
-@Alpha
-final class Field25519 {
-  /**
-   * During Field25519 computation, the mixed radix representation may be in different forms:
-   * <ul>
-   *  <li> Reduced-size form: the array has size at most 10.
-   *  <li> Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most
-   *  19.
-   * </ul>
-   *
-   * TODO(quannguyen):
-   * <ul>
-   *  <li> Clarify ill-defined terminologies.
-   *  <li> The reduction procedure is different from DJB's paper
-   *  (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and
-   *  reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to
-   *  see what's going on.
-   *  <li> Consider using method mult() everywhere and making product() private.
-   * </ul>
-   */
-
-  static final int FIELD_LEN = 32;
-  static final int LIMB_CNT = 10;
-  private static final long TWO_TO_25 = 1 << 25;
-  private static final long TWO_TO_26 = TWO_TO_25 << 1;
-
-  private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28};
-  private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6};
-  private static final int[] MASK = {0x3ffffff, 0x1ffffff};
-  private static final int[] SHIFT = {26, 25};
-
-  /**
-   * Sums two numbers: output = in1 + in2
-   *
-   * On entry: in1, in2 are in reduced-size form.
-   */
-  static void sum(long[] output, long[] in1, long[] in2) {
-    for (int i = 0; i < LIMB_CNT; i++) {
-      output[i] = in1[i] + in2[i];
-    }
-  }
-
-  /**
-   * Sums two numbers: output += in
-   *
-   * On entry: in is in reduced-size form.
-   */
-  static void sum(long[] output, long[] in) {
-    sum(output, output, in);
-  }
-
-  /**
-   * Find the difference of two numbers: output = in1 - in2
-   * (note the order of the arguments!).
-   *
-   * On entry: in1, in2 are in reduced-size form.
-   */
-  static void sub(long[] output, long[] in1, long[] in2) {
-    for (int i = 0; i < LIMB_CNT; i++) {
-      output[i] = in1[i] - in2[i];
-    }
-  }
-
-  /**
-   * Find the difference of two numbers: output = in - output
-   * (note the order of the arguments!).
-   *
-   * On entry: in, output are in reduced-size form.
-   */
-  static void sub(long[] output, long[] in) {
-    sub(output, in, output);
-  }
-
-  /**
-   * Multiply a number by a scalar: output = in * scalar
-   */
-  static void scalarProduct(long[] output, long[] in, long scalar) {
-    for (int i = 0; i < LIMB_CNT; i++) {
-      output[i] = in[i] * scalar;
-    }
-  }
-
-  /**
-   * Multiply two numbers: out = in2 * in
-   *
-   * output must be distinct to both inputs. The inputs are reduced coefficient form,
-   * the output is not.
-   *
-   * out[x] <= 14 * the largest product of the input limbs.
-   */
-  static void product(long[] out, long[] in2, long[] in) {
-    out[0] = in2[0] * in[0];
-    out[1] = in2[0] * in[1]
-        + in2[1] * in[0];
-    out[2] = 2 * in2[1] * in[1]
-        + in2[0] * in[2]
-        + in2[2] * in[0];
-    out[3] = in2[1] * in[2]
-        + in2[2] * in[1]
-        + in2[0] * in[3]
-        + in2[3] * in[0];
-    out[4] = in2[2] * in[2]
-        + 2 * (in2[1] * in[3] + in2[3] * in[1])
-        + in2[0] * in[4]
-        + in2[4] * in[0];
-    out[5] = in2[2] * in[3]
-        + in2[3] * in[2]
-        + in2[1] * in[4]
-        + in2[4] * in[1]
-        + in2[0] * in[5]
-        + in2[5] * in[0];
-    out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1])
-        + in2[2] * in[4]
-        + in2[4] * in[2]
-        + in2[0] * in[6]
-        + in2[6] * in[0];
-    out[7] = in2[3] * in[4]
-        + in2[4] * in[3]
-        + in2[2] * in[5]
-        + in2[5] * in[2]
-        + in2[1] * in[6]
-        + in2[6] * in[1]
-        + in2[0] * in[7]
-        + in2[7] * in[0];
-    out[8] = in2[4] * in[4]
-        + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1])
-        + in2[2] * in[6]
-        + in2[6] * in[2]
-        + in2[0] * in[8]
-        + in2[8] * in[0];
-    out[9] = in2[4] * in[5]
-        + in2[5] * in[4]
-        + in2[3] * in[6]
-        + in2[6] * in[3]
-        + in2[2] * in[7]
-        + in2[7] * in[2]
-        + in2[1] * in[8]
-        + in2[8] * in[1]
-        + in2[0] * in[9]
-        + in2[9] * in[0];
-    out[10] =
-        2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1])
-            + in2[4] * in[6]
-            + in2[6] * in[4]
-            + in2[2] * in[8]
-            + in2[8] * in[2];
-    out[11] = in2[5] * in[6]
-        + in2[6] * in[5]
-        + in2[4] * in[7]
-        + in2[7] * in[4]
-        + in2[3] * in[8]
-        + in2[8] * in[3]
-        + in2[2] * in[9]
-        + in2[9] * in[2];
-    out[12] = in2[6] * in[6]
-        + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3])
-        + in2[4] * in[8]
-        + in2[8] * in[4];
-    out[13] = in2[6] * in[7]
-        + in2[7] * in[6]
-        + in2[5] * in[8]
-        + in2[8] * in[5]
-        + in2[4] * in[9]
-        + in2[9] * in[4];
-    out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5])
-        + in2[6] * in[8]
-        + in2[8] * in[6];
-    out[15] = in2[7] * in[8]
-        + in2[8] * in[7]
-        + in2[6] * in[9]
-        + in2[9] * in[6];
-    out[16] = in2[8] * in[8]
-        + 2 * (in2[7] * in[9] + in2[9] * in[7]);
-    out[17] = in2[8] * in[9]
-        + in2[9] * in[8];
-    out[18] = 2 * in2[9] * in[9];
-  }
-
-  /**
-   * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients.
-   *
-   * @param input An input array of any length. If the array has 19 elements, it will be used as
-   * temporary buffer and its contents changed.
-   * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold.
-   *
-   */
-  static void reduce(long[] input, long[] output) {
-    long[] tmp;
-    if (input.length == 19) {
-      tmp = input;
-    } else {
-      tmp = new long[19];
-      System.arraycopy(input, 0, tmp, 0, input.length);
-    }
-    reduceSizeByModularReduction(tmp);
-    reduceCoefficients(tmp);
-    System.arraycopy(tmp, 0, output, 0, LIMB_CNT);
-  }
-
-  /**
-   * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19.
-   *
-   * On entry: |output[i]| < 14*2^54
-   * On exit: |output[0..8]| < 280*2^54
-   */
-  static void reduceSizeByModularReduction(long[] output) {
-    // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19.
-    // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8].
-    //
-    // Each of these shifts and adds ends up multiplying the value by 19.
-    //
-    // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus,
-    // on exit, |output[0..8]| < 280*2^54.
-    output[8] += output[18] << 4;
-    output[8] += output[18] << 1;
-    output[8] += output[18];
-    output[7] += output[17] << 4;
-    output[7] += output[17] << 1;
-    output[7] += output[17];
-    output[6] += output[16] << 4;
-    output[6] += output[16] << 1;
-    output[6] += output[16];
-    output[5] += output[15] << 4;
-    output[5] += output[15] << 1;
-    output[5] += output[15];
-    output[4] += output[14] << 4;
-    output[4] += output[14] << 1;
-    output[4] += output[14];
-    output[3] += output[13] << 4;
-    output[3] += output[13] << 1;
-    output[3] += output[13];
-    output[2] += output[12] << 4;
-    output[2] += output[12] << 1;
-    output[2] += output[12];
-    output[1] += output[11] << 4;
-    output[1] += output[11] << 1;
-    output[1] += output[11];
-    output[0] += output[10] << 4;
-    output[0] += output[10] << 1;
-    output[0] += output[10];
-  }
-
-  /**
-   * Reduce all coefficients of the short form input so that |x| < 2^26.
-   *
-   * On entry: |output[i]| < 280*2^54
-   */
-  static void reduceCoefficients(long[] output) {
-    output[10] = 0;
-
-    for (int i = 0; i < LIMB_CNT; i += 2) {
-      long over = output[i] / TWO_TO_26;
-      // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in
-      // the first iteration of this loop. This is added to the next limb and we can approximate the
-      // resulting bound of that limb by 281*2^54.
-      output[i] -= over << 26;
-      output[i + 1] += over;
-
-      // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is
-      // added to the next limb, the resulting bound can be approximated as 281*2^54.
-      //
-      // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no
-      // overflow occurs.
-      over = output[i + 1] / TWO_TO_25;
-      output[i + 1] -= over << 25;
-      output[i + 2] += over;
-    }
-    // Now |output[10]| < 281*2^29 and all other coefficients are reduced.
-    output[0] += output[10] << 4;
-    output[0] += output[10] << 1;
-    output[0] += output[10];
-
-    output[10] = 0;
-    // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more
-    // than 2^16.
-    long over = output[0] / TWO_TO_26;
-    output[0] -= over << 26;
-    output[1] += over;
-    // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on
-    // |output[1]| is sufficient to meet our needs.
-  }
-
-  /**
-   * A helpful wrapper around {@ref Field25519#product}: output = in * in2.
-   *
-   * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.
-   *
-   * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and
-   * |output[i]| < 2^26.
-   */
-  static void mult(long[] output, long[] in, long[] in2) {
-    long[] t = new long[19];
-    product(t, in, in2);
-    // |t[i]| < 2^26
-    reduce(t, output);
-  }
-
-  /**
-   * Square a number: out = in**2
-   *
-   * output must be distinct from the input. The inputs are reduced coefficient form, the output is
-   * not.
-   *
-   * out[x] <= 14 * the largest product of the input limbs.
-   */
-  private static void squareInner(long[] out, long[] in) {
-    out[0] = in[0] * in[0];
-    out[1] =  2 * in[0] * in[1];
-    out[2] =  2 * (in[1] * in[1] + in[0] * in[2]);
-    out[3] =  2 * (in[1] * in[2] + in[0] * in[3]);
-    out[4] = in[2] * in[2]
-        + 4 * in[1] * in[3]
-        + 2 * in[0] * in[4];
-    out[5] =  2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]);
-    out[6] =  2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 *  in[1] * in[5]);
-    out[7] =  2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]);
-    out[8] = in[4] * in[4]
-        + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5]));
-    out[9] =  2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]);
-    out[10] = 2 * (in[5] * in[5]
-        + in[4] * in[6]
-        + in[2] * in[8]
-        + 2 * (in[3] * in[7] + in[1] * in[9]));
-    out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]);
-    out[12] = in[6] * in[6]
-        + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9]));
-    out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]);
-    out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 *  in[5] * in[9]);
-    out[15] = 2 * (in[7] * in[8] + in[6] * in[9]);
-    out[16] = in[8] * in[8] + 4 * in[7] * in[9];
-    out[17] = 2 *  in[8] * in[9];
-    out[18] = 2 *  in[9] * in[9];
-  }
-
-  /**
-   * Returns in^2.
-   *
-   * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27.
-   *
-   * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide
-   * storage for 10 limbs) and |out[i]| < 2^26.
-   */
-  static void square(long[] output, long[] in) {
-    long[] t = new long[19];
-    squareInner(t, in);
-    // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner
-    // adds together, at most, 14 of those products.
-    reduce(t, output);
-  }
-
-  /**
-   * Takes a little-endian, 32-byte number and expands it into mixed radix form.
-   */
-  static long[] expand(byte[] input) {
-    long[] output = new long[LIMB_CNT];
-    for (int i = 0; i < LIMB_CNT; i++) {
-      output[i] = ((((long) (input[EXPAND_START[i]] & 0xff))
-          | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8
-          | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16
-          | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1];
-    }
-    return output;
-  }
-
-  /**
-   * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte
-   * array.
-   *
-   * On entry: |input_limbs[i]| < 2^26
-   */
-  @SuppressWarnings("NarrowingCompoundAssignment")
-  static byte[] contract(long[] inputLimbs) {
-    long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT);
-    for (int j = 0; j < 2; j++) {
-      for (int i = 0; i < 9; i++) {
-        // This calculation is a time-invariant way to make input[i] non-negative by borrowing
-        // from the next-larger limb.
-        int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]);
-        input[i] = input[i] + (carry << SHIFT[i & 1]);
-        input[i + 1] -= carry;
-      }
-
-      // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow
-      // from input[0], which is valid mod 2^255-19.
-      {
-        int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25);
-        input[9] += (carry << 25);
-        input[0] -= (carry * 19);
-      }
-
-      // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits,
-      // depending on position. However, input[0] may be negative.
-    }
-
-    // The first borrow-propagation pass above ended with every limb except (possibly) input[0]
-    // non-negative.
-    //
-    // If input[0] was negative after the first pass, then it was because of a carry from input[9].
-    // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus
-    // input[0] >= -19.
-    //
-    // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation
-    // pass could only have wrapped around to decrease input[0] again if the first pass left
-    // input[0] negative *and* input[1] through input[9] were all zero.  In that case, input[1] is
-    // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative.
-    {
-      int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26);
-      input[0] += (carry << 26);
-      input[1] -= carry;
-    }
-
-    // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a
-    // limb which is, nominally, 25 bits wide.
-    for (int j = 0; j < 2; j++) {
-      for (int i = 0; i < 9; i++) {
-        int carry = (int) (input[i] >> SHIFT[i & 1]);
-        input[i] &= MASK[i & 1];
-        input[i + 1] += carry;
-      }
-    }
-
-    {
-      int carry = (int) (input[9] >> 25);
-      input[9] &= 0x1ffffff;
-      input[0] += 19 * carry;
-    }
-
-    // If the first carry-chain pass, just above, ended up with a carry from input[9], and that
-    // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was,
-    // at most, two.
-    //
-    // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] ->
-    // input[0] carry didn't push input[0] out of bounds.
-
-    // It still remains the case that input might be between 2^255-19 and 2^255. In this case,
-    // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff,
-    // which is 0x3ffffed.
-    int mask = gte((int) input[0], 0x3ffffed);
-    for (int i = 1; i < LIMB_CNT; i++) {
-      mask &= eq((int) input[i], MASK[i & 1]);
-    }
-
-    // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally
-    // subtracts 2^255-19.
-    input[0] -= mask & 0x3ffffed;
-    input[1] -= mask & 0x1ffffff;
-    for (int i = 2; i < LIMB_CNT; i += 2) {
-      input[i] -= mask & 0x3ffffff;
-      input[i + 1] -= mask & 0x1ffffff;
-    }
-
-    for (int i = 0; i < LIMB_CNT; i++) {
-      input[i] <<= EXPAND_SHIFT[i];
-    }
-    byte[] output = new byte[FIELD_LEN];
-    for (int i = 0; i < LIMB_CNT; i++) {
-      output[EXPAND_START[i]] |= input[i] & 0xff;
-      output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff;
-      output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff;
-      output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff;
-    }
-    return output;
-  }
-
-  /**
-   * Computes inverse of z = z(2^255 - 21)
-   *
-   * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the
-   * comment format and the variable namings are different from those.
-   */
-  static void inverse(long[] out, long[] z) {
-    long[] z2 = new long[Field25519.LIMB_CNT];
-    long[] z9 = new long[Field25519.LIMB_CNT];
-    long[] z11 = new long[Field25519.LIMB_CNT];
-    long[] z2To5Minus1 = new long[Field25519.LIMB_CNT];
-    long[] z2To10Minus1 = new long[Field25519.LIMB_CNT];
-    long[] z2To20Minus1 = new long[Field25519.LIMB_CNT];
-    long[] z2To50Minus1 = new long[Field25519.LIMB_CNT];
-    long[] z2To100Minus1 = new long[Field25519.LIMB_CNT];
-    long[] t0 = new long[Field25519.LIMB_CNT];
-    long[] t1 = new long[Field25519.LIMB_CNT];
-
-    square(z2, z);                          // 2
-    square(t1, z2);                         // 4
-    square(t0, t1);                         // 8
-    mult(z9, t0, z);                        // 9
-    mult(z11, z9, z2);                      // 11
-    square(t0, z11);                        // 22
-    mult(z2To5Minus1, t0, z9);              // 2^5 - 2^0 = 31
-
-    square(t0, z2To5Minus1);                // 2^6 - 2^1
-    square(t1, t0);                         // 2^7 - 2^2
-    square(t0, t1);                         // 2^8 - 2^3
-    square(t1, t0);                         // 2^9 - 2^4
-    square(t0, t1);                         // 2^10 - 2^5
-    mult(z2To10Minus1, t0, z2To5Minus1);    // 2^10 - 2^0
-
-    square(t0, z2To10Minus1);               // 2^11 - 2^1
-    square(t1, t0);                         // 2^12 - 2^2
-    for (int i = 2; i < 10; i += 2) {       // 2^20 - 2^10
-      square(t0, t1);
-      square(t1, t0);
-    }
-    mult(z2To20Minus1, t1, z2To10Minus1);   // 2^20 - 2^0
-
-    square(t0, z2To20Minus1);               // 2^21 - 2^1
-    square(t1, t0);                         // 2^22 - 2^2
-    for (int i = 2; i < 20; i += 2) {       // 2^40 - 2^20
-      square(t0, t1);
-      square(t1, t0);
-    }
-    mult(t0, t1, z2To20Minus1);             // 2^40 - 2^0
-
-    square(t1, t0);                         // 2^41 - 2^1
-    square(t0, t1);                         // 2^42 - 2^2
-    for (int i = 2; i < 10; i += 2) {       // 2^50 - 2^10
-      square(t1, t0);
-      square(t0, t1);
-    }
-    mult(z2To50Minus1, t0, z2To10Minus1);   // 2^50 - 2^0
-
-    square(t0, z2To50Minus1);               // 2^51 - 2^1
-    square(t1, t0);                         // 2^52 - 2^2
-    for (int i = 2; i < 50; i += 2) {       // 2^100 - 2^50
-      square(t0, t1);
-      square(t1, t0);
-    }
-    mult(z2To100Minus1, t1, z2To50Minus1);  // 2^100 - 2^0
-
-    square(t1, z2To100Minus1);              // 2^101 - 2^1
-    square(t0, t1);                         // 2^102 - 2^2
-    for (int i = 2; i < 100; i += 2) {      // 2^200 - 2^100
-      square(t1, t0);
-      square(t0, t1);
-    }
-    mult(t1, t0, z2To100Minus1);            // 2^200 - 2^0
-
-    square(t0, t1);                         // 2^201 - 2^1
-    square(t1, t0);                         // 2^202 - 2^2
-    for (int i = 2; i < 50; i += 2) {       // 2^250 - 2^50
-      square(t0, t1);
-      square(t1, t0);
-    }
-    mult(t0, t1, z2To50Minus1);             // 2^250 - 2^0
-
-    square(t1, t0);                         // 2^251 - 2^1
-    square(t0, t1);                         // 2^252 - 2^2
-    square(t1, t0);                         // 2^253 - 2^3
-    square(t0, t1);                         // 2^254 - 2^4
-    square(t1, t0);                         // 2^255 - 2^5
-    mult(out, t1, z11);                     // 2^255 - 21
-  }
-
-
-  /**
-   * Returns 0xffffffff iff a == b and zero otherwise.
-   */
-  private static int eq(int a, int b) {
-    a = ~(a ^ b);
-    a &= a << 16;
-    a &= a << 8;
-    a &= a << 4;
-    a &= a << 2;
-    a &= a << 1;
-    return a >> 31;
-  }
-
-  /**
-   * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative.
-   */
-  private static int gte(int a, int b) {
-    a -= b;
-    // a >= 0 iff a >= b.
-    return ~(a >> 31);
-  }
-
-  private Field25519() {}
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Kwp.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Kwp.java
index b2c0c03..ae0ffe8 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Kwp.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/Kwp.java
@@ -25,26 +25,29 @@
 import javax.crypto.spec.SecretKeySpec;
 
 /**
- * Implements the key wrapping primitive KWP defined in NIST SP 800 38f.
- * The same encryption mode is also defined in RFC 5649. The NIST document is used here
- * as a primary reference, since it contains a security analysis and further
- * recommendations. In particular, Section 8 of NIST SP 800 38f suggests that the
- * allowed key sizes may be restricted. The implementation in this class
+ * Implements the key wrapping primitive KWP defined in NIST SP 800 38f. The same encryption mode is
+ * also defined in RFC 5649. The NIST document is used here as a primary reference, since it
+ * contains a security analysis and further recommendations. In particular, Section 8 of NIST SP 800
+ * 38f suggests that the allowed key sizes may be restricted. The implementation in this class
  * requires that the key sizes are in the range MIN_WRAP_KEY_SIZE and MAX_WRAP_KEY_SIZE.
  *
- * <p>The minimum of 16 bytes has been chosen, because 128 bit keys are the smallest
- * key sizes used in tink. Additionally, wrapping short keys with KWP does not use
- * the function W and hence prevents using security arguments based on the assumption
- * that W is strong pseudorandom. (I.e. one consequence of using a strong pseudorandom
- * permutation as an underlying function is that leaking partial information about
- * decrypted bytes is not useful for an attack.)
+ * <p>The minimum of 16 bytes has been chosen, because 128 bit keys are the smallest key sizes used
+ * in tink. Additionally, wrapping short keys with KWP does not use the function W and hence
+ * prevents using security arguments based on the assumption that W is strong pseudorandom. (I.e.
+ * one consequence of using a strong pseudorandom permutation as an underlying function is that
+ * leaking partial information about decrypted bytes is not useful for an attack.)
  *
- * <p>The upper bound for the key size is somewhat arbitrary. Setting an upper bound is
- * motivated by the analysis in section A.4 of NIST SP 800 38f: forgeries of long
- * messages is simpler than forgeries of short message.
+ * <p>The upper bound for the key size is somewhat arbitrary. Setting an upper bound is motivated by
+ * the analysis in section A.4 of NIST SP 800 38f: forgeries of long messages is simpler than
+ * forgeries of short message.
  *
+ * @deprecated Tink does not support KeyWrap anymore. This implementation was fallback code for old
+ *     providers that did not implement KWP. It implements the same functionality as {@code
+ *     Cipher.getInstance("AESWRAPPAD");}. Some provider use a different algorithm name: {@code
+ *     Cipher.getInstance("AES/KWP/NoPadding");}.
  * @since 1.?.?
  */
+@Deprecated
 public class Kwp implements KeyWrap {
   private final SecretKey aesKey;
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/Poly1305.java b/java_src/src/main/java/com/google/crypto/tink/subtle/Poly1305.java
deleted file mode 100644
index c1c2496..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/Poly1305.java
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/**
- * Poly1305 one-time MAC based on RFC 7539.
- *
- * <p>This is not an implementation of the MAC interface on purpose and it is not equivalent to
- * HMAC.
- *
- * <p>The implementation is based on poly1305 implementation by Andrew Moon
- * (https://github.com/floodyberry/poly1305-donna) and released as public domain.
- *
- * @deprecated replaced by {@link com.google.crypto.tink.aead.internal.Poly1305}.
- */
-@Deprecated
-class Poly1305 {
-
-  public static final int MAC_TAG_SIZE_IN_BYTES = 16;
-  public static final int MAC_KEY_SIZE_IN_BYTES = 32;
-
-  private Poly1305() {}
-
-  private static long load32(byte[] in, int idx) {
-    return ((in[idx] & 0xff)
-            | ((in[idx + 1] & 0xff) << 8)
-            | ((in[idx + 2] & 0xff) << 16)
-            | ((in[idx + 3] & 0xff) << 24))
-        & 0xffffffffL;
-  }
-
-  private static long load26(byte[] in, int idx, int shift) {
-    return (load32(in, idx) >> shift) & 0x3ffffff;
-  }
-
-  private static void toByteArray(byte[] output, long num, int idx) {
-    for (int i = 0; i < 4; i++, num >>= 8) {
-      output[idx + i] = (byte) (num & 0xff);
-    }
-  }
-
-  private static void copyBlockSize(byte[] output, byte[] in, int idx) {
-    int copyCount = Math.min(MAC_TAG_SIZE_IN_BYTES, in.length - idx);
-    System.arraycopy(in, idx, output, 0, copyCount);
-    output[copyCount] = 1;
-    if (copyCount != MAC_TAG_SIZE_IN_BYTES) {
-      Arrays.fill(output, copyCount + 1, output.length, (byte) 0);
-    }
-  }
-
-  static byte[] computeMac(final byte[] key, byte[] data) {
-    if (key.length != MAC_KEY_SIZE_IN_BYTES) {
-      throw new IllegalArgumentException("The key length in bytes must be 32.");
-    }
-    long h0 = 0;
-    long h1 = 0;
-    long h2 = 0;
-    long h3 = 0;
-    long h4 = 0;
-    long d0;
-    long d1;
-    long d2;
-    long d3;
-    long d4;
-    long c;
-
-    // r &= 0xffffffc0ffffffc0ffffffc0fffffff
-    long r0 = load26(key, 0, 0) & 0x3ffffff;
-    long r1 = load26(key, 3, 2) & 0x3ffff03;
-    long r2 = load26(key, 6, 4) & 0x3ffc0ff;
-    long r3 = load26(key, 9, 6) & 0x3f03fff;
-    long r4 = load26(key, 12, 8) & 0x00fffff;
-
-    long s1 = r1 * 5;
-    long s2 = r2 * 5;
-    long s3 = r3 * 5;
-    long s4 = r4 * 5;
-
-    byte[] buf = new byte[MAC_TAG_SIZE_IN_BYTES + 1];
-    for (int i = 0; i < data.length; i += MAC_TAG_SIZE_IN_BYTES) {
-      copyBlockSize(buf, data, i);
-      h0 += load26(buf, 0, 0);
-      h1 += load26(buf, 3, 2);
-      h2 += load26(buf, 6, 4);
-      h3 += load26(buf, 9, 6);
-      h4 += load26(buf, 12, 8) | (buf[MAC_TAG_SIZE_IN_BYTES] << 24);
-
-      // d = r * h
-      d0 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1;
-      d1 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2;
-      d2 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3;
-      d3 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4;
-      d4 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
-
-      // Partial reduction mod 2^130-5, resulting h1 might not be 26bits.
-      c = d0 >> 26;
-      h0 = d0 & 0x3ffffff;
-      d1 += c;
-      c = d1 >> 26;
-      h1 = d1 & 0x3ffffff;
-      d2 += c;
-      c = d2 >> 26;
-      h2 = d2 & 0x3ffffff;
-      d3 += c;
-      c = d3 >> 26;
-      h3 = d3 & 0x3ffffff;
-      d4 += c;
-      c = d4 >> 26;
-      h4 = d4 & 0x3ffffff;
-      h0 += c * 5;
-      c = h0 >> 26;
-      h0 = h0 & 0x3ffffff;
-      h1 += c;
-    }
-    // Do final reduction mod 2^130-5
-    c = h1 >> 26;
-    h1 = h1 & 0x3ffffff;
-    h2 += c;
-    c = h2 >> 26;
-    h2 = h2 & 0x3ffffff;
-    h3 += c;
-    c = h3 >> 26;
-    h3 = h3 & 0x3ffffff;
-    h4 += c;
-    c = h4 >> 26;
-    h4 = h4 & 0x3ffffff;
-    h0 += c * 5; // c * 5 can be at most 5
-    c = h0 >> 26;
-    h0 = h0 & 0x3ffffff;
-    h1 += c;
-
-    // Compute h - p
-    long g0 = h0 + 5;
-    c = g0 >> 26;
-    g0 &= 0x3ffffff;
-    long g1 = h1 + c;
-    c = g1 >> 26;
-    g1 &= 0x3ffffff;
-    long g2 = h2 + c;
-    c = g2 >> 26;
-    g2 &= 0x3ffffff;
-    long g3 = h3 + c;
-    c = g3 >> 26;
-    g3 &= 0x3ffffff;
-    long g4 = h4 + c - (1 << 26);
-
-    // Select h if h < p, or h - p if h >= p
-    long mask = g4 >> 63; // mask is either 0 (h >= p) or -1 (h < p)
-    h0 &= mask;
-    h1 &= mask;
-    h2 &= mask;
-    h3 &= mask;
-    h4 &= mask;
-    mask = ~mask;
-    h0 |= g0 & mask;
-    h1 |= g1 & mask;
-    h2 |= g2 & mask;
-    h3 |= g3 & mask;
-    h4 |= g4 & mask;
-
-    // h = h % (2^128)
-    h0 = (h0 | (h1 << 26)) & 0xffffffffL;
-    h1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffffL;
-    h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffffL;
-    h3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffffL;
-
-    // mac = (h + pad) % (2^128)
-    c = h0 + load32(key, 16);
-    h0 = c & 0xffffffffL;
-    c = h1 + load32(key, 20) + (c >> 32);
-    h1 = c & 0xffffffffL;
-    c = h2 + load32(key, 24) + (c >> 32);
-    h2 = c & 0xffffffffL;
-    c = h3 + load32(key, 28) + (c >> 32);
-    h3 = c & 0xffffffffL;
-
-    byte[] mac = new byte[MAC_TAG_SIZE_IN_BYTES];
-    toByteArray(mac, h0, 0);
-    toByteArray(mac, h1, 4);
-    toByteArray(mac, h2, 8);
-    toByteArray(mac, h3, 12);
-
-    return mac;
-  }
-
-  static void verifyMac(final byte[] key, byte[] data, byte[] mac) throws GeneralSecurityException {
-    if (!Bytes.equal(computeMac(key, data), mac)) {
-      throw new GeneralSecurityException("invalid MAC");
-    }
-  }
-}
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/PrfAesCmac.java b/java_src/src/main/java/com/google/crypto/tink/subtle/PrfAesCmac.java
index ae56edd..b92541a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/PrfAesCmac.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/PrfAesCmac.java
@@ -16,8 +16,11 @@
 
 package com.google.crypto.tink.subtle;
 
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
 import com.google.crypto.tink.mac.internal.AesUtil;
+import com.google.crypto.tink.prf.AesCmacPrfKey;
 import com.google.crypto.tink.prf.Prf;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
@@ -31,6 +34,7 @@
  * An implementation of CMAC following <a href="https://tools.ietf.org/html/rfc4493">RFC 4493</a>.
  */
 @Immutable
+@AccessesPartialKey
 public final class PrfAesCmac implements Prf {
   public static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS =
       TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_NOT_FIPS;
@@ -58,6 +62,10 @@
     generateSubKeys();
   }
 
+  public static Prf create(AesCmacPrfKey key) throws GeneralSecurityException {
+    return new PrfAesCmac(key.getKeyBytes().toByteArray(InsecureSecretKeyAccess.get()));
+  }
+
   // https://tools.ietf.org/html/rfc4493#section-2.4
   @Override
   public byte[] compute(final byte[] data, int outputLength) throws GeneralSecurityException {
@@ -100,8 +108,7 @@
     y = Bytes.xor(mLast, x);
 
     // Step 7
-    byte[] output = Arrays.copyOf(aes.doFinal(y), outputLength);
-    return output;
+    return Arrays.copyOf(aes.doFinal(y), outputLength);
   }
 
   // https://tools.ietf.org/html/rfc4493#section-2.3
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/PrfHmacJce.java b/java_src/src/main/java/com/google/crypto/tink/subtle/PrfHmacJce.java
index bc514c7..39cc292 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/PrfHmacJce.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/PrfHmacJce.java
@@ -16,7 +16,10 @@
 
 package com.google.crypto.tink.subtle;
 
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.prf.HmacPrfKey;
 import com.google.crypto.tink.prf.Prf;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
@@ -24,9 +27,11 @@
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
 
 /** {@link Prf} implementation using JCE. */
 @Immutable
+@AccessesPartialKey
 public final class PrfHmacJce implements Prf {
   public static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS =
       TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_REQUIRES_BORINGCRYPTO;
@@ -92,6 +97,13 @@
     localMac.get();
   }
 
+  /** Given an HmacPrfKey, returns an instance of the Prf interface. */
+  public static Prf create(HmacPrfKey key) throws GeneralSecurityException {
+    return new PrfHmacJce(
+        "HMAC" + key.getParameters().getHashType(),
+        new SecretKeySpec(key.getKeyBytes().toByteArray(InsecureSecretKeyAccess.get()), "HMAC"));
+  }
+
   @Override
   public byte[] compute(byte[] data, int outputLength) throws GeneralSecurityException {
     if (outputLength > maxOutputLength) {
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/PrfMac.java b/java_src/src/main/java/com/google/crypto/tink/subtle/PrfMac.java
index 24a4ff3..2b778f1 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/PrfMac.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/PrfMac.java
@@ -16,26 +16,43 @@
 
 package com.google.crypto.tink.subtle;
 
+import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
 import com.google.crypto.tink.prf.Prf;
 import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
 import java.security.InvalidAlgorithmParameterException;
+import java.util.Arrays;
 
 /**
  * Class that provides the functionality expressed by the Mac primitive using a Prf implementation.
  */
 @Immutable
+@AccessesPartialKey
 public class PrfMac implements Mac {
+  // A single byte to be added to the plaintext for the legacy key type.
+  private static final byte[] FORMAT_VERSION = new byte[] {0};
   static final int MIN_TAG_SIZE_IN_BYTES = 10;
 
   private final Prf wrappedPrf;
   private final int tagSize;
 
+  @SuppressWarnings("Immutable")
+  private final byte[] outputPrefix;
+  // A field that regulates whether we add a zero-byte to the plaintext or not (because of
+  // the LEGACY variant).
+  @SuppressWarnings("Immutable")
+  private final byte[] plaintextLegacySuffix;
+
   /** Wrap {@code wrappedPrf } in a Mac primitive with the specified {@code tagSize} */
   public PrfMac(Prf wrappedPrf, int tagSize) throws GeneralSecurityException {
     this.wrappedPrf = wrappedPrf;
     this.tagSize = tagSize;
+    this.outputPrefix = new byte[0];
+    this.plaintextLegacySuffix = new byte[0];
 
     // The output length is restricted by the HMAC spec. Check that first.
     if (tagSize < MIN_TAG_SIZE_IN_BYTES) {
@@ -45,12 +62,33 @@
 
     // Some Prf implementations have restrictions on maximum tag length. These throw on compute().
     // Check for those restrictions on tag length here by doing a compute() pass.
-    wrappedPrf.compute(new byte[0], tagSize);
+    Object unused = wrappedPrf.compute(new byte[0], tagSize);
+  }
+
+  private PrfMac(AesCmacKey key) throws GeneralSecurityException {
+    wrappedPrf = new PrfAesCmac(key.getAesKey().toByteArray(InsecureSecretKeyAccess.get()));
+    // Due to the correctness checks during AesCmacKey creation, there is no need to perform
+    // additional tag size checks here.
+    tagSize = key.getParameters().getCryptographicTagSizeBytes();
+    outputPrefix = key.getOutputPrefix().toByteArray();
+    if (key.getParameters().getVariant().equals(Variant.LEGACY)) {
+      plaintextLegacySuffix = Arrays.copyOf(FORMAT_VERSION, FORMAT_VERSION.length);
+    } else {
+      plaintextLegacySuffix = new byte[0];
+    }
+  }
+
+  public static Mac create(AesCmacKey key) throws GeneralSecurityException {
+    return new PrfMac(key);
   }
 
   @Override
   public byte[] computeMac(byte[] data) throws GeneralSecurityException {
-    return wrappedPrf.compute(data, tagSize);
+    if (plaintextLegacySuffix.length > 0) {
+      return Bytes.concat(
+          outputPrefix, wrappedPrf.compute(Bytes.concat(data, plaintextLegacySuffix), tagSize));
+    }
+    return Bytes.concat(outputPrefix, wrappedPrf.compute(data, tagSize));
   }
 
   @Override
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/StreamingAeadSeekableDecryptingChannel.java b/java_src/src/main/java/com/google/crypto/tink/subtle/StreamingAeadSeekableDecryptingChannel.java
index 62161c1..d37ec02 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/StreamingAeadSeekableDecryptingChannel.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/StreamingAeadSeekableDecryptingChannel.java
@@ -16,7 +16,7 @@
 
 package com.google.crypto.tink.subtle;
 
-import androidx.annotation.RequiresApi;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.ClosedChannelException;
@@ -29,7 +29,6 @@
  * An instance of {@link SeekableByteChannel} that allows random access to the plaintext of some
  * ciphertext.
  */
-@RequiresApi(24) // https://developer.android.com/reference/java/nio/channels/SeekableByteChannel
 class StreamingAeadSeekableDecryptingChannel implements SeekableByteChannel {
   // Each plaintext segment has 16 bytes more of memory than the actual plaintext that it contains.
   // This is a workaround for an incompatibility between Conscrypt and OpenJDK in their
@@ -106,7 +105,7 @@
   /**
    * A description of the state of this StreamingAeadSeekableDecryptingChannel.
    * While this description does not contain plaintext or key material
-   * it contains length information that might be confidential.
+   * it contains length information that might leak some information.
    */
   @Override
   public synchronized String toString() {
@@ -150,10 +149,10 @@
   }
 
   /**
-   * Sets the position in the plaintext.
-   * Setting the position to a value greater than the plaintext size is legal.
-   * A later attempt to read byte will throw an IOException.
+   * Sets the position in the plaintext. Setting the position to a value greater than the plaintext
+   * size is legal. A later attempt to read byte will throw an IOException.
    */
+  @CanIgnoreReturnValue
   @Override
   public synchronized SeekableByteChannel position(long newPosition) {
     plaintextPosition = newPosition;
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java b/java_src/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java
index ca7f9fe..298c087 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java
@@ -16,13 +16,13 @@
 
 package com.google.crypto.tink.subtle;
 
+import com.google.crypto.tink.internal.BigIntegerEncoding;
 import com.google.crypto.tink.internal.Util;
 import com.google.crypto.tink.subtle.Enums.HashType;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import java.security.MessageDigest;
-import java.util.Arrays;
 import javax.annotation.Nullable;
 
 /** Helper methods. */
@@ -33,7 +33,7 @@
    *
    * @param hash the hash type
    * @return the JCE's Ecdsa algorithm name for the hash.
-   * @throw GeneralSecurityExceptio if {@code hash} is not supported or is not safe for digital
+   * @throws GeneralSecurityException if {@code hash} is not supported or is not safe for digital
    *     signature.
    */
   public static String toEcdsaAlgo(HashType hash) throws GeneralSecurityException {
@@ -47,7 +47,7 @@
    *
    * @param hash the hash type.
    * @return the JCE's RSA SSA PKCS1 algorithm name for the hash.
-   * @throw GeneralSecurityException if {@code hash} is not supported or is not safe for digital
+   * @throws GeneralSecurityException if {@code hash} is not supported or is not safe for digital
    *     signature.
    */
   public static String toRsaSsaPkcs1Algo(HashType hash) throws GeneralSecurityException {
@@ -60,7 +60,7 @@
    *
    * @param hash the hash type.
    * @return theh JCE's hash algorithm name.
-   * @throw GeneralSecurityException if {@code hash} is not supported.
+   * @throws GeneralSecurityException if {@code hash} is not supported.
    */
   public static String toDigestAlgo(HashType hash) throws GeneralSecurityException {
     switch (hash) {
@@ -104,14 +104,14 @@
   }
 
   /**
-   * Converts an byte array to a nonnegative integer
+   * Converts an unsigned, big-endian encoded byte array to a non-negative integer
    * (https://tools.ietf.org/html/rfc8017#section-4.1).
    *
    * @param bs the byte array to be converted to integer.
    * @return the corresponding integer.
    */
   public static BigInteger bytes2Integer(byte[] bs) {
-    return new BigInteger(1, bs);
+    return BigIntegerEncoding.fromUnsignedBigEndianBytes(bs);
   }
 
   /**
@@ -124,24 +124,7 @@
    */
   public static byte[] integer2Bytes(BigInteger num, int intendedLength)
       throws GeneralSecurityException {
-    byte[] b = num.toByteArray();
-    if (b.length == intendedLength) {
-      return b;
-    }
-    if (b.length > intendedLength + 1 /* potential leading zero */) {
-      throw new GeneralSecurityException("integer too large");
-    }
-    if (b.length == intendedLength + 1) {
-      if (b[0] == 0 /* leading zero */) {
-        return Arrays.copyOfRange(b, 1, b.length);
-      } else {
-        throw new GeneralSecurityException("integer too large");
-      }
-    }
-    // Left zero pad b.
-    byte[] res = new byte[intendedLength];
-    System.arraycopy(b, 0, res, intendedLength - b.length, b.length);
-    return res;
+    return BigIntegerEncoding.toBigEndianBytesOfFixedLength(num, intendedLength);
   }
 
   /** Computes MGF1 as defined at https://tools.ietf.org/html/rfc8017#appendix-B.2.1. */
diff --git a/java_src/src/main/java/com/google/crypto/tink/subtle/X25519.java b/java_src/src/main/java/com/google/crypto/tink/subtle/X25519.java
index 54a1755..40f6d8a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/subtle/X25519.java
+++ b/java_src/src/main/java/com/google/crypto/tink/subtle/X25519.java
@@ -17,6 +17,8 @@
 package com.google.crypto.tink.subtle;
 
 import com.google.crypto.tink.annotations.Alpha;
+import com.google.crypto.tink.internal.Curve25519;
+import com.google.crypto.tink.internal.Field25519;
 import java.security.InvalidKeyException;
 import java.util.Arrays;
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/testing/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/testing/BUILD.bazel
index e60b8d3..02922b8 100644
--- a/java_src/src/main/java/com/google/crypto/tink/testing/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/testing/BUILD.bazel
@@ -27,14 +27,16 @@
         "//proto:rsa_ssa_pss_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_key_templates",
+        "//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
         "//src/main/java/com/google/crypto/tink/mac:mac_config",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/prf:prf_config",
@@ -42,8 +44,9 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -70,14 +73,16 @@
         "//proto:rsa_ssa_pss_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
-        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
         "//src/main/java/com/google/crypto/tink:key_template-android",
         "//src/main/java/com/google/crypto/tink:primitive_set-android",
         "//src/main/java/com/google/crypto/tink:registry-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format-android",
         "//src/main/java/com/google/crypto/tink/aead:aead_config-android",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config-android",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_key_templates-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter-android",
         "//src/main/java/com/google/crypto/tink/mac:mac_config-android",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations-android",
         "//src/main/java/com/google/crypto/tink/prf:prf_config-android",
@@ -85,8 +90,9 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves-android",
         "//src/main/java/com/google/crypto/tink/subtle:hex-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -98,7 +104,9 @@
     deps = [
         ":test_util",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:junit_junit",
     ],
 )
@@ -109,7 +117,9 @@
     deps = [
         ":test_util-android",
         "//src/main/java/com/google/crypto/tink:streaming_aead-android",
+        "//src/main/java/com/google/crypto/tink/subtle:hex-android",
         "//src/main/java/com/google/crypto/tink/subtle:random-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:junit_junit",
     ],
 )
@@ -120,7 +130,8 @@
     deps = [
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
     ],
 )
@@ -131,7 +142,8 @@
     deps = [
         "//src/main/java/com/google/crypto/tink:key_template-android",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager-android",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_javalite",
         "@maven//:com_google_truth_truth",
     ],
 )
@@ -162,17 +174,14 @@
     name = "fake_kms_client",
     srcs = ["FakeKmsClient.java"],
     deps = [
-        "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
-        "//src/main/java/com/google/crypto/tink:binary_keyset_writer",
-        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:kms_client",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
-        "//src/main/java/com/google/crypto/tink/subtle:validators",
     ],
 )
 
@@ -198,17 +207,14 @@
     name = "fake_kms_client-android",
     srcs = ["FakeKmsClient.java"],
     deps = [
-        "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:aead-android",
-        "//src/main/java/com/google/crypto/tink:binary_keyset_reader-android",
-        "//src/main/java/com/google/crypto/tink:binary_keyset_writer-android",
-        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle-android",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access-android",
         "//src/main/java/com/google/crypto/tink:key_template-android",
         "//src/main/java/com/google/crypto/tink:kms_client-android",
         "//src/main/java/com/google/crypto/tink:registry_cluster-android",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format-android",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager-android",
         "//src/main/java/com/google/crypto/tink/subtle:base64-android",
-        "//src/main/java/com/google/crypto/tink/subtle:validators-android",
     ],
 )
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/testing/FakeKmsClient.java b/java_src/src/main/java/com/google/crypto/tink/testing/FakeKmsClient.java
index eb1f395..b54f0d4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/testing/FakeKmsClient.java
+++ b/java_src/src/main/java/com/google/crypto/tink/testing/FakeKmsClient.java
@@ -17,19 +17,15 @@
 package com.google.crypto.tink.testing;
 
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.BinaryKeysetWriter;
-import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KmsClient;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.aead.AesCtrHmacAeadKeyManager;
-import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.Validators;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
+import java.util.Locale;
 
 /** An implementation of a fake {@link KmsClient}. */
 public final class FakeKmsClient implements KmsClient {
@@ -72,6 +68,14 @@
     return this;
   }
 
+  private static String removePrefix(String expectedPrefix, String kmsKeyUri) {
+    if (!kmsKeyUri.toLowerCase(Locale.US).startsWith(expectedPrefix)) {
+      throw new IllegalArgumentException(
+          String.format("key URI must start with %s", expectedPrefix));
+    }
+    return kmsKeyUri.substring(expectedPrefix.length());
+  }
+
   @Override
   public Aead getAead(String uri) throws GeneralSecurityException {
     if (this.keyUri != null && !this.keyUri.equals(uri)) {
@@ -79,14 +83,11 @@
           String.format(
               "this client is bound to %s, cannot load keys bound to %s", this.keyUri, uri));
     }
-    String encodedKey = Validators.validateKmsKeyUriAndRemovePrefix(PREFIX, uri);
+    String encodedKey = removePrefix(PREFIX, uri);
     byte[] bytes = Base64.urlSafeDecode(encodedKey);
-    try {
-      KeysetHandle keysetHandle = CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(bytes));
-      return keysetHandle.getPrimitive(Aead.class);
-    } catch (IOException e) {
-      throw new GeneralSecurityException("Failed to create AEAD ", e);
-    }
+    KeysetHandle keysetHandle =
+        TinkProtoKeysetFormat.parseKeyset(bytes, InsecureSecretKeyAccess.get());
+    return keysetHandle.getPrimitive(Aead.class);
   }
 
   /** @return a new, random fake key_uri. */
@@ -94,16 +95,9 @@
     // The key_uri contains an encoded keyset with a new aes128CtrHmacSha256 key.
     KeyTemplate template = AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template();
     KeysetHandle keysetHandle = KeysetHandle.generateNew(template);
-    Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
-    ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
-    try {
-      BinaryKeysetWriter.withOutputStream(keysetStream).write(keyset);
-      keysetStream.close();
-    } catch (IOException e) {
-      throw new GeneralSecurityException("Failed to create key URI ", e);
-    }
-    String encodedKey = Base64.urlSafeEncode(keysetStream.toByteArray());
-    return PREFIX + encodedKey;
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+    return PREFIX + Base64.urlSafeEncode(serializedKeyset);
   }
 
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/testing/KeyTypeManagerTestUtil.java b/java_src/src/main/java/com/google/crypto/tink/testing/KeyTypeManagerTestUtil.java
index 4f5d47f..137c109 100644
--- a/java_src/src/main/java/com/google/crypto/tink/testing/KeyTypeManagerTestUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/testing/KeyTypeManagerTestUtil.java
@@ -20,6 +20,7 @@
 
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.MessageLite;
 
@@ -39,6 +40,7 @@
    * Checks that the given keyTemplate will be handed to the given KeyTypeManager (if registered),
    * that it validates, and returns a key if needed.
    */
+  @CanIgnoreReturnValue
   public static <KeyProtoT extends MessageLite> KeyProtoT testKeyTemplateCompatible(
       KeyTypeManager<KeyProtoT> manager, KeyTemplate template) throws Exception {
     assertThat(template.getTypeUrl()).isEqualTo(manager.getKeyType());
diff --git a/java_src/src/main/java/com/google/crypto/tink/testing/StreamingTestUtil.java b/java_src/src/main/java/com/google/crypto/tink/testing/StreamingTestUtil.java
index 3baf689..c606762 100644
--- a/java_src/src/main/java/com/google/crypto/tink/testing/StreamingTestUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/testing/StreamingTestUtil.java
@@ -22,7 +22,9 @@
 import static org.junit.Assert.fail;
 
 import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -77,6 +79,7 @@
       return buffer.position();
     }
 
+    @CanIgnoreReturnValue
     @Override
     public synchronized SeekableByteBufferChannel position(long newPosition)
         throws ClosedChannelException {
@@ -136,7 +139,7 @@
     private boolean isopen;
 
     public ByteBufferChannel(ByteBuffer buffer) {
-      this(buffer, java.lang.Integer.MAX_VALUE);
+      this(buffer, Integer.MAX_VALUE);
     }
 
     public ByteBufferChannel(ByteBuffer buffer, int maxChunkSize) {
@@ -215,10 +218,10 @@
    * read()-operation is repeated until the specified size of the channel.
    */
   public static class PseudorandomReadableByteChannel implements ReadableByteChannel {
-    private long size;
+    private final long size;
     private long position;
     private boolean open;
-    private byte[] repeatedBlock;
+    private final byte[] repeatedBlock;
     public static final int BLOCK_SIZE = 1024;
 
     /** Returns a plaintext of a given size. */
@@ -246,7 +249,7 @@
         return -1;
       }
       long start = position;
-      long end = java.lang.Math.min(size, start + dst.remaining());
+      long end = Math.min(size, start + dst.remaining());
       long firstBlock = start / BLOCK_SIZE;
       long lastBlock = end / BLOCK_SIZE;
       int startOffset = (int) (start % BLOCK_SIZE);
@@ -313,8 +316,8 @@
 
   public static byte[] concatBytes(byte[] first, byte[] last) {
     byte[] res = new byte[first.length + last.length];
-    java.lang.System.arraycopy(first, 0, res, 0, first.length);
-    java.lang.System.arraycopy(last, 0, res, first.length, last.length);
+    System.arraycopy(first, 0, res, 0, first.length);
+    System.arraycopy(last, 0, res, first.length, last.length);
     return res;
   }
 
@@ -367,11 +370,11 @@
 
     // Encrypt plaintext.
     ByteArrayOutputStream ciphertext = new ByteArrayOutputStream();
-    WritableByteChannel encChannel =
+    try (WritableByteChannel encChannel =
         encryptionStreamingAead.newEncryptingChannel(
-            Channels.newChannel(ciphertext), associatedData);
-    encChannel.write(ByteBuffer.wrap(plaintext));
-    encChannel.close();
+            Channels.newChannel(ciphertext), associatedData)) {
+      encChannel.write(ByteBuffer.wrap(plaintext));
+    }
 
     // Decrypt ciphertext via ReadableByteChannel.
     {
@@ -473,9 +476,10 @@
 
     // Encrypt with an OutputStream.
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
-    OutputStream encStream = encryptionStreamingAead.newEncryptingStream(bos, associatedData);
-    encStream.write(plaintext);
-    encStream.close();
+    try (OutputStream encStream =
+        encryptionStreamingAead.newEncryptingStream(bos, associatedData)) {
+      encStream.write(plaintext);
+    }
     byte[] ciphertext2 = bos.toByteArray();
 
     // Check that the stream encrypted ciphertext is correct.
@@ -509,11 +513,10 @@
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
     WritableByteChannel ctChannel = Channels.newChannel(bos);
     ctChannel.write(ByteBuffer.allocate(firstSegmentOffset));
-    WritableByteChannel encChannel = ags.newEncryptingChannel(ctChannel, associatedData);
-    encChannel.write(ByteBuffer.wrap(plaintext));
-    encChannel.close();
-    byte[] ciphertext = bos.toByteArray();
-    return ciphertext;
+    try (WritableByteChannel encChannel = ags.newEncryptingChannel(ctChannel, associatedData)) {
+      encChannel.write(ByteBuffer.wrap(plaintext));
+    }
+    return bos.toByteArray();
   }
 
   // Methods for testEncryptDecryptLong.
@@ -558,9 +561,9 @@
       throws Exception {
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
     bos.write(new byte[firstSegmentOffset]);
-    OutputStream encChannel = ags.newEncryptingStream(bos, associatedData);
-    encChannel.write(plaintext);
-    encChannel.close();
+    try (OutputStream encChannel = ags.newEncryptingStream(bos, associatedData)) {
+      encChannel.write(plaintext);
+    }
     byte[] ciphertext = bos.toByteArray();
     return ciphertext;
   }
@@ -572,7 +575,7 @@
   private static void testEncryptDecryptWithChannel(
       StreamingAead ags, int firstSegmentOffset, int plaintextSize, int chunkSize)
       throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] plaintext = generatePlaintext(plaintextSize);
     byte[] ciphertext = encryptWithChannel(ags, plaintext, associatedData, firstSegmentOffset);
 
@@ -617,7 +620,7 @@
   private static void testEncryptDecryptWithStream(
       StreamingAead ags, int firstSegmentOffset, int plaintextSize, int chunkSize)
       throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] plaintext = generatePlaintext(plaintextSize);
     byte[] ciphertext = encryptWithStream(ags, plaintext, associatedData, firstSegmentOffset);
 
@@ -659,7 +662,7 @@
   /** Encrypt and then decrypt partially, and check that the result is the same. */
   public static void testEncryptDecryptRandomAccess(
       StreamingAead ags, int firstSegmentOffset, int plaintextSize) throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] plaintext = generatePlaintext(plaintextSize);
     byte[] ciphertext = encryptWithChannel(ags, plaintext, associatedData, firstSegmentOffset);
 
@@ -676,9 +679,8 @@
         assertTrue(
             "start:" + start + " read:" + read + " length:" + length,
             pt.remaining() == 0 || start + pt.position() == plaintext.length);
-        String expected =
-            TestUtil.hexEncode(Arrays.copyOfRange(plaintext, start, start + pt.position()));
-        String actual = TestUtil.hexEncode(Arrays.copyOf(pt.array(), pt.position()));
+        String expected = Hex.encode(Arrays.copyOfRange(plaintext, start, start + pt.position()));
+        String actual = Hex.encode(Arrays.copyOf(pt.array(), pt.position()));
         assertEquals("start: " + start, expected, actual);
       }
     }
@@ -696,7 +698,7 @@
   public static void testSkipWithStream(
       StreamingAead ags, int firstSegmentOffset, int plaintextSize, int chunkSize)
       throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] plaintext = generatePlaintext(plaintextSize);
     byte[] ciphertext = encryptWithStream(ags, plaintext, associatedData, firstSegmentOffset);
 
@@ -767,31 +769,31 @@
 
   private static void testEncryptSingleBytesWithChannel(StreamingAead ags, int plaintextSize)
       throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] plaintext = generatePlaintext(plaintextSize);
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
     WritableByteChannel ctChannel = Channels.newChannel(bos);
     WritableByteChannel encChannel = ags.newEncryptingChannel(ctChannel, associatedData);
-    OutputStream encStream = Channels.newOutputStream(encChannel);
-    for (int i = 0; i < plaintext.length; i++) {
-      encStream.write(plaintext[i]);
+    try (OutputStream encStream = Channels.newOutputStream(encChannel)) {
+      for (int i = 0; i < plaintext.length; i++) {
+        encStream.write(plaintext[i]);
+      }
     }
-    encStream.close();
     isValidCiphertext(ags, plaintext, associatedData, bos.toByteArray());
   }
 
   private static void testEncryptSingleBytesWithStream(StreamingAead ags, int plaintextSize)
       throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] plaintext = generatePlaintext(plaintextSize);
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
     WritableByteChannel ctChannel = Channels.newChannel(bos);
     WritableByteChannel encChannel = ags.newEncryptingChannel(ctChannel, associatedData);
-    OutputStream encStream = Channels.newOutputStream(encChannel);
-    for (int i = 0; i < plaintext.length; i++) {
-      encStream.write(plaintext[i]);
+    try (OutputStream encStream = Channels.newOutputStream(encChannel)) {
+      for (int i = 0; i < plaintext.length; i++) {
+        encStream.write(plaintext[i]);
+      }
     }
-    encStream.close();
     isValidCiphertext(ags, plaintext, associatedData, bos.toByteArray());
   }
 
@@ -806,19 +808,19 @@
    * Encrypts and decrypts a with non-ASCII characters using CharsetEncoders and CharsetDecoders.
    */
   public static void testEncryptDecryptString(StreamingAead ags) throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     String stringWithNonAsciiChars = "αβγδ áéíóúý ∀∑∊∫≅⊕⊄";
     int repetitions = 1000;
 
     // Encrypts a sequence of strings.
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
     WritableByteChannel ctChannel = Channels.newChannel(bos);
-    Writer writer =
-        Channels.newWriter(ags.newEncryptingChannel(ctChannel, associatedData), "UTF-8");
-    for (int i = 0; i < repetitions; i++) {
-      writer.write(stringWithNonAsciiChars);
+    try (Writer writer =
+        Channels.newWriter(ags.newEncryptingChannel(ctChannel, associatedData), "UTF-8")) {
+      for (int i = 0; i < repetitions; i++) {
+        writer.write(stringWithNonAsciiChars);
+      }
     }
-    writer.close();
     byte[] ciphertext = bos.toByteArray();
 
     // Decrypts a sequence of strings.
@@ -895,7 +897,7 @@
 
   public static void testModifiedCiphertext(
       StreamingAead ags, int segmentSize, int firstSegmentOffset) throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int plaintextSize = 512;
     byte[] plaintext = generatePlaintext(plaintextSize);
     byte[] ciphertext = encryptWithChannel(ags, plaintext, associatedData, firstSegmentOffset);
@@ -996,9 +998,8 @@
               "start:" + start + " read:" + read + " length:" + length,
               start + read <= plaintext.length);
           // Check that the decrypted plaintext matches the original plaintext.
-          String expected =
-              TestUtil.hexEncode(Arrays.copyOfRange(plaintext, start, start + pt.position()));
-          String actual = TestUtil.hexEncode(Arrays.copyOf(pt.array(), pt.position()));
+          String expected = Hex.encode(Arrays.copyOfRange(plaintext, start, start + pt.position()));
+          String actual = Hex.encode(Arrays.copyOf(pt.array(), pt.position()));
           assertEquals("start: " + start, expected, actual);
         }
       }
@@ -1007,7 +1008,7 @@
 
   public static void testModifiedCiphertextWithSeekableByteChannel(
       StreamingAead ags, int segmentSize, int firstSegmentOffset) throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int plaintextSize = 2000;
     byte[] plaintext = generatePlaintext(plaintextSize);
     byte[] ciphertext = encryptWithChannel(ags, plaintext, associatedData, firstSegmentOffset);
@@ -1093,7 +1094,7 @@
   /** Encrypt and decrypt a long ciphertext. */
   public static void testEncryptDecryptLong(StreamingAead ags, long plaintextSize)
       throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     ReadableByteChannel plaintext = new PseudorandomReadableByteChannel(plaintextSize);
     ReadableByteChannel copy = new PseudorandomReadableByteChannel(plaintextSize);
     ReadableByteChannel ciphertext =
@@ -1119,7 +1120,7 @@
   /** Encrypt some plaintext to a file, then decrypt from the file */
   private static void testFileEncryptionWithChannel(
       StreamingAead ags, File tmpFile, int plaintextSize) throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     SeekableByteBufferChannel plaintext =
         new SeekableByteBufferChannel(generatePlaintext(plaintextSize));
 
@@ -1149,7 +1150,7 @@
       read = ptStream.read(decrypted);
       if (read > 0) {
         ByteBuffer expected = ByteBuffer.allocate(read);
-        plaintext.read(expected);
+        assertEquals(plaintext.read(expected), read);
         decrypted.flip();
         TestUtil.assertByteBufferContains(expected.array(), decrypted);
         decryptedSize += read;
@@ -1181,7 +1182,7 @@
       }
       byte[] expected = new byte[read];
       plaintext.position(start);
-      plaintext.read(ByteBuffer.wrap(expected));
+      assertEquals(plaintext.read(ByteBuffer.wrap(expected)), read);
       decrypted.flip();
       TestUtil.assertByteBufferContains(expected, decrypted);
     }
@@ -1193,7 +1194,7 @@
    */
   private static void testFileEncryptionWithStream(
       StreamingAead ags, File tmpFile, int plaintextSize) throws Exception {
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     byte[] pt = generatePlaintext(plaintextSize);
     FileOutputStream ctStream = new FileOutputStream(tmpFile);
     WritableByteChannel channel = Channels.newChannel(ctStream);
diff --git a/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java b/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java
index 4b4bbd9..7e0c247 100644
--- a/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java
@@ -22,13 +22,15 @@
 import static org.junit.Assert.assertTrue;
 
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
 import com.google.crypto.tink.hybrid.HybridKeyTemplates;
+import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
 import com.google.crypto.tink.mac.MacConfig;
 import com.google.crypto.tink.monitoring.MonitoringAnnotations;
 import com.google.crypto.tink.prf.PrfConfig;
@@ -74,9 +76,11 @@
 import com.google.crypto.tink.subtle.EllipticCurves;
 import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
+import com.google.errorprone.annotations.InlineMe;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.MessageLite;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
@@ -106,7 +110,7 @@
   }
 
   // This GCP KMS CryptoKey is restricted to the service account in {@code SERVICE_ACCOUNT_FILE}.
-  public static final String RESTRICTED_CRYPTO_KEY_URI =
+  public static final String GCP_KMS_TEST_KEY_URI =
       String.format(
           "gcp-kms://projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s",
           "tink-test-infrastructure", "global", "unit-and-integration-testing", "aead-key");
@@ -116,11 +120,11 @@
   public static final String SERVICE_ACCOUNT_FILE = "testdata/gcp/credential.json";
 
   // This AWS KMS CryptoKey is restricted to Google use only and {@code AWS_CREDS}.
-  public static final String AWS_CRYPTO_URI =
+  public static final String AWS_KMS_TEST_KEY_URI =
       "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f";
 
   // This is a credential for the AWS service account with granted access to
-  // {@code AWS_CRYPTO_URI}.
+  // {@code AWS_KMS_TEST_KEY_URI}.
   public static final String AWS_CREDS = "testdata/aws/credentials.cred";
 
   /** A dummy Aead-implementation that just throws exception. */
@@ -167,14 +171,25 @@
     return builder.build();
   }
 
-  /** @return a {@code Keyset} from a {@code handle}. */
+  /**
+   * @return a {@code Keyset} from a {@code handle}.
+   * @deprecated The return value of this function is a Keyset and typically should not be used.
+   *     Instead, operate on the KeysetHandle directly.
+   */
+  @Deprecated
   public static Keyset getKeyset(final KeysetHandle handle) {
-    return CleartextKeysetHandle.getKeyset(handle);
+    try {
+      return Keyset.parseFrom(
+          TinkProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get()),
+          ExtensionRegistryLite.getEmptyRegistry());
+    } catch (GeneralSecurityException | IOException e) {
+      throw new RuntimeException(e);
+    }
   }
 
   /** @return a keyset handle from a {@code keyset}. */
   public static KeysetHandle createKeysetHandle(Keyset keyset) throws Exception {
-    return CleartextKeysetHandle.parseFrom(keyset.toByteArray());
+    return TinkProtoKeysetFormat.parseKeyset(keyset.toByteArray(), InsecureSecretKeyAccess.get());
   }
 
   /** @return a keyset from a list of keys. The first key is primary. */
@@ -215,7 +230,11 @@
         .build();
   }
 
-  /** @return a {@code HkdfPrfKey}. */
+  /**
+   * @return a {@code HkdfPrfKey}.
+   * @deprecated Do not use this function
+   */
+  @Deprecated
   public static HkdfPrfKey createPrfKey(byte[] keyValue) throws Exception {
     HkdfPrfParams params = HkdfPrfParams.newBuilder().setHash(HashType.SHA256).build();
 
@@ -302,7 +321,11 @@
         KeyData.KeyMaterialType.SYMMETRIC);
   }
 
-  /** @return a {@code KeyData} containing a {@code AesCtrHmacAeadKey}. */
+  /**
+   * @return a {@code KeyData} containing a {@code AesCtrHmacAeadKey}.
+   * @deprecated Do not use this function
+   */
+  @Deprecated
   public static KeyData createAesCtrHmacAeadKeyData(
       byte[] aesCtrKeyValue, int ivSize, byte[] hmacKeyValue, int tagSize) throws Exception {
     AesCtrKey aesCtrKey = createAesCtrKey(aesCtrKeyValue, ivSize);
@@ -328,8 +351,13 @@
     return createKeyData(keyProto, AeadConfig.AES_GCM_TYPE_URL, KeyData.KeyMaterialType.SYMMETRIC);
   }
 
-  /** @return a {@code KeyData} containing a {@code AesEaxKey}. */
-  public static KeyData createAesEaxKeyData(byte[] keyValue, int ivSizeInBytes) throws Exception {
+  /**
+   * @return a {@code KeyData} containing a {@code AesEaxKey}.
+   * @deprecated DO not use this function.
+   */
+  @Deprecated
+  public static KeyData createAesEaxKeyData(byte[] keyValue, int ivSizeInBytes)
+      throws Exception {
     AesEaxKey keyProto =
         AesEaxKey.newBuilder()
             .setKeyValue(ByteString.copyFrom(keyValue))
@@ -514,7 +542,9 @@
   /**
    * @return a {@code KeyData} containing a {@code EciesAeadHkdfPrivateKey} with the specified key
    *     material and parameters.
+   * @deprecated Do not use this function
    */
+  @Deprecated
   public static EciesAeadHkdfPrivateKey createEciesAeadHkdfPrivKey(
       EciesAeadHkdfPublicKey pubKey, byte[] privKeyValue) throws Exception {
     final int version = 0;
@@ -525,7 +555,11 @@
         .build();
   }
 
-  /** @return a {@code EciesAeadHkdfPublicKey} with the specified key material and parameters. */
+  /**
+   * @return a {@code EciesAeadHkdfPublicKey} with the specified key material and parameters.
+   * @deprecated Do not use this function.
+   */
+  @Deprecated
   public static EciesAeadHkdfPublicKey createEciesAeadHkdfPubKey(
       EllipticCurveType curve,
       HashType hashType,
@@ -556,12 +590,28 @@
     assertArrayEquals(plaintext, decrypted);
   }
 
-  /** Decodes hex string. */
+  /**
+   * Decodes hex string.
+   *
+   * @deprecated Usages should be inlined.
+   */
+  @InlineMe(
+      replacement = "Hex.decode(hexData)",
+      imports = {"com.google.crypto.tink.subtle.Hex"})
+  @Deprecated
   public static byte[] hexDecode(String hexData) {
     return Hex.decode(hexData);
   }
 
-  /** Encodes bytes to hex string. */
+  /**
+   * Encodes bytes to hex string.
+   *
+   * @deprecated Usages should be inlined.
+   */
+  @InlineMe(
+      replacement = "Hex.encode(data)",
+      imports = {"com.google.crypto.tink.subtle.Hex"})
+  @Deprecated
   public static String hexEncode(byte[] data) {
     return Hex.encode(data);
   }
@@ -592,11 +642,11 @@
    * Check that this is running in Remote Build Execution.
    *
    * @return true if running on Remote Build Execution.
+   * @deprecated This isn't supported anymore
    */
+  @Deprecated
   public static boolean isRemoteBuildExecution() {
-    // This check depends on the system property rbe being set to 1.
-    // The property is set in kokoro/presubmit-remote.sh via bazel: --jvmopt=-Drbe=1.
-    return "1".equals(System.getProperty("rbe"));
+    return false;
   }
 
   /**
@@ -607,7 +657,13 @@
     return false;
   }
 
-  /** Returns whether we should skip a test with some AES key size. */
+  /**
+   * Returns whether we should skip a test with some AES key size.
+   *
+   * @deprecated Do not call this function. If you have any instance, you probably want to use
+   *     "false" instead.
+   */
+  @Deprecated
   public static boolean shouldSkipTestWithAesKeySize(int keySizeInBytes)
       throws NoSuchAlgorithmException {
     int maxKeySize = Cipher.getMaxAllowedKeyLength("AES/CTR/NoPadding");
@@ -641,24 +697,36 @@
     assertTrue(message, e.getMessage().contains(contains));
   }
 
-  /** Asserts that {@code key} is generated from {@code keyTemplate}. */
-  public static void assertHmacKey(com.google.crypto.tink.KeyTemplate keyTemplate, Keyset.Key key)
-      throws Exception {
+  /**
+   * Asserts that {@code key} is generated from {@code keyTemplate}.
+   *
+   * @deprecated Do not use this function.
+   */
+  @Deprecated
+  public static void assertHmacKey(
+      com.google.crypto.tink.KeyTemplate keyTemplate, Keyset.Key key) throws Exception {
+    KeyTemplate protoTemplate = KeyTemplateProtoConverter.toProto(keyTemplate);
+
     assertThat(key.getKeyId()).isGreaterThan(0);
     assertThat(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
     assertThat(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
     assertThat(key.hasKeyData()).isTrue();
-    assertThat(key.getKeyData().getTypeUrl()).isEqualTo(keyTemplate.getTypeUrl());
+    assertThat(key.getKeyData().getTypeUrl()).isEqualTo(protoTemplate.getTypeUrl());
 
     HmacKeyFormat hmacKeyFormat =
-        HmacKeyFormat.parseFrom(keyTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+        HmacKeyFormat.parseFrom(protoTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
     HmacKey hmacKey =
         HmacKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
     assertThat(hmacKey.getParams()).isEqualTo(hmacKeyFormat.getParams());
     assertThat(hmacKey.getKeyValue().size()).isEqualTo(hmacKeyFormat.getKeySize());
   }
 
-  /** Asserts that {@code KeyInfo} is corresponding to a key from {@code keyTemplate}. */
+  /**
+   * Asserts that {@code KeyInfo} is corresponding to a key from {@code keyTemplate}.
+   *
+   * @deprecated Do not use this function.
+   */
+  @Deprecated
   public static void assertKeyInfo(
       com.google.crypto.tink.KeyTemplate keyTemplate, KeysetInfo.KeyInfo keyInfo) throws Exception {
     assertThat(keyInfo.getKeyId()).isGreaterThan(0);
@@ -702,7 +770,12 @@
     assertByteBufferContains("", expected, buffer);
   }
 
-  /** Verifies that the given entry has the specified contents. */
+  /**
+   * Verifies that the given entry has the specified contents.
+   *
+   * @deprecated Do not use this function.
+   */
+  @Deprecated
   public static void verifyConfigEntry(
       KeyTypeEntry entry,
       String catalogueName,
diff --git a/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyAccess.java b/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyAccess.java
index aca2416..2411e76 100644
--- a/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyAccess.java
+++ b/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyAccess.java
@@ -19,10 +19,13 @@
 
 /**
  * An access token for {@code TinkKey}. Access to Tink keys is governed by {@code KeyHandle}. A
- * {@code TinkKey} which does not have a secret should be accessible by tokens generated by
- * {@code KeyAccess.publicAccess()}. A {@code TinkKey} with a secret should need a token generated
- * by {@code SecretKeyAccess.secretAccess()}.
- **/
+ * {@code TinkKey} which does not have a secret should be accessible by tokens generated by {@code
+ * KeyAccess.publicAccess()}. A {@code TinkKey} with a secret should need a token generated by
+ * {@code SecretKeyAccess.secretAccess()}.
+ *
+ * <p>Do not use this in new code. Instead, use {@link com.google.crypto.tink.SecretKeyAccess} and
+ * these facilities.
+ */
 @Immutable
 public final class KeyAccess {
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyHandle.java b/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyHandle.java
index ce03d69..c515372 100644
--- a/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyHandle.java
+++ b/java_src/src/main/java/com/google/crypto/tink/tinkkey/KeyHandle.java
@@ -28,6 +28,9 @@
  * Wraps a {@link TinkKey} and enforces access to the underlying {@link TinkKey} with {@link
  * KeyAccess}. Specifically, if the underlying {@link TinkKey} has a secret, then one can only get
  * it with a {@link SecretKeyAccess} instance.
+ *
+ * <p>Do not use this in new code. Instead, use {@link com.google.crypto.tink.Key} and
+ * these facilities.
  */
 @Immutable
 public class KeyHandle {
@@ -62,9 +65,9 @@
    * {@code keyData}. The returned KeyHandle has a secret if keyData has key material of type
    * UNKNOWN_KEYMATERIAL, SYMMETRIC, or ASYMMETRIC_PRIVATE.
    *
-   * @deprecated Use the KeyHandle(TinkKey, KeyAccess) constructor instead.
+   * <p>Do not use this in new code. Instead, use {@link com.google.crypto.tink.Key} and these
+   * facilities.
    */
-  @Deprecated
   public static KeyHandle createFromKey(KeyData keyData, OutputPrefixType opt) {
     return new KeyHandle(new ProtoKey(keyData, opt));
   }
diff --git a/java_src/src/main/java/com/google/crypto/tink/tinkkey/SecretKeyAccess.java b/java_src/src/main/java/com/google/crypto/tink/tinkkey/SecretKeyAccess.java
index bbbe528..f187e64 100644
--- a/java_src/src/main/java/com/google/crypto/tink/tinkkey/SecretKeyAccess.java
+++ b/java_src/src/main/java/com/google/crypto/tink/tinkkey/SecretKeyAccess.java
@@ -22,7 +22,10 @@
  *
  * <p>This class can be used to keep track of places where secret keys are accessed directly in
  * code, as opposed to indirectly via a primitive.
- **/
+ *
+ * <p>Do not use this in new code. Instead, use {@link
+ * com.google.crypto.tink.InsecureSecretKeyAccess} instead.
+ */
 @Immutable
 public final class SecretKeyAccess {
 
diff --git a/java_src/src/main/java/com/google/crypto/tink/tinkkey/TinkKey.java b/java_src/src/main/java/com/google/crypto/tink/tinkkey/TinkKey.java
index 2c8c093..5dcccab 100644
--- a/java_src/src/main/java/com/google/crypto/tink/tinkkey/TinkKey.java
+++ b/java_src/src/main/java/com/google/crypto/tink/tinkkey/TinkKey.java
@@ -22,10 +22,12 @@
  * {@code TinkKey} represents how Tink views individual keys. In contrast, {@code KeysetHandle} only
  * provides access to a {@code Keyset}, which represents multiple keys.
  *
- * <p> A {@code TinkKey} contains the data associated to a type of key and provides ways of getting
+ * <p>A {@code TinkKey} contains the data associated to a type of key and provides ways of getting
  * that data. The {@code TinkKey} interface does not specify how the key data is represented nor how
  * it provides access to the data.
-**/
+ *
+ * <p>Do not use this in new code. Instead, use {@link com.google.crypto.tink.Key}.
+ */
 @Immutable
 public interface TinkKey {
   /** Returns true if the key contains secret key material, and false otherwise. */
diff --git a/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel
index 686e1fe..38bcbae 100644
--- a/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel
@@ -9,18 +9,8 @@
     srcs = ["KeysDownloader.java"],
     deps = [
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_http_client_google_http_client",
-        "@maven//:joda_time_joda_time",
-    ],
-)
-
-android_library(
-    name = "keys_downloader-android",
-    srcs = ["KeysDownloader.java"],
-    deps = [
-        "@maven//:com_google_code_findbugs_jsr305",
-        "@maven//:com_google_http_client_google_http_client",
-        "@maven//:joda_time_joda_time",
     ],
 )
 
@@ -65,3 +55,23 @@
         "@maven//:com_google_errorprone_error_prone_annotations",
     ],
 )
+
+java_library(
+    name = "secret_big_integer",
+    srcs = ["SecretBigInteger.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
+
+android_library(
+    name = "secret_big_integer-android",
+    srcs = ["SecretBigInteger.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/annotations:alpha-android",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+    ],
+)
diff --git a/java_src/src/main/java/com/google/crypto/tink/util/KeysDownloader.java b/java_src/src/main/java/com/google/crypto/tink/util/KeysDownloader.java
index 87640bf..c76620b 100644
--- a/java_src/src/main/java/com/google/crypto/tink/util/KeysDownloader.java
+++ b/java_src/src/main/java/com/google/crypto/tink/util/KeysDownloader.java
@@ -23,6 +23,7 @@
 import com.google.api.client.http.HttpStatusCodes;
 import com.google.api.client.http.HttpTransport;
 import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -37,7 +38,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import javax.annotation.concurrent.GuardedBy;
-import org.joda.time.Instant;
 
 /**
  * Thread-safe downloader.
@@ -53,7 +53,10 @@
  * to proactively fetch the data.
  *
  * @since 1.1.0
+ * @deprecated This is not supported by Tink, as it incurs a dependency on <code>
+ *     com.google.api.client.http</code>. If you need this, please copy it into your codebase.
  */
+@Deprecated
 public class KeysDownloader {
   private static final Charset UTF_8 = Charset.forName("UTF-8");
 
@@ -156,10 +159,11 @@
    * <p>Visible so tests can override it in subclasses.
    */
   long getCurrentTimeInMillis() {
-    return Instant.now().getMillis();
+    return System.currentTimeMillis();
   }
 
   @GuardedBy("fetchDataLock")
+  @CanIgnoreReturnValue
   private String fetchAndCacheData() throws IOException {
     long currentTimeInMillis = getCurrentTimeInMillis();
     HttpRequest httpRequest =
@@ -282,12 +286,14 @@
     private String url;
 
     /** Sets the url which must point to a HTTPS server. */
+    @CanIgnoreReturnValue
     public Builder setUrl(String val) {
       this.url = val;
       return this;
     }
 
     /** Sets the background executor. */
+    @CanIgnoreReturnValue
     public Builder setExecutor(Executor val) {
       this.executor = val;
       return this;
@@ -299,6 +305,7 @@
      * <p>You generally should not need to set a custom transport as the default transport {@link
      * KeysDownloader#DEFAULT_HTTP_TRANSPORT} should be suited for most use cases.
      */
+    @CanIgnoreReturnValue
     public Builder setHttpTransport(HttpTransport httpTransport) {
       this.httpTransport = httpTransport;
       return this;
diff --git a/java_src/src/main/java/com/google/crypto/tink/util/SecretBigInteger.java b/java_src/src/main/java/com/google/crypto/tink/util/SecretBigInteger.java
new file mode 100644
index 0000000..f634a8c
--- /dev/null
+++ b/java_src/src/main/java/com/google/crypto/tink/util/SecretBigInteger.java
@@ -0,0 +1,72 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.util;
+
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.annotations.Alpha;
+import com.google.errorprone.annotations.Immutable;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+
+/** A class storing a secret BigInteger, protecting the value via {@link SecretKeyAccess}. */
+@Alpha
+@Immutable
+public final class SecretBigInteger {
+  private final BigInteger value;
+
+  private SecretBigInteger(BigInteger value) {
+    this.value = value;
+  }
+
+  /**
+   * Creates a new SecretBigInteger with the content given in {@code value}.
+   *
+   * <p>The parameter {@code access} must be non-null.
+   */
+  public static SecretBigInteger fromBigInteger(BigInteger value, SecretKeyAccess access) {
+    if (access == null) {
+      throw new NullPointerException("SecretKeyAccess required");
+    }
+    // Since BigInteger is immutable, there is no need to make a copy.
+    return new SecretBigInteger(value);
+  }
+
+  /**
+   * Returns the value wrapped by this object.
+   *
+   * <p>The parameter {@code access} must be non-null.
+   */
+  public BigInteger getBigInteger(SecretKeyAccess access) {
+    if (access == null) {
+      throw new NullPointerException("SecretKeyAccess required");
+    }
+    return value;
+  }
+
+  /**
+   * Returns true if {@code other} has the same secret value.
+   *
+   * <p>Note that the time may depend on the length of the byte-encoding of the BigIntegers.
+   */
+  public boolean equalsSecretBigInteger(SecretBigInteger other) {
+    // BigInteger.toByteArray always return the minimal encoding, so it is not possible that two
+    // BigInteger of the same values return different encodings.
+    byte[] myArray = value.toByteArray();
+    byte[] otherArray = other.value.toByteArray();
+    return MessageDigest.isEqual(myArray, otherArray);
+  }
+}
diff --git a/java_src/src/main/java/com/google/crypto/tink/util/SecretBytes.java b/java_src/src/main/java/com/google/crypto/tink/util/SecretBytes.java
index 5085087..3e884a5 100644
--- a/java_src/src/main/java/com/google/crypto/tink/util/SecretBytes.java
+++ b/java_src/src/main/java/com/google/crypto/tink/util/SecretBytes.java
@@ -20,6 +20,7 @@
 import com.google.crypto.tink.annotations.Alpha;
 import com.google.crypto.tink.subtle.Random;
 import com.google.errorprone.annotations.Immutable;
+import java.security.MessageDigest;
 
 /** A class storing an immutable byte array, protecting the data via {@link SecretKeyAccess}. */
 @Alpha
@@ -72,13 +73,6 @@
   public boolean equalsSecretBytes(SecretBytes other) {
     byte[] myArray = bytes.toByteArray();
     byte[] otherArray = other.bytes.toByteArray();
-    if (myArray.length != otherArray.length) {
-      return false;
-    }
-    int res = 0;
-    for (int i = 0; i < myArray.length; i++) {
-      res |= myArray[i] ^ otherArray[i];
-    }
-    return res == 0;
+    return MessageDigest.isEqual(myArray, otherArray);
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel
index 4dc1a83..59e2410 100644
--- a/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel
@@ -7,11 +7,12 @@
     deps = [
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:no_secret_keyset_handle",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/config:tink_config",
-        "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -65,7 +66,7 @@
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -79,16 +80,18 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "//src/main/java/com/google/crypto/tink:config",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:json_keyset_reader",
         "//src/main/java/com/google/crypto/tink:json_keyset_writer",
+        "//src/main/java/com/google/crypto/tink:key_status",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:mac",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "//src/main/java/com/google/crypto/tink/config:tink_config",
-        "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_truth_truth",
@@ -102,23 +105,24 @@
     srcs = ["KeysetManagerTest.java"],
     deps = [
         "//proto:aes_gcm_java_proto",
-        "//proto:common_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_status",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
         "//src/main/java/com/google/crypto/tink/config:tink_config",
-        "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_access",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_handle",
         "//src/main/java/com/google/crypto/tink/tinkkey:secret_key_access",
         "//src/main/java/com/google/crypto/tink/tinkkey:tink_key",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -143,16 +147,16 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "//src/main/java/com/google/crypto/tink:config",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:json_keyset_reader",
         "//src/main/java/com/google/crypto/tink:json_keyset_writer",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:mac",
-        "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "//src/main/java/com/google/crypto/tink/config:tink_config",
-        "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -176,6 +180,7 @@
     deps = [
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:crypto_format",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -187,6 +192,8 @@
     size = "small",
     srcs = ["PrimitiveSetTest.java"],
     deps = [
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:crypto_format",
         "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
@@ -194,9 +201,12 @@
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink:primitive_set",
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -224,6 +234,7 @@
         "//src/main/java/com/google/crypto/tink:public_key_sign",
         "//src/main/java/com/google/crypto/tink:public_key_verify",
         "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
@@ -234,7 +245,7 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
         "//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:signature_key_templates",
         "//src/main/java/com/google/crypto/tink/subtle:aes_eax_jce",
@@ -242,7 +253,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -254,12 +265,11 @@
     srcs = ["IntegrationTest.java"],
     data = ["//testdata/keysets:ecies"],
     deps = [
-        "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
-        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "//src/main/java/com/google/crypto/tink:config",
         "//src/main/java/com/google/crypto/tink:crypto_format",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt",
         "//src/main/java/com/google/crypto/tink:hybrid_encrypt",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/config:tink_config",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
@@ -285,13 +295,16 @@
     srcs = ["KeysetHandleTest.java"],
     deps = [
         "//proto:aes_eax_java_proto",
+        "//proto:common_java_proto",
         "//proto:ecdsa_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:hmac_prf_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
         "//src/main/java/com/google/crypto/tink:binary_keyset_writer",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "//src/main/java/com/google/crypto/tink:config",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key",
         "//src/main/java/com/google/crypto/tink:key_status",
         "//src/main/java/com/google/crypto/tink:key_template",
@@ -307,24 +320,32 @@
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
-        "//src/main/java/com/google/crypto/tink/config:tink_config",
+        "//src/main/java/com/google/crypto/tink/internal:internal_configuration",
         "//src/main/java/com/google/crypto/tink/internal:key_parser",
         "//src/main/java/com/google/crypto/tink/internal:key_status_type_proto_converter",
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key",
         "//src/main/java/com/google/crypto/tink/internal:monitoring_util",
         "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_registry",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal/testing:fake_monitoring_client",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client",
-        "//src/main/java/com/google/crypto/tink/signature:public_key_sign_factory",
-        "//src/main/java/com/google/crypto/tink/signature:public_key_verify_factory",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
         "//src/main/java/com/google/crypto/tink/signature:signature_config",
-        "//src/main/java/com/google/crypto/tink/signature:signature_key_templates",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
@@ -334,9 +355,9 @@
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key",
         "//src/main/java/com/google/crypto/tink/util:bytes",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -366,7 +387,7 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -394,7 +415,7 @@
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -430,7 +451,7 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -448,7 +469,7 @@
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:private_key_type_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -467,6 +488,7 @@
         "//src/main/java/com/google/crypto/tink/internal:monitoring_util",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -491,7 +513,184 @@
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "BinaryKeysetReaderTest",
+    size = "small",
+    srcs = ["BinaryKeysetReaderTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:binary_keyset_writer",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:key_status",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:keyset_reader",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "TinkProtoKeysetFormatTest",
+    size = "small",
+    srcs = ["TinkProtoKeysetFormatTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:binary_keyset_writer",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_status",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "TinkJsonProtoKeysetFormatTest",
+    size = "small",
+    srcs = ["TinkJsonProtoKeysetFormatTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:json_keyset_writer",
+        "//src/main/java/com/google/crypto/tink:key_status",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeysetHandleFullPrimitiveTest",
+    size = "small",
+    srcs = ["KeysetHandleFullPrimitiveTest.java"],
+    deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:key_status",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "TinkProtoParametersFormatTest",
+    size = "small",
+    srcs = ["TinkProtoParametersFormatTest.java"],
+    deps = [
+        "//proto:aes_cmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
+        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeyTemplateTest",
+    size = "small",
+    srcs = ["KeyTemplateTest.java"],
+    deps = [
+        "//proto:aes_gcm_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeyTemplatesAsParametersTest",
+    size = "small",
+    srcs = ["KeyTemplatesAsParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/config:tink_config",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:predefined_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:predefined_signature_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:predefined_streaming_aead_parameters",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/BinaryKeysetReaderTest.java b/java_src/src/test/java/com/google/crypto/tink/BinaryKeysetReaderTest.java
new file mode 100644
index 0000000..29b3ca3
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/BinaryKeysetReaderTest.java
@@ -0,0 +1,172 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.mac.MacConfig;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for BinaryKeysetReader. */
+@RunWith(JUnit4.class)
+public class BinaryKeysetReaderTest {
+
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    MacConfig.register();
+    AeadConfig.register();
+  }
+
+  private void assertKeysetHandle(KeysetHandle handle1, KeysetHandle handle2) throws Exception {
+    Mac mac1 = handle1.getPrimitive(Mac.class);
+    Mac mac2 = handle2.getPrimitive(Mac.class);
+    byte[] message = "message".getBytes(UTF_8);
+
+    assertThat(handle2.getKeyset()).isEqualTo(handle1.getKeyset());
+    mac2.verifyMac(mac1.computeMac(message), message);
+  }
+
+  @Test
+  public void testReadWithInputStream_singleKey_shouldWork() throws Exception {
+    KeyTemplate template = KeyTemplates.get("HMAC_SHA256_128BITTAG");
+    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(handle1, BinaryKeysetWriter.withOutputStream(outputStream));
+    KeysetHandle handle2 =
+        CleartextKeysetHandle.read(
+            BinaryKeysetReader.withInputStream(
+                new ByteArrayInputStream(outputStream.toByteArray())));
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testReadWithInputStream_multipleKeys_shouldWork() throws Exception {
+    KeysetHandle handle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .build();
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(handle1, BinaryKeysetWriter.withOutputStream(outputStream));
+    KeysetHandle handle2 =
+        CleartextKeysetHandle.read(
+            BinaryKeysetReader.withInputStream(
+                new ByteArrayInputStream(outputStream.toByteArray())));
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testReadWithBytes_shouldWork() throws Exception {
+    KeyTemplate template = KeyTemplates.get("HMAC_SHA256_128BITTAG");
+    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(handle1, BinaryKeysetWriter.withOutputStream(outputStream));
+    byte[] binaryKeyset = outputStream.toByteArray();
+
+    KeysetReader reader = BinaryKeysetReader.withBytes(binaryKeyset);
+    KeysetHandle handle2 = CleartextKeysetHandle.read(reader);
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testWithBytesReadTwice_fails() throws Exception {
+    KeyTemplate template = KeyTemplates.get("HMAC_SHA256_128BITTAG");
+    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(handle1, BinaryKeysetWriter.withOutputStream(outputStream));
+    byte[] binaryKeyset = outputStream.toByteArray();
+
+    KeysetReader reader = BinaryKeysetReader.withBytes(binaryKeyset);
+    KeysetHandle unused = CleartextKeysetHandle.read(reader);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> CleartextKeysetHandle.read(reader));
+  }
+
+  @Test
+  public void testReadEncrypted_singleKey_shouldWork() throws Exception {
+    Aead keysetEncryptionAead =
+        KeysetHandle.generateNew(KeyTemplates.get("AES128_EAX")).getPrimitive(Aead.class);
+    KeysetHandle handle1 = KeysetHandle.generateNew(KeyTemplates.get("HMAC_SHA256_128BITTAG"));
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    handle1.write(BinaryKeysetWriter.withOutputStream(outputStream), keysetEncryptionAead);
+    KeysetHandle handle2 =
+        KeysetHandle.read(
+            BinaryKeysetReader.withInputStream(
+                new ByteArrayInputStream(outputStream.toByteArray())),
+            keysetEncryptionAead);
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+  @Test
+  public void testReadEncrypted_multipleKeys_shouldWork() throws Exception {
+    KeysetHandle handle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG_RAW")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG")
+                    .withRandomId().setStatus(KeyStatus.DESTROYED))
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG_RAW")
+                    .withRandomId().setStatus(KeyStatus.DISABLED))
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+                    .withRandomId())
+            .build();
+
+    Aead keysetEncryptionAead =
+        KeysetHandle.generateNew(KeyTemplates.get("AES128_EAX")).getPrimitive(Aead.class);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    handle1.write(BinaryKeysetWriter.withOutputStream(outputStream), keysetEncryptionAead);
+    KeysetHandle handle2 =
+        KeysetHandle.read(
+            BinaryKeysetReader.withInputStream(
+                new ByteArrayInputStream(outputStream.toByteArray())),
+            keysetEncryptionAead);
+
+    assertKeysetHandle(handle1, handle2);
+  }
+
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java b/java_src/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java
index 94b9559..5a4b72b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/CleartextKeysetHandleTest.java
@@ -21,6 +21,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 import com.google.crypto.tink.config.TinkConfig;
 import com.google.crypto.tink.internal.MutableMonitoringRegistry;
@@ -55,7 +56,7 @@
     Keyset keyset = CleartextKeysetHandle.getKeyset(handle);
     handle = CleartextKeysetHandle.parseFrom(keyset.toByteArray());
     assertEquals(keyset, handle.getKeyset());
-    handle.getPrimitive(Mac.class);
+    Object unused = handle.getPrimitive(Mac.class);
   }
 
   @Test
@@ -69,13 +70,10 @@
         CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(keyset1.toByteArray()));
     assertEquals(keyset1, handle1.getKeyset());
 
-    KeysetHandle handle2 = KeysetHandle.generateNew(template);
-    Keyset keyset2 = handle2.getKeyset();
-    assertEquals(1, keyset2.getKeyCount());
-    Keyset.Key key2 = keyset2.getKey(0);
-    assertEquals(keyset2.getPrimaryKeyId(), key2.getKeyId());
-    assertEquals(template.getTypeUrl(), key2.getKeyData().getTypeUrl());
-    Mac unused = handle2.getPrimitive(Mac.class);
+    assertThat(handle1.size()).isEqualTo(1);
+    assertThat(handle1.getAt(0).getId()).isEqualTo(handle.getAt(0).getId());
+    assertThat(handle1.getAt(0).getStatus()).isEqualTo(handle.getAt(0).getStatus());
+    assertTrue(handle1.getAt(0).getKey().equalsKey(handle.getAt(0).getKey()));
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/CryptoFormatTest.java b/java_src/src/test/java/com/google/crypto/tink/CryptoFormatTest.java
index 081fa5a..247017d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/CryptoFormatTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/CryptoFormatTest.java
@@ -22,6 +22,7 @@
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.TestUtil;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -47,19 +48,19 @@
   @Test
   public void testTinkPrefix() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.TINK, 0x66AABBCC)))
-        .isEqualTo(TestUtil.hexDecode("0166AABBCC"));
+        .isEqualTo(Hex.decode("0166AABBCC"));
   }
 
   @Test
   public void testLegacyPrefix() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.LEGACY, 0x66AABBCC)))
-        .isEqualTo(TestUtil.hexDecode("0066AABBCC"));
+        .isEqualTo(Hex.decode("0066AABBCC"));
   }
 
   @Test
   public void testCrunchyPrefix() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.CRUNCHY, 0x66AABBCC)))
-        .isEqualTo(TestUtil.hexDecode("0066AABBCC"));
+        .isEqualTo(Hex.decode("0066AABBCC"));
   }
 
   @Test
@@ -93,50 +94,50 @@
   public void testKeyIdWithMsbSet() throws Exception {
     int keyId = 0xFF7F1058;
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.TINK, keyId)))
-        .isEqualTo(TestUtil.hexDecode("01FF7F1058"));
+        .isEqualTo(Hex.decode("01FF7F1058"));
   }
 
   @Test
   public void testKeyIdIsZero() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.RAW, 0))).isEmpty();
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.TINK, 0)))
-        .isEqualTo(TestUtil.hexDecode("0100000000"));
+        .isEqualTo(Hex.decode("0100000000"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.LEGACY, 0)))
-        .isEqualTo(TestUtil.hexDecode("0000000000"));
+        .isEqualTo(Hex.decode("0000000000"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.CRUNCHY, 0)))
-        .isEqualTo(TestUtil.hexDecode("0000000000"));
+        .isEqualTo(Hex.decode("0000000000"));
   }
 
   @Test
   public void testKeyIdIsMinusOne() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.RAW, -1))).isEmpty();
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.TINK, -1)))
-        .isEqualTo(TestUtil.hexDecode("01FFFFFFFF"));
+        .isEqualTo(Hex.decode("01FFFFFFFF"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.LEGACY, -1)))
-        .isEqualTo(TestUtil.hexDecode("00FFFFFFFF"));
+        .isEqualTo(Hex.decode("00FFFFFFFF"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.CRUNCHY, -1)))
-        .isEqualTo(TestUtil.hexDecode("00FFFFFFFF"));
+        .isEqualTo(Hex.decode("00FFFFFFFF"));
   }
 
   @Test
   public void testKeyIdIsMaxInt() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.RAW, 2147483647))).isEmpty();
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.TINK, 2147483647)))
-        .isEqualTo(TestUtil.hexDecode("017FFFFFFF"));
+        .isEqualTo(Hex.decode("017FFFFFFF"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.LEGACY, 2147483647)))
-        .isEqualTo(TestUtil.hexDecode("007FFFFFFF"));
+        .isEqualTo(Hex.decode("007FFFFFFF"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.CRUNCHY, 2147483647)))
-        .isEqualTo(TestUtil.hexDecode("007FFFFFFF"));
+        .isEqualTo(Hex.decode("007FFFFFFF"));
   }
 
   @Test
   public void testKeyIdIsMinInt() throws Exception {
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.RAW, -2147483648))).isEmpty();
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.TINK, -2147483648)))
-        .isEqualTo(TestUtil.hexDecode("0180000000"));
+        .isEqualTo(Hex.decode("0180000000"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.LEGACY, -2147483648)))
-        .isEqualTo(TestUtil.hexDecode("0080000000"));
+        .isEqualTo(Hex.decode("0080000000"));
     assertThat(CryptoFormat.getOutputPrefix(getKey(OutputPrefixType.CRUNCHY, -2147483648)))
-        .isEqualTo(TestUtil.hexDecode("0080000000"));
+        .isEqualTo(Hex.decode("0080000000"));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/IntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/IntegrationTest.java
index f2fedae..f379a21 100644
--- a/java_src/src/test/java/com/google/crypto/tink/IntegrationTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/IntegrationTest.java
@@ -22,7 +22,8 @@
 import com.google.crypto.tink.config.TinkConfig;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
-import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -36,7 +37,7 @@
 public class IntegrationTest {
   @BeforeClass
   public static void setUp() throws GeneralSecurityException {
-    Config.register(TinkConfig.TINK_1_0_0);
+    TinkConfig.register();
   }
 
   /**
@@ -49,15 +50,16 @@
       return;
     }
     HybridDecrypt hybridDecrypt =
-        CleartextKeysetHandle.read(
-                BinaryKeysetReader.withFile(
-                    new File("testdata/keysets/ecies_private_keyset2.bin")))
+        TinkProtoKeysetFormat.parseKeyset(
+                Files.readAllBytes(
+                    Paths.get("testdata/keysets/ecies_private_keyset2.bin")),
+                InsecureSecretKeyAccess.get())
             .getPrimitive(HybridDecrypt.class);
 
     HybridEncrypt hybridEncrypt =
-        CleartextKeysetHandle.read(
-                BinaryKeysetReader.withFile(
-                    new File("testdata/keysets/ecies_public_keyset2.bin")))
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(
+                Files.readAllBytes(
+                    Paths.get("testdata/keysets/ecies_public_keyset2.bin")))
             .getPrimitive(HybridEncrypt.class);
 
     byte[] plaintext = Random.randBytes(20);
@@ -84,15 +86,16 @@
     }
 
     HybridDecrypt hybridDecrypt =
-        CleartextKeysetHandle.read(
-                BinaryKeysetReader.withFile(
-                    new File("testdata/keysets/ecies_private_keyset.bin")))
+        TinkProtoKeysetFormat.parseKeyset(
+                Files.readAllBytes(
+                    Paths.get("testdata/keysets/ecies_private_keyset.bin")),
+                InsecureSecretKeyAccess.get())
             .getPrimitive(HybridDecrypt.class);
 
     HybridEncrypt hybridEncrypt =
-        CleartextKeysetHandle.read(
-                BinaryKeysetReader.withFile(
-                    new File("testdata/keysets/ecies_public_keyset.bin")))
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(
+                Files.readAllBytes(
+                    Paths.get("testdata/keysets/ecies_public_keyset.bin")))
             .getPrimitive(HybridEncrypt.class);
 
     byte[] plaintext = Random.randBytes(20);
diff --git a/java_src/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java b/java_src/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java
index 607b208..3d01b77 100644
--- a/java_src/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java
@@ -20,11 +20,11 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
-import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.config.TinkConfig;
-import com.google.crypto.tink.mac.MacKeyTemplates;
-import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
 import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
@@ -77,7 +77,7 @@
 
   @BeforeClass
   public static void setUp() throws GeneralSecurityException {
-    Config.register(TinkConfig.TINK_1_0_0);
+    TinkConfig.register();
   }
 
   private void assertKeysetHandle(KeysetHandle handle1, KeysetHandle handle2) throws Exception {
@@ -91,8 +91,7 @@
 
   @Test
   public void testRead_singleKey_shouldWork() throws Exception {
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
-    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    KeysetHandle handle1 = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     CleartextKeysetHandle.write(handle1, JsonKeysetWriter.withOutputStream(outputStream));
     KeysetHandle handle2 =
@@ -104,13 +103,19 @@
 
   @Test
   public void testRead_multipleKeys_shouldWork() throws Exception {
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
     KeysetHandle handle1 =
-        KeysetManager.withEmptyKeyset()
-            .rotate(template)
-            .add(template)
-            .add(template)
-            .getKeysetHandle();
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .build();
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     CleartextKeysetHandle.write(handle1, JsonKeysetWriter.withOutputStream(outputStream));
     KeysetHandle handle2 =
@@ -121,6 +126,47 @@
   }
 
   @Test
+  public void readTestKeysetVerifyTestTag() throws Exception {
+    KeysetHandle handle = CleartextKeysetHandle.read(JsonKeysetReader.withString(JSON_KEYSET));
+    byte[] data = "data".getBytes(UTF_8);
+    Mac mac = handle.getPrimitive(Mac.class);
+    byte[] tag = Hex.decode("0120a4107f3549e4fb3137415a63f5c8a0524f8ca7");
+    mac.verifyMac(tag, data);
+  }
+
+  @Test
+  public void readEncryptedTestKeysetVerifyTestTag() throws Exception {
+    // This is the same test vector as in KeysetHandleTest.
+    // An AEAD key, with which we encrypted the mac keyset below.
+    byte[] serializedKeysetEncryptionKeyset =
+        Hex.decode(
+            "08b891f5a20412580a4c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e6372797"
+                + "0746f2e74696e6b2e4165734561784b65791216120208101a10e5d7d0cdd649e81e7952260689b2"
+                + "e1971801100118b891f5a2042001");
+    KeysetHandle keysetEncryptionHandle = TinkProtoKeysetFormat.parseKeyset(
+        serializedKeysetEncryptionKeyset, InsecureSecretKeyAccess.get());
+    Aead keysetEncryptionAead = keysetEncryptionHandle.getPrimitive(Aead.class);
+    byte[] associatedData = Hex.decode("abcdef330012");
+
+    String encryptedKeyset =
+        "{\"encryptedKeyset\":"
+            + "\"AURdSLhZcFEgMBptDyi4/D8hL3h+Iz7ICgLrdeVRH26Fi3uSeewFoFA5cV5wfNueme3/BBR60yJ4hGpQ"
+            + "p+/248ZIgfuWyfmAGZ4dmYnYC1qd/IWkZZfVr3aOsx4j4kFZHkkvA+XIZUh/INbdPsMUNJy9cmu6s8osdH"
+            + "zu0XzP2ltWUowbr0fLQJwy92eAvU6gv91k6Tc=\","
+            + "\"keysetInfo\":{\"primaryKeyId\":547623039,\"keyInfo\":[{\"typeUrl\":"
+            + "\"type.googleapis.com/google.crypto.tink.HmacKey\",\"status\":\"ENABLED\","
+            + "\"keyId\":547623039,\"outputPrefixType\":\"TINK\"}]}}";
+
+    KeysetHandle handle = KeysetHandle.readWithAssociatedData(
+        JsonKeysetReader.withString(encryptedKeyset), keysetEncryptionAead, associatedData);
+    byte[] data = "data".getBytes(UTF_8);
+    Mac mac = handle.getPrimitive(Mac.class);
+    byte[] tag = Hex.decode("0120a4107f3549e4fb3137415a63f5c8a0524f8ca7");
+    mac.verifyMac(tag, data);
+  }
+
+
+  @Test
   public void testRead_urlSafeKeyset_shouldWork() throws Exception {
     KeysetHandle handle1 = CleartextKeysetHandle.read(JsonKeysetReader.withString(JSON_KEYSET));
     KeysetHandle handle2 =
@@ -235,8 +281,7 @@
 
   @Test
   public void testRead_jsonKeysetWriter_shouldWork() throws Exception {
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
-    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    KeysetHandle handle1 = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     CleartextKeysetHandle.write(handle1, JsonKeysetWriter.withOutputStream(outputStream));
     KeysetHandle handle2 =
@@ -265,9 +310,9 @@
 
   @Test
   public void testReadEncrypted_singleKey_shouldWork() throws Exception {
-    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_EAX;
-    Aead masterKey = Registry.getPrimitive(Registry.newKeyData(masterKeyTemplate));
-    KeysetHandle handle1 = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    Aead masterKey =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_EAX).getPrimitive(Aead.class);
+    KeysetHandle handle1 = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
     KeysetHandle handle2 =
@@ -283,25 +328,27 @@
     Aead keysetEncryptionAead =
         KeysetHandle.generateNew(KeyTemplates.get("AES128_EAX")).getPrimitive(Aead.class);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    KeysetManager manager =
-        KeysetManager.withEmptyKeyset()
-            .add(KeyTemplates.get("HMAC_SHA256_128BITTAG"))
-            .add(KeyTemplates.get("HMAC_SHA256_128BITTAG_RAW"))
-            .add(KeyTemplates.get("HMAC_SHA256_256BITTAG"))
-            .add(KeyTemplates.get("HMAC_SHA256_256BITTAG_RAW"))
-            .add(KeyTemplates.get("AES256_CMAC"));
-    // To get the key IDs
-    KeysetHandle h = manager.getKeysetHandle();
-    int keyId1 = h.getKeysetInfo().getKeyInfo(1).getKeyId();
-    int keyId2 = h.getKeysetInfo().getKeyInfo(2).getKeyId();
-    int keyId3 = h.getKeysetInfo().getKeyInfo(3).getKeyId();
-    int keyId4 = h.getKeysetInfo().getKeyInfo(4).getKeyId();
-    manager.setPrimary(keyId1);
-    manager.delete(keyId2);
-    manager.destroy(keyId3);
-    manager.disable(keyId4);
-    KeysetHandle handle1 = manager.getKeysetHandle();
-
+    KeysetHandle handle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG_RAW")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG_RAW")
+                    .withRandomId()
+                    .setStatus(KeyStatus.DESTROYED))
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+                    .withRandomId()
+                    .setStatus(KeyStatus.DISABLED))
+            .build();
     handle1.write(JsonKeysetWriter.withOutputStream(outputStream), keysetEncryptionAead);
     KeysetHandle handle2 =
         KeysetHandle.read(
@@ -335,10 +382,10 @@
 
   @Test
   public void testReadEncrypted_missingEncryptedKeyset_shouldThrowException() throws Exception {
-    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_EAX;
-    Aead masterKey = Registry.getPrimitive(Registry.newKeyData(masterKeyTemplate));
+    Aead masterKey =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_EAX).getPrimitive(Aead.class);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    KeysetHandle handle = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    KeysetHandle handle = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
     handle.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
     JsonObject json =
         JsonParser.parseString(new String(outputStream.toByteArray(), UTF_8)).getAsJsonObject();
@@ -352,9 +399,9 @@
 
   @Test
   public void testReadEncrypted_jsonKeysetWriter_shouldWork() throws Exception {
-    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_EAX;
-    Aead masterKey = Registry.getPrimitive(Registry.newKeyData(masterKeyTemplate));
-    KeysetHandle handle1 = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    Aead masterKey =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_EAX).getPrimitive(Aead.class);
+    KeysetHandle handle1 = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
     KeysetHandle handle2 =
@@ -364,26 +411,94 @@
   }
 
   @Test
-  public void testReadKeyset_negativeKeyId_works() throws Exception {
+  public void readKeyset_negativeKeyId_works() throws Exception {
     String jsonKeysetString = createJsonKeysetWithId("-21");
     Keyset keyset = JsonKeysetReader.withString(jsonKeysetString).read();
     assertThat(keyset.getPrimaryKeyId()).isEqualTo(-21);
   }
 
   @Test
-  public void testReadKeyset_hugeKeyId_convertsIntoSignedInt32() throws Exception {
+  public void readKeyset_convertsUnsignedUint32IntoSignedInt32() throws Exception {
     String jsonKeysetString = createJsonKeysetWithId("4294967275"); // 2^32 - 21
     Keyset keyset = JsonKeysetReader.withString(jsonKeysetString).read();
     assertThat(keyset.getPrimaryKeyId()).isEqualTo(-21);
   }
 
   @Test
+  public void readKeyset_acceptsMaxUint32() throws Exception {
+    String jsonKeysetString = createJsonKeysetWithId("4294967295"); // 2^32 - 1 = 0xffffffff
+    Keyset keyset = JsonKeysetReader.withString(jsonKeysetString).read();
+    assertThat(keyset.getPrimaryKeyId()).isEqualTo(-1);
+  }
+
+  @Test
+  public void readKeyset_acceptsMinInt32() throws Exception {
+    String jsonKeysetString = createJsonKeysetWithId("-2147483648"); // - 2^31
+    Keyset keyset = JsonKeysetReader.withString(jsonKeysetString).read();
+    assertThat(keyset.getPrimaryKeyId()).isEqualTo(-2147483648);
+  }
+
+  @Test
+  public void readKeyset_rejectsKeyIdLargerThanUint32() throws Exception {
+    String jsonKeysetString = createJsonKeysetWithId("4294967296"); // 2^32
+    assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read());
+  }
+
+  @Test
+  public void readKeyset_rejectsKeyIdLargerThanUint64() throws Exception {
+    String jsonKeysetString = createJsonKeysetWithId("18446744073709551658"); // 2^64 + 42
+    assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read());
+  }
+
+  @Test
+  public void readKeyset_rejectsKeyIdSmallerThanInt32() throws Exception {
+    String jsonKeysetString = createJsonKeysetWithId("-2147483649"); // - 2^31 - 1
+    assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read());
+  }
+
+  @Test
   public void testReadKeyset_keyIdWithComment_throws() throws Exception {
     String jsonKeysetString = createJsonKeysetWithId("123 /* comment on key ID */");
     assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read());
   }
 
   @Test
+  public void testReadKeyset_withDuplicatedMapKey_throws() throws Exception {
+    String jsonKeysetString = "{"
+        + "\"primaryKeyId\": 123,"
+        + "\"key\": [{"
+        + "\"keyData\": {"
+        + "\"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+        + "\"keyMaterialType\": \"SYMMETRIC\","
+        + "\"keyMaterialType\": \"SYMMETRIC\","
+        + "\"value\": \"EgQIAxAQGiBYhMkitTWFVefTIBg6kpvac+bwFOGSkENGmU+1EYgocg==\""
+        + "},"
+        + "\"outputPrefixType\": \"TINK\","
+        + "\"keyId\": 123,"
+        + "\"status\": \"ENABLED\""
+        + "}]}";
+    assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read());
+  }
+
+  @Test
+  public void testReadKeyset_withInvalidCharacterInTypeUrl_throws() throws Exception {
+    String jsonKeysetString =
+        "{"
+            + "\"primaryKeyId\": 123,"
+            + "\"key\": [{"
+            + "\"keyData\": {"
+            + "\"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\\uD834\","
+            + "\"keyMaterialType\": \"SYMMETRIC\","
+            + "\"value\": \"EgQIAxAQGiBYhMkitTWFVefTIBg6kpvac+bwFOGSkENGmU+1EYgocg==\""
+            + "},"
+            + "\"outputPrefixType\": \"TINK\","
+            + "\"keyId\": 123,"
+            + "\"status\": \"ENABLED\""
+            + "}]}";
+    assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read());
+  }
+
+  @Test
   public void testReadKeyset_withoutQuotes_throws() throws Exception {
     String jsonKeysetString = "{"
         + "primaryKeyId: 123,"
diff --git a/java_src/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java b/java_src/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java
index ec6bbdd..59c19e6 100644
--- a/java_src/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/JsonKeysetWriterTest.java
@@ -19,10 +19,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.config.TinkConfig;
-import com.google.crypto.tink.mac.MacKeyTemplates;
-import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.subtle.Random;
 import java.io.ByteArrayInputStream;
@@ -38,7 +37,7 @@
 public class JsonKeysetWriterTest {
   @BeforeClass
   public static void setUp() throws GeneralSecurityException {
-    Config.register(TinkConfig.TINK_1_0_0);
+    TinkConfig.register();
   }
 
   private void assertKeysetHandle(KeysetHandle handle1, KeysetHandle handle2) throws Exception {
@@ -62,29 +61,33 @@
 
   @Test
   public void testWrite_singleKey_shouldWork() throws Exception {
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
-    KeysetHandle handle1 = KeysetHandle.generateNew(template);
+    KeysetHandle handle1 = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
 
     testWrite_shouldWork(handle1);
   }
 
   @Test
   public void testWrite_multipleKeys_shouldWork() throws Exception {
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
     KeysetHandle handle1 =
-        KeysetManager.withEmptyKeyset()
-            .rotate(template)
-            .add(template)
-            .add(template)
-            .getKeysetHandle();
-
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .build();
     testWrite_shouldWork(handle1);
   }
 
   private void testWriteEncrypted_shouldWork(KeysetHandle handle1) throws Exception {
     // Encrypt the keyset with an AeadKey.
-    KeyTemplate masterKeyTemplate = AeadKeyTemplates.AES128_EAX;
-    Aead masterKey = Registry.getPrimitive(Registry.newKeyData(masterKeyTemplate));
+    Aead masterKey =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_EAX).getPrimitive(Aead.class);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     handle1.write(JsonKeysetWriter.withOutputStream(outputStream), masterKey);
     KeysetHandle handle2 =
@@ -98,7 +101,7 @@
   @Test
   public void testWriteEncrypted_singleKey_shouldWork() throws Exception {
     // Encrypt the keyset with an AeadKey.
-    KeysetHandle handle1 = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    KeysetHandle handle1 = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
 
     testWriteEncrypted_shouldWork(handle1);
   }
@@ -106,14 +109,19 @@
   @Test
   public void testWriteEncrypted_multipleKeys_shouldWork() throws Exception {
     // Encrypt the keyset with an AeadKey.
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
     KeysetHandle handle1 =
-        KeysetManager.withEmptyKeyset()
-            .rotate(template)
-            .add(template)
-            .add(template)
-            .getKeysetHandle();
-
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                    .withRandomId())
+            .build();
     testWriteEncrypted_shouldWork(handle1);
   }
 
@@ -122,13 +130,14 @@
     int magicKeyId = -19230912;
     Keyset unmodified =
         CleartextKeysetHandle.getKeyset(
-            KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG));
+            KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG));
     Keyset modified =
         Keyset.newBuilder(unmodified)
             .setPrimaryKeyId(magicKeyId)
             .setKey(0, Keyset.Key.newBuilder(unmodified.getKey(0)).setKeyId(magicKeyId).build())
             .build();
-    KeysetHandle modifiedHandle = CleartextKeysetHandle.parseFrom(modified.toByteArray());
+    KeysetHandle modifiedHandle =
+        TinkProtoKeysetFormat.parseKeyset(modified.toByteArray(), InsecureSecretKeyAccess.get());
 
     // Write cleartext keyset
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeyManagerImplTest.java b/java_src/src/test/java/com/google/crypto/tink/KeyManagerImplTest.java
index 5871a4e..b75ae62 100644
--- a/java_src/src/test/java/com/google/crypto/tink/KeyManagerImplTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/KeyManagerImplTest.java
@@ -117,7 +117,7 @@
   public void getPrimitive_byteString_works() throws Exception {
     KeyManager<Aead> keyManager = new KeyManagerImpl<>(new TestKeyTypeManager(), Aead.class);
     MessageLite key = keyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build());
-    keyManager.getPrimitive(key.toByteString());
+    Object unused = keyManager.getPrimitive(key.toByteString());
   }
 
   @Test
@@ -126,7 +126,7 @@
         new KeyManagerImpl<>(new TestKeyTypeManager(), FakeAead.class);
     MessageLite key =
         fakeAeadKeyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build());
-    fakeAeadKeyManager.getPrimitive(key.toByteString());
+    Object unused = fakeAeadKeyManager.getPrimitive(key.toByteString());
   }
 
   @Test
@@ -150,7 +150,7 @@
   public void getPrimitive_messageLite_works() throws Exception {
     KeyManager<Aead> keyManager = new KeyManagerImpl<>(new TestKeyTypeManager(), Aead.class);
     MessageLite key = keyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build());
-    keyManager.getPrimitive(key);
+    Object unused = keyManager.getPrimitive(key);
   }
 
   @Test
@@ -183,7 +183,8 @@
   @Test
   public void newKey_byteString_works() throws Exception {
     KeyManager<Aead> keyManager = new KeyManagerImpl<>(new TestKeyTypeManager(), Aead.class);
-    keyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build().toByteString());
+    Object unused =
+        keyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build().toByteString());
   }
 
   @Test
@@ -201,7 +202,7 @@
   @Test
   public void newKey_messageLite_works() throws Exception {
     KeyManager<Aead> keyManager = new KeyManagerImpl<>(new TestKeyTypeManager(), Aead.class);
-    keyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build());
+    Object unused = keyManager.newKey(AesGcmKeyFormat.newBuilder().setKeySize(16).build());
   }
 
   @Test
@@ -246,7 +247,8 @@
   @Test
   public void newKeyData_works() throws Exception {
     KeyManager<Aead> keyManager = new KeyManagerImpl<>(new TestKeyTypeManager(), Aead.class);
-    keyManager.newKeyData(AesGcmKeyFormat.newBuilder().setKeySize(16).build().toByteString());
+    Object unused =
+        keyManager.newKeyData(AesGcmKeyFormat.newBuilder().setKeySize(16).build().toByteString());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeyManagerRegistryTest.java b/java_src/src/test/java/com/google/crypto/tink/KeyManagerRegistryTest.java
index 64d0df6..3cd5802 100644
--- a/java_src/src/test/java/com/google/crypto/tink/KeyManagerRegistryTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/KeyManagerRegistryTest.java
@@ -33,7 +33,6 @@
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.MessageLite;
 import java.security.GeneralSecurityException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,22 +53,7 @@
     private final String typeUrl;
 
     @Override
-    public Primitive1 getPrimitive(ByteString proto) throws GeneralSecurityException {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
-    public Primitive1 getPrimitive(MessageLite proto) throws GeneralSecurityException {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
-    public MessageLite newKey(ByteString template) throws GeneralSecurityException {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
-    public MessageLite newKey(MessageLite template) throws GeneralSecurityException {
+    public Primitive1 getPrimitive(ByteString serializedKey) throws GeneralSecurityException {
       throw new UnsupportedOperationException("Not needed for test");
     }
 
@@ -79,21 +63,11 @@
     }
 
     @Override
-    public boolean doesSupport(String typeUrl) {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
     public String getKeyType() {
       return this.typeUrl;
     }
 
     @Override
-    public int getVersion() {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
     public Class<Primitive1> getPrimitiveClass() {
       return Primitive1.class;
     }
@@ -156,7 +130,6 @@
     KeyManagerRegistry registry = new KeyManagerRegistry();
     assertThrows(
         GeneralSecurityException.class, () -> registry.getKeyManager("customTypeUrl", Aead.class));
-    assertThrows(GeneralSecurityException.class, () -> registry.getKeyManager("customTypeUrl"));
     assertThrows(
         GeneralSecurityException.class, () -> registry.getUntypedKeyManager("customTypeUrl"));
     assertThat(registry.typeUrlExists("customTypeUrl")).isFalse();
@@ -299,50 +272,11 @@
     KeyManager<?> registered = new TestKeyManager("typeUrl");
     registry.registerKeyManager(registered);
     KeyManager<Primitive1> aeadManager1 = registry.getKeyManager("typeUrl", Primitive1.class);
-    KeyManager<Primitive1> aeadManager2 = registry.getKeyManager("typeUrl");
     KeyManager<?> manager = registry.getUntypedKeyManager("typeUrl");
     assertThat(aeadManager1).isSameInstanceAs(registered);
-    assertThat(aeadManager2).isSameInstanceAs(registered);
     assertThat(manager).isSameInstanceAs(registered);
   }
 
-  // The method "parseKeyData" only works if a KeyTypeManager was registered -- KeyManager objects
-  // do not support this.
-  @Test
-  public void testParseKeyData_keyTypeManager_works() throws Exception {
-    if (TinkFipsUtil.useOnlyFips()) {
-      assumeTrue(
-          "If FIPS is required, we can only register managers if the fips module is available",
-          TinkFipsUtil.fipsModuleAvailable());
-    }
-
-    KeyManagerRegistry registry = new KeyManagerRegistry();
-    registry.registerKeyManager(new TestKeyTypeManager("typeUrl"));
-    AesGcmKey key = AesGcmKey.newBuilder().setVersion(13).build();
-    KeyData keyData =
-        KeyData.newBuilder()
-            .setTypeUrl("typeUrl")
-            .setValue(key.toByteString())
-            .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
-            .build();
-    assertThat(registry.parseKeyData(keyData)).isEqualTo(key);
-  }
-
-  @Test
-  public void testParseKeyData_keyManager_returnsNull() throws Exception {
-    assumeFalse("Unable to test KeyManagers in Fips mode", TinkFipsUtil.useOnlyFips());
-    KeyManagerRegistry registry = new KeyManagerRegistry();
-    registry.registerKeyManager(new TestKeyManager("typeUrl"));
-    AesGcmKey key = AesGcmKey.newBuilder().setVersion(13).build();
-    KeyData keyData =
-        KeyData.newBuilder()
-            .setTypeUrl("typeUrl")
-            .setValue(key.toByteString())
-            .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
-            .build();
-    assertThat(registry.parseKeyData(keyData)).isNull();
-  }
-
   private static class TestPublicKeyTypeManager extends KeyTypeManager<Ed25519PublicKey> {
     private final String typeUrl;
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeyTemplateTest.java b/java_src/src/test/java/com/google/crypto/tink/KeyTemplateTest.java
new file mode 100644
index 0000000..2a36dfb
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/KeyTemplateTest.java
@@ -0,0 +1,96 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.AesGcmParameters;
+import com.google.crypto.tink.internal.LegacyProtoParameters;
+import com.google.crypto.tink.proto.AesGcmKeyFormat;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class KeyTemplateTest {
+  private static final String AES_GCM_TYPE_URL = "type.googleapis.com/google.crypto.tink.AesGcmKey";
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+  }
+
+  @Test
+  public void testToParameters_aesGcm_works() throws Exception {
+    AesGcmKeyFormat format = AesGcmKeyFormat.newBuilder().setKeySize(16).build();
+
+    assertThat(
+            KeyTemplate.create(
+                    AES_GCM_TYPE_URL, format.toByteArray(), KeyTemplate.OutputPrefixType.RAW)
+                .toParameters())
+        .isEqualTo(
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(12)
+                .setTagSizeBytes(16)
+                .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+                .build());
+
+    assertThat(
+            KeyTemplate.create(
+                    AES_GCM_TYPE_URL, format.toByteArray(), KeyTemplate.OutputPrefixType.TINK)
+                .toParameters())
+        .isEqualTo(
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(12)
+                .setTagSizeBytes(16)
+                .setVariant(AesGcmParameters.Variant.TINK)
+                .build());
+  }
+
+  @Test
+  public void testToParameters_unParseableAesGcm_fails() throws Exception {
+    // Invalid Key size
+    AesGcmKeyFormat format = AesGcmKeyFormat.newBuilder().setKeySize(17).build();
+    KeyTemplate template =
+        KeyTemplate.create(
+            AES_GCM_TYPE_URL, format.toByteArray(), KeyTemplate.OutputPrefixType.RAW);
+    assertThrows(GeneralSecurityException.class, template::toParameters);
+  }
+
+  @Test
+  public void testToParameters_notRegisteredTypeUrl_givesLegacy() throws Exception {
+    Parameters p =
+        KeyTemplate.create("nonexistenttypeurl", new byte[] {1}, KeyTemplate.OutputPrefixType.TINK)
+            .toParameters();
+    assertThat(p).isInstanceOf(LegacyProtoParameters.class);
+    LegacyProtoParameters parameters = (LegacyProtoParameters) p;
+    assertThat(parameters.getSerialization().getKeyTemplate().getTypeUrl())
+        .isEqualTo("nonexistenttypeurl");
+    assertThat(parameters.getSerialization().getKeyTemplate().getValue())
+        .isEqualTo(ByteString.copyFrom(new byte[] {1}));
+    assertThat(parameters.getSerialization().getKeyTemplate().getOutputPrefixType())
+        .isEqualTo(OutputPrefixType.TINK);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeyTemplatesAsParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/KeyTemplatesAsParametersTest.java
new file mode 100644
index 0000000..c4684dc
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/KeyTemplatesAsParametersTest.java
@@ -0,0 +1,496 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+
+import com.google.crypto.tink.aead.AesCtrHmacAeadParameters;
+import com.google.crypto.tink.aead.AesEaxParameters;
+import com.google.crypto.tink.aead.AesGcmParameters;
+import com.google.crypto.tink.aead.AesGcmSivParameters;
+import com.google.crypto.tink.aead.ChaCha20Poly1305Parameters;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.aead.XChaCha20Poly1305Parameters;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.daead.AesSivParameters;
+import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
+import com.google.crypto.tink.internal.Util;
+import com.google.crypto.tink.mac.AesCmacParameters;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import com.google.crypto.tink.prf.PredefinedPrfParameters;
+import com.google.crypto.tink.signature.EcdsaParameters;
+import com.google.crypto.tink.signature.PredefinedSignatureParameters;
+import com.google.crypto.tink.signature.RsaSsaPkcs1Parameters;
+import com.google.crypto.tink.signature.RsaSsaPssParameters;
+import com.google.crypto.tink.streamingaead.PredefinedStreamingAeadParameters;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * This test compares all KeyTemplates (available via {@code KeyTemplates.get("SomeString")} to
+ * corresponding parameters objects.
+ *
+ * <p>This can be used to find {@link Parameters} object which correspond to results of {@code
+ * KeyTemplates.get(s)} for a given string {@code s}: simply check the list in {@code TEMPLATES}
+ * below: if a pair {@code (s,p)} is in this list, this means that {@code
+ * KeyTemplates.get(s).toParameters()} is equal to {@code p}.
+ */
+@RunWith(Theories.class)
+public final class KeyTemplatesAsParametersTest {
+  public static final class Pair {
+    Pair(String templateName, Parameters parameters) {
+      this.templateName = templateName;
+      this.parameters = parameters;
+    }
+
+    String templateName;
+    Parameters parameters;
+
+    @Override
+    public String toString() {
+      return templateName + ":" + parameters;
+    }
+  }
+
+  @BeforeClass
+  public static void registerTink() throws Exception {
+    TinkConfig.register();
+  }
+
+  private static final List<Pair> createTemplates() throws GeneralSecurityException {
+    List<Pair> result = new ArrayList<>();
+    // Aead
+    result.add(new Pair("AES128_GCM", PredefinedAeadParameters.AES128_GCM));
+    result.add(new Pair("AES256_GCM", PredefinedAeadParameters.AES256_GCM));
+    result.add(
+        new Pair(
+            "AES128_GCM_RAW",
+            AesGcmParameters.builder()
+                .setIvSizeBytes(12)
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "AES256_GCM_RAW",
+            AesGcmParameters.builder()
+                .setIvSizeBytes(12)
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(new Pair("AES128_EAX", PredefinedAeadParameters.AES128_EAX));
+    result.add(new Pair("AES256_EAX", PredefinedAeadParameters.AES256_EAX));
+    result.add(
+        new Pair(
+            "AES128_EAX_RAW",
+            AesEaxParameters.builder()
+                .setIvSizeBytes(16)
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "AES256_EAX_RAW",
+            AesEaxParameters.builder()
+                .setIvSizeBytes(16)
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(new Pair("AES128_CTR_HMAC_SHA256", PredefinedAeadParameters.AES128_CTR_HMAC_SHA256));
+    result.add(new Pair("AES256_CTR_HMAC_SHA256", PredefinedAeadParameters.AES256_CTR_HMAC_SHA256));
+    result.add(
+        new Pair(
+            "AES128_CTR_HMAC_SHA256_RAW",
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "AES256_CTR_HMAC_SHA256_RAW",
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(32)
+                .setHmacKeySizeBytes(32)
+                .setTagSizeBytes(32)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(new Pair("CHACHA20_POLY1305", PredefinedAeadParameters.CHACHA20_POLY1305));
+    result.add(
+        new Pair(
+            "CHACHA20_POLY1305_RAW",
+            ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.NO_PREFIX)));
+    result.add(new Pair("XCHACHA20_POLY1305", PredefinedAeadParameters.XCHACHA20_POLY1305));
+    result.add(
+        new Pair(
+            "XCHACHA20_POLY1305_RAW",
+            XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.NO_PREFIX)));
+    // Mac
+    result.add(new Pair("HMAC_SHA256_128BITTAG", PredefinedMacParameters.HMAC_SHA256_128BITTAG));
+    result.add(
+        new Pair(
+            "HMAC_SHA256_128BITTAG_RAW",
+            HmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .build()));
+    result.add(
+        new Pair(
+            "HMAC_SHA256_256BITTAG_RAW",
+            HmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(32)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .build()));
+    result.add(
+        new Pair(
+            "HMAC_SHA512_128BITTAG",
+            HmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setTagSizeBytes(16)
+                .setVariant(HmacParameters.Variant.TINK)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .build()));
+    result.add(
+        new Pair(
+            "HMAC_SHA512_128BITTAG_RAW",
+            HmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setTagSizeBytes(16)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .build()));
+    result.add(
+        new Pair(
+            "HMAC_SHA512_256BITTAG_RAW",
+            HmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setTagSizeBytes(32)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .build()));
+    result.add(
+        new Pair(
+            "HMAC_SHA512_512BITTAG_RAW",
+            HmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setTagSizeBytes(64)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .build()));
+    result.add(new Pair("HMAC_SHA256_256BITTAG", PredefinedMacParameters.HMAC_SHA256_256BITTAG));
+    result.add(new Pair("HMAC_SHA512_256BITTAG", PredefinedMacParameters.HMAC_SHA512_256BITTAG));
+    result.add(new Pair("HMAC_SHA512_512BITTAG", PredefinedMacParameters.HMAC_SHA512_512BITTAG));
+    result.add(new Pair("AES_CMAC", PredefinedMacParameters.AES_CMAC));
+    result.add(new Pair("AES256_CMAC", PredefinedMacParameters.AES_CMAC));
+    result.add(
+        new Pair(
+            "AES256_CMAC_RAW",
+            AesCmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(AesCmacParameters.Variant.NO_PREFIX)
+                .build()));
+    // DeterministicAead
+    result.add(new Pair("AES256_SIV", PredefinedDeterministicAeadParameters.AES256_SIV));
+    result.add(
+        new Pair(
+            "AES256_SIV_RAW",
+            AesSivParameters.builder()
+                .setKeySizeBytes(64)
+                .setVariant(AesSivParameters.Variant.NO_PREFIX)
+                .build()));
+    // StreamingAead
+    result.add(
+        new Pair(
+            "AES128_CTR_HMAC_SHA256_4KB",
+            PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_4KB));
+    result.add(
+        new Pair(
+            "AES128_CTR_HMAC_SHA256_1MB",
+            PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_1MB));
+    result.add(
+        new Pair(
+            "AES256_CTR_HMAC_SHA256_4KB",
+            PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_4KB));
+    result.add(
+        new Pair(
+            "AES256_CTR_HMAC_SHA256_1MB",
+            PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_1MB));
+    result.add(
+        new Pair("AES128_GCM_HKDF_4KB", PredefinedStreamingAeadParameters.AES128_GCM_HKDF_4KB));
+    result.add(
+        new Pair("AES128_GCM_HKDF_1MB", PredefinedStreamingAeadParameters.AES128_GCM_HKDF_1MB));
+    result.add(
+        new Pair("AES256_GCM_HKDF_4KB", PredefinedStreamingAeadParameters.AES256_GCM_HKDF_4KB));
+    result.add(
+        new Pair("AES256_GCM_HKDF_1MB", PredefinedStreamingAeadParameters.AES256_GCM_HKDF_1MB));
+    // Prf
+    result.add(new Pair("HKDF_SHA256", PredefinedPrfParameters.HKDF_SHA256));
+    result.add(new Pair("HMAC_SHA256_PRF", PredefinedPrfParameters.HMAC_SHA256_PRF));
+    result.add(new Pair("HMAC_SHA512_PRF", PredefinedPrfParameters.HMAC_SHA512_PRF));
+    result.add(new Pair("AES256_CMAC_PRF", PredefinedPrfParameters.AES_CMAC_PRF));
+    result.add(new Pair("AES_CMAC_PRF", PredefinedPrfParameters.AES_CMAC_PRF));
+
+    result.add(new Pair("ECDSA_P256", PredefinedSignatureParameters.ECDSA_P256));
+    result.add(
+        new Pair("ECDSA_P256_IEEE_P1363", PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363));
+    result.add(
+        new Pair(
+            "ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX",
+            PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX));
+    result.add(
+        new Pair(
+            "ECDSA_P256_RAW",
+            EcdsaParameters.builder()
+                .setHashType(EcdsaParameters.HashType.SHA256)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(new Pair("ECDSA_P384", PredefinedSignatureParameters.ECDSA_P384));
+    result.add(
+        new Pair("ECDSA_P384_IEEE_P1363", PredefinedSignatureParameters.ECDSA_P384_IEEE_P1363));
+    result.add(
+        new Pair(
+            "ECDSA_P384_SHA384",
+            EcdsaParameters.builder()
+                .setHashType(EcdsaParameters.HashType.SHA384)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                .setVariant(EcdsaParameters.Variant.TINK)
+                .build()));
+    result.add(
+        new Pair(
+            "ECDSA_P384_SHA512",
+            EcdsaParameters.builder()
+                .setHashType(EcdsaParameters.HashType.SHA512)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                .setVariant(EcdsaParameters.Variant.TINK)
+                .build()));
+    result.add(new Pair("ECDSA_P521", PredefinedSignatureParameters.ECDSA_P521));
+    result.add(
+        new Pair("ECDSA_P521_IEEE_P1363", PredefinedSignatureParameters.ECDSA_P521_IEEE_P1363));
+    result.add(new Pair("ED25519", PredefinedSignatureParameters.ED25519));
+    result.add(new Pair("ED25519_RAW", PredefinedSignatureParameters.ED25519WithRawOutput));
+    result.add(
+        new Pair("ED25519WithRawOutput", PredefinedSignatureParameters.ED25519WithRawOutput));
+    result.add(
+        new Pair(
+            "RSA_SSA_PKCS1_3072_SHA256_F4",
+            PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4));
+    result.add(
+        new Pair(
+            "RSA_SSA_PKCS1_3072_SHA256_F4_RAW",
+            RsaSsaPkcs1Parameters.builder()
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX",
+            PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX));
+    result.add(
+        new Pair(
+            "RSA_SSA_PKCS1_4096_SHA512_F4",
+            PredefinedSignatureParameters.RSA_SSA_PKCS1_4096_SHA512_F4));
+    result.add(
+        new Pair(
+            "RSA_SSA_PKCS1_4096_SHA512_F4_RAW",
+            RsaSsaPkcs1Parameters.builder()
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "RSA_SSA_PSS_3072_SHA256_F4",
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setSaltLengthBytes(32)
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.TINK)
+                .build()));
+    result.add(
+        new Pair(
+            "RSA_SSA_PSS_3072_SHA256_F4_RAW",
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setSaltLengthBytes(32)
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "RSA_SSA_PSS_3072_SHA256_SHA256_32_F4",
+            PredefinedSignatureParameters.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4));
+    result.add(
+        new Pair(
+            "RSA_SSA_PSS_4096_SHA512_F4",
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                .setSaltLengthBytes(64)
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.TINK)
+                .build()));
+    result.add(
+        new Pair(
+            "RSA_SSA_PSS_4096_SHA512_F4_RAW",
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                .setSaltLengthBytes(64)
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .build()));
+    result.add(
+        new Pair(
+            "RSA_SSA_PSS_4096_SHA512_SHA512_64_F4",
+            PredefinedSignatureParameters.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4));
+
+    if (Util.isAndroid()) {
+      result.add(
+          new Pair(
+              "AES128_GCM_SIV",
+              AesGcmSivParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setVariant(AesGcmSivParameters.Variant.TINK)
+                  .build()));
+      result.add(
+          new Pair(
+              "AES128_GCM_SIV_RAW",
+              AesGcmSivParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+                  .build()));
+      result.add(
+          new Pair(
+              "AES256_GCM_SIV",
+              AesGcmSivParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setVariant(AesGcmSivParameters.Variant.TINK)
+                  .build()));
+      result.add(
+          new Pair(
+              "AES256_GCM_SIV_RAW",
+              AesGcmSivParameters.builder()
+                  .setKeySizeBytes(32)
+                  .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+                  .build()));
+    }
+    return result;
+  }
+
+  @DataPoints("EquivalentPairs")
+  public static final List<Pair> templates = exceptionIsBug(() -> createTemplates());
+
+  @Theory
+  public void testParametersEqualsKeyTemplate(@FromDataPoints("EquivalentPairs") Pair p)
+      throws Exception {
+    assertThat(KeyTemplates.get(p.templateName).toParameters()).isEqualTo(p.parameters);
+  }
+
+  private static Set<String> getAllTestedNames() {
+    Set<String> result = new HashSet<>();
+    for (Pair p : templates) {
+      result.add(p.templateName);
+    }
+    return result;
+  }
+
+  private static Set<String> getUntestedNames() {
+    Set<String> result = new HashSet<>();
+    result.add("DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM");
+    result.add("DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW");
+    result.add("DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM");
+    result.add("DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW");
+    result.add("DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM");
+    result.add("DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM_RAW");
+    result.add("DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM");
+    result.add("DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM_RAW");
+    result.add("DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM");
+    result.add("DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM_RAW");
+    result.add("DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM");
+    result.add("DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM_RAW");
+    result.add("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM");
+    result.add("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW");
+    result.add("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM");
+    result.add("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW");
+    result.add("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305");
+    result.add("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW");
+    result.add("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256");
+    result.add("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256_RAW");
+    result.add("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM");
+    result.add("ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM_RAW");
+    result.add("ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256");
+    result.add("ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256_RAW");
+    result.add("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM");
+    result.add("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM_COMPRESSED_WITHOUT_PREFIX");
+    result.add("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM_RAW");
+    return result;
+  }
+
+  /** Tests that we test all available names */
+  @Test
+  public void testCompletenessOfThisTest() throws Exception {
+    Set<String> testedNames = getAllTestedNames();
+    Set<String> untestedNames = getUntestedNames();
+
+    // Note that this means the two sets do not intersect.
+    assertThat(testedNames).containsNoneIn(untestedNames);
+
+    Set<String> testedPlusMissing = new HashSet<>();
+    testedPlusMissing.addAll(testedNames);
+    testedPlusMissing.addAll(untestedNames);
+    assertThat(Registry.keyTemplateMap().keySet()).containsExactlyElementsIn(testedPlusMissing);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeysetHandleFullPrimitiveTest.java b/java_src/src/test/java/com/google/crypto/tink/KeysetHandleFullPrimitiveTest.java
new file mode 100644
index 0000000..d6eb4c1
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/KeysetHandleFullPrimitiveTest.java
@@ -0,0 +1,351 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.PrimitiveSet.Entry;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
+import com.google.crypto.tink.internal.MutablePrimitiveRegistry;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.PrimitiveConstructor;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.AesCmacParameters;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.PrfAesCmac;
+import com.google.crypto.tink.subtle.PrfMac;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests how {@link KeysetHandle} handles the situation when there is a {@link
+ * PrimitiveSet#fullPrimitive} registered.
+ */
+@RunWith(JUnit4.class)
+public class KeysetHandleFullPrimitiveTest {
+
+  private static final PrimitiveConstructor<TestKey, SingleTestPrimitive>
+      TEST_PRIMITIVE_CONSTRUCTOR =
+          PrimitiveConstructor.create(
+              KeysetHandleFullPrimitiveTest::createTestPrimitive,
+              TestKey.class,
+              SingleTestPrimitive.class);
+  public static final KeySerializer<TestKey, ProtoKeySerialization> TEST_KEY_SERIALIZER =
+      KeySerializer.create(
+          KeysetHandleFullPrimitiveTest::serializeTestKey,
+          TestKey.class,
+          ProtoKeySerialization.class);
+  public static final KeyParser<ProtoKeySerialization> TEST_KEY_PARSER =
+      KeyParser.create(
+          KeysetHandleFullPrimitiveTest::parseTestKey,
+          Bytes.copyFrom("testKeyForFullPrimitiveUrl".getBytes(UTF_8)),
+          ProtoKeySerialization.class);
+  private static final PrimitiveConstructor<AesCmacKey, Mac> MAC_PRIMITIVE_CONSTRUCTOR =
+      PrimitiveConstructor.create(
+          KeysetHandleFullPrimitiveTest::createMacPrimitive, AesCmacKey.class, Mac.class);
+
+  private static class SingleTestPrimitive {}
+
+  /**
+   * In combination with {@link KeysetHandleFullPrimitiveTest#TestWrapper}, this test primitive lets
+   * us check some assumptions about the {@link PrimitiveSet} which was created by the {@link
+   * KeysetHandle}.
+   */
+  private static class WrappedTestPrimitive {
+
+    private final PrimitiveSet<SingleTestPrimitive> primitiveSet;
+
+    WrappedTestPrimitive(PrimitiveSet<SingleTestPrimitive> primitiveSet) {
+      this.primitiveSet = primitiveSet;
+    }
+
+    PrimitiveSet<SingleTestPrimitive> getPrimitiveSet() {
+      return primitiveSet;
+    }
+  }
+
+  private static class TestWrapper
+      implements PrimitiveWrapper<SingleTestPrimitive, WrappedTestPrimitive> {
+
+    private static final TestWrapper WRAPPER = new TestWrapper();
+
+    @Override
+    public WrappedTestPrimitive wrap(PrimitiveSet<SingleTestPrimitive> primitiveSet)
+        throws GeneralSecurityException {
+      return new WrappedTestPrimitive(primitiveSet);
+    }
+
+    @Override
+    public Class<WrappedTestPrimitive> getPrimitiveClass() {
+      return WrappedTestPrimitive.class;
+    }
+
+    @Override
+    public Class<SingleTestPrimitive> getInputPrimitiveClass() {
+      return SingleTestPrimitive.class;
+    }
+
+    public static void register() throws GeneralSecurityException {
+      Registry.registerPrimitiveWrapper(WRAPPER);
+    }
+  }
+
+  private static final class TestKey extends Key {
+
+    private final int id;
+
+    TestKey(int id) {
+      this.id = id;
+    }
+
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public Integer getIdRequirementOrNull() {
+      return id;
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    public int getId() {
+      return id;
+    }
+  }
+
+  private static ProtoKeySerialization serializeTestKey(
+      TestKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        "testKeyForFullPrimitiveUrl",
+        ByteString.EMPTY,
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        OutputPrefixType.TINK,
+        key.getId());
+  }
+
+  private static TestKey parseTestKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access) {
+    return new TestKey(serialization.getIdRequirementOrNull());
+  }
+
+  private static SingleTestPrimitive createTestPrimitive(TestKey key)
+      throws GeneralSecurityException {
+    return new SingleTestPrimitive();
+  }
+
+  /**
+   * In combination with {@link KeysetHandleFullPrimitiveTest#MacTestWrapper}, this test primitive
+   * lets us check some assumptions about the {@link PrimitiveSet} which was created by the {@link
+   * KeysetHandle}.
+   */
+  private static class WrappedMacTestPrimitive {
+
+    private final PrimitiveSet<Mac> primitiveSet;
+
+    WrappedMacTestPrimitive(PrimitiveSet<Mac> primitiveSet) {
+      this.primitiveSet = primitiveSet;
+    }
+
+    PrimitiveSet<Mac> getPrimitiveSet() {
+      return primitiveSet;
+    }
+  }
+
+  private static class MacTestWrapper implements PrimitiveWrapper<Mac, WrappedMacTestPrimitive> {
+
+    private static final MacTestWrapper WRAPPER = new MacTestWrapper();
+
+    @Override
+    public WrappedMacTestPrimitive wrap(PrimitiveSet<Mac> primitiveSet)
+        throws GeneralSecurityException {
+      return new WrappedMacTestPrimitive(primitiveSet);
+    }
+
+    @Override
+    public Class<WrappedMacTestPrimitive> getPrimitiveClass() {
+      return WrappedMacTestPrimitive.class;
+    }
+
+    @Override
+    public Class<Mac> getInputPrimitiveClass() {
+      return Mac.class;
+    }
+
+    public static void register() throws GeneralSecurityException {
+      Registry.registerPrimitiveWrapper(WRAPPER);
+    }
+  }
+
+  private static Mac createMacPrimitive(AesCmacKey key) throws GeneralSecurityException {
+    return new PrfMac(
+        new PrfAesCmac(key.getAesKey().toByteArray(InsecureSecretKeyAccess.get())),
+        key.getParameters().getTotalTagSizeBytes());
+  }
+
+  @Test
+  public void getPrimitive_fullPrimitiveWithoutPrimitive_worksCorrectly() throws Exception {
+    Registry.reset();
+    MutableSerializationRegistry.globalInstance().registerKeySerializer(TEST_KEY_SERIALIZER);
+    MutableSerializationRegistry.globalInstance().registerKeyParser(TEST_KEY_PARSER);
+    MutablePrimitiveRegistry.globalInstance()
+        .registerPrimitiveConstructor(TEST_PRIMITIVE_CONSTRUCTOR);
+    TestWrapper.register();
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1234))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1234)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1235))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1235))
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1236))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1236))
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1237))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1237))
+            .build();
+
+    WrappedTestPrimitive primitive = keysetHandle.getPrimitive(WrappedTestPrimitive.class);
+
+    for (List<Entry<SingleTestPrimitive>> list : primitive.getPrimitiveSet().getAll()) {
+      for (PrimitiveSet.Entry<SingleTestPrimitive> entry : list) {
+        assertThat(entry.getFullPrimitive()).isNotNull();
+        assertThat(entry.getPrimitive()).isNull();
+      }
+    }
+  }
+
+  @Test
+  public void getPrimitive_noFullPrimitiveNoPrimitive_throws() throws Exception {
+    Registry.reset();
+    MutableSerializationRegistry.globalInstance().registerKeySerializer(TEST_KEY_SERIALIZER);
+    MutableSerializationRegistry.globalInstance().registerKeyParser(TEST_KEY_PARSER);
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1234))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1234)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1235))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1235))
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1236))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1236))
+            .addEntry(
+                KeysetHandle.importKey(new TestKey(1237))
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1237))
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> keysetHandle.getPrimitive(WrappedTestPrimitive.class));
+  }
+
+  @Test
+  public void getPrimitive_fullPrimitiveWithPrimitive_worksCorrectly() throws Exception {
+    Registry.reset();
+    MutablePrimitiveRegistry.globalInstance()
+        .registerPrimitiveConstructor(MAC_PRIMITIVE_CONSTRUCTOR);
+    MacTestWrapper.register();
+    MacConfig.register();
+    AesCmacParameters parameters =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setVariant(Variant.TINK)
+            .build();
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(
+                        AesCmacKey.builder()
+                            .setParameters(parameters)
+                            .setAesKeyBytes(SecretBytes.randomBytes(32))
+                            .setIdRequirement(1234)
+                            .build())
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1234)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.importKey(
+                        AesCmacKey.builder()
+                            .setParameters(parameters)
+                            .setAesKeyBytes(SecretBytes.randomBytes(32))
+                            .setIdRequirement(1235)
+                            .build())
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1235))
+            .addEntry(
+                KeysetHandle.importKey(
+                        AesCmacKey.builder()
+                            .setParameters(parameters)
+                            .setAesKeyBytes(SecretBytes.randomBytes(32))
+                            .setIdRequirement(1236)
+                            .build())
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1236))
+            .addEntry(
+                KeysetHandle.importKey(
+                        AesCmacKey.builder()
+                            .setParameters(parameters)
+                            .setAesKeyBytes(SecretBytes.randomBytes(32))
+                            .setIdRequirement(1237)
+                            .build())
+                    .setStatus(KeyStatus.ENABLED)
+                    .withFixedId(1237))
+            .build();
+
+    WrappedMacTestPrimitive primitive = keysetHandle.getPrimitive(WrappedMacTestPrimitive.class);
+
+    for (List<Entry<Mac>> list : primitive.getPrimitiveSet().getAll()) {
+      for (PrimitiveSet.Entry<Mac> entry : list) {
+        assertThat(entry.getFullPrimitive()).isNotNull();
+        assertThat(entry.getPrimitive()).isNotNull();
+      }
+    }
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java b/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
index 50caf74..6e66849 100644
--- a/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
@@ -22,31 +22,43 @@
 import static org.junit.Assume.assumeFalse;
 
 import com.google.common.truth.Expect;
+import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.aead.AesEaxKeyManager;
-import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.internal.InternalConfiguration;
 import com.google.crypto.tink.internal.KeyParser;
 import com.google.crypto.tink.internal.KeyStatusTypeProtoConverter;
 import com.google.crypto.tink.internal.LegacyProtoKey;
 import com.google.crypto.tink.internal.MonitoringUtil;
 import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.internal.MutablePrimitiveRegistry;
 import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.PrimitiveConstructor;
+import com.google.crypto.tink.internal.PrimitiveRegistry;
 import com.google.crypto.tink.internal.ProtoKeySerialization;
 import com.google.crypto.tink.internal.testing.FakeMonitoringClient;
 import com.google.crypto.tink.mac.AesCmacKey;
 import com.google.crypto.tink.mac.AesCmacParameters;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.mac.ChunkedMac;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.mac.MacConfig;
 import com.google.crypto.tink.monitoring.MonitoringAnnotations;
 import com.google.crypto.tink.monitoring.MonitoringClient;
+import com.google.crypto.tink.prf.PrfConfig;
 import com.google.crypto.tink.proto.AesEaxKey;
-import com.google.crypto.tink.proto.AesEaxKeyFormat;
 import com.google.crypto.tink.proto.EcdsaPrivateKey;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HmacParams;
+import com.google.crypto.tink.proto.HmacPrfKey;
+import com.google.crypto.tink.proto.HmacPrfParams;
 import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.signature.PublicKeySignFactory;
-import com.google.crypto.tink.signature.PublicKeyVerifyFactory;
 import com.google.crypto.tink.signature.SignatureConfig;
-import com.google.crypto.tink.signature.SignatureKeyTemplates;
 import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
@@ -61,6 +73,7 @@
 import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import java.util.HashSet;
 import java.util.List;
@@ -75,8 +88,16 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests for KeysetHandle. */
+/**
+ * Tests for {@link KeysetHandle}.
+ *
+ * <p>Please note, that in relation to the {@link PrimitiveSet#fullPrimitive} this file only tests
+ * the legacy scenario where the {@link PrimitiveSet#primitive} is set and {@link
+ * PrimitiveSet#fullPrimitive} isn't; the other scenarios are tested in
+ * {@link KeysetHandleFullPrimitiveTest}.
+ */
 @RunWith(JUnit4.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
 public class KeysetHandleTest {
 
   @Rule public final Expect expect = Expect.create();
@@ -86,6 +107,9 @@
   }
 
   private static class AeadToEncryptOnlyWrapper implements PrimitiveWrapper<Aead, EncryptOnly> {
+
+    private static final AeadToEncryptOnlyWrapper WRAPPER = new AeadToEncryptOnlyWrapper();
+
     private static class EncryptOnlyWithMonitoring implements EncryptOnly {
 
       private final MonitoringClient.Logger logger;
@@ -120,28 +144,64 @@
     public Class<Aead> getInputPrimitiveClass() {
       return Aead.class;
     }
+
+    static void register() throws GeneralSecurityException {
+      Registry.registerPrimitiveWrapper(WRAPPER);
+    }
   }
 
+  private static final int HMAC_KEY_SIZE = 20;
+  private static final int HMAC_TAG_SIZE = 10;
+
+  private static HmacKey rawKey;
+
   @BeforeClass
   public static void setUp() throws GeneralSecurityException {
-    Config.register(TinkConfig.TINK_1_0_0);
-    Registry.registerPrimitiveWrapper(new AeadToEncryptOnlyWrapper());
+    MacConfig.register();
+    AeadConfig.register();
+    PrfConfig.register();
+    SignatureConfig.register();
+    AeadToEncryptOnlyWrapper.register();
+
+    createTestKeys();
   }
 
-  @Test
-  public void getKeys() throws Exception {
-    KeyTemplate keyTemplate = KeyTemplates.get("AES128_EAX");
-    KeysetManager keysetManager = KeysetManager.withEmptyKeyset();
-    final int numKeys = 3;
-    for (int i = 0; i < numKeys; i++) {
-      keysetManager.add(keyTemplate);
+  private static void createTestKeys() {
+    try {
+      rawKey =
+          HmacKey.builder()
+              .setParameters(
+                  HmacParameters.builder()
+                      .setKeySizeBytes(HMAC_KEY_SIZE)
+                      .setTagSizeBytes(HMAC_TAG_SIZE)
+                      .setVariant(HmacParameters.Variant.NO_PREFIX)
+                      .setHashType(HmacParameters.HashType.SHA256)
+                      .build())
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
     }
-    KeysetHandle handle = keysetManager.getKeysetHandle();
+  }
+
+  @SuppressWarnings("deprecation") // This is a test for the deprecated function
+  @Test
+  public void deprecated_getKeys() throws Exception {
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.generateEntryFromParametersName("AES128_EAX").withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_EAX")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(KeysetHandle.generateEntryFromParametersName("AES128_EAX").withRandomId())
+            .build();
     Keyset keyset = handle.getKeyset();
 
     List<KeyHandle> keysetKeys = handle.getKeys();
 
-    expect.that(keysetKeys).hasSize(numKeys);
+    expect.that(keysetKeys).hasSize(3);
     Map<Integer, KeyHandle> keysetKeysMap =
         keysetKeys.stream().collect(Collectors.toMap(KeyHandle::getId, key -> key));
     for (Keyset.Key key : keyset.getKeyList()) {
@@ -157,46 +217,62 @@
   }
 
   @Test
-  public void generateNew_shouldWork() throws Exception {
+  public void generateNew_tink_shouldWork() throws Exception {
     KeyTemplate template = KeyTemplates.get("AES128_EAX");
 
     KeysetHandle handle = KeysetHandle.generateNew(template);
 
-    Keyset keyset = handle.getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(1);
-    Keyset.Key key = keyset.getKey(0);
-    expect.that(keyset.getPrimaryKeyId()).isEqualTo(key.getKeyId());
-    expect.that(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key.hasKeyData()).isTrue();
-    expect.that(key.getKeyData().getTypeUrl()).isEqualTo(template.getTypeUrl());
-    AesEaxKeyFormat aesEaxKeyFormat =
-        AesEaxKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesEaxKey aesEaxKey =
-        AesEaxKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesEaxKey.getKeyValue().size()).isEqualTo(aesEaxKeyFormat.getKeySize());
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getAt(0).getKey().getParameters()).isEqualTo(template.toParameters());
+  }
+
+  @Test
+  public void testKeysetHandleGenerateNew_parameters_works() throws Exception {
+    AesCmacParameters parameters =
+        AesCmacParameters.builder()
+            .setVariant(Variant.CRUNCHY)
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(16)
+            .build();
+    KeysetHandle h = KeysetHandle.generateNew(parameters);
+    assertThat(h.size()).isEqualTo(1);
+    assertThat(h.getAt(0).getKey().getParameters()).isEqualTo(parameters);
+  }
+
+  @Test
+  public void testKeysetHandleGenerateNew_parameters_fails() throws Exception {
+    Parameters p =
+        new Parameters() {
+          @Override
+          public boolean hasIdRequirement() {
+            return false;
+          }
+        };
+
+    assertThrows(GeneralSecurityException.class, () -> KeysetHandle.generateNew(p));
+  }
+
+
+  @Test
+  public void generateNew_raw_shouldWork() throws Exception {
+    KeyTemplate template = KeyTemplates.get("AES128_EAX_RAW");
+
+    KeysetHandle handle = KeysetHandle.generateNew(template);
+
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getAt(0).getKey().getParameters()).isEqualTo(template.toParameters());
   }
 
   @Test
   public void generateNew_withProtoKeyTemplate_shouldWork() throws Exception {
-    com.google.crypto.tink.proto.KeyTemplate template = KeyTemplates.get("AES128_EAX").getProto();
+    KeyTemplate template = KeyTemplates.get("AES128_EAX");
+    com.google.crypto.tink.proto.KeyTemplate protoTemplate = template.getProto();
 
     @SuppressWarnings("deprecation") // Need to test the deprecated function
-    KeysetHandle handle = KeysetHandle.generateNew(template);
+    KeysetHandle handle = KeysetHandle.generateNew(protoTemplate);
 
-    Keyset keyset = handle.getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(1);
-    Keyset.Key key = keyset.getKey(0);
-    expect.that(keyset.getPrimaryKeyId()).isEqualTo(key.getKeyId());
-    expect.that(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key.hasKeyData()).isTrue();
-    expect.that(key.getKeyData().getTypeUrl()).isEqualTo(template.getTypeUrl());
-    AesEaxKeyFormat aesEaxKeyFormat =
-        AesEaxKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesEaxKey aesEaxKey =
-        AesEaxKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesEaxKey.getKeyValue().size()).isEqualTo(aesEaxKeyFormat.getKeySize());
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getAt(0).getKey().getParameters()).isEqualTo(template.toParameters());
   }
 
   @Test
@@ -217,27 +293,17 @@
     assertThat(keys).hasSize(numKeys);
   }
 
+  @SuppressWarnings("deprecation")  // This is a test for the deprecated function
   @Test
-  public void createFromKey_shouldWork() throws Exception {
+  public void deprecated_createFromKey_shouldWork() throws Exception {
     KeyTemplate template = KeyTemplates.get("AES128_EAX");
     KeyHandle keyHandle = KeyHandle.generateNew(template);
     KeyAccess token = SecretKeyAccess.insecureSecretAccess();
 
     KeysetHandle handle = KeysetHandle.createFromKey(keyHandle, token);
 
-    Keyset keyset = handle.getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(1);
-    Keyset.Key key = keyset.getKey(0);
-    expect.that(keyset.getPrimaryKeyId()).isEqualTo(key.getKeyId());
-    expect.that(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key.hasKeyData()).isTrue();
-    expect.that(key.getKeyData().getTypeUrl()).isEqualTo(template.getTypeUrl());
-    AesEaxKeyFormat aesEaxKeyFormat =
-        AesEaxKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesEaxKey aesEaxKey =
-        AesEaxKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesEaxKey.getKeyValue().size()).isEqualTo(aesEaxKeyFormat.getKeySize());
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getAt(0).getKey().getParameters()).isEqualTo(template.toParameters());
   }
 
   @Test
@@ -345,7 +411,7 @@
 
   @Test
   public void getPublicKeysetHandle_shouldWork() throws Exception {
-    KeysetHandle privateHandle = KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256);
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
     KeyData privateKeyData = privateHandle.getKeyset().getKey(0).getKeyData();
     EcdsaPrivateKey privateKey =
         EcdsaPrivateKey.parseFrom(
@@ -365,8 +431,8 @@
     expect
         .that(publicKeyData.getValue().toByteArray())
         .isEqualTo(privateKey.getPublicKey().toByteArray());
-    PublicKeySign signer = PublicKeySignFactory.getPrimitive(privateHandle);
-    PublicKeyVerify verifier = PublicKeyVerifyFactory.getPrimitive(publicHandle);
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
     byte[] message = Random.randBytes(20);
     verifier.verify(signer.sign(message), message);
   }
@@ -387,7 +453,7 @@
     KeysetReader reader = BinaryKeysetReader.withBytes(new byte[0]);
 
     assertThrows(
-        GeneralSecurityException.class, () -> KeysetHandle.read(reader, null /* masterKey */));
+        GeneralSecurityException.class, () -> KeysetHandle.read(reader, /* masterKey= */ null));
   }
 
   @Test
@@ -437,7 +503,8 @@
   }
 
   @Test
-  public void monitoringClientGetsAnnotationsWithKeysetInfo() throws Exception {
+  public void noBuilderSetMonitoringAnnotations_monitoringClientGetsAnnotationsWithKeysetInfo()
+      throws Exception {
     MutableMonitoringRegistry.globalInstance().clear();
     FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
     MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
@@ -445,8 +512,7 @@
     Keyset keyset =
         TestUtil.createKeyset(
             TestUtil.createKey(
-                TestUtil.createAesGcmKeyData(
-                    TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f")),
+                TestUtil.createAesGcmKeyData(Hex.decode("000102030405060708090a0b0c0d0e0f")),
                 42,
                 KeyStatusType.ENABLED,
                 OutputPrefixType.TINK));
@@ -455,68 +521,200 @@
         MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
     KeysetHandle handleWithAnnotations = KeysetHandle.fromKeysetAndAnnotations(keyset, annotations);
     EncryptOnly encryptOnlyWithAnnotations = handleWithAnnotations.getPrimitive(EncryptOnly.class);
-    encryptOnlyWithAnnotations.encrypt(message);
+    Object unused = encryptOnlyWithAnnotations.encrypt(message);
     List<FakeMonitoringClient.LogEntry> entries = fakeMonitoringClient.getLogEntries();
     assertThat(entries).hasSize(1);
     assertThat(entries.get(0).getKeysetInfo().getAnnotations()).isEqualTo(annotations);
   }
 
   @Test
-  public void readNoSecret_shouldWork() throws Exception {
-    KeysetHandle privateHandle = KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256);
-    Keyset keyset = privateHandle.getPublicKeysetHandle().getKeyset();
+  public void builderSetMonitoringAnnotations_works() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+    MonitoringAnnotations annotations =
+        MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
+    KeysetHandle keysetHandleWithAnnotations =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(
+                        AesCmacParameters.builder()
+                            .setVariant(Variant.TINK)
+                            .setKeySizeBytes(32)
+                            .setTagSizeBytes(16)
+                            .build())
+                    .withFixedId(42)
+                    .makePrimary())
+            .setMonitoringAnnotations(annotations)
+            .build();
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    Mac mac = keysetHandleWithAnnotations.getPrimitive(Mac.class);
 
-    Keyset keyset2 = KeysetHandle.readNoSecret(keyset.toByteArray()).getKeyset();
-    Keyset keyset3 =
-        KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(keyset.toByteArray())).getKeyset();
+    // Work triggering various code paths.
+    byte[] tag = mac.computeMac(plaintext);
+    mac.verifyMac(tag, plaintext);
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tag, new byte[0]));
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(new byte[0], plaintext));
 
-    expect.that(keyset).isEqualTo(keyset2);
-    expect.that(keyset).isEqualTo(keyset3);
+    // With annotations set, the events get logged.
+    List<FakeMonitoringClient.LogEntry> logEntries = fakeMonitoringClient.getLogEntries();
+    System.out.println(logEntries);
+    assertThat(logEntries).hasSize(2);
+
+    FakeMonitoringClient.LogEntry tinkComputeEntry = logEntries.get(0);
+    assertThat(tinkComputeEntry.getKeyId()).isEqualTo(42);
+    assertThat(tinkComputeEntry.getPrimitive()).isEqualTo("mac");
+    assertThat(tinkComputeEntry.getApi()).isEqualTo("compute");
+    assertThat(tinkComputeEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
+    assertThat(tinkComputeEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+
+    FakeMonitoringClient.LogEntry rawComputeEntry = logEntries.get(1);
+    assertThat(rawComputeEntry.getKeyId()).isEqualTo(42);
+    assertThat(rawComputeEntry.getPrimitive()).isEqualTo("mac");
+    assertThat(rawComputeEntry.getApi()).isEqualTo("verify");
+    assertThat(rawComputeEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
+    assertThat(rawComputeEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
   }
 
   @Test
-  public void readNoSecret_withTypeSymmetric_shouldThrow() throws Exception {
+  public void builderNotSetMonitoringAnnotations_setsEmptyAnnotations() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+    KeysetHandle keysetHandleWithAnnotations =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(
+                        AesCmacParameters.builder()
+                            .setVariant(Variant.TINK)
+                            .setKeySizeBytes(32)
+                            .setTagSizeBytes(16)
+                            .build())
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    Mac mac = keysetHandleWithAnnotations.getPrimitive(Mac.class);
+
+    // Work triggering various code paths.
+    byte[] tag = mac.computeMac(plaintext);
+    mac.verifyMac(tag, plaintext);
+
+    // Without annotations, nothing gets logged.
+    assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
+    assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
+  }
+
+  @SuppressWarnings("deprecation") // This is a test for the deprecated function
+  @Test
+  public void deprecated_readNoSecretWithBytesInput_sameAs_parseKeysetWithoutSecret()
+      throws Exception {
+    // Public keyset should have the same output
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
+    byte[] serializedPublicKeyset = privateHandle.getPublicKeysetHandle().getKeyset().toByteArray();
+
+    KeysetHandle readNoSecretOutput = KeysetHandle.readNoSecret(serializedPublicKeyset);
+    KeysetHandle parseKeysetWithoutSecretOutput =
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(serializedPublicKeyset);
+    expect
+        .that(readNoSecretOutput.getKeyset())
+        .isEqualTo(parseKeysetWithoutSecretOutput.getKeyset());
+
+    // Symmetric Keyset should fail
+    byte[] serializedSymmetricKeyset =
+        TestUtil.createKeyset(
+                TestUtil.createKey(
+                    TestUtil.createHmacKeyData("01234567890123456".getBytes(UTF_8), 16),
+                    42,
+                    KeyStatusType.ENABLED,
+                    OutputPrefixType.TINK))
+            .toByteArray();
+    assertThrows(
+        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(serializedSymmetricKeyset));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkProtoKeysetFormat.parseKeysetWithoutSecret(serializedSymmetricKeyset));
+
+    // Private Keyset should fail
+    byte[] serializedPrivateKeyset = privateHandle.getKeyset().toByteArray();
+    assertThrows(
+        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(serializedPrivateKeyset));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkProtoKeysetFormat.parseKeysetWithoutSecret(serializedPrivateKeyset));
+
+    // Empty Keyset should fail
+    assertThrows(GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(new byte[0]));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkProtoKeysetFormat.parseKeysetWithoutSecret(new byte[0]));
+
+    // Invalid Keyset should fail
+    byte[] proto = new byte[] {0x00, 0x01, 0x02};
+    assertThrows(GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(proto));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkProtoKeysetFormat.parseKeysetWithoutSecret(proto));
+  }
+
+  @Test
+  public void readNoSecretWithBinaryKeysetReader_shouldWork() throws Exception {
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
+    Keyset keyset = privateHandle.getPublicKeysetHandle().getKeyset();
+    byte[] serializedKeyset = keyset.toByteArray();
+
+    Keyset readKeyset =
+        KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(serializedKeyset)).getKeyset();
+
+    expect.that(readKeyset).isEqualTo(keyset);
+  }
+
+  @Test
+  public void readNoSecretWithBinaryKeysetReader_withTypeSymmetric_shouldThrow() throws Exception {
     String keyValue = "01234567890123456";
-    Keyset keyset =
+    byte[] serializedKeyset =
         TestUtil.createKeyset(
             TestUtil.createKey(
                 TestUtil.createHmacKeyData(keyValue.getBytes(UTF_8), 16),
                 42,
                 KeyStatusType.ENABLED,
-                OutputPrefixType.TINK));
+                OutputPrefixType.TINK)).toByteArray();
 
     assertThrows(
-        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(keyset.toByteArray()));
-    assertThrows(
         GeneralSecurityException.class,
-        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(serializedKeyset)));
   }
 
   @Test
-  public void readNoSecret_withTypeAsymmetricPrivate_shouldThrow() throws Exception {
-    Keyset keyset = KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256).getKeyset();
+  public void readNoSecretWithBinaryKeysetReader_withTypeAsymmetricPrivate_shouldThrow()
+      throws Exception {
+    byte[] serializedKeyset =
+        KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256")).getKeyset().toByteArray();
 
     assertThrows(
-        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(keyset.toByteArray()));
-    assertThrows(
         GeneralSecurityException.class,
-        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(serializedKeyset)));
   }
 
   @Test
-  public void readNoSecret_withEmptyKeyset_shouldThrow() throws Exception {
-    assertThrows(GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(new byte[0]));
+  public void readNoSecretWithBinaryKeysetReader_withEmptyKeyset_shouldThrow() throws Exception {
+    byte[] emptySerializedKeyset = new byte[0];
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(emptySerializedKeyset)));
   }
 
   @Test
-  public void readNoSecret_withInvalidKeyset_shouldThrow() throws Exception {
-    byte[] proto = new byte[] {0x00, 0x01, 0x02};
-    assertThrows(GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(proto));
+  public void readNoSecretWithBinaryKeysetReader_withInvalidKeyset_shouldThrow() throws Exception {
+    byte[] invalidSerializedKeyset = new byte[] {0x00, 0x01, 0x02};
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(invalidSerializedKeyset)));
   }
 
   @Test
   public void writeNoSecretThenReadNoSecret_returnsSameKeyset() throws Exception {
-    KeysetHandle privateHandle = KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256);
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
     KeysetHandle publicHandle = privateHandle.getPublicKeysetHandle();
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     KeysetWriter writer = BinaryKeysetWriter.withOutputStream(outputStream);
@@ -547,35 +745,30 @@
 
   @Test
   public void writeNoSecret_withTypeAsymmetricPrivate_shouldThrow() throws Exception {
-    KeysetHandle handle = KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256);
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
 
-    assertThrows(GeneralSecurityException.class, () -> handle.writeNoSecret(null /* writer */));
+    assertThrows(GeneralSecurityException.class, () -> handle.writeNoSecret(/* writer= */ null));
   }
 
+  @SuppressWarnings("deprecation")  // This is a test for the deprecated function
   @Test
-  public void primaryKey_shouldWork() throws Exception {
-    KeyTemplate kt1 = KeyTemplates.get("AES128_EAX");
-    KeyTemplate kt2 = KeyTemplates.get("HMAC_SHA256_256BITTAG");
-    KeysetHandle ksh =
-        KeysetManager.withKeysetHandle(KeysetHandle.generateNew(kt1)).add(kt2).getKeysetHandle();
+  public void deprecated_primaryKey_shouldWork() throws Exception {
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("AES128_EAX").withFixedId(123))
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG")
+                .withFixedId(234).makePrimary())
+        .build();
 
-    KeyHandle kh = ksh.primaryKey();
-
-    ProtoKey pk = (ProtoKey) kh.getKey(SecretKeyAccess.insecureSecretAccess());
-    assertThat(pk.getProtoKey().getTypeUrl()).isEqualTo(kt1.getTypeUrl());
+    KeyHandle keyHandle = handle.primaryKey();
+    assertThat(keyHandle.getId()).isEqualTo(234);
   }
 
+  @SuppressWarnings("deprecation")  // This is a test for the deprecated function
   @Test
-  public void primaryKey_noPrimaryPresent_shouldThrow() throws Exception {
-    KeyTemplate kt1 = KeyTemplates.get("AES128_EAX");
-    KeyTemplate kt2 = KeyTemplates.get("HMAC_SHA256_256BITTAG");
-    KeysetHandle ksh = KeysetManager.withEmptyKeyset().add(kt1).add(kt2).getKeysetHandle();
-
-    assertThrows(GeneralSecurityException.class, ksh::primaryKey);
-  }
-
-  @Test
-  public void testGetAt_singleKey_works() throws Exception {
+  public void deprecated_primaryKey_primaryNotPresent_shouldThrow() throws Exception {
     Keyset keyset =
         TestUtil.createKeyset(
             TestUtil.createKey(
@@ -583,6 +776,72 @@
                 42,
                 KeyStatusType.ENABLED,
                 OutputPrefixType.TINK));
+    KeysetHandle handle =
+        KeysetHandle.fromKeyset(Keyset.newBuilder(keyset).setPrimaryKeyId(77).build());
+
+    assertThrows(GeneralSecurityException.class, handle::primaryKey);
+  }
+
+  @Test
+  public void testGetAt_singleKeyWithRegisteredProtoSerialization_works() throws Exception {
+    // HmacKey's proto serialization HmacProtoSerialization is registed in HmacKeyManager.
+    Keyset keyset =
+        TestUtil.createKeyset(
+            TestUtil.createKey(
+                TestUtil.createHmacKeyData("01234567890123456".getBytes(UTF_8), 16),
+                42,
+                KeyStatusType.ENABLED,
+                OutputPrefixType.TINK));
+    KeysetHandle handle = KeysetHandle.fromKeyset(keyset);
+    assertThat(handle.size()).isEqualTo(1);
+    KeysetHandle.Entry entry = handle.getAt(0);
+    assertThat(entry.getId()).isEqualTo(42);
+    assertThat(entry.getStatus()).isEqualTo(KeyStatus.ENABLED);
+    assertThat(entry.isPrimary()).isTrue();
+    assertThat(entry.getKey().getClass()).isEqualTo(HmacKey.class);
+  }
+
+  @Test
+  public void getAt_invalidKeyWithRegisteredProtoSerialization_throwsIllegalStateException()
+      throws Exception {
+    // HmacKey's proto serialization HmacProtoSerialization is registed in HmacKeyManager.
+    com.google.crypto.tink.proto.HmacKey invalidProtoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(999)
+            .setKeyValue(ByteString.copyFromUtf8("01234567890123456"))
+            .setParams(HmacParams.newBuilder().setHash(HashType.UNKNOWN_HASH).setTagSize(0))
+            .build();
+    Keyset keyset =
+        TestUtil.createKeyset(
+            TestUtil.createKey(
+                TestUtil.createKeyData(
+                    invalidProtoHmacKey,
+                    "type.googleapis.com/google.crypto.tink.HmacKey",
+                    KeyData.KeyMaterialType.SYMMETRIC),
+                42,
+                KeyStatusType.ENABLED,
+                OutputPrefixType.TINK));
+    KeysetHandle handle = KeysetHandle.fromKeyset(keyset);
+    assertThat(handle.size()).isEqualTo(1);
+    assertThrows(IllegalStateException.class, () -> handle.getAt(0));
+  }
+
+  @Test
+  public void testGetAt_singleKeyWithoutRegisteredProtoSerialization_wrapsToLegacyProtoKey()
+      throws Exception {
+    HmacPrfKey key =
+        HmacPrfKey.newBuilder()
+            .setParams(HmacPrfParams.newBuilder().setHash(HashType.SHA256).build())
+            .setKeyValue(ByteString.copyFromUtf8("01234567890123456"))
+            .build();
+    Keyset keyset =
+        TestUtil.createKeyset(
+            TestUtil.createKey(
+                TestUtil.createKeyData(
+                    key, "i.am.an.unregistered.key.type", KeyData.KeyMaterialType.SYMMETRIC),
+                42,
+                KeyStatusType.ENABLED,
+                OutputPrefixType.RAW));
     KeysetHandle handle = KeysetHandle.fromKeyset(keyset);
     assertThat(handle.size()).isEqualTo(1);
     KeysetHandle.Entry entry = handle.getAt(0);
@@ -791,7 +1050,40 @@
 
     assertThat(keysetHandle.size()).isEqualTo(1);
     assertThat(keysetHandle.getAt(0).getKey().getParameters())
-        .isEqualTo(AesCmacParameters.create(16));
+        .isEqualTo(AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(16).build());
+  }
+
+  @Test
+  public void keysetRotationWithBuilder_works() throws Exception {
+    KeysetHandle oldKeyset =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC_RAW")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+
+    // Add new key.
+    KeysetHandle keysetWithNewKey =
+        KeysetHandle.newBuilder(oldKeyset)
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC_RAW").withRandomId())
+            .build();
+
+    // Make latest key primary.
+    KeysetHandle.Builder builder = KeysetHandle.newBuilder(keysetWithNewKey);
+    builder.getAt(builder.size() - 1).makePrimary();
+    KeysetHandle keysetWithNewPrimary = builder.build();
+
+    assertThat(oldKeyset.size()).isEqualTo(1);
+
+    assertThat(keysetWithNewKey.size()).isEqualTo(2);
+    assertThat(keysetWithNewKey.getAt(0).isPrimary()).isTrue();
+    assertThat(keysetWithNewKey.getAt(1).isPrimary()).isFalse();
+
+    assertThat(keysetWithNewPrimary.size()).isEqualTo(2);
+    assertThat(keysetWithNewPrimary.getAt(0).isPrimary()).isFalse();
+    assertThat(keysetWithNewPrimary.getAt(1).isPrimary()).isTrue();
   }
 
   @Test
@@ -804,19 +1096,20 @@
                     .setStatus(KeyStatus.DISABLED))
             .addEntry(
                 KeysetHandle.generateEntryFromParameters(
-                        AesCmacParameters.createForKeysetWithCryptographicTagSize(
-                            10, AesCmacParameters.Variant.CRUNCHY))
+                        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10)
+                            .setVariant(Variant.CRUNCHY).build())
                     .withRandomId()
                     .makePrimary())
             .addEntry(
                 KeysetHandle.generateEntryFromParameters(
-                        AesCmacParameters.createForKeysetWithCryptographicTagSize(
-                            13, AesCmacParameters.Variant.LEGACY))
+                        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(13)
+                            .setVariant(Variant.LEGACY).build())
                     .withRandomId())
             .build();
     assertThat(keysetHandle.size()).isEqualTo(3);
     KeysetHandle.Entry entry0 = keysetHandle.getAt(0);
-    assertThat(entry0.getKey().getParameters()).isEqualTo(AesCmacParameters.create(16));
+    assertThat(entry0.getKey().getParameters())
+        .isEqualTo(AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(16).build());
     assertThat(entry0.isPrimary()).isFalse();
     assertThat(entry0.getStatus()).isEqualTo(KeyStatus.DISABLED);
 
@@ -825,16 +1118,16 @@
     assertThat(entry1.getStatus()).isEqualTo(KeyStatus.ENABLED);
     assertThat(entry1.getKey().getParameters())
         .isEqualTo(
-            AesCmacParameters.createForKeysetWithCryptographicTagSize(
-                10, AesCmacParameters.Variant.CRUNCHY));
+            AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10)
+                .setVariant(Variant.CRUNCHY).build());
 
     KeysetHandle.Entry entry2 = keysetHandle.getAt(2);
     assertThat(entry2.isPrimary()).isFalse();
     assertThat(entry2.getStatus()).isEqualTo(KeyStatus.ENABLED);
     assertThat(keysetHandle.getAt(2).getKey().getParameters())
         .isEqualTo(
-            AesCmacParameters.createForKeysetWithCryptographicTagSize(
-                13, AesCmacParameters.Variant.LEGACY));
+            AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(13)
+                .setVariant(Variant.LEGACY).build());
   }
 
   @Test
@@ -863,7 +1156,7 @@
   public void testBuilder_withRandomId_doesNotHaveCollisions() throws Exception {
     // Test takes longer on Android; and a simple Java test suffices.
     assumeFalse(TestUtil.isAndroid());
-    int numNonPrimaryKeys = 2 << 16;
+    int numNonPrimaryKeys = 1 << 16;
     KeysetHandle.Builder builder = KeysetHandle.newBuilder();
     builder.addEntry(
         KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId().makePrimary());
@@ -902,17 +1195,69 @@
   }
 
   @Test
-  public void testBuilder_removeAt_works() throws Exception {
+  public void testBuilder_deprecated_removeAt_works() throws Exception {
+    KeysetHandle.Builder builder = KeysetHandle.newBuilder();
+    builder.addEntry(
+        KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+            .withRandomId()
+            .setStatus(KeyStatus.DISABLED));
+    builder.addEntry(
+        KeysetHandle.generateEntryFromParameters(
+                AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(13).build())
+            .withRandomId()
+            .makePrimary()
+            .setStatus(KeyStatus.ENABLED));
+    KeysetHandle.Builder.Entry removedEntry = builder.removeAt(0);
+    assertThat(removedEntry.getStatus()).isEqualTo(KeyStatus.DISABLED);
+    KeysetHandle handle = builder.build();
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getAt(0).getKey().getParameters())
+        .isEqualTo(AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(13).build());
+  }
+
+  @Test
+  public void testBuilder_deprecated_removeAtInvalidIndex_throws() throws Exception {
     KeysetHandle.Builder builder = KeysetHandle.newBuilder();
     builder.addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId());
     builder.addEntry(
-        KeysetHandle.generateEntryFromParameters(AesCmacParameters.create(13))
-            .withRandomId()
-            .makePrimary());
-    builder.removeAt(0);
-    KeysetHandle handle = builder.build();
-    assertThat(handle.size()).isEqualTo(1);
-    assertThat(handle.getAt(0).getKey().getParameters()).isEqualTo(AesCmacParameters.create(13));
+        KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId().makePrimary());
+    assertThrows(IndexOutOfBoundsException.class, () -> builder.removeAt(2));
+  }
+
+  @Test
+  public void testBuilder_deleteAt_works() throws Exception {
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(
+                        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(13).build())
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    assertThat(handle.size()).isEqualTo(2);
+
+    KeysetHandle handle2 = KeysetHandle.newBuilder(handle).deleteAt(0).build();
+
+    assertThat(handle2.size()).isEqualTo(1);
+    assertThat(handle2.getAt(0).getKey().getParameters())
+        .isEqualTo(AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(13).build());
+  }
+
+  @Test
+  public void testBuilder_deleteAtInvalidIndex_works() throws Exception {
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    assertThat(handle.size()).isEqualTo(2);
+
+    assertThrows(
+        IndexOutOfBoundsException.class, () -> KeysetHandle.newBuilder(handle).deleteAt(2));
   }
 
   @Test
@@ -945,6 +1290,19 @@
   }
 
   @Test
+  public void testBuilder_deletedPrimary_throws() throws Exception {
+    KeysetHandle.Builder builder =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+                    .withRandomId()
+                    .makePrimary())
+            .addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId())
+            .deleteAt(0);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
   public void testBuilder_addPrimary_clearsOtherPrimary() throws Exception {
     KeysetHandle.Builder builder = KeysetHandle.newBuilder();
     builder.addEntry(
@@ -1000,9 +1358,87 @@
   }
 
   @Test
+  public void testBuilder_copyKeyset_works() throws Exception {
+    KeysetHandle original =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withFixedId(777))
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+                    .makePrimary()
+                    .withFixedId(778))
+            .build();
+    KeysetHandle copy = KeysetHandle.newBuilder(original).build();
+    assertThat(copy.size()).isEqualTo(2);
+    assertThat(copy.getAt(0).getId()).isEqualTo(777);
+    assertThat(copy.getAt(0).getKey().equalsKey(original.getAt(0).getKey())).isTrue();
+    assertThat(copy.getAt(0).getStatus()).isEqualTo(original.getAt(0).getStatus());
+    assertThat(copy.getAt(1).getId()).isEqualTo(778);
+    assertThat(copy.getAt(1).getKey().equalsKey(original.getAt(1).getKey())).isTrue();
+    assertThat(copy.getAt(1).getStatus()).isEqualTo(original.getAt(1).getStatus());
+  }
+
+  @Test
+  public void testBuilder_copyKeyset_originalHasInvalidKey_throws() throws Exception {
+    Keyset keyset =
+        Keyset.newBuilder()
+            .setPrimaryKeyId(1)
+            .addKey(
+                Keyset.Key.newBuilder()
+                    .setKeyId(1)
+                    .setStatus(KeyStatusType.ENABLED)
+                    .setKeyData(
+                        KeyData.newBuilder()
+                            .setTypeUrl("type.googleapis.com/google.crypto.tink.AesGcmKey")
+                            .setValue(ByteString.EMPTY)))
+            .build();
+    KeysetHandle.Builder builder = KeysetHandle.newBuilder(KeysetHandle.fromKeyset(keyset));
+    GeneralSecurityException thrown = assertThrows(GeneralSecurityException.class, builder::build);
+    assertThat(thrown)
+        .hasCauseThat()
+        .hasMessageThat()
+        .contains("wrong status or key parsing failed");
+  }
+
+  @Test
+  public void testBuilder_copyKeyset_originalHasNoPrimary_throws() throws Exception {
+    KeysetHandle original =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC")
+                    .makePrimary()
+                    .withFixedId(778))
+            .build();
+    Keyset keyset = original.getKeyset();
+    Keyset keysetWithoutPrimary = keyset.toBuilder().setPrimaryKeyId(3843).build();
+
+    KeysetHandle.Builder builder =
+        KeysetHandle.newBuilder(KeysetHandle.fromKeyset(keysetWithoutPrimary));
+    GeneralSecurityException thrown = assertThrows(GeneralSecurityException.class, builder::build);
+    assertThat(thrown).hasMessageThat().contains("No primary was set");
+  }
+
+  @Test
+  public void testBuilder_buildTwice_fails() throws Exception {
+    KeysetHandle.Builder builder =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES256_CMAC_RAW")
+                    .withRandomId()
+                    .makePrimary());
+
+    Object unused = builder.build();
+    // We disallow calling build on the same builder twice. The reason is that build assigns IDs
+    // which were marked with "withRandomId()". Doing this twice results in incompatible keysets,
+    // which would be confusing.
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
   public void testImportKey_withoutIdRequirement_withFixedId_works() throws Exception {
-    AesCmacParameters params = AesCmacParameters.create(10);
-    AesCmacKey key = AesCmacKey.create(params, SecretBytes.randomBytes(32));
+    AesCmacParameters params = AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10)
+        .build();
+    AesCmacKey key = AesCmacKey.builder().setParameters(params)
+        .setAesKeyBytes(SecretBytes.randomBytes(32)).build();
     KeysetHandle handle =
         KeysetHandle.newBuilder()
             .addEntry(KeysetHandle.importKey(key).withFixedId(102).makePrimary())
@@ -1014,8 +1450,10 @@
 
   @Test
   public void testImportKey_withoutIdRequirement_noIdAssigned_throws() throws Exception {
-    AesCmacParameters params = AesCmacParameters.create(10);
-    AesCmacKey key = AesCmacKey.create(params, SecretBytes.randomBytes(32));
+    AesCmacParameters params = AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10)
+        .build();
+    AesCmacKey key = AesCmacKey.builder().setParameters(params)
+        .setAesKeyBytes(SecretBytes.randomBytes(32)).build();
     KeysetHandle.Builder builder =
         KeysetHandle.newBuilder().addEntry(KeysetHandle.importKey(key).makePrimary());
     assertThrows(GeneralSecurityException.class, builder::build);
@@ -1023,8 +1461,10 @@
 
   @Test
   public void testImportKey_withoutIdRequirement_withRandomId_works() throws Exception {
-    AesCmacParameters params = AesCmacParameters.create(10);
-    AesCmacKey key = AesCmacKey.create(params, SecretBytes.randomBytes(32));
+    AesCmacParameters params = AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10)
+        .build();
+    AesCmacKey key = AesCmacKey.builder().setParameters(params)
+        .setAesKeyBytes(SecretBytes.randomBytes(32)).build();
     KeysetHandle handle =
         KeysetHandle.newBuilder()
             .addEntry(KeysetHandle.importKey(key).withRandomId().makePrimary())
@@ -1036,10 +1476,14 @@
   @Test
   public void testImportKey_withIdRequirement_noId_works() throws Exception {
     AesCmacParameters params =
-        AesCmacParameters.createForKeysetWithCryptographicTagSize(
-            10, AesCmacParameters.Variant.TINK);
+        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10).setVariant(Variant.TINK)
+            .build();
     AesCmacKey key =
-        AesCmacKey.createForKeyset(params, SecretBytes.randomBytes(32), /*idRequirement=*/ 105);
+        AesCmacKey.builder()
+            .setParameters(params)
+            .setAesKeyBytes(SecretBytes.randomBytes(32))
+            .setIdRequirement(105)
+            .build();
     KeysetHandle handle =
         KeysetHandle.newBuilder().addEntry(KeysetHandle.importKey(key).makePrimary()).build();
     assertThat(handle.size()).isEqualTo(1);
@@ -1050,10 +1494,14 @@
   @Test
   public void testImportKey_withIdRequirement_randomId_throws() throws Exception {
     AesCmacParameters params =
-        AesCmacParameters.createForKeysetWithCryptographicTagSize(
-            10, AesCmacParameters.Variant.TINK);
+        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10).setVariant(Variant.TINK)
+            .build();
     AesCmacKey key =
-        AesCmacKey.createForKeyset(params, SecretBytes.randomBytes(32), /*idRequirement=*/ 105);
+        AesCmacKey.builder()
+            .setParameters(params)
+            .setAesKeyBytes(SecretBytes.randomBytes(32))
+            .setIdRequirement(105)
+            .build();
     KeysetHandle.Builder builder =
         KeysetHandle.newBuilder()
             .addEntry(KeysetHandle.importKey(key).withRandomId().makePrimary());
@@ -1063,10 +1511,14 @@
   @Test
   public void testImportKey_withIdRequirement_explicitlySetToCorrectId_works() throws Exception {
     AesCmacParameters params =
-        AesCmacParameters.createForKeysetWithCryptographicTagSize(
-            10, AesCmacParameters.Variant.TINK);
+        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10).setVariant(Variant.TINK)
+            .build();
     AesCmacKey key =
-        AesCmacKey.createForKeyset(params, SecretBytes.randomBytes(32), /*idRequirement=*/ 105);
+        AesCmacKey.builder()
+            .setParameters(params)
+            .setAesKeyBytes(SecretBytes.randomBytes(32))
+            .setIdRequirement(105)
+            .build();
     KeysetHandle handle =
         KeysetHandle.newBuilder()
             .addEntry(KeysetHandle.importKey(key).withFixedId(105).makePrimary())
@@ -1079,10 +1531,14 @@
   @Test
   public void testImportKey_withIdRequirement_explicitlySetToWrongId_throws() throws Exception {
     AesCmacParameters params =
-        AesCmacParameters.createForKeysetWithCryptographicTagSize(
-            10, AesCmacParameters.Variant.TINK);
+        AesCmacParameters.builder().setKeySizeBytes(32).setTagSizeBytes(10).setVariant(Variant.TINK)
+            .build();
     AesCmacKey key =
-        AesCmacKey.createForKeyset(params, SecretBytes.randomBytes(32), /*idRequirement=*/ 105);
+        AesCmacKey.builder()
+            .setParameters(params)
+            .setAesKeyBytes(SecretBytes.randomBytes(32))
+            .setIdRequirement(105)
+            .build();
     KeysetHandle.Builder builder =
         KeysetHandle.newBuilder()
             .addEntry(KeysetHandle.importKey(key).withFixedId(106).makePrimary());
@@ -1108,4 +1564,145 @@
             .setStatus(null));
     assertThrows(GeneralSecurityException.class, builder::build);
   }
+
+  @Test
+  public void testStatusNotSet_getPrimitive_throws() throws Exception {
+    Keyset keyset =
+        Keyset.newBuilder()
+            .setPrimaryKeyId(1)
+            .addKey(
+                Keyset.Key.newBuilder()
+                    .setKeyId(1)
+                    .setStatus(KeyStatusType.ENABLED)
+                    .setOutputPrefixType(OutputPrefixType.TINK)
+                    .setKeyData(KeyData.newBuilder().setTypeUrl("unregisteredTypeUrl")))
+            .build();
+    KeysetHandle handle = KeysetHandle.fromKeyset(keyset);
+    GeneralSecurityException e =
+        assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(Aead.class));
+    assertThat(e).hasMessageThat().contains("Unable to get primitive");
+    assertThat(e).hasMessageThat().contains("unregisteredTypeUrl");
+  }
+
+  @Immutable
+  private static final class TestPrimitiveA {
+    public TestPrimitiveA() {}
+  }
+
+  @Immutable
+  private static final class TestPrimitiveB {
+    public TestPrimitiveB() {}
+  }
+
+  @Immutable
+  private static final class TestWrapperA
+      implements PrimitiveWrapper<TestPrimitiveA, TestPrimitiveB> {
+
+    @Override
+    public TestPrimitiveB wrap(final PrimitiveSet<TestPrimitiveA> primitives) {
+      return new TestPrimitiveB();
+    }
+
+    @Override
+    public Class<TestPrimitiveB> getPrimitiveClass() {
+      return TestPrimitiveB.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getInputPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+  }
+
+  private static TestPrimitiveA getPrimitiveAHmacKey(HmacKey key) {
+    return new TestPrimitiveA();
+  }
+
+  @Test
+  public void getPrimitive_usesProvidedConfigurationWhenProvided() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    KeysetHandleTest::getPrimitiveAHmacKey,
+                    HmacKey.class,
+                    TestPrimitiveA.class))
+            .registerPrimitiveWrapper(new TestWrapperA())
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+    HmacKey hmacKey =
+        HmacKey.builder()
+            .setParameters(
+                HmacParameters.builder()
+                    .setKeySizeBytes(20)
+                    .setTagSizeBytes(10)
+                    .setVariant(HmacParameters.Variant.NO_PREFIX)
+                    .setHashType(HmacParameters.HashType.SHA256)
+                    .build())
+            .setKeyBytes(SecretBytes.randomBytes(20))
+            .setIdRequirement(null)
+            .build();
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hmacKey).withRandomId().makePrimary())
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class, () -> keysetHandle.getPrimitive(TestPrimitiveB.class));
+    assertThat(keysetHandle.getPrimitive(configuration, TestPrimitiveB.class)).isNotNull();
+  }
+
+  @Test
+  public void getPrimitive_usesRegistryWhenNoConfigurationProvided() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey).withRandomId().makePrimary())
+            .build();
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    ChunkedMac registryMac =
+        MutablePrimitiveRegistry.globalInstance().getPrimitive(rawKey, ChunkedMac.class);
+    ChunkedMacComputation registryMacComputation = registryMac.createComputation();
+    registryMacComputation.update(ByteBuffer.wrap(plaintext));
+    ChunkedMac keysetHandleMac = keysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation keysetHandleMacComputation = keysetHandleMac.createComputation();
+    keysetHandleMacComputation.update(ByteBuffer.wrap(plaintext));
+
+    assertThat(keysetHandleMacComputation.computeMac())
+        .isEqualTo(registryMacComputation.computeMac());
+  }
+
+  @Test
+  public void getLegacyPrimitive_usesRegistryWhenNoConfigurationProvided()
+      throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey).withRandomId().makePrimary())
+            .build();
+    KeyData rawKeyData =
+        KeyData.newBuilder()
+            .setValue(
+                com.google.crypto.tink.proto.HmacKey.newBuilder()
+                    .setParams(
+                        HmacParams.newBuilder()
+                            .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                            .setTagSize(HMAC_TAG_SIZE)
+                            .build())
+                    .setKeyValue(
+                        ByteString.copyFrom(
+                            rawKey.getKeyBytes().toByteArray(InsecureSecretKeyAccess.get())))
+                    .build()
+                    .toByteString())
+            .setTypeUrl(keysetHandle.getKeysetInfo().getKeyInfo(0).getTypeUrl())
+            .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
+            .build();
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    Mac registryMac = Registry.getPrimitive(rawKeyData, Mac.class);
+    Mac keysetHandleMac = keysetHandle.getPrimitive(Mac.class);
+
+    assertThat(keysetHandleMac.computeMac(plaintext)).isEqualTo(registryMac.computeMac(plaintext));
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeysetManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/KeysetManagerTest.java
index fe592d2..6e795b6 100644
--- a/java_src/src/test/java/com/google/crypto/tink/KeysetManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/KeysetManagerTest.java
@@ -24,10 +24,7 @@
 import com.google.common.truth.Expect;
 import com.google.crypto.tink.aead.AesGcmKeyManager;
 import com.google.crypto.tink.config.TinkConfig;
-import com.google.crypto.tink.mac.MacKeyTemplates;
-import com.google.crypto.tink.proto.AesGcmKey;
-import com.google.crypto.tink.proto.AesGcmKeyFormat;
-import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.Keyset.Key;
@@ -39,6 +36,7 @@
 import com.google.crypto.tink.tinkkey.TinkKey;
 import com.google.crypto.tink.tinkkey.internal.ProtoKey;
 import com.google.protobuf.ExtensionRegistryLite;
+import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.List;
 import org.junit.BeforeClass;
@@ -77,7 +75,14 @@
   @Test
   public void testEnable_shouldEnableKey() throws Exception {
     int keyId = 42;
-    KeysetHandle handle = KeysetHandle.fromKeyset(TestUtil.createKeyset(createDisabledKey(keyId)));
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(keyId)
+                    .setStatus(KeyStatus.DISABLED)
+                    .makePrimary())
+            .build();
     Keyset keyset =
         KeysetManager.withKeysetHandle(handle).enable(keyId).getKeysetHandle().getKeyset();
 
@@ -114,7 +119,14 @@
   @Test
   public void testEnable_keyNotFound_shouldThrowException() throws Exception {
     int keyId = 42;
-    KeysetHandle handle = KeysetHandle.fromKeyset(TestUtil.createKeyset(createDisabledKey(keyId)));
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(keyId)
+                    .setStatus(KeyStatus.DISABLED)
+                    .makePrimary())
+            .build();
 
     GeneralSecurityException e =
         assertThrows(
@@ -128,9 +140,15 @@
     int primaryKeyId = 42;
     int newPrimaryKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(
-                createEnabledKey(primaryKeyId), createEnabledKey(newPrimaryKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(newPrimaryKeyId))
+            .build();
     Keyset keyset =
         KeysetManager.withKeysetHandle(handle)
             .setPrimary(newPrimaryKeyId)
@@ -146,9 +164,16 @@
     int primaryKeyId = 42;
     int newPrimaryKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(
-                createEnabledKey(primaryKeyId), createEnabledKey(newPrimaryKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(newPrimaryKeyId))
+            .build();
+
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -161,9 +186,16 @@
     int primaryKeyId = 42;
     int newPrimaryKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(
-                createEnabledKey(primaryKeyId), createDisabledKey(newPrimaryKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(newPrimaryKeyId)
+                    .setStatus(KeyStatus.DISABLED))
+            .build();
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -207,9 +239,15 @@
     int primaryKeyId = 42;
     int newPrimaryKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(
-                createEnabledKey(primaryKeyId), createEnabledKey(newPrimaryKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(newPrimaryKeyId))
+            .build();
     Keyset keyset =
         KeysetManager.withKeysetHandle(handle)
             .promote(newPrimaryKeyId)
@@ -225,9 +263,15 @@
     int primaryKeyId = 42;
     int newPrimaryKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(
-                createEnabledKey(primaryKeyId), createEnabledKey(newPrimaryKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(newPrimaryKeyId))
+            .build();
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -240,9 +284,16 @@
     int primaryKeyId = 42;
     int newPrimaryKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(
-                createEnabledKey(primaryKeyId), createDisabledKey(newPrimaryKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(newPrimaryKeyId)
+                    .setStatus(KeyStatus.DISABLED))
+            .build();
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -285,8 +336,14 @@
     int primaryKeyId = 42;
     int otherKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(primaryKeyId), createEnabledKey(otherKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(otherKeyId))
+            .build();
     Keyset keyset =
         KeysetManager.withKeysetHandle(handle).disable(otherKeyId).getKeysetHandle().getKeyset();
 
@@ -302,8 +359,14 @@
     int primaryKeyId = 42;
     int otherKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(primaryKeyId), createEnabledKey(otherKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(otherKeyId))
+            .build();
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -336,7 +399,13 @@
   @Test
   public void testDisable_keyNotFound_shouldThrowException() throws Exception {
     int keyId = 42;
-    KeysetHandle handle = KeysetHandle.fromKeyset(TestUtil.createKeyset(createDisabledKey(keyId)));
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(keyId)
+                    .makePrimary())
+            .build();
 
     GeneralSecurityException e =
         assertThrows(
@@ -350,8 +419,14 @@
     int primaryKeyId = 42;
     int otherKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(primaryKeyId), createEnabledKey(otherKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(otherKeyId))
+            .build();
     Keyset keyset =
         KeysetManager.withKeysetHandle(handle).destroy(otherKeyId).getKeysetHandle().getKeyset();
 
@@ -368,8 +443,14 @@
     int primaryKeyId = 42;
     int otherKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(primaryKeyId), createEnabledKey(otherKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(otherKeyId))
+            .build();
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -417,8 +498,14 @@
     int primaryKeyId = 42;
     int otherKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(primaryKeyId), createEnabledKey(otherKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(otherKeyId))
+            .build();
     Keyset keyset =
         KeysetManager.withKeysetHandle(handle).delete(otherKeyId).getKeysetHandle().getKeyset();
 
@@ -432,8 +519,14 @@
     int primaryKeyId = 42;
     int otherKeyId = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(primaryKeyId), createEnabledKey(otherKeyId)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(primaryKeyId)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(otherKeyId))
+            .build();
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -450,8 +543,14 @@
     int keyId1 = 42;
     final int keyId2 = 43;
     KeysetHandle handle =
-        KeysetHandle.fromKeyset(
-            TestUtil.createKeyset(createEnabledKey(keyId1), createEnabledKey(keyId2)));
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM")
+                    .withFixedId(keyId1)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_GCM").withFixedId(keyId2))
+            .build();
 
     GeneralSecurityException e =
         assertThrows(
@@ -465,12 +564,12 @@
     // Create a keyset that contains a single HmacKey.
     KeyTemplate template = KeyTemplates.get("HMAC_SHA256_128BITTAG");
     @SuppressWarnings("deprecation") // Need to test the deprecated function
-    Keyset keyset =
-        KeysetManager.withEmptyKeyset().rotate(template.getProto()).getKeysetHandle().getKeyset();
-
-    assertThat(keyset.getKeyCount()).isEqualTo(1);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(keyset.getKey(0).getKeyId());
-    TestUtil.assertHmacKey(template, keyset.getKey(0));
+    KeysetHandle keyset =
+        KeysetManager.withEmptyKeyset().rotate(template.getProto()).getKeysetHandle();
+    assertThat(keyset.size()).isEqualTo(1);
+    assertThat(keyset.getAt(0).isPrimary()).isTrue();
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_128BITTAG").toParameters());
   }
 
   @Test
@@ -492,49 +591,39 @@
             .rotate(KeyTemplates.get("HMAC_SHA256_128BITTAG").getProto())
             .getKeysetHandle();
     @SuppressWarnings("deprecation") // Need to test the deprecated function
-    Keyset keyset =
+    KeysetHandle keyset =
         KeysetManager.withKeysetHandle(existing)
             .rotate(KeyTemplates.get("HMAC_SHA256_256BITTAG").getProto())
-            .getKeysetHandle()
-            .getKeyset();
-
-    assertThat(keyset.getKeyCount()).isEqualTo(2);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(keyset.getKey(1).getKeyId());
-    TestUtil.assertHmacKey(KeyTemplates.get("HMAC_SHA256_128BITTAG"), keyset.getKey(0));
-    TestUtil.assertHmacKey(KeyTemplates.get("HMAC_SHA256_256BITTAG"), keyset.getKey(1));
+            .getKeysetHandle();
+    assertThat(keyset.size()).isEqualTo(2);
+    assertThat(keyset.getAt(1).isPrimary()).isTrue();
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_128BITTAG").toParameters());
+    assertThat(keyset.getAt(1).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_256BITTAG").toParameters());
   }
 
   @Test
   public void testAdd_shouldAddNewKey() throws Exception {
     KeyTemplate kt = KeyTemplates.get("AES128_GCM");
-    Keyset keyset = KeysetManager.withEmptyKeyset().add(kt).getKeysetHandle().getKeyset();
+    KeysetHandle keyset = KeysetManager.withEmptyKeyset().add(kt).getKeysetHandle();
 
-    assertThat(keyset.getKeyCount()).isEqualTo(1);
+    assertThat(keyset.size()).isEqualTo(1);
     // No primary key because add doesn't automatically promote the new key to primary.
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(0);
-
-    Keyset.Key key = keyset.getKey(0);
-    assertThat(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    assertThat(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    assertThat(key.hasKeyData()).isTrue();
-    assertThat(key.getKeyData().getTypeUrl()).isEqualTo(kt.getTypeUrl());
-
-    AesGcmKeyFormat aesGcmKeyFormat =
-        AesGcmKeyFormat.parseFrom(kt.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey =
-        AesGcmKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    assertThat(aesGcmKey.getKeyValue().size()).isEqualTo(aesGcmKeyFormat.getKeySize());
+    assertThat(keyset.getAt(0).isPrimary()).isFalse();
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("AES128_GCM").toParameters());
   }
 
   @Test
   public void testAdd_shouldAddNewKey_proto() throws Exception {
     // Create a keyset that contains a single HmacKey.
     KeyTemplate template = KeyTemplates.get("HMAC_SHA256_128BITTAG");
-    Keyset keyset = KeysetManager.withEmptyKeyset().add(template).getKeysetHandle().getKeyset();
+    KeysetHandle keyset = KeysetManager.withEmptyKeyset().add(template).getKeysetHandle();
 
-    assertThat(keyset.getKeyCount()).isEqualTo(1);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(0);
-    TestUtil.assertHmacKey(template, keyset.getKey(0));
+    assertThat(keyset.size()).isEqualTo(1);
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_128BITTAG").toParameters());
   }
 
   @Test
@@ -561,10 +650,12 @@
 
   @Test
   public void testAdd_protoKeyTemplateWithoutPrefix_shouldThrowException() throws Exception {
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
     com.google.crypto.tink.proto.KeyTemplate templateWithoutPrefix =
-        MacKeyTemplates.createHmacKeyTemplate(32, 16, HashType.SHA256).toBuilder()
-            .setOutputPrefixType(OutputPrefixType.UNKNOWN_PREFIX)
-            .build();
+        template.toBuilder().setOutputPrefixType(OutputPrefixType.UNKNOWN_PREFIX).build();
     assertThrows(
         GeneralSecurityException.class,
         () -> KeysetManager.withEmptyKeyset().add(templateWithoutPrefix));
@@ -575,54 +666,41 @@
     KeyTemplate kt1 = AesGcmKeyManager.aes128GcmTemplate();
     KeysetHandle existing = KeysetManager.withEmptyKeyset().add(kt1).getKeysetHandle();
     KeyTemplate kt2 = AesGcmKeyManager.aes256GcmTemplate();
-    Keyset keyset = KeysetManager.withKeysetHandle(existing).add(kt2).getKeysetHandle().getKeyset();
+    KeysetHandle keyset = KeysetManager.withKeysetHandle(existing).add(kt2).getKeysetHandle();
 
-    assertThat(keyset.getKeyCount()).isEqualTo(2);
+    assertThat(keyset.size()).isEqualTo(2);
     // None of the keys are primary.
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(0);
-
-    Keyset.Key key1 = keyset.getKey(0);
-    assertThat(key1.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    assertThat(key1.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    assertThat(key1.hasKeyData()).isTrue();
-    assertThat(key1.getKeyData().getTypeUrl()).isEqualTo(kt1.getTypeUrl());
-
-    AesGcmKeyFormat aesGcmKeyFormat1 =
-        AesGcmKeyFormat.parseFrom(kt1.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey1 =
-        AesGcmKey.parseFrom(key1.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    assertThat(aesGcmKey1.getKeyValue().size()).isEqualTo(aesGcmKeyFormat1.getKeySize());
-
-    Keyset.Key key2 = keyset.getKey(1);
-    assertThat(key2.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    assertThat(key2.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    assertThat(key2.hasKeyData()).isTrue();
-    assertThat(key2.getKeyData().getTypeUrl()).isEqualTo(kt2.getTypeUrl());
-
-    AesGcmKeyFormat aesGcmKeyFormat2 =
-        AesGcmKeyFormat.parseFrom(kt2.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey2 =
-        AesGcmKey.parseFrom(key2.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    assertThat(aesGcmKey2.getKeyValue().size()).isEqualTo(aesGcmKeyFormat2.getKeySize());
+    assertThat(keyset.getAt(0).isPrimary()).isFalse();
+    assertThat(keyset.getAt(1).isPrimary()).isFalse();
+    assertThat(keyset.getAt(0).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    assertThat(keyset.getAt(1).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("AES128_GCM").toParameters());
+    assertThat(keyset.getAt(1).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("AES256_GCM").toParameters());
   }
 
   @Test
   public void testAdd_existingKeySet_shouldAddNewKey_proto() throws Exception {
-    KeysetHandle existing =
-        KeysetManager.withEmptyKeyset()
-            .rotate(MacKeyTemplates.HMAC_SHA256_128BITTAG)
-            .getKeysetHandle();
+    com.google.crypto.tink.proto.KeyTemplate template1 =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
+    com.google.crypto.tink.proto.KeyTemplate template2 =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_256BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
+    KeysetHandle existing = KeysetManager.withEmptyKeyset().rotate(template1).getKeysetHandle();
     int existingPrimaryKeyId = existing.getKeyset().getPrimaryKeyId();
-    Keyset keyset =
-        KeysetManager.withKeysetHandle(existing)
-            .add(MacKeyTemplates.HMAC_SHA256_256BITTAG)
-            .getKeysetHandle()
-            .getKeyset();
+    KeysetHandle keyset = KeysetManager.withKeysetHandle(existing).add(template2).getKeysetHandle();
 
-    assertThat(keyset.getKeyCount()).isEqualTo(2);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(existingPrimaryKeyId);
-    TestUtil.assertHmacKey(KeyTemplates.get("HMAC_SHA256_128BITTAG"), keyset.getKey(0));
-    TestUtil.assertHmacKey(KeyTemplates.get("HMAC_SHA256_256BITTAG"), keyset.getKey(1));
+    assertThat(keyset.size()).isEqualTo(2);
+    assertThat(keyset.getAt(0).isPrimary()).isTrue();
+    assertThat(keyset.getAt(0).getId()).isEqualTo(existingPrimaryKeyId);
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_128BITTAG").toParameters());
+    assertThat(keyset.getAt(1).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_256BITTAG").toParameters());
   }
 
   @Test
@@ -634,21 +712,13 @@
     keysetManager = keysetManager.add(keyHandle);
 
     KeysetHandle keysetHandle = keysetManager.getKeysetHandle();
-    Keyset keyset = keysetHandle.getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(1);
-    Keyset.Key key = keyset.getKey(0);
-    expect.that(key.getKeyId()).isEqualTo(keyHandle.getId());
-    expect.that(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key.hasKeyData()).isTrue();
-    expect.that(key.getKeyData().getTypeUrl()).isEqualTo(keyTemplate.getTypeUrl());
-    AesGcmKeyFormat aesGcmKeyFormat =
-        AesGcmKeyFormat.parseFrom(keyTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey =
-        AesGcmKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesGcmKey.getKeyValue().size()).isEqualTo(aesGcmKeyFormat.getKeySize());
-    // No primary key because add doesn't automatically promote the new key to primary.
-    assertThrows(GeneralSecurityException.class, () -> keysetHandle.getPrimitive(Aead.class));
+    expect.that(keysetHandle.size()).isEqualTo(1);
+    expect.that(keysetHandle.getAt(0).getId()).isEqualTo(keyHandle.getId());
+    expect.that(keysetHandle.getAt(0).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    expect.that(keysetHandle.getAt(0).isPrimary()).isFalse();
+    expect
+        .that(keysetHandle.getAt(0).getKey().getParameters())
+        .isEqualTo(keyTemplate.toParameters());
   }
 
   @Test
@@ -662,33 +732,16 @@
 
     keysetManager = keysetManager.add(keyHandle2);
 
-    Keyset keyset = keysetManager.getKeysetHandle().getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(2);
-    expect.that(keyset.getPrimaryKeyId()).isEqualTo(keyHandle1.getId());
-    Keyset.Key key1 = keyset.getKey(0);
-    expect.that(key1.getKeyId()).isEqualTo(keyHandle1.getId());
-    expect.that(key1.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key1.getOutputPrefixType()).isEqualTo(OutputPrefixType.RAW);
-    expect.that(key1.hasKeyData()).isTrue();
-    expect.that(key1.getKeyData().getTypeUrl()).isEqualTo(keyTemplate1.getTypeUrl());
-    AesGcmKeyFormat aesGcmKeyFormat1 =
-        AesGcmKeyFormat.parseFrom(
-            keyTemplate1.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey1 =
-        AesGcmKey.parseFrom(key1.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesGcmKey1.getKeyValue().size()).isEqualTo(aesGcmKeyFormat1.getKeySize());
-    Keyset.Key key2 = keyset.getKey(1);
-    expect.that(key2.getKeyId()).isEqualTo(keyHandle2.getId());
-    expect.that(key2.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key2.getOutputPrefixType()).isEqualTo(OutputPrefixType.RAW);
-    expect.that(key2.hasKeyData()).isTrue();
-    expect.that(key2.getKeyData().getTypeUrl()).isEqualTo(keyTemplate2.getTypeUrl());
-    AesGcmKeyFormat aesGcmKeyFormat2 =
-        AesGcmKeyFormat.parseFrom(
-            keyTemplate2.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey2 =
-        AesGcmKey.parseFrom(key2.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesGcmKey2.getKeyValue().size()).isEqualTo(aesGcmKeyFormat2.getKeySize());
+    KeysetHandle result = keysetManager.getKeysetHandle();
+    expect.that(result.size()).isEqualTo(2);
+    expect.that(result.getAt(0).isPrimary()).isTrue();
+    expect.that(result.getAt(0).getKey().getParameters()).isEqualTo(keyTemplate1.toParameters());
+    expect.that(result.getAt(0).getId()).isEqualTo(keyHandle1.getId());
+    expect.that(result.getAt(0).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    expect.that(result.getAt(1).isPrimary()).isFalse();
+    expect.that(result.getAt(1).getKey().getParameters()).isEqualTo(keyTemplate2.toParameters());
+    expect.that(result.getAt(1).getId()).isEqualTo(keyHandle2.getId());
+    expect.that(result.getAt(1).getStatus()).isEqualTo(KeyStatus.ENABLED);
   }
 
   @Test
@@ -763,20 +816,12 @@
     keysetManager = keysetManager.add(keyHandle, keyAccess);
 
     KeysetHandle keysetHandle = keysetManager.getKeysetHandle();
-    Keyset keyset = keysetHandle.getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(1);
-    Keyset.Key key = keyset.getKey(0);
-    expect.that(key.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key.hasKeyData()).isTrue();
-    expect.that(key.getKeyData().getTypeUrl()).isEqualTo(keyTemplate.getTypeUrl());
-    AesGcmKeyFormat aesGcmKeyFormat =
-        AesGcmKeyFormat.parseFrom(keyTemplate.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey =
-        AesGcmKey.parseFrom(key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesGcmKey.getKeyValue().size()).isEqualTo(aesGcmKeyFormat.getKeySize());
-    // No primary key because add doesn't automatically promote the new key to primary.
-    assertThrows(GeneralSecurityException.class, () -> keysetHandle.getPrimitive(Aead.class));
+    expect.that(keysetHandle.size()).isEqualTo(1);
+    expect.that(keysetHandle.getAt(0).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    expect.that(keysetHandle.getAt(0).isPrimary()).isFalse();
+    expect
+        .that(keysetHandle.getAt(0).getKey().getParameters())
+        .isEqualTo(keyTemplate.toParameters());
   }
 
   @Test
@@ -793,32 +838,17 @@
     keysetManager = keysetManager.add(keyHandle, keyAccess);
 
     KeysetHandle keysetHandle = keysetManager.getKeysetHandle();
-    Keyset keyset = keysetHandle.getKeyset();
-    expect.that(keyset.getKeyCount()).isEqualTo(2);
-    Keyset.Key key1 = keyset.getKey(0);
-    expect.that(key1.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key1.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key1.hasKeyData()).isTrue();
-    expect.that(key1.getKeyData().getTypeUrl()).isEqualTo(keyTemplate1.getTypeUrl());
-    AesGcmKeyFormat aesGcmKeyFormat1 =
-        AesGcmKeyFormat.parseFrom(
-            keyTemplate1.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey1 =
-        AesGcmKey.parseFrom(key1.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesGcmKey1.getKeyValue().size()).isEqualTo(aesGcmKeyFormat1.getKeySize());
-    Keyset.Key key2 = keyset.getKey(1);
-    expect.that(key2.getStatus()).isEqualTo(KeyStatusType.ENABLED);
-    expect.that(key2.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
-    expect.that(key2.hasKeyData()).isTrue();
-    expect.that(key2.getKeyData().getTypeUrl()).isEqualTo(keyTemplate2.getTypeUrl());
-    AesGcmKeyFormat aesGcmKeyFormat2 =
-        AesGcmKeyFormat.parseFrom(
-            keyTemplate2.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    AesGcmKey aesGcmKey2 =
-        AesGcmKey.parseFrom(key2.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesGcmKey2.getKeyValue().size()).isEqualTo(aesGcmKeyFormat2.getKeySize());
-    // No primary key because add doesn't automatically promote the new key to primary.
-    assertThrows(GeneralSecurityException.class, () -> keysetHandle.getPrimitive(Aead.class));
+    assertThat(keysetHandle.size()).isEqualTo(2);
+    expect.that(keysetHandle.getAt(0).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    expect.that(keysetHandle.getAt(0).isPrimary()).isFalse();
+    expect
+        .that(keysetHandle.getAt(0).getKey().getParameters())
+        .isEqualTo(keyTemplate1.toParameters());
+    expect.that(keysetHandle.getAt(1).getStatus()).isEqualTo(KeyStatus.ENABLED);
+    expect.that(keysetHandle.getAt(1).isPrimary()).isFalse();
+    expect
+        .that(keysetHandle.getAt(1).getKey().getParameters())
+        .isEqualTo(keyTemplate2.toParameters());
   }
 
   @Test
@@ -845,19 +875,31 @@
 
   @Test
   public void testAddNewKey_onePrimary() throws Exception {
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
+
     KeysetManager keysetManager = KeysetManager.withEmptyKeyset();
-    int keyId = keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, true);
-    Keyset keyset = keysetManager.getKeysetHandle().getKeyset();
-    assertThat(keyset.getKeyCount()).isEqualTo(1);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(keyId);
-    TestUtil.assertHmacKey(KeyTemplates.get("HMAC_SHA256_128BITTAG"), keyset.getKey(0));
+    int keyId = keysetManager.addNewKey(template, true);
+    KeysetHandle keyset = keysetManager.getKeysetHandle();
+    assertThat(keyset.size()).isEqualTo(1);
+    assertThat(keyset.getAt(0).isPrimary()).isTrue();
+    assertThat(keyset.getAt(0).getId()).isEqualTo(keyId);
+    assertThat(keyset.getAt(0).getKey().getParameters())
+        .isEqualTo(KeyTemplates.get("HMAC_SHA256_128BITTAG").toParameters());
   }
 
   @Test
   public void testAddNewKey_onePrimaryAnotherPrimary() throws Exception {
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
+
     KeysetManager keysetManager = KeysetManager.withEmptyKeyset();
-    keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, true);
-    int primaryKeyId = keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, true);
+    keysetManager.addNewKey(template, true);
+    int primaryKeyId = keysetManager.addNewKey(template, true);
     Keyset keyset = keysetManager.getKeysetHandle().getKeyset();
     assertThat(keyset.getKeyCount()).isEqualTo(2);
     assertThat(keyset.getPrimaryKeyId()).isEqualTo(primaryKeyId);
@@ -865,9 +907,14 @@
 
   @Test
   public void testAddNewKey_primaryThenNonPrimary() throws Exception {
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
+
     KeysetManager keysetManager = KeysetManager.withEmptyKeyset();
-    int primaryKeyId = keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, true);
-    keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, false);
+    int primaryKeyId = keysetManager.addNewKey(template, true);
+    keysetManager.addNewKey(template, false);
     Keyset keyset = keysetManager.getKeysetHandle().getKeyset();
     assertThat(keyset.getKeyCount()).isEqualTo(2);
     assertThat(keyset.getPrimaryKeyId()).isEqualTo(primaryKeyId);
@@ -875,9 +922,14 @@
 
   @Test
   public void testAddNewKey_addThenDestroy() throws Exception {
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
+
     KeysetManager keysetManager = KeysetManager.withEmptyKeyset();
-    keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, true);
-    int secondaryKeyId = keysetManager.addNewKey(MacKeyTemplates.HMAC_SHA256_128BITTAG, false);
+    keysetManager.addNewKey(template, true);
+    int secondaryKeyId = keysetManager.addNewKey(template, false);
     keysetManager.destroy(secondaryKeyId);
     Keyset keyset = keysetManager.getKeysetHandle().getKeyset();
     assertThat(keyset.getKeyCount()).isEqualTo(2);
@@ -887,9 +939,12 @@
 
   private void manipulateKeyset(KeysetManager manager) {
     try {
-      com.google.crypto.tink.proto.KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+      com.google.crypto.tink.proto.KeyTemplate template =
+          com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+              TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+              ExtensionRegistryLite.getEmptyRegistry());
       manager.rotate(template).add(template).rotate(template).add(template);
-    } catch (GeneralSecurityException e) {
+    } catch (GeneralSecurityException | IOException e) {
       fail("should not throw exception: " + e);
     }
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/MonitoringUtilTest.java b/java_src/src/test/java/com/google/crypto/tink/MonitoringUtilTest.java
index 4f825ed..0ea2c1b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/MonitoringUtilTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/MonitoringUtilTest.java
@@ -24,6 +24,7 @@
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.TestUtil;
 import java.util.List;
 import org.junit.Test;
@@ -33,8 +34,8 @@
 @RunWith(JUnit4.class)
 public final class MonitoringUtilTest {
 
-  private static final byte[] KEY = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY2 = TestUtil.hexDecode("100102030405060708090a0b0c0d0e0f");
+  private static final byte[] KEY = Hex.decode("000102030405060708090a0b0c0d0e0f");
+  private static final byte[] KEY2 = Hex.decode("100102030405060708090a0b0c0d0e0f");
 
   @Test
   public void monitoringKeysetInfoFromPrimitiveSet() throws Exception {
@@ -55,9 +56,8 @@
     assertThat(entries).hasSize(1);
     assertThat(entries.get(0).getStatus()).isEqualTo(KeyStatus.ENABLED);
     assertThat(entries.get(0).getKeyId()).isEqualTo(42);
-    assertThat(entries.get(0).getParameters().toString())
-        .isEqualTo(
-            "(typeUrl=type.googleapis.com/google.crypto.tink.AesGcmKey, outputPrefixType=TINK)");
+    assertThat(entries.get(0).getKeyType()).isEqualTo("tink.AesGcmKey");
+    assertThat(entries.get(0).getKeyPrefix()).isEqualTo("TINK");
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java b/java_src/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java
index 4074ce5..95a8eca 100644
--- a/java_src/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/NoSecretKeysetHandleTest.java
@@ -16,15 +16,19 @@
 
 package com.google.crypto.tink;
 
-import static com.google.crypto.tink.testing.TestUtil.assertExceptionContains;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertThrows;
 
+import com.google.common.truth.Expect;
 import com.google.crypto.tink.config.TinkConfig;
-import com.google.crypto.tink.mac.MacKeyTemplates;
-import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.testing.TestUtil;
+import java.io.IOException;
 import java.security.GeneralSecurityException;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -32,38 +36,113 @@
 /** Tests for NoSecretKeysetHandle. */
 @RunWith(JUnit4.class)
 public class NoSecretKeysetHandleTest {
+  @Rule public final Expect expect = Expect.create();
+
   @BeforeClass
   public static void setUp() throws GeneralSecurityException {
     TinkConfig.register();
   }
 
+  @SuppressWarnings("deprecation")  // This is a test for deprecated functions
   @Test
-  public void testBasic() throws Exception {
-    // Create a keyset that contains a single HmacKey.
-    KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
-    KeysetManager manager = KeysetManager.withEmptyKeyset().rotate(template);
-    Keyset keyset = manager.getKeysetHandle().getKeyset();
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class,
-            () -> {
-              KeysetHandle unused = NoSecretKeysetHandle.parseFrom(keyset.toByteArray());
-            });
-    assertExceptionContains(e, "keyset contains secret key material");
+  public void withTypeAsymmetricPublic_deprecated_noSecretKeysetHandle_sameAs_readNoSecret()
+      throws Exception {
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
+    Keyset keyset = privateHandle.getPublicKeysetHandle().getKeyset();
+
+    Keyset keyset2 = NoSecretKeysetHandle.parseFrom(keyset.toByteArray()).getKeyset();
+    Keyset keyset3 =
+        NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(keyset.toByteArray())).getKeyset();
+
+    Keyset keyset4 = KeysetHandle.readNoSecret(keyset.toByteArray()).getKeyset();
+    Keyset keyset5 =
+        KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(keyset.toByteArray())).getKeyset();
+
+    expect.that(keyset).isEqualTo(keyset2);
+    expect.that(keyset).isEqualTo(keyset3);
+    expect.that(keyset).isEqualTo(keyset4);
+    expect.that(keyset).isEqualTo(keyset5);
   }
 
+  @SuppressWarnings("deprecation")  // This is a test for the deprecated functions
   @Test
-  public void testVoidInputs() throws Exception {
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class,
-            () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0])));
-    assertExceptionContains(e, "empty keyset");
+  public void withTypeSymmetric_deprecated_noSecretKeysetHandle_sameAs_readNoSecret()
+      throws Exception {
+    String keyValue = "01234567890123456";
+    Keyset keyset =
+        TestUtil.createKeyset(
+            TestUtil.createKey(
+                TestUtil.createHmacKeyData(keyValue.getBytes(UTF_8), 16),
+                42,
+                KeyStatusType.ENABLED,
+                OutputPrefixType.TINK));
 
-    e =
-        assertThrows(
-            GeneralSecurityException.class,
-            () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0])));
-    assertExceptionContains(e, "empty keyset");
+    assertThrows(
+        GeneralSecurityException.class, () -> NoSecretKeysetHandle.parseFrom(keyset.toByteArray()));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+
+    assertThrows(
+        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(keyset.toByteArray()));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+  }
+
+  @SuppressWarnings("deprecation")  // This is a test for deprecated functions
+  @Test
+  public void withTypeAsymmetricPrivate_deprecated_noSecretKeysetHandle_sameAs_readNoSecret()
+      throws Exception {
+    Keyset keyset = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256")).getKeyset();
+
+    assertThrows(
+        GeneralSecurityException.class, () -> NoSecretKeysetHandle.parseFrom(keyset.toByteArray()));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+
+    assertThrows(
+        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(keyset.toByteArray()));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(keyset.toByteArray())));
+  }
+
+  @SuppressWarnings("deprecation")  // This is a test for deprecated functions
+  @Test
+  public void withEmptyKeyset_deprecated_noSecretKeysetHandle_sameAs_readNoSecret()
+      throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> NoSecretKeysetHandle.parseFrom(new byte[0]));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(new byte[0])));
+
+    assertThrows(GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(new byte[0]));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(new byte[0])));
+  }
+
+  @SuppressWarnings("deprecation")  // This is a test for the deprecated function
+  @Test
+  public void withInvalidKeyset_deprecated_noSecretKeysetHandle_almostSameAs_readNoSecret()
+      throws Exception {
+    byte[] invalidSerializedProto = new byte[] {0x00, 0x01, 0x02};
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> NoSecretKeysetHandle.parseFrom(invalidSerializedProto));
+    assertThrows(
+        IOException.class, // This is inconsistent
+        () -> NoSecretKeysetHandle.read(BinaryKeysetReader.withBytes(invalidSerializedProto)));
+
+    assertThrows(
+        GeneralSecurityException.class, () -> KeysetHandle.readNoSecret(invalidSerializedProto));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(invalidSerializedProto)));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/PrimitiveSetTest.java b/java_src/src/test/java/com/google/crypto/tink/PrimitiveSetTest.java
index f1d36c9..271d06a 100644
--- a/java_src/src/test/java/com/google/crypto/tink/PrimitiveSetTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/PrimitiveSetTest.java
@@ -24,16 +24,23 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.internal.LegacyProtoKey;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacKeyManager;
 import com.google.crypto.tink.monitoring.MonitoringAnnotations;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HmacParams;
 import com.google.crypto.tink.proto.KeyData;
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.testing.TestUtil;
+import com.google.protobuf.ByteString;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -70,6 +77,11 @@
     }
   }
 
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    HmacKeyManager.register(true);
+  }
+
   @Test
   public void testBasicFunctionalityWithDeprecatedMutableInterface() throws Exception {
     PrimitiveSet<Mac> pset = PrimitiveSet.newPrimitiveSet(Mac.class);
@@ -97,7 +109,7 @@
 
     assertThat(pset.getAll()).hasSize(3);
 
-    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(key1);
+    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1));
     assertThat(entries).hasSize(1);
     PrimitiveSet.Entry<Mac> entry = entries.get(0);
     assertEquals(
@@ -105,9 +117,9 @@
     assertEquals(KeyStatusType.ENABLED, entry.getStatus());
     assertEquals(CryptoFormat.TINK_START_BYTE, entry.getIdentifier()[0]);
     assertArrayEquals(CryptoFormat.getOutputPrefix(key1), entry.getIdentifier());
-    assertEquals(entry.getKeyId(), 1);
+    assertEquals(1, entry.getKeyId());
 
-    entries = pset.getPrimitive(key2);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2));
     assertThat(entries).hasSize(1);
     entry = entries.get(0);
     assertEquals(
@@ -117,7 +129,7 @@
     assertArrayEquals(CryptoFormat.getOutputPrefix(key2), entry.getIdentifier());
     assertEquals(2, entry.getKeyId());
 
-    entries = pset.getPrimitive(key3);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3));
     assertThat(entries).hasSize(1);
     entry = entries.get(0);
     assertEquals(
@@ -125,7 +137,7 @@
     assertEquals(KeyStatusType.ENABLED, entry.getStatus());
     assertEquals(CryptoFormat.LEGACY_START_BYTE, entry.getIdentifier()[0]);
     assertArrayEquals(CryptoFormat.getOutputPrefix(key3), entry.getIdentifier());
-    assertEquals(entry.getKeyId(), 3);
+    assertEquals(3, entry.getKeyId());
 
     entry = pset.getPrimary();
     assertEquals(
@@ -179,7 +191,7 @@
 
     assertThat(pset.getAll()).hasSize(3);
 
-    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(key1);
+    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1));
     assertThat(entries).hasSize(1);
     PrimitiveSet.Entry<Mac> entry = entries.get(0);
     assertEquals(
@@ -187,9 +199,9 @@
     assertEquals(KeyStatusType.ENABLED, entry.getStatus());
     assertEquals(CryptoFormat.TINK_START_BYTE, entry.getIdentifier()[0]);
     assertArrayEquals(CryptoFormat.getOutputPrefix(key1), entry.getIdentifier());
-    assertEquals(entry.getKeyId(), 1);
+    assertEquals(1, entry.getKeyId());
 
-    entries = pset.getPrimitive(key2);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2));
     assertThat(entries).hasSize(1);
     entry = entries.get(0);
     assertEquals(
@@ -199,7 +211,7 @@
     assertArrayEquals(CryptoFormat.getOutputPrefix(key2), entry.getIdentifier());
     assertEquals(2, entry.getKeyId());
 
-    entries = pset.getPrimitive(key3);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3));
     assertThat(entries).hasSize(1);
     entry = entries.get(0);
     assertEquals(
@@ -207,7 +219,7 @@
     assertEquals(KeyStatusType.ENABLED, entry.getStatus());
     assertEquals(CryptoFormat.LEGACY_START_BYTE, entry.getIdentifier()[0]);
     assertArrayEquals(CryptoFormat.getOutputPrefix(key3), entry.getIdentifier());
-    assertEquals(entry.getKeyId(), 3);
+    assertEquals(3, entry.getKeyId());
 
     entry = pset.getPrimary();
     assertEquals(
@@ -241,6 +253,227 @@
   }
 
   @Test
+  public void testAddFullPrimitiveAndOptionalPrimitive_works() throws Exception {
+    Key key1 =
+        Key.newBuilder()
+            .setKeyId(1)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .build();
+    Key key2 =
+        Key.newBuilder()
+            .setKeyId(2)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build();
+    Key key3 =
+        Key.newBuilder()
+            .setKeyId(3)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .build();
+    PrimitiveSet<Mac> pset =
+        PrimitiveSet.newBuilder(Mac.class)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), null, key1)
+            .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac2(), null, key2)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), new DummyMac2(), key3)
+            .build();
+
+    assertThat(pset.getAll()).hasSize(3);
+
+    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1));
+    assertThat(entries).hasSize(1);
+
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2));
+    assertThat(entries).hasSize(1);
+
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3));
+    assertThat(entries).hasSize(1);
+
+    PrimitiveSet.Entry<Mac> entry = pset.getPrimary();
+    assertThat(entry).isNotNull();
+  }
+
+  @Test
+  public void testAddFullPrimitiveAndOptionalPrimitive_fullPrimitiveHandledCorrectly()
+      throws Exception {
+    Key key1 =
+        Key.newBuilder()
+            .setKeyId(1)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .build();
+    Key key2 =
+        Key.newBuilder()
+            .setKeyId(2)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build();
+    Key key3 =
+        Key.newBuilder()
+            .setKeyId(3)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .build();
+    Key key4 =
+        Key.newBuilder()
+            .setKeyId(4)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .build();
+    PrimitiveSet<Mac> pset =
+        PrimitiveSet.newBuilder(Mac.class)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), null, key1)
+            .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac2(), null, key2)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), new DummyMac2(), key3)
+            .addFullPrimitiveAndOptionalPrimitive(null, new DummyMac2(), key4)
+            .build();
+
+    PrimitiveSet.Entry<Mac> entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1)).get(0);
+    assertEquals(
+        DummyMac1.class.getSimpleName(),
+        new String(entry.getFullPrimitive().computeMac(null), UTF_8));
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2)).get(0);
+    assertEquals(
+        DummyMac2.class.getSimpleName(),
+        new String(entry.getFullPrimitive().computeMac(null), UTF_8));
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3)).get(0);
+    assertEquals(
+        DummyMac1.class.getSimpleName(),
+        new String(entry.getFullPrimitive().computeMac(null), UTF_8));
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key4)).get(0);
+    assertThat(entry.getFullPrimitive()).isNull();
+
+    entry = pset.getPrimary();
+    assertEquals(
+        DummyMac2.class.getSimpleName(),
+        new String(entry.getFullPrimitive().computeMac(null), UTF_8));
+  }
+
+  @Test
+  public void testAddFullPrimitiveAndOptionalPrimitive_keysHandledCorrectly() throws Exception {
+    Key key1 =
+        Key.newBuilder()
+            .setKeyId(1)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .build();
+    Key key2 =
+        Key.newBuilder()
+            .setKeyId(2)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build();
+    Key key3 =
+        Key.newBuilder()
+            .setKeyId(3)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .build();
+    PrimitiveSet<Mac> pset =
+        PrimitiveSet.newBuilder(Mac.class)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), null, key1)
+            .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac2(), null, key2)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), new DummyMac2(), key3)
+            .build();
+
+    PrimitiveSet.Entry<Mac> entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1)).get(0);
+    assertEquals(KeyStatusType.ENABLED, entry.getStatus());
+    assertArrayEquals(CryptoFormat.getOutputPrefix(key1), entry.getIdentifier());
+    assertEquals(1, entry.getKeyId());
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2)).get(0);
+    assertEquals(KeyStatusType.ENABLED, entry.getStatus());
+    assertArrayEquals(CryptoFormat.getOutputPrefix(key2), entry.getIdentifier());
+    assertEquals(2, entry.getKeyId());
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3)).get(0);
+    assertEquals(KeyStatusType.ENABLED, entry.getStatus());
+    assertArrayEquals(CryptoFormat.getOutputPrefix(key3), entry.getIdentifier());
+    assertEquals(3, entry.getKeyId());
+
+    entry = pset.getPrimary();
+    assertEquals(KeyStatusType.ENABLED, entry.getStatus());
+    assertArrayEquals(CryptoFormat.getOutputPrefix(key2), entry.getIdentifier());
+    assertEquals(2, entry.getKeyId());
+  }
+
+  @Test
+  public void testAddFullPrimitiveAndOptionalPrimitive_primitiveHandledCorrectly()
+      throws Exception {
+    Key key1 =
+        Key.newBuilder()
+            .setKeyId(1)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .build();
+    Key key2 =
+        Key.newBuilder()
+            .setKeyId(2)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build();
+    Key key3 =
+        Key.newBuilder()
+            .setKeyId(3)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .build();
+    PrimitiveSet<Mac> pset =
+        PrimitiveSet.newBuilder(Mac.class)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), null, key1)
+            .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac2(), null, key2)
+            .addFullPrimitiveAndOptionalPrimitive(new DummyMac1(), new DummyMac2(), key3)
+            .build();
+
+    PrimitiveSet.Entry<Mac> entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1)).get(0);
+    assertThat(entry.getPrimitive()).isNull();
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2)).get(0);
+    assertThat(entry.getPrimitive()).isNull();
+
+    entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3)).get(0);
+    assertEquals(
+        DummyMac2.class.getSimpleName(), new String(entry.getPrimitive().computeMac(null), UTF_8));
+
+    entry = pset.getPrimary();
+    assertThat(entry.getPrimitive()).isNull();
+  }
+
+  @Test
+  public void testAddFullPrimitiveAndOptionalPrimitive_throwsOnDoublePrimaryAdd() throws Exception {
+    Key key1 =
+        Key.newBuilder()
+            .setKeyId(1)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .build();
+    Key key2 =
+        Key.newBuilder()
+            .setKeyId(2)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build();
+    assertThrows(
+        IllegalStateException.class,
+        () ->
+            PrimitiveSet.newBuilder(Mac.class)
+                .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac1(), null, key1)
+                .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac2(), null, key2)
+                .build());
+    assertThrows(
+        IllegalStateException.class,
+        () ->
+            PrimitiveSet.newBuilder(Mac.class)
+                .addPrimaryFullPrimitiveAndOptionalPrimitive(new DummyMac1(), null, key1)
+                .addPrimaryPrimitive(new DummyMac2(), key2)
+                .build());
+  }
+
+  @Test
   public void testNoPrimary_getPrimaryReturnsNull() throws Exception {
     Key key =
         Key.newBuilder()
@@ -265,18 +498,23 @@
             .build();
     pset.addPrimitive(new DummyMac1(), key1);
 
-    PrimitiveSet.Entry<Mac> entry = pset.getPrimitive(key1).get(0);
+    PrimitiveSet.Entry<Mac> entry = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1)).get(0);
     assertThat(entry.getParameters().toString())
         .isEqualTo("(typeUrl=typeUrl1, outputPrefixType=TINK)");
 
     PrimitiveSet<Mac> pset2 =
         PrimitiveSet.newBuilder(Mac.class).addPrimaryPrimitive(new DummyMac1(), key1).build();
-    assertThat(pset2.getPrimitive(key1).get(0).getParameters().toString())
+    assertThat(
+            pset2
+                .getPrimitive(CryptoFormat.getOutputPrefix(key1))
+                .get(0)
+                .getParameters()
+                .toString())
         .isEqualTo("(typeUrl=typeUrl1, outputPrefixType=TINK)");
   }
 
   @Test
-  public void testEntryGetKey() throws Exception {
+  public void getKeyWithoutParser_givesLegacyProtoKey() throws Exception {
     PrimitiveSet.Builder<Mac> builder = PrimitiveSet.newBuilder(Mac.class);
     Key key1 =
         Key.newBuilder()
@@ -287,7 +525,8 @@
             .build();
     builder.addPrimitive(new DummyMac1(), key1);
     PrimitiveSet<Mac> pset = builder.build();
-    com.google.crypto.tink.Key key = pset.getPrimitive(key1).get(0).getKey();
+    com.google.crypto.tink.Key key =
+        pset.getPrimitive(CryptoFormat.getOutputPrefix(key1)).get(0).getKey();
 
     assertThat(key).isInstanceOf(LegacyProtoKey.class);
     LegacyProtoKey legacyProtoKey = (LegacyProtoKey) key;
@@ -296,6 +535,50 @@
   }
 
   @Test
+  public void getKeyWithParser_works() throws Exception {
+    // HmacKey's proto serialization HmacProtoSerialization is registed in HmacKeyManager.
+    Key protoKey =
+        TestUtil.createKey(
+            TestUtil.createHmacKeyData("01234567890123456".getBytes(UTF_8), 16),
+            /* keyId= */ 42,
+            KeyStatusType.ENABLED,
+            OutputPrefixType.TINK);
+    byte[] prefix = CryptoFormat.getOutputPrefix(protoKey);
+    PrimitiveSet.Builder<Mac> builder = PrimitiveSet.newBuilder(Mac.class);
+    builder.addPrimitive(new DummyMac1(), protoKey);
+    PrimitiveSet<Mac> pset = builder.build();
+
+    com.google.crypto.tink.Key key = pset.getPrimitive(prefix).get(0).getKey();
+    assertThat(key).isInstanceOf(HmacKey.class);
+    HmacKey hmacKey = (HmacKey) key;
+    assertThat(hmacKey.getIdRequirementOrNull()).isEqualTo(42);
+  }
+
+  @Test
+  public void addPrimitiveWithInvalidKeyThatHasAParser_throws() throws Exception {
+    // HmacKey's proto serialization HmacProtoSerialization is registed in HmacKeyManager.
+    com.google.crypto.tink.proto.HmacKey invalidProtoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(999)
+            .setKeyValue(ByteString.copyFromUtf8("01234567890123456"))
+            .setParams(HmacParams.newBuilder().setHash(HashType.UNKNOWN_HASH).setTagSize(0))
+            .build();
+    Key protoKey =
+        TestUtil.createKey(
+            TestUtil.createKeyData(
+                invalidProtoHmacKey,
+                "type.googleapis.com/google.crypto.tink.HmacKey",
+                KeyData.KeyMaterialType.SYMMETRIC),
+            /* keyId= */ 42,
+            KeyStatusType.ENABLED,
+            OutputPrefixType.TINK);
+
+    PrimitiveSet.Builder<Mac> builder = PrimitiveSet.newBuilder(Mac.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> builder.addPrimitive(new DummyMac1(), protoKey));
+  }
+
+  @Test
   public void testWithAnnotations() throws Exception {
     MonitoringAnnotations annotations =
         MonitoringAnnotations.newBuilder().add("name", "value").build();
@@ -369,7 +652,7 @@
     assertThat(pset.getAll()).hasSize(3); // 3 instead of 6 because of duplicated key ids
 
     // tink keys
-    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(key1);
+    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1));
     assertThat(entries).hasSize(1);
     PrimitiveSet.Entry<Mac> entry = entries.get(0);
     assertEquals(
@@ -381,7 +664,7 @@
 
     // raw keys
     List<Integer> ids = new ArrayList<>(); // The order of the keys is an implementation detail.
-    entries = pset.getPrimitive(key2);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2));
     assertThat(entries).hasSize(3);
     entry = entries.get(0);
     assertEquals(
@@ -404,7 +687,7 @@
 
     assertThat(ids).containsExactly(1, 3, 3);
     // legacy keys
-    entries = pset.getPrimitive(key3);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3));
     assertEquals(2, entries.size());
     entry = entries.get(0);
     assertEquals(
@@ -480,7 +763,7 @@
     assertThat(pset.getAll()).hasSize(3); // 3 instead of 6 because of duplicated key ids
 
     // tink keys
-    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(key1);
+    List<PrimitiveSet.Entry<Mac>> entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key1));
     assertThat(entries).hasSize(1);
     PrimitiveSet.Entry<Mac> entry = entries.get(0);
     assertEquals(
@@ -492,7 +775,7 @@
 
     // raw keys
     List<Integer> ids = new ArrayList<>(); // The order of the keys is an implementation detail.
-    entries = pset.getPrimitive(key2);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key2));
     assertThat(entries).hasSize(3);
     entry = entries.get(0);
     assertEquals(
@@ -515,7 +798,7 @@
 
     assertThat(ids).containsExactly(1, 3, 3);
     // legacy keys
-    entries = pset.getPrimitive(key3);
+    entries = pset.getPrimitive(CryptoFormat.getOutputPrefix(key3));
     assertThat(entries).hasSize(2);
     entry = entries.get(0);
     assertEquals(
@@ -550,8 +833,7 @@
 
     assertThrows(
         GeneralSecurityException.class,
-        () ->
-            PrimitiveSet.newBuilder(Mac.class).addPrimitive(new DummyMac1(), key1).build());
+        () -> PrimitiveSet.newBuilder(Mac.class).addPrimitive(new DummyMac1(), key1).build());
     assertThrows(
         GeneralSecurityException.class,
         () ->
@@ -574,8 +856,7 @@
 
     assertThrows(
         GeneralSecurityException.class,
-        () ->
-            PrimitiveSet.newBuilder(Mac.class).addPrimitive(new DummyMac1(), key1).build());
+        () -> PrimitiveSet.newBuilder(Mac.class).addPrimitive(new DummyMac1(), key1).build());
     assertThrows(
         GeneralSecurityException.class,
         () ->
@@ -649,4 +930,38 @@
     assertThat(pset.getPrimitive(Hex.decode("00ffffffff"))).isEmpty();
     assertThat(pset.getPrimitive(Hex.decode("00ffffffef"))).hasSize(1);
   }
+
+  @Test
+  public void getAllInKeysetOrder_works() throws Exception {
+    Key key0 =
+        Key.newBuilder()
+            .setKeyId(0xffffffff)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .build();
+    Key key1 =
+        Key.newBuilder()
+            .setKeyId(0xffffffdf)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .build();
+    Key key2 =
+        Key.newBuilder()
+            .setKeyId(0xffffffef)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .build();
+    PrimitiveSet<Mac> pset =
+        PrimitiveSet.newBuilder(Mac.class)
+            .addPrimitive(new DummyMac1(), key0)
+            .addPrimaryPrimitive(new DummyMac2(), key1)
+            .addPrimitive(new DummyMac1(), key2)
+            .build();
+
+    List<PrimitiveSet.Entry<Mac>> entries = pset.getAllInKeysetOrder();
+    assertThat(entries).hasSize(3);
+    assertThat(entries.get(0).getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
+    assertThat(entries.get(1).getOutputPrefixType()).isEqualTo(OutputPrefixType.RAW);
+    assertThat(entries.get(2).getOutputPrefixType()).isEqualTo(OutputPrefixType.LEGACY);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/RegistryMultithreadTest.java b/java_src/src/test/java/com/google/crypto/tink/RegistryMultithreadTest.java
index 1dd7a41..39ac80d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/RegistryMultithreadTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/RegistryMultithreadTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertNotNull;
 
 import com.google.crypto.tink.internal.KeyTypeManager;
 import com.google.crypto.tink.internal.PrivateKeyTypeManager;
@@ -28,7 +29,6 @@
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.MessageLite;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.List;
@@ -52,22 +52,7 @@
     private final String typeUrl;
 
     @Override
-    public Primitive getPrimitive(ByteString proto) throws GeneralSecurityException {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
-    public Primitive getPrimitive(MessageLite proto) throws GeneralSecurityException {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
-    public MessageLite newKey(ByteString template) throws GeneralSecurityException {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
-    public MessageLite newKey(MessageLite template) throws GeneralSecurityException {
+    public Primitive getPrimitive(ByteString serializedKey) throws GeneralSecurityException {
       throw new UnsupportedOperationException("Not needed for test");
     }
 
@@ -77,21 +62,11 @@
     }
 
     @Override
-    public boolean doesSupport(String typeUrl) {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
     public String getKeyType() {
       return this.typeUrl;
     }
 
     @Override
-    public int getVersion() {
-      throw new UnsupportedOperationException("Not needed for test");
-    }
-
-    @Override
     public Class<Primitive> getPrimitiveClass() {
       return Primitive.class;
     }
@@ -252,11 +227,10 @@
             () -> {
               try {
                 for (int i = 0; i < REPETITIONS; ++i) {
-
-                  Registry.getKeyManager("KeyManagerStart");
-                  Registry.getKeyManager("KeyTypeManagerStart");
-                  Registry.getKeyManager("PrivateKeyTypeManagerStart");
-                  Registry.getKeyManager("PublicKeyTypeManagerStart");
+                  assertNotNull(Registry.getUntypedKeyManager("KeyManagerStart"));
+                  assertNotNull(Registry.getUntypedKeyManager("KeyTypeManagerStart"));
+                  assertNotNull(Registry.getUntypedKeyManager("PrivateKeyTypeManagerStart"));
+                  assertNotNull(Registry.getUntypedKeyManager("PublicKeyTypeManagerStart"));
                 }
               } catch (GeneralSecurityException e) {
                 throw new RuntimeException(e);
diff --git a/java_src/src/test/java/com/google/crypto/tink/RegistryTest.java b/java_src/src/test/java/com/google/crypto/tink/RegistryTest.java
index 3b65a3d..5b3c9ef 100644
--- a/java_src/src/test/java/com/google/crypto/tink/RegistryTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/RegistryTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.crypto.tink.testing.TestUtil.assertExceptionContains;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -33,7 +34,7 @@
 import com.google.crypto.tink.internal.PrimitiveFactory;
 import com.google.crypto.tink.internal.PrivateKeyTypeManager;
 import com.google.crypto.tink.mac.MacConfig;
-import com.google.crypto.tink.mac.MacKeyTemplates;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
 import com.google.crypto.tink.proto.AesEaxKey;
 import com.google.crypto.tink.proto.AesGcmKey;
 import com.google.crypto.tink.proto.AesGcmKeyFormat;
@@ -136,6 +137,8 @@
   }
 
   private static class AeadToEncryptOnlyWrapper implements PrimitiveWrapper<Aead, EncryptOnly> {
+    public static final AeadToEncryptOnlyWrapper WRAPPER = new AeadToEncryptOnlyWrapper();
+
     @Override
     public EncryptOnly wrap(PrimitiveSet<Aead> set) throws GeneralSecurityException {
       return new EncryptOnly() {
@@ -168,7 +171,7 @@
     TinkFipsUtil.unsetFipsRestricted();
     Registry.reset();
     TinkConfig.register();
-    Registry.registerPrimitiveWrapper(new AeadToEncryptOnlyWrapper());
+    Registry.registerPrimitiveWrapper(AeadToEncryptOnlyWrapper.WRAPPER);
   }
 
   private void testGetKeyManagerShouldWork(String typeUrl, String className) throws Exception {
@@ -207,7 +210,13 @@
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
     KeyManager<Aead> wrongType = Registry.getKeyManager(MacConfig.HMAC_TYPE_URL);
-    HmacKey hmacKey = (HmacKey) Registry.newKey(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    HmacKey hmacKey =
+        (HmacKey)
+            Registry.newKey(
+                com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+                    TinkProtoParametersFormat.serialize(
+                        PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+                    ExtensionRegistryLite.getEmptyRegistry()));
 
     ClassCastException e =
         assertThrows(
@@ -407,7 +416,11 @@
 
   @Test
   public void testGetPublicKeyData_shouldThrow() throws Exception {
-    KeyData keyData = Registry.newKeyData(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    KeyData keyData =
+        Registry.newKeyData(
+            com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+                TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+                ExtensionRegistryLite.getEmptyRegistry()));
     GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
@@ -458,7 +471,10 @@
     // Skip test if in FIPS mode, as no provider available to instantiate.
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    com.google.crypto.tink.proto.KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
     HmacKey hmacKey = (HmacKey) Registry.newKey(template);
     KeyData hmacKeyData = Registry.newKeyData(template);
     Mac mac = Registry.getPrimitive(hmacKeyData);
@@ -476,7 +492,10 @@
     // Skip test if in FIPS mode, as no provider available to instantiate.
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    com.google.crypto.tink.proto.KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
+    com.google.crypto.tink.proto.KeyTemplate template =
+        com.google.crypto.tink.proto.KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+            ExtensionRegistryLite.getEmptyRegistry());
     HmacKey hmacKey = (HmacKey) Registry.newKey(template);
     KeyData hmacKeyData = Registry.newKeyData(template);
     Mac mac = Registry.getPrimitive(hmacKeyData, Mac.class);
@@ -708,7 +727,7 @@
             throws GeneralSecurityException {
           byte[] pseudorandomness = new byte[format.getKeySize()];
           try {
-            stream.read(pseudorandomness);
+            readFully(stream, pseudorandomness);
           } catch (IOException e) {
             throw new AssertionError("Unexpected IOException", e);
           }
@@ -914,22 +933,6 @@
   }
 
   @Test
-  public void testParseKeyData_succeeds() throws Exception {
-    Registry.reset();
-    Registry.registerKeyManager(new TestKeyTypeManager(), true);
-    AesGcmKey key =
-        AesGcmKey.newBuilder()
-            .setKeyValue(ByteString.copyFrom("0123456789abcdef".getBytes(UTF_8)))
-            .build();
-    KeyData keyData =
-        KeyData.newBuilder()
-            .setTypeUrl(new TestKeyTypeManager().getKeyType())
-            .setValue(key.toByteString())
-            .build();
-    assertThat(Registry.parseKeyData(keyData)).isEqualTo(key);
-  }
-
-  @Test
   public void testDeriveKey_succeeds() throws Exception {
     Registry.reset();
     Registry.registerKeyManager(new TestKeyTypeManager(), true);
@@ -1699,7 +1702,6 @@
 
     assertThat(count).isEqualTo(2);
   }
-  // TODO(przydatek): Add more tests for creation of PrimitiveSets.
 
   private static PrimitiveSet<Aead> createAeadPrimitiveSet() throws Exception {
     return TestUtil.createPrimitiveSet(
@@ -1718,7 +1720,7 @@
     // Skip test if in FIPS mode, as EAX is not allowed in FipsMode.
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    Registry.wrap(createAeadPrimitiveSet());
+    assertNotNull(Registry.wrap(createAeadPrimitiveSet()));
   }
 
   @Test
@@ -1777,7 +1779,24 @@
   }
 
   @Test
+  public void testFips_succeedsOnSuccessiveRestrictToFips() throws Exception {
+    Registry.reset();
+    Registry.restrictToFipsIfEmpty();
+    Registry.restrictToFipsIfEmpty();
+    Registry.restrictToFipsIfEmpty();
+    assertTrue(TinkFipsUtil.useOnlyFips());
+  }
+
+  @Test
+  public void testFips_succeedsOnRestrictToFipsWhenBuiltInFipsMode() throws Exception {
+    Assume.assumeTrue(TinkFipsUtil.useOnlyFips());
+    Registry.restrictToFipsIfEmpty();
+    assertTrue(TinkFipsUtil.useOnlyFips());
+  }
+
+  @Test
   public void testFips_failsOnNonEmptyRegistry() throws Exception {
+    Assume.assumeFalse(TinkFipsUtil.useOnlyFips());
     assertThrows(GeneralSecurityException.class, Registry::restrictToFipsIfEmpty);
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/TinkJsonProtoKeysetFormatTest.java b/java_src/src/test/java/com/google/crypto/tink/TinkJsonProtoKeysetFormatTest.java
new file mode 100644
index 0000000..fb2e99f
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/TinkJsonProtoKeysetFormatTest.java
@@ -0,0 +1,385 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.signature.SignatureConfig;
+import com.google.crypto.tink.subtle.Hex;
+import java.io.ByteArrayOutputStream;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class TinkJsonProtoKeysetFormatTest {
+
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    MacConfig.register();
+    AeadConfig.register();
+    SignatureConfig.register();
+  }
+
+  private void assertKeysetHandleAreEqual(KeysetHandle keysetHandle1, KeysetHandle keysetHandle2)
+      throws Exception {
+    // This assertion is too strong, but it works here because we don't parse or serialize
+    // keydata.value fields.
+    assertThat(CleartextKeysetHandle.getKeyset(keysetHandle2))
+        .isEqualTo(CleartextKeysetHandle.getKeyset(keysetHandle1));
+  }
+
+  private KeysetHandle generateKeyset() throws GeneralSecurityException {
+    return KeysetHandle.newBuilder()
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                .withRandomId()
+                .makePrimary())
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG_RAW")
+                .withRandomId())
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG")
+                .withRandomId()
+                .setStatus(KeyStatus.DESTROYED))
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG_RAW")
+                .withRandomId()
+                .setStatus(KeyStatus.DISABLED))
+        .addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId())
+        .build();
+  }
+
+  private KeysetHandle generatePublicKeyset() throws GeneralSecurityException {
+    return KeysetHandle.newBuilder()
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("ECDSA_P256_RAW")
+                .withRandomId()
+                .setStatus(KeyStatus.DISABLED))
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("ECDSA_P256").withRandomId().makePrimary())
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("ECDSA_P521")
+                .withRandomId()
+                .setStatus(KeyStatus.DESTROYED))
+        .build()
+        .getPublicKeysetHandle();
+  }
+
+  private Aead generateAead() throws GeneralSecurityException {
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_CTR_HMAC_SHA256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    return handle.getPrimitive(Aead.class);
+  }
+
+  @Test
+  public void serializeAndParse_successWithSameKeyset() throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+    KeysetHandle parseKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void serializeKeyset_withoutInsecureSecretKeyAccess_fails() throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    assertThrows(
+        NullPointerException.class,
+        () -> TinkJsonProtoKeysetFormat.serializeKeyset(keysetHandle, null));
+  }
+
+  @Test
+  public void parseKeyset_withoutInsecureSecretKeyAccess_fails() throws Exception {
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(generateKeyset(), InsecureSecretKeyAccess.get());
+
+    assertThrows(
+        NullPointerException.class,
+        () -> TinkJsonProtoKeysetFormat.parseKeyset(serializedKeyset, null));
+  }
+
+  @Test
+  public void parseInvalidSerializedKeyset_fails() throws Exception {
+    String invalidSerializedKeyset = "invalid";
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkJsonProtoKeysetFormat.parseKeyset(
+                invalidSerializedKeyset, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void serializeEncryptedAndParseEncrypted_successWithSameKeyset() throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, associatedData);
+    KeysetHandle parseKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            serializedKeyset, keyEncryptionAead, associatedData);
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseEncryptedKeysetWithInvalidKey_fails() throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    Aead invalidKeyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, associatedData);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+                serializedKeyset, invalidKeyEncryptionAead, associatedData));
+  }
+
+  @Test
+  public void parseEncryptedKeysetWithInvalidAssociatedData_fails() throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, "associatedData".getBytes(UTF_8));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+                serializedKeyset, keyEncryptionAead, "invalidAssociatedData".getBytes(UTF_8)));
+  }
+
+  @Test
+  public void serializeAndParseWithoutSecret_successWithSameKeyset() throws Exception {
+    KeysetHandle publicKeysetHandle = generatePublicKeyset();
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeysetWithoutSecret(publicKeysetHandle);
+    KeysetHandle parsePublicKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(serializedKeyset);
+
+    assertKeysetHandleAreEqual(publicKeysetHandle, parsePublicKeysetHandle);
+  }
+
+  @Test
+  public void serializeWithoutSecret_keysetWithSecretKeys_fails() throws Exception {
+    KeysetHandle secretKeysetHandle = generateKeyset();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkJsonProtoKeysetFormat.serializeKeysetWithoutSecret(secretKeysetHandle));
+  }
+
+  @Test
+  public void parseWithoutSecret_keysetWithSecretKeys_fails() throws Exception {
+    KeysetHandle secretKeysetHandle = generateKeyset();
+    String serializedSecretKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(
+            secretKeysetHandle, InsecureSecretKeyAccess.get());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(serializedSecretKeyset));
+  }
+
+  @Test
+  public void parseWithoutSecretInvalidSerializedKeyset_fails() throws Exception {
+    String invalidSerializedKeyset = "invalid";
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(invalidSerializedKeyset));
+  }
+
+  @Test
+  public void serializeKeyset_worksWithCleartextKeysetHandleReadAndJsonKeysetReader()
+      throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+
+    KeysetHandle parseKeysetHandle =
+        CleartextKeysetHandle.read(JsonKeysetReader.withString(serializedKeyset));
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseKeyset_worksWithCleartextKeysetHandleWriteAndJsonKeysetWriter()
+      throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(keysetHandle, JsonKeysetWriter.withOutputStream(outputStream));
+    String serializedKeyset = new String(outputStream.toByteArray(), UTF_8);
+
+    KeysetHandle parseKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void serializeKeysetWithoutSecret_worksWithKeysetHandleReadNoSecretAndJsonKeysetReader()
+      throws Exception {
+    KeysetHandle publicKeysetHandle = generatePublicKeyset();
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeysetWithoutSecret(publicKeysetHandle);
+
+    KeysetHandle parsePublicKeysetHandle =
+        KeysetHandle.readNoSecret(JsonKeysetReader.withString(serializedKeyset));
+
+    assertKeysetHandleAreEqual(publicKeysetHandle, parsePublicKeysetHandle);
+  }
+
+  @Test
+  public void parseKeysetWithoutSecret_worksWithKeysetHandleWriteNoSecretAndJsonKeysetWriter()
+      throws Exception {
+    KeysetHandle publicKeysetHandle = generatePublicKeyset();
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    publicKeysetHandle.writeNoSecret(JsonKeysetWriter.withOutputStream(outputStream));
+    String serializedKeyset = new String(outputStream.toByteArray(), UTF_8);
+
+    KeysetHandle parsePublicKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(serializedKeyset);
+
+    assertKeysetHandleAreEqual(publicKeysetHandle, parsePublicKeysetHandle);
+  }
+
+  @Test
+  public void serializeEncrypted_worksWithKeysetHandleReadWithAssociatedDataAndJsonKeysetReader()
+      throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, associatedData);
+
+    KeysetHandle parseKeysetHandle =
+        KeysetHandle.readWithAssociatedData(
+            JsonKeysetReader.withString(serializedKeyset), keyEncryptionAead, associatedData);
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseEncrypted_worksWithKeysetHandleWriteWithAssociatedDataAndJsonKeysetWriter()
+      throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    keysetHandle.writeWithAssociatedData(
+        JsonKeysetWriter.withOutputStream(outputStream), keyEncryptionAead, associatedData);
+    String serializedKeyset = new String(outputStream.toByteArray(), UTF_8);
+
+    KeysetHandle parseKeysetHandle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            serializedKeyset, keyEncryptionAead, associatedData);
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseKeysetFromTestVector()
+      throws Exception {
+    // The same key as in JsonKeysetReaderTest.
+    String serializedKeyset =
+        "{"
+            + "\"primaryKeyId\": 547623039,"
+            + "\"key\": [{"
+            + "\"keyData\": {"
+            + "\"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+            + "\"keyMaterialType\": \"SYMMETRIC\","
+            + "\"value\": \"EgQIAxAQGiBYhMkitTWFVefTIBg6kpvac+bwFOGSkENGmU+1EYgocg==\""
+            + "},"
+            + "\"outputPrefixType\": \"TINK\","
+            + "\"keyId\": 547623039,"
+            + "\"status\": \"ENABLED\""
+            + "}]}";
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+    Mac mac = handle.getPrimitive(Mac.class);
+    mac.verifyMac(Hex.decode("0120a4107f3549e4fb3137415a63f5c8a0524f8ca7"), "data".getBytes(UTF_8));
+  }
+
+  @Test
+  public void parseEncryptedKeysetFromTestVector() throws Exception {
+    // This is the same test vector as in KeysetHandleTest.
+    // An AEAD key, with which we encrypted the mac keyset below.
+    byte[] serializedKeysetEncryptionKeyset =
+        Hex.decode(
+            "08b891f5a20412580a4c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e6372797"
+                + "0746f2e74696e6b2e4165734561784b65791216120208101a10e5d7d0cdd649e81e7952260689b2"
+                + "e1971801100118b891f5a2042001");
+    KeysetHandle keysetEncryptionHandle = TinkProtoKeysetFormat.parseKeyset(
+        serializedKeysetEncryptionKeyset, InsecureSecretKeyAccess.get());
+    Aead keysetEncryptionAead = keysetEncryptionHandle.getPrimitive(Aead.class);
+
+    // A keyset that contains one HMAC key, encrypted with the above, using associatedData
+    String encryptedKeyset =
+        "{\"encryptedKeyset\":"
+            + "\"AURdSLhZcFEgMBptDyi4/D8hL3h+Iz7ICgLrdeVRH26Fi3uSeewFoFA5cV5wfNueme3/BBR60yJ4hGpQ"
+            + "p+/248ZIgfuWyfmAGZ4dmYnYC1qd/IWkZZfVr3aOsx4j4kFZHkkvA+XIZUh/INbdPsMUNJy9cmu6s8osdH"
+            + "zu0XzP2ltWUowbr0fLQJwy92eAvU6gv91k6Tc=\","
+            + "\"keysetInfo\":{\"primaryKeyId\":547623039,\"keyInfo\":[{\"typeUrl\":"
+            + "\"type.googleapis.com/google.crypto.tink.HmacKey\",\"status\":\"ENABLED\","
+            + "\"keyId\":547623039,\"outputPrefixType\":\"TINK\"}]}}";
+    byte[] associatedData = Hex.decode("abcdef330012");
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            encryptedKeyset, keysetEncryptionAead, associatedData);
+
+    Mac mac = handle.getPrimitive(Mac.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = Hex.decode("0120a4107f3549e4fb3137415a63f5c8a0524f8ca7");
+    mac.verifyMac(tag, data);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/TinkProtoKeysetFormatTest.java b/java_src/src/test/java/com/google/crypto/tink/TinkProtoKeysetFormatTest.java
new file mode 100644
index 0000000..8cffac0
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/TinkProtoKeysetFormatTest.java
@@ -0,0 +1,376 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.signature.SignatureConfig;
+import com.google.crypto.tink.subtle.Hex;
+import java.io.ByteArrayOutputStream;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class TinkProtoKeysetFormatTest {
+
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    MacConfig.register();
+    AeadConfig.register();
+    SignatureConfig.register();
+  }
+
+  private void assertKeysetHandleAreEqual(KeysetHandle keysetHandle1, KeysetHandle keysetHandle2)
+      throws Exception {
+    // This assertion is too strong, but it works here because we don't parse or serialize
+    // keydata.value fields.
+    assertThat(CleartextKeysetHandle.getKeyset(keysetHandle2))
+        .isEqualTo(CleartextKeysetHandle.getKeyset(keysetHandle1));
+  }
+
+  private KeysetHandle generateKeyset() throws GeneralSecurityException {
+    return KeysetHandle.newBuilder()
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG")
+                .withRandomId()
+                .makePrimary())
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_128BITTAG_RAW")
+                .withRandomId())
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG")
+                .withRandomId()
+                .setStatus(KeyStatus.DESTROYED))
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("HMAC_SHA256_256BITTAG_RAW")
+                .withRandomId()
+                .setStatus(KeyStatus.DISABLED))
+        .addEntry(KeysetHandle.generateEntryFromParametersName("AES256_CMAC").withRandomId())
+        .build();
+  }
+
+  private KeysetHandle generatePublicKeyset() throws GeneralSecurityException {
+    return KeysetHandle.newBuilder()
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("ECDSA_P256_RAW")
+                .withRandomId()
+                .setStatus(KeyStatus.DISABLED))
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("ECDSA_P256").withRandomId().makePrimary())
+        .addEntry(
+            KeysetHandle.generateEntryFromParametersName("ECDSA_P521")
+                .withRandomId()
+                .setStatus(KeyStatus.DESTROYED))
+        .build()
+        .getPublicKeysetHandle();
+  }
+
+  private Aead generateAead() throws GeneralSecurityException {
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("AES128_CTR_HMAC_SHA256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    return handle.getPrimitive(Aead.class);
+  }
+
+  @Test
+  public void serializeAndParse_successWithSameKeyset() throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+    KeysetHandle parseKeysetHandle =
+        TinkProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void serializeKeyset_withoutInsecureSecretKeyAccess_fails() throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    assertThrows(
+        NullPointerException.class,
+        () -> TinkProtoKeysetFormat.serializeKeyset(keysetHandle, null));
+  }
+
+  @Test
+  public void parseKeyset_withoutInsecureSecretKeyAccess_fails() throws Exception {
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(generateKeyset(), InsecureSecretKeyAccess.get());
+
+    assertThrows(
+        NullPointerException.class,
+        () -> TinkProtoKeysetFormat.parseKeyset(serializedKeyset, null));
+  }
+
+  @Test
+  public void parseInvalidSerializedKeyset_fails() throws Exception {
+    byte[] invalidSerializedKeyset = "invalid".getBytes(UTF_8);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkProtoKeysetFormat.parseKeyset(
+                invalidSerializedKeyset, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void serializeEncryptedAndParseEncrypted_successWithSameKeyset() throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, associatedData);
+    KeysetHandle parseKeysetHandle =
+        TinkProtoKeysetFormat.parseEncryptedKeyset(
+            serializedKeyset, keyEncryptionAead, associatedData);
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseEncryptedKeysetWithInvalidKey_fails() throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    Aead invalidKeyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, associatedData);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkProtoKeysetFormat.parseEncryptedKeyset(
+                serializedKeyset, invalidKeyEncryptionAead, associatedData));
+  }
+
+  @Test
+  public void parseEncryptedKeysetWithInvalidAssociatedData_fails() throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, "associatedData".getBytes(UTF_8));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkProtoKeysetFormat.parseEncryptedKeyset(
+                serializedKeyset, keyEncryptionAead, "invalidAssociatedData".getBytes(UTF_8)));
+  }
+
+  @Test
+  public void serializeAndParseWithoutSecret_successWithSameKeyset() throws Exception {
+    KeysetHandle publicKeysetHandle = generatePublicKeyset();
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeysetWithoutSecret(publicKeysetHandle);
+    KeysetHandle parsePublicKeysetHandle =
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(serializedKeyset);
+
+    assertKeysetHandleAreEqual(publicKeysetHandle, parsePublicKeysetHandle);
+  }
+
+  @Test
+  public void serializeWithoutSecret_keysetWithSecretKeys_fails() throws Exception {
+    KeysetHandle secretKeysetHandle = generateKeyset();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkProtoKeysetFormat.serializeKeysetWithoutSecret(secretKeysetHandle));
+  }
+
+  @Test
+  public void parseWithoutSecret_keysetWithSecretKeys_fails() throws Exception {
+    KeysetHandle secretKeysetHandle = generateKeyset();
+    byte[] serializedSecretKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(secretKeysetHandle, InsecureSecretKeyAccess.get());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkProtoKeysetFormat.parseKeysetWithoutSecret(serializedSecretKeyset));
+  }
+
+  @Test
+  public void parseWithoutSecretInvalidSerializedKeyset_fails() throws Exception {
+    byte[] invalidSerializedKeyset = "invalid".getBytes(UTF_8);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkProtoKeysetFormat.parseKeysetWithoutSecret(invalidSerializedKeyset));
+  }
+
+  @Test
+  public void serializeKeyset_worksWithCleartextKeysetHandleReadAndBinaryKeysetReader()
+      throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+
+    KeysetHandle parseKeysetHandle =
+        CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(serializedKeyset));
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseKeyset_worksWithCleartextKeysetHandleWriteAndBinaryKeysetWriter()
+      throws Exception {
+    KeysetHandle keysetHandle = generateKeyset();
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    CleartextKeysetHandle.write(keysetHandle, BinaryKeysetWriter.withOutputStream(outputStream));
+    byte[] serializedKeyset = outputStream.toByteArray();
+
+    KeysetHandle parseKeysetHandle =
+        TinkProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void serializeKeysetWithoutSecret_worksWithKeysetHandleReadNoSecretAndBinaryKeysetReader()
+      throws Exception {
+    KeysetHandle publicKeysetHandle = generatePublicKeyset();
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeysetWithoutSecret(publicKeysetHandle);
+
+    KeysetHandle parsePublicKeysetHandle =
+        KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(serializedKeyset));
+
+    assertKeysetHandleAreEqual(publicKeysetHandle, parsePublicKeysetHandle);
+  }
+
+  @Test
+  public void parseKeysetWithoutSecret_worksWithKeysetHandleWriteNoSecretAndBinaryKeysetWriter()
+      throws Exception {
+    KeysetHandle publicKeysetHandle = generatePublicKeyset();
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    publicKeysetHandle.writeNoSecret(BinaryKeysetWriter.withOutputStream(outputStream));
+    byte[] serializedKeyset = outputStream.toByteArray();
+
+    KeysetHandle parsePublicKeysetHandle =
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(serializedKeyset);
+
+    assertKeysetHandleAreEqual(publicKeysetHandle, parsePublicKeysetHandle);
+  }
+
+  @Test
+  public void serializeEncrypted_worksWithKeysetHandleReadWithAssociatedDataAndBinaryKeysetReader()
+      throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(
+            keysetHandle, keyEncryptionAead, associatedData);
+
+    KeysetHandle parseKeysetHandle =
+        KeysetHandle.readWithAssociatedData(
+            BinaryKeysetReader.withBytes(serializedKeyset), keyEncryptionAead, associatedData);
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseEncrypted_worksWithKeysetHandleWriteWithAssociatedDataAndBinaryKeysetWriter()
+      throws Exception {
+    Aead keyEncryptionAead = generateAead();
+    KeysetHandle keysetHandle = generateKeyset();
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    keysetHandle.writeWithAssociatedData(
+        BinaryKeysetWriter.withOutputStream(outputStream), keyEncryptionAead, associatedData);
+    byte[] serializedKeyset = outputStream.toByteArray();
+
+    KeysetHandle parseKeysetHandle =
+        TinkProtoKeysetFormat.parseEncryptedKeyset(
+            serializedKeyset, keyEncryptionAead, associatedData);
+
+    assertKeysetHandleAreEqual(keysetHandle, parseKeysetHandle);
+  }
+
+  @Test
+  public void parseKeysetFromTestVector()
+      throws Exception {
+    // This was generated in Python using the BinaryKeysetWriter. It contains one HMAC key.
+    byte[] serializedKeyset =
+        Hex.decode(
+            "0895e59bcc0612680a5c0a2e747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63"
+                + "727970746f2e74696e6b2e486d61634b657912281a20cca20f02278003b3513f5d01759ac1302f7d"
+                + "883f2f4a40025532ee1b11f9e587120410100803180110011895e59bcc062001");
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+    Mac mac = handle.getPrimitive(Mac.class);
+    mac.verifyMac(Hex.decode("016986f2956092d259136923c6f4323557714ec499"), "data".getBytes(UTF_8));
+  }
+
+  @Test
+  public void parseEncryptedKeysetFromTestVector() throws Exception {
+    // This is the same test vector as in KeysetHandleTest.
+    // An AEAD key, with which we encrypted the mac keyset below.
+    final byte[] serializedKeysetEncryptionKeyset =
+        Hex.decode(
+            "08b891f5a20412580a4c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e6372797"
+                + "0746f2e74696e6b2e4165734561784b65791216120208101a10e5d7d0cdd649e81e7952260689b2"
+                + "e1971801100118b891f5a2042001");
+    KeysetHandle keysetEncryptionHandle = TinkProtoKeysetFormat.parseKeyset(
+        serializedKeysetEncryptionKeyset, InsecureSecretKeyAccess.get());
+    Aead keysetEncryptionAead = keysetEncryptionHandle.getPrimitive(Aead.class);
+
+    // A keyset that contains one HMAC key, encrypted with the above, using associatedData
+    final byte[] encryptedSerializedKeyset =
+        Hex.decode(
+            "12950101445d48b8b5f591efaf73a46df9ebd7b6ac471cc0cf4f815a4f012fcaffc8f0b2b10b30c33194f"
+                + "0b291614bd8e1d2e80118e5d6226b6c41551e104ef8cd8ee20f1c14c1b87f6eed5fb04a91feafaa"
+                + "cbf6f368519f36f97f7d08b24c8e71b5e620c4f69615ef0479391666e2fb32e46b416893fc4e564"
+                + "ba927b22ebff2a77bd3b5b8d5afa162cbd35c94c155cdfa13c8a9c964cde21a4208f5909ce90112"
+                + "3a0a2e747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696"
+                + "e6b2e486d61634b6579100118f5909ce9012001");
+    final byte[] associatedData = Hex.decode("abcdef330012");
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseEncryptedKeyset(
+            encryptedSerializedKeyset, keysetEncryptionAead, associatedData);
+
+    Mac mac = handle.getPrimitive(Mac.class);
+    final byte[] message = Hex.decode("");
+    final byte[] tag = Hex.decode("011d270875989dd6fbd5f54dbc9520bb41efd058d5");
+    mac.verifyMac(tag, message);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/TinkProtoParametersFormatTest.java b/java_src/src/test/java/com/google/crypto/tink/TinkProtoParametersFormatTest.java
new file mode 100644
index 0000000..09bb073
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/TinkProtoParametersFormatTest.java
@@ -0,0 +1,146 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.LegacyProtoParameters;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.mac.AesCmacParameters;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.proto.AesCmacKeyFormat;
+import com.google.crypto.tink.proto.AesCmacParams;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class TinkProtoParametersFormatTest {
+
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    MacConfig.register();
+  }
+
+  @Test
+  public void testParseAesCmacFormat() throws GeneralSecurityException {
+    AesCmacKeyFormat format =
+        AesCmacKeyFormat.newBuilder()
+            .setKeySize(32)
+            .setParams(AesCmacParams.newBuilder().setTagSize(16))
+            .build();
+    KeyTemplate template =
+        KeyTemplate.newBuilder()
+            .setValue(format.toByteString())
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .setTypeUrl("type.googleapis.com/google.crypto.tink.AesCmacKey")
+            .build();
+    assertThat(TinkProtoParametersFormat.parse(template.toByteArray()))
+        .isEqualTo(
+            AesCmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(AesCmacParameters.Variant.TINK)
+                .build());
+  }
+
+  @Test
+  public void testParseInvalidAesCmacFormat_throws() throws GeneralSecurityException {
+    AesCmacKeyFormat format =
+        AesCmacKeyFormat.newBuilder()
+            .setKeySize(37) // Invalid Key Size
+            .setParams(AesCmacParams.newBuilder().setTagSize(16))
+            .build();
+    KeyTemplate template =
+        KeyTemplate.newBuilder()
+            .setValue(format.toByteString())
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .setTypeUrl("type.googleapis.com/google.crypto.tink.AesCmacKey")
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> TinkProtoParametersFormat.parse(template.toByteArray()));
+  }
+
+  @Test
+  public void testSerializeAesCmacFormat() throws Exception {
+    AesCmacParameters params =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(16)
+            .setVariant(AesCmacParameters.Variant.TINK)
+            .build();
+
+    byte[] serialized = TinkProtoParametersFormat.serialize(params);
+
+    KeyTemplate template =
+        KeyTemplate.parseFrom(serialized, ExtensionRegistryLite.getEmptyRegistry());
+
+    assertThat(template.getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
+    assertThat(template.getTypeUrl())
+        .isEqualTo("type.googleapis.com/google.crypto.tink.AesCmacKey");
+    assertThat(AesCmacKeyFormat.parseFrom(template.getValue()))
+        .isEqualTo(
+            AesCmacKeyFormat.newBuilder()
+                .setKeySize(32)
+                .setParams(AesCmacParams.newBuilder().setTagSize(16))
+                .build());
+  }
+
+  /** When parsing, if a TypeURL is not recognized we currently always parse into a Legacy object */
+  @Test
+  public void testParseToLegacyFormat() throws Exception {
+    KeyTemplate template =
+        KeyTemplate.newBuilder()
+            .setValue(ByteString.copyFrom(Hex.decode("80")))
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .setTypeUrl("SomeInvalidTypeURL")
+            .build();
+    Parameters parsed = TinkProtoParametersFormat.parse(template.toByteArray());
+
+    LegacyProtoParameters expected =
+        new LegacyProtoParameters(ProtoParametersSerialization.create(template));
+
+    assertThat(parsed).isEqualTo(expected);
+  }
+
+  /** When serializing a legacy object, we always succeed, even if an object is registered. */
+  @Test
+  public void testSerializeFromLegacyFormat() throws Exception {
+    KeyTemplate template =
+        KeyTemplate.newBuilder()
+            .setValue(ByteString.copyFrom(Hex.decode("80")))
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .setTypeUrl("SomeInvalidTypeURL")
+            .build();
+    LegacyProtoParameters legacyParameters =
+        new LegacyProtoParameters(ProtoParametersSerialization.create(template));
+
+    byte[] serialized = TinkProtoParametersFormat.serialize(legacyParameters);
+
+    assertThat(KeyTemplate.parseFrom(serialized, ExtensionRegistryLite.getEmptyRegistry()))
+        .isEqualTo(template);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AeadConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AeadConfigTest.java
index 7eacd1a..9e9f756 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AeadConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AeadConfigTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.aead;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Aead;
@@ -44,23 +45,17 @@
   public void aaaTestInitialization() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    GeneralSecurityException e =
-        assertThrows(GeneralSecurityException.class, () -> Registry.getCatalogue("tinkmac"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("MacConfig.register()");
-    e = assertThrows(GeneralSecurityException.class, () -> Registry.getCatalogue("tinkaead"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("AeadConfig.register()");
     // Before registration, key manager should be absent.
     String typeUrl = "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey";
-    e = assertThrows(GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
+    GeneralSecurityException e =
+        assertThrows(GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
     assertThat(e.toString()).contains("No key manager found");
 
     // Initialize the config.
     AeadConfig.register();
 
     // After registration the key manager should be present.
-    Registry.getKeyManager(typeUrl, Aead.class);
+    assertNotNull(Registry.getKeyManager(typeUrl, Aead.class));
 
     // Running init() manually again should succeed.
     AeadConfig.register();
@@ -85,7 +80,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, Aead.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, Aead.class));
     }
   }
 
@@ -103,7 +98,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, Aead.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, Aead.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AeadKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AeadKeyTemplatesTest.java
index af6b4c6..4e1c858 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AeadKeyTemplatesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AeadKeyTemplatesTest.java
@@ -16,9 +16,11 @@
 
 package com.google.crypto.tink.aead;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat;
 import com.google.crypto.tink.proto.AesEaxKeyFormat;
 import com.google.crypto.tink.proto.AesGcmKeyFormat;
@@ -28,13 +30,22 @@
 import com.google.crypto.tink.proto.KmsEnvelopeAeadKeyFormat;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.protobuf.ExtensionRegistryLite;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests for AeadKeyTemplates. */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public class AeadKeyTemplatesTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AeadConfig.register();
+  }
+
   @Test
   public void aes128Gcm() throws Exception {
     KeyTemplate template = AeadKeyTemplates.AES128_GCM;
@@ -161,8 +172,9 @@
     int hmacKeySize = 24;
     int tagSize = 27;
     HashType hashType = HashType.UNKNOWN_HASH;
-    KeyTemplate template = AeadKeyTemplates.createAesCtrHmacAeadKeyTemplate(
-        aesKeySize, ivSize, hmacKeySize, tagSize, hashType);
+    KeyTemplate template =
+        AeadKeyTemplates.createAesCtrHmacAeadKeyTemplate(
+            aesKeySize, ivSize, hmacKeySize, tagSize, hashType);
     assertEquals(new AesCtrHmacAeadKeyManager().getKeyType(), template.getTypeUrl());
     assertEquals(OutputPrefixType.TINK, template.getOutputPrefixType());
     AesCtrHmacAeadKeyFormat format =
@@ -186,7 +198,7 @@
     KeyTemplate template = AeadKeyTemplates.CHACHA20_POLY1305;
     assertEquals(new ChaCha20Poly1305KeyManager().getKeyType(), template.getTypeUrl());
     assertEquals(OutputPrefixType.TINK, template.getOutputPrefixType());
-    assertTrue(template.getValue().isEmpty());  // Empty format.
+    assertTrue(template.getValue().isEmpty()); // Empty format.
   }
 
   @Test
@@ -227,4 +239,37 @@
     assertEquals(kekUri, format.getKekUri());
     assertEquals(dekTemplate.toString(), format.getDekTemplate().toString());
   }
+
+  public static class Pair {
+    public Pair(KeyTemplate template, AeadParameters parameters) {
+      this.template = template;
+      this.parameters = parameters;
+    }
+
+    KeyTemplate template;
+    AeadParameters parameters;
+  }
+
+  @DataPoints("EquivalentPairs")
+  public static final Pair[] TEMPLATES =
+      new Pair[] {
+        new Pair(AeadKeyTemplates.AES128_GCM, PredefinedAeadParameters.AES128_GCM),
+        new Pair(AeadKeyTemplates.AES256_GCM, PredefinedAeadParameters.AES256_GCM),
+        new Pair(AeadKeyTemplates.AES128_EAX, PredefinedAeadParameters.AES128_EAX),
+        new Pair(AeadKeyTemplates.AES256_EAX, PredefinedAeadParameters.AES256_EAX),
+        new Pair(
+            AeadKeyTemplates.AES128_CTR_HMAC_SHA256,
+            PredefinedAeadParameters.AES128_CTR_HMAC_SHA256),
+        new Pair(
+            AeadKeyTemplates.AES256_CTR_HMAC_SHA256,
+            PredefinedAeadParameters.AES256_CTR_HMAC_SHA256),
+        new Pair(AeadKeyTemplates.CHACHA20_POLY1305, PredefinedAeadParameters.CHACHA20_POLY1305),
+        new Pair(AeadKeyTemplates.XCHACHA20_POLY1305, PredefinedAeadParameters.XCHACHA20_POLY1305),
+      };
+
+  @Theory
+  public void testParametersEqualsKeyTemplate(@FromDataPoints("EquivalentPairs") Pair p)
+      throws Exception {
+    assertThat(TinkProtoParametersFormat.parse(p.template.toByteArray())).isEqualTo(p.parameters);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AeadTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AeadTest.java
index e217273..b05f14d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AeadTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AeadTest.java
@@ -21,11 +21,11 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.DeterministicAead;
-import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
 import java.security.GeneralSecurityException;
 import org.junit.BeforeClass;
@@ -113,7 +113,8 @@
   @Theory
   public void readKeysetEncryptDecrypt()
       throws Exception {
-    KeysetHandle handle = CleartextKeysetHandle.read(JsonKeysetReader.withString(JSON_AEAD_KEYSET));
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_AEAD_KEYSET, InsecureSecretKeyAccess.get());
 
     Aead aead = handle.getPrimitive(Aead.class);
 
@@ -166,8 +167,8 @@
   public void multipleKeysReadKeysetWithEncryptDecrypt()
       throws Exception {
     KeysetHandle handle =
-        CleartextKeysetHandle.read(
-            JsonKeysetReader.withString(JSON_AEAD_KEYSET_WITH_MULTIPLE_KEYS));
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_AEAD_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
 
     Aead aead = handle.getPrimitive(Aead.class);
 
@@ -179,7 +180,8 @@
     // Also test that aead can decrypt ciphertexts encrypted with a non-primary key. We use
     // JSON_AEAD_KEYSET to encrypt with the first key.
     KeysetHandle handle1 =
-        CleartextKeysetHandle.read(JsonKeysetReader.withString(JSON_AEAD_KEYSET));
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_AEAD_KEYSET, InsecureSecretKeyAccess.get());
+
     Aead aead1 = handle1.getPrimitive(Aead.class);
     byte[] ciphertext1 = aead1.encrypt(plaintext, associatedData);
     assertThat(aead.decrypt(ciphertext1, associatedData)).isEqualTo(plaintext);
@@ -209,10 +211,9 @@
   public void getPrimitiveFromNonAeadKeyset_throws()
       throws Exception {
     KeysetHandle handle =
-        CleartextKeysetHandle.read(
-            JsonKeysetReader.withString(JSON_DAEAD_KEYSET));
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
     // Test that the keyset can create a DeterministicAead primitive, but not a Aead.
-    handle.getPrimitive(DeterministicAead.class);
+    Object unused = handle.getPrimitive(DeterministicAead.class);
     assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(Aead.class));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AeadWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AeadWrapperTest.java
index 42e517f..a5ad1e3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AeadWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AeadWrapperTest.java
@@ -36,6 +36,7 @@
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
@@ -126,7 +127,7 @@
         GeneralSecurityException.class, () -> wrappedAead.decrypt(rawCiphertext, invalid));
     assertThrows(
         GeneralSecurityException.class, () -> wrappedAead.decrypt(invalid, associatedData));
-    byte[] ciphertextWithTinkPrefix = Bytes.concat(TestUtil.hexDecode("0166AABBCC"), rawCiphertext);
+    byte[] ciphertextWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), rawCiphertext);
     assertThrows(
         GeneralSecurityException.class,
         () -> wrappedAead.decrypt(ciphertextWithTinkPrefix, associatedData));
@@ -153,7 +154,7 @@
     byte[] tinkPrefix = Arrays.copyOf(ciphertext, 5);
     byte[] ciphertextWithoutPrefix =
         Arrays.copyOfRange(ciphertext, 5, ciphertext.length);
-    assertThat(tinkPrefix).isEqualTo(TestUtil.hexDecode("0166AABBCC"));
+    assertThat(tinkPrefix).isEqualTo(Hex.decode("0166AABBCC"));
     assertThat(rawAead.decrypt(ciphertextWithoutPrefix, associatedData)).isEqualTo(plaintext);
   }
 
@@ -171,8 +172,7 @@
     byte[] plaintext = "plaintext".getBytes(UTF_8);
     byte[] associatedData = "associatedData".getBytes(UTF_8);
     byte[] rawCiphertext = rawAead.encrypt(plaintext, associatedData);
-    byte[] rawCiphertextWithTinkPrefix =
-        Bytes.concat(TestUtil.hexDecode("0166AABBCC"), rawCiphertext);
+    byte[] rawCiphertextWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), rawCiphertext);
 
     assertThat(wrappedAead.decrypt(rawCiphertextWithTinkPrefix, associatedData))
         .isEqualTo(plaintext);
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManagerTest.java
index d5be0ff..3d57024 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyManagerTest.java
@@ -18,9 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.crypto.tink.testing.KeyTypeManagerTestUtil.testKeyTemplateCompatible;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 
 import com.google.crypto.tink.Aead;
 import com.google.crypto.tink.KeyTemplate;
@@ -34,12 +32,15 @@
 import com.google.crypto.tink.proto.HmacParams;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.subtle.EncryptThenAuthenticate;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.ByteString;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -51,6 +52,11 @@
   private final KeyTypeManager.KeyFactory<AesCtrHmacAeadKeyFormat, AesCtrHmacAeadKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    AeadConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType())
@@ -90,6 +96,21 @@
         .setHmacKeyFormat(createHmacKeyFormat());
   }
 
+  private static AesCtrHmacAeadKeyFormat createKeyFormatForKeySize(int keySize) {
+    return AesCtrHmacAeadKeyFormat.newBuilder()
+        .setAesCtrKeyFormat(
+            AesCtrKeyFormat.newBuilder()
+                .setKeySize(keySize)
+                .setParams(AesCtrParams.newBuilder().setIvSize(16))
+                .build())
+        .setHmacKeyFormat(
+            HmacKeyFormat.newBuilder()
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(32).build())
+                .setKeySize(keySize)
+                .build())
+        .build();
+  }
+
   @Test
   public void validateKeyFormat_valid() throws Exception {
     factory.validateKeyFormat(createKeyFormat().build());
@@ -125,14 +146,149 @@
   }
 
   @Test
+  public void deriveKey_size32() throws Exception {
+    final int keySize = 32;
+    AesCtrHmacAeadKeyFormat keyFormat = createKeyFormatForKeySize(keySize);
+    byte[] keyMaterial = Random.randBytes(100);
+
+    AesCtrHmacAeadKey key = factory.deriveKey(keyFormat, new ByteArrayInputStream(keyMaterial));
+
+    assertThat(key.getAesCtrKey().getKeyValue()).isNotEqualTo(key.getHmacKey().getKeyValue());
+    assertThat(key.getAesCtrKey().getKeyValue())
+        .isEqualTo(ByteString.copyFrom(keyMaterial, 0, keySize));
+    assertThat(key.getHmacKey().getKeyValue())
+        .isEqualTo(ByteString.copyFrom(keyMaterial, keySize, keySize));
+  }
+
+  @Test
+  public void deriveKey_size16() throws Exception {
+    final int keySize = 16;
+    AesCtrHmacAeadKeyFormat keyFormat = createKeyFormatForKeySize(keySize);
+    byte[] keyMaterial = Random.randBytes(100);
+
+    AesCtrHmacAeadKey key = factory.deriveKey(keyFormat, new ByteArrayInputStream(keyMaterial));
+
+    assertThat(key.getAesCtrKey().getKeyValue()).isNotEqualTo(key.getHmacKey().getKeyValue());
+    assertThat(key.getAesCtrKey().getKeyValue())
+        .isEqualTo(ByteString.copyFrom(keyMaterial, 0, keySize));
+    assertThat(key.getHmacKey().getKeyValue())
+        .isEqualTo(ByteString.copyFrom(keyMaterial, keySize, keySize));
+  }
+
+  @Test
+  public void deriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    AesCtrHmacAeadKeyFormat keyFormat = createKeyFormatForKeySize(keySize);
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          // Will fill one byte per each `read` call, see:
+          // google3/third_party/tink/java_src/src/main/java/com/google/crypto/tink/internal/KeyTypeManager.java;l=255;rcl=531814261.
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    AesCtrHmacAeadKey key = factory.deriveKey(keyFormat, fragmentedInputStream);
+
+    assertThat(key.getAesCtrKey().getKeyValue()).hasSize(keySize);
+    assertThat(key.getHmacKey().getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getAesCtrKey().getKeyValue().byteAt(i)).isEqualTo(randomness);
+      assertThat(key.getHmacKey().getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
+  public void deriveKey_notEnoughAesCtrKeyMaterial_throws() throws Exception {
+    final int keySize = 32;
+    AesCtrHmacAeadKeyFormat keyFormat = createKeyFormatForKeySize(keySize);
+    byte[] keyMaterial = Random.randBytes(keySize - 1);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> factory.deriveKey(keyFormat, new ByteArrayInputStream(keyMaterial)));
+  }
+
+  @Test
+  public void deriveKey_notEnoughHmacKeyMaterial_throws() throws Exception {
+    final int keySize = 32;
+    AesCtrHmacAeadKeyFormat keyFormat = createKeyFormatForKeySize(keySize);
+    byte[] keyMaterial = Random.randBytes(keySize + keySize - 1);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> factory.deriveKey(keyFormat, new ByteArrayInputStream(keyMaterial)));
+  }
+
+  @Test
+  public void deriveKey_badVersion_throws() throws Exception {
+    final int keySize = 32;
+    AesCtrHmacAeadKeyFormat keyFormat =
+        AesCtrHmacAeadKeyFormat.newBuilder()
+            .setAesCtrKeyFormat(
+                AesCtrKeyFormat.newBuilder()
+                    .setKeySize(keySize)
+                    .setParams(AesCtrParams.newBuilder().setIvSize(16))
+                    .build())
+            .setHmacKeyFormat(
+                HmacKeyFormat.newBuilder()
+                    .setVersion(1)
+                    .setParams(
+                        HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(32).build())
+                    .setKeySize(keySize)
+                    .build())
+            .build();
+    byte[] keyMaterial = Random.randBytes(keySize + keySize);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> factory.deriveKey(keyFormat, new ByteArrayInputStream(keyMaterial)));
+  }
+
+  @Test
+  public void deriveKey_justEnoughKeyMaterial() throws Exception {
+    final int keySize = 32;
+    AesCtrHmacAeadKeyFormat keyFormat =
+        AesCtrHmacAeadKeyFormat.newBuilder()
+            .setAesCtrKeyFormat(
+                AesCtrKeyFormat.newBuilder()
+                    .setKeySize(keySize)
+                    .setParams(AesCtrParams.newBuilder().setIvSize(16))
+                    .build())
+            .setHmacKeyFormat(
+                HmacKeyFormat.newBuilder()
+                    .setParams(
+                        HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(32).build())
+                    .setKeySize(keySize)
+                    .build())
+            .build();
+    byte[] keyMaterial = Random.randBytes(keySize + keySize);
+
+    AesCtrHmacAeadKey key = factory.deriveKey(keyFormat, new ByteArrayInputStream(keyMaterial));
+
+    assertThat(key.getAesCtrKey().getKeyValue()).isNotEqualTo(key.getHmacKey().getKeyValue());
+    assertThat(key.getAesCtrKey().getKeyValue())
+        .isEqualTo(ByteString.copyFrom(keyMaterial, 0, keySize));
+    assertThat(key.getHmacKey().getKeyValue())
+        .isEqualTo(ByteString.copyFrom(keyMaterial, keySize, keySize));
+  }
+
+  @Test
   public void createKey_multipleTimes_distinctAesKeys() throws Exception {
     AesCtrHmacAeadKeyFormat format = createKeyFormat().build();
     Set<String> keys = new TreeSet<>();
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 50;
     for (int i = 0; i < numTests; i++) {
-      keys.add(
-          TestUtil.hexEncode(factory.createKey(format).getAesCtrKey().getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getAesCtrKey().getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -144,8 +300,7 @@
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 50;
     for (int i = 0; i < numTests; i++) {
-      keys.add(
-          TestUtil.hexEncode(factory.createKey(format).getHmacKey().getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getHmacKey().getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -176,43 +331,31 @@
   @Test
   public void testAes128CtrHmacSha256Template() throws Exception {
     KeyTemplate template = AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template();
-    assertEquals(new AesCtrHmacAeadKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesCtrHmacAeadKeyFormat format =
-        AesCtrHmacAeadKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertTrue(format.hasAesCtrKeyFormat());
-    assertTrue(format.getAesCtrKeyFormat().hasParams());
-    assertEquals(16, format.getAesCtrKeyFormat().getKeySize());
-    assertEquals(16, format.getAesCtrKeyFormat().getParams().getIvSize());
-
-    assertTrue(format.hasHmacKeyFormat());
-    assertTrue(format.getHmacKeyFormat().hasParams());
-    assertEquals(32, format.getHmacKeyFormat().getKeySize());
-    assertEquals(16, format.getHmacKeyFormat().getParams().getTagSize());
-    assertEquals(HashType.SHA256, format.getHmacKeyFormat().getParams().getHash());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(32)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testAes256CtrHmacSha256Template() throws Exception {
     KeyTemplate template = AesCtrHmacAeadKeyManager.aes256CtrHmacSha256Template();
-    assertEquals(new AesCtrHmacAeadKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesCtrHmacAeadKeyFormat format =
-        AesCtrHmacAeadKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertTrue(format.hasAesCtrKeyFormat());
-    assertTrue(format.getAesCtrKeyFormat().hasParams());
-    assertEquals(32, format.getAesCtrKeyFormat().getKeySize());
-    assertEquals(16, format.getAesCtrKeyFormat().getParams().getIvSize());
-
-    assertTrue(format.hasHmacKeyFormat());
-    assertTrue(format.getHmacKeyFormat().hasParams());
-    assertEquals(32, format.getHmacKeyFormat().getKeySize());
-    assertEquals(32, format.getHmacKeyFormat().getParams().getTagSize());
-    assertEquals(HashType.SHA256, format.getHmacKeyFormat().getParams().getHash());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(32)
+                .setHmacKeySizeBytes(32)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(32)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyTest.java
new file mode 100644
index 0000000..ffb4ec7
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadKeyTest.java
@@ -0,0 +1,430 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesCtrHmacAeadKeyTest {
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes aesKeyBytes = SecretBytes.randomBytes(16);
+    SecretBytes hmacKeyBytes = SecretBytes.randomBytes(16);
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(aesKeyBytes)
+            .setHmacKeyBytes(hmacKeyBytes)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getAesKeyBytes()).isEqualTo(aesKeyBytes);
+    assertThat(key.getHmacKeyBytes()).isEqualTo(hmacKeyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes aesKeyBytes = SecretBytes.randomBytes(16);
+    SecretBytes hmacKeyBytes = SecretBytes.randomBytes(16);
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(aesKeyBytes)
+            .setHmacKeyBytes(hmacKeyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getAesKeyBytes()).isEqualTo(aesKeyBytes);
+    assertThat(key.getHmacKeyBytes()).isEqualTo(hmacKeyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes aesKeyBytes = SecretBytes.randomBytes(16);
+    SecretBytes hmacKeyBytes = SecretBytes.randomBytes(16);
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(aesKeyBytes)
+            .setHmacKeyBytes(hmacKeyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getAesKeyBytes()).isEqualTo(aesKeyBytes);
+    assertThat(key.getHmacKeyBytes()).isEqualTo(hmacKeyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesCtrHmacAeadKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadKey.builder()
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .setHmacKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_fails() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesCtrHmacAeadKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void paramtersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    AesCtrHmacAeadParameters parametersWithIdRequirement =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadKey.builder()
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setHmacKeyBytes(SecretBytes.randomBytes(16))
+                .setParameters(parametersWithIdRequirement)
+                .build());
+  }
+
+  @Test
+  public void paramtersDoesNotRequireIdButIdIsSetInBuild_fails() throws Exception {
+    AesCtrHmacAeadParameters parametersWithoutIdRequirement =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parametersWithoutIdRequirement.hasIdRequirement()).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadKey.builder()
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setHmacKeyBytes(SecretBytes.randomBytes(16))
+                .setParameters(parametersWithoutIdRequirement)
+                .setIdRequirement(0x66AABBCC)
+                .build());
+  }
+
+  @Test
+  public void build_keyTooSmall_fails() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadKey.builder()
+                .setParameters(parameters)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setHmacKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+  }
+
+  @Test
+  public void build_keyTooLarge_fails() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadKey.builder()
+                .setParameters(parameters)
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .setHmacKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes1 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes1Copy =
+        SecretBytes.copyFrom(
+            keyBytes1.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes2 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes16 = SecretBytes.randomBytes(16);
+
+    AesCtrHmacAeadParameters noPrefixParameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    AesCtrHmacAeadParameters noPrefixParameters16 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    AesCtrHmacAeadParameters tinkPrefixParameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    AesCtrHmacAeadParameters crunchyPrefixParameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    AesCtrHmacAeadParameters noPrefixParametersSha512 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    AesCtrHmacAeadParameters noPrefixParametersIvSize12 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(12)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes1",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .build(),
+            // the same key built twice must be equal
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .build(),
+            // the same key built with a copy of key bytes must be equal
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters)
+                .setAesKeyBytes(keyBytes1Copy)
+                .setHmacKeyBytes(keyBytes1Copy)
+                .build(),
+            // setting id requirement to null is equal to not setting it
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .setIdRequirement(null)
+                .build())
+        // This 2 groups check that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "No prefix, different aes key bytes",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters)
+                .setAesKeyBytes(keyBytes2)
+                .setHmacKeyBytes(keyBytes1)
+                .build())
+        .addEqualityGroup(
+            "No prefix, different hmac key bytes",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes2)
+                .build())
+        // This group checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "No prefix with SHA512, keyBytes1",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParametersSha512)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .build())
+        .addEqualityGroup(
+            "No prefix, keyBytes16",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(keyBytes16)
+                .setHmacKeyBytes(keyBytes16)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes1",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .setIdRequirement(1907)
+                .build(),
+            AesCtrHmacAeadKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setAesKeyBytes(keyBytes1Copy)
+                .setHmacKeyBytes(keyBytes1Copy)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "No prefix, IV size 12",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(noPrefixParametersIvSize12)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .build())
+        // This group checks that keys with different key ids are not equal
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes1",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .setIdRequirement(1908)
+                .build())
+        // This group checks that keys with different output prefix types are not equal
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes1",
+            AesCtrHmacAeadKey.builder()
+                .setParameters(crunchyPrefixParameters)
+                .setAesKeyBytes(keyBytes1)
+                .setHmacKeyBytes(keyBytes1)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    AesGcmParameters aesGcmParameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesCtrHmacAeadParameters aesCtrHmacAeadParameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesGcmKey aesGcmKey =
+        AesGcmKey.builder().setParameters(aesGcmParameters).setKeyBytes(keyBytes).build();
+    AesCtrHmacAeadKey aesCtrHmacAeadKey =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(aesCtrHmacAeadParameters)
+            .setAesKeyBytes(keyBytes)
+            .setHmacKeyBytes(keyBytes)
+            .build();
+
+    assertThat(aesCtrHmacAeadKey.equalsKey(aesGcmKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadParametersTest.java
new file mode 100644
index 0000000..f1e537f
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadParametersTest.java
@@ -0,0 +1,562 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesCtrHmacAeadParametersTest {
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getAesKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getHmacKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getCiphertextOverheadSizeBytes()).isEqualTo(37);
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getHashType()).isEqualTo(AesCtrHmacAeadParameters.HashType.SHA256);
+    assertThat(parameters.getVariant()).isEqualTo(AesCtrHmacAeadParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .build();
+    assertThat(parameters.getCiphertextOverheadSizeBytes()).isEqualTo(37);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getVariant()).isEqualTo(AesCtrHmacAeadParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingAesKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setHmacKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(21)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingHmacKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingTagSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingIvSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingHashType_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(21)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(21)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(null)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.getCiphertextOverheadSizeBytes()).isEqualTo(42);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getVariant()).isEqualTo(AesCtrHmacAeadParameters.Variant.TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getCiphertextOverheadSizeBytes()).isEqualTo(42);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getVariant()).isEqualTo(AesCtrHmacAeadParameters.Variant.CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithBadAesKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(40)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setIvSizeBytes(11)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithBadHmacKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(12)
+                .setTagSizeBytes(21)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithBadIvSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(12)
+                .setTagSizeBytes(21)
+                .setIvSizeBytes(11)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(12)
+                .setTagSizeBytes(21)
+                .setIvSizeBytes(17)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSha1_acceptsTagSizesBetween10And20() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA1)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA1)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+
+    AesCtrHmacAeadParameters unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA1)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(20)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA1)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+  }
+
+  @Test
+  public void buildParametersWithSha224_acceptsTagSizesBetween10And28() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA224)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(29)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA224)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+
+    AesCtrHmacAeadParameters unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA224)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(28)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA224)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+  }
+
+  @Test
+  public void buildParametersWithSha256_acceptsTagSizesBetween10And32() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(33)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+
+    AesCtrHmacAeadParameters unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(32)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+  }
+
+  @Test
+  public void buildParametersWithSha384_acceptsTagSizesBetween10And48() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(49)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    AesCtrHmacAeadParameters unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(48)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+  }
+
+  @Test
+  public void buildParametersWithSha512_acceptsTagSizesBetween10And64() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCtrHmacAeadParameters.builder()
+                .setAesKeySizeBytes(16)
+                .setHmacKeySizeBytes(16)
+                .setTagSizeBytes(65)
+                .setIvSizeBytes(16)
+                .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+                .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                .build());
+    AesCtrHmacAeadParameters unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(64)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    AesCtrHmacAeadParameters parameters1 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    AesCtrHmacAeadParameters parameters2 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesCtrHmacAeadParameters parameters1 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesCtrHmacAeadParameters parameters2 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(22)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(14)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadProtoSerializationTest.java
new file mode 100644
index 0000000..a29ce25
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrHmacAeadProtoSerializationTest.java
@@ -0,0 +1,746 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesCtrHmacAeadProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesCtrHmacAeadProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey";
+
+  private static final SecretBytes KEY_BYTES_32 = SecretBytes.randomBytes(32);
+  private static final ByteString KEY_BYTES_32_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_32.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final SecretBytes KEY_BYTES_16 = SecretBytes.randomBytes(16);
+  private static final ByteString KEY_BYTES_16_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  private static com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat getAesCtrHmacKeyFormatProto(
+      int aesCtrKeySizeBytes,
+      int hmacKeySizeBytes,
+      int ivSizeBytes,
+      int tagSizeBytes,
+      HashType hashType) {
+    return com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.newBuilder()
+        .setAesCtrKeyFormat(
+            com.google.crypto.tink.proto.AesCtrKeyFormat.newBuilder()
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                        .setIvSize(ivSizeBytes)
+                        .build())
+                .setKeySize(aesCtrKeySizeBytes)
+                .build())
+        .setHmacKeyFormat(
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setParams(
+                    com.google.crypto.tink.proto.HmacParams.newBuilder()
+                        .setTagSize(tagSizeBytes)
+                        .setHash(hashType)
+                        .build())
+                .setKeySize(hmacKeySizeBytes)
+                .build())
+        .build();
+  }
+
+  private static com.google.crypto.tink.proto.AesCtrHmacAeadKey getAesCtrHmacKeyProto(
+      int version,
+      ByteString aesKeyValue,
+      ByteString hmacKeyValue,
+      int ivSize,
+      int tagSizeBytes,
+      HashType hashType) {
+    return com.google.crypto.tink.proto.AesCtrHmacAeadKey.newBuilder()
+        .setVersion(version)
+        .setAesCtrKey(
+            com.google.crypto.tink.proto.AesCtrKey.newBuilder()
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                        .setIvSize(ivSize)
+                        .build())
+                .setKeyValue(aesKeyValue)
+                .build())
+        .setHmacKey(
+            com.google.crypto.tink.proto.HmacKey.newBuilder()
+                .setParams(
+                    com.google.crypto.tink.proto.HmacParams.newBuilder()
+                        .setTagSize(tagSizeBytes)
+                        .setHash(hashType)
+                        .build())
+                .setKeyValue(hmacKeyValue)
+                .build())
+        .build();
+  }
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesCtrHmacAeadProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesCtrHmacAeadProtoSerialization.register(registry);
+    AesCtrHmacAeadProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix_sha1_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA1)
+            .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            OutputPrefixType.RAW,
+            getAesCtrHmacKeyFormatProto(32, 32, 16, 13, HashType.SHA1));
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink_sha224_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA224)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            OutputPrefixType.TINK,
+            getAesCtrHmacKeyFormatProto(32, 32, 16, 13, HashType.SHA224));
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy_sha256_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            OutputPrefixType.CRUNCHY,
+            getAesCtrHmacKeyFormatProto(32, 32, 16, 13, HashType.SHA256));
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy_sha384_ivSize12_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(12)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            OutputPrefixType.CRUNCHY,
+            getAesCtrHmacKeyFormatProto(16, 32, 12, 13, HashType.SHA384));
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink_sha512_ivSize14_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(14)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            OutputPrefixType.TINK,
+            getAesCtrHmacKeyFormatProto(32, 16, 14, 13, HashType.SHA512));
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseKey_noPrefix_sha1_equal() throws Exception {
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(
+                AesCtrHmacAeadParameters.builder()
+                    .setAesKeySizeBytes(32)
+                    .setHmacKeySizeBytes(32)
+                    .setTagSizeBytes(13)
+                    .setIvSizeBytes(16)
+                    .setHashType(AesCtrHmacAeadParameters.HashType.SHA1)
+                    .setVariant(AesCtrHmacAeadParameters.Variant.NO_PREFIX)
+                    .build())
+            .setAesKeyBytes(KEY_BYTES_32)
+            .setHmacKeyBytes(KEY_BYTES_32)
+            .build();
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_32_AS_BYTE_STRING, KEY_BYTES_32_AS_BYTE_STRING, 16, 13, HashType.SHA1);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink_sha224_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA224)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(KEY_BYTES_32)
+            .setHmacKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_32_AS_BYTE_STRING, KEY_BYTES_32_AS_BYTE_STRING, 16, 13, HashType.SHA224);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy_sha256_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA256)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(KEY_BYTES_32)
+            .setHmacKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_32_AS_BYTE_STRING, KEY_BYTES_32_AS_BYTE_STRING, 16, 13, HashType.SHA256);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy_sha384_ivSize12_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(16)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(12)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA384)
+            .setVariant(AesCtrHmacAeadParameters.Variant.CRUNCHY)
+            .build();
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(KEY_BYTES_16)
+            .setHmacKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_16_AS_BYTE_STRING, KEY_BYTES_32_AS_BYTE_STRING, 12, 13, HashType.SHA384);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink_sha512_ivSize14_equal() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(16)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(14)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(KEY_BYTES_32)
+            .setHmacKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_32_AS_BYTE_STRING, KEY_BYTES_16_AS_BYTE_STRING, 14, 13, HashType.SHA512);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacAeadKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_32_AS_BYTE_STRING, KEY_BYTES_32_AS_BYTE_STRING, 16, 13, HashType.SHA512);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void testParseKey_legacy() throws Exception {
+    com.google.crypto.tink.proto.AesCtrHmacAeadKey protoAesCtrHmacAeadKey =
+        getAesCtrHmacKeyProto(
+            0, KEY_BYTES_32_AS_BYTE_STRING, KEY_BYTES_32_AS_BYTE_STRING, 16, 13, HashType.SHA512);
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+            protoAesCtrHmacAeadKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            /* idRequirement= */ 1479);
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((AesCtrHmacAeadParameters) parsed.getParameters()).getVariant())
+        .isEqualTo(AesCtrHmacAeadParameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    AesCtrHmacAeadParameters parameters =
+        AesCtrHmacAeadParameters.builder()
+            .setAesKeySizeBytes(32)
+            .setHmacKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setIvSizeBytes(16)
+            .setHashType(AesCtrHmacAeadParameters.HashType.SHA512)
+            .setVariant(AesCtrHmacAeadParameters.Variant.TINK)
+            .build();
+    AesCtrHmacAeadKey key =
+        AesCtrHmacAeadKey.builder()
+            .setParameters(parameters)
+            .setAesKeyBytes(KEY_BYTES_32)
+            .setHmacKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Tag size too small
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            getAesCtrHmacKeyFormatProto(32, 32, 16, 9, HashType.SHA256)),
+        // IV size too small
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            getAesCtrHmacKeyFormatProto(32, 32, 11, 13, HashType.SHA256)),
+        // Aes key size too small
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            getAesCtrHmacKeyFormatProto(12, 32, 16, 13, HashType.SHA256)),
+        // Hmac key size too small
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            getAesCtrHmacKeyFormatProto(32, 12, 16, 13, HashType.SHA256)),
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            getAesCtrHmacKeyFormatProto(32, 32, 16, 13, HashType.SHA256)),
+        // Wrong version:
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacAeadKeyFormat.newBuilder()
+                .setAesCtrKeyFormat(
+                    com.google.crypto.tink.proto.AesCtrKeyFormat.newBuilder()
+                        .setParams(
+                            com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                                .setIvSize(32)
+                                .build())
+                        .setKeySize(32)
+                        .build())
+                .setHmacKeyFormat(
+                    com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                        // Here is the version
+                        .setVersion(1)
+                        .setParams(
+                            com.google.crypto.tink.proto.HmacParams.newBuilder()
+                                .setTagSize(32)
+                                .setHash(HashType.SHA256)
+                                .build())
+                        .setKeySize(32)
+                        .build())
+                .build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            getAesCtrHmacKeyProto(
+                    1,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    16,
+                    13,
+                    HashType.SHA512)
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            getAesCtrHmacKeyProto(
+                    0,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    16,
+                    13,
+                    HashType.SHA512)
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Tag Length (9)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            getAesCtrHmacKeyProto(
+                    0,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    16,
+                    9,
+                    HashType.SHA512)
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad IV Length (11)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            getAesCtrHmacKeyProto(
+                    0,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    11,
+                    9,
+                    HashType.SHA512)
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Aes Key Length (8)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            getAesCtrHmacKeyProto(
+                    0,
+                    ByteString.copyFrom(new byte[8]),
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    16,
+                    13,
+                    HashType.SHA512)
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Hmac Key Length (8)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            getAesCtrHmacKeyProto(
+                    0,
+                    KEY_BYTES_32_AS_BYTE_STRING,
+                    ByteString.copyFrom(new byte[8]),
+                    16,
+                    13,
+                    HashType.SHA512)
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version in Aes Ctr Key
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCtrHmacAeadKey.newBuilder()
+                .setAesCtrKey(
+                    com.google.crypto.tink.proto.AesCtrKey.newBuilder()
+                        .setVersion(1)
+                        .setParams(
+                            com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                                .setIvSize(16)
+                                .build())
+                        .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                        .build())
+                .setHmacKey(
+                    com.google.crypto.tink.proto.HmacKey.newBuilder()
+                        .setParams(
+                            com.google.crypto.tink.proto.HmacParams.newBuilder()
+                                .setTagSize(16)
+                                .setHash(HashType.SHA1)
+                                .build())
+                        .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version in Hmac Key
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCtrHmacAeadKey.newBuilder()
+                .setAesCtrKey(
+                    com.google.crypto.tink.proto.AesCtrKey.newBuilder()
+                        .setParams(
+                            com.google.crypto.tink.proto.AesCtrParams.newBuilder()
+                                .setIvSize(16)
+                                .build())
+                        .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                        .build())
+                .setHmacKey(
+                    com.google.crypto.tink.proto.HmacKey.newBuilder()
+                        .setVersion(1)
+                        .setParams(
+                            com.google.crypto.tink.proto.HmacParams.newBuilder()
+                                .setTagSize(16)
+                                .setHash(HashType.SHA1)
+                                .build())
+                        .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrKeyManagerTest.java
index f281037..983eedc 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesCtrKeyManagerTest.java
@@ -25,9 +25,9 @@
 import com.google.crypto.tink.proto.AesCtrParams;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.subtle.AesCtrJceCipher;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.IndCpaCipher;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
@@ -124,8 +124,7 @@
     Set<String> keys = new TreeSet<>();
     final int numKeys = 100;
     for (int i = 0; i < numKeys; ++i) {
-      keys.add(
-          TestUtil.hexEncode(factory.createKey(createFormat(16, 16)).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(createFormat(16, 16)).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numKeys);
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyManagerTest.java
index fedb195..b32b2d1 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyManagerTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.crypto.tink.Aead;
@@ -34,13 +33,13 @@
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.subtle.AesEaxJce;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -52,6 +51,11 @@
   private final KeyTypeManager.KeyFactory<AesEaxKeyFormat, AesEaxKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    AeadConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType()).isEqualTo("type.googleapis.com/google.crypto.tink.AesEaxKey");
@@ -128,7 +132,7 @@
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 50;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -164,12 +168,12 @@
         String tag) {
       try {
         this.name = name;
-        this.keyValue = TestUtil.hexDecode(keyValue);
-        this.plaintext = TestUtil.hexDecode(plaintext);
-        this.aad = TestUtil.hexDecode(aad);
-        this.iv = TestUtil.hexDecode(iv);
-        this.ciphertext = TestUtil.hexDecode(ciphertext);
-        this.tag = TestUtil.hexDecode(tag);
+        this.keyValue = Hex.decode(keyValue);
+        this.plaintext = Hex.decode(plaintext);
+        this.aad = Hex.decode(aad);
+        this.iv = Hex.decode(iv);
+        this.ciphertext = Hex.decode(ciphertext);
+        this.tag = Hex.decode(tag);
       } catch (Exception ignored) {
         // Ignored
       }
@@ -264,9 +268,6 @@
   @Test
   public void testPublicTestVectors() throws Exception {
     for (PublicTestVector t : publicTestVectors) {
-      if (TestUtil.shouldSkipTestWithAesKeySize(t.keyValue.length)) {
-        continue;
-      }
       AesEaxKey key =
           AesEaxKey.newBuilder()
               .setKeyValue(ByteString.copyFrom(t.keyValue))
@@ -296,53 +297,52 @@
   @Test
   public void testAes128EaxTemplate() throws Exception {
     KeyTemplate template = AesEaxKeyManager.aes128EaxTemplate();
-    assertEquals(new AesEaxKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesEaxKeyFormat format =
-        AesEaxKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(16, format.getKeySize());
-    assertTrue(format.hasParams());
-    assertEquals(16, format.getParams().getIvSize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(AesEaxParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes128EaxTemplate() throws Exception {
     KeyTemplate template = AesEaxKeyManager.rawAes128EaxTemplate();
-    assertEquals(new AesEaxKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    AesEaxKeyFormat format =
-        AesEaxKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(16, format.getKeySize());
-    assertTrue(format.hasParams());
-    assertEquals(16, format.getParams().getIvSize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testAes256EaxTemplate() throws Exception {
     KeyTemplate template = AesEaxKeyManager.aes256EaxTemplate();
-    assertEquals(new AesEaxKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesEaxKeyFormat format =
-        AesEaxKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(32, format.getKeySize());
-    assertTrue(format.hasParams());
-    assertEquals(16, format.getParams().getIvSize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesEaxParameters.builder()
+                .setKeySizeBytes(32)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(AesEaxParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes256EaxTemplate() throws Exception {
     KeyTemplate template = AesEaxKeyManager.rawAes256EaxTemplate();
-    assertEquals(new AesEaxKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    AesEaxKeyFormat format =
-        AesEaxKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(32, format.getKeySize());
-    assertTrue(format.hasParams());
-    assertEquals(16, format.getParams().getIvSize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesEaxParameters.builder()
+                .setIvSizeBytes(16)
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyTest.java
new file mode 100644
index 0000000..b167e02
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxKeyTest.java
@@ -0,0 +1,298 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesEaxKeyTest {
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesEaxKey key = AesEaxKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesEaxKey key =
+        AesEaxKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesEaxKey key =
+        AesEaxKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesEaxKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesEaxKey.builder().setKeyBytes(SecretBytes.randomBytes(32)).build());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_fails() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesEaxKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void paramtersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    AesEaxParameters parametersWithIdRequirement =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxKey.builder()
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .setParameters(parametersWithIdRequirement)
+                .build());
+  }
+
+  @Test
+  public void build_keyTooSmall_fails() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+  }
+
+  @Test
+  public void build_keyTooLarge_fails() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes32 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes32Copy =
+        SecretBytes.copyFrom(
+            keyBytes32.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes32Diff = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes16 = SecretBytes.randomBytes(16);
+
+    AesEaxParameters noPrefixParametersKeySize32 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    AesEaxParameters noPrefixParametersKeySize16 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    AesEaxParameters noPrefixParametersKeySize32IvSize16 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+    AesEaxParameters tinkPrefixParametersKeySize32 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.TINK)
+            .build();
+    AesEaxParameters crunchyPrefixParametersKeySize32 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.CRUNCHY)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes32",
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built twice must be equal.
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built with a copy of key bytes must be equal.
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .build(),
+            // Setting id requirement to null is equal to not setting it.
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal.
+        .addEqualityGroup(
+            "No prefix, newly generated keyBytes32",
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Diff)
+                .build())
+        // This group checks that keys with different IV sizes are not equal.
+        .addEqualityGroup(
+            "No prefix with IV size 16, keyBytes32",
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize32IvSize16)
+                .setKeyBytes(keyBytes32)
+                .build())
+        // This group checks that keys with different key sizes are not equal.
+        .addEqualityGroup(
+            "No prefix, keyBytes16",
+            AesEaxKey.builder()
+                .setParameters(noPrefixParametersKeySize16)
+                .setKeyBytes(keyBytes16)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes32",
+            AesEaxKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build(),
+            AesEaxKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal.
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes32",
+            AesEaxKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1908)
+                .build())
+        // This groups checks that keys with different output prefix types are not equal.
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes32",
+            AesEaxKey.builder()
+                .setParameters(crunchyPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxParametersTest.java
new file mode 100644
index 0000000..f102844
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxParametersTest.java
@@ -0,0 +1,313 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesEaxParametersTest {
+  private static final AesEaxParameters.Variant NO_PREFIX = AesEaxParameters.Variant.NO_PREFIX;
+  private static final AesEaxParameters.Variant TINK = AesEaxParameters.Variant.TINK;
+  private static final AesEaxParameters.Variant CRUNCHY = AesEaxParameters.Variant.CRUNCHY;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingIvSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingTagSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(null)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(TINK)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(CRUNCHY)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    AesEaxParameters parameters1 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    AesEaxParameters parameters2 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void buildParametersWithKeySize16And32() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(32);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(8)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(12)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithLargerTagSizeFails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(32)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithIvSize12And16() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(12);
+    parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(16);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(8)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesEaxParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(22)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesEaxParameters parameters1 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+
+    AesEaxParameters parameters2 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(TINK)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(CRUNCHY)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxProtoSerializationTest.java
new file mode 100644
index 0000000..93c8248
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesEaxProtoSerializationTest.java
@@ -0,0 +1,449 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.AesEaxParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesEaxProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesEaxProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesEaxKey";
+
+  private static final SecretBytes KEY_BYTES_16 = SecretBytes.randomBytes(16);
+  private static final ByteString KEY_BYTES_16_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesEaxProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesEaxProtoSerialization.register(registry);
+    AesEaxProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(AesEaxParams.newBuilder().setIvSize(16))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesEaxKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(24)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesEaxParameters.Variant.TINK)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                .setKeySize(24)
+                .setParams(AesEaxParams.newBuilder().setIvSize(12))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesEaxKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.CRUNCHY)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                .setKeySize(32)
+                .setParams(AesEaxParams.newBuilder().setIvSize(16))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesEaxKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParameters_badTagSize_fails() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(12)
+            .setIvSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+
+    // Fails when tag size is not a 16-byte value
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeParameters(parameters, ProtoParametersSerialization.class));
+  }
+
+  @Test
+  public void serializeParseKey_noPrefix() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesEaxKey key = AesEaxKey.builder().setParameters(parameters).setKeyBytes(KEY_BYTES_16).build();
+
+    com.google.crypto.tink.proto.AesEaxKey protoAesEaxKey =
+        com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .setParams(AesEaxParams.newBuilder().setIvSize(16))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            protoAesEaxKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesEaxKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesEaxParameters.Variant.TINK)
+            .build();
+
+    AesEaxKey key =
+        AesEaxKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesEaxKey protoAesEaxKey =
+        com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .setParams(AesEaxParams.newBuilder().setIvSize(12))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            protoAesEaxKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesEaxKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.CRUNCHY)
+            .build();
+
+    AesEaxKey key =
+        AesEaxKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesEaxKey protoAesEaxKey =
+        com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .setParams(AesEaxParams.newBuilder().setIvSize(16))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            protoAesEaxKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesEaxKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.AesEaxKey protoAesEaxKey =
+        com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .setParams(AesEaxParams.newBuilder().setIvSize(16))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesEaxKey",
+            protoAesEaxKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void parseKey_legacy() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(AesEaxParams.newBuilder().setIvSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            1479);
+    // Legacy keys are parsed to crunchy
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((AesEaxParameters) parsed.getParameters()).getVariant())
+        .isEqualTo(AesEaxParameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    AesEaxParameters parameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.TINK)
+            .build();
+    AesEaxKey key =
+        AesEaxKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Bad IV size: only 12 or 16 bytes values accepted
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(AesEaxParams.newBuilder().setIvSize(10))
+                .build()),
+        // Bad key size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                .setKeySize(10)
+                .setParams(AesEaxParams.newBuilder().setIvSize(12))
+                .build()),
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.AesEaxKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(AesEaxParams.newBuilder().setIvSize(12))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(AesEaxParams.newBuilder().setIvSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(AesEaxParams.newBuilder().setIvSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad IV Size
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(AesEaxParams.newBuilder().setIvSize(10))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Key Length
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesEaxKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[8]))
+                .setParams(AesEaxParams.newBuilder().setIvSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyManagerTest.java
index b4851b5..f6c745e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyManagerTest.java
@@ -20,7 +20,6 @@
 import static com.google.crypto.tink.testing.KeyTypeManagerTestUtil.testKeyTemplateCompatible;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
@@ -32,14 +31,15 @@
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.subtle.AesGcmJce;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -51,6 +51,11 @@
   private final KeyTypeManager.KeyFactory<AesGcmKeyFormat, AesGcmKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    AeadConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType()).isEqualTo("type.googleapis.com/google.crypto.tink.AesGcmKey");
@@ -112,7 +117,7 @@
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 50;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -148,12 +153,12 @@
         String tag) {
       try {
         this.name = name;
-        this.keyValue = TestUtil.hexDecode(keyValue);
-        this.plaintext = TestUtil.hexDecode(plaintext);
-        this.aad = TestUtil.hexDecode(aad);
-        this.iv = TestUtil.hexDecode(iv);
-        this.ciphertext = TestUtil.hexDecode(ciphertext);
-        this.tag = TestUtil.hexDecode(tag);
+        this.keyValue = Hex.decode(keyValue);
+        this.plaintext = Hex.decode(plaintext);
+        this.aad = Hex.decode(aad);
+        this.iv = Hex.decode(iv);
+        this.ciphertext = Hex.decode(ciphertext);
+        this.tag = Hex.decode(tag);
       } catch (Exception ignored) {
         // Ignored
       }
@@ -318,9 +323,6 @@
   @Test
   public void testNistVectors() throws Exception {
     for (NistTestVector t : nistTestVectors) {
-      if (TestUtil.shouldSkipTestWithAesKeySize(t.keyValue.length)) {
-        continue;
-      }
       if (t.iv.length != 12 || t.tag.length != 16) {
         // We support only 12-byte IV and 16-byte tag.
         continue;
@@ -379,6 +381,35 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    AesGcmKey key =
+        factory.deriveKey(
+            AesGcmKeyFormat.newBuilder().setVersion(0).setKeySize(keySize).build(),
+            fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKey_notEnoughKeyMaterial_throws() throws Exception {
     byte[] keyMaterial = Random.randBytes(31);
     AesGcmKeyFormat format = AesGcmKeyFormat.newBuilder().setVersion(0).setKeySize(32).build();
@@ -416,45 +447,53 @@
   @Test
   public void testAes128GcmTemplate() throws Exception {
     KeyTemplate template = AesGcmKeyManager.aes128GcmTemplate();
-    assertEquals(new AesGcmKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesGcmKeyFormat format =
-        AesGcmKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(16, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmParameters.builder()
+                .setIvSizeBytes(12)
+                .setTagSizeBytes(16)
+                .setKeySizeBytes(16)
+                .setVariant(AesGcmParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes128GcmTemplate() throws Exception {
     KeyTemplate template = AesGcmKeyManager.rawAes128GcmTemplate();
-    assertEquals(new AesGcmKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    AesGcmKeyFormat format =
-        AesGcmKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(16, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmParameters.builder()
+                .setIvSizeBytes(12)
+                .setTagSizeBytes(16)
+                .setKeySizeBytes(16)
+                .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testAes256GcmTemplate() throws Exception {
     KeyTemplate template = AesGcmKeyManager.aes256GcmTemplate();
-    assertEquals(new AesGcmKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesGcmKeyFormat format =
-        AesGcmKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(32, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmParameters.builder()
+                .setIvSizeBytes(12)
+                .setTagSizeBytes(16)
+                .setKeySizeBytes(32)
+                .setVariant(AesGcmParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes256GcmTemplate() throws Exception {
     KeyTemplate template = AesGcmKeyManager.rawAes256GcmTemplate();
-    assertEquals(new AesGcmKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    AesGcmKeyFormat format =
-        AesGcmKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(32, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmParameters.builder()
+                .setIvSizeBytes(12)
+                .setTagSizeBytes(16)
+                .setKeySizeBytes(32)
+                .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyTest.java
new file mode 100644
index 0000000..7d9d5c2
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmKeyTest.java
@@ -0,0 +1,294 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesGcmKeyTest {
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesGcmKey key = AesGcmKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(24)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(24);
+    AesGcmKey key =
+        AesGcmKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    AesGcmKey key =
+        AesGcmKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesGcmKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmKey.builder().setKeyBytes(SecretBytes.randomBytes(32)).build());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_fails() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void paramtersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    AesGcmParameters parametersWithIdRequirement =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmKey.builder()
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .setParameters(parametersWithIdRequirement)
+                .build());
+  }
+
+  @Test
+  public void buildBadKeySize_fails() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes32 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes32Copy =
+        SecretBytes.copyFrom(
+            keyBytes32.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes32Diff = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes16 = SecretBytes.randomBytes(16);
+
+    AesGcmParameters noPrefixParametersKeySize32 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(32)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+    AesGcmParameters noPrefixParametersKeySize16 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+    AesGcmParameters tinkPrefixParametersKeySize32 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(32)
+            .setVariant(AesGcmParameters.Variant.TINK)
+            .build();
+    AesGcmParameters crunchyPrefixParametersKeySize32 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(32)
+            .setVariant(AesGcmParameters.Variant.CRUNCHY)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes32",
+            AesGcmKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built twice must be equal.
+            AesGcmKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built with a copy of key bytes must be equal.
+            AesGcmKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .build(),
+            // Setting id requirement to null is equal to not setting it.
+            AesGcmKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal.
+        .addEqualityGroup(
+            "No prefix, newly generated keyBytes32",
+            AesGcmKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Diff)
+                .build())
+        // This group checks that keys with different key sizes are not equal.
+        .addEqualityGroup(
+            "No prefix, keyBytes16",
+            AesGcmKey.builder()
+                .setParameters(noPrefixParametersKeySize16)
+                .setKeyBytes(keyBytes16)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes32",
+            AesGcmKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build(),
+            AesGcmKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal.
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes32",
+            AesGcmKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1908)
+                .build())
+        // This groups checks that keys with different output prefix types are not equal.
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes32",
+            AesGcmKey.builder()
+                .setParameters(crunchyPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    AesGcmParameters aesGcmParameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesEaxParameters aesEaxParameters =
+        AesEaxParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+            .build();
+
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesEaxKey aesEaxKey =
+        AesEaxKey.builder().setParameters(aesEaxParameters).setKeyBytes(keyBytes).build();
+    AesGcmKey aesGcmKey =
+        AesGcmKey.builder().setParameters(aesGcmParameters).setKeyBytes(keyBytes).build();
+
+    assertThat(aesGcmKey.equalsKey(aesEaxKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmParametersTest.java
new file mode 100644
index 0000000..04af50b
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmParametersTest.java
@@ -0,0 +1,257 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesGcmParametersTest {
+  private static final AesGcmParameters.Variant NO_PREFIX = AesGcmParameters.Variant.NO_PREFIX;
+  private static final AesGcmParameters.Variant TINK = AesGcmParameters.Variant.TINK;
+  private static final AesGcmParameters.Variant CRUNCHY = AesGcmParameters.Variant.CRUNCHY;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getIvSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getTagSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setVariant(NO_PREFIX)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingIvSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingTagSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(null)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(24)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(TINK)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(24);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(CRUNCHY)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    AesGcmParameters parameters1 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    AesGcmParameters parameters2 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void buildParametersWithBadKeySizeFails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(12)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(34)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithBadTagSizeFails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(16)
+                .setTagSizeBytes(17)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithBadIvSizeFails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmParameters.builder()
+                .setKeySizeBytes(16)
+                .setIvSizeBytes(0)
+                .setTagSizeBytes(17)
+                .setVariant(NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesGcmParameters parameters1 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+
+    AesGcmParameters parameters2 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(24)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(14)
+            .setVariant(NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(TINK)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(CRUNCHY)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmProtoSerializationTest.java
new file mode 100644
index 0000000..5bc009f
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmProtoSerializationTest.java
@@ -0,0 +1,430 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesGcmSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesGcmProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesGcmKey";
+
+  private static final SecretBytes KEY_BYTES_16 = SecretBytes.randomBytes(16);
+  private static final ByteString KEY_BYTES_16_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesGcmProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesGcmProtoSerialization.register(registry);
+    AesGcmProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder().setKeySize(16).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(24)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.TINK)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder().setKeySize(24).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.CRUNCHY)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder().setKeySize(32).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParameters_badTagSize_fails() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(12)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    // Fails when tag size is not a 16-byte value
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeParameters(parameters, ProtoParametersSerialization.class));
+  }
+
+  @Test
+  public void serializeParameters_badIvSize_fails() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    // Fails when IV size is not a 12-byte value
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeParameters(parameters, ProtoParametersSerialization.class));
+  }
+
+  @Test
+  public void serializeParseKey_noPrefix() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesGcmKey key = AesGcmKey.builder().setParameters(parameters).setKeyBytes(KEY_BYTES_16).build();
+
+    com.google.crypto.tink.proto.AesGcmKey protoAesGcmKey =
+        com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            protoAesGcmKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.TINK)
+            .build();
+
+    AesGcmKey key =
+        AesGcmKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesGcmKey protoAesGcmKey =
+        com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            protoAesGcmKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(16)
+            .setIvSizeBytes(12)
+            .setVariant(AesGcmParameters.Variant.CRUNCHY)
+            .build();
+
+    AesGcmKey key =
+        AesGcmKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesGcmKey protoAesGcmKey =
+        com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            protoAesGcmKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.AesGcmKey protoAesGcmKey =
+        com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmKey",
+            protoAesGcmKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void parseKey_legacy() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            1479);
+    // Legacy keys are parsed to crunchy
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((AesGcmParameters) parsed.getParameters()).getVariant())
+        .isEqualTo(AesGcmParameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    AesGcmParameters parameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(12)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.TINK)
+            .build();
+    AesGcmKey key =
+        AesGcmKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Bad key size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder().setKeySize(10).build()),
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder().setKeySize(16).build()),
+        // Bad version
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setVersion(1)
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Key Length
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[8]))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyManagerTest.java
index 945e791..95ecdc1 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyManagerTest.java
@@ -19,7 +19,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.crypto.tink.testing.KeyTypeManagerTestUtil.testKeyTemplateCompatible;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Aead;
@@ -29,11 +28,10 @@
 import com.google.crypto.tink.proto.AesGcmSivKey;
 import com.google.crypto.tink.proto.AesGcmSivKeyFormat;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.security.Security;
 import java.util.Set;
@@ -52,7 +50,7 @@
       manager.keyFactory();
 
   @Before
-  public void setUpConscrypt() throws Exception {
+  public void setUp() throws Exception {
     try {
       Conscrypt.checkAvailability();
       Security.addProvider(Conscrypt.newProvider());
@@ -60,6 +58,7 @@
       throw new IllegalStateException(
           "Cannot test AesGcmSivKeyManager without Conscrypt Provider", cause);
     }
+    AeadConfig.register();
   }
 
   @Test
@@ -124,7 +123,7 @@
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 50;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -183,6 +182,35 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    AesGcmSivKey key =
+        factory.deriveKey(
+            AesGcmSivKeyFormat.newBuilder().setVersion(0).setKeySize(keySize).build(),
+            fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKey_notEnoughKeyMaterial_throws() throws Exception {
     byte[] keyMaterial = Random.randBytes(31);
     AesGcmSivKeyFormat format =
@@ -222,45 +250,45 @@
   @Test
   public void testAes128GcmSivTemplate() throws Exception {
     KeyTemplate template = AesGcmSivKeyManager.aes128GcmSivTemplate();
-    assertEquals(new AesGcmSivKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesGcmSivKeyFormat format =
-        AesGcmSivKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(16, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmSivParameters.builder()
+                .setKeySizeBytes(16)
+                .setVariant(AesGcmSivParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes128GcmSivTemplate() throws Exception {
     KeyTemplate template = AesGcmSivKeyManager.rawAes128GcmSivTemplate();
-    assertEquals(new AesGcmSivKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    AesGcmSivKeyFormat format =
-        AesGcmSivKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(16, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmSivParameters.builder()
+                .setKeySizeBytes(16)
+                .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testAes256GcmSivTemplate() throws Exception {
     KeyTemplate template = AesGcmSivKeyManager.aes256GcmSivTemplate();
-    assertEquals(new AesGcmSivKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    AesGcmSivKeyFormat format =
-        AesGcmSivKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(32, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmSivParameters.builder()
+                .setKeySizeBytes(32)
+                .setVariant(AesGcmSivParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes256GcmSivTemplate() throws Exception {
     KeyTemplate template = AesGcmSivKeyManager.rawAes256GcmSivTemplate();
-    assertEquals(new AesGcmSivKeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    AesGcmSivKeyFormat format =
-        AesGcmSivKeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
-    assertEquals(32, format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmSivParameters.builder()
+                .setKeySizeBytes(32)
+                .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyTest.java
new file mode 100644
index 0000000..d244f9b
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivKeyTest.java
@@ -0,0 +1,272 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesGcmSivKeyTest {
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesGcmSivKey key =
+        AesGcmSivKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesGcmSivKey key =
+        AesGcmSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    AesGcmSivKey key =
+        AesGcmSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesGcmSivKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmSivKey.builder().setKeyBytes(SecretBytes.randomBytes(32)).build());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_fails() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmSivKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void paramtersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    AesGcmSivParameters parametersWithIdRequirement =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmSivKey.builder()
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .setParameters(parametersWithIdRequirement)
+                .build());
+  }
+
+  @Test
+  public void buildBadKeySize_fails() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesGcmSivKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes32 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes32Copy =
+        SecretBytes.copyFrom(
+            keyBytes32.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes32Diff = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes16 = SecretBytes.randomBytes(16);
+
+    AesGcmSivParameters noPrefixParametersKeySize32 =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+    AesGcmSivParameters noPrefixParametersKeySize16 =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+    AesGcmSivParameters tinkPrefixParametersKeySize32 =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.TINK)
+            .build();
+    AesGcmSivParameters crunchyPrefixParametersKeySize32 =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.CRUNCHY)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes32",
+            AesGcmSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built twice must be equal.
+            AesGcmSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built with a copy of key bytes must be equal.
+            AesGcmSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .build(),
+            // Setting id requirement to null is equal to not setting it.
+            AesGcmSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal.
+        .addEqualityGroup(
+            "No prefix, newly generated keyBytes32",
+            AesGcmSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Diff)
+                .build())
+        // This group checks that keys with different key sizes are not equal.
+        .addEqualityGroup(
+            "No prefix, keyBytes16",
+            AesGcmSivKey.builder()
+                .setParameters(noPrefixParametersKeySize16)
+                .setKeyBytes(keyBytes16)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes32",
+            AesGcmSivKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build(),
+            AesGcmSivKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal.
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes32",
+            AesGcmSivKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1908)
+                .build())
+        // This groups checks that keys with different output prefix types are not equal.
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes32",
+            AesGcmSivKey.builder()
+                .setParameters(crunchyPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    AesGcmSivParameters aesGcmSivParameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesGcmParameters aesGcmParameters =
+        AesGcmParameters.builder()
+            .setKeySizeBytes(16)
+            .setIvSizeBytes(16)
+            .setTagSizeBytes(16)
+            .setVariant(AesGcmParameters.Variant.NO_PREFIX)
+            .build();
+
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    AesGcmKey aesGcmKey =
+        AesGcmKey.builder().setParameters(aesGcmParameters).setKeyBytes(keyBytes).build();
+    AesGcmSivKey aesGcmSivKey =
+        AesGcmSivKey.builder().setParameters(aesGcmSivParameters).setKeyBytes(keyBytes).build();
+
+    assertThat(aesGcmSivKey.equalsKey(aesGcmKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivParametersTest.java
new file mode 100644
index 0000000..967fe53
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivParametersTest.java
@@ -0,0 +1,121 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesGcmSivParametersTest {
+  private static final AesGcmSivParameters.Variant NO_PREFIX =
+      AesGcmSivParameters.Variant.NO_PREFIX;
+  private static final AesGcmSivParameters.Variant TINK = AesGcmSivParameters.Variant.TINK;
+  private static final AesGcmSivParameters.Variant CRUNCHY = AesGcmSivParameters.Variant.CRUNCHY;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(NO_PREFIX).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    AesGcmSivParameters parameters = AesGcmSivParameters.builder().setKeySizeBytes(16).build();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmSivParameters.builder().setVariant(NO_PREFIX).build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(null).build());
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(TINK).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder().setKeySizeBytes(32).setVariant(CRUNCHY).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithBadKeySizeFails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmSivParameters.builder().setKeySizeBytes(12).setVariant(NO_PREFIX).build());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesGcmSivParameters.builder().setKeySizeBytes(34).setVariant(NO_PREFIX).build());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    AesGcmSivParameters parameters1 =
+        AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(NO_PREFIX).build();
+    AesGcmSivParameters parameters2 =
+        AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(NO_PREFIX).build();
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesGcmSivParameters parameters1 =
+        AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(NO_PREFIX).build();
+
+    AesGcmSivParameters parameters2 =
+        AesGcmSivParameters.builder().setKeySizeBytes(32).setVariant(NO_PREFIX).build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 = AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(TINK).build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 = AesGcmSivParameters.builder().setKeySizeBytes(16).setVariant(CRUNCHY).build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivProtoSerializationTest.java
new file mode 100644
index 0000000..7e103c3
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/AesGcmSivProtoSerializationTest.java
@@ -0,0 +1,385 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesGcmSivSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesGcmSivProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesGcmSivKey";
+
+  private static final SecretBytes KEY_BYTES_16 = SecretBytes.randomBytes(16);
+  private static final ByteString KEY_BYTES_16_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesGcmSivProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesGcmSivProtoSerialization.register(registry);
+    AesGcmSivProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder().setKeySize(16).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmSivKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.TINK)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder().setKeySize(32).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmSivKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.CRUNCHY)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder().setKeySize(32).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmSivKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseKey_noPrefix() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesGcmSivKey key =
+        AesGcmSivKey.builder().setParameters(parameters).setKeyBytes(KEY_BYTES_16).build();
+
+    com.google.crypto.tink.proto.AesGcmSivKey protoAesGcmSivKey =
+        com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            protoAesGcmSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmSivKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.TINK)
+            .build();
+
+    AesGcmSivKey key =
+        AesGcmSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesGcmSivKey protoAesGcmSivKey =
+        com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            protoAesGcmSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmSivKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.CRUNCHY)
+            .build();
+
+    AesGcmSivKey key =
+        AesGcmSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesGcmSivKey protoAesGcmSivKey =
+        com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            protoAesGcmSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmSivKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.AesGcmSivKey protoAesGcmSivKey =
+        com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+            protoAesGcmSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void parseKey_legacy() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            1479);
+    // Legacy keys are parsed to crunchy
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((AesGcmSivParameters) parsed.getParameters()).getVariant())
+        .isEqualTo(AesGcmSivParameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    AesGcmSivParameters parameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(16)
+            .setVariant(AesGcmSivParameters.Variant.TINK)
+            .build();
+    AesGcmSivKey key =
+        AesGcmSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_16)
+            .setIdRequirement(123)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Bad key size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder().setKeySize(10).build()),
+        // Bad version
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setVersion(1)
+                .build()),
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.AesGcmSivKeyFormat.newBuilder().setKeySize(16).build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Key Length
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesGcmSivKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[8]))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/aead/BUILD.bazel
index b21932a..e0092c6 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/BUILD.bazel
@@ -52,11 +52,14 @@
     srcs = ["KmsEnvelopeAeadTest.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead",
         "//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -70,12 +73,14 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -95,12 +100,14 @@
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:fake_kms_client",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -115,14 +122,16 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -140,13 +149,15 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:encrypt_then_authenticate",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -171,6 +182,7 @@
         "//src/main/java/com/google/crypto/tink/internal/testing:fake_monitoring_client",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
@@ -194,7 +206,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:fake_kms_client",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -212,15 +224,20 @@
         "//proto:kms_aead_java_proto",
         "//proto:kms_envelope_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/aead:aead_parameters",
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key_manager",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -234,14 +251,16 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:aes_eax_jce",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -257,9 +276,9 @@
         "//src/main/java/com/google/crypto/tink/aead:aes_ctr_key_manager",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_jce_cipher",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -274,13 +293,14 @@
         "//proto:xchacha20_poly1305_java_proto",
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:x_cha_cha20_poly1305",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -292,14 +312,348 @@
     srcs = ["AeadTest.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
         "//src/main/java/com/google/crypto/tink:deterministic_aead",
-        "//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "AesEaxParametersTest",
+    size = "small",
+    srcs = ["AesEaxParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesEaxKeyTest",
+    size = "small",
+    srcs = ["AesEaxKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesEaxProtoSerializationTest",
+    size = "small",
+    srcs = ["AesEaxProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_eax_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmParametersTest",
+    size = "small",
+    srcs = ["AesGcmParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmKeyTest",
+    size = "small",
+    srcs = ["AesGcmKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmSivParametersTest",
+    size = "small",
+    srcs = ["AesGcmSivParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmSivKeyTest",
+    size = "small",
+    srcs = ["AesGcmSivKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmProtoSerializationTest",
+    size = "small",
+    srcs = ["AesGcmProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_gcm_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmSivProtoSerializationTest",
+    size = "small",
+    srcs = ["AesGcmSivProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_gcm_siv_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "ChaCha20Poly1305KeyTest",
+    size = "small",
+    srcs = ["ChaCha20Poly1305KeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "ChaCha20Poly1305ParametersTest",
+    size = "small",
+    srcs = ["ChaCha20Poly1305ParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "XChaCha20Poly1305KeyTest",
+    size = "small",
+    srcs = ["XChaCha20Poly1305KeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "XChaCha20Poly1305ParametersTest",
+    size = "small",
+    srcs = ["XChaCha20Poly1305ParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "ChaCha20Poly1305ProtoSerializationTest",
+    size = "small",
+    srcs = ["ChaCha20Poly1305ProtoSerializationTest.java"],
+    deps = [
+        "//proto:chacha20_poly1305_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "XChaCha20Poly1305ProtoSerializationTest",
+    size = "small",
+    srcs = ["XChaCha20Poly1305ProtoSerializationTest.java"],
+    deps = [
+        "//proto:tink_java_proto",
+        "//proto:xchacha20_poly1305_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCtrHmacAeadParametersTest",
+    size = "small",
+    srcs = ["AesCtrHmacAeadParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCtrHmacAeadKeyTest",
+    size = "small",
+    srcs = ["AesCtrHmacAeadKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCtrHmacAeadProtoSerializationTest",
+    size = "small",
+    srcs = ["AesCtrHmacAeadProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_ctr_hmac_aead_java_proto",
+        "//proto:aes_ctr_java_proto",
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:aes_ctr_hmac_aead_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PredefinedAeadParametersTest",
+    size = "small",
+    srcs = ["PredefinedAeadParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/aead:aead_parameters",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManagerTest.java
index 66b801e..240a759 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyManagerTest.java
@@ -28,12 +28,12 @@
 import com.google.crypto.tink.proto.ChaCha20Poly1305Key;
 import com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -45,6 +45,11 @@
   private final KeyTypeManager.KeyFactory<ChaCha20Poly1305KeyFormat, ChaCha20Poly1305Key> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    AeadConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(new ChaCha20Poly1305KeyManager().getKeyType())
@@ -117,7 +122,7 @@
     final int numKeys = 1000;
     for (int i = 0; i < numKeys; ++i) {
       keys.add(
-          TestUtil.hexEncode(
+          Hex.encode(
               factory.createKey(ChaCha20Poly1305KeyFormat.getDefaultInstance()).toByteArray()));
     }
     assertThat(keys).hasSize(numKeys);
@@ -143,21 +148,14 @@
   @Test
   public void testChaCha20Poly1305Template() throws Exception {
     KeyTemplate template = ChaCha20Poly1305KeyManager.chaCha20Poly1305Template();
-    assertEquals(new ChaCha20Poly1305KeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.TINK, template.getOutputPrefixType());
-    ChaCha20Poly1305KeyFormat unused =
-        ChaCha20Poly1305KeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters())
+        .isEqualTo(ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK));
   }
 
   @Test
   public void testRawChaCha20Poly1305Template() throws Exception {
     KeyTemplate template = ChaCha20Poly1305KeyManager.rawChaCha20Poly1305Template();
-    assertEquals(new ChaCha20Poly1305KeyManager().getKeyType(), template.getTypeUrl());
-    assertEquals(KeyTemplate.OutputPrefixType.RAW, template.getOutputPrefixType());
-    ChaCha20Poly1305KeyFormat unused =
-        ChaCha20Poly1305KeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters()).isEqualTo(ChaCha20Poly1305Parameters.create());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyTest.java
new file mode 100644
index 0000000..ba8f314
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305KeyTest.java
@@ -0,0 +1,130 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ChaCha20Poly1305KeyTest {
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    ChaCha20Poly1305Key key = ChaCha20Poly1305Key.create(keyBytes);
+    assertThat(key.getParameters()).isEqualTo(ChaCha20Poly1305Parameters.create());
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildNoPrefixVariantExplicitAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    ChaCha20Poly1305Key key =
+        ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytes, null);
+    assertThat(key.getParameters()).isEqualTo(ChaCha20Poly1305Parameters.create());
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    ChaCha20Poly1305Key key =
+        ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.TINK, keyBytes, 0x0708090a);
+    assertThat(key.getParameters())
+        .isEqualTo(ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK));
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x01, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    ChaCha20Poly1305Key key =
+        ChaCha20Poly1305Key.create(
+            ChaCha20Poly1305Parameters.Variant.CRUNCHY, keyBytes, 0x0708090a);
+    assertThat(key.getParameters())
+        .isEqualTo(ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY));
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void wrongIdRequirement_throws() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            ChaCha20Poly1305Key.create(
+                ChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytes, 1115));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY, keyBytes, null));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.TINK, keyBytes, null));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    SecretBytes keyBytesCopy =
+        SecretBytes.copyFrom(
+            keyBytes.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytesDiff = SecretBytes.randomBytes(32);
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes",
+            ChaCha20Poly1305Key.create(
+                ChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytes, null),
+            ChaCha20Poly1305Key.create(keyBytes),
+            ChaCha20Poly1305Key.create(
+                ChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytesCopy, null))
+        .addEqualityGroup(
+            "No prefix, different key bytes",
+            ChaCha20Poly1305Key.create(
+                ChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytesDiff, null))
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes32",
+            ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.TINK, keyBytes, 1907),
+            ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.TINK, keyBytesCopy, 1907))
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes32",
+            ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.TINK, keyBytes, 1908))
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes32",
+            ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY, keyBytes, 1907))
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305ParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305ParametersTest.java
new file mode 100644
index 0000000..efdf94b
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305ParametersTest.java
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ChaCha20Poly1305ParametersTest {
+  private static final ChaCha20Poly1305Parameters.Variant NO_PREFIX =
+      ChaCha20Poly1305Parameters.Variant.NO_PREFIX;
+  private static final ChaCha20Poly1305Parameters.Variant TINK =
+      ChaCha20Poly1305Parameters.Variant.TINK;
+  private static final ChaCha20Poly1305Parameters.Variant CRUNCHY =
+      ChaCha20Poly1305Parameters.Variant.CRUNCHY;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    ChaCha20Poly1305Parameters parameters = ChaCha20Poly1305Parameters.create();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParameters_setVariantExplicitly() throws Exception {
+    ChaCha20Poly1305Parameters parameters =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.NO_PREFIX);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParameters_tink() throws Exception {
+    ChaCha20Poly1305Parameters parameters =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParameters_crunchy() throws Exception {
+    ChaCha20Poly1305Parameters parameters =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_noPrefix() throws Exception {
+    ChaCha20Poly1305Parameters parametersNoPrefix0 = ChaCha20Poly1305Parameters.create();
+    ChaCha20Poly1305Parameters parametersNoPrefix1 =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.NO_PREFIX);
+    assertThat(parametersNoPrefix0).isEqualTo(parametersNoPrefix1);
+    assertThat(parametersNoPrefix0.hashCode()).isEqualTo(parametersNoPrefix1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_tink() throws Exception {
+    ChaCha20Poly1305Parameters parametersTink0 =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK);
+    ChaCha20Poly1305Parameters parametersTink1 =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK);
+    assertThat(parametersTink0).isEqualTo(parametersTink1);
+    assertThat(parametersTink0.hashCode()).isEqualTo(parametersTink1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_crunchy() throws Exception {
+    ChaCha20Poly1305Parameters parametersCrunchy0 =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY);
+    ChaCha20Poly1305Parameters parametersCrunchy1 =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY);
+    assertThat(parametersCrunchy0).isEqualTo(parametersCrunchy1);
+    assertThat(parametersCrunchy0.hashCode()).isEqualTo(parametersCrunchy1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_different() throws Exception {
+    ChaCha20Poly1305Parameters parametersNoPrefix = ChaCha20Poly1305Parameters.create();
+
+    ChaCha20Poly1305Parameters parametersTink =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK);
+
+    ChaCha20Poly1305Parameters parametersCrunchy =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY);
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersTink);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersTink.hashCode());
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersCrunchy);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersCrunchy.hashCode());
+
+    assertThat(parametersTink).isNotEqualTo(parametersNoPrefix);
+    assertThat(parametersTink.hashCode()).isNotEqualTo(parametersNoPrefix.hashCode());
+
+    assertThat(parametersTink).isNotEqualTo(parametersCrunchy);
+    assertThat(parametersTink.hashCode()).isNotEqualTo(parametersCrunchy.hashCode());
+
+    assertThat(parametersCrunchy).isNotEqualTo(parametersNoPrefix);
+    assertThat(parametersCrunchy.hashCode()).isNotEqualTo(parametersNoPrefix.hashCode());
+
+    assertThat(parametersCrunchy).isNotEqualTo(parametersTink);
+    assertThat(parametersCrunchy.hashCode()).isNotEqualTo(parametersTink.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305ProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305ProtoSerializationTest.java
new file mode 100644
index 0000000..8eb3091
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/ChaCha20Poly1305ProtoSerializationTest.java
@@ -0,0 +1,299 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for ChaCha20Poly1305Serialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class ChaCha20Poly1305ProtoSerializationTest {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key";
+
+  private static final SecretBytes KEY_BYTES_32 = SecretBytes.randomBytes(32);
+  private static final ByteString KEY_BYTES_32_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_32.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    ChaCha20Poly1305ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    ChaCha20Poly1305ProtoSerialization.register(registry);
+    ChaCha20Poly1305ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    ChaCha20Poly1305Parameters parameters = ChaCha20Poly1305Parameters.create();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink() throws Exception {
+    ChaCha20Poly1305Parameters parameters =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK);
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy() throws Exception {
+    ChaCha20Poly1305Parameters parameters =
+        ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY);
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseKey_tink() throws Exception {
+    ChaCha20Poly1305Key key =
+        ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.TINK, KEY_BYTES_32, 123);
+
+    com.google.crypto.tink.proto.ChaCha20Poly1305Key protoChaCha20Poly1305Key =
+        com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key",
+            protoChaCha20Poly1305Key.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.ChaCha20Poly1305Key.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy() throws Exception {
+    ChaCha20Poly1305Key key =
+        ChaCha20Poly1305Key.create(ChaCha20Poly1305Parameters.Variant.CRUNCHY, KEY_BYTES_32, 123);
+
+    com.google.crypto.tink.proto.ChaCha20Poly1305Key protoChaCha20Poly1305Key =
+        com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key",
+            protoChaCha20Poly1305Key.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.ChaCha20Poly1305Key.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.ChaCha20Poly1305Key protoChaCha20Poly1305Key =
+        com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key",
+            protoChaCha20Poly1305Key.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void parseKey_legacy() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            1479);
+    // Legacy keys are parsed to crunchy
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((ChaCha20Poly1305Parameters) parsed.getParameters()).getVariant())
+        .isEqualTo(ChaCha20Poly1305Parameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    ChaCha20Poly1305Key key = ChaCha20Poly1305Key.create(KEY_BYTES_32);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.ChaCha20Poly1305KeyFormat.getDefaultInstance()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Key Length
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.ChaCha20Poly1305Key.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[16]))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManagerTest.java
index f4e5fd1..bbef1f3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadKeyManagerTest.java
@@ -25,6 +25,7 @@
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.mac.HmacKeyManager;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.KmsEnvelopeAeadKey;
 import com.google.crypto.tink.proto.KmsEnvelopeAeadKeyFormat;
@@ -95,6 +96,16 @@
   }
 
   @Test
+  public void createKeyFormatWithInvalidDekTemplate_fails() throws Exception {
+    String kekUri = FakeKmsClient.createFakeKeyUri();
+    KeyTemplate invalidDekTemplate = HmacKeyManager.hmacSha256Template();
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> KmsEnvelopeAeadKeyManager.createKeyFormat(kekUri, invalidDekTemplate));
+  }
+
+  @Test
   public void createKey() throws Exception {
     String kekUri = FakeKmsClient.createFakeKeyUri();
     KeyTemplate dekTemplate = AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template();
@@ -215,16 +226,16 @@
   }
 
   @Test
-  public void createKeyTemplate_multipleKeysWithSameKek() throws Exception {
+  public void multipleAeadsWithSameKekAndSameDekTemplate_canDecryptEachOther() throws Exception {
     String kekUri = FakeKmsClient.createFakeKeyUri();
     KeyTemplate dekTemplate = AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template();
 
-    KeyTemplate kt1 = KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate);
-    KeysetHandle handle1 = KeysetHandle.generateNew(kt1);
+    KeysetHandle handle1 =
+        KeysetHandle.generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate));
     Aead aead1 = handle1.getPrimitive(Aead.class);
 
-    KeyTemplate kt2 = KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate);
-    KeysetHandle handle2 = KeysetHandle.generateNew(kt2);
+    KeysetHandle handle2 =
+        KeysetHandle.generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate));
     Aead aead2 = handle2.getPrimitive(Aead.class);
 
     byte[] plaintext = Random.randBytes(20);
@@ -233,4 +244,67 @@
     assertThat(aead1.decrypt(aead2.encrypt(plaintext, associatedData), associatedData))
         .isEqualTo(plaintext);
   }
+
+  @Test
+  public void multipleAeadsWithSameKekAndDifferentDekTemplateOfSameKeyType_canDecryptEachOther()
+      throws Exception {
+    String kekUri = FakeKmsClient.createFakeKeyUri();
+
+    KeyTemplate dek1Template = AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template();
+    KeysetHandle handle1 =
+        KeysetHandle.generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dek1Template));
+    Aead aead1 = handle1.getPrimitive(Aead.class);
+
+    KeyTemplate dek2Template = AesCtrHmacAeadKeyManager.aes256CtrHmacSha256Template();
+    KeysetHandle handle2 =
+        KeysetHandle.generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dek2Template));
+    Aead aead2 = handle2.getPrimitive(Aead.class);
+
+    byte[] plaintext = Random.randBytes(20);
+    byte[] associatedData = Random.randBytes(20);
+
+    byte[] ciphertext = aead1.encrypt(plaintext, associatedData);
+
+    // This works because ciphertext contains an encrypted AesCtrHmacAeadKey, which aead2 correctly
+    // decrypts and parses. The resulting key can then decrypt the ciphertext.
+    assertThat(aead2.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void multipleAeadsWithSameKekAndDifferentDekTemplateKeyType_cannotDecryptEachOther()
+      throws Exception {
+    String kekUri = FakeKmsClient.createFakeKeyUri();
+
+    KeyTemplate dek1Template = AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template();
+    KeysetHandle handle1 =
+        KeysetHandle.generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dek1Template));
+    Aead aead1 = handle1.getPrimitive(Aead.class);
+
+    KeyTemplate dek2Template = AesGcmKeyManager.aes128GcmTemplate();
+    KeysetHandle handle2 =
+        KeysetHandle.generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dek2Template));
+    Aead aead2 = handle2.getPrimitive(Aead.class);
+
+    byte[] plaintext = Random.randBytes(20);
+    byte[] associatedData = Random.randBytes(20);
+
+    byte[] ciphertext = aead1.encrypt(plaintext, associatedData);
+
+    // ciphertext contains an encrypted AesCtrHmacAeadKey proto. aead2 can decrypt it, but it
+    // tries to parse it as an AesGcmKey proto. Either the parsing fails or the resulting key is
+    // not able to decrypt the ciphertext.
+    assertThrows(GeneralSecurityException.class, () -> aead2.decrypt(ciphertext, associatedData));
+  }
+
+  @Test
+  public void createKeyTemplateWithEnvelopeKeyTemplateAsDekTemplate_fails() throws Exception {
+    String kekUri = FakeKmsClient.createFakeKeyUri();
+
+    KeyTemplate dekTemplate =
+        KmsEnvelopeAeadKeyManager.createKeyTemplate(
+            kekUri, AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template());
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate));
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadTest.java
index 76b899e..96634eb 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/KmsEnvelopeAeadTest.java
@@ -16,28 +16,32 @@
 
 package com.google.crypto.tink.aead;
 
-
+import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
+import com.google.crypto.tink.mac.HmacKeyManager;
 import java.security.GeneralSecurityException;
-import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests for {@link KmsEnvelopeAead} */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public final class KmsEnvelopeAeadTest {
   private static final byte[] EMPTY_ADD = new byte[0];
 
-  @Before
-  public void setUp() throws GeneralSecurityException {
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
     AeadConfig.register();
   }
 
@@ -46,28 +50,59 @@
     return keysetHandle.getPrimitive(Aead.class);
   }
 
+  @DataPoints("tinkDekTemplates")
+  public static final String[] TINK_DEK_TEMPLATES =
+      new String[] {
+        "AES128_GCM",
+        "AES256_GCM",
+        "AES128_EAX",
+        "AES256_EAX",
+        "AES128_CTR_HMAC_SHA256",
+        "AES256_CTR_HMAC_SHA256",
+        "CHACHA20_POLY1305",
+        "XCHACHA20_POLY1305",
+        "AES128_GCM_RAW",
+      };
 
-  @Test
-  public void encryptDecrypt_works() throws GeneralSecurityException {
-    Aead remoteAead =  this.generateNewRemoteAead();
+  @Theory
+  public void encryptDecrypt_works(@FromDataPoints("tinkDekTemplates") String dekTemplateName)
+      throws Exception {
+    Aead remoteAead = this.generateNewRemoteAead();
     KmsEnvelopeAead envAead =
         new KmsEnvelopeAead(
-            KeyTemplateProtoConverter.toProto(KeyTemplates.get("AES128_EAX")), remoteAead);
-    byte[] plaintext = "helloworld".getBytes(UTF_8);
-    byte[] ciphertext = envAead.encrypt(plaintext, EMPTY_ADD);
-    assertArrayEquals(plaintext, envAead.decrypt(ciphertext, EMPTY_ADD));
+            KeyTemplateProtoConverter.toProto(KeyTemplates.get(dekTemplateName)), remoteAead);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = envAead.encrypt(plaintext, associatedData);
+    assertThat(envAead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+
+    assertThat(envAead.decrypt(envAead.encrypt(plaintext, EMPTY_ADD), EMPTY_ADD))
+        .isEqualTo(plaintext);
   }
 
+  @Test
+  public void createKeyFormatWithInvalidDekTemplate_fails() throws Exception {
+    Aead remoteAead = this.generateNewRemoteAead();
+    KeyTemplate invalidDekTemplate = HmacKeyManager.hmacSha256Template();
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            new KmsEnvelopeAead(KeyTemplateProtoConverter.toProto(invalidDekTemplate), remoteAead));
+  }
 
   @Test
-  public void encryptDecryptMissingAd_fails() throws GeneralSecurityException {
+  public void decryptWithInvalidAssociatedData_fails() throws GeneralSecurityException {
     Aead remoteAead =  this.generateNewRemoteAead();
     KmsEnvelopeAead envAead =
         new KmsEnvelopeAead(
             KeyTemplateProtoConverter.toProto(KeyTemplates.get("AES128_EAX")), remoteAead);
-    byte[] plaintext = "helloworld".getBytes(UTF_8);
-    byte[] associatedData = "envelope_ad".getBytes(UTF_8);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
     byte[] ciphertext = envAead.encrypt(plaintext, associatedData);
+    byte[] invalidAssociatedData = "invalidAssociatedData".getBytes(UTF_8);
+    assertThrows(
+        GeneralSecurityException.class, () -> envAead.decrypt(ciphertext, invalidAssociatedData));
     assertThrows(GeneralSecurityException.class, () -> envAead.decrypt(ciphertext, EMPTY_ADD));
   }
 
@@ -83,7 +118,7 @@
     ciphertext[ciphertext.length - 1] = (byte) (ciphertext[ciphertext.length - 1] ^ 0x1);
     byte[] corruptedCiphertext = ciphertext;
     assertThrows(
-        GeneralSecurityException.class, () -> envAead.decrypt(corruptedCiphertext, EMPTY_ADD));
+        GeneralSecurityException.class, () -> envAead.decrypt(corruptedCiphertext, associatedData));
   }
 
   @Test
@@ -98,7 +133,7 @@
     ciphertext[4] = (byte) (ciphertext[4] ^ 0x1);
     byte[] corruptedCiphertext = ciphertext;
     assertThrows(
-        GeneralSecurityException.class, () -> envAead.decrypt(corruptedCiphertext, EMPTY_ADD));
+        GeneralSecurityException.class, () -> envAead.decrypt(corruptedCiphertext, associatedData));
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/PredefinedAeadParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/PredefinedAeadParametersTest.java
new file mode 100644
index 0000000..6ba0c28
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/PredefinedAeadParametersTest.java
@@ -0,0 +1,69 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class PredefinedAeadParametersTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AeadConfig.register();
+  }
+
+  @DataPoints("AllParameters")
+  public static final AeadParameters[] TEMPLATES =
+      new AeadParameters[] {
+        PredefinedAeadParameters.AES128_GCM,
+        PredefinedAeadParameters.AES256_GCM,
+        PredefinedAeadParameters.AES128_EAX,
+        PredefinedAeadParameters.AES256_EAX,
+        PredefinedAeadParameters.AES128_CTR_HMAC_SHA256,
+        PredefinedAeadParameters.AES256_CTR_HMAC_SHA256,
+        PredefinedAeadParameters.CHACHA20_POLY1305,
+        PredefinedAeadParameters.XCHACHA20_POLY1305
+      };
+
+  @Theory
+  public void testInstantiation(@FromDataPoints("AllParameters") AeadParameters parameters)
+      throws Exception {
+    Key key = KeysetHandle.generateNew(parameters).getAt(0).getKey();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+  }
+
+  @Test
+  public void testNotNull() {
+    assertThat(PredefinedAeadParameters.AES128_GCM).isNotNull();
+    assertThat(PredefinedAeadParameters.AES256_GCM).isNotNull();
+    assertThat(PredefinedAeadParameters.AES128_EAX).isNotNull();
+    assertThat(PredefinedAeadParameters.AES256_EAX).isNotNull();
+    assertThat(PredefinedAeadParameters.AES128_CTR_HMAC_SHA256).isNotNull();
+    assertThat(PredefinedAeadParameters.AES256_CTR_HMAC_SHA256).isNotNull();
+    assertThat(PredefinedAeadParameters.CHACHA20_POLY1305).isNotNull();
+    assertThat(PredefinedAeadParameters.XCHACHA20_POLY1305).isNotNull();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManagerTest.java
index 1185b07..5f9c5c8 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyManagerTest.java
@@ -26,15 +26,15 @@
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.XChaCha20Poly1305Key;
 import com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.XChaCha20Poly1305;
-import com.google.crypto.tink.testing.TestUtil;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -46,6 +46,11 @@
   private final KeyTypeManager.KeyFactory<XChaCha20Poly1305KeyFormat, XChaCha20Poly1305Key>
       factory = manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    AeadConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType())
@@ -82,7 +87,7 @@
     final int numKeys = 100;
     for (int i = 0; i < numKeys; ++i) {
       keys.add(
-          TestUtil.hexEncode(
+          Hex.encode(
               factory
                   .createKey(XChaCha20Poly1305KeyFormat.getDefaultInstance())
                   .getKeyValue()
@@ -106,6 +111,34 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    XChaCha20Poly1305Key key =
+        factory.deriveKey(
+            XChaCha20Poly1305KeyFormat.newBuilder().setVersion(0).build(), fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKeyNotEnoughRandomness() throws Exception {
     byte[] keyMaterial = Random.randBytes(10);
     assertThrows(
@@ -142,21 +175,14 @@
   @Test
   public void testXChaCha20Poly1305Template() throws Exception {
     KeyTemplate template = XChaCha20Poly1305KeyManager.xChaCha20Poly1305Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new XChaCha20Poly1305KeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    XChaCha20Poly1305KeyFormat unused =
-        XChaCha20Poly1305KeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters())
+        .isEqualTo(XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK));
   }
 
   @Test
   public void testRawXChaCha20Poly1305Template() throws Exception {
     KeyTemplate template = XChaCha20Poly1305KeyManager.rawXChaCha20Poly1305Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new XChaCha20Poly1305KeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    XChaCha20Poly1305KeyFormat unused =
-        XChaCha20Poly1305KeyFormat.parseFrom(
-            ByteString.copyFrom(template.getValue()), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters()).isEqualTo(XChaCha20Poly1305Parameters.create());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyTest.java
new file mode 100644
index 0000000..a235f9b
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305KeyTest.java
@@ -0,0 +1,134 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class XChaCha20Poly1305KeyTest {
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    XChaCha20Poly1305Key key = XChaCha20Poly1305Key.create(keyBytes);
+    assertThat(key.getParameters()).isEqualTo(XChaCha20Poly1305Parameters.create());
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildNoPrefixVariantExplicitAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    XChaCha20Poly1305Key key =
+        XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytes, null);
+    assertThat(key.getParameters()).isEqualTo(XChaCha20Poly1305Parameters.create());
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    XChaCha20Poly1305Key key =
+        XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.TINK, keyBytes, 0x0708090a);
+    assertThat(key.getParameters())
+        .isEqualTo(XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK));
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x01, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    XChaCha20Poly1305Key key =
+        XChaCha20Poly1305Key.create(
+            XChaCha20Poly1305Parameters.Variant.CRUNCHY, keyBytes, 0x0708090a);
+    assertThat(key.getParameters())
+        .isEqualTo(XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY));
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void wrongIdRequirement_throws() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytes, 1115));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.CRUNCHY, keyBytes, null));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.TINK, keyBytes, null));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+    SecretBytes keyBytesCopy =
+        SecretBytes.copyFrom(
+            keyBytes.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytesDiff = SecretBytes.randomBytes(32);
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes",
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytes, null),
+            XChaCha20Poly1305Key.create(keyBytes),
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytesCopy, null))
+        .addEqualityGroup(
+            "No prefix, different key bytes",
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.NO_PREFIX, keyBytesDiff, null))
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes32",
+            XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.TINK, keyBytes, 1907),
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.TINK, keyBytesCopy, 1907))
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes32",
+            XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.TINK, keyBytes, 1908))
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes32",
+            XChaCha20Poly1305Key.create(
+                XChaCha20Poly1305Parameters.Variant.CRUNCHY, keyBytes, 1907))
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305ParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305ParametersTest.java
new file mode 100644
index 0000000..f461345
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305ParametersTest.java
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class XChaCha20Poly1305ParametersTest {
+  private static final XChaCha20Poly1305Parameters.Variant NO_PREFIX =
+      XChaCha20Poly1305Parameters.Variant.NO_PREFIX;
+  private static final XChaCha20Poly1305Parameters.Variant TINK =
+      XChaCha20Poly1305Parameters.Variant.TINK;
+  private static final XChaCha20Poly1305Parameters.Variant CRUNCHY =
+      XChaCha20Poly1305Parameters.Variant.CRUNCHY;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    XChaCha20Poly1305Parameters parameters = XChaCha20Poly1305Parameters.create();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParameters_setVariantExplicitly() throws Exception {
+    XChaCha20Poly1305Parameters parameters =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.NO_PREFIX);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParameters_tink() throws Exception {
+    XChaCha20Poly1305Parameters parameters =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParameters_crunchy() throws Exception {
+    XChaCha20Poly1305Parameters parameters =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_noPrefix() throws Exception {
+    XChaCha20Poly1305Parameters parametersNoPrefix0 = XChaCha20Poly1305Parameters.create();
+    XChaCha20Poly1305Parameters parametersNoPrefix1 =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.NO_PREFIX);
+    assertThat(parametersNoPrefix0).isEqualTo(parametersNoPrefix1);
+    assertThat(parametersNoPrefix0.hashCode()).isEqualTo(parametersNoPrefix1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_tink() throws Exception {
+    XChaCha20Poly1305Parameters parametersTink0 =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK);
+    XChaCha20Poly1305Parameters parametersTink1 =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK);
+    assertThat(parametersTink0).isEqualTo(parametersTink1);
+    assertThat(parametersTink0.hashCode()).isEqualTo(parametersTink1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_crunchy() throws Exception {
+    XChaCha20Poly1305Parameters parametersCrunchy0 =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY);
+    XChaCha20Poly1305Parameters parametersCrunchy1 =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY);
+    assertThat(parametersCrunchy0).isEqualTo(parametersCrunchy1);
+    assertThat(parametersCrunchy0.hashCode()).isEqualTo(parametersCrunchy1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_different() throws Exception {
+    XChaCha20Poly1305Parameters parametersNoPrefix = XChaCha20Poly1305Parameters.create();
+
+    XChaCha20Poly1305Parameters parametersTink =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK);
+
+    XChaCha20Poly1305Parameters parametersCrunchy =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY);
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersTink);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersTink.hashCode());
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersCrunchy);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersCrunchy.hashCode());
+
+    assertThat(parametersTink).isNotEqualTo(parametersNoPrefix);
+    assertThat(parametersTink.hashCode()).isNotEqualTo(parametersNoPrefix.hashCode());
+
+    assertThat(parametersTink).isNotEqualTo(parametersCrunchy);
+    assertThat(parametersTink.hashCode()).isNotEqualTo(parametersCrunchy.hashCode());
+
+    assertThat(parametersCrunchy).isNotEqualTo(parametersNoPrefix);
+    assertThat(parametersCrunchy.hashCode()).isNotEqualTo(parametersNoPrefix.hashCode());
+
+    assertThat(parametersCrunchy).isNotEqualTo(parametersTink);
+    assertThat(parametersCrunchy.hashCode()).isNotEqualTo(parametersTink.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305ProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305ProtoSerializationTest.java
new file mode 100644
index 0000000..fb9cdde
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/XChaCha20Poly1305ProtoSerializationTest.java
@@ -0,0 +1,312 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.aead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for XChaCha20Poly1305Serialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class XChaCha20Poly1305ProtoSerializationTest {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key";
+
+  private static final SecretBytes KEY_BYTES_32 = SecretBytes.randomBytes(32);
+  private static final ByteString KEY_BYTES_32_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_32.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    XChaCha20Poly1305ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    XChaCha20Poly1305ProtoSerialization.register(registry);
+    XChaCha20Poly1305ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    XChaCha20Poly1305Parameters parameters = XChaCha20Poly1305Parameters.create();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink() throws Exception {
+    XChaCha20Poly1305Parameters parameters =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.TINK);
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy() throws Exception {
+    XChaCha20Poly1305Parameters parameters =
+        XChaCha20Poly1305Parameters.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY);
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseKey_tink() throws Exception {
+    XChaCha20Poly1305Key key =
+        XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.TINK, KEY_BYTES_32, 123);
+
+    com.google.crypto.tink.proto.XChaCha20Poly1305Key protoXChaCha20Poly1305Key =
+        com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+            protoXChaCha20Poly1305Key.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.XChaCha20Poly1305Key.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy() throws Exception {
+    XChaCha20Poly1305Key key =
+        XChaCha20Poly1305Key.create(XChaCha20Poly1305Parameters.Variant.CRUNCHY, KEY_BYTES_32, 123);
+
+    com.google.crypto.tink.proto.XChaCha20Poly1305Key protoXChaCha20Poly1305Key =
+        com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+            protoXChaCha20Poly1305Key.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.XChaCha20Poly1305Key.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.XChaCha20Poly1305Key protoXChaCha20Poly1305Key =
+        com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+            protoXChaCha20Poly1305Key.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void parseKey_legacy() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            1479);
+    // Legacy keys are parsed to crunchy
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((XChaCha20Poly1305Parameters) parsed.getParameters()).getVariant())
+        .isEqualTo(XChaCha20Poly1305Parameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    XChaCha20Poly1305Key key = XChaCha20Poly1305Key.create(KEY_BYTES_32);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.getDefaultInstance()),
+        // Wrong version
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.XChaCha20Poly1305KeyFormat.newBuilder()
+                .setVersion(1)
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Key Length
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.XChaCha20Poly1305Key.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[16]))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel
index dfde015..5201fe3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel
@@ -26,12 +26,13 @@
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -47,7 +48,6 @@
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -101,7 +101,6 @@
         "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_x_cha_cha20",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -113,8 +112,8 @@
     srcs = ["Poly1305Test.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink/aead/internal:poly1305",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJceTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJceTest.java
index d19e25f..ed3197e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceAesGcmJceTest.java
@@ -24,10 +24,10 @@
 
 import com.google.crypto.tink.config.TinkFips;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.internal.Util;
 import com.google.crypto.tink.subtle.Bytes;
 import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.subtle.SubtleUtil;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.crypto.tink.testing.TestUtil.BytesMutation;
 import com.google.crypto.tink.testing.WycheproofTestUtil;
@@ -37,7 +37,7 @@
 import java.security.Security;
 import java.util.Arrays;
 import java.util.HashSet;
-import javax.crypto.Cipher;
+import javax.annotation.Nullable;
 import org.conscrypt.Conscrypt;
 import org.junit.Assume;
 import org.junit.Before;
@@ -53,14 +53,7 @@
 
   @Before
   public void setUp() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip tests with keys larger than 128 bits.");
-      keySizeInBytes = new Integer[] {16};
-    } else {
-      keySizeInBytes = new Integer[] {16, 32};
-    }
+    keySizeInBytes = new Integer[] {16, 32};
   }
 
   @Before
@@ -116,7 +109,9 @@
   @Test
   public void testEncryptWithAad_shouldFailOnAndroid19OrOlder() throws Exception {
     Assume.assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
-    Assume.assumeFalse(!SubtleUtil.isAndroid() || SubtleUtil.androidApiLevel() > 19);
+    @Nullable Integer apiLevel = Util.getAndroidApiLevel();
+    Assume.assumeNotNull(apiLevel);
+    Assume.assumeTrue(apiLevel <= 19);
 
     InsecureNonceAesGcmJce gcm =
         new InsecureNonceAesGcmJce(Random.randBytes(16), /*prependIv=*/ false);
@@ -262,7 +257,8 @@
         byte[] key = Hex.decode(testcase.get("key").getAsString());
         byte[] msg = Hex.decode(testcase.get("msg").getAsString());
         byte[] aad = Hex.decode(testcase.get("aad").getAsString());
-        if (SubtleUtil.isAndroid() && SubtleUtil.androidApiLevel() <= 19 && aad.length != 0) {
+        @Nullable Integer androidApiLevel = Util.getAndroidApiLevel();
+        if (androidApiLevel != null && androidApiLevel <= 19 && aad.length != 0) {
           cntSkippedTests++;
           continue;
         }
@@ -421,7 +417,7 @@
     for (int i = 0; i < samples; i++) {
       byte[] iv = Random.randBytes(InsecureNonceAesGcmJce.IV_SIZE_IN_BYTES);
       byte[] ct = gcm.encrypt(iv, message, aad);
-      String ctHex = TestUtil.hexEncode(ct);
+      String ctHex = Hex.encode(ct);
       assertThat(ciphertexts).doesNotContain(ctHex);
       ciphertexts.add(ctHex);
     }
@@ -431,7 +427,8 @@
     byte[] aad = Random.randBytes(20);
     // AES-GCM on Android <= 19 doesn't support AAD. See last bullet point in
     // https://github.com/google/tink/blob/master/docs/KNOWN-ISSUES.md#android.
-    if (SubtleUtil.isAndroid() && SubtleUtil.androidApiLevel() <= 19) {
+    @Nullable Integer apiLevel = Util.getAndroidApiLevel();
+    if (apiLevel != null && apiLevel <= 19) {
       aad = new byte[0];
     }
     return aad;
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Test.java b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Test.java
index 08f7554..64dda69 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Test.java
@@ -250,7 +250,7 @@
     for (int i = 0; i < samples; i++) {
       byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES);
       byte[] ct = cipher.encrypt(nonce, message, aad);
-      String ctHex = TestUtil.hexEncode(ct);
+      String ctHex = Hex.encode(ct);
       assertThat(ciphertexts).doesNotContain(ctHex);
       ciphertexts.add(ctHex);
     }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Test.java b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Test.java
index eefacd2..dacc132 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Test.java
@@ -23,7 +23,6 @@
 import com.google.common.truth.Truth;
 import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import org.junit.Test;
@@ -50,10 +49,10 @@
         assertArrayEquals(
             String.format(
                 "\n\nMessage: %s\nKey: %s\nOutput: %s\nDecrypted Msg: %s\n",
-                TestUtil.hexEncode(expectedInput),
-                TestUtil.hexEncode(key),
-                TestUtil.hexEncode(output),
-                TestUtil.hexEncode(actualInput)),
+                Hex.encode(expectedInput),
+                Hex.encode(key),
+                Hex.encode(output),
+                Hex.encode(actualInput)),
             expectedInput,
             actualInput);
       }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceXChaCha20Test.java b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceXChaCha20Test.java
index 9b1a4c4..4daf36c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceXChaCha20Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceXChaCha20Test.java
@@ -22,7 +22,6 @@
 
 import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import org.junit.Test;
@@ -49,10 +48,10 @@
         assertArrayEquals(
             String.format(
                 "\n\nMessage: %s\nKey: %s\nOutput: %s\nDecrypted Msg: %s\n",
-                TestUtil.hexEncode(expectedInput),
-                TestUtil.hexEncode(key),
-                TestUtil.hexEncode(output),
-                TestUtil.hexEncode(actualInput)),
+                Hex.encode(expectedInput),
+                Hex.encode(key),
+                Hex.encode(output),
+                Hex.encode(actualInput)),
             expectedInput,
             actualInput);
       }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/Poly1305Test.java b/java_src/src/test/java/com/google/crypto/tink/aead/internal/Poly1305Test.java
index fd0fdfb..600aa11 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/Poly1305Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/Poly1305Test.java
@@ -22,8 +22,8 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.truth.Truth;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import java.nio.charset.Charset;
 import java.security.GeneralSecurityException;
 import org.junit.Test;
@@ -86,12 +86,10 @@
       try {
         Poly1305.verifyMac(key, in, mac);
       } catch (Throwable e) {
-        String error = String.format(
-            "\n\nIteration: %d\nInput: %s\nKey: %s\nMac: %s\n",
-            i,
-            TestUtil.hexEncode(in),
-            TestUtil.hexEncode(key),
-            TestUtil.hexEncode(mac));
+        String error =
+            String.format(
+                "\n\nIteration: %d\nInput: %s\nKey: %s\nMac: %s\n",
+                i, Hex.encode(in), Hex.encode(key), Hex.encode(mac));
         fail(error + e.getMessage());
       }
     }
@@ -114,12 +112,11 @@
    */
   @Test
   public void testPoly1305() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "85d6be7857556d337f4452fe42d506a8"
-        + "0103808afb0db2fd4abff6af4149f51b");
+    byte[] key =
+        Hex.decode("" + "85d6be7857556d337f4452fe42d506a8" + "0103808afb0db2fd4abff6af4149f51b");
     byte[] in = ("Cryptographic Forum Research Group").getBytes(UTF_8);
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "a8061dc1305136c6c22b8baf0c0127a9"));
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "a8061dc1305136c6c22b8baf0c0127a9"));
   }
 
   /**
@@ -128,16 +125,17 @@
    */
   @Test
   public void testPoly1305TestVector1() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"));
+    byte[] key =
+        Hex.decode("" + "00000000000000000000000000000000" + "00000000000000000000000000000000");
+    byte[] in =
+        Hex.decode(
+            ""
+                + "00000000000000000000000000000000"
+                + "00000000000000000000000000000000"
+                + "00000000000000000000000000000000"
+                + "00000000000000000000000000000000");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "00000000000000000000000000000000"));
   }
 
   /**
@@ -146,9 +144,8 @@
    */
   @Test
   public void testPoly1305TestVector2() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"
-        + "36e5f6b5c5e06070f0efca96227a863e");
+    byte[] key =
+        Hex.decode("" + "00000000000000000000000000000000" + "36e5f6b5c5e06070f0efca96227a863e");
     byte[] in = (
         "Any submission to the IETF intended by the Contributor for publication as all or "
             + "part of an IETF Internet-Draft or RFC and any statement made within the context "
@@ -156,8 +153,8 @@
             + "include oral statements in IETF sessions, as well as written and electronic "
             + "communications made at any time or place, which are addressed to")
         .getBytes(UTF_8);
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "36e5f6b5c5e06070f0efca96227a863e"));
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "36e5f6b5c5e06070f0efca96227a863e"));
   }
 
   /**
@@ -166,9 +163,8 @@
    */
   @Test
   public void testPoly1305TestVector3() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "36e5f6b5c5e06070f0efca96227a863e"
-        + "00000000000000000000000000000000");
+    byte[] key =
+        Hex.decode("" + "36e5f6b5c5e06070f0efca96227a863e" + "00000000000000000000000000000000");
     byte[] in = (
         "Any submission to the IETF intended by the Contributor for publication as all or "
             + "part of an IETF Internet-Draft or RFC and any statement made within the context "
@@ -176,8 +172,8 @@
             + "include oral statements in IETF sessions, as well as written and electronic "
             + "communications made at any time or place, which are addressed to")
         .getBytes(UTF_8);
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "f3477e7cd95417af89a6b8794c310cf0"));
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "f3477e7cd95417af89a6b8794c310cf0"));
   }
 
   /**
@@ -186,20 +182,21 @@
    */
   @Test
   public void testPoly1305TestVector4() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "1c9240a5eb55d38af333888604f6b5f0"
-        + "473917c1402b80099dca5cbc207075c0");
-    byte[] in = TestUtil.hexDecode(""
-        + "2754776173206272696c6c69672c2061"
-        + "6e642074686520736c6974687920746f"
-        + "7665730a446964206779726520616e64"
-        + "2067696d626c6520696e207468652077"
-        + "6162653a0a416c6c206d696d73792077"
-        + "6572652074686520626f726f676f7665"
-        + "732c0a416e6420746865206d6f6d6520"
-        + "7261746873206f757467726162652e");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "4541669a7eaaee61e708dc7cbcc5eb62"));
+    byte[] key =
+        Hex.decode("" + "1c9240a5eb55d38af333888604f6b5f0" + "473917c1402b80099dca5cbc207075c0");
+    byte[] in =
+        Hex.decode(
+            ""
+                + "2754776173206272696c6c69672c2061"
+                + "6e642074686520736c6974687920746f"
+                + "7665730a446964206779726520616e64"
+                + "2067696d626c6520696e207468652077"
+                + "6162653a0a416c6c206d696d73792077"
+                + "6572652074686520626f726f676f7665"
+                + "732c0a416e6420746865206d6f6d6520"
+                + "7261746873206f757467726162652e");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "4541669a7eaaee61e708dc7cbcc5eb62"));
   }
 
   /**
@@ -208,13 +205,11 @@
    */
   @Test
   public void testPoly1305TestVector5() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "ffffffffffffffffffffffffffffffff");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "03000000000000000000000000000000"));
+    byte[] key =
+        Hex.decode("" + "02000000000000000000000000000000" + "00000000000000000000000000000000");
+    byte[] in = Hex.decode("" + "ffffffffffffffffffffffffffffffff");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "03000000000000000000000000000000"));
   }
 
   /**
@@ -223,13 +218,11 @@
    */
   @Test
   public void testPoly1305TestVector6() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000"
-        + "ffffffffffffffffffffffffffffffff");
-    byte[] in = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "03000000000000000000000000000000"));
+    byte[] key =
+        Hex.decode("" + "02000000000000000000000000000000" + "ffffffffffffffffffffffffffffffff");
+    byte[] in = Hex.decode("" + "02000000000000000000000000000000");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "03000000000000000000000000000000"));
   }
 
   /**
@@ -238,15 +231,16 @@
    */
   @Test
   public void testPoly1305TestVector7() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "ffffffffffffffffffffffffffffffff"
-        + "f0ffffffffffffffffffffffffffffff"
-        + "11000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "05000000000000000000000000000000"));
+    byte[] key =
+        Hex.decode("" + "01000000000000000000000000000000" + "00000000000000000000000000000000");
+    byte[] in =
+        Hex.decode(
+            ""
+                + "ffffffffffffffffffffffffffffffff"
+                + "f0ffffffffffffffffffffffffffffff"
+                + "11000000000000000000000000000000");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "05000000000000000000000000000000"));
   }
 
   /**
@@ -255,15 +249,16 @@
    */
   @Test
   public void testPoly1305TestVector8() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "ffffffffffffffffffffffffffffffff"
-        + "fbfefefefefefefefefefefefefefefe"
-        + "01010101010101010101010101010101");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"));
+    byte[] key =
+        Hex.decode("" + "01000000000000000000000000000000" + "00000000000000000000000000000000");
+    byte[] in =
+        Hex.decode(
+            ""
+                + "ffffffffffffffffffffffffffffffff"
+                + "fbfefefefefefefefefefefefefefefe"
+                + "01010101010101010101010101010101");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "00000000000000000000000000000000"));
   }
 
   /**
@@ -272,13 +267,11 @@
    */
   @Test
   public void testPoly1305TestVector9() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "fdffffffffffffffffffffffffffffff");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "faffffffffffffffffffffffffffffff"));
+    byte[] key =
+        Hex.decode("" + "02000000000000000000000000000000" + "00000000000000000000000000000000");
+    byte[] in = Hex.decode("" + "fdffffffffffffffffffffffffffffff");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "faffffffffffffffffffffffffffffff"));
   }
 
   /**
@@ -287,16 +280,17 @@
    */
   @Test
   public void testPoly1305TestVector10() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000400000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "e33594d7505e43b90000000000000000"
-        + "3394d7505e4379cd0100000000000000"
-        + "00000000000000000000000000000000"
-        + "01000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "14000000000000005500000000000000"));
+    byte[] key =
+        Hex.decode("" + "01000000000000000400000000000000" + "00000000000000000000000000000000");
+    byte[] in =
+        Hex.decode(
+            ""
+                + "e33594d7505e43b90000000000000000"
+                + "3394d7505e4379cd0100000000000000"
+                + "00000000000000000000000000000000"
+                + "01000000000000000000000000000000");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "14000000000000005500000000000000"));
   }
 
   /**
@@ -305,14 +299,15 @@
    */
   @Test
   public void testPoly1305TestVector11() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000400000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "e33594d7505e43b90000000000000000"
-        + "3394d7505e4379cd0100000000000000"
-        + "00000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "13000000000000000000000000000000"));
+    byte[] key =
+        Hex.decode("" + "01000000000000000400000000000000" + "00000000000000000000000000000000");
+    byte[] in =
+        Hex.decode(
+            ""
+                + "e33594d7505e43b90000000000000000"
+                + "3394d7505e4379cd0100000000000000"
+                + "00000000000000000000000000000000");
+    Truth.assertThat(Poly1305.computeMac(key, in))
+        .isEqualTo(Hex.decode("" + "13000000000000000000000000000000"));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/subtle/AesGcmSivTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/subtle/AesGcmSivTest.java
index 7c0d798..26c7ff0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/subtle/AesGcmSivTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/subtle/AesGcmSivTest.java
@@ -33,7 +33,6 @@
 import java.security.Security;
 import java.util.Arrays;
 import java.util.HashSet;
-import javax.crypto.Cipher;
 import org.conscrypt.Conscrypt;
 import org.junit.Before;
 import org.junit.Test;
@@ -48,14 +47,7 @@
 
   @Before
   public void setUp() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip tests with keys larger than 128 bits.");
-      keySizeInBytes = new Integer[] {16};
-    } else {
-      keySizeInBytes = new Integer[] {16, 32};
-    }
+    keySizeInBytes = new Integer[] {16, 32};
   }
 
   @Before
@@ -84,7 +76,7 @@
   }
 
   @Test
-  /** BC had a bug, where GCM failed for messages of size > 8192 */
+  /* BC had a bug, where GCM failed for messages of size > 8192 */
   public void testLongMessages() throws Exception {
     if (TestUtil.isAndroid()) {
       System.out.println("testLongMessages doesn't work on Android, skipping");
@@ -273,7 +265,7 @@
   }
 
   @Test
-  /**
+  /*
    * This is a very simple test for the randomness of the nonce. The test simply checks that the
    * multiple ciphertexts of the same message are distinct.
    */
@@ -286,7 +278,7 @@
     HashSet<String> ciphertexts = new HashSet<String>();
     for (int i = 0; i < samples; i++) {
       byte[] ct = gcm.encrypt(message, aad);
-      String ctHex = TestUtil.hexEncode(ct);
+      String ctHex = Hex.encode(ct);
       assertFalse(ciphertexts.contains(ctHex));
       ciphertexts.add(ctHex);
     }
diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/subtle/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/aead/subtle/BUILD.bazel
index 40adae8..dff9519 100644
--- a/java_src/src/test/java/com/google/crypto/tink/aead/subtle/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/aead/subtle/BUILD.bazel
@@ -7,10 +7,7 @@
     name = "AesGcmSivTest",
     srcs = ["AesGcmSivTest.java"],
     data = ["@wycheproof//testvectors:aes_gcm_siv"],
-    tags = [
-        "manual",
-        "notsan",
-    ],
+    tags = ["notsan"],
     deps = [
         "//src/main/java/com/google/crypto/tink/aead/subtle:aes_gcm_siv",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
diff --git a/java_src/src/test/java/com/google/crypto/tink/config/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/config/BUILD.bazel
index bc559af..82d2620 100644
--- a/java_src/src/test/java/com/google/crypto/tink/config/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/config/BUILD.bazel
@@ -25,4 +25,3 @@
         "@maven//:junit_junit",
     ],
 )
-
diff --git a/java_src/src/test/java/com/google/crypto/tink/config/TinkConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/config/TinkConfigTest.java
index 9b9f3af..59236d0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/config/TinkConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/config/TinkConfigTest.java
@@ -17,9 +17,9 @@
 package com.google.crypto.tink.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.Config;
 import com.google.crypto.tink.Registry;
 import java.security.GeneralSecurityException;
 import org.junit.FixMethodOrder;
@@ -37,40 +37,13 @@
 public class TinkConfigTest {
   @Test
   public void aaaTestInitialization() throws Exception {
-    GeneralSecurityException e =
-        assertThrows(GeneralSecurityException.class, () -> Registry.getCatalogue("tinkmac"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("MacConfig.register()");
-    e = assertThrows(GeneralSecurityException.class, () -> Registry.getCatalogue("tinkaead"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("AeadConfig.register()");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkhybriddecrypt"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("HybridConfig.register()");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkhybridencrypt"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("HybridConfig.register()");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkpublickeysign"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("SignatureConfig.register()");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkpublickeyverify"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("SignatureConfig.register()");
     String macTypeUrl = "type.googleapis.com/google.crypto.tink.HmacKey";
     String aeadTypeUrl = "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey";
     String daeadTypeUrl = "type.googleapis.com/google.crypto.tink.AesSivKey";
     String hybridTypeUrl = "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey";
     String signTypeUrl = "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
     String streamingAeadTypeUrl = "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey";
-    e =
+    GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(macTypeUrl));
     assertThat(e.toString()).contains("No key manager found");
@@ -100,12 +73,11 @@
     TinkConfig.register();
 
     // After registration the key managers should be present.
-    Config.register(TinkConfig.TINK_1_1_0);
-    Registry.getUntypedKeyManager(macTypeUrl);
-    Registry.getUntypedKeyManager(aeadTypeUrl);
-    Registry.getUntypedKeyManager(daeadTypeUrl);
-    Registry.getUntypedKeyManager(hybridTypeUrl);
-    Registry.getUntypedKeyManager(signTypeUrl);
-    Registry.getUntypedKeyManager(streamingAeadTypeUrl);
+    assertNotNull(Registry.getUntypedKeyManager(macTypeUrl));
+    assertNotNull(Registry.getUntypedKeyManager(aeadTypeUrl));
+    assertNotNull(Registry.getUntypedKeyManager(daeadTypeUrl));
+    assertNotNull(Registry.getUntypedKeyManager(hybridTypeUrl));
+    assertNotNull(Registry.getUntypedKeyManager(signTypeUrl));
+    assertNotNull(Registry.getUntypedKeyManager(streamingAeadTypeUrl));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/custom/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/custom/BUILD.bazel
new file mode 100644
index 0000000..d3b10fb
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/custom/BUILD.bazel
@@ -0,0 +1,26 @@
+licenses(["notice"])
+
+java_test(
+    name = "CustomAeadKeyManagerTest",
+    size = "small",
+    srcs = ["CustomAeadKeyManagerTest.java"],
+    deps = [
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_manager",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/custom/CustomAeadKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/custom/CustomAeadKeyManagerTest.java
new file mode 100644
index 0000000..3842dc9
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/custom/CustomAeadKeyManagerTest.java
@@ -0,0 +1,262 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.custom;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyManager;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoParametersFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.ChaCha20Poly1305Parameters;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.AesGcmJce;
+import com.google.crypto.tink.subtle.Random;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.StringValue;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** This test creates a custom Aead KeyManager and uses it. */
+@RunWith(JUnit4.class)
+public final class CustomAeadKeyManagerTest {
+
+  /**
+   * A custom implementation of {@link com.google.crypto.tink.KeyManager} for AES GCM 128.
+   *
+   * <p>It only implements the methods of the KeyManager interface that are needed.
+   */
+  static class MyCustomKeyManager implements KeyManager<Aead> {
+
+    private static final String TYPE_URL =
+        "type.googleapis.com/google.crypto.tink.testonly.CustomAeadKey";
+
+    private static final String AEAD_AES_128_GCM = "AEAD_AES_128_GCM";
+
+    @Override
+    public Aead getPrimitive(ByteString serializedKey) throws GeneralSecurityException {
+      try {
+        BytesValue key =
+            BytesValue.parseFrom(serializedKey, ExtensionRegistryLite.getEmptyRegistry());
+        byte[] keyValue = key.getValue().toByteArray();
+        if (keyValue.length != 16) {
+          throw new GeneralSecurityException("unexpected length of keyValue");
+        }
+        return new AesGcmJce(keyValue);
+      } catch (InvalidProtocolBufferException e) {
+        throw new GeneralSecurityException(e);
+      }
+    }
+
+    @Override
+    public String getKeyType() {
+      return TYPE_URL;
+    }
+
+    @Override
+    public Class<Aead> getPrimitiveClass() {
+      return Aead.class;
+    }
+
+    @Override
+    public KeyData newKeyData(ByteString serializedKeyFormat) throws GeneralSecurityException {
+      // serializedKeyFormat is a StringValue proto. The only allowed string is "AEAD_AES_128_GCM".
+      try {
+        StringValue keyFormat =
+            StringValue.parseFrom(serializedKeyFormat, ExtensionRegistryLite.getEmptyRegistry());
+        if (!keyFormat.getValue().equals(AEAD_AES_128_GCM)) {
+          throw new GeneralSecurityException("unknown algorithm");
+        }
+        byte[] rawAesKey = Random.randBytes(16);
+        BytesValue value = BytesValue.of(ByteString.copyFrom(rawAesKey));
+        return KeyData.newBuilder()
+            .setTypeUrl(getKeyType())
+            .setValue(value.toByteString())
+            .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
+            .build();
+      } catch (InvalidProtocolBufferException e) {
+        throw new GeneralSecurityException(e);
+      }
+    }
+
+    static Parameters aesGcm128Parameters() throws GeneralSecurityException {
+      StringValue format = StringValue.of(AEAD_AES_128_GCM);
+      KeyTemplate template =
+          KeyTemplate.newBuilder()
+              .setValue(format.toByteString())
+              .setTypeUrl(TYPE_URL)
+              .setOutputPrefixType(OutputPrefixType.RAW)
+              .build();
+      return TinkProtoParametersFormat.parse(template.toByteArray());
+    }
+
+    static KeysetHandle aesGcm128KeyToKeysetHandle(
+        byte[] rawAesKey, int keyId, OutputPrefixType outputPrefixType)
+        throws GeneralSecurityException {
+      if (rawAesKey.length != 16) {
+        throw new IllegalArgumentException("unexpected raw key length");
+      }
+      BytesValue value = BytesValue.of(ByteString.copyFrom(rawAesKey));
+      Keyset keyset =
+          Keyset.newBuilder()
+              .addKey(
+                  Keyset.Key.newBuilder()
+                      .setStatus(KeyStatusType.ENABLED)
+                      .setOutputPrefixType(outputPrefixType)
+                      .setKeyId(keyId)
+                      .setKeyData(
+                          KeyData.newBuilder()
+                              .setTypeUrl(TYPE_URL)
+                              .setValue(value.toByteString())
+                              .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
+                              .build())
+                      .build())
+              .setPrimaryKeyId(keyId)
+              .build();
+      return CleartextKeysetHandle.fromKeyset(keyset);
+    }
+  }
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+    Registry.registerKeyManager(new MyCustomKeyManager(), /* newKeyAllowed= */ true);
+  }
+
+  @Test
+  public void createEncryptAndDecrypt_success() throws Exception {
+    Parameters aesGcm128Parameters = MyCustomKeyManager.aesGcm128Parameters();
+    KeysetHandle handle = KeysetHandle.generateNew(aesGcm128Parameters);
+    Aead aead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void importExistingKey_decrypts() throws Exception {
+    byte[] rawAesKey = Random.randBytes(16);
+    Aead jceAead = new AesGcmJce(rawAesKey);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = jceAead.encrypt(plaintext, associatedData);
+
+    KeysetHandle handle =
+        MyCustomKeyManager.aesGcm128KeyToKeysetHandle(
+            rawAesKey, /* keyId= */ 0x11223344, OutputPrefixType.RAW);
+    Aead aead = handle.getPrimitive(Aead.class);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void encryptAndDecryptWithTinkPrefix_success() throws Exception {
+    // Create a new key and import it with output prefix type TINK with a fixed key ID.
+    byte[] rawAesKey = Random.randBytes(16);
+    int keyId = 0x11223344;
+    KeysetHandle handle =
+        MyCustomKeyManager.aesGcm128KeyToKeysetHandle(rawAesKey, keyId, OutputPrefixType.TINK);
+
+    Aead aead = handle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+
+    // Check that ciphertext generated using OutputPrefixType.TINK has a 5 byte prefix:
+    // the first byte is always 0x01, and the next 4 bytes are the big-endian encoded key ID.
+    byte[] prefix = Arrays.copyOf(ciphertext, 5);
+    assertThat(prefix)
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44});
+
+    // Check that AesGcmJce can decrypt using the raw key, if the prefix is removed.
+    byte[] ciphertextWithoutPrefix = Arrays.copyOfRange(ciphertext, 5, ciphertext.length);
+    Aead jceAead = new AesGcmJce(rawAesKey);
+    assertThat(jceAead.decrypt(ciphertextWithoutPrefix, associatedData)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void keysetWithCustomAndTinkKeys_decrypts() throws Exception {
+    byte[] rawAesKey = Random.randBytes(16);
+    Aead jceAead = new AesGcmJce(rawAesKey);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = jceAead.encrypt(plaintext, associatedData);
+
+    // Create keyset handle with normal Tink key
+    KeysetHandle handleWithTinkKey =
+        KeysetHandle.generateNew(
+            ChaCha20Poly1305Parameters.create(ChaCha20Poly1305Parameters.Variant.TINK));
+    Aead aead2 = handleWithTinkKey.getPrimitive(Aead.class);
+    byte[] ciphertext2 = aead2.encrypt(plaintext, associatedData);
+
+    KeysetHandle handle =
+        MyCustomKeyManager.aesGcm128KeyToKeysetHandle(
+            rawAesKey, /* keyId= */ 0x11223344, OutputPrefixType.RAW);
+    // Create keyset handle with both the custom key and the normal Tink key
+    KeysetHandle handle2 =
+        KeysetHandle.newBuilder(handle)
+            .addEntry(KeysetHandle.importKey(handleWithTinkKey.getAt(0).getKey()).makePrimary())
+            .build();
+
+    // Decrypt both ciphertexts
+    Aead aead = handle2.getPrimitive(Aead.class);
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+    assertThat(aead.decrypt(ciphertext2, associatedData)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void serializeAndParse_decrypts() throws Exception {
+    Parameters aesGcm128Parameters = MyCustomKeyManager.aesGcm128Parameters();
+    KeysetHandle handle = KeysetHandle.generateNew(aesGcm128Parameters);
+    Aead aead = handle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get());
+
+    KeysetHandle handle2 =
+        TinkProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+    Aead aead2 = handle2.getPrimitive(Aead.class);
+    byte[] decrypted = aead2.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyManagerTest.java
index 01abd3c..b8d6d1e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyManagerTest.java
@@ -25,14 +25,14 @@
 import com.google.crypto.tink.proto.AesSivKey;
 import com.google.crypto.tink.proto.AesSivKeyFormat;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.TreeSet;
-import javax.crypto.Cipher;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -44,6 +44,11 @@
   private final KeyTypeManager.KeyFactory<AesSivKeyFormat, AesSivKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    DeterministicAeadConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(new AesSivKeyManager().getKeyType())
@@ -128,7 +133,7 @@
         new AesSivKeyManager().keyFactory();
     final int numKeys = 1000;
     for (int i = 0; i < numKeys; ++i) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).toByteArray()));
     }
     assertThat(keys).hasSize(numKeys);
   }
@@ -150,6 +155,37 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 64;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    AesSivKey key =
+        new AesSivKeyManager()
+            .keyFactory()
+            .deriveKey(
+                AesSivKeyFormat.newBuilder().setVersion(0).setKeySize(keySize).build(),
+                fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKeyNotEnoughRandomness() throws Exception {
     final int keySize = 64;
     byte[] keyMaterial = Random.randBytes(10);
@@ -179,13 +215,6 @@
 
   @Test
   public void testCiphertextSize() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skipping testCiphertextSize");
-      return;
-    }
-
     DeterministicAead daead =
         new AesSivKeyManager().getPrimitive(createAesSivKey(64), DeterministicAead.class);
     byte[] plaintext = "plaintext".getBytes("UTF-8");
@@ -207,23 +236,23 @@
   @Test
   public void testAes256SivTemplate() throws Exception {
     KeyTemplate template = AesSivKeyManager.aes256SivTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesSivKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    AesSivKeyFormat format =
-        AesSivKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesSivParameters.builder()
+                .setKeySizeBytes(64)
+                .setVariant(AesSivParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes256SivTemplate() throws Exception {
     KeyTemplate template = AesSivKeyManager.rawAes256SivTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesSivKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesSivKeyFormat format =
-        AesSivKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(format.getKeySize());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesSivParameters.builder()
+                .setKeySizeBytes(64)
+                .setVariant(AesSivParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyTest.java
new file mode 100644
index 0000000..f33befc
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivKeyTest.java
@@ -0,0 +1,277 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.aead.AesGcmSivKey;
+import com.google.crypto.tink.aead.AesGcmSivParameters;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesSivKeyTest {
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+
+    AesSivKey key = AesSivKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+
+    AesSivKey key =
+        AesSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+
+    AesSivKey key =
+        AesSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(IllegalArgumentException.class, () -> AesSivKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> AesSivKey.builder().setKeyBytes(SecretBytes.randomBytes(32)).build());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_fails() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> AesSivKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void paramtersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    AesSivParameters parametersWithIdRequirement =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesSivKey.builder()
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .setParameters(parametersWithIdRequirement)
+                .build());
+  }
+
+  @Test
+  public void buildBadKeySize_fails() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesSivKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes32 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes32Copy =
+        SecretBytes.copyFrom(
+            keyBytes32.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes32Diff = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes64 = SecretBytes.randomBytes(64);
+
+    AesSivParameters noPrefixParametersKeySize32 =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+    AesSivParameters tinkPrefixParametersKeySize32 =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.TINK)
+            .build();
+    AesSivParameters crunchyPrefixParametersKeySize32 =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.CRUNCHY)
+            .build();
+    AesSivParameters noPrefixParametersKeySize64 =
+        AesSivParameters.builder()
+            .setKeySizeBytes(64)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes32",
+            AesSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built twice must be equal.
+            AesSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .build(),
+            // The same key built with a copy of key bytes must be equal.
+            AesSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .build(),
+            // Setting id requirement to null is equal to not setting it.
+            AesSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal.
+        .addEqualityGroup(
+            "No prefix, newly generated keyBytes32",
+            AesSivKey.builder()
+                .setParameters(noPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Diff)
+                .build())
+        // This group checks that keys with different key sizes are not equal.
+        .addEqualityGroup(
+            "No prefix, keyBytes64",
+            AesSivKey.builder()
+                .setParameters(noPrefixParametersKeySize64)
+                .setKeyBytes(keyBytes64)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes32",
+            AesSivKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build(),
+            AesSivKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32Copy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal.
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes32",
+            AesSivKey.builder()
+                .setParameters(tinkPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1908)
+                .build())
+        // This groups checks that keys with different output prefix types are not equal.
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes32",
+            AesSivKey.builder()
+                .setParameters(crunchyPrefixParametersKeySize32)
+                .setKeyBytes(keyBytes32)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    AesSivParameters aesSivParameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesGcmSivParameters aesGcmSivParameters =
+        AesGcmSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesGcmSivParameters.Variant.NO_PREFIX)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(32);
+
+    AesSivKey aesSivKey =
+        AesSivKey.builder().setParameters(aesSivParameters).setKeyBytes(keyBytes).build();
+    AesGcmSivKey aesGcmSivKey =
+        AesGcmSivKey.builder().setParameters(aesGcmSivParameters).setKeyBytes(keyBytes).build();
+
+    assertThat(aesSivKey.equalsKey(aesGcmSivKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/AesSivParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivParametersTest.java
new file mode 100644
index 0000000..6ce0181
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivParametersTest.java
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesSivParametersTest {
+  private static final AesSivParameters.Variant NO_PREFIX = AesSivParameters.Variant.NO_PREFIX;
+  private static final AesSivParameters.Variant TINK = AesSivParameters.Variant.TINK;
+  private static final AesSivParameters.Variant CRUNCHY = AesSivParameters.Variant.CRUNCHY;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder().setKeySizeBytes(32).setVariant(NO_PREFIX).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    AesSivParameters parameters = AesSivParameters.builder().setKeySizeBytes(32).build();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesSivParameters.builder().setVariant(NO_PREFIX).build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesSivParameters.builder().setKeySizeBytes(32).setVariant(null).build());
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder().setKeySizeBytes(32).setVariant(TINK).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder().setKeySizeBytes(32).setVariant(CRUNCHY).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithBadKeySizeFails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesSivParameters.builder().setKeySizeBytes(16).setVariant(NO_PREFIX).build());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesSivParameters.builder().setKeySizeBytes(72).setVariant(NO_PREFIX).build());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    AesSivParameters parameters1 =
+        AesSivParameters.builder().setKeySizeBytes(32).setVariant(NO_PREFIX).build();
+    AesSivParameters parameters2 =
+        AesSivParameters.builder().setKeySizeBytes(32).setVariant(NO_PREFIX).build();
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesSivParameters parameters1 =
+        AesSivParameters.builder().setKeySizeBytes(32).setVariant(NO_PREFIX).build();
+    AesSivParameters parameters2 =
+        AesSivParameters.builder().setKeySizeBytes(64).setVariant(NO_PREFIX).build();
+
+    // Different key size
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different variants
+    parameters2 = AesSivParameters.builder().setKeySizeBytes(32).setVariant(TINK).build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 = AesSivParameters.builder().setKeySizeBytes(32).setVariant(CRUNCHY).build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/AesSivProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivProtoSerializationTest.java
new file mode 100644
index 0000000..158c719
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/AesSivProtoSerializationTest.java
@@ -0,0 +1,384 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesSivProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesSivProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesSivKey";
+
+  private static final SecretBytes KEY_BYTES_32 = SecretBytes.randomBytes(32);
+  private static final ByteString KEY_BYTES_32_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_32.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesSivProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesSivProtoSerialization.register(registry);
+    AesSivProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder().setKeySize(32).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesSivKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.TINK)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder().setKeySize(32).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesSivKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.CRUNCHY)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder().setKeySize(32).build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesSivKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseKey_noPrefix() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.NO_PREFIX)
+            .build();
+
+    AesSivKey key = AesSivKey.builder().setParameters(parameters).setKeyBytes(KEY_BYTES_32).build();
+
+    com.google.crypto.tink.proto.AesSivKey protoAesSivKey =
+        com.google.crypto.tink.proto.AesSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            protoAesSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesSivKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.TINK)
+            .build();
+
+    AesSivKey key =
+        AesSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesSivKey protoAesSivKey =
+        com.google.crypto.tink.proto.AesSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            protoAesSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesSivKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.CRUNCHY)
+            .build();
+
+    AesSivKey key =
+        AesSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+
+    com.google.crypto.tink.proto.AesSivKey protoAesSivKey =
+        com.google.crypto.tink.proto.AesSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            protoAesSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesSivKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_legacy() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesSivKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            1479);
+    // Legacy keys are parsed to crunchy
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(((AesSivParameters) parsed.getParameters()).getVariant())
+        .isEqualTo(AesSivParameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.AesSivKey protoAesSivKey =
+        com.google.crypto.tink.proto.AesSivKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesSivKey",
+            protoAesSivKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    AesSivParameters parameters =
+        AesSivParameters.builder()
+            .setKeySizeBytes(32)
+            .setVariant(AesSivParameters.Variant.TINK)
+            .build();
+    AesSivKey key =
+        AesSivKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_32)
+            .setIdRequirement(123)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Bad version number
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder()
+                .setVersion(1)
+                .setKeySize(32)
+                .build()),
+        // Bad key size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder().setKeySize(16).build()),
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.AesSivKeyFormat.newBuilder().setKeySize(32).build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesSivKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesSivKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_32_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Key Length
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesSivKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[8]))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/daead/BUILD.bazel
index 6e50a11..d21a5d8 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/BUILD.bazel
@@ -47,10 +47,12 @@
         "//src/main/java/com/google/crypto/tink:deterministic_aead",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/daead:aes_siv_key_manager",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -63,9 +65,13 @@
     deps = [
         "//proto:aes_siv_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/daead:aes_siv_key_manager",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -101,19 +107,87 @@
 )
 
 java_test(
-    name = "DeterministicAeadIntegrationTest",
+    name = "DaeadTest",
     size = "small",
-    srcs = ["DeterministicAeadIntegrationTest.java"],
+    srcs = ["DaeadTest.java"],
     deps = [
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:crypto_format",
+        "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
-        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesSivParametersTest",
+    size = "small",
+    srcs = ["AesSivParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesSivKeyTest",
+    size = "small",
+    srcs = ["AesSivKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_key",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesSivProtoSerializationTest",
+    size = "small",
+    srcs = ["AesSivProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_siv_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_key",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:aes_siv_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PredefinedDeterministicAeadParametersTest",
+    size = "small",
+    srcs = ["PredefinedDeterministicAeadParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DaeadTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DaeadTest.java
new file mode 100644
index 0000000..8daff3e
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/DaeadTest.java
@@ -0,0 +1,211 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the Daead package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class DaeadTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    DeterministicAeadConfig.register();
+    AeadConfig.register(); // Needed for getPrimitiveFromNonDeterministicAeadKeyset_throws.
+  }
+
+  @Test
+  public void createEncryptDecryptDeterministically()
+      throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("AES256_SIV"));
+    DeterministicAead daead = handle.getPrimitive(DeterministicAead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
+    byte[] decrypted = daead.decryptDeterministically(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    assertThat(daead.encryptDeterministically(plaintext, associatedData)).isEqualTo(ciphertext);
+
+    KeysetHandle otherHandle = KeysetHandle.generateNew(KeyTemplates.get("AES256_SIV"));
+    DeterministicAead otherAead = otherHandle.getPrimitive(DeterministicAead.class);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> otherAead.decryptDeterministically(ciphertext, associatedData));
+
+    byte[] invalid = "invalid".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    assertThrows(
+        GeneralSecurityException.class, () -> daead.decryptDeterministically(ciphertext, invalid));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> daead.decryptDeterministically(invalid, associatedData));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> daead.decryptDeterministically(empty, associatedData));
+    assertThat(
+            daead.decryptDeterministically(
+                daead.encryptDeterministically(empty, associatedData), associatedData))
+        .isEqualTo(empty);
+    assertThat(
+            daead.decryptDeterministically(daead.encryptDeterministically(plaintext, empty), empty))
+        .isEqualTo(plaintext);
+  }
+
+  // A keyset with one AEAD key, serialized in Tink's JSON format.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void readKeyset_EncryptDecryptDeterministically_success()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+
+    DeterministicAead daead = handle.getPrimitive(DeterministicAead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
+    byte[] decrypted = daead.decryptDeterministically(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  // A keyset with multiple keys. The first key is the same as in JSON_AEAD_KEYSET.
+  private static final String JSON_DAEAD_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 385749617,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }, {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"value\": \"EkCGjyLCW8IOilSjFtkBOvpQoOA8ZsCAsFnCawU9ySiii3KefQkY4pGZcdl"
+          + "wJypOZem1/L+wPthYeCo4xmdq68hl\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 385749617,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }, {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"value\": \"EkCCo6EJBokVl3uTcZMA5iCtQArJliOlBBBfjmZ+IHdLGCatgWJ/tsUi2cm"
+          + "pw0o3yXyJaJbyT06kUCEP+GvFIjCQ\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 919668303,"
+          + "      \"outputPrefixType\": \"LEGACY\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void multipleKeysReadKeyset_encryptDecryptDeterministically_success()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_DAEAD_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+
+    DeterministicAead daead = handle.getPrimitive(DeterministicAead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
+    assertThat(daead.decryptDeterministically(ciphertext, associatedData)).isEqualTo(plaintext);
+
+    // Also test that daead can decrypt ciphertexts encrypted with a non-primary key. We use
+    // JSON_DAEAD_KEYSET to encrypt with the first key.
+    KeysetHandle handle1 =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    DeterministicAead daead1 = handle1.getPrimitive(DeterministicAead.class);
+    byte[] ciphertext1 = daead1.encryptDeterministically(plaintext, associatedData);
+    assertThat(daead.decryptDeterministically(ciphertext1, associatedData)).isEqualTo(plaintext);
+  }
+
+  // A keyset with a valid Aead key. This keyset can't be used with the DeterministicAead primitive.
+  private static final String JSON_AEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 42818733,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"GhCC74uJ+2f4qlpaHwR4ylNQ\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 42818733,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonDeterministicAeadKeyset_throws() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_AEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a Aead primitive, but not a DeterministicAead.
+    Object unused = handle.getPrimitive(Aead.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> handle.getPrimitive(DeterministicAead.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadConfigTest.java
index 86d6a83..6040c6f 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadConfigTest.java
@@ -16,6 +16,7 @@
 
 package com.google.crypto.tink.daead;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.DeterministicAead;
@@ -41,8 +42,6 @@
   @Test
   public void aaaTestInitialization() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
-    assertThrows(
-        GeneralSecurityException.class, () -> Registry.getCatalogue("tinkdeterministicaead"));
 
     // Before registration, the key manager should be absent.
     String typeUrl = "type.googleapis.com/google.crypto.tink.AesSivKey";
@@ -52,7 +51,7 @@
     DeterministicAeadConfig.register();
 
     // After registration, the key manager should be present.
-    Registry.getKeyManager(typeUrl, DeterministicAead.class);
+    assertNotNull(Registry.getKeyManager(typeUrl, DeterministicAead.class));
 
     // Running init() manually again should succeed.
     DeterministicAeadConfig.register();
@@ -71,7 +70,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, DeterministicAead.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, DeterministicAead.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryTest.java
index 7dd37bb..5b5a2e9 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryTest.java
@@ -24,7 +24,6 @@
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
 import java.security.GeneralSecurityException;
-import javax.crypto.Cipher;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -43,10 +42,6 @@
   @SuppressWarnings("deprecation") // This is a test that the deprecated function works.
   public void deprecatedDeterministicAeadFactoryGetPrimitive_sameAs_keysetHandleGetPrimitive()
       throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      // skip all tests.
-      return;
-    }
     KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("AES256_SIV"));
 
     DeterministicAead daead = handle.getPrimitive(DeterministicAead.class);
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryWithoutWrapperRegisteredTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryWithoutWrapperRegisteredTest.java
index f6f2446..7fcfcc4 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryWithoutWrapperRegisteredTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadFactoryWithoutWrapperRegisteredTest.java
@@ -22,7 +22,6 @@
 import com.google.crypto.tink.DeterministicAead;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
-import javax.crypto.Cipher;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -40,10 +39,6 @@
   @SuppressWarnings("deprecation") // This is a test that the deprecated function works.
   public void deprecatedFactoryGetPrimitive_whenWrapperHasNotBeenRegistered_works()
       throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      // skip all tests.
-      return;
-    }
     // Only register AesSivKeyManager, but not the DeterministicAeadWrapper.
     AesSivKeyManager.register(/* newKeyAllowed = */ true);
     KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("AES256_SIV"));
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadIntegrationTest.java
deleted file mode 100644
index 98df515..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadIntegrationTest.java
+++ /dev/null
@@ -1,230 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.daead;
-
-import static com.google.crypto.tink.testing.TestUtil.assertExceptionContains;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.CryptoFormat;
-import com.google.crypto.tink.DeterministicAead;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.aead.AeadConfig;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset.Key;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import javax.crypto.Cipher;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests which run the everything for the DeterministicAead primitives. */
-@RunWith(JUnit4.class)
-public class DeterministicAeadIntegrationTest {
-  private Integer[] keySizeInBytes;
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    AeadConfig.register(); // need this for testInvalidKeyMaterial.
-    DeterministicAeadConfig.register();
-  }
-
-  @Before
-  public void setUp2() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip all DeterministicAeadFactory tests");
-      keySizeInBytes = new Integer[] {};
-    } else {
-      keySizeInBytes = new Integer[] {64};
-    }
-  }
-
-  @Test
-  public void testEncrytDecrypt() throws Exception {
-    KeysetHandle keysetHandle = KeysetHandle.generateNew(DeterministicAeadKeyTemplates.AES256_SIV);
-    DeterministicAead aead = keysetHandle.getPrimitive(DeterministicAead.class);
-    byte[] plaintext = Random.randBytes(20);
-    byte[] associatedData = Random.randBytes(20);
-    byte[] ciphertext = aead.encryptDeterministically(plaintext, associatedData);
-    byte[] ciphertext2 = aead.encryptDeterministically(plaintext, associatedData);
-    byte[] decrypted = aead.decryptDeterministically(ciphertext, associatedData);
-    byte[] decrypted2 = aead.decryptDeterministically(ciphertext2, associatedData);
-
-    assertArrayEquals(ciphertext, ciphertext2);
-    assertArrayEquals(plaintext, decrypted);
-    assertArrayEquals(plaintext, decrypted2);
-  }
-
-  @Test
-  public void testMultipleKeys() throws Exception {
-    for (int keySize : keySizeInBytes) {
-      testMultipleKeys(keySize);
-    }
-  }
-
-  private static void testMultipleKeys(int keySize) throws Exception {
-    Key primary =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize),
-            42,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.TINK);
-    Key raw =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize), 43, KeyStatusType.ENABLED, OutputPrefixType.RAW);
-    Key legacy =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize),
-            44,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.LEGACY);
-    Key tink =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize),
-            45,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.TINK);
-    KeysetHandle keysetHandle =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(primary, raw, legacy, tink));
-
-    DeterministicAead daead = keysetHandle.getPrimitive(DeterministicAead.class);
-    byte[] plaintext = Random.randBytes(20);
-    byte[] associatedData = Random.randBytes(20);
-    byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
-    byte[] prefix = Arrays.copyOfRange(ciphertext, 0, CryptoFormat.NON_RAW_PREFIX_SIZE);
-    assertArrayEquals(prefix, CryptoFormat.getOutputPrefix(primary));
-    assertArrayEquals(plaintext, daead.decryptDeterministically(ciphertext, associatedData));
-    assertEquals(CryptoFormat.NON_RAW_PREFIX_SIZE + plaintext.length + 16, ciphertext.length);
-
-    // encrypt with a non-primary RAW key and decrypt with the keyset
-    KeysetHandle keysetHandle2 =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(raw, legacy, tink));
-    DeterministicAead daead2 = keysetHandle2.getPrimitive(DeterministicAead.class);
-    ciphertext = daead2.encryptDeterministically(plaintext, associatedData);
-    assertArrayEquals(plaintext, daead.decryptDeterministically(ciphertext, associatedData));
-
-    // encrypt with a random key not in the keyset, decrypt with the keyset should fail
-    Key random =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize),
-            44,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.TINK);
-    keysetHandle2 = TestUtil.createKeysetHandle(TestUtil.createKeyset(random));
-    daead2 = keysetHandle2.getPrimitive(DeterministicAead.class);
-    ciphertext = daead2.encryptDeterministically(plaintext, associatedData);
-    try {
-      daead.decryptDeterministically(ciphertext, associatedData);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertExceptionContains(e, "decryption failed");
-    }
-  }
-
-  @Test
-  public void testRawKeyAsPrimary() throws Exception {
-    for (int keySize : keySizeInBytes) {
-      testRawKeyAsPrimary(keySize);
-    }
-  }
-
-  private static void testRawKeyAsPrimary(int keySize) throws Exception {
-    Key primary =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize), 42, KeyStatusType.ENABLED, OutputPrefixType.RAW);
-    Key raw =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize), 43, KeyStatusType.ENABLED, OutputPrefixType.RAW);
-    Key legacy =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize),
-            44,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.LEGACY);
-    KeysetHandle keysetHandle =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(primary, raw, legacy));
-
-    DeterministicAead daead = keysetHandle.getPrimitive(DeterministicAead.class);
-    byte[] plaintext = Random.randBytes(20);
-    byte[] associatedData = Random.randBytes(20);
-    byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
-
-    assertArrayEquals(plaintext, daead.decryptDeterministically(ciphertext, associatedData));
-    assertEquals(CryptoFormat.RAW_PREFIX_SIZE + plaintext.length + 16, ciphertext.length);
-  }
-
-  @Test
-  public void testSmallPlaintextWithRawKey() throws Exception {
-    for (int keySize : keySizeInBytes) {
-      testSmallPlaintextWithRawKey(keySize);
-    }
-  }
-
-  private static void testSmallPlaintextWithRawKey(int keySize) throws Exception {
-    Key primary =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(keySize), 42, KeyStatusType.ENABLED, OutputPrefixType.RAW);
-    KeysetHandle keysetHandle = TestUtil.createKeysetHandle(TestUtil.createKeyset(primary));
-
-    DeterministicAead daead = keysetHandle.getPrimitive(DeterministicAead.class);
-    byte[] plaintext = Random.randBytes(1);
-    byte[] associatedData = Random.randBytes(20);
-    byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
-
-    assertArrayEquals(plaintext, daead.decryptDeterministically(ciphertext, associatedData));
-    assertEquals(CryptoFormat.RAW_PREFIX_SIZE + plaintext.length + 16, ciphertext.length);
-  }
-
-  @Test
-  public void testInvalidKeyMaterial() throws Exception {
-    Key valid =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(64), 42, KeyStatusType.ENABLED, OutputPrefixType.TINK);
-    Key invalid =
-        TestUtil.createKey(
-            TestUtil.createAesCtrHmacAeadKeyData(
-                Random.randBytes(16), 12, Random.randBytes(16), 16),
-            43,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-
-    KeysetHandle keysetHandle = TestUtil.createKeysetHandle(TestUtil.createKeyset(valid, invalid));
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class,
-            () -> keysetHandle.getPrimitive(DeterministicAead.class));
-    assertExceptionContains(e, "com.google.crypto.tink.DeterministicAead not supported");
-
-    // invalid as the primary key.
-    KeysetHandle keysetHandle2 = TestUtil.createKeysetHandle(TestUtil.createKeyset(invalid, valid));
-    GeneralSecurityException e2 =
-        assertThrows(
-            GeneralSecurityException.class,
-            () -> keysetHandle2.getPrimitive(DeterministicAead.class));
-    assertExceptionContains(e2, "com.google.crypto.tink.DeterministicAead not supported");
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplatesTest.java
index d8334dc..2962b3b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplatesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadKeyTemplatesTest.java
@@ -16,12 +16,15 @@
 
 package com.google.crypto.tink.daead;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.proto.AesSivKeyFormat;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.protobuf.ExtensionRegistryLite;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -29,6 +32,11 @@
 /** Tests for DeterministicAeadKeyTemplates. */
 @RunWith(JUnit4.class)
 public class DeterministicAeadKeyTemplatesTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    DeterministicAeadConfig.register();
+  }
+
   @Test
   public void aes256Siv() throws Exception {
     KeyTemplate template = DeterministicAeadKeyTemplates.AES256_SIV;
@@ -53,4 +61,11 @@
 
     assertEquals(keySize, format.getKeySize());
   }
+
+  @Test
+  public void testAesSivParametersEqualsKeyTemplate() throws Exception {
+    assertThat(
+            TinkProtoParametersFormat.parse(DeterministicAeadKeyTemplates.AES256_SIV.toByteArray()))
+        .isEqualTo(PredefinedDeterministicAeadParameters.AES256_SIV);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadWrapperTest.java
index bc126e4..1867dca 100644
--- a/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/DeterministicAeadWrapperTest.java
@@ -36,7 +36,6 @@
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
 import java.util.List;
-import javax.crypto.Cipher;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -55,14 +54,7 @@
 
   @Before
   public void setUp2() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip all DeterministicAeadFactory tests");
-      keySizeInBytes = new Integer[] {};
-    } else {
-      keySizeInBytes = new Integer[] {64};
-    }
+    keySizeInBytes = new Integer[] {64};
   }
 
   @Test
@@ -255,8 +247,8 @@
 
     byte[] ciphertext = daead.encryptDeterministically(plaintext, associatedData);
 
-    daead.decryptDeterministically(ciphertext, associatedData);
-    daead.decryptDeterministically(ciphertext2, associatedData);
+    Object unused = daead.decryptDeterministically(ciphertext, associatedData);
+    unused = daead.decryptDeterministically(ciphertext2, associatedData);
     assertThrows(
         GeneralSecurityException.class,
         () -> daead.decryptDeterministically(ciphertext, new byte[0]));
diff --git a/java_src/src/test/java/com/google/crypto/tink/daead/PredefinedDeterministicAeadParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/daead/PredefinedDeterministicAeadParametersTest.java
new file mode 100644
index 0000000..11d7541
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/daead/PredefinedDeterministicAeadParametersTest.java
@@ -0,0 +1,49 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.daead;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class PredefinedDeterministicAeadParametersTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    DeterministicAeadConfig.register();
+  }
+
+  @Test
+  public void testNotNull() {
+    assertThat(PredefinedDeterministicAeadParameters.AES256_SIV).isNotNull();
+  }
+
+  @Test
+  public void testInstantiation()
+      throws Exception {
+    Key key =
+        KeysetHandle.generateNew(PredefinedDeterministicAeadParameters.AES256_SIV)
+            .getAt(0)
+            .getKey();
+    assertThat(key.getParameters()).isEqualTo(PredefinedDeterministicAeadParameters.AES256_SIV);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/hybrid/BUILD.bazel
index be58c70..80bb9f5 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/BUILD.bazel
@@ -22,15 +22,19 @@
     srcs = ["RegistryEciesAeadHkdfDemHelperTest.java"],
     deps = [
         "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:config",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
-        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
         "//src/main/java/com/google/crypto/tink/hybrid:registry_ecies_aead_hkdf_dem_helper",
         "//src/main/java/com/google/crypto/tink/hybrid/subtle:aead_or_daead",
         "//src/main/java/com/google/crypto/tink/signature:signature_key_templates",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:junit_junit",
     ],
 )
@@ -51,6 +55,10 @@
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_decrypt_wrapper",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_encrypt_wrapper",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/internal/testing:fake_monitoring_client",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -73,7 +81,11 @@
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_decrypt_wrapper",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_encrypt_wrapper",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/internal/testing:fake_monitoring_client",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -140,7 +152,7 @@
         "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
         "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_private_key_manager",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:junit_junit",
     ],
 )
@@ -152,11 +164,12 @@
     deps = [
         "//proto:common_java_proto",
         "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:config",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt",
         "//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
-        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_util",
         "//src/main/java/com/google/crypto/tink/hybrid:registry_ecies_aead_hkdf_dem_helper",
@@ -165,6 +178,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:junit_junit",
     ],
 )
@@ -190,10 +204,10 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_dem_helper",
         "//src/main/java/com/google/crypto/tink/subtle:ecies_aead_hkdf_hybrid_encrypt",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -205,11 +219,12 @@
     deps = [
         "//proto:common_java_proto",
         "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:config",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt",
         "//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
-        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_key_templates",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
+        "//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/daead:predefined_deterministic_aead_parameters",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_util",
         "//src/main/java/com/google/crypto/tink/hybrid:registry_ecies_aead_hkdf_dem_helper",
@@ -219,6 +234,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:junit_junit",
     ],
 )
@@ -239,30 +255,103 @@
         "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_public_key_manager",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
 
 java_test(
-    name = "HybridEncryptIntegrationTest",
+    name = "HybridTest",
     size = "small",
-    srcs = ["HybridEncryptIntegrationTest.java"],
+    srcs = ["HybridTest.java"],
     deps = [
-        "//proto:common_java_proto",
-        "//proto:ecies_aead_hkdf_java_proto",
-        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
         "//src/main/java/com/google/crypto/tink:hybrid_decrypt",
         "//src/main/java/com/google/crypto/tink:hybrid_encrypt",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
-        "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_private_key_manager",
-        "//src/main/java/com/google/crypto/tink/hybrid:ecies_aead_hkdf_public_key_manager",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HpkeParametersTest",
+    size = "small",
+    srcs = ["HpkeParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HpkePublicKeyTest",
+    size = "small",
+    srcs = ["HpkePublicKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_public_key",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
+        "//src/main/java/com/google/crypto/tink/subtle:x25519",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HpkePrivateKeyTest",
+    size = "small",
+    srcs = ["HpkePrivateKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_private_key",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_public_key",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
+        "//src/main/java/com/google/crypto/tink/subtle:x25519",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HpkeProtoSerializationTest",
+    size = "small",
+    srcs = ["HpkeProtoSerializationTest.java"],
+    deps = [
+        "//proto:hpke_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_private_key",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_public_key",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridDecryptTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridDecryptTest.java
index 8449516..dda312e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridDecryptTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridDecryptTest.java
@@ -20,11 +20,12 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.Config;
 import com.google.crypto.tink.HybridDecrypt;
 import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.TinkProtoParametersFormat;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.subtle.EciesAeadHkdfHybridDecrypt;
@@ -35,6 +36,7 @@
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.crypto.tink.testing.TestUtil.BytesMutation;
+import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.interfaces.ECPrivateKey;
@@ -50,11 +52,13 @@
 public class EciesAeadHkdfHybridDecryptTest {
   @Before
   public void setUp() throws GeneralSecurityException {
-    Config.register(HybridConfig.TINK_1_0_0);
+    HybridConfig.register();
   }
 
-  private static void testEncryptDecrypt(CurveType curveType, KeyTemplate keyTemplate)
+  private static void testEncryptDecrypt(CurveType curveType, Parameters parameters)
       throws Exception {
+    KeyTemplate keyTemplate =
+        KeyTemplate.parseFrom(TinkProtoParametersFormat.serialize(parameters));
     KeyPair recipientKey = EllipticCurves.generateKeyPair(curveType);
     ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
     ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
@@ -83,51 +87,55 @@
 
   @Test
   public void testEncryptDecryptP256CtrHmac() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P256, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+    testEncryptDecrypt(CurveType.NIST_P256, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP384CtrHmac() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P384, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+    testEncryptDecrypt(CurveType.NIST_P384, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP521CtrHmac() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P521, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+    testEncryptDecrypt(CurveType.NIST_P521, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP256Gcm() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P256, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt(CurveType.NIST_P256, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP384Gcm() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P384, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt(CurveType.NIST_P384, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP512Gcm() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P521, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt(CurveType.NIST_P521, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP256AesSiv() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P256, DeterministicAeadKeyTemplates.AES256_SIV);
+    testEncryptDecrypt(CurveType.NIST_P256, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP384AesSiv() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P384, DeterministicAeadKeyTemplates.AES256_SIV);
+    testEncryptDecrypt(CurveType.NIST_P384, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP512AesSiv() throws Exception {
-    testEncryptDecrypt(CurveType.NIST_P521, DeterministicAeadKeyTemplates.AES256_SIV);
+    testEncryptDecrypt(CurveType.NIST_P521, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   private static void testEncryptDecrypt_mutatedCiphertext_throws(
-      CurveType curveType, KeyTemplate keyTemplate) throws Exception {
+      CurveType curveType, Parameters parameters) throws Exception {
+    KeyTemplate keyTemplate =
+        KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(parameters),
+            ExtensionRegistryLite.getEmptyRegistry());
     KeyPair recipientKey = EllipticCurves.generateKeyPair(curveType);
     ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
     ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
@@ -163,56 +171,63 @@
   @Test
   public void testEncryptDecryptP256CtrHmac_mutatedCiphertext_throws() throws Exception {
     testEncryptDecrypt_mutatedCiphertext_throws(
-        CurveType.NIST_P256, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P256, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP384CtrHmac_mutatedCiphertext_throws() throws Exception {
     testEncryptDecrypt_mutatedCiphertext_throws(
-        CurveType.NIST_P384, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P384, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP521CtrHmac_mutatedCiphertext_throws() throws Exception {
     testEncryptDecrypt_mutatedCiphertext_throws(
-        CurveType.NIST_P521, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P521, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP256Gcm_mutatedCiphertext_throws() throws Exception {
-    testEncryptDecrypt_mutatedCiphertext_throws(CurveType.NIST_P256, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedCiphertext_throws(
+        CurveType.NIST_P256, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP384Gcm_mutatedCiphertext_throws() throws Exception {
-    testEncryptDecrypt_mutatedCiphertext_throws(CurveType.NIST_P384, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedCiphertext_throws(
+        CurveType.NIST_P384, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP512Gcm_mutatedCiphertext_throws() throws Exception {
-    testEncryptDecrypt_mutatedCiphertext_throws(CurveType.NIST_P521, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedCiphertext_throws(
+        CurveType.NIST_P521, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP256AesSiv_mutatedCiphertext_throws() throws Exception {
     testEncryptDecrypt_mutatedCiphertext_throws(
-        CurveType.NIST_P256, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P256, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP384AesSiv_mutatedCiphertext_throws() throws Exception {
     testEncryptDecrypt_mutatedCiphertext_throws(
-        CurveType.NIST_P384, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P384, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP512AesSiv_mutatedCiphertext_throws() throws Exception {
     testEncryptDecrypt_mutatedCiphertext_throws(
-        CurveType.NIST_P521, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P521, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   private static void testEncryptDecrypt_mutatedContext_throws(
-      CurveType curveType, KeyTemplate keyTemplate) throws Exception {
+      CurveType curveType, Parameters parameters) throws Exception {
+    KeyTemplate keyTemplate =
+        KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(parameters),
+            ExtensionRegistryLite.getEmptyRegistry());
     KeyPair recipientKey = EllipticCurves.generateKeyPair(curveType);
     ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
     ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
@@ -248,55 +263,63 @@
   @Test
   public void testEncryptDecryptP256CtrHmac_mutatedContext_throws() throws Exception {
     testEncryptDecrypt_mutatedContext_throws(
-        CurveType.NIST_P256, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P256, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP384CtrHmac_mutatedContext_throws() throws Exception {
     testEncryptDecrypt_mutatedContext_throws(
-        CurveType.NIST_P384, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P384, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP521CtrHmac_mutatedContext_throws() throws Exception {
-    testEncryptDecrypt_mutatedContext_throws(CurveType.NIST_P521, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedContext_throws(
+        CurveType.NIST_P521, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP256Gcm_mutatedContext_throws() throws Exception {
-    testEncryptDecrypt_mutatedContext_throws(CurveType.NIST_P256, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedContext_throws(
+        CurveType.NIST_P256, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP384Gcm_mutatedContext_throws() throws Exception {
-    testEncryptDecrypt_mutatedContext_throws(CurveType.NIST_P384, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedContext_throws(
+        CurveType.NIST_P384, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP512Gcm_mutatedContext_throws() throws Exception {
-    testEncryptDecrypt_mutatedContext_throws(CurveType.NIST_P521, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedContext_throws(
+        CurveType.NIST_P521, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP256AesSiv_mutatedContext_throws() throws Exception {
     testEncryptDecrypt_mutatedContext_throws(
-        CurveType.NIST_P256, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P256, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP384AesSiv_mutatedContext_throws() throws Exception {
     testEncryptDecrypt_mutatedContext_throws(
-        CurveType.NIST_P384, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P384, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP512AesSiv_mutatedContext_throws() throws Exception {
     testEncryptDecrypt_mutatedContext_throws(
-        CurveType.NIST_P521, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P521, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   private static void testEncryptDecrypt_mutatedSalt_throws(
-      CurveType curveType, KeyTemplate keyTemplate) throws Exception {
+      CurveType curveType, Parameters parameters) throws Exception {
+    KeyTemplate keyTemplate =
+        KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(parameters),
+            ExtensionRegistryLite.getEmptyRegistry());
     KeyPair recipientKey = EllipticCurves.generateKeyPair(curveType);
     ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
     ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
@@ -337,52 +360,52 @@
   @Test
   public void testEncryptDecryptP256CtrHmac_mutatedSalt_throws() throws Exception {
     testEncryptDecrypt_mutatedSalt_throws(
-        CurveType.NIST_P256, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P256, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP384CtrHmac_mutatedSalt_throws() throws Exception {
     testEncryptDecrypt_mutatedSalt_throws(
-        CurveType.NIST_P384, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P384, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP521CtrHmac_mutatedSalt_throws() throws Exception {
     testEncryptDecrypt_mutatedSalt_throws(
-        CurveType.NIST_P521, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+        CurveType.NIST_P521, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
   }
 
   @Test
   public void testEncryptDecryptP256Gcm_mutatedSalt_throws() throws Exception {
-    testEncryptDecrypt_mutatedSalt_throws(CurveType.NIST_P256, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedSalt_throws(CurveType.NIST_P256, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP384Gcm_mutatedSalt_throws() throws Exception {
-    testEncryptDecrypt_mutatedSalt_throws(CurveType.NIST_P384, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedSalt_throws(CurveType.NIST_P384, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP512Gcm_mutatedSalt_throws() throws Exception {
-    testEncryptDecrypt_mutatedSalt_throws(CurveType.NIST_P521, AeadKeyTemplates.AES128_GCM);
+    testEncryptDecrypt_mutatedSalt_throws(CurveType.NIST_P521, PredefinedAeadParameters.AES128_GCM);
   }
 
   @Test
   public void testEncryptDecryptP256AesSiv_mutatedSalt_throws() throws Exception {
     testEncryptDecrypt_mutatedSalt_throws(
-        CurveType.NIST_P256, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P256, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP384AesSiv_mutatedSalt_throws() throws Exception {
     testEncryptDecrypt_mutatedSalt_throws(
-        CurveType.NIST_P384, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P384, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   @Test
   public void testEncryptDecryptP512AesSiv_mutatedSalt_throws() throws Exception {
     testEncryptDecrypt_mutatedSalt_throws(
-        CurveType.NIST_P521, DeterministicAeadKeyTemplates.AES256_SIV);
+        CurveType.NIST_P521, PredefinedDeterministicAeadParameters.AES256_SIV);
   }
 
   /** Test vector for hybrid decryption. */
@@ -422,19 +445,23 @@
   };
 
   @Test
-  public void decryptWithTestVectors() throws GeneralSecurityException {
+  public void decryptWithTestVectors() throws Exception {
     for (HybridDecryptionTestVector vector : aesSivDemHybridTestVectors) {
       CurveType curveType = CurveType.NIST_P256;
       byte[] salt = new byte[0];
 
       ECPrivateKey recipientPrivateKey = EllipticCurves.getEcPrivateKey(curveType, vector.key);
+      KeyTemplate template =
+          KeyTemplate.parseFrom(
+              TinkProtoParametersFormat.serialize(PredefinedDeterministicAeadParameters.AES256_SIV),
+              ExtensionRegistryLite.getEmptyRegistry());
       HybridDecrypt hybridDecrypt =
           new EciesAeadHkdfHybridDecrypt(
               recipientPrivateKey,
               salt,
               HybridUtil.toHmacAlgo(HashType.SHA256),
               EllipticCurves.PointFormatType.UNCOMPRESSED,
-              new RegistryEciesAeadHkdfDemHelper(DeterministicAeadKeyTemplates.AES256_SIV));
+              new RegistryEciesAeadHkdfDemHelper(template));
 
       byte[] decrypted = hybridDecrypt.decrypt(vector.ciphertext, vector.context);
       assertArrayEquals(vector.plaintext, decrypted);
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridEncryptTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridEncryptTest.java
index f091914..5caf6d7 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridEncryptTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfHybridEncryptTest.java
@@ -19,11 +19,12 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 
-import com.google.crypto.tink.Config;
 import com.google.crypto.tink.HybridDecrypt;
 import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.TinkProtoParametersFormat;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.subtle.EciesAeadHkdfHybridDecrypt;
@@ -32,6 +33,7 @@
 import com.google.crypto.tink.subtle.EllipticCurves.CurveType;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
+import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.interfaces.ECPrivateKey;
@@ -52,11 +54,15 @@
 public class EciesAeadHkdfHybridEncryptTest {
   @Before
   public void setUp() throws GeneralSecurityException {
-    Config.register(HybridConfig.TINK_1_0_0);
+    HybridConfig.register();
   }
 
-  private void testBasicMultipleEncrypts(CurveType curveType, KeyTemplate keyTemplate)
+  private void testBasicMultipleEncrypts(CurveType curveType, Parameters parameters)
       throws Exception {
+    KeyTemplate keyTemplate =
+        KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(parameters),
+            ExtensionRegistryLite.getEmptyRegistry());
     KeyPair recipientKey = EllipticCurves.generateKeyPair(curveType);
     ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic();
     ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate();
@@ -95,16 +101,17 @@
 
   @Test
   public void testBasicMultipleEncrypts() throws Exception {
-    testBasicMultipleEncrypts(CurveType.NIST_P256, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
-    testBasicMultipleEncrypts(CurveType.NIST_P384, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
-    testBasicMultipleEncrypts(CurveType.NIST_P521, AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+    testBasicMultipleEncrypts(CurveType.NIST_P256, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
+    testBasicMultipleEncrypts(CurveType.NIST_P384, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
+    testBasicMultipleEncrypts(CurveType.NIST_P521, PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
 
-    testBasicMultipleEncrypts(CurveType.NIST_P521, DeterministicAeadKeyTemplates.AES256_SIV);
+    testBasicMultipleEncrypts(
+        CurveType.NIST_P521, PredefinedDeterministicAeadParameters.AES256_SIV);
 
     if (!TestUtil.isAndroid()) {
-      testBasicMultipleEncrypts(CurveType.NIST_P256, AeadKeyTemplates.AES128_GCM);
-      testBasicMultipleEncrypts(CurveType.NIST_P384, AeadKeyTemplates.AES128_GCM);
-      testBasicMultipleEncrypts(CurveType.NIST_P521, AeadKeyTemplates.AES128_GCM);
+      testBasicMultipleEncrypts(CurveType.NIST_P256, PredefinedAeadParameters.AES128_GCM);
+      testBasicMultipleEncrypts(CurveType.NIST_P384, PredefinedAeadParameters.AES128_GCM);
+      testBasicMultipleEncrypts(CurveType.NIST_P521, PredefinedAeadParameters.AES128_GCM);
     }
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManagerTest.java
index 6654a2c..e05c5f1 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/EciesAeadHkdfPrivateKeyManagerTest.java
@@ -40,8 +40,8 @@
 import com.google.crypto.tink.subtle.EciesAeadHkdfDemHelper;
 import com.google.crypto.tink.subtle.EciesAeadHkdfHybridEncrypt;
 import com.google.crypto.tink.subtle.EllipticCurves;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.security.interfaces.ECPublicKey;
@@ -99,7 +99,7 @@
             HashType.SHA256,
             EcPointFormat.UNCOMPRESSED,
             AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template(),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     factory.validateKeyFormat(format);
   }
 
@@ -111,7 +111,7 @@
             HashType.SHA256,
             EcPointFormat.UNKNOWN_FORMAT,
             AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template(),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     assertThrows(GeneralSecurityException.class, () -> factory.validateKeyFormat(format));
   }
 
@@ -123,7 +123,7 @@
             HashType.SHA256,
             EcPointFormat.UNCOMPRESSED,
             KeyTemplate.create("", new byte[0], KeyTemplate.OutputPrefixType.TINK),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     assertThrows(GeneralSecurityException.class, () -> factory.validateKeyFormat(format));
   }
 
@@ -135,7 +135,7 @@
             HashType.SHA256,
             EcPointFormat.UNCOMPRESSED,
             AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template(),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     assertThrows(GeneralSecurityException.class, () -> factory.validateKeyFormat(format));
   }
 
@@ -147,7 +147,7 @@
             HashType.UNKNOWN_HASH,
             EcPointFormat.UNCOMPRESSED,
             AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template(),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     assertThrows(GeneralSecurityException.class, () -> factory.validateKeyFormat(format));
   }
 
@@ -159,7 +159,7 @@
             HashType.SHA256,
             EcPointFormat.UNCOMPRESSED,
             AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template(),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     EciesAeadHkdfPrivateKey key = factory.createKey(format);
     assertThat(key.getPublicKey().getParams()).isEqualTo(format.getParams());
     assertThat(key.getPublicKey().getX()).isNotEmpty();
@@ -174,7 +174,7 @@
             HashType.SHA256,
             EcPointFormat.UNCOMPRESSED,
             AesCtrHmacAeadKeyManager.aes128CtrHmacSha256Template(),
-            TestUtil.hexDecode("aabbccddeeff"));
+            Hex.decode("aabbccddeeff"));
     return factory.createKey(format);
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkeParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkeParametersTest.java
new file mode 100644
index 0000000..b76cd6a
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkeParametersTest.java
@@ -0,0 +1,271 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HpkeParametersTest {
+
+  @DataPoints("variants")
+  public static final HpkeParameters.Variant[] VARIANTS =
+      new HpkeParameters.Variant[] {
+        HpkeParameters.Variant.TINK,
+        HpkeParameters.Variant.CRUNCHY,
+        HpkeParameters.Variant.NO_PREFIX,
+      };
+
+  @DataPoints("kemIds")
+  public static final HpkeParameters.KemId[] KEM_IDS =
+      new HpkeParameters.KemId[] {
+        HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256,
+        HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384,
+        HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512,
+        HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256,
+      };
+
+  @DataPoints("kdfIds")
+  public static final HpkeParameters.KdfId[] KDF_IDS =
+      new HpkeParameters.KdfId[] {
+        HpkeParameters.KdfId.HKDF_SHA256,
+        HpkeParameters.KdfId.HKDF_SHA384,
+        HpkeParameters.KdfId.HKDF_SHA512,
+      };
+
+  @DataPoints("aeadIds")
+  public static final HpkeParameters.AeadId[] AEAD_IDS =
+      new HpkeParameters.AeadId[] {
+        HpkeParameters.AeadId.AES_128_GCM,
+        HpkeParameters.AeadId.AES_256_GCM,
+        HpkeParameters.AeadId.CHACHA20_POLY1305,
+      };
+
+  @Theory
+  public void buildParameters(
+      @FromDataPoints("variants") HpkeParameters.Variant variant,
+      @FromDataPoints("kemIds") HpkeParameters.KemId kemId,
+      @FromDataPoints("kdfIds") HpkeParameters.KdfId kdfId,
+      @FromDataPoints("aeadIds") HpkeParameters.AeadId aeadId)
+      throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(variant)
+            .setKemId(kemId)
+            .setKdfId(kdfId)
+            .setAeadId(aeadId)
+            .build();
+
+    assertThat(params.getVariant()).isEqualTo(variant);
+    assertThat(params.getKemId()).isEqualTo(kemId);
+    assertThat(params.getKdfId()).isEqualTo(kdfId);
+    assertThat(params.getAeadId()).isEqualTo(aeadId);
+  }
+
+  @Theory
+  public void buildParametersWithDefaultVariant(
+      @FromDataPoints("kemIds") HpkeParameters.KemId kemId,
+      @FromDataPoints("kdfIds") HpkeParameters.KdfId kdfId,
+      @FromDataPoints("aeadIds") HpkeParameters.AeadId aeadId)
+      throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder().setKemId(kemId).setKdfId(kdfId).setAeadId(aeadId).build();
+
+    assertThat(params.getVariant()).isEqualTo(HpkeParameters.Variant.NO_PREFIX);
+    assertThat(params.getKemId()).isEqualTo(kemId);
+    assertThat(params.getKdfId()).isEqualTo(kdfId);
+    assertThat(params.getAeadId()).isEqualTo(aeadId);
+  }
+
+  @Test
+  public void buildParameters_failsWithoutKemId() throws Exception {
+    HpkeParameters.Builder builder =
+        HpkeParameters.builder()
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM);
+
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_failsWithoutKdfId() throws Exception {
+    HpkeParameters.Builder builder =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM);
+
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_failsWithoutAeadId() throws Exception {
+    HpkeParameters.Builder builder =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256);
+
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void hasIdRequirement() throws Exception {
+    HpkeParameters noPrefixParams =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    assertThat(noPrefixParams.hasIdRequirement()).isFalse();
+
+    HpkeParameters tinkParams =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.TINK)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    assertThat(tinkParams.hasIdRequirement()).isTrue();
+
+    HpkeParameters crunchyParams =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.CRUNCHY)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    assertThat(crunchyParams.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void sameParamsAreEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    HpkeParameters duplicateParams =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    assertThat(params).isEqualTo(duplicateParams);
+    assertThat(params.hashCode()).isEqualTo(duplicateParams.hashCode());
+  }
+
+  @Test
+  public void differentVariantsAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    HpkeParameters differentVariant =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.TINK)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    assertThat(params).isNotEqualTo(differentVariant);
+    assertThat(params.hashCode()).isNotEqualTo(differentVariant.hashCode());
+  }
+
+  @Test
+  public void differentKemAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    HpkeParameters differentKem =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    assertThat(params).isNotEqualTo(differentKem);
+    assertThat(params.hashCode()).isNotEqualTo(differentKem.hashCode());
+  }
+
+  @Test
+  public void differentKdfsAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    HpkeParameters differentKdf =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA384)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    assertThat(params).isNotEqualTo(differentKdf);
+    assertThat(params.hashCode()).isNotEqualTo(differentKdf.hashCode());
+  }
+
+  @Test
+  public void differentAeadsAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+
+    HpkeParameters differentAead =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_256_GCM)
+            .build();
+
+    assertThat(params).isNotEqualTo(differentAead);
+    assertThat(params.hashCode()).isNotEqualTo(differentAead.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkePrivateKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkePrivateKeyTest.java
new file mode 100644
index 0000000..1418d11
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkePrivateKeyTest.java
@@ -0,0 +1,303 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.subtle.EllipticCurves;
+import com.google.crypto.tink.subtle.EllipticCurves.PointFormatType;
+import com.google.crypto.tink.subtle.X25519;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HpkePrivateKeyTest {
+  private static final class NistKemTuple {
+    final HpkeParameters.KemId kemId;
+    final EllipticCurves.CurveType curve;
+    final int privateKeyLength;
+
+    NistKemTuple(HpkeParameters.KemId kemId, EllipticCurves.CurveType curve, int privateKeyLength) {
+      this.kemId = kemId;
+      this.curve = curve;
+      this.privateKeyLength = privateKeyLength;
+    }
+  }
+
+  @DataPoints("nistKemTuples")
+  public static final NistKemTuple[] KEMS =
+      new NistKemTuple[] {
+        new NistKemTuple(
+            HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256, EllipticCurves.CurveType.NIST_P256, 32),
+        new NistKemTuple(
+            HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384, EllipticCurves.CurveType.NIST_P384, 48),
+        new NistKemTuple(
+            HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512, EllipticCurves.CurveType.NIST_P521, 66),
+      };
+
+  @Theory
+  public void createNistCurvePrivateKey(@FromDataPoints("nistKemTuples") NistKemTuple tuple)
+      throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(tuple.kemId)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    KeyPair keyPair = EllipticCurves.generateKeyPair(tuple.curve);
+    ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
+    ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();
+    Bytes publicKeyBytes =
+        Bytes.copyFrom(
+            EllipticCurves.pointEncode(
+                tuple.curve, PointFormatType.UNCOMPRESSED, ecPublicKey.getW()));
+    byte[] privateKeyBytes =
+        BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+            ecPrivateKey.getS(), tuple.privateKeyLength);
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    HpkePrivateKey privateKey =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+
+    assertThat(privateKey.getParameters()).isEqualTo(params);
+    assertThat(privateKey.getPublicKey().equalsKey(publicKey)).isTrue();
+    assertThat(privateKey.getPrivateKeyBytes().toByteArray(InsecureSecretKeyAccess.get()))
+        .isEqualTo(privateKeyBytes);
+  }
+
+  @Test
+  public void createX25519PrivateKey() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    byte[] privateKeyBytes = X25519.generatePrivateKey();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(privateKeyBytes));
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    HpkePrivateKey privateKey =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+
+    assertThat(privateKey.getParameters()).isEqualTo(params);
+    assertThat(privateKey.getPublicKey().equalsKey(publicKey)).isTrue();
+    assertThat(privateKey.getPrivateKeyBytes().toByteArray(InsecureSecretKeyAccess.get()))
+        .isEqualTo(privateKeyBytes);
+  }
+
+  @Theory
+  public void createNistCurvePrivateKey_failsWithWrongKeyLength(
+      @FromDataPoints("nistKemTuples") NistKemTuple tuple) throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(tuple.kemId)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    KeyPair keyPair = EllipticCurves.generateKeyPair(tuple.curve);
+    ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
+    ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();
+    Bytes publicKeyBytes =
+        Bytes.copyFrom(
+            EllipticCurves.pointEncode(
+                tuple.curve, PointFormatType.UNCOMPRESSED, ecPublicKey.getW()));
+    byte[] privateKeyBytes =
+        BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+            ecPrivateKey.getS(), tuple.privateKeyLength);
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    byte[] tooShortBytes = Arrays.copyOf(privateKeyBytes, privateKeyBytes.length - 1);
+    SecretBytes tooShort = SecretBytes.copyFrom(tooShortBytes, InsecureSecretKeyAccess.get());
+    byte[] tooLongBytes = Arrays.copyOf(privateKeyBytes, privateKeyBytes.length + 1);
+    SecretBytes tooLong = SecretBytes.copyFrom(tooLongBytes, InsecureSecretKeyAccess.get());
+
+    assertThrows(GeneralSecurityException.class, () -> HpkePrivateKey.create(publicKey, tooShort));
+    assertThrows(GeneralSecurityException.class, () -> HpkePrivateKey.create(publicKey, tooLong));
+  }
+
+  @Theory
+  public void createNistCurvePrivateKey_failsWithMismatchedPublicKey(
+      @FromDataPoints("nistKemTuples") NistKemTuple tuple) throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(tuple.kemId)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    ECPublicKey ecPublicKey = (ECPublicKey) EllipticCurves.generateKeyPair(tuple.curve).getPublic();
+    ECPrivateKey ecPrivateKey =
+        (ECPrivateKey) EllipticCurves.generateKeyPair(tuple.curve).getPrivate();
+    Bytes publicKeyBytes =
+        Bytes.copyFrom(
+            EllipticCurves.pointEncode(
+                tuple.curve, PointFormatType.UNCOMPRESSED, ecPublicKey.getW()));
+    byte[] privateKeyBytes =
+        BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+            ecPrivateKey.getS(), tuple.privateKeyLength);
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HpkePrivateKey.create(
+                publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get())));
+  }
+
+  @Test
+  public void createX25519PrivateKey_failsWithWrongKeyLength() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    byte[] privateKeyBytes = X25519.generatePrivateKey();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(privateKeyBytes));
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    byte[] tooShortBytes = Arrays.copyOf(privateKeyBytes, privateKeyBytes.length - 1);
+    SecretBytes tooShort = SecretBytes.copyFrom(tooShortBytes, InsecureSecretKeyAccess.get());
+    byte[] tooLongBytes = Arrays.copyOf(privateKeyBytes, privateKeyBytes.length + 1);
+    SecretBytes tooLong = SecretBytes.copyFrom(tooLongBytes, InsecureSecretKeyAccess.get());
+
+    assertThrows(GeneralSecurityException.class, () -> HpkePrivateKey.create(publicKey, tooShort));
+    assertThrows(GeneralSecurityException.class, () -> HpkePrivateKey.create(publicKey, tooLong));
+  }
+
+  @Test
+  public void createX25519PrivateKey_failsWithMismatchedPublicKey() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    byte[] privateKeyBytes = X25519.generatePrivateKey();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HpkePrivateKey.create(
+                publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get())));
+  }
+
+  @Test
+  public void sameKeysAreEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    byte[] privateKeyBytes = X25519.generatePrivateKey();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(privateKeyBytes));
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    HpkePrivateKey privateKey1 =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+    HpkePrivateKey privateKey2 =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+
+    assertThat(privateKey1.equalsKey(privateKey2)).isTrue();
+  }
+
+  @Test
+  public void differentPublicKeyParamsAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.TINK)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    byte[] privateKeyBytes = X25519.generatePrivateKey();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(privateKeyBytes));
+    HpkePublicKey publicKey1 =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ 123);
+    HpkePublicKey publicKey2 =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ 456);
+
+    HpkePrivateKey privateKey1 =
+        HpkePrivateKey.create(
+            publicKey1, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+    HpkePrivateKey privateKey2 =
+        HpkePrivateKey.create(
+            publicKey2, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+
+    assertThat(privateKey1.getParameters()).isEqualTo(privateKey2.getParameters());
+    assertThat(privateKey1.getPrivateKeyBytes().equalsSecretBytes(privateKey2.getPrivateKeyBytes()))
+        .isTrue();
+    assertThat(privateKey1.getPublicKey()).isNotEqualTo(privateKey2.getPublicKey());
+    assertThat(privateKey1.equalsKey(privateKey2)).isFalse();
+  }
+
+  @Test
+  public void differentKeyTypesAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.TINK)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    byte[] privateKeyBytes = X25519.generatePrivateKey();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(privateKeyBytes));
+
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ 123);
+    HpkePrivateKey privateKey =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(privateKeyBytes, InsecureSecretKeyAccess.get()));
+
+    assertThat(publicKey.equalsKey(privateKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkeProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkeProtoSerializationTest.java
new file mode 100644
index 0000000..82ea300
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkeProtoSerializationTest.java
@@ -0,0 +1,594 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HpkeAead;
+import com.google.crypto.tink.proto.HpkeKdf;
+import com.google.crypto.tink.proto.HpkeKem;
+import com.google.crypto.tink.proto.HpkeKeyFormat;
+import com.google.crypto.tink.proto.HpkeParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link HpkeProtoSerialization}. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class HpkeProtoSerializationTest {
+  private static final class VariantTuple {
+    final HpkeParameters.Variant variant;
+    final OutputPrefixType outputPrefixType;
+    @Nullable final Integer idRequirement;
+
+    VariantTuple(
+        HpkeParameters.Variant variant,
+        OutputPrefixType outputPrefixType,
+        @Nullable Integer idRequirement) {
+      this.variant = variant;
+      this.outputPrefixType = outputPrefixType;
+      this.idRequirement = idRequirement;
+    }
+  }
+
+  private static final class KemTuple {
+    final HpkeParameters.KemId kemId;
+    final HpkeKem kemProto;
+    final byte[] publicKey;
+    final byte[] privateKey;
+
+    KemTuple(HpkeParameters.KemId kemId, HpkeKem kemProto, byte[] publicKey, byte[] privateKey) {
+      this.kemId = kemId;
+      this.kemProto = kemProto;
+      this.publicKey = publicKey;
+      this.privateKey = privateKey;
+    }
+  }
+
+  private static final class KdfTuple {
+    final HpkeParameters.KdfId kdfId;
+    final HpkeKdf kdfProto;
+
+    KdfTuple(HpkeParameters.KdfId kdfId, HpkeKdf kdfProto) {
+      this.kdfId = kdfId;
+      this.kdfProto = kdfProto;
+    }
+  }
+
+  private static final class AeadTuple {
+    final HpkeParameters.AeadId aeadId;
+    final HpkeAead aeadProto;
+
+    AeadTuple(HpkeParameters.AeadId aeadId, HpkeAead aeadProto) {
+      this.aeadId = aeadId;
+      this.aeadProto = aeadProto;
+    }
+  }
+
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.HpkePublicKey";
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.HpkePrivateKey";
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @DataPoints("variants")
+  public static final VariantTuple[] VARIANTS =
+      new VariantTuple[] {
+        new VariantTuple(
+            HpkeParameters.Variant.NO_PREFIX, OutputPrefixType.RAW, /* idRequirement= */ null),
+        new VariantTuple(
+            HpkeParameters.Variant.TINK, OutputPrefixType.TINK, /* idRequirement= */ 123),
+        new VariantTuple(
+            HpkeParameters.Variant.CRUNCHY, OutputPrefixType.CRUNCHY, /* idRequirement= */ 456),
+      };
+
+  @DataPoints("kems")
+  public static final KemTuple[] KEMS =
+      new KemTuple[] {
+        new KemTuple(
+            // Ephemeral key pair from https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1.
+            HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256,
+            HpkeKem.DHKEM_X25519_HKDF_SHA256,
+            /* publicKey= */ Hex.decode(
+                "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431"),
+            /* privateKey= */ Hex.decode(
+                "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736")),
+        new KemTuple(
+            // Ephemeral key pair from https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.3.1.
+            HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256,
+            HpkeKem.DHKEM_P256_HKDF_SHA256,
+            /* publicKey= */ Hex.decode(
+                "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b32"
+                    + "5ac98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4"),
+            /* privateKey= */ Hex.decode(
+                "4995788ef4b9d6132b249ce59a77281493eb39af373d236a1fe415cb0c2d7beb")),
+        new KemTuple(
+            // Ephemeral key pair from  https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.6.1.
+            HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512,
+            HpkeKem.DHKEM_P521_HKDF_SHA512,
+            /* publicKey= */ Hex.decode(
+                "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab8"
+                    + "900aaeed46ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731"
+                    + "ece2013dc3fb7c8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed0"
+                    + "692237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0"),
+            /* privateKey= */ Hex.decode(
+                "014784c692da35df6ecde98ee43ac425dbdd0969c0c72b42f2e708ab9d5354"
+                    + "15a8569bdacfcc0a114c85b8e3f26acf4d68115f8c91a66178cdbd03b7bcc5291e37"
+                    + "4b")),
+      };
+
+  @DataPoints("kdfs")
+  public static final KdfTuple[] KDFS =
+      new KdfTuple[] {
+        new KdfTuple(HpkeParameters.KdfId.HKDF_SHA256, HpkeKdf.HKDF_SHA256),
+        new KdfTuple(HpkeParameters.KdfId.HKDF_SHA384, HpkeKdf.HKDF_SHA384),
+        new KdfTuple(HpkeParameters.KdfId.HKDF_SHA512, HpkeKdf.HKDF_SHA512),
+      };
+
+  @DataPoints("aeads")
+  public static final AeadTuple[] AEADS =
+      new AeadTuple[] {
+        new AeadTuple(HpkeParameters.AeadId.AES_128_GCM, HpkeAead.AES_128_GCM),
+        new AeadTuple(HpkeParameters.AeadId.AES_256_GCM, HpkeAead.AES_256_GCM),
+        new AeadTuple(HpkeParameters.AeadId.CHACHA20_POLY1305, HpkeAead.CHACHA20_POLY1305),
+      };
+
+  private static final HpkeParams createHpkeProtoParams(HpkeKem kem, HpkeKdf kdf, HpkeAead aead) {
+    return HpkeParams.newBuilder().setKem(kem).setKdf(kdf).setAead(aead).build();
+  }
+
+  private static final com.google.crypto.tink.proto.HpkePublicKey createHpkeProtoPublicKey(
+      int version, HpkeParams params, byte[] publicKey) {
+    return com.google.crypto.tink.proto.HpkePublicKey.newBuilder()
+        .setVersion(version)
+        .setParams(params)
+        .setPublicKey(ByteString.copyFrom(publicKey))
+        .build();
+  }
+
+  private static final com.google.crypto.tink.proto.HpkePrivateKey createHpkeProtoPrivateKey(
+      int version, com.google.crypto.tink.proto.HpkePublicKey publicKey, byte[] privateKey) {
+    return com.google.crypto.tink.proto.HpkePrivateKey.newBuilder()
+        .setVersion(version)
+        .setPublicKey(publicKey)
+        .setPrivateKey(ByteString.copyFrom(privateKey))
+        .build();
+  }
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    HpkeProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void register_calledTwice_succeedsAndSecondCallHasNoEffect() throws Exception {
+    HpkeParameters parameters =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .build();
+
+    HpkeParams protoParams =
+        createHpkeProtoParams(
+            HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
+    HpkeKeyFormat format = HpkeKeyFormat.newBuilder().setParams(protoParams).build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HpkePrivateKey", OutputPrefixType.RAW, format);
+
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    assertThat(registry.hasParserForParameters(serialization)).isFalse();
+    assertThat(registry.hasSerializerForParameters(parameters, ProtoParametersSerialization.class))
+        .isFalse();
+
+    HpkeProtoSerialization.register(registry);
+
+    assertThat(registry.hasParserForParameters(serialization)).isTrue();
+    assertThat(registry.hasSerializerForParameters(parameters, ProtoParametersSerialization.class))
+        .isTrue();
+
+    HpkeProtoSerialization.register(registry);
+
+    assertThat(registry.hasParserForParameters(serialization)).isTrue();
+    assertThat(registry.hasSerializerForParameters(parameters, ProtoParametersSerialization.class))
+        .isTrue();
+  }
+
+  @Theory
+  public void serializeParseParameters(
+      @FromDataPoints("variants") VariantTuple variantTuple,
+      @FromDataPoints("kems") KemTuple kemTuple,
+      @FromDataPoints("kdfs") KdfTuple kdfTuple,
+      @FromDataPoints("aeads") AeadTuple aeadTuple)
+      throws Exception {
+    HpkeParameters parameters =
+        HpkeParameters.builder()
+            .setKemId(kemTuple.kemId)
+            .setKdfId(kdfTuple.kdfId)
+            .setAeadId(aeadTuple.aeadId)
+            .setVariant(variantTuple.variant)
+            .build();
+
+    HpkeParams protoParams =
+        createHpkeProtoParams(kemTuple.kemProto, kdfTuple.kdfProto, aeadTuple.aeadProto);
+    HpkeKeyFormat format = HpkeKeyFormat.newBuilder().setParams(protoParams).build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HpkePrivateKey",
+            variantTuple.outputPrefixType,
+            format);
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(HpkeKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Theory
+  public void serializeParsePublicKey(
+      @FromDataPoints("variants") VariantTuple variantTuple,
+      @FromDataPoints("kems") KemTuple kemTuple,
+      @FromDataPoints("kdfs") KdfTuple kdfTuple,
+      @FromDataPoints("aeads") AeadTuple aeadTuple)
+      throws Exception {
+    HpkeParameters parameters =
+        HpkeParameters.builder()
+            .setKemId(kemTuple.kemId)
+            .setKdfId(kdfTuple.kdfId)
+            .setAeadId(aeadTuple.aeadId)
+            .setVariant(variantTuple.variant)
+            .build();
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(
+            parameters, Bytes.copyFrom(kemTuple.publicKey), variantTuple.idRequirement);
+
+    HpkeParams protoParams =
+        createHpkeProtoParams(kemTuple.kemProto, kdfTuple.kdfProto, aeadTuple.aeadProto);
+    com.google.crypto.tink.proto.HpkePublicKey protoPublicKey =
+        createHpkeProtoPublicKey(/* version= */ 0, protoParams, kemTuple.publicKey);
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HpkePublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            variantTuple.outputPrefixType,
+            variantTuple.idRequirement);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(publicKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(publicKey, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HpkePublicKey.parser(), serialized, serialization);
+  }
+
+  @Theory
+  public void serializeParsePrivateKey(
+      @FromDataPoints("variants") VariantTuple variantTuple,
+      @FromDataPoints("kems") KemTuple kemTuple,
+      @FromDataPoints("kdfs") KdfTuple kdfTuple,
+      @FromDataPoints("aeads") AeadTuple aeadTuple)
+      throws Exception {
+    HpkeParameters parameters =
+        HpkeParameters.builder()
+            .setKemId(kemTuple.kemId)
+            .setKdfId(kdfTuple.kdfId)
+            .setAeadId(aeadTuple.aeadId)
+            .setVariant(variantTuple.variant)
+            .build();
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(
+            parameters, Bytes.copyFrom(kemTuple.publicKey), variantTuple.idRequirement);
+    HpkePrivateKey privateKey =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(kemTuple.privateKey, InsecureSecretKeyAccess.get()));
+
+    HpkeParams protoParams =
+        createHpkeProtoParams(kemTuple.kemProto, kdfTuple.kdfProto, aeadTuple.aeadProto);
+    com.google.crypto.tink.proto.HpkePublicKey protoPublicKey =
+        createHpkeProtoPublicKey(/* version= */ 0, protoParams, kemTuple.publicKey);
+    com.google.crypto.tink.proto.HpkePrivateKey protoPrivateKey =
+        createHpkeProtoPrivateKey(/* version= */ 0, protoPublicKey, kemTuple.privateKey);
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HpkePrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            variantTuple.outputPrefixType,
+            /* idRequirement= */ variantTuple.idRequirement);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HpkePublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void parsePrivateKey_noAccess_throws() throws Exception {
+    HpkeParams protoParams =
+        createHpkeProtoParams(KEMS[0].kemProto, KDFS[0].kdfProto, AEADS[0].aeadProto);
+    com.google.crypto.tink.proto.HpkePublicKey protoPublicKey =
+        createHpkeProtoPublicKey(/* version= */ 0, protoParams, KEMS[0].publicKey);
+    com.google.crypto.tink.proto.HpkePrivateKey protoPrivateKey =
+        createHpkeProtoPrivateKey(/* version= */ 0, protoPublicKey, KEMS[0].privateKey);
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HpkePrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+
+  @Test
+  public void serializePrivateKey_noAccess_throws() throws Exception {
+    HpkeParameters parameters =
+        HpkeParameters.builder()
+            .setKemId(KEMS[0].kemId)
+            .setKdfId(KDFS[0].kdfId)
+            .setAeadId(AEADS[0].aeadId)
+            .setVariant(VARIANTS[0].variant)
+            .build();
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(
+            parameters, Bytes.copyFrom(KEMS[0].publicKey), VARIANTS[0].idRequirement);
+    HpkePrivateKey privateKey =
+        HpkePrivateKey.create(
+            publicKey, SecretBytes.copyFrom(KEMS[0].privateKey, InsecureSecretKeyAccess.get()));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(privateKey, ProtoKeySerialization.class, /* access= */ null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Unknown output prefix.
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            HpkeKeyFormat.newBuilder()
+                .setParams(
+                    createHpkeProtoParams(KEMS[0].kemProto, KDFS[0].kdfProto, AEADS[0].aeadProto))
+                .build()),
+        // Unknown KEM.
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.TINK,
+            HpkeKeyFormat.newBuilder()
+                .setParams(
+                    createHpkeProtoParams(
+                        HpkeKem.KEM_UNKNOWN, KDFS[0].kdfProto, AEADS[0].aeadProto))
+                .build()),
+        // Unknown KDF.
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.TINK,
+            HpkeKeyFormat.newBuilder()
+                .setParams(
+                    createHpkeProtoParams(
+                        KEMS[0].kemProto, HpkeKdf.KDF_UNKNOWN, AEADS[0].aeadProto))
+                .build()),
+        // Unknown AEAD.
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.TINK,
+            HpkeKeyFormat.newBuilder()
+                .setParams(
+                    createHpkeProtoParams(
+                        KEMS[0].kemProto, KDFS[0].kdfProto, HpkeAead.AEAD_UNKNOWN))
+                .build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(PRIVATE_TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void parseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPublicKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            createHpkeProtoPublicKey(
+                    /* version= */ 1,
+                    createHpkeProtoParams(KEMS[0].kemProto, KDFS[0].kdfProto, AEADS[0].aeadProto),
+                    KEMS[0].publicKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            123),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            createHpkeProtoPublicKey(
+                    /* version= */ 0,
+                    createHpkeProtoParams(KEMS[0].kemProto, KDFS[0].kdfProto, AEADS[0].aeadProto),
+                    KEMS[0].publicKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            123),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            123),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            createHpkeProtoPublicKey(
+                    /* version= */ 0,
+                    createHpkeProtoParams(KEMS[0].kemProto, KDFS[0].kdfProto, AEADS[0].aeadProto),
+                    KEMS[0].publicKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            123),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPublicKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PUBLIC_KEY_SERIALIZATIONS =
+      createInvalidPublicKeySerializations();
+
+  @Theory
+  public void parseInvalidPublicKeys_throws(
+      @FromDataPoints("invalidPublicKeySerializations") ProtoKeySerialization serialization) {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPrivateKeySerializations() {
+    try {
+      HpkeParams protoParams =
+          createHpkeProtoParams(KEMS[0].kemProto, KDFS[0].kdfProto, AEADS[0].aeadProto);
+      com.google.crypto.tink.proto.HpkePublicKey validProtoPublicKey =
+          createHpkeProtoPublicKey(/* version= */ 0, protoParams, KEMS[0].publicKey);
+
+      return new ProtoKeySerialization[] {
+        // Bad private key value.
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            createHpkeProtoPrivateKey(/* version= */ 0, validProtoPublicKey, Random.randBytes(4))
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            123),
+        // Bad version number (1).
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            createHpkeProtoPrivateKey(/* version= */ 1, validProtoPublicKey, KEMS[0].privateKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            123),
+        // Unknown prefix.
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            createHpkeProtoPrivateKey(/* version= */ 0, validProtoPublicKey, KEMS[0].privateKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            123),
+        // Invalid public key.
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            createHpkeProtoPrivateKey(
+                    /* version= */ 0,
+                    createHpkeProtoPublicKey(/* version= */ 0, protoParams, Random.randBytes(4)),
+                    KEMS[0].privateKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            123),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            123),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            createHpkeProtoPrivateKey(/* version= */ 0, validProtoPublicKey, KEMS[0].privateKey)
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            123),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPrivateKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PRIVATE_KEY_SERIALIZATIONS =
+      createInvalidPrivateKeySerializations();
+
+  @Theory
+  public void parseInvalidPrivateKeys_throws(
+      @FromDataPoints("invalidPrivateKeySerializations") ProtoKeySerialization serialization) {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkePublicKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkePublicKeyTest.java
new file mode 100644
index 0000000..fa525ec
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HpkePublicKeyTest.java
@@ -0,0 +1,332 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.subtle.EllipticCurves;
+import com.google.crypto.tink.subtle.EllipticCurves.PointFormatType;
+import com.google.crypto.tink.subtle.X25519;
+import com.google.crypto.tink.util.Bytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HpkePublicKeyTest {
+  private static final class NistKemTuple {
+    final HpkeParameters.KemId kemId;
+    final EllipticCurves.CurveType curve;
+
+    NistKemTuple(HpkeParameters.KemId kemId, EllipticCurves.CurveType curve) {
+      this.kemId = kemId;
+      this.curve = curve;
+    }
+  }
+
+  @DataPoints("nistKemTuples")
+  public static final NistKemTuple[] KEMS =
+      new NistKemTuple[] {
+        new NistKemTuple(
+            HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256, EllipticCurves.CurveType.NIST_P256),
+        new NistKemTuple(
+            HpkeParameters.KemId.DHKEM_P384_HKDF_SHA384, EllipticCurves.CurveType.NIST_P384),
+        new NistKemTuple(
+            HpkeParameters.KemId.DHKEM_P521_HKDF_SHA512, EllipticCurves.CurveType.NIST_P521),
+      };
+
+  @Theory
+  public void createNistCurvePublicKey(@FromDataPoints("nistKemTuples") NistKemTuple tuple)
+      throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(tuple.kemId)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    ECPublicKey ecPublicKey = (ECPublicKey) EllipticCurves.generateKeyPair(tuple.curve).getPublic();
+    Bytes publicKeyBytes =
+        Bytes.copyFrom(
+            EllipticCurves.pointEncode(
+                tuple.curve, PointFormatType.UNCOMPRESSED, ecPublicKey.getW()));
+
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    assertThat(publicKey.getPublicKeyBytes()).isEqualTo(publicKeyBytes);
+    assertThat(publicKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(publicKey.getParameters()).isEqualTo(params);
+    assertThat(publicKey.getIdRequirementOrNull()).isEqualTo(null);
+  }
+
+  @Test
+  public void createX25519PublicKey() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+
+    HpkePublicKey publicKey =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    assertThat(publicKey.getPublicKeyBytes()).isEqualTo(publicKeyBytes);
+    assertThat(publicKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(publicKey.getParameters()).isEqualTo(params);
+    assertThat(publicKey.getIdRequirementOrNull()).isEqualTo(null);
+  }
+
+  @Theory
+  public void createNistCurvePublicKey_failsWithWrongKeyLength(
+      @FromDataPoints("nistKemTuples") NistKemTuple tuple) throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(tuple.kemId)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    ECPublicKey ecPublicKey = (ECPublicKey) EllipticCurves.generateKeyPair(tuple.curve).getPublic();
+    Bytes publicKeyBytes =
+        Bytes.copyFrom(
+            EllipticCurves.pointEncode(
+                tuple.curve, PointFormatType.UNCOMPRESSED, ecPublicKey.getW()));
+    Bytes tooShort = Bytes.copyFrom(publicKeyBytes.toByteArray(), 0, publicKeyBytes.size() - 1);
+    byte[] tooLongBytes = new byte[publicKeyBytes.size() + 1];
+    System.arraycopy(publicKeyBytes.toByteArray(), 0, tooLongBytes, 0, publicKeyBytes.size());
+    Bytes tooLong = Bytes.copyFrom(tooLongBytes);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(params, tooShort, /* idRequirement= */ null));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(params, tooLong, /* idRequirement= */ null));
+  }
+
+  @Test
+  public void createX25519PublicKey_failsWithWrongKeyLength() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+    Bytes tooShort = Bytes.copyFrom(publicKeyBytes.toByteArray(), 0, publicKeyBytes.size() - 1);
+    byte[] tooLongBytes = new byte[publicKeyBytes.size() + 1];
+    System.arraycopy(publicKeyBytes.toByteArray(), 0, tooLongBytes, 0, publicKeyBytes.size());
+    Bytes tooLong = Bytes.copyFrom(tooLongBytes);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(params, tooShort, /* idRequirement= */ null));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(params, tooLong, /* idRequirement= */ null));
+  }
+
+  /** Copied from {@link EllipticCurves#pointEncode} to bypass point validation. */
+  private static byte[] encodeUncompressedPoint(EllipticCurve curve, ECPoint point)
+      throws GeneralSecurityException {
+    int coordinateSize = EllipticCurves.fieldSizeInBytes(curve);
+    byte[] encoded = new byte[2 * coordinateSize + 1];
+    byte[] x = BigIntegerEncoding.toBigEndianBytes(point.getAffineX());
+    byte[] y = BigIntegerEncoding.toBigEndianBytes(point.getAffineY());
+    // Order of System.arraycopy is important because x,y can have leading 0's.
+    System.arraycopy(y, 0, encoded, 1 + 2 * coordinateSize - y.length, y.length);
+    System.arraycopy(x, 0, encoded, 1 + coordinateSize - x.length, x.length);
+    encoded[0] = 4;
+    return encoded;
+  }
+
+  @Theory
+  public void createNistCurvePublicKey_failsIfPointNotOnCurve(
+      @FromDataPoints("nistKemTuples") NistKemTuple tuple) throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(tuple.kemId)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    ECPublicKey ecPublicKey = (ECPublicKey) EllipticCurves.generateKeyPair(tuple.curve).getPublic();
+    ECPoint point = ecPublicKey.getW();
+    ECPoint badPoint = new ECPoint(point.getAffineX(), point.getAffineY().subtract(BigInteger.ONE));
+
+    Bytes publicKeyBytes =
+        Bytes.copyFrom(
+            encodeUncompressedPoint(EllipticCurves.getCurveSpec(tuple.curve).getCurve(), badPoint));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null));
+  }
+
+  @Test
+  public void createPublicKey_failsWithMismatchedIdRequirement() throws Exception {
+    HpkeParameters.Builder paramsBuilder =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM);
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+
+    HpkeParameters noPrefixParams =
+        paramsBuilder.setVariant(HpkeParameters.Variant.NO_PREFIX).build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(noPrefixParams, publicKeyBytes, /* idRequirement= */ 123));
+
+    HpkeParameters tinkParams = paramsBuilder.setVariant(HpkeParameters.Variant.TINK).build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(tinkParams, publicKeyBytes, /* idRequirement= */ null));
+
+    HpkeParameters crunchyParams = paramsBuilder.setVariant(HpkeParameters.Variant.CRUNCHY).build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HpkePublicKey.create(crunchyParams, publicKeyBytes, /* idRequirement= */ null));
+  }
+
+  @Test
+  public void getOutputPrefix() throws Exception {
+    HpkeParameters.Builder paramsBuilder =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM);
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+
+    HpkeParameters noPrefixParams =
+        paramsBuilder.setVariant(HpkeParameters.Variant.NO_PREFIX).build();
+    HpkePublicKey noPrefixPublicKey =
+        HpkePublicKey.create(noPrefixParams, publicKeyBytes, /* idRequirement= */ null);
+    assertThat(noPrefixPublicKey.getIdRequirementOrNull()).isEqualTo(null);
+    assertThat(noPrefixPublicKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+
+    HpkeParameters tinkParams = paramsBuilder.setVariant(HpkeParameters.Variant.TINK).build();
+    HpkePublicKey tinkPublicKey =
+        HpkePublicKey.create(tinkParams, publicKeyBytes, /* idRequirement= */ 0x02030405);
+    assertThat(tinkPublicKey.getIdRequirementOrNull()).isEqualTo(0x02030405);
+    assertThat(tinkPublicKey.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x01, 0x02, 0x03, 0x04, 0x05}));
+
+    HpkeParameters crunchyParams = paramsBuilder.setVariant(HpkeParameters.Variant.CRUNCHY).build();
+    HpkePublicKey crunchyPublicKey =
+        HpkePublicKey.create(crunchyParams, publicKeyBytes, /* idRequirement= */ 0x01020304);
+    assertThat(crunchyPublicKey.getIdRequirementOrNull()).isEqualTo(0x01020304);
+    assertThat(crunchyPublicKey.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}));
+  }
+
+  @Test
+  public void sameKeysAreEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+
+    HpkePublicKey publicKey1 =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+    HpkePublicKey publicKey2 =
+        HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ null);
+
+    assertThat(publicKey1.equalsKey(publicKey2)).isTrue();
+  }
+
+  @Test
+  public void differentParamsAreNotEqual() throws Exception {
+    HpkeParameters.Builder paramsBuilder =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM);
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+
+    HpkeParameters params1 = paramsBuilder.setVariant(HpkeParameters.Variant.TINK).build();
+    HpkePublicKey publicKey1 =
+        HpkePublicKey.create(params1, publicKeyBytes, /* idRequirement= */ 123);
+    HpkeParameters params2 = paramsBuilder.setVariant(HpkeParameters.Variant.CRUNCHY).build();
+    HpkePublicKey publicKey2 =
+        HpkePublicKey.create(params2, publicKeyBytes, /* idRequirement= */ 123);
+
+    assertThat(publicKey1.equalsKey(publicKey2)).isFalse();
+  }
+
+  @Test
+  public void differentKeyBytesAreNotEqual() throws Exception {
+    HpkeParameters params =
+        HpkeParameters.builder()
+            .setVariant(HpkeParameters.Variant.NO_PREFIX)
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM)
+            .build();
+    Bytes publicKeyBytes1 = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+    byte[] buf2 = publicKeyBytes1.toByteArray();
+    buf2[0] = (byte) (buf2[0] ^ 0x01);
+    Bytes publicKeyBytes2 = Bytes.copyFrom(buf2);
+
+    HpkePublicKey publicKey1 =
+        HpkePublicKey.create(params, publicKeyBytes1, /* idRequirement= */ null);
+    HpkePublicKey publicKey2 =
+        HpkePublicKey.create(params, publicKeyBytes2, /* idRequirement= */ null);
+
+    assertThat(publicKey1.equalsKey(publicKey2)).isFalse();
+  }
+
+  @Test
+  public void differentIdsAreNotEqual() throws Exception {
+    HpkeParameters.Builder paramsBuilder =
+        HpkeParameters.builder()
+            .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
+            .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
+            .setAeadId(HpkeParameters.AeadId.AES_128_GCM);
+    Bytes publicKeyBytes = Bytes.copyFrom(X25519.publicFromPrivate(X25519.generatePrivateKey()));
+
+    HpkeParameters params1 = paramsBuilder.setVariant(HpkeParameters.Variant.TINK).build();
+    HpkePublicKey publicKey1 =
+        HpkePublicKey.create(params1, publicKeyBytes, /* idRequirement= */ 123);
+    HpkeParameters params2 = paramsBuilder.setVariant(HpkeParameters.Variant.TINK).build();
+    HpkePublicKey publicKey2 =
+        HpkePublicKey.create(params2, publicKeyBytes, /* idRequirement= */ 456);
+
+    assertThat(publicKey1.equalsKey(publicKey2)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridConfigTest.java
index 5ee326a..5e1615e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridConfigTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.hybrid;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.HybridDecrypt;
@@ -43,23 +44,9 @@
   @Test
   public void aaaTestInitialization() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
-    GeneralSecurityException e =
-        assertThrows(GeneralSecurityException.class, () -> Registry.getCatalogue("tinkmac"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("MacConfig.register()");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkhybridencrypt"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("HybridConfig.register()");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkhybriddecrypt"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("HybridConfig.register()");
 
     String eciesPrivateKeyUrl = "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey";
-    e =
+    GeneralSecurityException e =
         assertThrows(
             GeneralSecurityException.class,
             () -> Registry.getUntypedKeyManager(eciesPrivateKeyUrl));
@@ -75,8 +62,8 @@
     // Initialize the config.
     HybridConfig.register();
 
-    Registry.getKeyManager(eciesPrivateKeyUrl, HybridDecrypt.class);
-    Registry.getKeyManager(hpkePrivateKeyUrl, HybridDecrypt.class);
+    assertNotNull(Registry.getKeyManager(eciesPrivateKeyUrl, HybridDecrypt.class));
+    assertNotNull(Registry.getKeyManager(hpkePrivateKeyUrl, HybridDecrypt.class));
 
     // Running init() manually again should succeed.
     HybridConfig.register();
@@ -96,7 +83,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, HybridDecrypt.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, HybridDecrypt.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridDecryptWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridDecryptWrapperTest.java
index 60c785e..02f99ec 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridDecryptWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridDecryptWrapperTest.java
@@ -25,6 +25,9 @@
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.internal.testing.FakeMonitoringClient;
+import com.google.crypto.tink.monitoring.MonitoringAnnotations;
 import com.google.crypto.tink.proto.EcPointFormat;
 import com.google.crypto.tink.proto.EciesAeadHkdfPrivateKey;
 import com.google.crypto.tink.proto.EciesAeadHkdfPublicKey;
@@ -35,8 +38,10 @@
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
+import java.util.List;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.experimental.theories.DataPoints;
@@ -122,8 +127,8 @@
     byte[] ciphertext = rawEncrypter.encrypt(plaintext, contextInfo);
     assertThat(wrappedDecrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext);
 
-    byte[] ciphertextWithTinkPrefix = Bytes.concat(TestUtil.hexDecode("0166AABBCC"), ciphertext);
-    byte[] ciphertextWithLegacyPrefix = Bytes.concat(TestUtil.hexDecode("0066AABBCC"), ciphertext);
+    byte[] ciphertextWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), ciphertext);
+    byte[] ciphertextWithLegacyPrefix = Bytes.concat(Hex.decode("0066AABBCC"), ciphertext);
     assertThrows(
         GeneralSecurityException.class,
         () -> wrappedDecrypter.decrypt(ciphertextWithTinkPrefix, contextInfo));
@@ -163,7 +168,7 @@
 
     byte[] rawCiphertext = rawEncrypter.encrypt(plaintext, contextInfo);
 
-    byte[] ciphertextWithTinkPrefix = Bytes.concat(TestUtil.hexDecode("0166AABBCC"), rawCiphertext);
+    byte[] ciphertextWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), rawCiphertext);
 
     assertThat(wrappedDecrypter.decrypt(ciphertextWithTinkPrefix, contextInfo))
         .isEqualTo(plaintext);
@@ -171,8 +176,7 @@
     assertThrows(
         GeneralSecurityException.class,
         () -> wrappedDecrypter.decrypt(rawCiphertext, contextInfo));
-    byte[] ciphertextWithLegacyPrefix =
-        Bytes.concat(TestUtil.hexDecode("0066AABBCC"), rawCiphertext);
+    byte[] ciphertextWithLegacyPrefix = Bytes.concat(Hex.decode("0066AABBCC"), rawCiphertext);
     assertThrows(
         GeneralSecurityException.class,
         () -> wrappedDecrypter.decrypt(ciphertextWithLegacyPrefix, contextInfo));
@@ -367,4 +371,115 @@
 
     assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext);
   }
+
+  @Theory
+  public void doesNotMonitorWithoutAnnotations() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
+    PrimitiveSet<HybridEncrypt> encPrimitives =
+        TestUtil.createPrimitiveSet(
+            TestUtil.createKeyset(
+                getPublicKey(
+                    eciesAeadHkdfPrivateKey1.getPublicKey(),
+                    /*keyId=*/ 123,
+                    OutputPrefixType.TINK)),
+            HybridEncrypt.class);
+    HybridEncrypt encrypter = new HybridEncryptWrapper().wrap(encPrimitives);
+
+    PrimitiveSet<HybridDecrypt> decPrimitives =
+        TestUtil.createPrimitiveSet(
+            TestUtil.createKeyset(
+                getPrivateKey(eciesAeadHkdfPrivateKey1, /*keyId=*/ 123, OutputPrefixType.TINK)),
+            HybridDecrypt.class);
+    HybridDecrypt decrypter = new HybridDecryptWrapper().wrap(decPrimitives);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo);
+
+    assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext);
+
+    assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
+    assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
+  }
+
+  @Theory
+  public void monitorsWithAnnotations() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
+    MonitoringAnnotations annotations =
+        MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
+
+    // We use two keys, to make sure that decryption logs the correct key id.
+    Key privateKey1 =
+        getPrivateKey(eciesAeadHkdfPrivateKey1, /*keyId=*/ 123, OutputPrefixType.TINK);
+    Key publicKey1 =
+        getPublicKey(
+            eciesAeadHkdfPrivateKey1.getPublicKey(), /*keyId=*/ 123, OutputPrefixType.TINK);
+
+    Key privateKey2 = getPrivateKey(eciesAeadHkdfPrivateKey2, /*keyId=*/ 234, OutputPrefixType.RAW);
+    Key publicKey2 =
+        getPublicKey(eciesAeadHkdfPrivateKey2.getPublicKey(), /*keyId=*/ 234, OutputPrefixType.RAW);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+
+    // Create for each key a ciphertext. Note that encrypter1 and encrypter2 are not monitored.
+    HybridEncrypt encrypter1 =
+        new HybridEncryptWrapper()
+            .wrap(
+                TestUtil.createPrimitiveSet(
+                    TestUtil.createKeyset(publicKey1), HybridEncrypt.class));
+    byte[] ciphertext1 = encrypter1.encrypt(plaintext, contextInfo);
+    HybridEncrypt encrypter2 =
+        new HybridEncryptWrapper()
+            .wrap(
+                TestUtil.createPrimitiveSet(
+                    TestUtil.createKeyset(publicKey2), HybridEncrypt.class));
+    byte[] ciphertext2 = encrypter2.encrypt(plaintext, contextInfo);
+
+    HybridDecrypt decrypter =
+        new HybridDecryptWrapper()
+            .wrap(
+                TestUtil.createPrimitiveSetWithAnnotations(
+                    TestUtil.createKeyset(privateKey1, privateKey2),
+                    annotations,
+                    HybridDecrypt.class));
+
+    assertThat(decrypter.decrypt(ciphertext1, contextInfo)).isEqualTo(plaintext);
+    assertThat(decrypter.decrypt(ciphertext2, contextInfo)).isEqualTo(plaintext);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> decrypter.decrypt(ciphertext1, "invalid".getBytes(UTF_8)));
+
+    List<FakeMonitoringClient.LogEntry> logEntries = fakeMonitoringClient.getLogEntries();
+    assertThat(logEntries).hasSize(2);
+    FakeMonitoringClient.LogEntry decEntry1 = logEntries.get(0);
+    assertThat(decEntry1.getKeyId()).isEqualTo(123);
+    assertThat(decEntry1.getPrimitive()).isEqualTo("hybrid_decrypt");
+    assertThat(decEntry1.getApi()).isEqualTo("decrypt");
+    // ciphertext1 was encrypted with key1, which has a TINK ouput prefix. This adds a 5 bytes
+    // prefix to the ciphertext. This prefix is not included in getNumBytesAsInput.
+    assertThat(decEntry1.getNumBytesAsInput()).isEqualTo(ciphertext1.length - 5);
+    assertThat(decEntry1.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+
+    FakeMonitoringClient.LogEntry decEntry2 = logEntries.get(1);
+    assertThat(decEntry2.getKeyId()).isEqualTo(234);
+    assertThat(decEntry2.getPrimitive()).isEqualTo("hybrid_decrypt");
+    assertThat(decEntry2.getApi()).isEqualTo("decrypt");
+    assertThat(decEntry2.getNumBytesAsInput()).isEqualTo(ciphertext2.length);
+    assertThat(decEntry2.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+
+    List<FakeMonitoringClient.LogFailureEntry> failures =
+        fakeMonitoringClient.getLogFailureEntries();
+    assertThat(failures).hasSize(1);
+    FakeMonitoringClient.LogFailureEntry decryptFailure = failures.get(0);
+    assertThat(decryptFailure.getPrimitive()).isEqualTo("hybrid_decrypt");
+    assertThat(decryptFailure.getApi()).isEqualTo("decrypt");
+    assertThat(decryptFailure.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptIntegrationTest.java
deleted file mode 100644
index 6e40e00..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptIntegrationTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.hybrid;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertThrows;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.KeyTemplates;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetManager;
-import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.proto.EcPointFormat;
-import com.google.crypto.tink.proto.EciesAeadHkdfPrivateKey;
-import com.google.crypto.tink.proto.EllipticCurveType;
-import com.google.crypto.tink.proto.HashType;
-import com.google.crypto.tink.proto.KeyData;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.KeyTemplate;
-import com.google.crypto.tink.proto.Keyset.Key;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.security.GeneralSecurityException;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests which run the everything for the Hybrid primitives. */
-@RunWith(JUnit4.class)
-public class HybridEncryptIntegrationTest {
-  @BeforeClass
-  public static void setUp() throws Exception {
-    HybridConfig.register();
-  }
-
-  @Test
-  public void testBasicEncryption() throws Exception {
-    EllipticCurveType curve = EllipticCurveType.NIST_P384;
-    HashType hashType = HashType.SHA256;
-    EcPointFormat primaryPointFormat = EcPointFormat.UNCOMPRESSED;
-    EcPointFormat rawPointFormat = EcPointFormat.COMPRESSED;
-    KeyTemplate primaryDemKeyTemplate = AeadKeyTemplates.AES128_CTR_HMAC_SHA256;
-
-    KeyTemplate rawDemKeyTemplate = AeadKeyTemplates.AES128_CTR_HMAC_SHA256;
-    byte[] primarySalt = "some salt".getBytes(UTF_8);
-    byte[] rawSalt = "other salt".getBytes(UTF_8);
-
-    EciesAeadHkdfPrivateKey primaryPrivProto =
-        TestUtil.generateEciesAeadHkdfPrivKey(
-            curve, hashType, primaryPointFormat, primaryDemKeyTemplate, primarySalt);
-
-    Key primaryPriv =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                primaryPrivProto,
-                new EciesAeadHkdfPrivateKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-            8,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    Key primaryPub =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                primaryPrivProto.getPublicKey(),
-                new EciesAeadHkdfPublicKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-            42,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-
-    EciesAeadHkdfPrivateKey rawPrivProto =
-        TestUtil.generateEciesAeadHkdfPrivKey(
-            curve, hashType, rawPointFormat, rawDemKeyTemplate, rawSalt);
-
-    Key rawPriv =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                rawPrivProto,
-                new EciesAeadHkdfPrivateKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-            11,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    Key rawPub =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                rawPrivProto.getPublicKey(),
-                new EciesAeadHkdfPublicKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-            43,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    KeysetHandle keysetHandlePub =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(primaryPub, rawPub));
-    KeysetHandle keysetHandlePriv =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(primaryPriv, rawPriv));
-    HybridEncrypt hybridEncrypt = keysetHandlePub.getPrimitive(HybridEncrypt.class);
-    HybridDecrypt hybridDecrypt = keysetHandlePriv.getPrimitive(HybridDecrypt.class);
-    byte[] plaintext = Random.randBytes(20);
-    byte[] contextInfo = Random.randBytes(20);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, contextInfo);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, contextInfo));
-  }
-
-  @Test
-  public void testEncryptDecryptWithoutPrimary() throws Exception {
-    // Generate a Keyset with a single private key.
-    KeysetManager manager =
-        KeysetManager.withEmptyKeyset()
-            .add(KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM"));
-
-    // Generate a keyset handle. This handle does not have a primary key set.
-    KeysetHandle handleWithoutPrimary = manager.getKeysetHandle();
-
-    // Now set the primary key, and generate another keyset handle with the same key.
-    manager.setPrimary(handleWithoutPrimary.getKeysetInfo().getKeyInfo(0).getKeyId());
-    KeysetHandle handleWithPrimary = manager.getKeysetHandle();
-
-    // Use handleWithPrimary, it should work fine.
-    HybridEncrypt hybridEncrypt =
-        handleWithPrimary.getPublicKeysetHandle().getPrimitive(HybridEncrypt.class);
-    HybridDecrypt hybridDecrypt = handleWithPrimary.getPrimitive(HybridDecrypt.class);
-    byte[] plaintext = "plaintext".getBytes(UTF_8);
-    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, contextInfo);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, contextInfo));
-
-    // Use hybridEncryptWithoutPrimary, it should not work.
-    HybridEncrypt hybridEncryptWithoutPrimary =
-        handleWithoutPrimary.getPublicKeysetHandle().getPrimitive(HybridEncrypt.class);
-    assertThrows(
-        GeneralSecurityException.class,
-        () -> hybridEncryptWithoutPrimary.encrypt(plaintext, contextInfo));
-    assertThrows(
-        GeneralSecurityException.class,
-        () -> handleWithoutPrimary.getPrimitive(HybridDecrypt.class));
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptWrapperTest.java
index cbf5e2d..1d033c0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridEncryptWrapperTest.java
@@ -25,6 +25,9 @@
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.internal.testing.FakeMonitoringClient;
+import com.google.crypto.tink.monitoring.MonitoringAnnotations;
 import com.google.crypto.tink.proto.EcPointFormat;
 import com.google.crypto.tink.proto.EciesAeadHkdfPrivateKey;
 import com.google.crypto.tink.proto.EciesAeadHkdfPublicKey;
@@ -34,9 +37,11 @@
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
+import java.util.List;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.experimental.theories.DataPoints;
@@ -150,7 +155,7 @@
     byte[] prefix = Arrays.copyOf(ciphertext, 5);
     byte[] ciphertextWithoutPrefix = Arrays.copyOfRange(ciphertext, 5, ciphertext.length);
 
-    assertThat(prefix).isEqualTo(TestUtil.hexDecode("0166AABBCC"));
+    assertThat(prefix).isEqualTo(Hex.decode("0166AABBCC"));
 
     assertThat(rawDecrypter.decrypt(ciphertextWithoutPrefix, contextInfo)).isEqualTo(plaintext);
   }
@@ -256,4 +261,105 @@
         GeneralSecurityException.class,
         () -> encrypterWithoutPrimary.encrypt(plaintext, contextInfo));
   }
+
+  @Theory
+  public void doesNotMonitorWithoutAnnotations() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
+    PrimitiveSet<HybridEncrypt> encPrimitives =
+        TestUtil.createPrimitiveSet(
+            TestUtil.createKeyset(
+                getPublicKey(
+                    eciesAeadHkdfPrivateKey1.getPublicKey(),
+                    /*keyId=*/ 123,
+                    OutputPrefixType.TINK)),
+            HybridEncrypt.class);
+    HybridEncrypt encrypter = new HybridEncryptWrapper().wrap(encPrimitives);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    byte[] unused = encrypter.encrypt(plaintext, contextInfo);
+
+    assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
+    assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
+  }
+
+  @Theory
+  public void monitorsWithAnnotations() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
+    Key publicKey1 =
+        getPublicKey(
+            eciesAeadHkdfPrivateKey1.getPublicKey(), /*keyId=*/ 123, OutputPrefixType.TINK);
+    Key publicKey2 =
+        getPublicKey(eciesAeadHkdfPrivateKey1.getPublicKey(), /*keyId=*/ 234, OutputPrefixType.RAW);
+
+    MonitoringAnnotations annotations =
+        MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
+    HybridEncrypt encrypter =
+        new HybridEncryptWrapper()
+            .wrap(
+                TestUtil.createPrimitiveSetWithAnnotations(
+                    TestUtil.createKeyset(publicKey1, publicKey2),
+                    annotations,
+                    HybridEncrypt.class));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    byte[] unused = encrypter.encrypt(plaintext, contextInfo);
+
+    List<FakeMonitoringClient.LogEntry> logEntries = fakeMonitoringClient.getLogEntries();
+    assertThat(logEntries).hasSize(1);
+    FakeMonitoringClient.LogEntry encEntry = logEntries.get(0);
+    assertThat(encEntry.getKeyId()).isEqualTo(123);
+    assertThat(encEntry.getPrimitive()).isEqualTo("hybrid_encrypt");
+    assertThat(encEntry.getApi()).isEqualTo("encrypt");
+    assertThat(encEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
+    assertThat(encEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+  }
+
+  private static class AlwaysFailingHybridEncrypt implements HybridEncrypt {
+    @Override
+    public byte[] encrypt(byte[] plaintext, byte[] contextInfo) throws GeneralSecurityException {
+      throw new GeneralSecurityException("fail");
+    }
+  }
+
+  @Theory
+  public void testAlwaysFailingHybridEncryptWithAnnotations_hasMonitoring() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
+    MonitoringAnnotations annotations =
+        MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
+    PrimitiveSet<HybridEncrypt> primitives =
+        PrimitiveSet.newBuilder(HybridEncrypt.class)
+            .setAnnotations(annotations)
+            .addPrimaryPrimitive(
+                new AlwaysFailingHybridEncrypt(),
+                getPublicKey(
+                    eciesAeadHkdfPrivateKey1.getPublicKey(), /*keyId=*/ 123, OutputPrefixType.TINK))
+            .build();
+    HybridEncrypt encrypter = new HybridEncryptWrapper().wrap(primitives);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    assertThrows(GeneralSecurityException.class, () -> encrypter.encrypt(plaintext, contextInfo));
+
+    assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
+
+    List<FakeMonitoringClient.LogFailureEntry> failures =
+        fakeMonitoringClient.getLogFailureEntries();
+    assertThat(failures).hasSize(1);
+    FakeMonitoringClient.LogFailureEntry encryptFailure = failures.get(0);
+    assertThat(encryptFailure.getPrimitive()).isEqualTo("hybrid_encrypt");
+    assertThat(encryptFailure.getApi()).isEqualTo("encrypt");
+    assertThat(encryptFailure.getKeysetInfo().getPrimaryKeyId()).isEqualTo(123);
+    assertThat(encryptFailure.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridTest.java
new file mode 100644
index 0000000..eadcc2b
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/HybridTest.java
@@ -0,0 +1,316 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.hybrid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.HybridDecrypt;
+import com.google.crypto.tink.HybridEncrypt;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import com.google.crypto.tink.testing.TestUtil;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the Hybrid package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class HybridTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    HybridConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonSignatureKeyset_throws.
+  }
+
+  @DataPoints("templates")
+  public static final String[] TEMPLATES =
+      new String[] {
+        "ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM",
+        "ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM_RAW",
+        "ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM",
+        "ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM_RAW",
+        "ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256",
+        "ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256_RAW",
+        "ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256",
+        "ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256_RAW",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM_RAW",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM_RAW",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM_RAW",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM_RAW"
+      };
+
+  @Theory
+  public void createEncryptDecrypt(@FromDataPoints("templates") String templateName)
+      throws Exception {
+    if (TestUtil.isTsan()) {
+      // KeysetHandle.generateNew is too slow in Tsan.
+      return;
+    }
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    KeysetHandle publicHandle = privateHandle.getPublicKeysetHandle();
+
+    HybridEncrypt encrypter = publicHandle.getPrimitive(HybridEncrypt.class);
+    HybridDecrypt decrypter = privateHandle.getPrimitive(HybridDecrypt.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo);
+    assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext);
+
+    KeysetHandle otherPrivateHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    HybridDecrypt otherDecrypter = otherPrivateHandle.getPrimitive(HybridDecrypt.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> otherDecrypter.decrypt(ciphertext, contextInfo));
+
+    byte[] invalid = "invalid".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    assertThrows(GeneralSecurityException.class, () -> decrypter.decrypt(ciphertext, invalid));
+    assertThrows(GeneralSecurityException.class, () -> decrypter.decrypt(invalid, contextInfo));
+    assertThrows(GeneralSecurityException.class, () -> decrypter.decrypt(empty, contextInfo));
+    assertThat(decrypter.decrypt(encrypter.encrypt(empty, contextInfo), contextInfo))
+        .isEqualTo(empty);
+    assertThat(decrypter.decrypt(encrypter.encrypt(plaintext, empty), empty)).isEqualTo(plaintext);
+  }
+
+  // Keyset with one private key for HybridDecrypt, serialized in Tink's JSON format.
+  private static final String JSON_PRIVATE_KEYSET = ""
+      + "{"
+      + "  \"primaryKeyId\": 1885000158,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePrivateKey\","
+      + "        \"value\": \"GiBXM1jmpJqe7HUTTkQxRwEld3bvIPTBhqGcI09ki9H0mRIqGiCwWh0y63G"
+      + "fObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 1885000158,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  // Keyset with the corresponding public key for HybridEncrypt, serialized in Tink's JSON format.
+  private static final String JSON_PUBLIC_KEYSET = ""
+      + "{"
+      + "  \"primaryKeyId\": 1885000158,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePublicKey\","
+      + "        \"value\": \"GiCwWh0y63GfObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 1885000158,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Theory
+  public void readKeysetEncryptDecrypt_success()
+      throws Exception {
+    KeysetHandle privateHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_PRIVATE_KEYSET, InsecureSecretKeyAccess.get());
+    KeysetHandle publicHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_PUBLIC_KEYSET, InsecureSecretKeyAccess.get());
+
+    HybridEncrypt encrypter = publicHandle.getPrimitive(HybridEncrypt.class);
+    HybridDecrypt decrypter = privateHandle.getPrimitive(HybridDecrypt.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo);
+    assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext);
+  }
+
+  // Keyset with multiple keys. The first key is the same as in JSON_PRIVATE_KEYSET. The second
+  // key is the primary key and will be used for signing.
+  private static final String JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 405658073,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePrivateKey\","
+          + "        \"value\": \"GiBXM1jmpJqe7HUTTkQxRwEld3bvIPTBhqGcI09ki9H0mRIqGiCwWh0y63G"
+          + "fObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1885000158,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey\","
+          + "        \"value\": \"GiAGLU3EgraobyU/aOJalcfR2jUUwK/ubd5mTYHIzLHBnBKiASIgJDF8fcN"
+          + "yDS6BcgYpeVPkJ2/ZBG+Mum30OId4D4CzDuQaIP9J2qo487Shr+MxMIkE3VvMro1r4Z+VFoTP3QWVTpz"
+          + "iElwYARJSElAYARISEggQIAoEEBAIAwoGEBAKAggQCjh0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5"
+          + "jcnlwdG8udGluay5BZXNDdHJIbWFjQWVhZEtleQoEEAMIAg==\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 405658073,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePrivateKey\","
+          + "        \"value\": \"GiAnd0VLE8exo149gJ49nkifg03YQLNnRMKfna0AdfYjnBIqGiABOUjRp8F"
+          + "QgppUbZlHCkxRgxGc3jYiChCkm+pf9BL3YhIGGAIQAQgB\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 2085058073,"
+          + "      \"outputPrefixType\": \"LEGACY\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  // Keyset with the public keys of the keys from JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS.
+  private static final String JSON_PUBLIC_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 405658073,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePublicKey\","
+          + "        \"value\": \"GiCwWh0y63GfObeWuYZcuLIiFz+15ElOFL7rhf9rbWxdBBIGGAEQAQgB\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1885000158,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey\","
+          + "        \"value\": \"IiAkMXx9w3INLoFyBil5U+Qnb9kEb4y6bfQ4h3gPgLMO5Bog/0naqjjztKG"
+          + "v4zEwiQTdW8yujWvhn5UWhM/dBZVOnOISXBgBElISUBgBEhISCBAgCgQQEAgDCgYQEAoCCBAKOHR5cGU"
+          + "uZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkFlc0N0ckhtYWNBZWFkS2V5CgQQAwgC\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 405658073,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HpkePublicKey\","
+          + "        \"value\": \"GiABOUjRp8FQgppUbZlHCkxRgxGc3jYiChCkm+pf9BL3YhIGGAIQAQgB\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 2085058073,"
+          + "      \"outputPrefixType\": \"LEGACY\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void multipleKeysReadKeysetWithEncryptDecrypt()
+      throws Exception {
+    KeysetHandle privateHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+    KeysetHandle publicHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_PUBLIC_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+
+    HybridEncrypt encrypter = publicHandle.getPrimitive(HybridEncrypt.class);
+    HybridDecrypt decrypter = privateHandle.getPrimitive(HybridDecrypt.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] contextInfo = "contextInfo".getBytes(UTF_8);
+    byte[] ciphertext = encrypter.encrypt(plaintext, contextInfo);
+    assertThat(decrypter.decrypt(ciphertext, contextInfo)).isEqualTo(plaintext);
+
+    // Also test that decrypter can decrypt ciphertext of a non-primary key. We use
+    // JSON_PUBLIC_KEYSET to create a ciphertext with the first key.
+    KeysetHandle publicHandle1 =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_PUBLIC_KEYSET, InsecureSecretKeyAccess.get());
+    HybridEncrypt encrypter1 = publicHandle1.getPrimitive(HybridEncrypt.class);
+    byte[] ciphertext1 = encrypter1.encrypt(plaintext, contextInfo);
+    assertThat(decrypter.decrypt(ciphertext1, contextInfo)).isEqualTo(plaintext);
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the HybridEncrypt
+  // or HybridDecrypt.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonSignatureKeyset_throws()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a DeterministicAead primitive, but neither HybridEncrypt
+    // nor HybridDecrypt primitives.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(HybridEncrypt.class));
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(HybridDecrypt.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelperTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelperTest.java
index 6cbd284..9bdb90c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/RegistryEciesAeadHkdfDemHelperTest.java
@@ -21,49 +21,47 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.Config;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
 import com.google.crypto.tink.hybrid.subtle.AeadOrDaead;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.signature.SignatureKeyTemplates;
 import com.google.crypto.tink.subtle.Random;
+import com.google.protobuf.ExtensionRegistryLite;
 import java.nio.charset.Charset;
 import java.security.GeneralSecurityException;
-import javax.crypto.Cipher;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests for RegistryEciesAeadHkdfDemHelper. */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public class RegistryEciesAeadHkdfDemHelperTest {
   private static final Charset UTF_8 = Charset.forName("UTF-8");
 
-  private KeyTemplate[] keyTemplates;
+  @DataPoints("parameters")
+  public static final Parameters[] PARAMETERS_TO_TEST =
+      new Parameters[] {
+        PredefinedAeadParameters.AES128_GCM,
+        PredefinedAeadParameters.AES256_GCM,
+        PredefinedAeadParameters.AES128_CTR_HMAC_SHA256,
+        PredefinedAeadParameters.AES256_CTR_HMAC_SHA256,
+        PredefinedDeterministicAeadParameters.AES256_SIV
+      };
 
   @Before
   public void setUp() throws Exception {
-    Config.register(AeadConfig.TINK_1_0_0);
-
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip tests with keys larger than 128 bits.");
-      keyTemplates =
-          new KeyTemplate[] {AeadKeyTemplates.AES128_GCM, AeadKeyTemplates.AES128_CTR_HMAC_SHA256};
-    } else {
-      keyTemplates =
-          new KeyTemplate[] {
-            AeadKeyTemplates.AES128_GCM,
-            AeadKeyTemplates.AES256_GCM,
-            AeadKeyTemplates.AES128_CTR_HMAC_SHA256,
-            AeadKeyTemplates.AES256_CTR_HMAC_SHA256,
-            DeterministicAeadKeyTemplates.AES256_SIV
-          };
-    }
+    AeadConfig.register();
+    DeterministicAeadConfig.register();
   }
 
   @Test
@@ -79,12 +77,6 @@
 
   @Test
   public void testConstructorWith256BitCiphers() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip tests with keys larger than 128 bits.");
-      return;
-    }
     // Supported templates.
     RegistryEciesAeadHkdfDemHelper helper =
         new RegistryEciesAeadHkdfDemHelper(AeadKeyTemplates.AES256_GCM);
@@ -127,38 +119,38 @@
         () -> new RegistryEciesAeadHkdfDemHelper(template));
   }
 
-  @Test
-  public void testGetAead() throws Exception {
+  @Theory
+  public void testGetAead(@FromDataPoints("parameters") Parameters parameters) throws Exception {
     byte[] plaintext = "some plaintext string".getBytes(UTF_8);
     byte[] associatedData = "some associated data".getBytes(UTF_8);
-    int count = 0;
-    for (KeyTemplate template : keyTemplates) {
-      RegistryEciesAeadHkdfDemHelper helper = new RegistryEciesAeadHkdfDemHelper(template);
-      byte[] symmetricKey = Random.randBytes(helper.getSymmetricKeySizeInBytes());
-      AeadOrDaead aead = helper.getAeadOrDaead(symmetricKey);
-      byte[] ciphertext = aead.encrypt(plaintext, associatedData);
-      byte[] decrypted = aead.decrypt(ciphertext, associatedData);
-      assertArrayEquals(plaintext, decrypted);
+    KeyTemplate template =
+        KeyTemplate.parseFrom(
+            TinkProtoParametersFormat.serialize(parameters),
+            ExtensionRegistryLite.getEmptyRegistry());
 
-      // Try using a symmetric key that is too short.
-      final byte[] symmetricKey2 = Random.randBytes(helper.getSymmetricKeySizeInBytes() - 1);
-      GeneralSecurityException e =
-          assertThrows(
-              "Symmetric key too short, should have thrown exception:\n" + template.toString(),
-              GeneralSecurityException.class,
-              () -> helper.getAeadOrDaead(symmetricKey2));
-      assertExceptionContains(e, "incorrect length");
+    RegistryEciesAeadHkdfDemHelper helper = new RegistryEciesAeadHkdfDemHelper(template);
+    byte[] symmetricKey = Random.randBytes(helper.getSymmetricKeySizeInBytes());
+    AeadOrDaead aead = helper.getAeadOrDaead(symmetricKey);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertArrayEquals(plaintext, decrypted);
 
-      // Try using a symmetric key that is too long.
-      final byte[] symmetricKey3 = Random.randBytes(helper.getSymmetricKeySizeInBytes() + 1);
-      e =
-          assertThrows(
-              "Symmetric key too long, should have thrown exception:\n" + template.toString(),
-              GeneralSecurityException.class,
-              () -> helper.getAeadOrDaead(symmetricKey3));
-      assertExceptionContains(e, "incorrect length");
-      count++;
-    }
-    assertEquals(keyTemplates.length, count);
+    // Try using a symmetric key that is too short.
+    final byte[] symmetricKey2 = Random.randBytes(helper.getSymmetricKeySizeInBytes() - 1);
+    GeneralSecurityException e =
+        assertThrows(
+            "Symmetric key too short, should have thrown exception:\n" + template,
+            GeneralSecurityException.class,
+            () -> helper.getAeadOrDaead(symmetricKey2));
+    assertExceptionContains(e, "incorrect length");
+
+    // Try using a symmetric key that is too long.
+    final byte[] symmetricKey3 = Random.randBytes(helper.getSymmetricKeySizeInBytes() + 1);
+    e =
+        assertThrows(
+            "Symmetric key too long, should have thrown exception:\n" + template,
+            GeneralSecurityException.class,
+            () -> helper.getAeadOrDaead(symmetricKey3));
+    assertExceptionContains(e, "incorrect length");
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel
index 48873cb..2441f4d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/BUILD.bazel
@@ -71,8 +71,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:hpke_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_guava_guava",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -91,7 +91,7 @@
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:x25519",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -110,8 +110,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:hpke_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_guava_guava",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -152,6 +152,7 @@
         "//src/main/java/com/google/crypto/tink/hybrid/internal:hpke_public_key_manager",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -170,7 +171,7 @@
         "//src/main/java/com/google/crypto/tink/hybrid/internal:hpke_public_key_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:x25519",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -192,8 +193,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/testing:hpke_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_guava_guava",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManagerTest.java
index 08b1798..1e036ba 100644
--- a/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/hybrid/internal/HpkePrivateKeyManagerTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.hybrid.internal;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.HybridDecrypt;
@@ -32,6 +33,7 @@
 import com.google.crypto.tink.proto.HpkePublicKey;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import org.junit.Before;
 import org.junit.Test;
@@ -66,12 +68,33 @@
     return HpkeKeyFormat.newBuilder().setParams(params).build();
   }
 
-  @Test
-  public void validateKeyFormat_succeeds() throws Exception {
-    HpkeKeyFormat format =
-        createKeyFormat(
-            HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
+  @DataPoints("templateNames")
+  public static final String[] KEY_TEMPLATES =
+      new String[] {
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305",
+        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM",
+        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM_RAW",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM",
+        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM_RAW",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM_RAW",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM",
+        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM_RAW",
+      };
 
+  @Theory
+  public void validateKeyFormat_succeeds(@FromDataPoints("templateNames") String template)
+      throws Exception {
+    HpkeKeyFormat format = factory.keyFormats().get(template).keyFormat;
     factory.validateKeyFormat(format);
   }
 
@@ -108,29 +131,6 @@
     assertThrows(GeneralSecurityException.class, () -> factory.validateKeyFormat(format));
   }
 
-  @DataPoints("templateNames")
-  public static final String[] KEY_TEMPLATES =
-      new String[] {
-        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM",
-        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW",
-        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM",
-        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW",
-        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305",
-        "DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW",
-        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM",
-        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW",
-        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM",
-        "DHKEM_P256_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW",
-        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM",
-        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_128_GCM_RAW",
-        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM",
-        "DHKEM_P384_HKDF_SHA384_HKDF_SHA384_AES_256_GCM_RAW",
-        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM",
-        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_128_GCM_RAW",
-        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM",
-        "DHKEM_P521_HKDF_SHA512_HKDF_SHA512_AES_256_GCM_RAW",
-      };
-
   @Theory
   public void keyFormats(@FromDataPoints("templateNames") String template) throws Exception {
     factory.validateKeyFormat(factory.keyFormats().get(template).keyFormat);
@@ -139,6 +139,10 @@
   @Theory
   public void createKey_succeeds(@FromDataPoints("templateNames") String template)
       throws Exception {
+    if (TestUtil.isTsan()) {
+      // key generation is too slow in Tsan.
+      return;
+    }
     HpkeKeyFormat format = factory.keyFormats().get(template).keyFormat;
     HpkePrivateKey key = factory.createKey(format);
 
@@ -151,6 +155,10 @@
   @Theory
   public void validateKey_succeeds(@FromDataPoints("templateNames") String template)
       throws Exception {
+    if (TestUtil.isTsan()) {
+      // key generation is too slow in Tsan.
+      return;
+    }
     HpkeKeyFormat format = factory.keyFormats().get(template).keyFormat;
 
     manager.validateKey(factory.createKey(format));
@@ -194,30 +202,26 @@
     assertThat(manager.getPublicKey(key)).isEqualTo(key.getPublicKey());
   }
 
-  @Test
-  public void parseKey() throws Exception {
-    HpkeKeyFormat format =
-        createKeyFormat(
-            HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
+  @Theory
+  public void parseKey(@FromDataPoints("templateNames") String template) throws Exception {
+    HpkeKeyFormat format = factory.keyFormats().get(template).keyFormat;
     HpkePrivateKey privateKey = factory.createKey(format);
-
     assertThat(manager.parseKey(privateKey.toByteString())).isEqualTo(privateKey);
   }
 
-  @Test
-  public void parseKeyFormat() throws Exception {
-    HpkeKeyFormat format =
-        createKeyFormat(
-            HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
-
+  @Theory
+  public void parseKeyFormat(@FromDataPoints("templateNames") String template) throws Exception {
+    HpkeKeyFormat format = factory.keyFormats().get(template).keyFormat;
     assertThat(factory.parseKeyFormat(format.toByteString())).isEqualTo(format);
   }
 
-  @Test
-  public void createPrimitive() throws Exception {
-    HpkeKeyFormat format =
-        createKeyFormat(
-            HpkeKem.DHKEM_X25519_HKDF_SHA256, HpkeKdf.HKDF_SHA256, HpkeAead.AES_128_GCM);
+  @Theory
+  public void createPrimitive(@FromDataPoints("templateNames") String template) throws Exception {
+    if (TestUtil.isTsan()) {
+      // key generation is too slow in Tsan.
+      return;
+    }
+    HpkeKeyFormat format = factory.keyFormats().get(template).keyFormat;
     HpkePrivateKey privateKey = factory.createKey(format);
     HpkePublicKey publicKey = manager.getPublicKey(privateKey);
     HybridDecrypt hybridDecrypt = manager.getPrimitive(privateKey, HybridDecrypt.class);
@@ -245,7 +249,7 @@
 
     HpkePrivateKeyManager.registerPair(/*newKeyAllowed=*/ true);
 
-    Registry.getKeyManager(publicKeyUrl, HybridEncrypt.class);
-    Registry.getKeyManager(privateKeyUrl, HybridDecrypt.class);
+    assertNotNull(Registry.getKeyManager(publicKeyUrl, HybridEncrypt.class));
+    assertNotNull(Registry.getKeyManager(privateKeyUrl, HybridDecrypt.class));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsAeadTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsAeadTest.java
index 40ecc40..873b254 100644
--- a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsAeadTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsAeadTest.java
@@ -16,121 +16,111 @@
 
 package com.google.crypto.tink.integration.awskms;
 
-import static org.junit.Assert.assertArrayEquals;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.asList;
 import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
-import com.amazonaws.AmazonServiceException;
 import com.amazonaws.services.kms.AWSKMS;
-import com.amazonaws.services.kms.model.DecryptRequest;
-import com.amazonaws.services.kms.model.DecryptResult;
-import com.amazonaws.services.kms.model.EncryptRequest;
-import com.amazonaws.services.kms.model.EncryptResult;
 import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.subtle.Random;
-import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.junit.runners.JUnit4;
 
-/**
- * Tests for AwsKmsAead.
- */
-@RunWith(MockitoJUnitRunner.class)
+/** Tests for AwsKmsAead. */
+@RunWith(JUnit4.class)
 public class AwsKmsAeadTest {
   private static final String KEY_ARN =
       "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
-  @Mock private AWSKMS mockKms;
+  private static final String KEY_ARN_DIFFERENT = "arn:aws:kms:us-west-2:123:key/different";
 
-  @Test
-  public void testEncryptDecrypt() throws Exception {
-    DecryptResult mockDecryptResult = mock(DecryptResult.class);
-    EncryptResult mockEncryptResult = mock(EncryptResult.class);
-    when(mockKms.decrypt(isA(DecryptRequest.class)))
-        .thenReturn(mockDecryptResult);
-    when(mockKms.encrypt(isA(EncryptRequest.class)))
-        .thenReturn(mockEncryptResult);
-
-    Aead aead = new AwsKmsAead(mockKms, KEY_ARN);
-    byte[] aad = Random.randBytes(20);
-    for (int messageSize = 0; messageSize < 75; messageSize++) {
-      byte[] message = Random.randBytes(messageSize);
-      when(mockDecryptResult.getKeyId()).thenReturn(KEY_ARN);
-      when(mockDecryptResult.getPlaintext()).thenReturn(ByteBuffer.wrap(message));
-      when(mockEncryptResult.getCiphertextBlob()).thenReturn(ByteBuffer.wrap(message));
-      byte[] ciphertext = aead.encrypt(message, aad);
-      byte[] decrypted = aead.decrypt(ciphertext, aad);
-      assertArrayEquals(message, decrypted);
-    }
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
   }
 
   @Test
-  public void testEncryptShouldThrowExceptionIfRequestFailed() throws Exception {
-    AmazonServiceException exception = mock(AmazonServiceException.class);
-    when(mockKms.encrypt(isA(EncryptRequest.class)))
-        .thenThrow(exception);
+  public void testEncryptDecryptWithKnownKeyArn_success() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ARN, KEY_ARN_DIFFERENT));
 
-    Aead aead = new AwsKmsAead(mockKms, KEY_ARN);
+    Aead aead = new AwsKmsAead(kms, KEY_ARN);
+    byte[] aad = Random.randBytes(20);
+    byte[] message = Random.randBytes(42);
+    byte[] ciphertext = aead.encrypt(message, aad);
+    byte[] decrypted = aead.decrypt(ciphertext, aad);
+    assertThat(decrypted).isEqualTo(message);
+  }
+
+  @Test
+  public void testEncryptWithUnknownKeyArn_fails() throws Exception {
+    AWSKMS kmsThatDoentKnowKeyArn = new FakeAwsKms(asList(KEY_ARN_DIFFERENT));
+
+    Aead aead = new AwsKmsAead(kmsThatDoentKnowKeyArn, KEY_ARN);
     byte[] aad = Random.randBytes(20);
     byte[] message = Random.randBytes(20);
     assertThrows(GeneralSecurityException.class, () -> aead.encrypt(message, aad));
   }
 
   @Test
-  public void testDecryptShouldThrowExceptionIfRequestFailed() throws Exception {
-    EncryptResult mockEncryptResult = mock(EncryptResult.class);
-    when(mockKms.encrypt(isA(EncryptRequest.class)))
-        .thenReturn(mockEncryptResult);
-    AmazonServiceException exception = mock(AmazonServiceException.class);
-    when(mockKms.decrypt(isA(DecryptRequest.class)))
-        .thenThrow(exception);
-
-    Aead aead = new AwsKmsAead(mockKms, KEY_ARN);
+  public void testDecryptWithInvalidKeyArn_fails() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ARN));
+    Aead aead = new AwsKmsAead(kms, KEY_ARN);
     byte[] aad = Random.randBytes(20);
-    byte[] message = Random.randBytes(20);
-    when(mockEncryptResult.getCiphertextBlob()).thenReturn(ByteBuffer.wrap(message));
-    byte[] ciphertext = aead.encrypt(message, aad);
-    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(ciphertext, aad));
+    byte[] invalidCiphertext = Random.randBytes(2);
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(invalidCiphertext, aad));
   }
 
   @Test
-  public void testDecryptShouldThrowExceptionIfKeyArnIsDifferent() throws Exception {
-    DecryptResult mockDecryptResult = mock(DecryptResult.class);
-    EncryptResult mockEncryptResult = mock(EncryptResult.class);
-    when(mockKms.decrypt(isA(DecryptRequest.class)))
-        .thenReturn(mockDecryptResult);
-    when(mockKms.encrypt(isA(EncryptRequest.class)))
-        .thenReturn(mockEncryptResult);
+  public void testDecryptWithDifferentKeyArn_fails() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ARN, KEY_ARN_DIFFERENT));
 
-    Aead aead = new AwsKmsAead(mockKms, KEY_ARN);
+    Aead aead = new AwsKmsAead(kms, KEY_ARN);
     byte[] aad = Random.randBytes(20);
     byte[] message = Random.randBytes(20);
-    when(mockEncryptResult.getCiphertextBlob()).thenReturn(ByteBuffer.wrap(message));
-    when(mockDecryptResult.getKeyId()).thenReturn(KEY_ARN + "1");
-    byte[] ciphertext = aead.encrypt(message, aad);
-    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(ciphertext, aad));
+
+    // Create a valid ciphertext with a different ARN
+    Aead aeadWithDifferentArn = new AwsKmsAead(kms, KEY_ARN_DIFFERENT);
+    byte[] ciphertextFromDifferentArn = aeadWithDifferentArn.encrypt(message, aad);
+
+    assertThrows(
+        GeneralSecurityException.class, () -> aead.decrypt(ciphertextFromDifferentArn, aad));
   }
 
   @Test
-  public void testDecryptShouldNotThrowExceptionIfKeyArnIsAlias() throws Exception {
-    DecryptResult mockDecryptResult = mock(DecryptResult.class);
-    EncryptResult mockEncryptResult = mock(EncryptResult.class);
-    when(mockKms.decrypt(isA(DecryptRequest.class))).thenReturn(mockDecryptResult);
-    when(mockKms.encrypt(isA(EncryptRequest.class))).thenReturn(mockEncryptResult);
+  public void testDecryptWithAliasKeyArn_success() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ARN));
 
+    byte[] aad = Random.randBytes(20);
+    byte[] message = Random.randBytes(20);
+
+    // Create ciphertext for KEY_ARN
+    Aead aead = new AwsKmsAead(kms, KEY_ARN);
+    byte[] ciphertext = aead.encrypt(message, aad);
+
+    // Use an alias ARN
     String aliasArn = "arn:aws:kms:us-west-2:111122223333:alias/ExampleAlias";
-    Aead aead = new AwsKmsAead(mockKms, aliasArn);
+    Aead aeadWithAliasArn = new AwsKmsAead(kms, aliasArn);
+    assertThat(aeadWithAliasArn.decrypt(ciphertext, aad)).isEqualTo(message);
+  }
+
+  @Test
+  public void testDecryptWithInvalidKeyArn_success() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ARN));
+
     byte[] aad = Random.randBytes(20);
     byte[] message = Random.randBytes(20);
-    when(mockEncryptResult.getCiphertextBlob()).thenReturn(ByteBuffer.wrap(message));
-    when(mockDecryptResult.getPlaintext()).thenReturn(ByteBuffer.wrap(message));
 
+    // Create ciphertext for KEY_ARN
+    Aead aead = new AwsKmsAead(kms, KEY_ARN);
     byte[] ciphertext = aead.encrypt(message, aad);
-    byte[] decrypted = aead.decrypt(ciphertext, aad);
-    assertArrayEquals(message, decrypted);
+
+    // Use an invalid Key ARN
+    // TODO(b/242149560): Make this test case fail
+    String invalidArn = "@#$@#$@#";
+    Aead aeadWithInvalidArn = new AwsKmsAead(kms, invalidArn);
+    assertThat(aeadWithInvalidArn.decrypt(ciphertext, aad)).isEqualTo(message);
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsClientTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsClientTest.java
index 69a932c..dca4eb0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsClientTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/AwsKmsClientTest.java
@@ -17,13 +17,24 @@
 package com.google.crypto.tink.integration.awskms;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KmsClient;
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.KmsClientsTestUtil;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.KmsAeadKeyManager;
+import com.google.crypto.tink.aead.KmsEnvelopeAeadKeyManager;
+import java.security.GeneralSecurityException;
 import java.util.Optional;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -34,13 +45,18 @@
   private static final String CREDENTIAL_FILE_PATH =
       "testdata/aws/credentials.cred";
 
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+  }
+
   @Before
   public void setUp() {
     KmsClientsTestUtil.reset();
   }
 
   @Test
-  public void register() throws Exception {
+  public void registerWithKeyUriAndCredentials_success() throws Exception {
     // Register a client bound to a single key.
     String keyUri = "aws-kms://register";
     AwsKmsClient.register(Optional.of(keyUri), Optional.of(CREDENTIAL_FILE_PATH));
@@ -53,11 +69,11 @@
   }
 
   @Test
-  public void register_unbound() throws Exception {
-    // Register an unbound client.
+  public void registerOnlyWithCredentials_success() throws Exception {
+    // Register a client that is not bound to a key URI.
     AwsKmsClient.register(Optional.empty(), Optional.of(CREDENTIAL_FILE_PATH));
 
-    // This should return the above unbound client.
+    // This should return the above client that should work with any aws-kms key URI.
     String keyUri = "aws-kms://register-unbound";
     KmsClient client = KmsClients.get(keyUri);
     assertThat(client.doesSupport(keyUri)).isTrue();
@@ -67,9 +83,218 @@
   }
 
   @Test
-  public void register_badKeyUri_fail() throws Exception {
+  public void registerWithCredentialsAndBadKeyUri_fail() throws Exception {
     assertThrows(
         IllegalArgumentException.class,
         () -> AwsKmsClient.register(Optional.of("blah"), Optional.of(CREDENTIAL_FILE_PATH)));
   }
+
+  @Test
+  public void registerWithKeyUriAndFakeAwsKms_kmsAeadWorks() throws Exception {
+    String keyId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String keyUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+
+    // Register a client bound to a single key.
+    AwsKmsClient.registerWithAwsKms(
+        Optional.of(keyUri), Optional.empty(), new FakeAwsKms(asList(keyId)));
+
+    // Create a KmsAead primitive
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void registerWithKeyUriAndFakeAwsKms_kmsEnvelopeAeadWorks() throws Exception {
+    String kekId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+
+    // Register a client bound to a single key.
+    AwsKmsClient.registerWithAwsKms(
+        Optional.of(kekUri), Optional.empty(), new FakeAwsKms(asList(kekId)));
+
+    // Create an envelope encryption AEAD primitive
+    KeyTemplate dekTemplate = KeyTemplates.get("AES128_CTR_HMAC_SHA256_RAW");
+    KeyTemplate envelopeTemplate = KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate);
+    KeysetHandle handle = KeysetHandle.generateNew(envelopeTemplate);
+    Aead kmsEnvelopeAead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsEnvelopeAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsEnvelopeAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void registerWithKeyUriAndFakeAwsKms_kmsAeadCanOnlyBeCreatedForRegisteredKeyUri()
+      throws Exception {
+    String keyId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String keyId2 = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+    String keyUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String keyUri2 =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+
+    AwsKmsClient.registerWithAwsKms(
+        Optional.of(keyUri), Optional.empty(), new FakeAwsKms(asList(keyId, keyId2)));
+
+    // getPrimitive works for keyUri
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead unused = handle.getPrimitive(Aead.class);
+
+    // getPrimitive does not work for keyUri2
+    KeyTemplate kmsTemplate2 = KmsAeadKeyManager.createKeyTemplate(keyUri2);
+    KeysetHandle handle2 = KeysetHandle.generateNew(kmsTemplate2);
+    assertThrows(GeneralSecurityException.class, () -> handle2.getPrimitive(Aead.class));
+  }
+
+  @Test
+  public void registerBoundWithFakeAwsKms_kmsEnvelopeAeadCanOnlyBeCreatedForBoundedUri()
+      throws Exception {
+    String kekId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekId2 = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+    String kekUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekUri2 =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+
+    AwsKmsClient.registerWithAwsKms(
+        Optional.of(kekUri), Optional.empty(), new FakeAwsKms(asList(kekId, kekId2)));
+
+    KeyTemplate dekTemplate = KeyTemplates.get("AES128_CTR_HMAC_SHA256_RAW");
+    // getPrimitive works for kekUri
+    KeyTemplate envelopeTemplate = KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, dekTemplate);
+    KeysetHandle handle = KeysetHandle.generateNew(envelopeTemplate);
+    Aead unused = handle.getPrimitive(Aead.class);
+
+    // getPrimitive does not work for kekUri2
+    KeyTemplate envelopeTemplate2 =
+        KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri2, dekTemplate);
+    KeysetHandle handle2 = KeysetHandle.generateNew(envelopeTemplate2);
+    assertThrows(GeneralSecurityException.class, () -> handle2.getPrimitive(Aead.class));
+  }
+
+  @Test
+  public void registerTwoBoundWithFakeAwsKms_kmsAeadWorks() throws Exception {
+    String kekId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekId2 = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+    String kekUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekUri2 =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+
+    FakeAwsKms fakeKms = new FakeAwsKms(asList(kekId, kekId2));
+    AwsKmsClient.registerWithAwsKms(Optional.of(kekUri), Optional.empty(), fakeKms);
+    AwsKmsClient.registerWithAwsKms(Optional.of(kekUri2), Optional.empty(), fakeKms);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(kekUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    KeyTemplate kmsTemplate2 = KmsAeadKeyManager.createKeyTemplate(kekUri2);
+    KeysetHandle handle2 = KeysetHandle.generateNew(kmsTemplate2);
+    Aead kmsAead2 = handle2.getPrimitive(Aead.class);
+    byte[] ciphertext2 = kmsAead2.encrypt(plaintext, associatedData);
+    byte[] decrypted2 = kmsAead2.decrypt(ciphertext2, associatedData);
+    assertThat(decrypted2).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void registerUnboundWithFakeAwsKms_kmsAeadWorks() throws Exception {
+    String kekId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+
+    AwsKmsClient.registerWithAwsKms(
+        Optional.empty(), Optional.empty(), new FakeAwsKms(asList(kekId)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(kekUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead aead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void kmsAeadCannotDecryptCiphertextOfDifferentUri() throws Exception {
+    String kekId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekId2 = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+    String kekUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekUri2 =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890xy";
+
+    AwsKmsClient.registerWithAwsKms(
+        Optional.empty(), Optional.empty(), new FakeAwsKms(asList(kekId, kekId2)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(kekUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+
+    KeyTemplate kmsTemplate2 = KmsAeadKeyManager.createKeyTemplate(kekUri2);
+    KeysetHandle handle2 = KeysetHandle.generateNew(kmsTemplate2);
+    Aead kmsAead2 = handle2.getPrimitive(Aead.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> kmsAead2.decrypt(ciphertext, associatedData));
+  }
+
+  @Test
+  public void kmsAeadCanDecryptCiphertextOfAnyUriIfItIsAnAlias() throws Exception {
+    String kekId = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String kekUri =
+        "aws-kms://arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+    String aliasUri = "aws-kms://arn:aws:kms:us-west-2:111122223333:alias/ExampleAlias";
+
+    AwsKmsClient.registerWithAwsKms(
+        Optional.empty(), Optional.empty(), new FakeAwsKms(asList(kekId)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(kekUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+
+    // TODO(b/242678738) This behavior is too general, we should change that.
+    KeyTemplate aliasKmsTemplate = KmsAeadKeyManager.createKeyTemplate(aliasUri);
+    KeysetHandle aliasHandle = KeysetHandle.generateNew(aliasKmsTemplate);
+    Aead aliasKmsAead = aliasHandle.getPrimitive(Aead.class);
+    byte[] decrypted = aliasKmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void invalidUri_fails() throws Exception {
+    String invalidUri = "aws-kms://@#$%&";
+
+    AwsKmsClient.registerWithAwsKms(
+        Optional.empty(), Optional.empty(), new FakeAwsKms(asList(invalidUri)));
+
+    KeyTemplate template = KmsAeadKeyManager.createKeyTemplate(invalidUri);
+    KeysetHandle handle = KeysetHandle.generateNew(template);
+    assertThrows(IllegalArgumentException.class, () -> handle.getPrimitive(Aead.class));
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/BUILD.bazel
index 5d86c32..6ff70d7 100644
--- a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/BUILD.bazel
@@ -6,12 +6,13 @@
     srcs = ["AwsKmsAeadTest.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_aead",
+        "//src/main/java/com/google/crypto/tink/integration/awskms:fake_aws_kms",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@maven//:com_amazonaws_aws_java_sdk_core",
         "@maven//:com_amazonaws_aws_java_sdk_kms",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
-        "@maven//:org_mockito_mockito_core",
     ],
 )
 
@@ -20,10 +21,32 @@
     size = "small",
     srcs = ["AwsKmsClientTest.java"],
     deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:kms_client",
         "//src/main/java/com/google/crypto/tink:kms_clients",
         "//src/main/java/com/google/crypto/tink:kms_clients_test_util",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_client",
+        "//src/main/java/com/google/crypto/tink/integration/awskms:fake_aws_kms",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "FakeAwsKmsTest",
+    size = "small",
+    srcs = ["FakeAwsKmsTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/integration/awskms:fake_aws_kms",
+        "@maven//:com_amazonaws_aws_java_sdk_core",
+        "@maven//:com_amazonaws_aws_java_sdk_kms",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/awskms/FakeAwsKmsTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/FakeAwsKmsTest.java
new file mode 100644
index 0000000..7f27536
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/awskms/FakeAwsKmsTest.java
@@ -0,0 +1,156 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package com.google.crypto.tink.integration.awskms;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertThrows;
+
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.services.kms.AWSKMS;
+import com.amazonaws.services.kms.model.DecryptRequest;
+import com.amazonaws.services.kms.model.DecryptResult;
+import com.amazonaws.services.kms.model.EncryptRequest;
+import com.amazonaws.services.kms.model.EncryptResult;
+import com.google.crypto.tink.aead.AeadConfig;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class FakeAwsKmsTest {
+
+  private static final String KEY_ID =
+      "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab";
+  private static final String KEY_ID_2 = "arn:aws:kms:us-west-2:123:key/different";
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+  }
+
+  @Test
+  public void testEncryptDecryptWithValidKeyId_success() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ID));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    Map<String, String> context = new HashMap<>();
+    context.put("name", "value");
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .withKeyId(KEY_ID)
+            .withPlaintext(ByteBuffer.wrap(plaintext))
+            .withEncryptionContext(context);
+    EncryptResult encResult = kms.encrypt(encRequest);
+    assertThat(encResult.getKeyId()).isEqualTo(KEY_ID);
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .withCiphertextBlob(ByteBuffer.wrap(encResult.getCiphertextBlob().array()))
+            .withEncryptionContext(context);
+
+    DecryptResult decResult = kms.decrypt(decRequest);
+    assertThat(decResult.getKeyId()).isEqualTo(KEY_ID);
+    assertThat(decResult.getPlaintext().array()).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void testEncryptWithInvalidKeyId_fails() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ID));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    Map<String, String> context = new HashMap<>();
+    context.put("name", "value");
+
+    EncryptRequest encRequestWithDifferentKeyArn =
+        new EncryptRequest()
+            .withKeyId(KEY_ID_2)
+            .withPlaintext(ByteBuffer.wrap(plaintext))
+            .withEncryptionContext(context);
+    assertThrows(AmazonServiceException.class, () -> kms.encrypt(encRequestWithDifferentKeyArn));
+  }
+
+  @Test
+  public void testDecryptWithInvalidKeyId_fails() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ID));
+
+    byte[] invalidCiphertext = "invalid".getBytes(UTF_8);
+
+    Map<String, String> context = new HashMap<>();
+    context.put("name", "value");
+
+    DecryptRequest decRequestWithInvalidCiphertext =
+        new DecryptRequest()
+            .withCiphertextBlob(ByteBuffer.wrap(invalidCiphertext))
+            .withEncryptionContext(context);
+    assertThrows(AmazonServiceException.class, () -> kms.decrypt(decRequestWithInvalidCiphertext));
+  }
+
+
+  @Test
+  public void testEncryptDecryptWithTwoValidKeyId_success() throws Exception {
+    AWSKMS kms = new FakeAwsKms(asList(KEY_ID, KEY_ID_2));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] plaintext2 = "plaintext2".getBytes(UTF_8);
+
+    Map<String, String> context = new HashMap<>();
+    context.put("name", "value");
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .withKeyId(KEY_ID)
+            .withPlaintext(ByteBuffer.wrap(plaintext))
+            .withEncryptionContext(context);
+    EncryptResult encResult = kms.encrypt(encRequest);
+    assertThat(encResult.getKeyId()).isEqualTo(KEY_ID);
+
+    EncryptRequest encRequest2 =
+        new EncryptRequest()
+            .withKeyId(KEY_ID_2)
+            .withPlaintext(ByteBuffer.wrap(plaintext2))
+            .withEncryptionContext(context);
+    EncryptResult encResult2 = kms.encrypt(encRequest2);
+    assertThat(encResult2.getKeyId()).isEqualTo(KEY_ID_2);
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .withCiphertextBlob(ByteBuffer.wrap(encResult.getCiphertextBlob().array()))
+            .withEncryptionContext(context);
+
+    DecryptResult decResult = kms.decrypt(decRequest);
+    assertThat(decResult.getKeyId()).isEqualTo(KEY_ID);
+    assertThat(decResult.getPlaintext().array()).isEqualTo(plaintext);
+
+    DecryptRequest decRequest2 =
+        new DecryptRequest()
+            .withCiphertextBlob(ByteBuffer.wrap(encResult2.getCiphertextBlob().array()))
+            .withEncryptionContext(context);
+
+    DecryptResult decResult2 = kms.decrypt(decRequest2);
+    assertThat(decResult2.getKeyId()).isEqualTo(KEY_ID_2);
+    assertThat(decResult2.getPlaintext().array()).isEqualTo(plaintext2);
+  }
+
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel
index a51543a..2817c77 100644
--- a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/BUILD.bazel
@@ -4,14 +4,19 @@
     name = "GcpKmsClientTest",
     size = "small",
     srcs = ["GcpKmsClientTest.java"],
-    data = ["//testdata/gcp:credential.json"],
-    runtime_deps = [
-        "@maven//:com_fasterxml_jackson_core_jackson_core",
-    ],
+    data = ["//testdata/gcp:credentials"],
     deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:kms_client",
         "//src/main/java/com/google/crypto/tink:kms_clients",
         "//src/main/java/com/google/crypto/tink:kms_clients_test_util",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/integration/gcpkms:fake_cloud_kms",
         "//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -19,9 +24,9 @@
 )
 
 java_test(
-    name = "KmsAeadKeyManagerWithGcpTest",
+    name = "GcpKmsIntegrationTest",
     size = "small",
-    srcs = ["KmsAeadKeyManagerWithGcpTest.java"],
+    srcs = ["GcpKmsIntegrationTest.java"],
     data = ["//testdata/gcp:credentials"],
     tags = [
         "manual",
@@ -30,33 +35,47 @@
     ],
     deps = [
         "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:binary_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/aead:kms_aead_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:kms_envelope_aead_key_manager",
         "//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
 
 java_test(
-    name = "KmsEnvelopeAeadKeyManagerWithGcpTest",
+    name = "FakeCloudKmsTest",
     size = "small",
-    srcs = ["KmsEnvelopeAeadKeyManagerWithGcpTest.java"],
-    data = ["//testdata/gcp:credentials"],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
+    srcs = ["FakeCloudKmsTest.java"],
     deps = [
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:aead",
-        "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
-        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
-        "//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "//src/main/java/com/google/crypto/tink/integration/gcpkms:fake_cloud_kms",
+        "@maven//:com_google_apis_google_api_services_cloudkms",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "GcpKmsAeadTest",
+    size = "small",
+    srcs = ["GcpKmsAeadTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/integration/gcpkms:fake_cloud_kms",
+        "//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_aead",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/FakeCloudKmsTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/FakeCloudKmsTest.java
new file mode 100644
index 0000000..a851b47
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/FakeCloudKmsTest.java
@@ -0,0 +1,263 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package com.google.crypto.tink.integration.gcpkms;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.services.cloudkms.v1.CloudKMS;
+import com.google.api.services.cloudkms.v1.model.DecryptRequest;
+import com.google.api.services.cloudkms.v1.model.DecryptResponse;
+import com.google.api.services.cloudkms.v1.model.EncryptRequest;
+import com.google.api.services.cloudkms.v1.model.EncryptResponse;
+import com.google.crypto.tink.aead.AeadConfig;
+import java.io.IOException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class FakeCloudKmsTest {
+
+  private static final String KEY_ID =
+      "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+  private static final String KEY_ID_2 =
+      "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key-2";
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+  }
+
+  @Test
+  public void testEncryptDecryptWithValidKeyId_success() throws Exception {
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    EncryptResponse encResponse =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID, encRequest).execute();
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .encodeCiphertext(encResponse.decodeCiphertext())
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    DecryptResponse decResponse =
+        kms.projects().locations().keyRings().cryptoKeys().decrypt(KEY_ID, decRequest).execute();
+
+    assertThat(decResponse.decodePlaintext()).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void encryptEmptyData_decryptReturnsNull() throws Exception {
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID));
+
+    byte[] plaintext = "".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    EncryptResponse encResponse =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID, encRequest).execute();
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .encodeCiphertext(encResponse.decodeCiphertext())
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    DecryptResponse decResponse =
+        kms.projects().locations().keyRings().cryptoKeys().decrypt(KEY_ID, decRequest).execute();
+
+    assertThat(decResponse.decodePlaintext()).isNull();
+  }
+
+  @Test
+  public void testEncryptWithUnknownKeyId_executeFails() throws Exception {
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    // The RPC to the KMS is done when execute is called. Therefore, calling encrypt for a
+    // valid but unknown key id does not (yet) fail.
+    CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Encrypt enc =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID_2, encRequest);
+    assertThrows(IOException.class, enc::execute);
+  }
+
+  @Test
+  public void testEncryptWithInvalidKeyId_encryptFails() throws Exception {
+    String invalidId = "invalid";
+
+    CloudKMS kms = new FakeCloudKms(asList(invalidId));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    // Encrypts validates the format of the key id, and fails if it is invalid.
+    CloudKMS.Projects.Locations.KeyRings.CryptoKeys cryptoKeys =
+        kms.projects().locations().keyRings().cryptoKeys();
+    assertThrows(IllegalArgumentException.class, () -> cryptoKeys.encrypt(invalidId, encRequest));
+  }
+
+  @Test
+  public void testDecryptWithInvalidKeyId_decryptFails() throws Exception {
+    String invalidId = "invalid";
+
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID, invalidId));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    EncryptResponse encResponse =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID, encRequest).execute();
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .encodeCiphertext(encResponse.decodeCiphertext())
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    // Decrypt validates the format of the key id, and fails if it is invalid.
+    CloudKMS.Projects.Locations.KeyRings.CryptoKeys cryptoKeys =
+        kms.projects().locations().keyRings().cryptoKeys();
+    assertThrows(IllegalArgumentException.class, () -> cryptoKeys.decrypt(invalidId, decRequest));
+  }
+
+
+  @Test
+  public void testDecryptWithWrongKeyId_decryptFails() throws Exception {
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID, KEY_ID_2));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    EncryptResponse encResponse =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID, encRequest).execute();
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .encodeCiphertext(encResponse.decodeCiphertext())
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    // The RPC to the KMS is done when execute is called. Therefore, calling decrypt with a wrong
+    // key id does not (yet) fail.
+    CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Decrypt dec =
+        kms.projects()
+            .locations()
+            .keyRings()
+            .cryptoKeys()
+            .decrypt(KEY_ID_2, decRequest);
+    assertThrows(IOException.class, dec::execute);
+  }
+
+  @Test
+  public void testDecryptExecuteWithInvalidCiphertext_executeFails() throws Exception {
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID));
+
+    byte[] invalidCiphertext = "invalid".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    DecryptRequest decRequestWithInvalidCiphertext =
+        new DecryptRequest()
+            .encodeCiphertext(invalidCiphertext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Decrypt dec =
+        kms.projects()
+            .locations()
+            .keyRings()
+            .cryptoKeys()
+            .decrypt(KEY_ID, decRequestWithInvalidCiphertext);
+    assertThrows(IOException.class, dec::execute);
+  }
+
+  @Test
+  public void testEncryptDecryptWithTwoValidKeyId_success() throws Exception {
+    CloudKMS kms = new FakeCloudKms(asList(KEY_ID, KEY_ID_2));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] plaintext2 = "plaintext2".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    EncryptRequest encRequest =
+        new EncryptRequest()
+            .encodePlaintext(plaintext)
+            .encodeAdditionalAuthenticatedData(associatedData);
+    EncryptResponse encResponse =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID, encRequest).execute();
+
+    EncryptRequest encRequest2 =
+        new EncryptRequest()
+            .encodePlaintext(plaintext2)
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    EncryptResponse encResponse2 =
+        kms.projects().locations().keyRings().cryptoKeys().encrypt(KEY_ID, encRequest2).execute();
+
+    DecryptRequest decRequest =
+        new DecryptRequest()
+            .encodeCiphertext(encResponse.decodeCiphertext())
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    DecryptResponse decResponse =
+        kms.projects().locations().keyRings().cryptoKeys().decrypt(KEY_ID, decRequest).execute();
+
+    assertThat(decResponse.decodePlaintext()).isEqualTo(plaintext);
+
+    DecryptRequest decRequest2 =
+        new DecryptRequest()
+            .encodeCiphertext(encResponse2.decodeCiphertext())
+            .encodeAdditionalAuthenticatedData(associatedData);
+
+    DecryptResponse decResponse2 =
+        kms.projects().locations().keyRings().cryptoKeys().decrypt(KEY_ID, decRequest2).execute();
+
+    assertThat(decResponse2.decodePlaintext()).isEqualTo(plaintext2);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAeadTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAeadTest.java
new file mode 100644
index 0000000..b8201bd
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsAeadTest.java
@@ -0,0 +1,100 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.integration.gcpkms;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.aead.AeadConfig;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class GcpKmsAeadTest {
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+  }
+
+  @Test
+  public void kmsAead_works() throws Exception {
+    String keyName = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    FakeCloudKms fakeKms = new FakeCloudKms(asList(keyName));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    Aead kmsAead = new GcpKmsAead(fakeKms, keyName);
+
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    byte[] associatedData2 = "associatedData2".getBytes(UTF_8);
+    assertThrows(
+        GeneralSecurityException.class, () -> kmsAead.decrypt(ciphertext, associatedData2));
+
+    ciphertext[7] = (byte) (ciphertext[7] ^ 42);
+    assertThrows(GeneralSecurityException.class, () -> kmsAead.decrypt(ciphertext, associatedData));
+  }
+
+  @Test
+  public void kmsAead_encryptDecryptEmptyString_success() throws Exception {
+    String keyName = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    FakeCloudKms fakeKms = new FakeCloudKms(asList(keyName));
+
+    byte[] plaintext = "".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    Aead kmsAead = new GcpKmsAead(fakeKms, keyName);
+
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void twoKmsAeads_canOnlyDecryptTheirOwnCiphertext() throws Exception {
+    String keyName = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyName2 = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+    FakeCloudKms fakeKms = new FakeCloudKms(asList(keyName, keyName2));
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    Aead kmsAead = new GcpKmsAead(fakeKms, keyName);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+
+    Aead kmsAead2 = new GcpKmsAead(fakeKms, keyName2);
+    byte[] ciphertext2 = kmsAead2.encrypt(plaintext, associatedData);
+
+    assertThat(kmsAead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+    assertThat(kmsAead2.decrypt(ciphertext2, associatedData)).isEqualTo(plaintext);
+
+    assertThrows(
+        GeneralSecurityException.class, () -> kmsAead2.decrypt(ciphertext, associatedData));
+    assertThrows(
+        GeneralSecurityException.class, () -> kmsAead.decrypt(ciphertext2, associatedData));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClientTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClientTest.java
index 315847d..4c73345 100644
--- a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClientTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsClientTest.java
@@ -17,30 +17,46 @@
 package com.google.crypto.tink.integration.gcpkms;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KmsClient;
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.KmsClientsTestUtil;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.KmsAeadKeyManager;
+import com.google.crypto.tink.aead.KmsEnvelopeAeadKeyManager;
+import java.security.GeneralSecurityException;
 import java.util.Optional;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests for GcpKmsClient. */
+/** Unit tests for {@link GcpKmsClient}.*/
 @RunWith(JUnit4.class)
 public final class GcpKmsClientTest {
   private static final String CREDENTIAL_FILE_PATH =
       "testdata/gcp/credential.json";
 
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    AeadConfig.register();
+  }
+
   @Before
   public void setUp() {
     KmsClientsTestUtil.reset();
   }
 
   @Test
-  public void register() throws Exception {
+  public void registerWithKeyUriAndCredentials_success() throws Exception {
     // Register a client bound to a single key.
     String keyUri = "gcp-kms://register";
     GcpKmsClient.register(Optional.of(keyUri), Optional.of(CREDENTIAL_FILE_PATH));
@@ -53,7 +69,7 @@
   }
 
   @Test
-  public void register_unbound() throws Exception {
+  public void registerOnlyWithCredentials_success() throws Exception {
     // Register an unbound client.
     GcpKmsClient.register(Optional.empty(), Optional.of(CREDENTIAL_FILE_PATH));
 
@@ -67,9 +83,227 @@
   }
 
   @Test
-  public void register_badKeyUri_fail() throws Exception {
+  public void registerWithCredentialsAndBadKeyUri_fail() throws Exception {
     assertThrows(
         IllegalArgumentException.class,
         () -> GcpKmsClient.register(Optional.of("blah"), Optional.of(CREDENTIAL_FILE_PATH)));
   }
+
+  @SuppressWarnings("deprecation") // We can't use register because we need to inject a FakeCloudKms
+  private void registerGcpKmsClient(FakeCloudKms cloudKms) {
+    KmsClients.add(new GcpKmsClient().withCloudKms(cloudKms));
+  }
+
+  @SuppressWarnings("deprecation") // We can't use register because we need to inject a FakeCloudKms
+  private void registerGcpKmsClient(String keyUri, FakeCloudKms cloudKms) {
+    KmsClients.add(new GcpKmsClient(keyUri).withCloudKms(cloudKms));
+  }
+
+  @Test
+  public void registerWithKeyUriAndFakeCloudKms_kmsAeadWorks() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+
+    // Register a client bound to a single key.
+    registerGcpKmsClient(keyUri, new FakeCloudKms(asList(keyId)));
+
+    // Create a KmsAead primitive
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void registerWithKeyUriAndFakeCloudKms_kmsEnvelopeAeadWorks() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+
+    // Register a client bound to a single key.
+    registerGcpKmsClient(keyUri, new FakeCloudKms(asList(keyId)));
+
+    // Create an envelope encryption AEAD primitive
+    KeyTemplate dekTemplate = KeyTemplates.get("AES128_CTR_HMAC_SHA256_RAW");
+    KeyTemplate envelopeTemplate = KmsEnvelopeAeadKeyManager.createKeyTemplate(keyUri, dekTemplate);
+    KeysetHandle handle = KeysetHandle.generateNew(envelopeTemplate);
+    Aead kmsEnvelopeAead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsEnvelopeAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsEnvelopeAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void registerWithKeyUriAndFakeCloudKms_kmsAeadCanOnlyBeCreatedForRegisteredKeyUri()
+      throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyId2 = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri2 =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+
+    registerGcpKmsClient(keyUri, new FakeCloudKms(asList(keyId, keyId2)));
+
+    // getPrimitive works for keyUri
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead unused = handle.getPrimitive(Aead.class);
+
+    // getPrimitive does not work for keyUri2
+    KeyTemplate kmsTemplate2 = KmsAeadKeyManager.createKeyTemplate(keyUri2);
+    KeysetHandle handle2 = KeysetHandle.generateNew(kmsTemplate2);
+    assertThrows(GeneralSecurityException.class, () -> handle2.getPrimitive(Aead.class));
+  }
+
+  @Test
+  public void registerWithKeyUriAndFakeCloudKms_kmsEnvelopeAeadCanOnlyBeCreatedForBoundedUri()
+      throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyId2 = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri2 =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+
+    registerGcpKmsClient(keyUri, new FakeCloudKms(asList(keyId, keyId2)));
+
+    KeyTemplate dekTemplate = KeyTemplates.get("AES128_CTR_HMAC_SHA256_RAW");
+    // getPrimitive works for keyUri
+    KeyTemplate envelopeTemplate = KmsEnvelopeAeadKeyManager.createKeyTemplate(keyUri, dekTemplate);
+    KeysetHandle handle = KeysetHandle.generateNew(envelopeTemplate);
+    Aead unused = handle.getPrimitive(Aead.class);
+
+    // getPrimitive does not work for keyUri2
+    KeyTemplate envelopeTemplate2 =
+        KmsEnvelopeAeadKeyManager.createKeyTemplate(keyUri2, dekTemplate);
+    KeysetHandle handle2 = KeysetHandle.generateNew(envelopeTemplate2);
+    assertThrows(GeneralSecurityException.class, () -> handle2.getPrimitive(Aead.class));
+  }
+
+  @Test
+  public void registerWithTwoKeyUriAndFakeCloudKms_kmsAeadWorks() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyId2 = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri2 =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+
+    FakeCloudKms fakeKms = new FakeCloudKms(asList(keyId, keyId2));
+    registerGcpKmsClient(keyUri, fakeKms);
+    registerGcpKmsClient(keyUri2, fakeKms);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+    byte[] decrypted = kmsAead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    KeyTemplate kmsTemplate2 = KmsAeadKeyManager.createKeyTemplate(keyUri2);
+    KeysetHandle handle2 = KeysetHandle.generateNew(kmsTemplate2);
+    Aead kmsAead2 = handle2.getPrimitive(Aead.class);
+    byte[] ciphertext2 = kmsAead2.encrypt(plaintext, associatedData);
+    byte[] decrypted2 = kmsAead2.decrypt(ciphertext2, associatedData);
+    assertThat(decrypted2).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void registerWithoutKeyUri_kmsAeadWorks() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+
+    registerGcpKmsClient(new FakeCloudKms(asList(keyId)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead aead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void kmsAead_encryptDecryptEmptyString_success() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+
+    registerGcpKmsClient(new FakeCloudKms(asList(keyId)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead aead = handle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void kmsAeadCannotDecryptCiphertextOfDifferentUri() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyId2 = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri2 =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key2";
+
+    registerGcpKmsClient(new FakeCloudKms(asList(keyId, keyId2)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+
+    KeyTemplate kmsTemplate2 = KmsAeadKeyManager.createKeyTemplate(keyUri2);
+    KeysetHandle handle2 = KeysetHandle.generateNew(kmsTemplate2);
+    Aead kmsAead2 = handle2.getPrimitive(Aead.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> kmsAead2.decrypt(ciphertext, associatedData));
+  }
+
+  @Test
+  public void kmsAeadCannotDecryptCiphertextOfDifferentUriIfItIsHasAnInvalidUri() throws Exception {
+    String keyId = "projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String keyUri =
+        "gcp-kms://projects/tink-test/locations/global/keyRings/unit-test/cryptoKeys/aead-key";
+    String invalidUri = "gcp-kms://@#$%&";
+
+    registerGcpKmsClient(new FakeCloudKms(asList(keyId)));
+
+    KeyTemplate kmsTemplate = KmsAeadKeyManager.createKeyTemplate(keyUri);
+    KeysetHandle handle = KeysetHandle.generateNew(kmsTemplate);
+    Aead kmsAead = handle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = kmsAead.encrypt(plaintext, associatedData);
+
+    KeyTemplate templateWithInvalidUri = KmsAeadKeyManager.createKeyTemplate(invalidUri);
+    KeysetHandle handleWithInvalidUri = KeysetHandle.generateNew(templateWithInvalidUri);
+    Aead kmsAeadWithInvalidUri = handleWithInvalidUri.getPrimitive(Aead.class);
+    assertThrows(IllegalArgumentException.class,
+        () -> kmsAeadWithInvalidUri.decrypt(ciphertext, associatedData));
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsIntegrationTest.java
new file mode 100644
index 0000000..a5d8572
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/GcpKmsIntegrationTest.java
@@ -0,0 +1,239 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.integration.gcpkms;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.BinaryKeysetReader;
+import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.KmsAeadKeyManager;
+import com.google.crypto.tink.aead.KmsEnvelopeAeadKeyManager;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.testing.TestUtil;
+import java.security.GeneralSecurityException;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for Tink's GcpKmsClient with the real GCP Cloud KMS. */
+@RunWith(JUnit4.class)
+public class GcpKmsIntegrationTest {
+
+  // A valid GCP KMS AEAD key URI.
+  // It is restricted to the service account in {@link
+  // com.google.crypto.tink.testing.TestUtil#SERVICE_ACCOUNT_FILE}.
+  private static final String GCP_KMS_TEST_KEY_URI =
+      "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/"
+          + "unit-and-integration-testing/cryptoKeys/aead-key";
+
+  // Another valid GCP KMS AEAD key URI in the same key ring as {@link #GCP_KMS_TEST_KEY_URI}.
+  // It is restricted to the service account in {@link
+  // com.google.crypto.tink.testing.TestUtil#SERVICE_ACCOUNT_FILE}.
+  private static final String GCP_KMS_TEST_KEY_URI_2 =
+      "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/"
+          + "unit-and-integration-testing/cryptoKeys/aead2-key";
+
+  @Before
+  public void setUp() throws Exception {
+    GcpKmsClient.register(Optional.empty(), Optional.of(TestUtil.SERVICE_ACCOUNT_FILE));
+    AeadConfig.register();
+  }
+
+  @Test
+  public void kmsAead_encryptDecrypt() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(GCP_KMS_TEST_KEY_URI));
+
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    byte[] invalid = "invalid".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(ciphertext, invalid));
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(invalid, associatedData));
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(empty, associatedData));
+    assertThat(aead.decrypt(aead.encrypt(empty, associatedData), associatedData)).isEqualTo(empty);
+    assertThat(aead.decrypt(aead.encrypt(plaintext, empty), empty)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void kmsAeadEncryptAndDecryptWithoutAssociatedData_success() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(GCP_KMS_TEST_KEY_URI));
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    byte[] ciphertextWithNullAd = aead.encrypt(plaintext, null);
+    byte[] ciphertextWithEmptyAd = aead.encrypt(plaintext, empty);
+
+    // Null and empty associated data should be treated the same.
+    assertThat(aead.decrypt(ciphertextWithNullAd, null)).isEqualTo(plaintext);
+    assertThat(aead.decrypt(ciphertextWithNullAd, empty)).isEqualTo(plaintext);
+    assertThat(aead.decrypt(ciphertextWithEmptyAd, null)).isEqualTo(plaintext);
+    assertThat(aead.decrypt(ciphertextWithEmptyAd, empty)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void kmsAeadDecryptWithDifferentKeyUri_fails() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(GCP_KMS_TEST_KEY_URI));
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(GCP_KMS_TEST_KEY_URI_2));
+    Aead aead2 = keysetHandle2.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] ciphertext2 = aead2.encrypt(plaintext, associatedData);
+
+    // Ciphertexts are valid.
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+    assertThat(aead2.decrypt(ciphertext2, associatedData)).isEqualTo(plaintext);
+
+    // Ciphertexts cannot be decrypted using a different key URI.
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(ciphertext2, associatedData));
+    assertThrows(GeneralSecurityException.class, () -> aead2.decrypt(ciphertext, associatedData));
+  }
+
+  @Test
+  public void kmsEnvelopeAead_encryptDecrypt() throws Exception {
+    KeyTemplate envelopeTemplate =
+        KmsEnvelopeAeadKeyManager.createKeyTemplate(
+            GCP_KMS_TEST_KEY_URI, KeyTemplates.get("AES128_CTR_HMAC_SHA256"));
+    KeysetHandle keysetHandle = KeysetHandle.generateNew(envelopeTemplate);
+
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] decrypted = aead.decrypt(ciphertext, associatedData);
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    byte[] invalid = "invalid".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(ciphertext, invalid));
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(invalid, associatedData));
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(empty, associatedData));
+    assertThat(aead.decrypt(aead.encrypt(empty, associatedData), associatedData)).isEqualTo(empty);
+    assertThat(aead.decrypt(aead.encrypt(plaintext, empty), empty)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void kmsEnvelopeAeadDecryptWithDifferentKeyUri_fails() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.generateNew(
+            KmsEnvelopeAeadKeyManager.createKeyTemplate(
+                GCP_KMS_TEST_KEY_URI, KeyTemplates.get("AES128_CTR_HMAC_SHA256")));
+    Aead aead = keysetHandle.getPrimitive(Aead.class);
+
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.generateNew(
+            KmsEnvelopeAeadKeyManager.createKeyTemplate(
+                GCP_KMS_TEST_KEY_URI_2, KeyTemplates.get("AES128_CTR_HMAC_SHA256")));
+    Aead aead2 = keysetHandle2.getPrimitive(Aead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    byte[] ciphertext2 = aead2.encrypt(plaintext, associatedData);
+
+    // Ciphertexts are valid.
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+    assertThat(aead2.decrypt(ciphertext2, associatedData)).isEqualTo(plaintext);
+
+    // Ciphertexts cannot be decrypted using a different key URI.
+    assertThrows(GeneralSecurityException.class, () -> aead.decrypt(ciphertext2, associatedData));
+    assertThrows(GeneralSecurityException.class, () -> aead2.decrypt(ciphertext, associatedData));
+  }
+
+  @Test
+  public void decryptCiphertextEncryptedInBigQueryUsingAWrappedKeyset() throws Exception {
+    String keyUri =
+        "gcp-kms://projects/tink-test-infrastructure/locations/us/keyRings/"
+            + "big-query-test-key/cryptoKeys/aead-key";
+
+    // This wrapped keyset was generated in BigQuery using this command:
+    // DECLARE kms_key_uri STRING;
+    // SET kms_key_uri =
+    // 'gcp-kms://projects/tink-test-infrastructure/locations/us/keyRings/big-query-test-key/cryptoKeys/aead-key';
+    // SELECT KEYS.NEW_WRAPPED_KEYSET(kms_key_uri, 'AEAD_AES_GCM_256')
+    byte[] wrappedKeyset =
+        Base64.decode(
+            "CiQAv82D2I7RT2gQRd/01m+Md8WAmOyehVog50vs5uPq2B+R36YSlQEAba+J9rC0gfmX9FSs8P"
+                + "sWIpCVbIvPiflsaHRxq5GQjknVgYuJLIMDXlGhQBa3NrfJSmj1T/KDHQ3EzCcPAXtOAbAExZr/"
+                + "7jsgiCzo/YQINyPb2rGkW4ofo/BVyvhZ/Pk40iuPHv8Q/PXVrNsq3Y2vkkpsyb3QUhJZseURGj"
+                + "jeQnZde6i3EmvDejXhOZJ3XdQUjwgorA==");
+
+    // This ciphertext was generated in BigQuery using this command:
+    // DECLARE kms_key_uri STRING;
+    // DECLARE wrapped_key BYTES;
+    // SET kms_key_uri =
+    // 'gcp-kms://projects/tink-test-infrastructure/locations/us/keyRings/big-query-test-key/cryptoKeys/aead-key';
+    // SET wrapped_key =
+    // FROM_BASE64('CiQAv82D2I7RT2gQRd/01m+Md8WAmOyehVog50vs5uPq2B+R36YSlQEAba+J9rC0gfmX9FSs8PsWIpCVbIvPiflsaHRxq5GQjknVgYuJLIMDXlGhQBa3NrfJSmj1T/KDHQ3EzCcPAXtOAbAExZr/7jsgiCzo/YQINyPb2rGkW4ofo/BVyvhZ/Pk40iuPHv8Q/PXVrNsq3Y2vkkpsyb3QUhJZseURGjjeQnZde6i3EmvDejXhOZJ3XdQUjwgorA==');
+    // SELECT AEAD.ENCRYPT(KEYS.KEYSET_CHAIN(kms_key_uri, wrapped_key), 'elephant', 'animal')
+    //     AS encrypted_animal;
+    byte[] ciphertext = Base64.decode("AcpCNBevmQnr9momhlKKEyKDOCj5bMfizqC22N/hLZd58LFpC+r99C0=");
+
+    KeysetHandle kmsKeysetHandle =
+        KeysetHandle.generateNew(KmsAeadKeyManager.createKeyTemplate(keyUri));
+    Aead kmsAead = kmsKeysetHandle.getPrimitive(Aead.class);
+
+    byte[] unwrappedKeyset =
+        kmsAead.decrypt(wrappedKeyset, /* associatedData= */ "".getBytes(UTF_8));
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseKeyset(unwrappedKeyset, InsecureSecretKeyAccess.get());
+    Aead aead = handle.getPrimitive(Aead.class);
+    byte[] decrypted = aead.decrypt(ciphertext, /* associatedData= */ "animal".getBytes(UTF_8));
+    assertThat(decrypted).isEqualTo("elephant".getBytes(UTF_8));
+
+    // CleartextKeysetHandle and BinaryKeysetReader instead of TinkProtoKeysetFormat also works
+    KeysetHandle handle2 =
+        CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(unwrappedKeyset));
+    Aead aead2 = handle2.getPrimitive(Aead.class);
+    byte[] decrypted2 = aead2.decrypt(ciphertext, /* associatedData= */ "animal".getBytes(UTF_8));
+    assertThat(decrypted2).isEqualTo("elephant".getBytes(UTF_8));
+
+    // BigQuery's wrappedKeyset is not compatible with TinkProtoKeysetFormat.parseEncryptedKeyset
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            TinkProtoKeysetFormat.parseEncryptedKeyset(wrappedKeyset, kmsAead, "".getBytes(UTF_8)));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/KmsAeadKeyManagerWithGcpTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/KmsAeadKeyManagerWithGcpTest.java
deleted file mode 100644
index b851465..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/KmsAeadKeyManagerWithGcpTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.integration.gcpkms;
-
-import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.aead.AeadConfig;
-import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.testing.TestUtil;
-import java.util.Optional;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for KmsAeadKeyManager using {@code GcpKmsClient}. */
-@RunWith(JUnit4.class)
-public class KmsAeadKeyManagerWithGcpTest {
-  @Before
-  public void setUp() throws Exception {
-    GcpKmsClient.register(Optional.empty(), Optional.of(TestUtil.SERVICE_ACCOUNT_FILE));
-    AeadConfig.register();
-  }
-
-  @Test
-  public void testGcpKmsKeyRestricted() throws Exception {
-    KeysetHandle keysetHandle =
-        KeysetHandle.generateNew(
-            AeadKeyTemplates.createKmsAeadKeyTemplate(TestUtil.RESTRICTED_CRYPTO_KEY_URI));
-    TestUtil.runBasicAeadTests(keysetHandle.getPrimitive(Aead.class));
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/KmsEnvelopeAeadKeyManagerWithGcpTest.java b/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/KmsEnvelopeAeadKeyManagerWithGcpTest.java
deleted file mode 100644
index 064c391..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/integration/gcpkms/KmsEnvelopeAeadKeyManagerWithGcpTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.integration.gcpkms;
-
-import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.aead.AeadConfig;
-import com.google.crypto.tink.aead.AeadKeyTemplates;
-import com.google.crypto.tink.proto.KeyTemplate;
-import com.google.crypto.tink.testing.TestUtil;
-import java.util.Optional;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Tests for {@code KmsEnvelopeAead} and {@code KmsEnvelopeAeadKeyManager} using {@code
- * GcpKmsClient}.
- */
-@RunWith(JUnit4.class)
-public class KmsEnvelopeAeadKeyManagerWithGcpTest {
-  @BeforeClass
-  public static void setUp() throws Exception {
-    GcpKmsClient.register(Optional.empty(), Optional.of(TestUtil.SERVICE_ACCOUNT_FILE));
-    AeadConfig.register();
-  }
-
-  @Test
-  public void testGcpKmsKeyRestricted() throws Exception {
-    KeyTemplate dekTemplate = AeadKeyTemplates.AES128_CTR_HMAC_SHA256;
-    KeysetHandle keysetHandle =
-        KeysetHandle.generateNew(
-            AeadKeyTemplates.createKmsEnvelopeAeadKeyTemplate(
-                TestUtil.RESTRICTED_CRYPTO_KEY_URI, dekTemplate));
-    TestUtil.runBasicAeadTests(keysetHandle.getPrimitive(Aead.class));
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/internal/BUILD.bazel
index e815d18..21088fd 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/BUILD.bazel
@@ -11,7 +11,7 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
         "//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -39,7 +39,7 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/internal:primitive_factory",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -119,7 +119,7 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/util:bytes",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -186,7 +186,7 @@
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_parameters",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -202,7 +202,7 @@
         "//src/main/java/com/google/crypto/tink:secret_key_access",
         "//src/main/java/com/google/crypto/tink/internal:legacy_proto_key",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -231,9 +231,9 @@
         "//src/main/java/com/google/crypto/tink/internal:serialization",
         "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/util:bytes",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -281,6 +281,210 @@
     srcs = ["TinkBugExceptionTest.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JsonParserTest",
+    size = "small",
+    srcs = ["JsonParserTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:json_parser",
+        "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PrimitiveRegistryTest",
+    size = "small",
+    srcs = ["PrimitiveRegistryTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_registry",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PrimitiveConstructorTest",
+    size = "small",
+    srcs = ["PrimitiveConstructorTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "MutablePrimitiveRegistryMultithreadTest",
+    size = "small",
+    srcs = ["MutablePrimitiveRegistryMultithreadTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "MutablePrimitiveRegistryTest",
+    size = "small",
+    srcs = ["MutablePrimitiveRegistryTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "EllipticCurvesUtilTest",
+    size = "small",
+    srcs = ["EllipticCurvesUtilTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "BigIntegerEncodingTest",
+    size = "small",
+    srcs = ["BigIntegerEncodingTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Curve25519Test",
+    size = "small",
+    srcs = ["Curve25519Test.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:curve25519",
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Ed25519Test",
+    size = "small",
+    srcs = ["Ed25519Test.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:ed25519_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Field25519Test",
+    size = "small",
+    srcs = ["Field25519Test.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:field25519",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "EnumTypeProtoConverterTest",
+    size = "small",
+    srcs = ["EnumTypeProtoConverterTest.java"],
+    deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink/hybrid:hpke_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:enum_type_proto_converter",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "InternalConfigurationTest",
+    size = "small",
+    srcs = ["InternalConfigurationTest.java"],
+    deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:primitive_wrapper",
+        "//src/main/java/com/google/crypto/tink/internal:internal_configuration",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_constructor",
+        "//src/main/java/com/google/crypto/tink/internal:primitive_registry",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RegistryConfigurationTest",
+    size = "small",
+    srcs = ["RegistryConfigurationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_key",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_primitive_registry",
+        "//src/main/java/com/google/crypto/tink/internal:registry_configuration",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/BigIntegerEncodingTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/BigIntegerEncodingTest.java
new file mode 100644
index 0000000..9707408
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/BigIntegerEncodingTest.java
@@ -0,0 +1,217 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class BigIntegerEncodingTest {
+
+  @Test
+  public void toBigEndianBytes() throws Exception {
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.ZERO))
+        .isEqualTo(new byte[] {(byte) 0x00});
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.ONE))
+        .isEqualTo(new byte[] {(byte) 0x01});
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.valueOf(127)))
+        .isEqualTo(new byte[] {(byte) 0x7F});
+    // The most significant bit of the first byte is used to encode the sign of the number.
+    // Therfore, 128 needs to be encoded with two bytes.
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.valueOf(128)))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x80});
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.valueOf(255)))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0xFF});
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.valueOf(256)))
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x00});
+    assertThat(BigIntegerEncoding.toBigEndianBytes(BigInteger.valueOf(258)))
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+  }
+
+  @Test
+  public void toBigEndianBytesOfFixedLength_success() throws Exception {
+    assertThat(BigIntegerEncoding.toBigEndianBytesOfFixedLength(BigInteger.ZERO, /* length= */ 0))
+        .isEqualTo(new byte[] {});
+    assertThat(BigIntegerEncoding.toBigEndianBytesOfFixedLength(BigInteger.ZERO, /* length= */ 1))
+        .isEqualTo(new byte[] {(byte) 0x00});
+    assertThat(BigIntegerEncoding.toBigEndianBytesOfFixedLength(BigInteger.ZERO, /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00});
+    assertThat(BigIntegerEncoding.toBigEndianBytesOfFixedLength(BigInteger.ONE, /* length= */ 1))
+        .isEqualTo(new byte[] {(byte) 0x01});
+    assertThat(BigIntegerEncoding.toBigEndianBytesOfFixedLength(BigInteger.ONE, /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x01});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(127), /* length= */ 1))
+        .isEqualTo(new byte[] {(byte) 0x7F});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(127), /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x7F});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(127), /* length= */ 3))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x7F});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(128), /* length= */ 1))
+        .isEqualTo(new byte[] {(byte) 0x80});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(128), /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x80});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(128), /* length= */ 3))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x80});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(255), /* length= */ 1))
+        .isEqualTo(new byte[] {(byte) 0xFF});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(255), /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0xFF});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(255), /* length= */ 3))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0xFF});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(256), /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x00});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(258), /* length= */ 2))
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+    assertThat(
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(258), /* length= */ 4))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02});
+  }
+
+  @Test
+  public void toBigEndianBytesOfFixedLength_failWhenIntegerIsNegative() throws Exception {
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(-1), /* length= */ 2));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(-42), /* length= */ 2));
+  }
+
+  @Test
+  public void toBigEndianBytesOfFixedLength_failWhenIntegerIsTooLargerForLength() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> BigIntegerEncoding.toBigEndianBytesOfFixedLength(BigInteger.ONE, /* length= */ 0));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(256), /* length= */ 1));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                BigInteger.valueOf(256 * 256), /* length= */ 2));
+  }
+
+  @Test
+  public void fromUnsignedBigEndianBytes() throws Exception {
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0x00}))
+        .isEqualTo(BigInteger.ZERO);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0x01}))
+        .isEqualTo(BigInteger.ONE);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0x7F}))
+        .isEqualTo(BigInteger.valueOf(127));
+    // The input should be interpreted as an unsigned integers. So 0x80 is 128.
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0x80}))
+        .isEqualTo(BigInteger.valueOf(128));
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0xFF}))
+        .isEqualTo(BigInteger.valueOf(255));
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0x01, (byte) 0x00}))
+        .isEqualTo(BigInteger.valueOf(256));
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {(byte) 0x01, (byte) 0x02}))
+        .isEqualTo(BigInteger.valueOf(258));
+    // leading zeros are ignored
+    assertThat(
+            BigIntegerEncoding.fromUnsignedBigEndianBytes(
+                new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02}))
+        .isEqualTo(BigInteger.valueOf(258));
+    // the empty array is decoded to 0.
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(new byte[] {}))
+        .isEqualTo(BigInteger.ZERO);
+  }
+
+  @Test
+  public void toBigEndianBytes_failsForNegativeNumbers() throws Exception {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> BigIntegerEncoding.toBigEndianBytes(BigInteger.valueOf(-1)));
+  }
+
+  private BigInteger fromSignedBigEndianBytes(byte[] bytes) {
+    // the constructor of BigInteger takes a two's-complement big-endian byte array as input.
+    return new BigInteger(bytes);
+  }
+
+  @Test
+  public void toBigEndianBytes_canBeParsedAsSignedOrUnsigned() throws Exception {
+    byte[] encoded0 = BigIntegerEncoding.toBigEndianBytes(BigInteger.ZERO);
+    assertThat(encoded0).hasLength(1);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(encoded0)).isEqualTo(BigInteger.ZERO);
+    assertThat(fromSignedBigEndianBytes(encoded0)).isEqualTo(BigInteger.ZERO);
+
+    // 10 is a 1-byte unsigned integer with the most significant bit 0.
+    byte[] encoded10 = BigIntegerEncoding.toBigEndianBytes(BigInteger.TEN);
+    assertThat(encoded10).hasLength(1);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(encoded10)).isEqualTo(BigInteger.TEN);
+    assertThat(fromSignedBigEndianBytes(encoded10)).isEqualTo(BigInteger.TEN);
+
+    // 130 is a 1-byte unsigned integer with the most significant bit 1.
+    BigInteger bigInt130 = BigInteger.valueOf(130);
+    byte[] encoded130 = BigIntegerEncoding.toBigEndianBytes(bigInt130);
+    assertThat(encoded130).hasLength(2);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(encoded130)).isEqualTo(bigInt130);
+    assertThat(fromSignedBigEndianBytes(encoded130)).isEqualTo(bigInt130);
+
+    // 30000 is a 2-byte unsigned integer with the most significant bit 0.
+    BigInteger bigInt30k = BigInteger.valueOf(30000);
+    byte[] encoded30k = BigIntegerEncoding.toBigEndianBytes(bigInt30k);
+    assertThat(encoded30k).hasLength(2);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(encoded30k)).isEqualTo(bigInt30k);
+    assertThat(fromSignedBigEndianBytes(encoded30k)).isEqualTo(bigInt30k);
+
+    // 60000 is a 2-byte unsigned integer with the most significant bit 1.
+    BigInteger bigInt60k = BigInteger.valueOf(60000);
+    byte[] encoded60k = BigIntegerEncoding.toBigEndianBytes(bigInt60k);
+    assertThat(encoded60k).hasLength(3);
+    assertThat(BigIntegerEncoding.fromUnsignedBigEndianBytes(encoded60k)).isEqualTo(bigInt60k);
+    assertThat(fromSignedBigEndianBytes(encoded60k)).isEqualTo(bigInt60k);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/Curve25519Test.java b/java_src/src/test/java/com/google/crypto/tink/internal/Curve25519Test.java
new file mode 100644
index 0000000..7bb42f5
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/Curve25519Test.java
@@ -0,0 +1,150 @@
+// Copyright 2022 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.truth.Expect;
+import com.google.crypto.tink.subtle.Hex;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Curve25519}. */
+@RunWith(JUnit4.class)
+public final class Curve25519Test {
+
+  @Rule public final Expect expect = Expect.create();
+
+  /** 1st iteration test in Section 5.2 of RFC 7748. https://tools.ietf.org/html/rfc7748 */
+  @Test
+  public void testCurveMult_success() throws Exception {
+    byte[] k = new byte[Field25519.FIELD_LEN];
+    k[0] = 9;
+
+    byte[] e = Arrays.copyOf(k, Field25519.FIELD_LEN);
+    e[0] &= (byte) 248;
+    e[31] &= (byte) 127;
+    e[31] |= (byte) 64;
+
+    long[] x = new long[Field25519.LIMB_CNT + 1];
+
+    Curve25519.curveMult(x, e, k);
+    assertEquals(
+        "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079",
+        Hex.encode(Field25519.contract(x)));
+  }
+
+  /**
+   * 1st iteration test in Section 5.2 of RFC 7748. https://tools.ietf.org/html/rfc7748, but with
+   * the MSB set.
+   */
+  @Test
+  public void testCurveMultWithMbs_ignoresMsbAndDoesNotChangeInput() throws Exception {
+    byte[] kOriginal = new byte[Field25519.FIELD_LEN];
+    kOriginal[0] = 9;
+    kOriginal[31] = (byte) 0x80; // set MSB
+
+    byte[] k = Arrays.copyOf(kOriginal, Field25519.FIELD_LEN);
+    byte[] e = Arrays.copyOf(kOriginal, Field25519.FIELD_LEN);
+    e[0] &= (byte) 248;
+    e[31] &= (byte) 127;
+    e[31] |= (byte) 64;
+
+    long[] x = new long[Field25519.LIMB_CNT + 1];
+
+    Curve25519.curveMult(x, e, k);
+    expect
+        .that(Hex.encode(Field25519.contract(x)))
+        .isEqualTo("422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079");
+    expect.that(k).isEqualTo(kOriginal);
+  }
+
+  private byte[] toLittleEndian(BigInteger n) {
+    byte[] b = new byte[32];
+    byte[] nBytes = n.toByteArray();
+
+    if ((nBytes.length > 33) || (nBytes.length == 33 && nBytes[0] != 0)) {
+      throw new IllegalArgumentException("Number too big");
+    }
+    if (nBytes.length == 33) {
+      System.arraycopy(nBytes, 1, b, 0, 32);
+    } else {
+      System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
+    }
+    for (int i = 0; i < b.length / 2; i++) {
+      byte t = b[i];
+      b[i] = b[b.length - i - 1];
+      b[b.length - i - 1] = t;
+    }
+    return b;
+  }
+
+  @Test
+  public void testBannedPublicKeys_fail() throws Exception {
+    // The values here are taken from https://cr.yp.to/ecdh.html#validate.
+
+    BigInteger two = BigInteger.valueOf(2);
+    BigInteger big25519 = two.pow(255).subtract(BigInteger.valueOf(19));
+    BigInteger big32 =
+        new BigInteger(
+            "325606250916557431795983626356110631294008115727848805560023387167927233504");
+    BigInteger big39 =
+        new BigInteger(
+            "39382357235489614581723060781553021112529911719440698176882885853963445705823");
+
+    byte[] n = toLittleEndian(two);
+    long[] x = new long[Field25519.LIMB_CNT + 1];
+
+    // 0
+    assertThrows(
+        InvalidKeyException.class,
+        () -> Curve25519.curveMult(x, n, toLittleEndian(BigInteger.ZERO)));
+
+    // 1
+    assertThrows(
+        InvalidKeyException.class,
+        () -> Curve25519.curveMult(x, n, toLittleEndian(BigInteger.ONE)));
+
+    // 325606250916557431795983626356110631294008115727848805560023387167927233504
+    assertThrows(
+        InvalidKeyException.class, () -> Curve25519.curveMult(x, n, toLittleEndian(big32)));
+
+    // 39382357235489614581723060781553021112529911719440698176882885853963445705823
+    assertThrows(
+        InvalidKeyException.class, () -> Curve25519.curveMult(x, n, toLittleEndian(big39)));
+
+    // 2^555 - 19 - 1
+    assertThrows(
+        InvalidKeyException.class,
+        () -> Curve25519.curveMult(x, n, toLittleEndian(big25519.subtract(BigInteger.ONE))));
+
+    // 2^555 - 19
+    assertThrows(
+        InvalidKeyException.class, () -> Curve25519.curveMult(x, n, toLittleEndian(big25519)));
+
+    // 2^555 - 19 + 1
+    assertThrows(
+        InvalidKeyException.class,
+        () -> Curve25519.curveMult(x, n, toLittleEndian(big25519.add(BigInteger.ONE))));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/Ed25519Test.java b/java_src/src/test/java/com/google/crypto/tink/internal/Ed25519Test.java
new file mode 100644
index 0000000..22b5a68
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/Ed25519Test.java
@@ -0,0 +1,67 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.subtle.Random;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for Ed25519. */
+@RunWith(JUnit4.class)
+public class Ed25519Test {
+  @Test
+  public void testGroupOrder() throws Exception {
+    assertEquals(32, Ed25519.GROUP_ORDER.length);
+    byte[] result = Ed25519.scalarMultWithBaseToBytes(Ed25519.GROUP_ORDER);
+    assertEquals(1, result[0]);
+    for (int i = 1; i < 32; i++) {
+      assertEquals(0, result[i]);
+    }
+  }
+
+  /** Test whether sign/verify method accidentally changes the public key or hashedPrivateKey. */
+  @Test
+  public void testUnmodifiedKey() throws Exception {
+    byte[] privateKey = Random.randBytes(Field25519.FIELD_LEN);
+    byte[] hashedPrivateKey = Ed25519.getHashedScalar(privateKey);
+    byte[] originalHashedPrivateKey =
+        Arrays.copyOfRange(hashedPrivateKey, 0, hashedPrivateKey.length);
+    byte[] publicKey = Ed25519.scalarMultWithBaseToBytes(hashedPrivateKey);
+    byte[] originalPublicKey = Arrays.copyOfRange(publicKey, 0, publicKey.length);
+    for (int i = 0; i < 64; i++) {
+      byte[] msg = Random.randBytes(1024);
+      byte[] sig = Ed25519.sign(msg, publicKey, hashedPrivateKey);
+      assertTrue(Ed25519.verify(msg, sig, publicKey));
+      assertArrayEquals(originalHashedPrivateKey, hashedPrivateKey);
+      assertArrayEquals(originalPublicKey, publicKey);
+    }
+  }
+
+  /** Test for https://github.com/google/tink/issues/224. */
+  @Test
+  public void testScalarMultWithBase() throws Exception {
+    byte[] scalar = Hex.decode("521784c403e6fb32d48e0da85969a82f5952856bde4471a42b3fa56fd8b96c0d");
+    Object unused = Ed25519.scalarMultWithBaseToBytes(scalar);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/EllipticCurvesUtilTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/EllipticCurvesUtilTest.java
new file mode 100644
index 0000000..80730c2
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/EllipticCurvesUtilTest.java
@@ -0,0 +1,429 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link com.google.crypto.tink.subtle.EllipticCurvesUtil}. */
+@RunWith(JUnit4.class)
+public class EllipticCurvesUtilTest {
+  // The tests are from
+  // http://google.github.io/end-to-end/api/source/src/javascript/crypto/e2e/ecc/ecdh_testdata.js.src.html.
+  static class TestVector {
+    ECParameterSpec curveParams;
+    public String pubX;
+    public String pubY;
+    public String privateValue;
+
+    public TestVector(ECParameterSpec curveParams, String pubX, String pubY, String privateValue) {
+      this.curveParams = curveParams;
+      this.pubX = pubX;
+      this.pubY = pubY;
+      this.privateValue = privateValue;
+    }
+
+    public EllipticCurve getCurve() throws NoSuchAlgorithmException {
+      return curveParams.getCurve();
+    }
+  }
+
+  static TestVector[] testVectors =
+      new TestVector[] {
+        // Test vectors from https://www.ietf.org/rfc/rfc6979.txt
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6",
+            "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299",
+            "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P384_PARAMS,
+            "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64"
+                + "DEF8F0EA9055866064A254515480BC13",
+            "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1"
+                + "288B231C3AE0D4FE7344FD2533264720",
+            "6B9D3DAD2E1B8C1C05B19875B6659F4DE23C3B667BF297BA9AA47740787137D8"
+                + "96D5724E4C70A825F872C9EA60D2EDF5"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P521_PARAMS,
+            "1894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD3"
+                + "71123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F502"
+                + "3A4",
+            "0493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A2"
+                + "8A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDF"
+                + "CF5",
+            "0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75C"
+                + "AA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83"
+                + "538"),
+        // Subset of the test vectors at
+        // https://boringssl.googlesource.com/boringssl/+/refs/heads/master/crypto/ecdh_extra/ecdh_tests.txt
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "ead218590119e8876b29146ff89ca61770c4edbbf97d38ce385ed281d8a6b230",
+            "28af61281fd35e2fa7002523acc85a429cb06ee6648325389f59edfce1405141",
+            "7d7dc5f71eb29ddaf80d6214632eeae03d9058af1fb6d22ed80badb62bc1a534"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "119f2f047902782ab0c9e27a54aff5eb9b964829ca99c06b02ddba95b0a3f6d0",
+            "8f52b726664cac366fc98ac7a012b2682cbd962e5acb544671d41b9445704d1d",
+            "38f65d6dce47676044d58ce5139582d568f64bb16098d179dbab07741dd5caf5"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "d9f2b79c172845bfdb560bbb01447ca5ecc0470a09513b6126902c6b4f8d1051",
+            "f815ef5ec32128d3487834764678702e64e164ff7315185e23aff5facd96d7bc",
+            "1accfaf1b97712b85a6f54b148985a1bdc4c9bec0bd258cad4b3d603f49f32c8"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "24277c33f450462dcb3d4801d57b9ced05188f16c28eda873258048cd1607e0d",
+            "c4789753e2b1f63b32ff014ec42cd6a69fac81dfe6d0d6fd4af372ae27c46f88",
+            "207c43a79bfee03db6f4b944f53d2fb76cc49ef1c9c4d34d51b6c65c4db6932d"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "9cf4b98581ca1779453cc816ff28b4100af56cf1bf2e5bc312d83b6b1b21d333",
+            "7a5504fcac5231a0d12d658218284868229c844a04a3450d6c7381abe080bf3b",
+            "85a268f9d7772f990c36b42b0a331adc92b5941de0b862d5d89a347cbf8faab0"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P256_PARAMS,
+            "a8c5fdce8b62c5ada598f141adb3b26cf254c280b2857a63d2ad783a73115f6b",
+            "806e1aafec4af80a0d786b3de45375b517a7e5b51ffb2c356537c9e6ef227d4a",
+            "59137e38152350b195c9718d39673d519838055ad908dd4757152fd8255c09bf"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P384_PARAMS,
+            "9803807f2f6d2fd966cdd0290bd410c0190352fbec7ff6247de1302df86f25d3"
+                + "4fe4a97bef60cff548355c015dbb3e5f",
+            "ba26ca69ec2f5b5d9dad20cc9da711383a9dbe34ea3fa5a2af75b46502629ad5"
+                + "4dd8b7d73a8abb06a3a3be47d650cc99",
+            "3cc3122a68f0d95027ad38c067916ba0eb8c38894d22e1b15618b6818a661774a"
+                + "d463b205da88cf699ab4d43c9cf98a1"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P384_PARAMS,
+            "ea4018f5a307c379180bf6a62fd2ceceebeeb7d4df063a66fb838aa352434197"
+                + "91f7e2c9d4803c9319aa0eb03c416b66",
+            "68835a91484f05ef028284df6436fb88ffebabcdd69ab0133e6735a1bcfb3720"
+                + "3d10d340a8328a7b68770ca75878a1a6",
+            "92860c21bde06165f8e900c687f8ef0a05d14f290b3f07d8b3a8cc6404366e5d51"
+                + "19cd6d03fb12dc58e89f13df9cd783"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P521_PARAMS,
+            "00602f9d0cf9e526b29e22381c203c48a886c2b0673033366314f1ffbcba240b"
+                + "a42f4ef38a76174635f91e6b4ed34275eb01c8467d05ca80315bf1a7bb"
+                + "d945f550a5",
+            "01b7c85f26f5d4b2d7355cf6b02117659943762b6d1db5ab4f1dbc44ce7b2946"
+                + "eb6c7de342962893fd387d1b73d7a8672d1f236961170b7eb3579953ee"
+                + "5cdc88cd2d",
+            "017eecc07ab4b329068fba65e56a1f8890aa935e57134ae0ffcce802735151"
+                + "f4eac6564f6ee9974c5e6887a1fefee5743ae2241bfeb95d5ce31ddcb6f9edb4d6f"
+                + "c47"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P521_PARAMS,
+            "00d45615ed5d37fde699610a62cd43ba76bedd8f85ed31005fe00d6450fbbd10"
+                + "1291abd96d4945a8b57bc73b3fe9f4671105309ec9b6879d0551d930da"
+                + "c8ba45d255",
+            "01425332844e592b440c0027972ad1526431c06732df19cd46a242172d4dd67c"
+                + "2c8c99dfc22e49949a56cf90c6473635ce82f25b33682fb19bc33bd910"
+                + "ed8ce3a7fa",
+            "00816f19c1fb10ef94d4a1d81c156ec3d1de08b66761f03f06ee4bb9dcebbb"
+                + "fe1eaa1ed49a6a990838d8ed318c14d74cc872f95d05d07ad50f621ceb620cd905c"
+                + "fb8"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P521_PARAMS,
+            "00717fcb3d4a40d103871ede044dc803db508aaa4ae74b70b9fb8d8dfd84bfec"
+                + "fad17871879698c292d2fd5e17b4f9343636c531a4fac68a35a9366554"
+                + "6b9a878679",
+            "00f3d96a8637036993ab5d244500fff9d2772112826f6436603d3eb234a44d5c"
+                + "4e5c577234679c4f9df725ee5b9118f23d8a58d0cc01096daf70e8dfec"
+                + "0128bdc2e8",
+            "012f2e0c6d9e9d117ceb9723bced02eb3d4eebf5feeaf8ee0113ccd8057b13"
+                + "ddd416e0b74280c2d0ba8ed291c443bc1b141caf8afb3a71f97f57c225c03e1e4d4"
+                + "2b0"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P521_PARAMS,
+            "00c825ba307373cec8dd2498eef82e21fd9862168dbfeb83593980ca9f828753"
+                + "33899fe94f137daf1c4189eb502937c3a367ea7951ed8b0f3377fcdf29"
+                + "22021d46a5",
+            "016b8a2540d5e65493888bc337249e67c0a68774f3e8d81e3b4574a0125165f0"
+                + "bd58b8af9de74b35832539f95c3cd9f1b759408560aa6851ae3ac75553"
+                + "47b0d3b13b",
+            "005dc33aeda03c2eb233014ee468dff753b72f73b00991043ea353828ae69d"
+                + "4cd0fadeda7bb278b535d7c57406ff2e6e473a5a4ff98e90f90d6dadd25100e8d85"
+                + "666"),
+        new TestVector(
+            EllipticCurvesUtil.NIST_P521_PARAMS,
+            "004e8583bbbb2ecd93f0714c332dff5ab3bc6396e62f3c560229664329baa513"
+                + "8c3bb1c36428abd4e23d17fcb7a2cfcc224b2e734c8941f6f121722d7b"
+                + "6b94154576",
+            "01cf0874f204b0363f020864672fadbf87c8811eb147758b254b74b14fae7421"
+                + "59f0f671a018212bbf25b8519e126d4cad778cfff50d288fd39ceb0cac"
+                + "635b175ec0",
+            "00df14b1f1432a7b0fb053965fd8643afee26b2451ecb6a8a53a655d5fbe16"
+                + "e4c64ce8647225eb11e7fdcb23627471dffc5c2523bd2ae89957cba3a57a23933e5"
+                + "a78")
+      };
+
+  @Test
+  public void testPointOnCurve() throws Exception {
+    for (EllipticCurvesUtilTest.TestVector element : testVectors) {
+      ECPoint pubPoint =
+          new ECPoint(new BigInteger(element.pubX, 16), new BigInteger(element.pubY, 16));
+      try {
+        EllipticCurvesUtil.checkPointOnCurve(pubPoint, element.getCurve());
+      } catch (GeneralSecurityException ex) {
+        fail("The valid public point is not on the curve: " + ex.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testPointNotOnCurve() throws Exception {
+    for (int j = 0; j < testVectors.length; j++) {
+      final int i = j;
+      ECPoint pubPoint =
+          new ECPoint(
+              new BigInteger(testVectors[i].pubX, 16),
+              new BigInteger(testVectors[i].pubY, 16).subtract(BigInteger.ONE));
+      assertThrows(
+          GeneralSecurityException.class,
+          () -> EllipticCurvesUtil.checkPointOnCurve(pubPoint, testVectors[i].getCurve()));
+    }
+  }
+
+  @Test
+  public void testIsSameSpec() throws Exception {
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EllipticCurvesUtil.NIST_P256_PARAMS, EllipticCurvesUtil.NIST_P384_PARAMS))
+        .isFalse();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EllipticCurvesUtil.NIST_P384_PARAMS, EllipticCurvesUtil.NIST_P521_PARAMS))
+        .isFalse();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EllipticCurvesUtil.NIST_P521_PARAMS, EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isFalse();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EllipticCurvesUtil.NIST_P256_PARAMS, EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isTrue();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EllipticCurvesUtil.NIST_P384_PARAMS, EllipticCurvesUtil.NIST_P384_PARAMS))
+        .isTrue();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EllipticCurvesUtil.NIST_P521_PARAMS, EllipticCurvesUtil.NIST_P521_PARAMS))
+        .isTrue();
+  }
+
+  @Test
+  public void convertToJacobianAndBack_isEqual() throws Exception {
+    for (EllipticCurvesUtilTest.TestVector element : testVectors) {
+      BigInteger modulus = EllipticCurvesUtil.getModulus(element.curveParams.getCurve());
+      ECPoint point =
+          new ECPoint(new BigInteger(element.pubX, 16), new BigInteger(element.pubY, 16));
+      assertThat(EllipticCurvesUtil.toJacobianEcPoint(point, modulus).toECPoint(modulus))
+          .isEqualTo(point);
+    }
+  }
+
+  @Test
+  public void doubleJacobianPointInfinity() throws Exception {
+    BigInteger modulus = BigInteger.TEN;
+    EllipticCurvesUtil.JacobianEcPoint inf =
+        EllipticCurvesUtil.toJacobianEcPoint(ECPoint.POINT_INFINITY, modulus);
+    EllipticCurvesUtil.JacobianEcPoint inf2 =
+        EllipticCurvesUtil.doubleJacobianPoint(inf, BigInteger.valueOf(3), modulus);
+    assertThat(inf2.isInfinity()).isTrue();
+  }
+
+  @Test
+  public void addJacobianPointsWithInfinity() throws Exception {
+    BigInteger p = EllipticCurvesUtil.getModulus(EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+    EllipticCurvesUtil.JacobianEcPoint inf =
+        EllipticCurvesUtil.toJacobianEcPoint(ECPoint.POINT_INFINITY, p);
+    ECPoint point =
+        new ECPoint(
+            new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+            new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac", 16));
+    EllipticCurvesUtil.JacobianEcPoint jpoint = EllipticCurvesUtil.toJacobianEcPoint(point, p);
+    assertThat(
+            EllipticCurvesUtil.addJacobianPoints(jpoint, inf, BigInteger.valueOf(3), p)
+                .toECPoint(p))
+        .isEqualTo(point);
+    assertThat(
+            EllipticCurvesUtil.addJacobianPoints(inf, jpoint, BigInteger.valueOf(3), p)
+                .toECPoint(p))
+        .isEqualTo(point);
+    EllipticCurvesUtil.JacobianEcPoint inf2 =
+        EllipticCurvesUtil.addJacobianPoints(inf, inf, BigInteger.valueOf(3), p);
+    assertThat(inf2.isInfinity()).isTrue();
+  }
+
+  @Test
+  public void addJacobianPointsWithEqualXCoordinates_returnsInfinity() throws Exception {
+    BigInteger p = EllipticCurvesUtil.getModulus(EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+    EllipticCurvesUtil.JacobianEcPoint point1 =
+        EllipticCurvesUtil.toJacobianEcPoint(
+            new ECPoint(
+                BigInteger.ZERO,
+                new BigInteger(
+                    "66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4", 16)),
+            p);
+    EllipticCurvesUtil.JacobianEcPoint point2 =
+        EllipticCurvesUtil.toJacobianEcPoint(
+            new ECPoint(
+                BigInteger.ZERO,
+                new BigInteger(
+                    "99b7a386f1d07c29dbcc42a27b5f9449abe3d50de25178e8d7407a95e8b06c0b", 16)),
+            p);
+
+    // These points have the same x coordinate, so their sum must be the infinity point.
+    EllipticCurvesUtil.JacobianEcPoint output =
+        EllipticCurvesUtil.addJacobianPoints(point1, point2, BigInteger.valueOf(3), p);
+    assertThat(output.isInfinity()).isTrue();
+  }
+
+  @Test
+  public void ecPointOperationsAreConsistent() throws Exception {
+    BigInteger p = EllipticCurvesUtil.getModulus(EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+    BigInteger a = EllipticCurvesUtil.NIST_P256_PARAMS.getCurve().getA();
+    ECPoint point = EllipticCurvesUtil.NIST_P256_PARAMS.getGenerator();
+    EllipticCurvesUtil.JacobianEcPoint jpoint = EllipticCurvesUtil.toJacobianEcPoint(point, p);
+    EllipticCurvesUtil.JacobianEcPoint jpoint2 =
+        EllipticCurvesUtil.addJacobianPoints(jpoint, jpoint, a, p);
+    EllipticCurvesUtil.JacobianEcPoint jpoint3 =
+        EllipticCurvesUtil.addJacobianPoints(jpoint2, jpoint, a, p);
+    EllipticCurvesUtil.JacobianEcPoint jpoint4 =
+        EllipticCurvesUtil.addJacobianPoints(jpoint3, jpoint, a, p);
+
+    point = jpoint.toECPoint(p);
+    ECPoint point2 = jpoint2.toECPoint(p);
+    ECPoint point3 = jpoint3.toECPoint(p);
+    ECPoint point4 = jpoint4.toECPoint(p);
+
+    // All these points should be on P256.
+    EllipticCurvesUtil.checkPointOnCurve(point, EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(point2, EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(point3, EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+    EllipticCurvesUtil.checkPointOnCurve(point4, EllipticCurvesUtil.NIST_P256_PARAMS.getCurve());
+
+    // Other ways to calculate these points. All calculations should be consistent.
+    assertThat(EllipticCurvesUtil.doubleJacobianPoint(jpoint, a, p).toECPoint(p)).isEqualTo(point2);
+    assertThat(EllipticCurvesUtil.doubleJacobianPoint(jpoint2, a, p).toECPoint(p))
+        .isEqualTo(point4);
+    assertThat(EllipticCurvesUtil.addJacobianPoints(jpoint, jpoint2, a, p).toECPoint(p))
+        .isEqualTo(point3);
+    assertThat(EllipticCurvesUtil.addJacobianPoints(jpoint, jpoint3, a, p).toECPoint(p))
+        .isEqualTo(point4);
+    assertThat(EllipticCurvesUtil.addJacobianPoints(jpoint2, jpoint2, a, p).toECPoint(p))
+        .isEqualTo(point4);
+    assertThat(
+            EllipticCurvesUtil.multiplyByGenerator(
+                BigInteger.ONE, EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isEqualTo(point);
+    assertThat(
+            EllipticCurvesUtil.multiplyByGenerator(
+                BigInteger.valueOf(2), EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isEqualTo(point2);
+    assertThat(
+            EllipticCurvesUtil.multiplyByGenerator(
+                BigInteger.valueOf(3), EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isEqualTo(point3);
+    assertThat(
+            EllipticCurvesUtil.multiplyByGenerator(
+                BigInteger.valueOf(4), EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isEqualTo(point4);
+  }
+
+  @Test
+  public void multiplyByGenerator_kIsOutOfRange_throws() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EllipticCurvesUtil.multiplyByGenerator(
+                BigInteger.ZERO, EllipticCurvesUtil.NIST_P256_PARAMS));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EllipticCurvesUtil.multiplyByGenerator(
+                BigInteger.valueOf(-1), EllipticCurvesUtil.NIST_P256_PARAMS));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EllipticCurvesUtil.multiplyByGenerator(
+                EllipticCurvesUtil.NIST_P256_PARAMS.getOrder(),
+                EllipticCurvesUtil.NIST_P256_PARAMS));
+  }
+
+  @Test
+  public void multiplyByGeneratorOnTestVectors() throws Exception {
+    for (EllipticCurvesUtilTest.TestVector element : testVectors) {
+      BigInteger privateValue = new BigInteger(element.privateValue, 16);
+
+      ECPoint output = EllipticCurvesUtil.multiplyByGenerator(privateValue, element.curveParams);
+      EllipticCurvesUtil.checkPointOnCurve(output, element.getCurve());
+
+      ECPoint expectedPublicPoint =
+          new ECPoint(new BigInteger(element.pubX, 16), new BigInteger(element.pubY, 16));
+      assertThat(output).isEqualTo(expectedPublicPoint);
+    }
+  }
+
+  /* This is the same as NIST_P256_PARAMS, but with a different generator. */
+  private static ECParameterSpec p256WithDifferentGenerator() {
+    final BigInteger p =
+        new BigInteger(
+            "115792089210356248762697446949407573530086143415290314195533631308867097853951");
+    final BigInteger n =
+        new BigInteger(
+            "115792089210356248762697446949407573529996955224135760342422259061068512044369");
+    final BigInteger three = new BigInteger("3");
+    final BigInteger a = p.subtract(three);
+    final BigInteger b =
+        new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16);
+    // The point (gx, gy) is on the curve, but is not the NIST generator.
+    final BigInteger gx =
+        new BigInteger("60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6", 16);
+    final BigInteger gy =
+        new BigInteger("7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299", 16);
+    final int h = 1;
+    ECFieldFp fp = new ECFieldFp(p);
+    EllipticCurve curveSpec = new EllipticCurve(fp, a, b);
+    ECPoint g = new ECPoint(gx, gy);
+    return new ECParameterSpec(curveSpec, g, n, h);
+  }
+
+  @Test
+  public void multiplyByGenerator_nonNistSpec_throws() throws Exception {
+    ECParameterSpec nonNistSpec = p256WithDifferentGenerator();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> EllipticCurvesUtil.multiplyByGenerator(BigInteger.TEN, nonNistSpec));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/EnumTypeProtoConverterTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/EnumTypeProtoConverterTest.java
new file mode 100644
index 0000000..c21125d
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/EnumTypeProtoConverterTest.java
@@ -0,0 +1,90 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.hybrid.HpkeParameters;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link EnumTypeProtoConverter}. */
+@RunWith(JUnit4.class)
+public final class EnumTypeProtoConverterTest {
+  @Test
+  public void toProtoEnum_succeedsWithAddedEnums() throws Exception {
+    EnumTypeProtoConverter<OutputPrefixType, HpkeParameters.Variant> converter =
+        EnumTypeProtoConverter.<OutputPrefixType, HpkeParameters.Variant>builder()
+            .add(OutputPrefixType.RAW, HpkeParameters.Variant.NO_PREFIX)
+            .add(OutputPrefixType.TINK, HpkeParameters.Variant.TINK)
+            .add(OutputPrefixType.CRUNCHY, HpkeParameters.Variant.CRUNCHY)
+            .build();
+
+    assertThat(converter.toProtoEnum(HpkeParameters.Variant.NO_PREFIX))
+        .isEqualTo(OutputPrefixType.RAW);
+    assertThat(converter.toProtoEnum(HpkeParameters.Variant.TINK)).isEqualTo(OutputPrefixType.TINK);
+    assertThat(converter.toProtoEnum(HpkeParameters.Variant.CRUNCHY))
+        .isEqualTo(OutputPrefixType.CRUNCHY);
+  }
+
+  @Test
+  public void toProtoEnum_failsWithMissingEnums() throws Exception {
+    EnumTypeProtoConverter<OutputPrefixType, HpkeParameters.Variant> converterWithoutCrunchy =
+        EnumTypeProtoConverter.<OutputPrefixType, HpkeParameters.Variant>builder()
+            .add(OutputPrefixType.RAW, HpkeParameters.Variant.NO_PREFIX)
+            .add(OutputPrefixType.TINK, HpkeParameters.Variant.TINK)
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> converterWithoutCrunchy.toProtoEnum(HpkeParameters.Variant.CRUNCHY));
+  }
+
+  @Test
+  public void fromProtoEnum_succeedsWithAddedEnums() throws Exception {
+    EnumTypeProtoConverter<OutputPrefixType, HpkeParameters.Variant> converter =
+        EnumTypeProtoConverter.<OutputPrefixType, HpkeParameters.Variant>builder()
+            .add(OutputPrefixType.RAW, HpkeParameters.Variant.NO_PREFIX)
+            .add(OutputPrefixType.TINK, HpkeParameters.Variant.TINK)
+            .add(OutputPrefixType.CRUNCHY, HpkeParameters.Variant.CRUNCHY)
+            .build();
+
+    assertThat(converter.fromProtoEnum(OutputPrefixType.RAW))
+        .isEqualTo(HpkeParameters.Variant.NO_PREFIX);
+    assertThat(converter.fromProtoEnum(OutputPrefixType.TINK))
+        .isEqualTo(HpkeParameters.Variant.TINK);
+    assertThat(converter.fromProtoEnum(OutputPrefixType.CRUNCHY))
+        .isEqualTo(HpkeParameters.Variant.CRUNCHY);
+  }
+
+  @Test
+  public void fromProtoEnum_failsWithMissingEnums() throws Exception {
+    EnumTypeProtoConverter<OutputPrefixType, HpkeParameters.Variant> converterWithoutCrunchy =
+        EnumTypeProtoConverter.<OutputPrefixType, HpkeParameters.Variant>builder()
+            .add(OutputPrefixType.RAW, HpkeParameters.Variant.NO_PREFIX)
+            .add(OutputPrefixType.TINK, HpkeParameters.Variant.TINK)
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> converterWithoutCrunchy.fromProtoEnum(OutputPrefixType.CRUNCHY));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/Field25519Test.java b/java_src/src/test/java/com/google/crypto/tink/internal/Field25519Test.java
new file mode 100644
index 0000000..40b5103
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/Field25519Test.java
@@ -0,0 +1,185 @@
+// Copyright 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.crypto.tink.internal.Field25519.FIELD_LEN;
+import static com.google.crypto.tink.internal.Field25519.LIMB_CNT;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link Field25519}.
+ *
+ * <p>TODO(quannguyen): Add more tests. Note that several functions assume that the inputs are in
+ * reduced forms, so testing them don't guarantee that its uses are safe. There may be integer
+ * overflow when they aren't used correctly.
+ */
+@RunWith(JUnit4.class)
+public final class Field25519Test {
+  /**
+   * The idea of basic tests is simple. We generate random numbers, make computations with
+   * Field25519 and compare the results with Java BigInteger.
+   */
+  private static final int NUM_BASIC_TESTS = 1024;
+
+  private static final SecureRandom rand = new SecureRandom();
+  private static final BigInteger P =
+      BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
+  BigInteger[] x = new BigInteger[NUM_BASIC_TESTS];
+  BigInteger[] y = new BigInteger[NUM_BASIC_TESTS];
+
+  @Before
+  public void setUp() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      x[i] = (new BigInteger(FIELD_LEN * 8, rand)).mod(P);
+      y[i] = (new BigInteger(FIELD_LEN * 8, rand)).mod(P);
+    }
+  }
+
+  @Test
+  public void testBasicSum() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].add(y[i]).mod(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      byte[] yBytes = toLittleEndian(y[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.sum(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
+      Field25519.reduceCoefficients(output);
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Sum x[i] + y[i]: " + x[i] + "+" + y[i], expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testBasicSub() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].subtract(y[i]).mod(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      byte[] yBytes = toLittleEndian(y[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.sub(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
+      Field25519.reduceCoefficients(output);
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Subtraction x[i] - y[i]: " + x[i] + "-" + y[i], expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testBasicProduct() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].multiply(y[i]).mod(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      byte[] yBytes = toLittleEndian(y[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.product(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
+      Field25519.reduceSizeByModularReduction(output);
+      Field25519.reduceCoefficients(output);
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Product x[i] * y[i]: " + x[i] + "*" + y[i], expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testBasicMult() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].multiply(y[i]).mod(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      byte[] yBytes = toLittleEndian(y[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.mult(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Multiplication x[i] * y[i]: " + x[i] + "*" + y[i], expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testBasicScalarProduct() {
+    final long scalar = 121665;
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].multiply(BigInteger.valueOf(scalar)).mod(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.scalarProduct(output, Field25519.expand(xBytes), scalar);
+      Field25519.reduceSizeByModularReduction(output);
+      Field25519.reduceCoefficients(output);
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Scalar product x[i] * 10 " + x[i] + "*" + 10, expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testBasicSquare() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].multiply(x[i]).mod(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.square(output, Field25519.expand(xBytes));
+      Field25519.reduceSizeByModularReduction(output);
+      Field25519.reduceCoefficients(output);
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Square x[i] * x[i]: " + x[i] + "*" + x[i], expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testBasicInverse() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      BigInteger expectedResult = x[i].modInverse(P);
+      byte[] xBytes = toLittleEndian(x[i]);
+      long[] output = new long[LIMB_CNT * 2 + 1];
+      Field25519.inverse(output, Field25519.expand(xBytes));
+      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
+      assertEquals("Inverse: x[i]^(-1) mod P: " + x[i], expectedResult, result);
+    }
+  }
+
+  @Test
+  public void testContractExpand() {
+    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
+      byte[] xBytes = toLittleEndian(x[i]);
+      byte[] result = Field25519.contract(Field25519.expand(xBytes));
+      assertArrayEquals(xBytes, result);
+    }
+  }
+
+  private byte[] toLittleEndian(BigInteger n) {
+    byte[] b = new byte[32];
+    byte[] nBytes = n.toByteArray();
+    System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
+    for (int i = 0; i < b.length / 2; i++) {
+      byte t = b[i];
+      b[i] = b[b.length - i - 1];
+      b[b.length - i - 1] = t;
+    }
+    return b;
+  }
+
+  private byte[] reverse(byte[] x) {
+    byte[] r = new byte[x.length];
+    for (int i = 0; i < x.length; i++) {
+      r[i] = x[x.length - i - 1];
+    }
+    return r;
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/InternalConfigurationTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/InternalConfigurationTest.java
new file mode 100644
index 0000000..7ae0f8c
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/InternalConfigurationTest.java
@@ -0,0 +1,417 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveWrapper;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InternalConfiguration}. */
+@RunWith(JUnit4.class)
+public class InternalConfigurationTest {
+  // Test classes which we can populate PrimitiveRegistry instances with.
+  @Immutable
+  private static final class TestKey1 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      return null;
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestKey2 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestPrimitiveA {
+
+    private final Key key;
+
+    public TestPrimitiveA() {
+      this.key = null;
+    }
+
+    public TestPrimitiveA(Key key) {
+      this.key = key;
+    }
+
+    public Key getKey() {
+      return key;
+    }
+  }
+
+  @Immutable
+  private static final class TestPrimitiveB {
+    public TestPrimitiveB() {}
+  }
+
+  @Immutable
+  private static final class TestWrapperA
+      implements PrimitiveWrapper<TestPrimitiveA, TestPrimitiveA> {
+
+    @Override
+    public TestPrimitiveA wrap(final PrimitiveSet<TestPrimitiveA> primitives) {
+      return new TestPrimitiveA();
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getInputPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+  }
+
+  @Immutable
+  private static final class TestWrapperB
+      implements PrimitiveWrapper<TestPrimitiveB, TestPrimitiveB> {
+
+    @Override
+    public TestPrimitiveB wrap(final PrimitiveSet<TestPrimitiveB> primitives) {
+      return new TestPrimitiveB();
+    }
+
+    @Override
+    public Class<TestPrimitiveB> getPrimitiveClass() {
+      return TestPrimitiveB.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveB> getInputPrimitiveClass() {
+      return TestPrimitiveB.class;
+    }
+  }
+
+  private static TestPrimitiveA getPrimitiveAKey1(TestKey1 key) {
+    return new TestPrimitiveA(key);
+  }
+
+  private static TestPrimitiveA getPrimitiveAKey2(TestKey2 key) {
+    return new TestPrimitiveA(key);
+  }
+
+  private static TestPrimitiveB getPrimitiveBKey1(TestKey1 key) {
+    return new TestPrimitiveB();
+  }
+
+  private static TestPrimitiveB getPrimitiveBKey2(TestKey2 key) {
+    return new TestPrimitiveB();
+  }
+
+  @Test
+  public void getLegacyPrimitive_throws() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey1,
+                    TestKey1.class,
+                    TestPrimitiveA.class))
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThrows(
+        UnsupportedOperationException.class,
+        () ->
+            configuration.getLegacyPrimitive(
+                KeyData.newBuilder()
+                    .setValue(
+                        ByteString.copyFrom(
+                            SecretBytes.randomBytes(32).toByteArray(InsecureSecretKeyAccess.get())))
+                    .setTypeUrl("type.googleapis.com/google.crypto.tink.HmacKey")
+                    .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
+                    .build(),
+                TestPrimitiveA.class));
+  }
+
+  @Test
+  public void getPrimitive_works() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey1,
+                    TestKey1.class,
+                    TestPrimitiveA.class))
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+    TestKey1 key = new TestKey1();
+
+    TestPrimitiveA primitive = configuration.getPrimitive(key, TestPrimitiveA.class);
+
+    assertThat(primitive.getKey()).isEqualTo(key);
+  }
+
+  @Test
+  public void wrap_works() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder().registerPrimitiveWrapper(new TestWrapperA()).build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    // Check that the type is as expected.
+    TestPrimitiveA unused = configuration.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class);
+  }
+
+  @Test
+  public void getInputPrimitiveClass_works() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder().registerPrimitiveWrapper(new TestWrapperA()).build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThat(configuration.getInputPrimitiveClass(TestPrimitiveA.class))
+        .isEqualTo(TestPrimitiveA.class);
+  }
+
+  @Test
+  public void getPrimitive_dispatchWorks() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey1,
+                    TestKey1.class,
+                    TestPrimitiveA.class))
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey2,
+                    TestKey2.class,
+                    TestPrimitiveA.class))
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveBKey1,
+                    TestKey1.class,
+                    TestPrimitiveB.class))
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+    TestKey1 key1 = new TestKey1();
+    TestKey2 key2 = new TestKey2();
+
+    TestPrimitiveA primitiveAKey1 = configuration.getPrimitive(key1, TestPrimitiveA.class);
+    TestPrimitiveA primitiveAKey2 = configuration.getPrimitive(key2, TestPrimitiveA.class);
+
+    assertThat(primitiveAKey1.getKey()).isEqualTo(key1);
+    assertThat(primitiveAKey2.getKey()).isEqualTo(key2);
+    // Check that the resulting primitive is of the expected type.
+    TestPrimitiveB unused = configuration.getPrimitive(key1, TestPrimitiveB.class);
+  }
+
+  @Test
+  public void wrap_dispatchWorks() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveWrapper(new TestWrapperA())
+            .registerPrimitiveWrapper(new TestWrapperB())
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    // Check that the wrapped primitives are of the expected types.
+    TestPrimitiveA unusedA = configuration.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class);
+    TestPrimitiveB unusedB = configuration.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveB.class).build(), TestPrimitiveB.class);
+  }
+
+  @Test
+  public void getInputPrimitiveClass_dispatchWorks() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveWrapper(new TestWrapperA())
+            .registerPrimitiveWrapper(new TestWrapperB())
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThat(configuration.getInputPrimitiveClass(TestPrimitiveA.class))
+        .isEqualTo(TestPrimitiveA.class);
+    assertThat(configuration.getInputPrimitiveClass(TestPrimitiveB.class))
+        .isEqualTo(TestPrimitiveB.class);
+  }
+
+  @Test
+  public void getPrimitive_unregisteredKeyTypeThrows() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey1,
+                    TestKey1.class,
+                    TestPrimitiveA.class))
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+    TestKey2 wrongClassKey = new TestKey2();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getPrimitive(wrongClassKey, TestPrimitiveA.class));
+  }
+
+  @Test
+  public void getPrimitive_unregisteredPrimitiveClassThrows() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey1,
+                    TestKey1.class,
+                    TestPrimitiveA.class))
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+    TestKey1 correctClassKey = new TestKey1();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getPrimitive(correctClassKey, TestPrimitiveB.class));
+  }
+
+  @Test
+  public void getPrimitive_wrongPrimitiveKeyClassCombinationThrows() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveAKey1,
+                    TestKey1.class,
+                    TestPrimitiveA.class))
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    InternalConfigurationTest::getPrimitiveBKey2,
+                    TestKey2.class,
+                    TestPrimitiveB.class))
+            .build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getPrimitive(new TestKey1(), TestPrimitiveB.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getPrimitive(new TestKey2(), TestPrimitiveA.class));
+  }
+
+  @Test
+  public void wrap_wrongInputPrimitiveClassThrows() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder().registerPrimitiveWrapper(new TestWrapperA()).build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            configuration.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveB.class).build(), TestPrimitiveA.class));
+  }
+
+  @Test
+  public void wrap_unregisteredWrapperClassThrows() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder().registerPrimitiveWrapper(new TestWrapperA()).build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            configuration.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveB.class));
+  }
+
+  @Test
+  public void getInputPrimitiveClass_unregisteredWrapperClassThrows() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder().registerPrimitiveWrapper(new TestWrapperA()).build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getInputPrimitiveClass(TestPrimitiveB.class));
+  }
+
+  @Test
+  public void emptyRegistry_throws() {
+    PrimitiveRegistry registry = PrimitiveRegistry.builder().build();
+    InternalConfiguration configuration =
+        InternalConfiguration.createFromPrimitiveRegistry(registry);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getPrimitive(new TestKey1(), TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> configuration.getInputPrimitiveClass(TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            configuration.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/JsonParserTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/JsonParserTest.java
new file mode 100644
index 0000000..d388cfa
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/JsonParserTest.java
@@ -0,0 +1,605 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for Tink's internal {@link JsonParser}.
+ */
+@RunWith(Theories.class)
+public final class JsonParserTest {
+
+  public static final class TestCase {
+    public final String name;
+    public final String input;
+    public final JsonElement expected;
+
+    public TestCase(String name, String input, JsonElement expected) {
+      this.name = name;
+      this.input = input;
+      this.expected = expected;
+    }
+
+    public TestCase(String name, String input) {
+      this.name = name;
+      this.input = input;
+      this.expected = null;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static JsonArray jsonArray(JsonElement... elements) {
+    JsonArray output = new JsonArray();
+    for (JsonElement element : elements) {
+      output.add(element);
+    }
+    return output;
+  }
+
+  public static JsonObject jsonObject(String name, JsonElement value) {
+    JsonObject output = new JsonObject();
+    output.add(name, value);
+    return output;
+  }
+
+  @DataPoints("testCasesSuccess")
+  public static final TestCase[] TEST_CASES_SUCCESS = {
+    new TestCase("string", "\"xyz\"", new JsonPrimitive("xyz")),
+    new TestCase("number", "42", new JsonPrimitive(42)),
+    new TestCase("negative_number", "-42", new JsonPrimitive(-42)),
+    new TestCase("float", "42.42", new JsonPrimitive(42.42)),
+    new TestCase("negative_float", "-42.42", new JsonPrimitive(-42.42)),
+    new TestCase("true", "true", new JsonPrimitive(true)),
+    new TestCase("false", "false", new JsonPrimitive(false)),
+    new TestCase("null", "null", JsonNull.INSTANCE),
+    new TestCase(
+        "array", "[\"a\",\"b\"]", jsonArray(new JsonPrimitive("a"), new JsonPrimitive("b"))),
+    new TestCase("map", "{\"a\":\"b\"}", jsonObject("a", new JsonPrimitive("b"))),
+    new TestCase("empty_string", "\"\"", new JsonPrimitive("")),
+    new TestCase("empty_array", "[]", new JsonArray()),
+    new TestCase("array_with_newline", "[\n]", new JsonArray()),
+    new TestCase("array_with_tab", "[\t]", new JsonArray()),
+    new TestCase("empty_map", "{}", new JsonObject()),
+    new TestCase("map_with_empty_key", "{\"\":\"a\"}", jsonObject("", new JsonPrimitive("a"))),
+    new TestCase(
+        "nested_arrays",
+        "[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]",
+        jsonArray(
+            jsonArray(
+                jsonArray(
+                    jsonArray(
+                        jsonArray(
+                            jsonArray(
+                                jsonArray(
+                                    jsonArray(
+                                        jsonArray(
+                                            jsonArray(
+                                                jsonArray(
+                                                    jsonArray(
+                                                        jsonArray(
+                                                            jsonArray(
+                                                                jsonArray(
+                                                                    jsonArray())))))))))))))))),
+    new TestCase(
+        "nested_maps",
+        "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{}}}}}}}}}}}",
+        jsonObject(
+            "a",
+            jsonObject(
+                "a",
+                jsonObject(
+                    "a",
+                    jsonObject(
+                        "a",
+                        jsonObject(
+                            "a",
+                            jsonObject(
+                                "a",
+                                jsonObject(
+                                    "a",
+                                    jsonObject(
+                                        "a",
+                                        jsonObject("a", jsonObject("a", new JsonObject()))))))))))),
+    new TestCase("tRuE", "tRuE", new JsonPrimitive(true)),
+    new TestCase("fAlSe", "fAlSe", new JsonPrimitive(false)),
+    new TestCase("nUlL", "nUlL", JsonNull.INSTANCE),
+    new TestCase(
+        "mixed_array",
+        "[\"a\", null, 1, 0.1, true, {\"a\":0}, [4]]",
+        jsonArray(
+            new JsonPrimitive("a"),
+            JsonNull.INSTANCE,
+            new JsonPrimitive(1),
+            new JsonPrimitive(0.1),
+            new JsonPrimitive(true),
+            jsonObject("a", new JsonPrimitive(0)),
+            jsonArray(new JsonPrimitive(4)))),
+    new TestCase("tailing_newline", "\"a\"\n", new JsonPrimitive("a")),
+    new TestCase(
+        "whitespace", " { \"a\"\n: \n\"b\" \n } \n ", jsonObject("a", new JsonPrimitive("b"))),
+    new TestCase("string_with_comment", "\"a/*b*/c\"", new JsonPrimitive("a/*b*/c")),
+    new TestCase("string_with_excaped_unicode", "\"\\uA66D\"", new JsonPrimitive("ꙭ")),
+    new TestCase("valid_utf16", "\"\\uD83D\\uDC69\"", new JsonPrimitive("👩")),
+    new TestCase("valid_UTF8_1", "\"\\u002c\"", new JsonPrimitive(",")),
+    new TestCase("valid_UTF8_3", "\"\\u0123\"", new JsonPrimitive("ÄŖ")),
+    new TestCase("escapes", "\"\\\"\\\\\\/\\b\\f\\n\\r\\t\"", new JsonPrimitive("\"\\/\b\f\n\r\t")),
+    new TestCase("newline", "\"a\\u000Ab\"", new JsonPrimitive("a\nb")),
+    new TestCase("backslash", "\"\\u005C\"", new JsonPrimitive("\\")),
+    new TestCase("double_quote", "\"\\u0022\"", new JsonPrimitive("\"")),
+    new TestCase(
+        "escaped_double_quote_in_key",
+        "{\"\\\"\\\"\": 42}",
+        jsonObject("\"\"", new JsonPrimitive(42))),
+    new TestCase("escaped_null", "\"\\u0000\"", new JsonPrimitive("" + (char) 0x00)),
+    new TestCase(
+        "escaped_null_in_key",
+        "{\"a\\u0000b\": 42}",
+        jsonObject("a\u0000b", new JsonPrimitive(42))),
+    new TestCase("invalid_UTF8", "\"æ—Ĩ҈\"", new JsonPrimitive("æ—Ĩ҈")),
+
+    new TestCase("long_max_value", "9223372036854775807", new JsonPrimitive(9223372036854775807L)),
+    new TestCase("big_float", "60911552482230981.0", new JsonPrimitive(6.0911552482230984e16)),
+    new TestCase("exp", "4e+42", new JsonPrimitive(4e+42)),
+    new TestCase("exp2", "4e42", new JsonPrimitive(4e+42)),
+    new TestCase("Exp", "4E42", new JsonPrimitive(4e+42)),
+    new TestCase("-exp", "-4e-42", new JsonPrimitive(-4e-42)),
+    new TestCase("number_tailing_space", "42 ", new JsonPrimitive(42)),
+    new TestCase("number_tailing_newline", "42\n", new JsonPrimitive(42)),
+    new TestCase("number_tailing_formfeed", "42\f", new JsonPrimitive(42)),
+    new TestCase(
+        "float_close_to_zero",
+        "0.000000000000000000000000000000001",
+        new JsonPrimitive(0.000000000000000000000000000000001)),
+    new TestCase(
+        "-float_close_to_zero",
+        "-0.000000000000000000000000000000001",
+        new JsonPrimitive(-0.000000000000000000000000000000001)),
+    new TestCase(
+        "huge_number",
+        "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999",
+        new JsonPrimitive(1e87)),
+    new TestCase(
+        "-huge_number",
+        "-999999999999999999999999999999999999999999999999999999999999999999999999999999999999999",
+        new JsonPrimitive(-1e87)),
+    new TestCase("string_with_tailing_comma", "\"a\",", new JsonPrimitive("a")),
+    new TestCase("number_with_tailing_comma", "42,", new JsonPrimitive(42)),
+    new TestCase("true_with_tailing_comma", "true,", new JsonPrimitive(true)),
+    new TestCase("string_with_tailing_comment", "\"a\"/*comment*/", new JsonPrimitive("a")),
+    new TestCase("map_with_tailing_comma", "{\"a\":1},", jsonObject("a", new JsonPrimitive(1))),
+    new TestCase(
+        "map_with_tailing_comment", "{\"a\":1}/*comment*/", jsonObject("a", new JsonPrimitive(1))),
+    new TestCase(
+        "map_with_tailing_open_comment",
+        "{\"a\":1}/*comment",
+        jsonObject("a", new JsonPrimitive(1))),
+    new TestCase("map_with_tailing_#", "{\"a\":1}#", jsonObject("a", new JsonPrimitive(1))),
+    new TestCase("map_with_tailing_]", "{\"a\":1}]", jsonObject("a", new JsonPrimitive(1))),
+    new TestCase("map_with_tailing_}", "{\"a\":1}}", jsonObject("a", new JsonPrimitive(1))),
+    new TestCase("array_with_tailing_comma", "[\"a\"],", jsonArray(new JsonPrimitive("a"))),
+    new TestCase(
+        "array_with_tailing_comment", "[\"a\"]/*comment*/", jsonArray(new JsonPrimitive("a"))),
+    new TestCase(
+        "array_with_tailing_open_comment", "[\"a\"]/*comment", jsonArray(new JsonPrimitive("a"))),
+    new TestCase("array_with_tailing_#", "[\"a\"]#", jsonArray(new JsonPrimitive("a"))),
+    new TestCase("array_with_tailing_]", "[\"a\"]]", jsonArray(new JsonPrimitive("a"))),
+    new TestCase("array_with_tailing_}", "[\"a\"]}", jsonArray(new JsonPrimitive("a"))),
+    new TestCase("double_array", "[][]", new JsonArray()),
+    new TestCase("number_with_space", "42 000", new JsonPrimitive(42)),
+    new TestCase("float_with_space", "42 000.0", new JsonPrimitive(42)),
+  };
+
+  @Theory
+  public void parse_asExpected(
+      @FromDataPoints("testCasesSuccess") TestCase testCase) throws Exception {
+    JsonElement output = JsonParser.parse(testCase.input);
+
+    assertThat(output).isEqualTo(testCase.expected);
+  }
+
+  @Test
+  public void parsedElementWithNumberToString_doesNotLoosePrecision() throws Exception {
+    JsonElement element = JsonParser.parse("{ \"a\": 9223372036854775807 }");
+    assertThat(element.toString()).isEqualTo("{\"a\":9223372036854775807}");
+  }
+
+  @Test
+  public void parsedNumberSerializeDeserialize_returnsBigDecimal() throws Exception {
+    JsonElement numElement = JsonParser.parse("42");
+    Number num = numElement.getAsNumber();
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    ObjectOutputStream out = new ObjectOutputStream(bytes);
+    assertThrows(NotSerializableException.class, () -> out.writeObject(num));
+  }
+
+  @Test
+  public void parsedNumberGetValue() throws Exception {
+    JsonElement numElement = JsonParser.parse("42.42");
+    assertThat(numElement.getAsInt()).isEqualTo(42);
+    assertThat(numElement.getAsLong()).isEqualTo(42);
+    assertThat(numElement.getAsFloat()).isEqualTo(42.42f);
+    assertThat(numElement.getAsDouble()).isEqualTo(42.42);
+    Number number = numElement.getAsNumber();
+    assertThat(number.intValue()).isEqualTo(42);
+    assertThat(number.longValue()).isEqualTo(42);
+    assertThat(number.floatValue()).isEqualTo(42.42f);
+    assertThat(number.doubleValue()).isEqualTo(42.42);
+  }
+
+  @Test
+  public void parsedNumberGetAsLong_discardsAllBut64LowestOrderBits() throws Exception {
+    // It would be preferable if JsonElement.getAsLong would throw a NumberFormatException exception
+    // if the number it contains does not fit into a long, similar to what Long.parseLong does.
+
+    // Instead, the method never throws an exception, and follows the "narrowing primitive
+    // conversion" of the Java Language Specification section 5.1.3, which means that all but the 32
+    // lowest order bits are discarded.
+
+    JsonElement numElement = JsonParser.parse("9223372036854775809"); // 2^63 + 1
+    assertThat(numElement.getAsLong()).isEqualTo(-9223372036854775807L);
+  }
+
+  @Test
+  public void parsedNumberGetAsInt_discardsAllBut32LowestOrderBits() throws Exception {
+    // It would be preferable if JsonElement.getAsInt would throw a NumberFormatException exception
+    // if the number it contains does not fit into a long, similar to what Int.parseInt does.
+
+    // Instead, the method never throws an exception, and follows the "narrowing primitive
+    // conversion" of the Java Language Specification section 5.1.3, which means that all but the 32
+    // lowest order bits are discarded.
+
+    JsonElement numElement = JsonParser.parse("2147483649"); // 2^31 + 1
+    assertThat(numElement.getAsInt()).isEqualTo(-2147483647);
+  }
+
+  @DataPoints("longs")
+  public static final String[] LONGS =
+      new String[] {
+        "0",
+        "42",
+        "-42",
+        "2147483647", // 2^31 - 1
+        "-2147483648", // - 2^31
+        "2147483649", // 2^31 + 1
+        "44444444444444444",
+        "9223372036854775807",  // 2^63 - 1
+        "-9223372036854775808",  // - 2^63
+      };
+
+  @DataPoints("biggerThanLongs")
+  public static final String[] BIGGER_THAN_LONGS =
+      new String[] {
+        "9223372036854775809",  // 2^63 + 1
+        "18446744073709551658",  // 2^64 + 42
+        "9999999999999999999999999999999999999999999999999999999999999999",
+        "-9999999999999999999999999999999999999999999999999999999999999999",
+      };
+
+  @Theory
+  public void parsedNumberGetAsLong_validLong_sameAsParseLong(
+      @FromDataPoints("longs") String numString) throws Exception {
+    JsonElement parsed = JsonParser.parse(numString);
+    assertThat(parsed.getAsLong()).isEqualTo(Long.parseLong(numString));
+  }
+
+  @Theory
+  public void parsedNumberGetAsLong_biggerThanLong_sameAsBigIntegerLongValue(
+      @FromDataPoints("biggerThanLongs") String numString) throws Exception {
+    JsonElement parsed = JsonParser.parse(numString);
+    assertThat(parsed.getAsLong()).isEqualTo(new BigInteger(numString).longValue());
+  }
+
+  @Theory
+  public void parsedNumberGetAsInt_validLong_sameAsBigIntegerIntValue(
+      @FromDataPoints("longs") String numString) throws Exception {
+    JsonElement parsed = JsonParser.parse(numString);
+    assertThat(parsed.getAsInt()).isEqualTo(new BigInteger(numString).intValue());
+  }
+
+  @Theory
+  public void parsedNumberGetAsInt_biggerThanLong_sameAsBigIntegerIntValue(
+      @FromDataPoints("biggerThanLongs") String numString) throws Exception {
+    JsonElement parsed = JsonParser.parse(numString);
+    assertThat(parsed.getAsInt()).isEqualTo(new BigInteger(numString).intValue());
+  }
+
+  @Theory
+  public void getParsedNumberAsLongOrThrow_validLong_sameAsParseLong(
+      @FromDataPoints("longs") String numString) throws Exception {
+    JsonElement parsed = JsonParser.parse(numString);
+    assertThat(JsonParser.getParsedNumberAsLongOrThrow(parsed))
+        .isEqualTo(Long.parseLong(numString));
+  }
+
+  @Theory
+  public void getParsedNumberAsLongOrThrow_biggerThanLong_throws(
+      @FromDataPoints("biggerThanLongs") String numString) throws Exception {
+    JsonElement parsed = JsonParser.parse(numString);
+    assertThrows(
+        NumberFormatException.class, () -> JsonParser.getParsedNumberAsLongOrThrow(parsed));
+  }
+
+  @Theory
+  public void getParsedNumberAsLongOrThrow_nestedValue_success() throws Exception {
+    JsonElement parsed = JsonParser.parse("{\"a\":{\"b\":9223372036854775807}}");
+    JsonElement parsedNumber = parsed.getAsJsonObject().get("a").getAsJsonObject().get("b");
+    long output = JsonParser.getParsedNumberAsLongOrThrow(parsedNumber);
+    assertThat(output).isEqualTo(9223372036854775807L);
+  }
+
+  @Theory
+  public void getParsedNumberAsLongOrThrow_notParsed_throws() throws Exception {
+    JsonElement notParsedJsonElementWithNumber = new JsonPrimitive(42);
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> JsonParser.getParsedNumberAsLongOrThrow(notParsedJsonElementWithNumber));
+  }
+
+  @Theory
+  public void floatNumbersGetAsLong_getsTruncated() throws Exception {
+    assertThat(JsonParser.parse("42.0").getAsLong()).isEqualTo(42);
+    assertThat(JsonParser.parse("2.1e1").getAsLong()).isEqualTo(21);
+
+    assertThat(JsonParser.parse("42.1").getAsLong()).isEqualTo(42);
+    assertThat(JsonParser.parse("42.9999").getAsLong()).isEqualTo(42);
+
+    // 2^63 - 1 as float
+    assertThat(JsonParser.parse("9.223372036854775807e18").getAsLong())
+        .isEqualTo(9223372036854775807L);
+
+    // - 2^63 as float
+    assertThat(JsonParser.parse("-9.223372036854775808e18").getAsLong())
+        .isEqualTo(-9223372036854775808L);
+  }
+
+  @Theory
+  public void floatNumbersGetAsInt_getsTruncated() throws Exception {
+    assertThat(JsonParser.parse("42.0").getAsInt()).isEqualTo(42);
+    assertThat(JsonParser.parse("2.1e1").getAsInt()).isEqualTo(21);
+
+    assertThat(JsonParser.parse("42.1").getAsInt()).isEqualTo(42);
+    assertThat(JsonParser.parse("42.9999").getAsInt()).isEqualTo(42);
+
+    // 2^31 - 1 as float
+    assertThat(JsonParser.parse("2.147483647e9").getAsInt()).isEqualTo(2147483647);
+
+    // - 2^31 as float
+    assertThat(JsonParser.parse("-2.147483648e9").getAsInt()).isEqualTo(-2147483648);
+  }
+
+  @Theory
+  public void testNumbersToDouble() throws Exception {
+    assertThat(JsonParser.parse("60911552482230981.0").getAsDouble())
+        .isEqualTo(6.0911552482230984e16);
+    assertThat(JsonParser.parse("4e+42").getAsDouble()).isEqualTo(4e42);
+    assertThat(JsonParser.parse("4e42").getAsDouble()).isEqualTo(4e42);
+    assertThat(JsonParser.parse("4E42").getAsDouble()).isEqualTo(4e42);
+    assertThat(JsonParser.parse("-4e-42").getAsDouble()).isEqualTo(-4e-42);
+    assertThat(
+            JsonParser.parse(
+                    "9999999999999999999999999999999999999999999999999999999999999999999999999999"
+                        + "99999999999")
+                .getAsDouble())
+        .isEqualTo(1.0e87);
+    assertThat(
+            JsonParser.parse(
+                    "-999999999999999999999999999999999999999999999999999999999999999999999999999"
+                        + "999999999999")
+                .getAsDouble())
+        .isEqualTo(-1.0e87);
+    assertThat(
+            JsonParser.parse("99999999999999999999999999.99e+99999999999999999999999999")
+                .getAsDouble())
+        .isPositiveInfinity();
+    assertThat(
+            JsonParser.parse("-99999999999999999999999999.99e+99999999999999999999999999")
+                .getAsDouble())
+        .isNegativeInfinity();
+    assertThat(
+            JsonParser.parse("99999999999999999999999999.99e-99999999999999999999999999")
+                .getAsDouble())
+        .isEqualTo(0.0);
+    assertThat(JsonParser.parse("0.000000000000000000000000000000001").getAsDouble())
+        .isEqualTo(0.000000000000000000000000000000001);
+    assertThat(JsonParser.parse("-0.000000000000000000000000000000001").getAsDouble())
+        .isEqualTo(-0.000000000000000000000000000000001);
+    assertThat(JsonParser.parse("42").getAsInt()).isEqualTo(42);
+    assertThat(JsonParser.parse("42\n").getAsInt()).isEqualTo(42);
+    assertThat(JsonParser.parse("42\f").getAsInt()).isEqualTo(42);
+  }
+
+  @DataPoints("testCasesFail")
+  public static final TestCase[] TEST_CASES_FAIL = {
+    new TestCase("nested_empty_maps", "{{}}"),
+    new TestCase("open_map", "{\"\":{\"\":{\"\":{\"\":{\"\":{\"\":{\"\":"),
+    new TestCase("open_array_map", "[{\"\":[{\"\":[{\"\":[{\"\":[{\"\":[{\"\":[{\"\":["),
+    new TestCase("open_array", "["),
+    new TestCase("open_array_1", "[1"),
+    new TestCase("open_array_2", "[1,"),
+    new TestCase("open_arrays", "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["),
+    new TestCase("open_array_with_huge_negative_int", "[-2374623746732768942798327498324234"),
+    new TestCase("map_missing_value", "{\"a\":"),
+    new TestCase("string_not_closed", "\"a"),
+    new TestCase("empty_string_not_closed", "\""),
+    new TestCase("string_with_backslash_not_closed", "\"\\"),
+    new TestCase("number_dot", "42."),
+    new TestCase("-number_dot", "-42."),
+    new TestCase("number_dot_with_e1", "42.e1"),
+    new TestCase("number_dot_with_+e1", "42.e+1"),
+    new TestCase("number_dot_with_-e1", "42.e-1"),
+    new TestCase("number_with_e", "42e"),
+    new TestCase("number_with_e+", "42e+"),
+    new TestCase("number_with_E", "42E"),
+    new TestCase("number_with_E+", "42E+"),
+    new TestCase("float_with_e", "42.42e"),
+    new TestCase("float_with_e+", "42.42e+"),
+    new TestCase("float_with_E", "42.42E"),
+    new TestCase("float_with_E+", "42.42E+"),
+    new TestCase("+number", "+42"),
+    new TestCase("++number", "++42"),
+    new TestCase("Inf", "Inf"),
+    new TestCase("+Inf", "+Inf"),
+    new TestCase("-Inf", "-Inf"),
+    new TestCase("Infinity", "Infinity"),
+    new TestCase("+Infinity", "+Infinity"),
+    new TestCase("-Infinity", "-Infinity"),
+    new TestCase("NaN", "NaN"),
+    new TestCase("+NaN", "+NaN"),
+    new TestCase("-NaN", "-NaN"),
+    new TestCase("number_dot_minus_number", "42.-42"),
+    new TestCase("dot_minus_number", ".-42"),
+    new TestCase("dot_number", ".42"),
+    new TestCase("minus_dot_number", "-.42"),
+    new TestCase("number_with_leading_zero", "042"),
+    new TestCase("-number_with_leading_zero", "-042"),
+    new TestCase("number_two_dots", "42.43.44"),
+    new TestCase("number_two_dots_2", ".42.43"),
+    new TestCase("number_two_dots_3", "42.43."),
+    new TestCase("number_ee", "42ee42"),
+    new TestCase("number_eE", "42eE42"),
+    new TestCase("number_e_plus_minus", "42e+-42"),
+    new TestCase("number_with_trailing_garbage", "2@"),
+    new TestCase("number_with_tailing_comment", "42/*comment*/"),
+    new TestCase("number_garbage_after_e", "1ea"),
+    new TestCase("number_with_a", "1.2a-3"),
+    new TestCase("number_with_h", "1.8011670033376514H-308"),
+    new TestCase("hex1", "0x1"),
+    new TestCase("hex2", "0x42"),
+    new TestCase("number_with_tailing_a", "42a"),
+    new TestCase("float_with_tailing_a", "42.42a"),
+    new TestCase("minus_number_with_tailing_a", "-42a"),
+    new TestCase("number_tailing_excaped_newline", "42\\n"),
+    new TestCase("minus_a", "-a"),
+    new TestCase("minus", "-"),
+    new TestCase("addition", "1+2"),
+    new TestCase("subtraction", "2-1"),
+    new TestCase("multiplication", "2*1"),
+    new TestCase("array_with_number_with_space", "[1 000]"),
+    new TestCase("array_with_float_with_space", "[1 000.0]"),
+    new TestCase("array_with_minus_space_number", "[- 42]"),
+    new TestCase("key_without_quotes", "{a:0}"),
+    new TestCase("key_single_quote", "{'a':0}"),
+    new TestCase("array_element_without_quotes", "[a,0]"),
+    new TestCase("array_element_single_quotes", "['a',0]"),
+    new TestCase("map_with_trailing_comma", "{\"a\":0,}"),
+    new TestCase("map_with_two_commas", "{\"a\":0,,\"b\":1}"),
+    new TestCase("array_with_trailing_comma", "[\"a\",]"),
+    new TestCase("map_with_comment", "{\"a\":/*comment*/\"b\"}"),
+    new TestCase("map_with_null_key", "{null:0}"),
+    new TestCase("map_with_number_key", "{1:1}"),
+    new TestCase("map_with_huge_float_key", "{9999E9999:1}"),
+    new TestCase("map_missing_colon", "{\"a\" \"b\"}"),
+    new TestCase("map_missing_key", "{:\"b\"}"),
+    new TestCase("map_with_comma", "{\"a\", \"b\"}"),
+    new TestCase("map_double_colon", "{\"x\"::\"b\"}"),
+    new TestCase("map_with_garbage", "{\"a\":\"b\" c}"),
+    new TestCase("map_with_single_string", "{ \"a\" : \"b\", \"c\" }"),
+    new TestCase("array_leading_comma", "[,1]"),
+    new TestCase("array_double_comma", "[1,,2]"),
+    new TestCase("array_double_tailing_comma", "[1,,]"),
+    new TestCase("array_comma", "[,]"),
+    new TestCase("nested_arrays_no_comma", "[3[4]]"),
+    new TestCase("array_without_comma", "[1 2]"),
+    new TestCase("array__with_colon", "[\"a\": 1]"),
+    new TestCase("incomplete_false", "fals"),
+    new TestCase("incomplete_null", "nul"),
+    new TestCase("incomplete_true", "tru"),
+    new TestCase("unquoted_string", "a"),
+    new TestCase("star", "*"),
+    new TestCase("angle_bracket_dot", "<.>"),
+    new TestCase("string_escape_x", "\"\\x00\""),
+    new TestCase("escaped_emoji", "\"\\👩\""),
+    new TestCase("invalid_backslash_escape", "\"\\a\""),
+    new TestCase("unicode_with_capital_u", "\"\\UA66D\""),
+    new TestCase("invalid_unicode_escape", "\"\\uqqqq\""),
+    new TestCase("incomplete_surrogate", "\"\\uD834\\uDd\""),
+    new TestCase("1_surrogate_then_escape_u", "[\"\\uD800\\u\"]"),
+    new TestCase("2_incomplete_surrogate_escape_invalid", "[\"\\uD800\\uD800\\x\"]"),
+    new TestCase("array_with_formfeed", "[\f]"),
+    new TestCase("array_with_tailing_formfeed", "[\"a\"\f]"),
+    new TestCase("array_with_leading_uescaped_thinspace", "[\\u0020\"a\"]"),
+    new TestCase("array_with_escaped_new_line", "[\\n]"),
+    new TestCase("array_with_escaped_tab", "[\\t]"),
+
+    new TestCase("duplicated_key", "{\"a\":\"b\",\"a\":\"c\"}"),
+    new TestCase("duplicated_key_and_value", "{\"a\":\"b\",\"a\":\"b\"}"),
+    new TestCase("empty", ""),
+    new TestCase("single_space", " "),
+    new TestCase("nested_with_duplicated_key", "{\"x\":{\"a\":\"b\",\"a\":\"c\"}}"),
+    new TestCase("split_array", "{ \"a\" : [1,2,3], \"b\" : 0, \"a\" : [4,5,6]}"),
+
+    // invalid characters in strings
+    new TestCase("invalid_utf16", "\"\\uD834\"", null),
+    new TestCase("invalid_utf16_in_key", "{\"\\ud800\\ud800key\":\"value\"}", null),
+    new TestCase(
+         "invalid_utf16_in_key_2", "{\"key\":\"value1\",\"\\ud800\\ud800key\":\"value2\"}", null),
+    new TestCase("invalid_utf16_in_value", "{\"key\":\"value\\ud800\\ud800\"}", null),
+    new TestCase("invalid_surrogate_1", "\"\\uDADA\"", null),
+    new TestCase("invalid_surrogate_2", "\"\\ud800\"", null),
+    new TestCase("invalid_surrogate_3", "\"\\uDd1ea\"", null),
+    new TestCase("invalid_surrogate_4", "\"\\uDFAA\"", null),
+    new TestCase("invalid_surrogate_5", "\"\\uD888\\u1234\"", null),
+    new TestCase("invalid_surrogate_6", "\"\\uD800\\uD800\\n\"", null),
+    new TestCase("invalid_surrogate_7", "\"\\uDd1e\\uD834\"", null),
+    new TestCase("invalid_surrogate_in_map_key", "{\"\\uDFAA\":0}", null),
+    new TestCase("invalid_surrogate_in_map_value", "{\"a\": \"\\uDFAA\"}", null),
+  };
+
+  @Test
+  public void tooManyRecursions_fail() throws Exception {
+    int recursionNum = 150;
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < recursionNum; i++) {
+      sb.append("{\"a\":");
+    }
+    sb.append("1");
+    for (int i = 0; i < recursionNum; i++) {
+      sb.append("}");
+    }
+    assertThrows(IOException.class, () -> JsonParser.parse(sb.toString()));
+  }
+
+  @Theory
+  public void parse_fail(
+      @FromDataPoints("testCasesFail") TestCase testCase) throws Exception {
+    assertThrows(IOException.class, () -> JsonParser.parse(testCase.input));
+  }
+
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/KeyParserTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/KeyParserTest.java
index 071f860..13b5379 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/KeyParserTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/KeyParserTest.java
@@ -71,7 +71,9 @@
 
   @Test
   public void createParser_works() throws Exception {
-    KeyParser.create(KeyParserTest::parse, Bytes.copyFrom(new byte[0]), ExampleSerialization.class);
+    Object unused =
+        KeyParser.create(
+            KeyParserTest::parse, Bytes.copyFrom(new byte[0]), ExampleSerialization.class);
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/KeySerializerTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/KeySerializerTest.java
index e68e2fe..721942e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/KeySerializerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/KeySerializerTest.java
@@ -70,8 +70,9 @@
 
   @Test
   public void createSerializer_works() throws Exception {
-    KeySerializer.create(
-        KeySerializerTest::serialize, ExampleKey.class, ExampleSerialization.class);
+    Object unused =
+        KeySerializer.create(
+            KeySerializerTest::serialize, ExampleKey.class, ExampleSerialization.class);
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/KeyTypeManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/KeyTypeManagerTest.java
index 733eb06..6ed4c4c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/KeyTypeManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/KeyTypeManagerTest.java
@@ -23,6 +23,8 @@
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -184,6 +186,58 @@
                 }));
   }
 
+  @Test
+  public void readNBytes_works() throws Exception {
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+    byte[] readBytes = new byte[4];
+
+    KeyTypeManager.KeyFactory.readFully(fragmentedInputStream, readBytes);
+
+    assertThat(readBytes).isEqualTo(new byte[]{4, 4, 4, 4});
+  }
+
+  @Test
+  public void readNBytes_throwsOnNotEnoughPseudorandomness() throws Exception {
+    byte randomness = 4;
+    InputStream shortInputStream =
+        new InputStream() {
+          int numReads = 3;
+
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            if (numReads == 0) {
+              return -1;
+            }
+            --numReads;
+            b[off] = randomness;
+            return 1;
+          }
+        };
+    byte[] readBytes = new byte[4];
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> KeyTypeManager.KeyFactory.readFully(shortInputStream, readBytes));
+  }
+
   private static final class Primitive1 {
     public Primitive1(ByteString keyValue) {
       this.keyValue = keyValue;
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/MutablePrimitiveRegistryMultithreadTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/MutablePrimitiveRegistryMultithreadTest.java
new file mode 100644
index 0000000..ab8900a
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/MutablePrimitiveRegistryMultithreadTest.java
@@ -0,0 +1,176 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MutablePrimitiveRegistryMultithreadTest {
+  @Immutable
+  private static final class TestKey1 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestKey2 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestPrimitiveA {
+    public TestPrimitiveA() {}
+  }
+
+  @Immutable
+  private static final class TestPrimitiveB {
+    public TestPrimitiveB() {}
+  }
+
+  private static MutablePrimitiveRegistryMultithreadTest.TestPrimitiveA getPrimitiveAKey1(
+      MutablePrimitiveRegistryMultithreadTest.TestKey1 key) {
+    return new MutablePrimitiveRegistryMultithreadTest.TestPrimitiveA();
+  }
+
+  private static MutablePrimitiveRegistryMultithreadTest.TestPrimitiveA getPrimitiveAKey2(
+      MutablePrimitiveRegistryMultithreadTest.TestKey2 key) {
+    return new MutablePrimitiveRegistryMultithreadTest.TestPrimitiveA();
+  }
+
+  private static MutablePrimitiveRegistryMultithreadTest.TestPrimitiveB getPrimitiveBKey1(
+      MutablePrimitiveRegistryMultithreadTest.TestKey1 key) {
+    return new MutablePrimitiveRegistryMultithreadTest.TestPrimitiveB();
+  }
+
+  private static MutablePrimitiveRegistryMultithreadTest.TestPrimitiveB getPrimitiveBKey2(
+      MutablePrimitiveRegistryMultithreadTest.TestKey2 key) {
+    return new MutablePrimitiveRegistryMultithreadTest.TestPrimitiveB();
+  }
+
+  private static final int REPETITIONS = 10000;
+  private static final int THREAD_NUMBER = 12;
+
+  @Test
+  public void registerAndGetPrimitivesInParallel_works() throws Exception {
+    MutablePrimitiveRegistry registry = new MutablePrimitiveRegistry();
+    ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUMBER);
+    List<Future<?>> futures = new ArrayList<>();
+    registry.registerPrimitiveConstructor(
+        PrimitiveConstructor.create(
+            MutablePrimitiveRegistryMultithreadTest::getPrimitiveAKey1,
+            TestKey1.class,
+            TestPrimitiveA.class));
+
+    // It's questionable how mixed up things are gonna be with so few registrations but
+    // registering many constructors would require around square as many of both key and
+    // primitive test classes created, and that's gonna be a serious code bloat.
+    futures.add(
+        threadPool.submit(
+            () -> {
+              try {
+                registry.registerPrimitiveConstructor(
+                    PrimitiveConstructor.create(
+                        MutablePrimitiveRegistryMultithreadTest::getPrimitiveAKey2,
+                        TestKey2.class,
+                        TestPrimitiveA.class));
+              } catch (GeneralSecurityException e) {
+                throw new RuntimeException(e);
+              }
+            }));
+    futures.add(
+        threadPool.submit(
+            () -> {
+              try {
+                registry.registerPrimitiveConstructor(
+                    PrimitiveConstructor.create(
+                        MutablePrimitiveRegistryMultithreadTest::getPrimitiveBKey1,
+                        TestKey1.class,
+                        TestPrimitiveB.class));
+                registry.registerPrimitiveConstructor(
+                    PrimitiveConstructor.create(
+                        MutablePrimitiveRegistryMultithreadTest::getPrimitiveBKey2,
+                        TestKey2.class,
+                        TestPrimitiveB.class));
+              } catch (GeneralSecurityException e) {
+                throw new RuntimeException(e);
+              }
+            }));
+    // Thread pool size - the number of registration threads.
+    for (int i = 0; i < THREAD_NUMBER - 2; ++i) {
+      futures.add(
+          threadPool.submit(
+              () -> {
+                try {
+                  for (int j = 0; j < REPETITIONS; ++j) {
+                    TestPrimitiveA unused =
+                        registry.getPrimitive(new TestKey1(), TestPrimitiveA.class);
+                  }
+                } catch (GeneralSecurityException e) {
+                  throw new RuntimeException(e);
+                }
+              }));
+    }
+
+    threadPool.shutdown();
+    assertThat(threadPool.awaitTermination(300, SECONDS)).isTrue();
+    for (Future<?> future : futures) {
+      future.get(); // This will throw an exception if the thread threw an exception.
+    }
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/MutablePrimitiveRegistryTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/MutablePrimitiveRegistryTest.java
new file mode 100644
index 0000000..518fa2e
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/MutablePrimitiveRegistryTest.java
@@ -0,0 +1,230 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveWrapper;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link MutablePrimitiveRegistryTest}.
+ *
+ * <p>We repeat the main tests in PrimitiveRegistryTest. There really shouldn't be both classes, but
+ * currently this is what we need, and the other is what we should have.
+ */
+@RunWith(JUnit4.class)
+public final class MutablePrimitiveRegistryTest {
+  @Immutable
+  private static final class TestKey1 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestKey2 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestPrimitiveA {
+    public TestPrimitiveA() {}
+  }
+
+  @Immutable
+  private static final class TestPrimitiveB {
+    public TestPrimitiveB() {}
+  }
+
+  private static MutablePrimitiveRegistryTest.TestPrimitiveA getPrimitiveAKey1(
+      MutablePrimitiveRegistryTest.TestKey1 key) {
+    return new MutablePrimitiveRegistryTest.TestPrimitiveA();
+  }
+
+  private static MutablePrimitiveRegistryTest.TestPrimitiveA getPrimitiveAKey2(
+      MutablePrimitiveRegistryTest.TestKey2 key) {
+    return new MutablePrimitiveRegistryTest.TestPrimitiveA();
+  }
+
+  private static MutablePrimitiveRegistryTest.TestPrimitiveB getPrimitiveBKey1(
+      MutablePrimitiveRegistryTest.TestKey1 key) {
+    return new MutablePrimitiveRegistryTest.TestPrimitiveB();
+  }
+
+  private static MutablePrimitiveRegistryTest.TestPrimitiveB getPrimitiveBKey2(
+      MutablePrimitiveRegistryTest.TestKey2 key) {
+    return new MutablePrimitiveRegistryTest.TestPrimitiveB();
+  }
+
+  @Immutable
+  private static final class TestWrapperA
+      implements PrimitiveWrapper<TestPrimitiveA, TestPrimitiveA> {
+
+    @Override
+    public TestPrimitiveA wrap(final PrimitiveSet<TestPrimitiveA> primitives)
+        throws GeneralSecurityException {
+      return new TestPrimitiveA();
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getInputPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+  }
+
+  @Immutable
+  private static final class TestWrapperB
+      implements PrimitiveWrapper<TestPrimitiveA, TestPrimitiveB> {
+
+    @Override
+    public TestPrimitiveB wrap(final PrimitiveSet<TestPrimitiveA> primitives)
+        throws GeneralSecurityException {
+      return new TestPrimitiveB();
+    }
+
+    @Override
+    public Class<TestPrimitiveB> getPrimitiveClass() {
+      return TestPrimitiveB.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getInputPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+  }
+
+  @Test
+  public void test_registerAll_checkDispatch() throws Exception {
+    MutablePrimitiveRegistry registry = new MutablePrimitiveRegistry();
+
+    registry.registerPrimitiveConstructor(
+        PrimitiveConstructor.create(
+            MutablePrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class));
+    registry.registerPrimitiveConstructor(
+        PrimitiveConstructor.create(
+            MutablePrimitiveRegistryTest::getPrimitiveAKey2, TestKey2.class, TestPrimitiveA.class));
+    registry.registerPrimitiveConstructor(
+        PrimitiveConstructor.create(
+            MutablePrimitiveRegistryTest::getPrimitiveBKey1, TestKey1.class, TestPrimitiveB.class));
+    registry.registerPrimitiveConstructor(
+        PrimitiveConstructor.create(
+            MutablePrimitiveRegistryTest::getPrimitiveBKey2, TestKey2.class, TestPrimitiveB.class));
+
+    assertThat(
+            registry.getPrimitive(
+                new MutablePrimitiveRegistryTest.TestKey1(),
+                MutablePrimitiveRegistryTest.TestPrimitiveA.class))
+        .isNotNull();
+    assertThat(
+            registry.getPrimitive(
+                new MutablePrimitiveRegistryTest.TestKey2(),
+                MutablePrimitiveRegistryTest.TestPrimitiveA.class))
+        .isNotNull();
+    assertThat(
+            registry.getPrimitive(
+                new MutablePrimitiveRegistryTest.TestKey1(),
+                MutablePrimitiveRegistryTest.TestPrimitiveB.class))
+        .isNotNull();
+    assertThat(
+            registry.getPrimitive(
+                new MutablePrimitiveRegistryTest.TestKey2(),
+                MutablePrimitiveRegistryTest.TestPrimitiveB.class))
+        .isNotNull();
+  }
+
+  @Test
+  public void test_emptyRegistry_throws() throws Exception {
+    MutablePrimitiveRegistry registry = new MutablePrimitiveRegistry();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            registry.getPrimitive(
+                new MutablePrimitiveRegistryTest.TestKey1(),
+                MutablePrimitiveRegistryTest.TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.getInputPrimitiveClass(MutablePrimitiveRegistryTest.TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            registry.wrap(
+                PrimitiveSet.newBuilder(MutablePrimitiveRegistryTest.TestPrimitiveA.class).build(),
+                MutablePrimitiveRegistryTest.TestPrimitiveA.class));
+  }
+
+  @Test
+  public void test_registerAllWrappers_checkDispatch() throws Exception {
+    MutablePrimitiveRegistry registry = new MutablePrimitiveRegistry();
+
+    registry.registerPrimitiveWrapper(new TestWrapperA());
+    registry.registerPrimitiveWrapper(new TestWrapperB());
+
+    assertThat(registry.getInputPrimitiveClass(TestPrimitiveA.class))
+        .isEqualTo(TestPrimitiveA.class);
+    assertThat(registry.getInputPrimitiveClass(TestPrimitiveB.class))
+        .isEqualTo(TestPrimitiveA.class);
+    assertThat(
+            registry.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class))
+        .isInstanceOf(TestPrimitiveA.class);
+    assertThat(
+            registry.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveB.class))
+        .isInstanceOf(TestPrimitiveB.class);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryMultithreadTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryMultithreadTest.java
index a3c5d65..af278a2 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryMultithreadTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryMultithreadTest.java
@@ -291,7 +291,7 @@
             () -> {
               try {
                 for (int i = 0; i < REPETITIONS; ++i) {
-                  registry.parseKey(new TestSerializationA(A_1), ACCESS);
+                  Object unused = registry.parseKey(new TestSerializationA(A_1), ACCESS);
                 }
               } catch (GeneralSecurityException e) {
                 throw new RuntimeException(e);
@@ -302,7 +302,8 @@
             () -> {
               try {
                 for (int i = 0; i < REPETITIONS; ++i) {
-                  registry.serializeKey(new TestKey1(), TestSerializationA.class, ACCESS);
+                  Object unused =
+                      registry.serializeKey(new TestKey1(), TestSerializationA.class, ACCESS);
                 }
               } catch (GeneralSecurityException e) {
                 throw new RuntimeException(e);
@@ -363,7 +364,7 @@
             () -> {
               try {
                 for (int i = 0; i < REPETITIONS; ++i) {
-                  registry.parseParameters(new TestSerializationA(A_1));
+                  Object unused = registry.parseParameters(new TestSerializationA(A_1));
                 }
               } catch (GeneralSecurityException e) {
                 throw new RuntimeException(e);
@@ -374,7 +375,8 @@
             () -> {
               try {
                 for (int i = 0; i < REPETITIONS; ++i) {
-                  registry.serializeParameters(new TestParameters1(), TestSerializationA.class);
+                  Object unused =
+                      registry.serializeParameters(new TestParameters1(), TestSerializationA.class);
                 }
               } catch (GeneralSecurityException e) {
                 throw new RuntimeException(e);
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryTest.java
index f2c7c1b..16130ae 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/MutableSerializationRegistryTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.Key;
@@ -36,7 +37,7 @@
 import org.junit.runners.JUnit4;
 
 /**
- * Thread safety tests for {@link MutableSerializationRegistry}.
+ * Tests for {@link MutableSerializationRegistry}.
  *
  * <p>We repeat the main tests in SerializationRegistryTest. There really shouldn't be both classes,
  * but currently this is what we need, and the other is what we should have.
@@ -220,6 +221,7 @@
             MutableSerializationRegistryTest::serializeKey2ToB,
             TestKey2.class,
             TestSerializationB.class));
+    assertThat(registry.hasSerializerForKey(new TestKey1(), TestSerializationA.class)).isTrue();
     assertThat(
             registry
                 .serializeKey(new TestKey1(), TestSerializationA.class, ACCESS)
@@ -243,6 +245,16 @@
   }
 
   @Test
+  public void emptyRegistry_serializeKey_throws() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    assertThat(registry.hasSerializerForKey(new TestKey1(), TestSerializationA.class)).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry
+                .serializeKey(new TestKey1(), TestSerializationA.class, ACCESS));
+  }
+
+  @Test
   public void test_registerAllParsers_checkDispatch() throws Exception {
     MutableSerializationRegistry registry = new MutableSerializationRegistry();
     registry.registerKeyParser(
@@ -257,12 +269,22 @@
     registry.registerKeyParser(
         KeyParser.create(
             MutableSerializationRegistryTest::parseBToKey2, B_2, TestSerializationB.class));
+    assertThat(registry.hasParserForKey(new TestSerializationA(A_1))).isTrue();
     assertThat(registry.parseKey(new TestSerializationA(A_1), ACCESS)).isInstanceOf(TestKey1.class);
     assertThat(registry.parseKey(new TestSerializationA(A_2), ACCESS)).isInstanceOf(TestKey2.class);
     assertThat(registry.parseKey(new TestSerializationB(B_1), ACCESS)).isInstanceOf(TestKey1.class);
     assertThat(registry.parseKey(new TestSerializationB(B_2), ACCESS)).isInstanceOf(TestKey2.class);
   }
 
+  @Test
+  public void emptyRegistry_parseKey_throws() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    assertThat(registry.hasParserForKey(new TestSerializationA(A_1))).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(new TestSerializationA(A_1), ACCESS));
+  }
+
   // ================================================================================================
   // PARAMETERS TESTS
   // ================================================================================================
@@ -341,6 +363,8 @@
             MutableSerializationRegistryTest::serializeParameters2ToB,
             TestParameters2.class,
             TestSerializationB.class));
+    assertThat(registry.hasSerializerForParameters(new TestParameters1(), TestSerializationA.class))
+        .isTrue();
     assertThat(
             registry
                 .serializeParameters(new TestParameters1(), TestSerializationA.class)
@@ -364,6 +388,16 @@
   }
 
   @Test
+  public void emptyRegistry_serializeParameters_throws() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    assertThat(registry.hasSerializerForParameters(new TestParameters1(), TestSerializationA.class))
+        .isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeParameters(new TestParameters1(), TestSerializationA.class));
+  }
+
+  @Test
   public void test_registerAllParametersParsers_checkDispatch() throws Exception {
     MutableSerializationRegistry registry = new MutableSerializationRegistry();
     registry.registerParametersParser(
@@ -378,6 +412,7 @@
     registry.registerParametersParser(
         ParametersParser.create(
             MutableSerializationRegistryTest::parseBToParameters2, B_2, TestSerializationB.class));
+    assertThat(registry.hasParserForParameters(new TestSerializationA(A_1))).isTrue();
     assertThat(registry.parseParameters(new TestSerializationA(A_1)))
         .isInstanceOf(TestParameters1.class);
     assertThat(registry.parseParameters(new TestSerializationA(A_2)))
@@ -389,6 +424,15 @@
   }
 
   @Test
+  public void emptyRegistry_parseParameters_throws() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    assertThat(registry.hasParserForParameters(new TestSerializationA(A_1))).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseParameters(new TestSerializationA(A_1)));
+  }
+
+  @Test
   public void test_parseParametersWithLegacyFallback_testFallback() throws Exception {
     MutableSerializationRegistry registry = new MutableSerializationRegistry();
     ProtoParametersSerialization protoParameters =
@@ -406,6 +450,11 @@
     return new TestParameters1();
   }
 
+  private static TestParameters1 parseParametersAlwaysThrows(
+      ProtoParametersSerialization serialization) throws GeneralSecurityException {
+    throw new GeneralSecurityException("Always throws");
+  }
+
   @Test
   public void test_parseParametersWithLegacyFallback_testRegistered() throws Exception {
     MutableSerializationRegistry registry = new MutableSerializationRegistry();
@@ -422,6 +471,23 @@
   }
 
   @Test
+  public void test_parseParametersWithLegacyFallback_testRegisteredButFaulty_throws()
+      throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    registry.registerParametersParser(
+        ParametersParser.create(
+            MutableSerializationRegistryTest::parseParametersAlwaysThrows,
+            Util.toBytesFromPrintableAscii("typeUrlForTesting98178"),
+            ProtoParametersSerialization.class));
+    ProtoParametersSerialization protoParameters =
+        ProtoParametersSerialization.create(
+            "typeUrlForTesting98178", OutputPrefixType.TINK, TestProto.getDefaultInstance());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseParametersWithLegacyFallback(protoParameters));
+  }
+
+  @Test
   public void test_parseKeyWithLegacyFallback_testFallback() throws Exception {
     MutableSerializationRegistry registry = new MutableSerializationRegistry();
     ProtoKeySerialization protoKey =
@@ -462,4 +528,33 @@
     Key key = registry.parseKeyWithLegacyFallback(protoKey, InsecureSecretKeyAccess.get());
     assertThat(key).isInstanceOf(TestKey1.class);
   }
+
+  @Test
+  public void test_parseKeyWithLegacyFallback_testFallback_missingAccess() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    ProtoKeySerialization protoKey =
+        ProtoKeySerialization.create(
+            "typeUrlForTesting21125",
+            ByteString.EMPTY,
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKeyWithLegacyFallback(protoKey, null));
+  }
+
+  @Test
+  public void test_parseKeyWithLegacyFallback_testFallback_accessNotNeededRemote()
+      throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    ProtoKeySerialization protoKey =
+        ProtoKeySerialization.create(
+            "typeUrlForTesting21125",
+            ByteString.EMPTY,
+            KeyMaterialType.REMOTE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+    Key key = registry.parseKeyWithLegacyFallback(protoKey, InsecureSecretKeyAccess.get());
+    assertThat(key).isInstanceOf(LegacyProtoKey.class);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/ParametersParserTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/ParametersParserTest.java
index 1a654ae..7849a6c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/ParametersParserTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/ParametersParserTest.java
@@ -53,8 +53,9 @@
 
   @Test
   public void createParser_works() throws Exception {
-    ParametersParser.create(
-        ParametersParserTest::parse, Bytes.copyFrom(new byte[0]), ExampleSerialization.class);
+    Object unused =
+        ParametersParser.create(
+            ParametersParserTest::parse, Bytes.copyFrom(new byte[0]), ExampleSerialization.class);
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/ParametersSerializerTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/ParametersSerializerTest.java
index 210aaef..9752ef4 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/ParametersSerializerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/ParametersSerializerTest.java
@@ -53,8 +53,11 @@
 
   @Test
   public void createSerializer_works() throws Exception {
-    ParametersSerializer.create(
-        ParametersSerializerTest::serialize, ExampleParameters.class, ExampleSerialization.class);
+    Object unused =
+        ParametersSerializer.create(
+            ParametersSerializerTest::serialize,
+            ExampleParameters.class,
+            ExampleSerialization.class);
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/PrimitiveConstructorTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/PrimitiveConstructorTest.java
new file mode 100644
index 0000000..65940b5
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/PrimitiveConstructorTest.java
@@ -0,0 +1,85 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.errorprone.annotations.Immutable;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class PrimitiveConstructorTest {
+  @Immutable
+  private static final class TestKey extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestPrimitive {
+    public TestPrimitive() {}
+  }
+
+  private static TestPrimitive getTestPrimitive(TestKey key) {
+    return new TestPrimitive();
+  }
+
+  @Test
+  public void test_primitiveConstructorCreate_works() throws Exception {
+    PrimitiveConstructor<TestKey, TestPrimitive> unused = PrimitiveConstructor.create(
+        PrimitiveConstructorTest::getTestPrimitive, TestKey.class, TestPrimitive.class);
+  }
+
+  @Test
+  public void test_primitiveConstructorConstructPrimitive_works() throws Exception {
+    PrimitiveConstructor<TestKey, TestPrimitive> primitiveConstructor = PrimitiveConstructor.create(
+        PrimitiveConstructorTest::getTestPrimitive, TestKey.class, TestPrimitive.class);
+    assertThat(primitiveConstructor.constructPrimitive(new TestKey())).isNotNull();
+  }
+
+  @Test
+  public void test_primitiveConstructorGetKeyClass_works() throws Exception {
+    PrimitiveConstructor<TestKey, TestPrimitive> primitiveConstructor = PrimitiveConstructor.create(
+        PrimitiveConstructorTest::getTestPrimitive, TestKey.class, TestPrimitive.class);
+    assertThat(primitiveConstructor.getKeyClass()).isEqualTo(TestKey.class);
+  }
+
+  @Test
+  public void test_primitiveConstructorGetPrimitiveClass_works() throws Exception {
+    PrimitiveConstructor<TestKey, TestPrimitive> primitiveConstructor = PrimitiveConstructor.create(
+        PrimitiveConstructorTest::getTestPrimitive, TestKey.class, TestPrimitive.class);
+    assertThat(primitiveConstructor.getPrimitiveClass()).isEqualTo(TestPrimitive.class);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/PrimitiveRegistryTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/PrimitiveRegistryTest.java
new file mode 100644
index 0000000..bc4cdc5
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/PrimitiveRegistryTest.java
@@ -0,0 +1,382 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.PrimitiveWrapper;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PrimitiveRegistry}. */
+@RunWith(JUnit4.class)
+public final class PrimitiveRegistryTest {
+  // ===============================================================================================
+  // SETUP:
+  // We create 2 different key classes (TestKey1, TestKey2) and two different primitive classes
+  // (TestPrimitiveA, TestPrimitiveB), and provide ways to create both primitives with both keys.
+  //
+  // For this, we provide the methods getPrimitive{A,B}Key{1,2}. The method getPrimitiveBKey1 then
+  // uses the key of type TestKey1 to create a primitive of type TestPrimitiveB.
+  //
+  // Note that calling these multiple times will give different objects (which allows us to test
+  // that registering different objects for the same task fails).
+  // ===============================================================================================
+
+  @Immutable
+  private static final class TestKey1 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestKey2 extends Key {
+    @Override
+    public Parameters getParameters() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    @Nullable
+    public Integer getIdRequirementOrNull() {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+
+    @Override
+    public boolean equalsKey(Key other) {
+      throw new UnsupportedOperationException("Not needed in test");
+    }
+  }
+
+  @Immutable
+  private static final class TestPrimitiveA {
+    public TestPrimitiveA() {}
+  }
+
+  @Immutable
+  private static final class TestPrimitiveB {
+    public TestPrimitiveB() {}
+  }
+
+  @Immutable
+  private static final class TestWrapperA
+      implements PrimitiveWrapper<TestPrimitiveA, TestPrimitiveA> {
+
+    @Override
+    public TestPrimitiveA wrap(final PrimitiveSet<TestPrimitiveA> primitives)
+        throws GeneralSecurityException {
+      return new TestPrimitiveA();
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getInputPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+  }
+
+  @Immutable
+  private static final class TestWrapperB
+      implements PrimitiveWrapper<TestPrimitiveA, TestPrimitiveB> {
+
+    @Override
+    public TestPrimitiveB wrap(final PrimitiveSet<TestPrimitiveA> primitives)
+        throws GeneralSecurityException {
+      return new TestPrimitiveB();
+    }
+
+    @Override
+    public Class<TestPrimitiveB> getPrimitiveClass() {
+      return TestPrimitiveB.class;
+    }
+
+    @Override
+    public Class<TestPrimitiveA> getInputPrimitiveClass() {
+      return TestPrimitiveA.class;
+    }
+  }
+
+  private static TestPrimitiveA getPrimitiveAKey1(TestKey1 key) {
+    return new TestPrimitiveA();
+  }
+
+  private static TestPrimitiveA getPrimitiveAKey2(TestKey2 key) {
+    return new TestPrimitiveA();
+  }
+
+  private static TestPrimitiveB getPrimitiveBKey1(TestKey1 key) {
+    return new TestPrimitiveB();
+  }
+
+  private static TestPrimitiveB getPrimitiveBKey2(TestKey2 key) {
+    return new TestPrimitiveB();
+  }
+
+  /** Test PrimitiveConstructor functionality. */
+  @Test
+  public void test_registerConstructorAndGet() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class))
+            .build();
+    assertThat(registry.getPrimitive(new TestKey1(), TestPrimitiveA.class)).isNotNull();
+  }
+
+  @Test
+  public void test_emptyRegistry_throws() throws Exception {
+    PrimitiveRegistry registry = PrimitiveRegistry.builder().build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.getPrimitive(new TestKey1(), TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            registry.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.getInputPrimitiveClass(TestPrimitiveA.class));
+  }
+
+  @Test
+  public void test_registerSameConstructorTwice_works() throws Exception {
+    PrimitiveConstructor<TestKey1, TestPrimitiveA> testPrimitiveConstructor =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class);
+    PrimitiveRegistry unused =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(testPrimitiveConstructor)
+            .registerPrimitiveConstructor(testPrimitiveConstructor)
+            .build();
+  }
+
+  @Test
+  public void test_registerDifferentConstructorWithSameKeyType_throws() throws Exception {
+    PrimitiveConstructor<TestKey1, TestPrimitiveA> testPrimitiveConstructor1 =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class);
+    PrimitiveConstructor<TestKey1, TestPrimitiveA> testPrimitiveConstructor2 =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class);
+    PrimitiveRegistry.Builder builder = PrimitiveRegistry.builder();
+    builder.registerPrimitiveConstructor(testPrimitiveConstructor1);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> builder.registerPrimitiveConstructor(testPrimitiveConstructor2));
+  }
+
+  @Test
+  public void test_registerDifferentConstructorWithDifferentKeyType_works() throws Exception {
+    PrimitiveConstructor<TestKey1, TestPrimitiveA> testPrimitiveConstructor1 =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class);
+    PrimitiveConstructor<TestKey2, TestPrimitiveA> testPrimitiveConstructor2 =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveAKey2, TestKey2.class, TestPrimitiveA.class);
+    PrimitiveRegistry unused =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(testPrimitiveConstructor1)
+            .registerPrimitiveConstructor(testPrimitiveConstructor2)
+            .build();
+  }
+
+  @Test
+  public void test_registerDifferentConstructorWithDifferentPrimitiveType_works()
+      throws Exception {
+    PrimitiveConstructor<TestKey1, TestPrimitiveA> testPrimitiveConstructor1 =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class);
+    PrimitiveConstructor<TestKey1, TestPrimitiveB> testPrimitiveConstructor2 =
+        PrimitiveConstructor.create(
+            PrimitiveRegistryTest::getPrimitiveBKey1, TestKey1.class, TestPrimitiveB.class);
+    PrimitiveRegistry unused =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(testPrimitiveConstructor1)
+            .registerPrimitiveConstructor(testPrimitiveConstructor2)
+            .build();
+  }
+
+  @Test
+  public void test_registerAllConstructors_checkDispatch() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class))
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    PrimitiveRegistryTest::getPrimitiveAKey2, TestKey2.class, TestPrimitiveA.class))
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    PrimitiveRegistryTest::getPrimitiveBKey1, TestKey1.class, TestPrimitiveB.class))
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    PrimitiveRegistryTest::getPrimitiveBKey2, TestKey2.class, TestPrimitiveB.class))
+            .build();
+    assertThat(registry.getPrimitive(new TestKey1(), TestPrimitiveA.class))
+        .isInstanceOf(TestPrimitiveA.class);
+    assertThat(registry.getPrimitive(new TestKey2(), TestPrimitiveA.class))
+        .isInstanceOf(TestPrimitiveA.class);
+    assertThat(registry.getPrimitive(new TestKey1(), TestPrimitiveB.class))
+        .isInstanceOf(TestPrimitiveB.class);
+    assertThat(registry.getPrimitive(new TestKey2(), TestPrimitiveB.class))
+        .isInstanceOf(TestPrimitiveB.class);
+  }
+
+  /** Test PrimitiveWrapper functionality. */
+  @Test
+  public void test_registerWrapperAndGet() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder().registerPrimitiveWrapper(new TestWrapperA()).build();
+    assertThat(
+            registry.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class))
+        .isNotNull();
+  }
+
+  @Test
+  public void test_registerSameWrapperTwice_works() throws Exception {
+    TestWrapperA wrapper = new TestWrapperA();
+    PrimitiveRegistry unused =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveWrapper(wrapper)
+            .registerPrimitiveWrapper(wrapper)
+            .build();
+  }
+
+  @Test
+  public void test_registerDifferentWrapperWithSamePrimitiveType_throws() throws Exception {
+    PrimitiveRegistry.Builder builder = PrimitiveRegistry.builder();
+    builder.registerPrimitiveWrapper(new TestWrapperA());
+    assertThrows(
+        GeneralSecurityException.class, () -> builder.registerPrimitiveWrapper(new TestWrapperA()));
+  }
+
+  @Test
+  public void test_registerDifferentWrapperWithDifferentPrimitiveType_works() throws Exception {
+    PrimitiveRegistry unused =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveWrapper(new TestWrapperA())
+            .registerPrimitiveWrapper(new TestWrapperB())
+            .build();
+  }
+
+  @Test
+  public void test_registerAllWrappers_checkDispatch() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveWrapper(new TestWrapperA())
+            .registerPrimitiveWrapper(new TestWrapperB())
+            .build();
+    assertThat(registry.getInputPrimitiveClass(TestPrimitiveA.class))
+        .isEqualTo(TestPrimitiveA.class);
+    assertThat(registry.getInputPrimitiveClass(TestPrimitiveB.class))
+        .isEqualTo(TestPrimitiveA.class);
+    assertThat(
+            registry.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class))
+        .isInstanceOf(TestPrimitiveA.class);
+    assertThat(
+            registry.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveB.class))
+        .isInstanceOf(TestPrimitiveB.class);
+  }
+
+  /** Test general functionality. */
+  @Test
+  public void test_copyWorks() throws Exception {
+    PrimitiveRegistry registry =
+        PrimitiveRegistry.builder()
+            .registerPrimitiveConstructor(
+                PrimitiveConstructor.create(
+                    PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class))
+            .registerPrimitiveWrapper(new TestWrapperA())
+            .build();
+    PrimitiveRegistry registry2 = PrimitiveRegistry.builder(registry).build();
+    assertThat(registry2.getPrimitive(new TestKey1(), TestPrimitiveA.class))
+        .isInstanceOf(TestPrimitiveA.class);
+    assertThat(
+            registry2.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class))
+        .isInstanceOf(TestPrimitiveA.class);
+    assertThat(registry2.getInputPrimitiveClass(TestPrimitiveA.class))
+        .isEqualTo(TestPrimitiveA.class);
+  }
+
+  @Test
+  public void test_copyDoesNotChangeOldVersion() throws Exception {
+    PrimitiveRegistry registry1 = PrimitiveRegistry.builder().build();
+    PrimitiveRegistry.Builder builder = PrimitiveRegistry.builder(registry1);
+    PrimitiveRegistry registry2 = builder.build();
+
+    builder
+        .registerPrimitiveConstructor(
+            PrimitiveConstructor.create(
+                PrimitiveRegistryTest::getPrimitiveAKey1, TestKey1.class, TestPrimitiveA.class))
+        .registerPrimitiveWrapper(new TestWrapperA());
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry1.getPrimitive(new TestKey1(), TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry2.getPrimitive(new TestKey1(), TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry1.getInputPrimitiveClass(TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry2.getInputPrimitiveClass(TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            registry1.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            registry2.wrap(
+                PrimitiveSet.newBuilder(TestPrimitiveA.class).build(), TestPrimitiveA.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/ProtoKeySerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/ProtoKeySerializationTest.java
index b0edf5e..23e330b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/ProtoKeySerializationTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/ProtoKeySerializationTest.java
@@ -69,11 +69,16 @@
     final ByteString value = ByteString.copyFrom(new byte[] {10, 11, 12});
     final KeyMaterialType keyMaterialType = KeyMaterialType.SYMMETRIC;
 
-    ProtoKeySerialization.create(
-        typeUrl, value, keyMaterialType, OutputPrefixType.RAW, /* idRequirement = */ null);
-    ProtoKeySerialization.create(typeUrl, value, keyMaterialType, OutputPrefixType.TINK, 123);
-    ProtoKeySerialization.create(typeUrl, value, keyMaterialType, OutputPrefixType.CRUNCHY, 123);
-    ProtoKeySerialization.create(typeUrl, value, keyMaterialType, OutputPrefixType.LEGACY, 123);
+    Object unused =
+        ProtoKeySerialization.create(
+            typeUrl, value, keyMaterialType, OutputPrefixType.RAW, /* idRequirement= */ null);
+    unused =
+        ProtoKeySerialization.create(typeUrl, value, keyMaterialType, OutputPrefixType.TINK, 123);
+    unused =
+        ProtoKeySerialization.create(
+            typeUrl, value, keyMaterialType, OutputPrefixType.CRUNCHY, 123);
+    unused =
+        ProtoKeySerialization.create(typeUrl, value, keyMaterialType, OutputPrefixType.LEGACY, 123);
 
     assertThrows(
         GeneralSecurityException.class,
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/RegistryConfigurationTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/RegistryConfigurationTest.java
new file mode 100644
index 0000000..5a7ef75
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/RegistryConfigurationTest.java
@@ -0,0 +1,201 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.aead.AesEaxKey;
+import com.google.crypto.tink.aead.AesEaxParameters;
+import com.google.crypto.tink.aead.AesEaxParameters.Variant;
+import com.google.crypto.tink.mac.ChunkedMac;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.mac.HmacParameters.HashType;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.proto.HmacParams;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link RegistryConfiguration}. */
+@RunWith(JUnit4.class)
+public class RegistryConfigurationTest {
+  private static final int HMAC_KEY_SIZE = 20;
+  private static final int HMAC_TAG_SIZE = 10;
+
+  private static HmacKey rawKey;
+  private static KeyData rawKeyData;
+  private static Keyset.Key rawKeysetKey;
+
+  @Before
+  public void setUp() throws GeneralSecurityException {
+    MacConfig.register();
+    createTestKeys();
+  }
+
+  private static void createTestKeys() {
+    try {
+      rawKey =
+          HmacKey.builder()
+              .setParameters(
+                  HmacParameters.builder()
+                      .setKeySizeBytes(HMAC_KEY_SIZE)
+                      .setTagSizeBytes(HMAC_TAG_SIZE)
+                      .setVariant(HmacParameters.Variant.NO_PREFIX)
+                      .setHashType(HashType.SHA256)
+                      .build())
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+
+      // Create the proto key artefacts.
+      KeysetHandle keysetHandle =
+          KeysetHandle.newBuilder()
+              .addEntry(KeysetHandle.importKey(rawKey).withRandomId().makePrimary())
+              .build();
+      rawKeyData =
+          KeyData.newBuilder()
+              .setValue(
+                  com.google.crypto.tink.proto.HmacKey.newBuilder()
+                      .setParams(
+                          HmacParams.newBuilder()
+                              .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                              .setTagSize(HMAC_TAG_SIZE)
+                              .build())
+                      .setKeyValue(
+                          ByteString.copyFrom(
+                              rawKey.getKeyBytes().toByteArray(InsecureSecretKeyAccess.get())))
+                      .build()
+                      .toByteString())
+              .setTypeUrl(keysetHandle.getKeysetInfo().getKeyInfo(0).getTypeUrl())
+              .setKeyMaterialType(KeyMaterialType.SYMMETRIC)
+              .build();
+      rawKeysetKey =
+          Keyset.Key.newBuilder()
+              .setKeyData(rawKeyData)
+              .setStatus(KeyStatusType.ENABLED)
+              .setKeyId(keysetHandle.getKeysetInfo().getPrimaryKeyId())
+              .setOutputPrefixType(OutputPrefixType.RAW)
+              .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void getLegacyPrimitive_matchesRegistry() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    Mac configurationMac =
+        RegistryConfiguration.get().getLegacyPrimitive(rawKeyData, Mac.class);
+    Mac registryMac = Registry.getPrimitive(rawKeyData, Mac.class);
+
+    assertThat(configurationMac.computeMac(plaintext)).isEqualTo(registryMac.computeMac(plaintext));
+  }
+
+  @Test
+  public void getPrimitive_matchesRegistry() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    ChunkedMac configurationMac =
+        RegistryConfiguration.get().getPrimitive(rawKey, ChunkedMac.class);
+    ChunkedMacComputation configurationComputation = configurationMac.createComputation();
+    ChunkedMac registryMac =
+        MutablePrimitiveRegistry.globalInstance().getPrimitive(rawKey, ChunkedMac.class);
+    ChunkedMacComputation registryComputation = registryMac.createComputation();
+
+    configurationComputation.update(ByteBuffer.wrap(plaintext));
+    registryComputation.update(ByteBuffer.wrap(plaintext));
+
+    assertThat(configurationComputation.computeMac()).isEqualTo(registryComputation.computeMac());
+  }
+
+  @Test
+  public void wrap_matchesRegistry() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+
+    Mac registryMac = Registry.getPrimitive(rawKeyData, Mac.class);
+    Mac configurationMac = RegistryConfiguration.get().getLegacyPrimitive(rawKeyData, Mac.class);
+    Mac wrappedConfigurationMac =
+        RegistryConfiguration.get()
+            .wrap(
+                PrimitiveSet.newBuilder(Mac.class)
+                    .addPrimaryFullPrimitiveAndOptionalPrimitive(
+                        null, configurationMac, rawKeysetKey)
+                    .build(),
+                Mac.class);
+
+    assertThat(wrappedConfigurationMac.computeMac(plaintext))
+        .isEqualTo(registryMac.computeMac(plaintext));
+  }
+
+  @Test
+  public void getInputPrimitiveClass_matchesRegistry() throws Exception {
+    assertThat(RegistryConfiguration.get().getInputPrimitiveClass(ChunkedMac.class))
+        .isEqualTo(Registry.getInputPrimitive(ChunkedMac.class));
+  }
+
+  @Test
+  public void getInputPrimitiveClass_returnsNullOnUnregisteredPrimitive() throws Exception {
+    assertThat(RegistryConfiguration.get().getInputPrimitiveClass(Aead.class))
+        .isNull();
+  }
+
+  @Test
+  public void requestingUnregisteredPrimitives_throws() throws GeneralSecurityException {
+    AesEaxKey aesEaxKey =
+        AesEaxKey.builder()
+            .setKeyBytes(SecretBytes.randomBytes(32))
+            .setIdRequirement(1234)
+            .setParameters(
+                AesEaxParameters.builder()
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setKeySizeBytes(32)
+                    .setVariant(Variant.TINK)
+                    .build())
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RegistryConfiguration.get().getPrimitive(aesEaxKey, Aead.class));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RegistryConfiguration.get()
+                .wrap(PrimitiveSet.newBuilder(Aead.class).build(), Aead.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/SerializationRegistryTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/SerializationRegistryTest.java
index e0f5398..8076d00 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/SerializationRegistryTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/SerializationRegistryTest.java
@@ -216,12 +216,14 @@
                     TestKey1.class,
                     TestSerializationA.class))
             .build();
+    assertThat(registry.hasSerializerForKey(new TestKey1(), TestSerializationA.class)).isTrue();
     assertThat(registry.serializeKey(new TestKey1(), TestSerializationA.class, ACCESS)).isNotNull();
   }
 
   @Test
   public void test_emptyRegistry_throws() throws Exception {
     SerializationRegistry registry = new SerializationRegistry.Builder().build();
+    assertThat(registry.hasSerializerForKey(new TestKey1(), TestSerializationA.class)).isFalse();
     assertThrows(
         GeneralSecurityException.class,
         () -> registry.serializeKey(new TestKey1(), TestSerializationA.class, ACCESS));
@@ -247,7 +249,7 @@
     KeySerializer<TestKey1, TestSerializationA> testSerializer =
         KeySerializer.create(
             SerializationRegistryTest::serializeKey1ToA, TestKey1.class, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerKeySerializer(testSerializer)
         .registerKeySerializer(testSerializer)
         .build();
@@ -265,7 +267,7 @@
     builder.registerKeySerializer(testSerializer1);
     assertThrows(
         GeneralSecurityException.class,
-        () -> builder.registerKeySerializer(testSerializer2).build());
+        () -> builder.registerKeySerializer(testSerializer2));
   }
 
   @Test
@@ -276,7 +278,7 @@
     KeySerializer<TestKey2, TestSerializationA> testSerializer2 =
         KeySerializer.create(
             SerializationRegistryTest::serializeKey2ToA, TestKey2.class, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerKeySerializer(testSerializer1)
         .registerKeySerializer(testSerializer2)
         .build();
@@ -291,7 +293,7 @@
     KeySerializer<TestKey2, TestSerializationA> testSerializer2 =
         KeySerializer.create(
             SerializationRegistryTest::serializeKey2ToA, TestKey2.class, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerKeySerializer(testSerializer1)
         .registerKeySerializer(testSerializer2)
         .build();
@@ -384,6 +386,7 @@
                 KeyParser.create(
                     SerializationRegistryTest::parseAToKey1, A_1, TestSerializationA.class))
             .build();
+    assertThat(registry.hasParserForKey(new TestSerializationA(A_1))).isTrue();
     assertThat(registry.parseKey(new TestSerializationA(A_1), ACCESS)).isNotNull();
   }
 
@@ -403,6 +406,7 @@
   @Test
   public void test_parse_emptyRegistry_throws() throws Exception {
     SerializationRegistry registry = new SerializationRegistry.Builder().build();
+    assertThat(registry.hasParserForKey(new TestSerializationA(A_1))).isFalse();
     assertThrows(
         GeneralSecurityException.class,
         () -> registry.parseKey(new TestSerializationA(A_1), ACCESS));
@@ -412,7 +416,7 @@
   public void test_registerSameParserTwice_works() throws Exception {
     KeyParser<TestSerializationA> testParser =
         KeyParser.create(SerializationRegistryTest::parseAToKey1, A_1, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerKeyParser(testParser)
         .registerKeyParser(testParser)
         .build();
@@ -427,7 +431,7 @@
     SerializationRegistry.Builder builder = new SerializationRegistry.Builder();
     builder.registerKeyParser(testParser1);
     assertThrows(
-        GeneralSecurityException.class, () -> builder.registerKeyParser(testParser2).build());
+        GeneralSecurityException.class, () -> builder.registerKeyParser(testParser2));
   }
 
   @Test
@@ -436,7 +440,7 @@
         KeyParser.create(SerializationRegistryTest::parseAToKey1, A_1, TestSerializationA.class);
     KeyParser<TestSerializationB> testParser2 =
         KeyParser.create(SerializationRegistryTest::parseBToKey1, B_1, TestSerializationB.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerKeyParser(testParser1)
         .registerKeyParser(testParser2)
         .build();
@@ -448,7 +452,7 @@
         KeyParser.create(SerializationRegistryTest::parseAToKey1, A_1, TestSerializationA.class);
     KeyParser<TestSerializationA> testParser2 =
         KeyParser.create(SerializationRegistryTest::parseAToKey2, A_2, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerKeyParser(testParser1)
         .registerKeyParser(testParser2)
         .build();
@@ -495,7 +499,7 @@
     SerializationRegistry.Builder builder = new SerializationRegistry.Builder(registry1);
     SerializationRegistry registry2 = builder.build();
 
-    builder
+    SerializationRegistry unused = builder
         .registerKeyParser(
             KeyParser.create(
                 SerializationRegistryTest::parseAToKey1, A_1, TestSerializationA.class))
@@ -574,6 +578,8 @@
                     TestParameters1.class,
                     TestSerializationA.class))
             .build();
+    assertThat(registry.hasSerializerForParameters(new TestParameters1(), TestSerializationA.class))
+        .isTrue();
     assertThat(registry.serializeParameters(new TestParameters1(), TestSerializationA.class))
         .isNotNull();
   }
@@ -581,6 +587,8 @@
   @Test
   public void test_emptyRegistrySerializeParameters_throws() throws Exception {
     SerializationRegistry registry = new SerializationRegistry.Builder().build();
+    assertThat(registry.hasSerializerForParameters(new TestParameters1(), TestSerializationA.class))
+        .isFalse();
     assertThrows(
         GeneralSecurityException.class,
         () -> registry.serializeParameters(new TestParameters1(), TestSerializationA.class));
@@ -593,7 +601,7 @@
             SerializationRegistryTest::serializeParameters1ToA,
             TestParameters1.class,
             TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerParametersSerializer(testSerializer)
         .registerParametersSerializer(testSerializer)
         .build();
@@ -615,7 +623,7 @@
     builder.registerParametersSerializer(testSerializer1);
     assertThrows(
         GeneralSecurityException.class,
-        () -> builder.registerParametersSerializer(testSerializer2).build());
+        () -> builder.registerParametersSerializer(testSerializer2));
   }
 
   @Test
@@ -630,7 +638,7 @@
             SerializationRegistryTest::serializeParameters2ToA,
             TestParameters2.class,
             TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerParametersSerializer(testSerializer1)
         .registerParametersSerializer(testSerializer2)
         .build();
@@ -649,7 +657,7 @@
             SerializationRegistryTest::serializeParameters2ToA,
             TestParameters2.class,
             TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerParametersSerializer(testSerializer1)
         .registerParametersSerializer(testSerializer2)
         .build();
@@ -744,12 +752,14 @@
                 ParametersParser.create(
                     SerializationRegistryTest::parseAToParameters1, A_1, TestSerializationA.class))
             .build();
+    assertThat(registry.hasParserForParameters(new TestSerializationA(A_1))).isTrue();
     assertThat(registry.parseParameters(new TestSerializationA(A_1))).isNotNull();
   }
 
   @Test
   public void test_formatParse_emptyRegistry_throws() throws Exception {
     SerializationRegistry registry = new SerializationRegistry.Builder().build();
+    assertThat(registry.hasParserForParameters(new TestSerializationA(A_1))).isFalse();
     assertThrows(
         GeneralSecurityException.class,
         () -> registry.parseParameters(new TestSerializationA(A_1)));
@@ -760,7 +770,7 @@
     ParametersParser<TestSerializationA> testParser =
         ParametersParser.create(
             SerializationRegistryTest::parseAToParameters1, A_1, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerParametersParser(testParser)
         .registerParametersParser(testParser)
         .build();
@@ -778,7 +788,7 @@
     builder.registerParametersParser(testParser1);
     assertThrows(
         GeneralSecurityException.class,
-        () -> builder.registerParametersParser(testParser2).build());
+        () -> builder.registerParametersParser(testParser2));
   }
 
   @Test
@@ -790,7 +800,7 @@
     ParametersParser<TestSerializationB> testParser2 =
         ParametersParser.create(
             SerializationRegistryTest::parseBToParameters1, B_1, TestSerializationB.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerParametersParser(testParser1)
         .registerParametersParser(testParser2)
         .build();
@@ -804,7 +814,7 @@
     ParametersParser<TestSerializationA> testParser2 =
         ParametersParser.create(
             SerializationRegistryTest::parseAToParameters2, A_2, TestSerializationA.class);
-    new SerializationRegistry.Builder()
+    SerializationRegistry unused = new SerializationRegistry.Builder()
         .registerParametersParser(testParser1)
         .registerParametersParser(testParser2)
         .build();
@@ -855,7 +865,7 @@
     SerializationRegistry.Builder builder = new SerializationRegistry.Builder(registry1);
     SerializationRegistry registry2 = builder.build();
 
-    builder
+    SerializationRegistry unused = builder
         .registerParametersParser(
             ParametersParser.create(
                 SerializationRegistryTest::parseAToParameters1, A_1, TestSerializationA.class))
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/TinkBugExceptionTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/TinkBugExceptionTest.java
index 9fd894f..5801c45 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/TinkBugExceptionTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/TinkBugExceptionTest.java
@@ -16,8 +16,10 @@
 
 package com.google.crypto.tink.internal;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
 
+import java.security.GeneralSecurityException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -32,4 +34,43 @@
   public void testException() throws Exception {
     assertThrows(TinkBugException.class, TinkBugExceptionTest::throwsTinkBugException);
   }
+
+  private static void doNothing() throws GeneralSecurityException {}
+
+  @Test
+  public void test_exceptionIsBugVoidVersion_whenNotThrown_notThrown() throws Exception {
+    TinkBugException.exceptionIsBug(TinkBugExceptionTest::doNothing);
+  }
+
+  private static void throwAnException() throws GeneralSecurityException {
+    throw new GeneralSecurityException("");
+  }
+
+  @Test
+  public void test_exceptionIsBugVoidVersion_whenThrown_throws() throws Exception {
+    assertThrows(
+        TinkBugException.class,
+        () -> TinkBugException.exceptionIsBug(TinkBugExceptionTest::throwAnException));
+  }
+
+  private static String returnHello() throws GeneralSecurityException {
+    return "Hello";
+  }
+
+  @Test
+  public void test_exceptionIsBugSupplierVersion_whenNotThrown_notThrown() throws Exception {
+    assertThat(TinkBugException.exceptionIsBug(TinkBugExceptionTest::returnHello))
+        .isEqualTo("Hello");
+  }
+
+  private static String throwAnExceptionB() throws GeneralSecurityException {
+    throw new GeneralSecurityException("");
+  }
+
+  @Test
+  public void test_exceptionIsBugSupplierVersion_whenThrown_throws() throws Exception {
+    assertThrows(
+        TinkBugException.class,
+        () -> TinkBugException.exceptionIsBug(TinkBugExceptionTest::throwAnExceptionB));
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/UtilTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/UtilTest.java
index b85301a..f91046e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/UtilTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/UtilTest.java
@@ -61,4 +61,14 @@
       assertThat(Util.getAndroidApiLevel()).isEqualTo(null);
     }
   }
+
+  @Test
+  public void testIsAndroid() throws Exception {
+    try {
+      Class<?> buildVersion = Class.forName("android.os.Build$VERSION");
+      assertThat(Util.isAndroid()).isTrue();
+    } catch (ReflectiveOperationException e) {
+      assertThat(Util.isAndroid()).isFalse();
+    }
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/testing/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/internal/testing/BUILD.bazel
index 0738f0f..012df0e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/testing/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/testing/BUILD.bazel
@@ -14,7 +14,7 @@
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_client",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -45,9 +45,9 @@
         "//src/main/java/com/google/crypto/tink:parameters",
         "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal/testing:key_with_serialization",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -64,7 +64,7 @@
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
         "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:junit_junit",
     ],
 )
diff --git a/java_src/src/test/java/com/google/crypto/tink/internal/testing/FakeMonitoringClientTest.java b/java_src/src/test/java/com/google/crypto/tink/internal/testing/FakeMonitoringClientTest.java
index 1359270..074be8d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/internal/testing/FakeMonitoringClientTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/internal/testing/FakeMonitoringClientTest.java
@@ -20,15 +20,9 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.KeyStatus;
-import com.google.crypto.tink.Parameters;
-import com.google.crypto.tink.internal.LegacyProtoParameters;
-import com.google.crypto.tink.internal.ProtoParametersSerialization;
 import com.google.crypto.tink.monitoring.MonitoringAnnotations;
 import com.google.crypto.tink.monitoring.MonitoringClient;
 import com.google.crypto.tink.monitoring.MonitoringKeysetInfo;
-import com.google.crypto.tink.proto.KeyTemplate;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.protobuf.ByteString;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,17 +31,6 @@
 @RunWith(JUnit4.class)
 public final class FakeMonitoringClientTest {
 
-  Parameters makeLegacyProtoParameters(String typeUrl) {
-    KeyTemplate template =
-        KeyTemplate.newBuilder()
-            .setTypeUrl(typeUrl)
-            .setOutputPrefixType(OutputPrefixType.TINK)
-            .setValue(ByteString.EMPTY)
-            .build();
-    ProtoParametersSerialization serialization = ProtoParametersSerialization.create(template);
-    return new LegacyProtoParameters(serialization);
-  }
-
   @Test
   public void log() throws Exception {
     FakeMonitoringClient client = new FakeMonitoringClient();
@@ -57,8 +40,8 @@
                 MonitoringAnnotations.newBuilder()
                     .add("annotation_name", "annotation_value")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringClient.Logger encLogger = client.createLogger(keysetInfo, "aead", "encrypt");
@@ -89,8 +72,8 @@
                 MonitoringAnnotations.newBuilder()
                     .add("annotation_name", "annotation_value")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringClient.Logger encLogger = client.createLogger(keysetInfo, "aead", "encrypt");
@@ -118,8 +101,8 @@
                 MonitoringAnnotations.newBuilder()
                     .add("annotation_name", "annotation_value")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringClient.Logger encLogger = client.createLogger(info, "aead", "encrypt");
@@ -144,7 +127,7 @@
     FakeMonitoringClient client = new FakeMonitoringClient();
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringClient.Logger encLogger = client.createLogger(info, "aead", "encrypt");
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel
index 217abe1..b1a6a3e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel
@@ -48,13 +48,16 @@
         "//proto:jwt_hmac_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:key",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/jwt:json_util",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_format",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key_manager",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_parameters",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_invalid_exception",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config",
@@ -62,13 +65,14 @@
         "//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -79,16 +83,19 @@
     size = "small",
     srcs = ["JwtMacWrapperTest.java"],
     deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_invalid_exception",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_validator",
         "//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -120,12 +127,13 @@
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pss_sign_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -142,7 +150,7 @@
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pss_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pss_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -159,7 +167,7 @@
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pkcs1_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_rsa_ssa_pkcs1_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -191,12 +199,13 @@
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
         "//src/main/java/com/google/crypto/tink/subtle:base64",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_sign_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -235,6 +244,7 @@
         "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/jwt:json_util",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_format",
@@ -250,10 +260,11 @@
         "//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -281,10 +292,13 @@
     size = "large",
     srcs = ["JwtPublicKeySignVerifyWrappersTest.java"],
     deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:parameters",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_invalid_exception",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_sign",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_verify",
@@ -293,6 +307,7 @@
         "//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
         "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -305,9 +320,12 @@
     deps = [
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:json_keyset_reader",
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/jwt:jwk_set_converter",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_sign",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_public_key_verify",
@@ -318,6 +336,7 @@
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_access",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -334,3 +353,146 @@
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "JwtTest",
+    size = "small",
+    srcs = ["JwtTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_mac",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_validator",
+        "//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
+        "//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtHmacParametersTest",
+    size = "small",
+    srcs = ["JwtHmacParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtHmacKeyTest",
+    size = "small",
+    srcs = ["JwtHmacKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtHmacProtoSerializationTest",
+    size = "small",
+    srcs = ["JwtHmacProtoSerializationTest.java"],
+    deps = [
+        "//proto:jwt_hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_hmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtEcdsaParametersTest",
+    size = "small",
+    srcs = ["JwtEcdsaParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtEcdsaPublicKeyTest",
+    size = "small",
+    srcs = ["JwtEcdsaPublicKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_public_key",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtEcdsaPrivateKeyTest",
+    size = "small",
+    srcs = ["JwtEcdsaPrivateKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "JwtEcdsaProtoSerializationTest",
+    size = "small",
+    srcs = ["JwtEcdsaProtoSerializationTest.java"],
+    deps = [
+        "//proto:jwt_ecdsa_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JsonUtilTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JsonUtilTest.java
index 708a73f..6bb34f4 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JsonUtilTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JsonUtilTest.java
@@ -75,11 +75,7 @@
     for (int i = 0; i < 10000; i++) {
       sb.append("}");
     }
-    try {
-      JsonUtil.parseJson(sb.toString());
-    } catch (JwtInvalidException ex) {
-      // JwtInvalidException is fine, no exception as well.
-    }
+    assertThrows(JwtInvalidException.class, () -> JsonUtil.parseJson(sb.toString()));
   }
 
   @Test
@@ -88,6 +84,11 @@
   }
 
   @Test
+  public void parseJsonObjectWithoutDuplicateKey_fail() throws Exception {
+    assertThrows(JwtInvalidException.class, () -> JsonUtil.parseJson("{\"a\":1,\"a\":2}"));
+  }
+
+  @Test
   public void parseJsonArrayWithoutComments_fail() throws Exception {
     assertThrows(JwtInvalidException.class, () -> JsonUtil.parseJson("[1, \"foo\" /* comment */]"));
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwkSetConverterTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwkSetConverterTest.java
index 58fb011..7e310a1 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwkSetConverterTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwkSetConverterTest.java
@@ -20,9 +20,16 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.JsonKeysetReader;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.KeysetInfo;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.testing.TestUtil;
@@ -30,7 +37,7 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import java.io.IOException;
+import com.google.protobuf.ByteString;
 import java.security.GeneralSecurityException;
 import java.util.HashSet;
 import org.junit.Before;
@@ -58,8 +65,8 @@
       "{\"keys\":[{"
           + "\"kty\":\"EC\","
           + "\"crv\":\"P-256\","
-          + "\"x\":\"EM8jrqGMse-PGZQnafIcgEOZ061DiA-y9aBhiBnSDKA\","
-          + "\"y\":\"UxCtK0wAqQG_e5vpr7SSgJNKt5h4z3FGZtAuBLng1uE\","
+          + "\"x\":\"ABDPI66hjLHvjxmUJ2nyHIBDmdOtQ4gPsvWgYYgZ0gyg\","
+          + "\"y\":\"AFMQrStMAKkBv3ub6a-0koCTSreYeM9xRmbQLgS54Nbh\","
           + "\"use\":\"sig\",\"alg\":\"ES256\",\"key_ops\":[\"verify\"]}]}";
 
   private static final String ES256_KEYSET_TINK =
@@ -73,8 +80,8 @@
       "{\"keys\":[{"
           + "\"kty\":\"EC\","
           + "\"crv\":\"P-256\","
-          + "\"x\":\"EM8jrqGMse-PGZQnafIcgEOZ061DiA-y9aBhiBnSDKA\","
-          + "\"y\":\"UxCtK0wAqQG_e5vpr7SSgJNKt5h4z3FGZtAuBLng1uE\","
+          + "\"x\":\"ABDPI66hjLHvjxmUJ2nyHIBDmdOtQ4gPsvWgYYgZ0gyg\","
+          + "\"y\":\"AFMQrStMAKkBv3ub6a-0koCTSreYeM9xRmbQLgS54Nbh\","
           + "\"use\":\"sig\",\"alg\":\"ES256\",\"key_ops\":[\"verify\"],"
           + "\"kid\":\"ENgjPA\"}]}";
 
@@ -88,7 +95,7 @@
   private static final String ES384_JWK_SET =
       "{\"keys\":[{\"kty\":\"EC\",\"crv\":\"P-384\","
           + "\"x\":\"ANKO9aKGgoaavieUO4j8qS-4-8Rmv4HhtTQy5dBBeDI0wYCkWbaJN6i2_ssJKwbeZA\","
-          + "\"y\":\"QcgwbHjBr5jzJ4lBYyJKm6Sk8zuM_0aprKRlBDYZj80uD-f34Em2e2C8WEbmBYtO\","
+          + "\"y\":\"AEHIMGx4wa-Y8yeJQWMiSpukpPM7jP9GqaykZQQ2GY_NLg_n9-BJtntgvFhG5gWLTg\","
           + "\"use\":\"sig\",\"alg\":\"ES384\",\"key_ops\":[\"verify\"]}]}";
 
   private static final String ES512_KEYSET =
@@ -101,10 +108,8 @@
           + "},\"status\":\"ENABLED\",\"keyId\":1570200439,\"outputPrefixType\":\"RAW\"}]}";
   private static final String ES512_JWK_SET =
       "{\"keys\":[{\"kty\":\"EC\",\"crv\":\"P-521\","
-          + "\"x\":\"ARXefB5F6PpnX9o9OoKRzW1CVrl5Ujrz6p_BHWQH_BcK5gIHmi1quGiZS3rgVqH_xON_RYkcxnIWvz"
-          + "pFSK2JFCbV\","
-          + "\"y\":\"ATht_NOX8RcbaEr1MaH-0BFTaepvpTzSfQ04C2P8VCoURB3GeVKk4VQh8O_KLSYfX-58bqEnaZ0G7W"
-          + "9qjHa2ols2\","
+          + "\"x\":\"AAEV3nweRej6Z1_aPTqCkc1tQla5eVI68-qfwR1kB_wXCuYCB5otarhomUt64Fah_8Tjf0WJHMZyFr86RUitiRQm1Q\","
+          + "\"y\":\"AAE4bfzTl_EXG2hK9TGh_tARU2nqb6U80n0NOAtj_FQqFEQdxnlSpOFUIfDvyi0mH1_ufG6hJ2mdBu1vaox2tqJbNg\","
           + "\"use\":\"sig\",\"alg\":\"ES512\",\"key_ops\":[\"verify\"]}]}";
 
   private static final String RS256_KEYSET =
@@ -325,8 +330,8 @@
       "{\"keys\":[{"
           + "\"kty\":\"EC\","
           + "\"crv\":\"P-256\","
-          + "\"x\":\"EM8jrqGMse-PGZQnafIcgEOZ061DiA-y9aBhiBnSDKA\","
-          + "\"y\":\"UxCtK0wAqQG_e5vpr7SSgJNKt5h4z3FGZtAuBLng1uE\","
+          + "\"x\":\"ABDPI66hjLHvjxmUJ2nyHIBDmdOtQ4gPsvWgYYgZ0gyg\","
+          + "\"y\":\"AFMQrStMAKkBv3ub6a-0koCTSreYeM9xRmbQLgS54Nbh\","
           + "\"use\":\"sig\",\"alg\":\"ES256\",\"key_ops\":[\"verify\"]},"
           + "{\"kty\":\"RSA\","
           + "\"n\":\"AJLKZN-5Rgal5jz6tgi-SnQ3kce8RYk2naS943OJ12qn7QraTOqhMX63NiS2iLJ8KcHxjApX3v2pSL"
@@ -336,7 +341,6 @@
           + "xlvs188\","
           + "\"e\":\"AQAB\",\"use\":\"sig\",\"alg\":\"RS256\",\"key_ops\":[\"verify\"]}]}";
 
-
   private static void assertEqualJwkSets(String jwkSet1, String jwkSet2) throws Exception {
     // Consider these strings equal, if their equal after parsing them.
     // The keys may have any order.
@@ -368,7 +372,8 @@
   }
 
   private static String convertToJwkSet(String jsonKeyset) throws Exception {
-    KeysetHandle handle = CleartextKeysetHandle.read(JsonKeysetReader.withString(jsonKeyset));
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(jsonKeyset, InsecureSecretKeyAccess.get());
     return JwkSetConverter.fromPublicKeysetHandle(handle);
   }
 
@@ -541,19 +546,19 @@
   @Test
   public void primaryKeyIdMissing_fromPublicKeysetHandleSuccess() throws Exception {
     String keyset = ES256_KEYSET.replace("\"primaryKeyId\":282600252,", "");
-    assertEqualJwkSets(convertToJwkSet(keyset), ES256_JWK_SET);
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
   }
 
   @Test
   public void legacyEcdsaKeysets_fromPublicKeysetHandleFails() throws Exception {
     String keyset = ES256_KEYSET.replace("RAW", "LEGACY");
-    assertThrows(IOException.class, () -> convertToJwkSet(keyset));
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
   }
 
   @Test
   public void crunchyEcdsaKeysets_fromPublicKeysetHandleFails() throws Exception {
     String keyset = ES256_KEYSET.replace("RAW", "CRUNCHY");
-    assertThrows(IOException.class, () -> convertToJwkSet(keyset));
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
   }
 
   @Test
@@ -570,25 +575,47 @@
   @Test
   public void legacyRsaSsaPkcs1Keysets_fromPublicKeysetHandleFails() throws Exception {
     String keyset = RS256_KEYSET.replace("RAW", "LEGACY");
-    assertThrows(IOException.class, () -> convertToJwkSet(keyset));
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
   }
 
   @Test
   public void crunchyRsaSsaPkcs1Keysets_fromPublicKeysetHandleFails() throws Exception {
     String keyset = RS256_KEYSET.replace("RAW", "CRUNCHY");
-    assertThrows(IOException.class, () -> convertToJwkSet(keyset));
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
   }
 
   @Test
   public void legacyRsaSsaPssKeysets_fromPublicKeysetHandleFails() throws Exception {
     String keyset = PS256_KEYSET.replace("RAW", "LEGACY");
-    assertThrows(IOException.class, () -> convertToJwkSet(keyset));
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
   }
 
   @Test
   public void crunchyRsaSsaPssKeysets_fromPublicKeysetHandleFails() throws Exception {
     String keyset = PS256_KEYSET.replace("RAW", "CRUNCHY");
-    assertThrows(IOException.class, () -> convertToJwkSet(keyset));
+    assertThrows(GeneralSecurityException.class, () -> convertToJwkSet(keyset));
+  }
+
+  @Test
+  public void fromPublicKeysetHandle_throwsOnInvalidKeysetHandle() throws Exception {
+    Keyset keyset =
+        Keyset.newBuilder()
+            .setPrimaryKeyId(1)
+            .addKey(
+                Keyset.Key.newBuilder()
+                    .setKeyId(1)
+                    // Keysets with unknown status are not parsed properly and will throw unchecked
+                    // at getAt()
+                    .setStatus(KeyStatusType.UNKNOWN_STATUS)
+                    .setKeyData(
+                        KeyData.newBuilder()
+                            .setTypeUrl("somenonexistenttypeurl")
+                            .setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
+                            .setValue(ByteString.EMPTY)))
+            .build();
+    KeysetHandle handle = TinkProtoKeysetFormat.parseKeysetWithoutSecret(keyset.toByteArray());
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.fromPublicKeysetHandle(handle));
   }
 
   @Test
@@ -603,7 +630,7 @@
             + "\"alg\":\"ES256\""
             + "}]}";
     // ignore returned value, we only test that it worked.
-    JwkSetConverter.toPublicKeysetHandle(jwksString);
+    Object unused = JwkSetConverter.toPublicKeysetHandle(jwksString);
   }
 
   @Test
@@ -639,7 +666,7 @@
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
     // ignore returned value, we only test that it worked.
-    JwkSetConverter.toPublicKeysetHandle(jwksString);
+    Object unused = JwkSetConverter.toPublicKeysetHandle(jwksString);
   }
 
   @Test
@@ -654,7 +681,8 @@
             + "\"use\":\"sig\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -669,7 +697,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -684,29 +713,12 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
-  }
-
-  @Test
-  public void ecdsaWithSmallX_getPrimitiveFails() throws Exception {
-    String jwksString =
-        "{"
-            + "\"keys\":[{"
-            + "\"kty\":\"EC\","
-            + "\"crv\":\"P-256\","
-            + "\"x\":\"AAAwOQ\","
-            + "\"y\":\"b22m_Y4sT-jUJSxBVqjrW_DxWyBLopxYHTuFVfx70ZI\","
-            + "\"use\":\"sig\","
-            + "\"alg\":\"ES256\","
-            + "\"key_ops\":[\"verify\"]"
-            + "}]}";
-    KeysetHandle handle = JwkSetConverter.toPublicKeysetHandle(jwksString);
     assertThrows(
-        GeneralSecurityException.class, () -> handle.getPrimitive(JwtPublicKeyVerify.class));
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
-  public void ecdsaWithSmallY_getPrimitiveFails() throws Exception {
+  public void ecdsa_pointNotOnCurve_getPrimitiveFails() throws Exception {
     String jwksString =
         "{"
             + "\"keys\":[{"
@@ -718,9 +730,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    KeysetHandle handle = JwkSetConverter.toPublicKeysetHandle(jwksString);
     assertThrows(
-        GeneralSecurityException.class, () -> handle.getPrimitive(JwtPublicKeyVerify.class));
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -736,7 +747,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -752,7 +764,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -768,7 +781,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"verify\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -784,7 +798,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":[\"invalid\"]"
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -800,7 +815,8 @@
             + "\"alg\":\"ES256\","
             + "\"key_ops\":\"verify\""
             + "}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -814,11 +830,11 @@
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"alg\":\"RS256\"}]}";
     // ignore returned value, we only test that it worked.
-    JwkSetConverter.toPublicKeysetHandle(jwksString);
+    Object unused = JwkSetConverter.toPublicKeysetHandle(jwksString);
 
     String psJwksString = jwksString.replace("RS256", "PS256");
     // ignore returned value, we only test that it worked.
-    JwkSetConverter.toPublicKeysetHandle(psJwksString);
+    unused = JwkSetConverter.toPublicKeysetHandle(psJwksString);
   }
 
   @Test
@@ -833,11 +849,11 @@
             + "\"unknown\":1234,"
             + "\"e\":\"AQAB\",\"use\":\"sig\",\"alg\":\"RS256\",\"key_ops\":[\"verify\"]}]}";
     // ignore returned value, we only test that it worked.
-    JwkSetConverter.toPublicKeysetHandle(jwksString);
+    Object unused = JwkSetConverter.toPublicKeysetHandle(jwksString);
 
     String psJwksString = jwksString.replace("RS256", "PS256");
     // ignore returned value, we only test that it worked.
-    JwkSetConverter.toPublicKeysetHandle(psJwksString);
+    unused = JwkSetConverter.toPublicKeysetHandle(psJwksString);
   }
 
   @Test
@@ -896,7 +912,8 @@
             + "azfkIogKMV7Xk0aw6nCW6h49BYuIu3TVjiToLEu5kX0z501whcCI8SA1tlicl7CzOCvVF70vg03RAB5vZQWY"
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"use\":\"sig\",\"key_ops\":[\"verify\"]}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
   }
 
   @Test
@@ -909,10 +926,12 @@
             + "azfkIogKMV7Xk0aw6nCW6h49BYuIu3TVjiToLEu5kX0z501whcCI8SA1tlicl7CzOCvVF70vg03RAB5vZQWY"
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"use\":\"sig\",\"alg\":\"RS256\",\"key_ops\":[\"verify\"]}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
 
     String psJwksString = jwksString.replace("RS256", "PS256");
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
   }
 
   @Test
@@ -941,10 +960,12 @@
             + "azfkIogKMV7Xk0aw6nCW6h49BYuIu3TVjiToLEu5kX0z501whcCI8SA1tlicl7CzOCvVF70vg03RAB5vZQWY"
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"use\":\"sig\",\"alg\":\"RS256\",\"key_ops\":[\"verify\"]}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
 
     String psJwksString = jwksString.replace("RS256", "PS256");
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
   }
 
   @Test
@@ -957,10 +978,12 @@
             + "azfkIogKMV7Xk0aw6nCW6h49BYuIu3TVjiToLEu5kX0z501whcCI8SA1tlicl7CzOCvVF70vg03RAB5vZQWY"
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"use\":\"invalid\",\"alg\":\"RS256\",\"key_ops\":[\"verify\"]}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
 
     String psJwksString = jwksString.replace("RS256", "PS256");
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
   }
 
   @Test
@@ -973,10 +996,12 @@
             + "azfkIogKMV7Xk0aw6nCW6h49BYuIu3TVjiToLEu5kX0z501whcCI8SA1tlicl7CzOCvVF70vg03RAB5vZQWY"
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"use\":\"sig\",\"alg\":\"RS256\",\"key_ops\":[\"invalid\"]}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
 
     String psJwksString = jwksString.replace("RS256", "PS256");
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
   }
 
   @Test
@@ -989,10 +1014,12 @@
             + "azfkIogKMV7Xk0aw6nCW6h49BYuIu3TVjiToLEu5kX0z501whcCI8SA1tlicl7CzOCvVF70vg03RAB5vZQWY"
             + "2oFr3AwKBYDHvsc\","
             + "\"e\":\"AQAB\",\"use\":\"sig\",\"alg\":\"RS256\",\"key_ops\":\"verify\"}]}";
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(jwksString));
 
     String psJwksString = jwksString.replace("RS256", "PS256");
-    assertThrows(IOException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
+    assertThrows(
+        GeneralSecurityException.class, () -> JwkSetConverter.toPublicKeysetHandle(psJwksString));
   }
 
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaParametersTest.java
new file mode 100644
index 0000000..c3f8215
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaParametersTest.java
@@ -0,0 +1,151 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JwtEcdsaParametersTest {
+  @Test
+  public void buildParametersAndGetProperties_es256() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtEcdsaParameters.KidStrategy.IGNORED);
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtEcdsaParameters.Algorithm.ES256);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.allowKidAbsent()).isTrue();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_es384() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtEcdsaParameters.KidStrategy.IGNORED);
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtEcdsaParameters.Algorithm.ES384);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.allowKidAbsent()).isTrue();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_es512() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtEcdsaParameters.KidStrategy.IGNORED);
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtEcdsaParameters.Algorithm.ES512);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.allowKidAbsent()).isTrue();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_kidCustom() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .build();
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtEcdsaParameters.KidStrategy.CUSTOM);
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtEcdsaParameters.Algorithm.ES256);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.allowKidAbsent()).isTrue();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_kidBase64() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    assertThat(parameters.getKidStrategy())
+        .isEqualTo(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID);
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtEcdsaParameters.Algorithm.ES256);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    assertThat(parameters.allowKidAbsent()).isFalse();
+  }
+
+  @Test
+  public void testEqualsTwoInstances() throws Exception {
+    JwtEcdsaParameters parameters1 =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    JwtEcdsaParameters parameters2 =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testEqualsHashCodeDependsOnAlgorithm() throws Exception {
+    JwtEcdsaParameters parameters1 =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    JwtEcdsaParameters parameters2 =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testEqualsHashCodeDependsOnKidStrategy() throws Exception {
+    JwtEcdsaParameters parameters1 =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    JwtEcdsaParameters parameters2 =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testAlgorithmGetEcParamtersSpec() throws Exception {
+    assertThat(JwtEcdsaParameters.Algorithm.ES256.getECParameterSpec())
+        .isEqualTo(EllipticCurvesUtil.NIST_P256_PARAMS);
+    assertThat(JwtEcdsaParameters.Algorithm.ES384.getECParameterSpec())
+        .isEqualTo(EllipticCurvesUtil.NIST_P384_PARAMS);
+    assertThat(JwtEcdsaParameters.Algorithm.ES512.getECParameterSpec())
+        .isEqualTo(EllipticCurvesUtil.NIST_P521_PARAMS);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaPrivateKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaPrivateKeyTest.java
new file mode 100644
index 0000000..a778057
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaPrivateKeyTest.java
@@ -0,0 +1,334 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.aead.ChaCha20Poly1305Key;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.crypto.tink.util.SecretBytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JwtEcdsaPrivateKeyTest {
+
+  // Test case from https://www.ietf.org/rfc/rfc6979.txt, A.2.5
+  private static final ECPoint P256_PUBLIC_POINT =
+      new ECPoint(
+          new BigInteger("60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6", 16),
+          new BigInteger("7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299", 16));
+  private static final BigInteger P256_PRIVATE_VALUE =
+      new BigInteger("C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721", 16);
+
+  // Test case from https://www.ietf.org/rfc/rfc6979.txt, A.2.5
+  private static final ECPoint P521_PUBLIC_POINT =
+      new ECPoint(
+          new BigInteger(
+              "1894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD3"
+                  + "71123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F502"
+                  + "3A4",
+              16),
+          new BigInteger(
+              "0493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A2"
+                  + "8A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDF"
+                  + "CF5",
+              16));
+  private static final BigInteger P521_PRIVATE_VALUE =
+      new BigInteger(
+          "0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75C"
+              + "AA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83"
+              + "538",
+          16);
+
+  @Test
+  public void build_kidStrategyIgnored_getProperties_es256() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+    JwtEcdsaPrivateKey privateKey =
+        JwtEcdsaPrivateKey.create(
+            publicKey,
+            SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()));
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    assertThat(privateKey.getKid()).isEqualTo(Optional.empty());
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_getProperties_es512() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+            .build();
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P521_PUBLIC_POINT)
+            .build();
+    JwtEcdsaPrivateKey privateKey =
+        JwtEcdsaPrivateKey.create(
+            publicKey,
+            SecretBigInteger.fromBigInteger(P521_PRIVATE_VALUE, InsecureSecretKeyAccess.get()));
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P521_PRIVATE_VALUE);
+    assertThat(privateKey.getKid()).isEqualTo(Optional.empty());
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void build_kidStrategyCustom_getProperties_es256() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setCustomKid("Funny Custom KID")
+            .build();
+    JwtEcdsaPrivateKey privateKey =
+        JwtEcdsaPrivateKey.create(
+            publicKey,
+            SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()));
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    assertThat(privateKey.getKid()).isEqualTo(Optional.of("Funny Custom KID"));
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void build_kidStrategyBase64_getProperties_es256() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setIdRequirement(0x1ac6a944)
+            .build();
+    JwtEcdsaPrivateKey privateKey =
+        JwtEcdsaPrivateKey.create(
+            publicKey,
+            SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()));
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    // See JwtFormatTest.getKidFromTinkOutputPrefixType_success
+    assertThat(privateKey.getKid()).isEqualTo(Optional.of("GsapRA"));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x1ac6a944);
+  }
+
+  @Test
+  public void build_validatesPrivateValue() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+    JwtEcdsaPrivateKey privateKey =
+        JwtEcdsaPrivateKey.create(
+            publicKey,
+            SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()));
+
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+
+    // If we add 1 to the private key validation will fail.
+    SecretBigInteger invalidPrivateValue =
+        SecretBigInteger.fromBigInteger(
+            P256_PRIVATE_VALUE.add(BigInteger.ONE), InsecureSecretKeyAccess.get());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> JwtEcdsaPrivateKey.create(publicKey, invalidPrivateValue));
+    // If we use 0 as private key validation will fail.
+    SecretBigInteger zero =
+        SecretBigInteger.fromBigInteger(BigInteger.ZERO, InsecureSecretKeyAccess.get());
+    assertThrows(GeneralSecurityException.class, () -> JwtEcdsaPrivateKey.create(publicKey, zero));
+    // If we use -1 as private key validation will fail.
+    SecretBigInteger minusOne =
+        SecretBigInteger.fromBigInteger(new BigInteger("-1"), InsecureSecretKeyAccess.get());
+    assertThrows(
+        GeneralSecurityException.class, () -> JwtEcdsaPrivateKey.create(publicKey, minusOne));
+  }
+
+  @Test
+  public void build_rejectsPrivateValueThatIsLargerThanOrder() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+    SecretBigInteger privateValuePlusOrder =
+        SecretBigInteger.fromBigInteger(
+            P256_PRIVATE_VALUE.add(
+                JwtEcdsaParameters.Algorithm.ES256.getECParameterSpec().getOrder()),
+            InsecureSecretKeyAccess.get());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> JwtEcdsaPrivateKey.create(publicKey, privateValuePlusOrder));
+    SecretBigInteger privateValueMinusOrder =
+        SecretBigInteger.fromBigInteger(
+            P256_PRIVATE_VALUE.subtract(
+                JwtEcdsaParameters.Algorithm.ES256.getECParameterSpec().getOrder()),
+            InsecureSecretKeyAccess.get());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> JwtEcdsaPrivateKey.create(publicKey, privateValueMinusOrder));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey kidStrategyIgnoredPublicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+
+    // Uses generator as public key, and private key as ONE
+    JwtEcdsaPublicKey kidStrategyIgnoredPublicKeyForOne =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(parameters.getAlgorithm().getECParameterSpec().getGenerator())
+            .build();
+
+    JwtEcdsaPublicKey kidStrategyBase64PublicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .build())
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setIdRequirement(1907)
+            .build();
+
+    JwtEcdsaPublicKey kidStrategyIgnoredPublicKeyES512 =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+                    .build())
+            .setPublicPoint(P521_PUBLIC_POINT)
+            .build();
+
+    JwtEcdsaParameters parametersCustomKid =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey publicKeyCustomKid1 =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parametersCustomKid)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setCustomKid("CustomKID1")
+            .build();
+    JwtEcdsaPublicKey publicKeyCustomKid2 =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parametersCustomKid)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setCustomKid("CustomKID2")
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "kidStrategyIgnored",
+            JwtEcdsaPrivateKey.create(
+                kidStrategyIgnoredPublicKey,
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get())),
+            // the same key built twice must be equal
+            JwtEcdsaPrivateKey.create(
+                kidStrategyIgnoredPublicKey,
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get())))
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "KID ignored, ONE",
+            JwtEcdsaPrivateKey.create(
+                kidStrategyIgnoredPublicKeyForOne,
+                SecretBigInteger.fromBigInteger(BigInteger.ONE, InsecureSecretKeyAccess.get())))
+        // This group checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "KID ignored, ES512",
+            JwtEcdsaPrivateKey.create(
+                kidStrategyIgnoredPublicKeyES512,
+                SecretBigInteger.fromBigInteger(P521_PRIVATE_VALUE, InsecureSecretKeyAccess.get())))
+        .addEqualityGroup(
+            "KID Base 64",
+            JwtEcdsaPrivateKey.create(
+                kidStrategyBase64PublicKey,
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get())))
+        .addEqualityGroup(
+            "CustomKID1",
+            JwtEcdsaPrivateKey.create(
+                publicKeyCustomKid1,
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get())))
+        .addEqualityGroup(
+            "CustomKID2",
+            JwtEcdsaPrivateKey.create(
+                publicKeyCustomKid2,
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get())))
+        .addEqualityGroup(
+            "different key class",
+            ChaCha20Poly1305Key.create(SecretBytes.randomBytes(32)))
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java
new file mode 100644
index 0000000..ece0078
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java
@@ -0,0 +1,672 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.JwtEcdsaAlgorithm;
+import com.google.crypto.tink.proto.JwtEcdsaPublicKey.CustomKid;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JwtEcdsaProtoSerializationTest {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey";
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    JwtEcdsaProtoSerialization.register(registry);
+  }
+
+  // PARAMETERS PARSING ========================================================= PARAMETERS PARSING
+  @Test
+  public void serializeParseParameters_kidStrategyIsIgnored_works() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.JwtEcdsaKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_kidStrategyBase64_works() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.JwtEcdsaKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_kidStrategyIsIgnored_es384_works() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.JwtEcdsaKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtEcdsaAlgorithm.ES384)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_kidStrategyIsIgnored_es512_works() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.JwtEcdsaKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtEcdsaAlgorithm.ES512)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  // INVALID PARAMETERS SERIALIZATIONS =========================== INVALID PARAMETERS SERIALIZATIONS
+  @Test
+  public void serializeParameters_kidStrategyCustom_cannotBeSerialized_throws() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeParameters(parameters, ProtoParametersSerialization.class));
+  }
+
+  @Test
+  public void parseParameters_crunchy_cannotBeParsed_throws() throws Exception {
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.JwtEcdsaKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtEcdsaAlgorithm.ES512)
+                .build());
+    assertThrows(GeneralSecurityException.class, () -> registry.parseParameters(serialization));
+  }
+
+  // PUBLIC KEY PARSING ========================================================= PUBLIC KEY PARSING
+  @Test
+  public void serializeParsePublicKey_es256_kidIgnored_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es384_kidIgnored_equal() throws Exception {
+    // a valid P384 point. Each coordinate is encoded in 48 bytes.
+    String hexX =
+        "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64"
+            + "DEF8F0EA9055866064A254515480BC13";
+    String hexY =
+        "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1"
+            + "288B231C3AE0D4FE7344FD2533264720";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES384)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es521_kidIgnored_equal() throws Exception {
+    // a valid P521 point, but encoded with leading zeros or truncated zeros.
+    String hexXTruncated =
+        "685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433"
+            + "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d";
+    String hexYWithLeadingZeros =
+        "0000000000000001ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83"
+            + "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(
+                new ECPoint(
+                    new BigInteger(hexXTruncated, 16), new BigInteger(hexYWithLeadingZeros, 16)))
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setX(ByteString.copyFrom(Hex.decode(hexXTruncated)))
+            .setY(ByteString.copyFrom(Hex.decode(hexYWithLeadingZeros)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES512)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    // X and Y are currently serialized with an extra zero at the beginning. So we expect X and Y to
+    // always be encoded in 67 bytes.
+    String expectedHexX =
+        "00"
+            + "00685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433"
+            + "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d";
+    String expectedHexY =
+        "00"
+            + "01ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83"
+            + "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676";
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey expectedProtoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setX(ByteString.copyFrom(Hex.decode(expectedHexX)))
+            .setY(ByteString.copyFrom(Hex.decode(expectedHexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES512)
+            .build();
+    ProtoKeySerialization expectedSerialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            expectedProtoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, expectedSerialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es256_kidCustom_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setCustomKid("weirdCustomKid")
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .setCustomKid(CustomKid.newBuilder().setValue("weirdCustomKid").build())
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es256_base64Kid_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setIdRequirement(12345)
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  // INVALID PUBLIC KEY SERIALIZATIONS =========================== INVALID PUBLIC KEY SERIALIZATIONS
+  @Test
+  public void parsePublicKey_crunchy_cannotBeParsed_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void parsePublicKey_tinkAndCustomKeyId_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .setCustomKid(CustomKid.newBuilder().setValue("WillNotParseWithRAW").build())
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void parsePublicKey_wrongVersion_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(1)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void parsePublicKey_unknownAlgorithm_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES_UNKNOWN)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+  // PRIVATE KEY PARSING ======================================================= PRIVATE KEY PARSING
+  @Test
+  public void serializeParsePrivateKey_es256_kidIgnored_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+    SecretBigInteger privateValue =
+        SecretBigInteger.fromBigInteger(
+            BigIntegerEncoding.fromUnsignedBigEndianBytes(Hex.decode(hexPrivateValue)),
+            InsecureSecretKeyAccess.get());
+
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    JwtEcdsaPrivateKey privateKey = JwtEcdsaPrivateKey.create(publicKey, privateValue);
+
+    com.google.crypto.tink.proto.JwtEcdsaPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.JwtEcdsaPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+                    .setVersion(0)
+                    // X and Y are currently serialized with an extra zero at the beginning.
+                    .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+                    .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+                    .setAlgorithm(JwtEcdsaAlgorithm.ES256))
+            .setKeyValue(ByteString.copyFrom(Hex.decode("00" + hexPrivateValue)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPrivateKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void parsePrivateKey_invalidVersion() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+
+    com.google.crypto.tink.proto.JwtEcdsaPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.JwtEcdsaPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+                    .setVersion(1)
+                    // X and Y are currently serialized with an extra zero at the beginning.
+                    .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+                    .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+                    .setAlgorithm(JwtEcdsaAlgorithm.ES256))
+            .setKeyValue(ByteString.copyFrom(Hex.decode("00" + hexPrivateValue)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void serialize_noSecretKeyAccess_throws() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+    SecretBigInteger privateValue =
+        SecretBigInteger.fromBigInteger(
+            BigIntegerEncoding.fromUnsignedBigEndianBytes(Hex.decode(hexPrivateValue)),
+            InsecureSecretKeyAccess.get());
+
+    JwtEcdsaPublicKey publicKey =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    JwtEcdsaPrivateKey privateKey = JwtEcdsaPrivateKey.create(publicKey, privateValue);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(privateKey, ProtoKeySerialization.class, /* access= */ null));
+  }
+
+  @Test
+  public void parse_noSecretKeyAccess_throws() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+
+    com.google.crypto.tink.proto.JwtEcdsaPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.JwtEcdsaPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+                    .setVersion(0)
+                    // X and Y are currently serialized with an extra zero at the beginning.
+                    .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+                    .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+                    .setAlgorithm(JwtEcdsaAlgorithm.ES256))
+            .setKeyValue(ByteString.copyFrom(Hex.decode("00" + hexPrivateValue)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaPublicKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaPublicKeyTest.java
new file mode 100644
index 0000000..66fc7aa
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaPublicKeyTest.java
@@ -0,0 +1,374 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.KeyTester;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JwtEcdsaPublicKeyTest {
+  private static final ECPoint A_P256_POINT =
+      new ECPoint(
+          new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+          new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac", 16));
+
+  private static final ECPoint INVALID_P256_POINT =
+      new ECPoint(
+          new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+          new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ad", 16));
+
+  private static final ECPoint A_P384_POINT =
+      new ECPoint(
+          new BigInteger(
+              "a7c76b970c3b5fe8b05d2838ae04ab47697b9eaf52e764592efda27fe7513272"
+                  + "734466b400091adbf2d68c58e0c50066",
+              16),
+          new BigInteger(
+              "ac68f19f2e1cb879aed43a9969b91a0839c4c38a49749b661efedf243451915e"
+                  + "d0905a32b060992b468c64766fc8437a",
+              16));
+
+  private static final ECPoint A_P521_POINT =
+      new ECPoint(
+          new BigInteger(
+              "000000685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a9490340"
+                  + "854334b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2"
+                  + "046d",
+              16),
+          new BigInteger(
+              "000001ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b7398"
+                  + "84a83bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302"
+                  + "f676",
+              16));
+
+  @Test
+  public void build_kidStrategyIgnored_getProperties_es256() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P256_POINT).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P256_POINT);
+    assertThat(key.getKid()).isEqualTo(Optional.empty());
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_andGetProperties_es384() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+            .build();
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P384_POINT).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P384_POINT);
+    assertThat(key.getKid()).isEqualTo(Optional.empty());
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_andGetProperties_es512() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+            .build();
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P521_POINT).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P521_POINT);
+    assertThat(key.getKid()).isEqualTo(Optional.empty());
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_setCustomKidCalled_fails() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setCustomKid("customKid23")
+            .setPublicPoint(A_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_setIdRequirement_fails() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setIdRequirement(123)
+            .setPublicPoint(A_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildKidStrategyCustom_getProperties_succeeds() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setCustomKid("customKid777")
+            .setParameters(parameters)
+            .setPublicPoint(A_P256_POINT)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P256_POINT);
+    assertThat(key.getKid().get()).isEqualTo("customKid777");
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildKidStrategyCustom_setIdRequirement_fails() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder()
+            .setCustomKid("customKid777")
+            .setIdRequirement(1234)
+            .setParameters(parameters)
+            .setPublicPoint(A_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildKidStrategyCustom_missingCustomKid_fails() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyBase64_getProperties_succeeds() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(A_P256_POINT)
+            .setIdRequirement(0x1ac6a944)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x1ac6a944);
+    // See JwtFormatTest.getKidFromTinkOutputPrefixType_success
+    assertThat(key.getKid()).isEqualTo(Optional.of("GsapRA"));
+  }
+
+  @Test
+  public void build_kidStrategyBase64_noIdRequirement_throws() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyBase64_setCustomKid_throws() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setIdRequirement(0x89abcdef)
+            .setCustomKid("customKid")
+            .setPublicPoint(A_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> JwtEcdsaPublicKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> JwtEcdsaPublicKey.builder().setPublicPoint(A_P256_POINT).build());
+  }
+
+  @Test
+  public void buildWithoutPublicPoint_fails() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> JwtEcdsaPublicKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void build_invalidPublicPoint_fails() throws Exception {
+    JwtEcdsaParameters parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaPublicKey.Builder builder =
+        JwtEcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(INVALID_P256_POINT);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    ECPoint aP256PointCopy =
+        new ECPoint(
+            new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+            new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac", 16));
+    ECPoint anotherP256Point =
+        new ECPoint(
+            new BigInteger("809f04289c64348c01515eb03d5ce7ac1a8cb9498f5caa50197e58d43a86a7ae", 16),
+            new BigInteger("b29d84e811197f25eba8f5194092cb6ff440e26d4421011372461f579271cda3", 16));
+
+    JwtEcdsaParameters kidStrategyIgnoredParameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+    JwtEcdsaParameters kidStrategyIgnoredParametersCopy =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+
+    JwtEcdsaParameters kidStrategyCustomParameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+
+    JwtEcdsaParameters kidStrategyBase64Parameters =
+        JwtEcdsaParameters.builder()
+            .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "KID Ignored, P256",
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyIgnoredParameters)
+                .setPublicPoint(A_P256_POINT)
+                .build(),
+            // the same key built twice must be equal
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyIgnoredParameters)
+                .setPublicPoint(A_P256_POINT)
+                .build(),
+            // the same key built with a copy of key bytes must be equal
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyIgnoredParameters)
+                .setPublicPoint(aP256PointCopy)
+                .build(),
+            // the same key built with a copy of parameters must be equal
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyIgnoredParametersCopy)
+                .setPublicPoint(A_P256_POINT)
+                .build())
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "KID Ignored, different P256 point",
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyIgnoredParameters)
+                .setPublicPoint(anotherP256Point)
+                .build())
+        // These groups checks that keys with different customKid are not equal
+        .addEqualityGroup(
+            "KID Custom, customKid1",
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyCustomParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setCustomKid("customKid1")
+                .build())
+        .addEqualityGroup(
+            "KID Custom, customKid2",
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyCustomParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setCustomKid("customKid2")
+                .build())
+        // These groups checks that keys with different ID Requirements are not equal
+        .addEqualityGroup(
+            "Tink with key id 1907, P256",
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyBase64Parameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1907)
+                .build(),
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyBase64Parameters)
+                .setPublicPoint(aP256PointCopy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal
+        .addEqualityGroup(
+            "Tink with key id 1908, P256",
+            JwtEcdsaPublicKey.builder()
+                .setParameters(kidStrategyBase64Parameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1908)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManagerTest.java
index 6752198..ee8c755 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaSignKeyManagerTest.java
@@ -38,6 +38,7 @@
 import com.google.crypto.tink.subtle.EllipticCurves;
 import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
 import com.google.crypto.tink.subtle.Enums;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.gson.JsonObject;
@@ -135,7 +136,7 @@
     int numTests = 5;
     for (int i = 0; i < numTests; i++) {
       JwtEcdsaPrivateKey key = factory.createKey(format);
-      keys.add(TestUtil.hexEncode(key.getKeyValue().toByteArray()));
+      keys.add(Hex.encode(key.getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -189,31 +190,70 @@
                 new ByteArrayInputStream(Random.randBytes(100))));
   }
 
-  private static void checkTemplate(KeyTemplate template, JwtEcdsaAlgorithm algorithm)
-      throws Exception {
-    assertThat(template.getTypeUrl()).isEqualTo(new JwtEcdsaSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    JwtEcdsaKeyFormat format =
-        JwtEcdsaKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    assertThat(format.getAlgorithm()).isEqualTo(algorithm);
+  @Test
+  public void testJwtES256RawTemplate_ok() throws Exception {
+    KeyTemplate template = KeyTemplates.get("JWT_ES256_RAW");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtEcdsaParameters.builder()
+                .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                .build());
+  }
+
+  @Test
+  public void testJwtES384RawTemplate_ok() throws Exception {
+    KeyTemplate template = KeyTemplates.get("JWT_ES384_RAW");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtEcdsaParameters.builder()
+                .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+                .build());
+  }
+
+  @Test
+  public void testJwtES512RawTemplate_ok() throws Exception {
+    KeyTemplate template = KeyTemplates.get("JWT_ES512_RAW");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtEcdsaParameters.builder()
+                .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+                .build());
   }
 
   @Test
   public void testJwtES256Template_ok() throws Exception {
-    KeyTemplate template = KeyTemplates.get("JWT_ES256_RAW");
-    checkTemplate(template, JwtEcdsaAlgorithm.ES256);
+    KeyTemplate template = KeyTemplates.get("JWT_ES256");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtEcdsaParameters.builder()
+                .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                .build());
   }
 
   @Test
   public void testJwtES384Template_ok() throws Exception {
-    KeyTemplate template = KeyTemplates.get("JWT_ES384_RAW");
-    checkTemplate(template, JwtEcdsaAlgorithm.ES384);
+    KeyTemplate template = KeyTemplates.get("JWT_ES384");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtEcdsaParameters.builder()
+                .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+                .build());
   }
 
   @Test
   public void testJwtES512Template_ok() throws Exception {
-    KeyTemplate template = KeyTemplates.get("JWT_ES512_RAW");
-    checkTemplate(template, JwtEcdsaAlgorithm.ES512);
+    KeyTemplate template = KeyTemplates.get("JWT_ES512");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtEcdsaParameters.builder()
+                .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+                .build());
   }
 
   @Test
@@ -407,16 +447,20 @@
     JsonObject normalHeader = new JsonObject();
     normalHeader.addProperty("alg", "ES256");
     String normalSignedCompact = generateSignedCompact(rawSigner, normalHeader, payload);
-    verifier.verifyAndDecode(normalSignedCompact, validator);
+    Object unused = verifier.verifyAndDecode(normalSignedCompact, validator);
 
     // valid token, with "typ" set in the header
     JsonObject goodHeader = new JsonObject();
     goodHeader.addProperty("alg", "ES256");
     goodHeader.addProperty("typ", "typeHeader");
     String goodSignedCompact = generateSignedCompact(rawSigner, goodHeader, payload);
-    verifier.verifyAndDecode(
-        goodSignedCompact,
-        JwtValidator.newBuilder().expectTypeHeader("typeHeader").allowMissingExpiration().build());
+    unused =
+        verifier.verifyAndDecode(
+            goodSignedCompact,
+            JwtValidator.newBuilder()
+                .expectTypeHeader("typeHeader")
+                .allowMissingExpiration()
+                .build());
 
     // invalid token with an empty header
     JsonObject emptyHeader = new JsonObject();
@@ -438,7 +482,7 @@
     unknownKidHeader.addProperty("alg", "ES256");
     unknownKidHeader.addProperty("kid", "unknown");
     String unknownKidSignedCompact = generateSignedCompact(rawSigner, unknownKidHeader, payload);
-    verifier.verifyAndDecode(unknownKidSignedCompact, validator);
+    unused = verifier.verifyAndDecode(unknownKidSignedCompact, validator);
   }
 
   @Test
@@ -471,7 +515,7 @@
     normalHeader.addProperty("alg", "ES256");
     normalHeader.addProperty("kid", kid);
     String normalToken = generateSignedCompact(rawSigner, normalHeader, payload);
-    verifier.verifyAndDecode(normalToken, validator);
+    Object unused = verifier.verifyAndDecode(normalToken, validator);
 
     // token without kid are rejected, even if they are valid.
     JsonObject headerWithoutKid = new JsonObject();
@@ -586,17 +630,4 @@
         JwtInvalidException.class,
         () -> verifierWithWrongKid.verifyAndDecode(signedCompactWithKid, validator));
   }
-
-  @Test
-  public void signWithTinkKeyAndCustomKid_fails() throws Exception {
-    assumeFalse(TestUtil.isTsan()); // KeysetHandle.generateNew is too slow in Tsan.
-    KeyTemplate template = KeyTemplates.get("JWT_ES256");
-    KeysetHandle handleWithoutKid = KeysetHandle.generateNew(template);
-    KeysetHandle handleWithKid =
-        withCustomKid(handleWithoutKid, "Lorem ipsum dolor sit amet, consectetur adipiscing elit");
-
-    JwtPublicKeySign signerWithKid = handleWithKid.getPrimitive(JwtPublicKeySign.class);
-    RawJwt rawToken = RawJwt.newBuilder().setJwtId("jwtId").withoutExpiration().build();
-    assertThrows(JwtInvalidException.class, () -> signerWithKid.signAndEncode(rawToken));
-  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManagerTest.java
index d54b649..d16273f 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaVerifyKeyManagerTest.java
@@ -94,8 +94,9 @@
         verifyManager.getPrimitive(publicKey, JwtPublicKeyVerifyInternal.class);
     RawJwt token = RawJwt.newBuilder().withoutExpiration().build();
     JwtValidator validator = JwtValidator.newBuilder().allowMissingExpiration().build();
-    verifier.verifyAndDecodeWithKid(
-        signer.signAndEncodeWithKid(token, Optional.empty()), validator, Optional.empty());
+    Object unused =
+        verifier.verifyAndDecodeWithKid(
+            signer.signAndEncodeWithKid(token, Optional.empty()), validator, Optional.empty());
   }
 
   // Note: we use Theory as a parametrized test -- different from what the Theory framework intends.
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtFormatTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtFormatTest.java
index 587f9ae..92ad1b7 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtFormatTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtFormatTest.java
@@ -68,6 +68,10 @@
   public void getKidFromTinkOutputPrefixType_success() throws Exception {
     int keyId = 0x1ac6a944;
     Optional<String> kid = JwtFormat.getKid(keyId, OutputPrefixType.TINK);
+    // ID Requirement(Hex):       1    a    c    6    a    9    4    4
+    // ID Requirement(Binary): 0001 1010 1100 0110 1010 1001 0100 0100
+    // Regroup for base64:     000110  101100 011010  101001 010001 00 (+0000)
+    // Base64 encode:          G       s      a       p      R      A
     assertThat(kid.get()).isEqualTo("GsapRA");
     assertThat(JwtFormat.getKeyId(kid.get()).get()).isEqualTo(0x1ac6a944);
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyManagerTest.java
index 603a115..8cad474 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyManagerTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.Key;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
@@ -35,6 +36,7 @@
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.PrfHmacJce;
 import com.google.crypto.tink.subtle.PrfMac;
 import com.google.crypto.tink.subtle.Random;
@@ -158,7 +160,7 @@
     int numKeys = 100;
     Set<String> keys = new TreeSet<>();
     for (int i = 0; i < numKeys; ++i) {
-      keys.add(TestUtil.hexEncode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numKeys);
   }
@@ -196,49 +198,73 @@
   @Test
   public void testHs256Template() throws Exception {
     KeyTemplate template = KeyTemplates.get("JWT_HS256");
-    assertThat(template.getTypeUrl()).isEqualTo(manager.getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    JwtHmacKeyFormat format =
-        JwtHmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getAlgorithm()).isEqualTo(JwtHmacAlgorithm.HS256);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+                .build());
   }
 
   @Test
   public void testHs384Template() throws Exception {
     KeyTemplate template = KeyTemplates.get("JWT_HS384");
-    assertThat(template.getTypeUrl()).isEqualTo(new JwtHmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    JwtHmacKeyFormat format =
-        JwtHmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(48);
-    assertThat(format.getAlgorithm()).isEqualTo(JwtHmacAlgorithm.HS384);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(48)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+                .build());
   }
 
   @Test
   public void testHs512Template() throws Exception {
     KeyTemplate template = KeyTemplates.get("JWT_HS512");
-    assertThat(template.getTypeUrl()).isEqualTo(new JwtHmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    JwtHmacKeyFormat format =
-        JwtHmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(64);
-    assertThat(format.getAlgorithm()).isEqualTo(JwtHmacAlgorithm.HS512);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS512)
+                .build());
   }
 
   @Test
-  public void testRawHs256Template() throws Exception {
+  public void testHs256RawTemplate() throws Exception {
     KeyTemplate template = KeyTemplates.get("JWT_HS256_RAW");
-    assertThat(template.getTypeUrl()).isEqualTo(manager.getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    JwtHmacKeyFormat format =
-        JwtHmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+                .build());
+  }
 
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getAlgorithm()).isEqualTo(JwtHmacAlgorithm.HS256);
+  @Test
+  public void testHs384RawTemplate() throws Exception {
+    KeyTemplate template = KeyTemplates.get("JWT_HS384_RAW");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(48)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+                .build());
+  }
+
+  @Test
+  public void testHs512RawTemplate() throws Exception {
+    KeyTemplate template = KeyTemplates.get("JWT_HS512_RAW");
+    assertThat(template.toParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS512)
+                .build());
   }
 
   @Test
@@ -249,6 +275,21 @@
     testKeyTemplateCompatible(manager, KeyTemplates.get("JWT_HS512_RAW"));
   }
 
+  @Test
+  public void createKeysetHandle_works() throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("JWT_HS256"));
+    Key key = handle.getAt(0).getKey();
+    assertThat(key).isInstanceOf(com.google.crypto.tink.jwt.JwtHmacKey.class);
+    com.google.crypto.tink.jwt.JwtHmacKey jwtHmacKey = (com.google.crypto.tink.jwt.JwtHmacKey) key;
+    assertThat(jwtHmacKey.getParameters())
+        .isEqualTo(
+            JwtHmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+                .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                .build());
+  }
+
   // Note: we use Theory as a parametrized test -- different from what the Theory framework intends.
   @Theory
   public void createSignVerify_success(String templateNames) throws Exception {
@@ -615,16 +656,20 @@
     JsonObject normalHeader = new JsonObject();
     normalHeader.addProperty("alg", "HS256");
     String normalSignedCompact = generateSignedCompact(rawPrimitive, normalHeader, payload);
-    primitive.verifyMacAndDecode(normalSignedCompact, validator);
+    Object unused = primitive.verifyMacAndDecode(normalSignedCompact, validator);
 
     // valid token, with "typ" set in the header
     JsonObject goodHeader = new JsonObject();
     goodHeader.addProperty("alg", "HS256");
     goodHeader.addProperty("typ", "typeHeader");
     String goodSignedCompact = generateSignedCompact(rawPrimitive, goodHeader, payload);
-    primitive.verifyMacAndDecode(
-        goodSignedCompact,
-        JwtValidator.newBuilder().expectTypeHeader("typeHeader").allowMissingExpiration().build());
+    unused =
+        primitive.verifyMacAndDecode(
+            goodSignedCompact,
+            JwtValidator.newBuilder()
+                .expectTypeHeader("typeHeader")
+                .allowMissingExpiration()
+                .build());
 
     // invalid token with an empty header
     JsonObject emptyHeader = new JsonObject();
@@ -647,7 +692,7 @@
     headerWithUnknownKid.addProperty("kid", "unknown");
     String tokenWithUnknownKid = generateSignedCompact(
         rawPrimitive, headerWithUnknownKid, payload);
-    primitive.verifyMacAndDecode(tokenWithUnknownKid, validator);
+    unused = primitive.verifyMacAndDecode(tokenWithUnknownKid, validator);
   }
 
   @Test
@@ -675,7 +720,7 @@
     normalHeader.addProperty("alg", "HS256");
     normalHeader.addProperty("kid", kid);
     String normalToken = generateSignedCompact(rawPrimitive, normalHeader, payload);
-    primitive.verifyMacAndDecode(normalToken, validator);
+    Object unused = primitive.verifyMacAndDecode(normalToken, validator);
 
     // valid token, with "typ" set in the header
     JsonObject headerWithTyp = new JsonObject();
@@ -683,9 +728,13 @@
     headerWithTyp.addProperty("typ", "typeHeader");
     headerWithTyp.addProperty("kid", kid);
     String tokenWithTyp = generateSignedCompact(rawPrimitive, headerWithTyp, payload);
-    primitive.verifyMacAndDecode(
-        tokenWithTyp,
-        JwtValidator.newBuilder().expectTypeHeader("typeHeader").allowMissingExpiration().build());
+    unused =
+        primitive.verifyMacAndDecode(
+            tokenWithTyp,
+            JwtValidator.newBuilder()
+                .expectTypeHeader("typeHeader")
+                .allowMissingExpiration()
+                .build());
 
     // invalid token without algorithm
     JsonObject headerWithoutAlg = new JsonObject();
@@ -876,8 +925,6 @@
     KeysetHandle handleWithKid =
         CleartextKeysetHandle.fromKeyset(keyset.toBuilder().setKey(0, keyWithKid).build());
 
-    JwtMac jwtMacWithKid = handleWithKid.getPrimitive(JwtMac.class);
-    RawJwt rawToken = RawJwt.newBuilder().setJwtId("jwtId").withoutExpiration().build();
-    assertThrows(JwtInvalidException.class, () -> jwtMacWithKid.computeMacAndEncode(rawToken));
+    assertThrows(GeneralSecurityException.class, () -> handleWithKid.getPrimitive(JwtMac.class));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyTest.java
new file mode 100644
index 0000000..2d6ca49
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacKeyTest.java
@@ -0,0 +1,355 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JwtHmacKeyTest {
+  @Test
+  public void buildSimpleVariantCheckProperties() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey key = JwtHmacKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getKid()).isEqualTo(Optional.empty());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacKey.Builder builder = JwtHmacKey.builder().setParameters(parameters);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_WithCustomKid_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey.Builder builder =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setCustomKid("customKid")
+            .setKeyBytes(keyBytes);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyIgnored_withIdRequirement_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey.Builder builder =
+        JwtHmacKey.builder().setParameters(parameters).setIdRequirement(120).setKeyBytes(keyBytes);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyCustom() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey key =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setCustomKid("CustomKid")
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getKid()).isEqualTo(Optional.of("CustomKid"));
+  }
+
+  @Test
+  public void build_kidStrategyCustom_differentAlgorithmAndKeyId_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey key =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setCustomKid("myCustomTestKid")
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getKid()).isEqualTo(Optional.of("myCustomTestKid"));
+  }
+
+  @Test
+  public void build_kidStrategyCustom_doNotSetCustomKid_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey.Builder builder =
+        JwtHmacKey.builder().setParameters(parameters).setKeyBytes(keyBytes);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyCustom_setIdRequirement_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey.Builder builder =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setCustomKid("myCustomTestKid")
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(2930);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyBase64_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey key =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x1ac6a944)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x1ac6a944);
+    // See JwtFormatTest.getKidFromTinkOutputPrefixType_success
+    assertThat(key.getKid()).isEqualTo(Optional.of("GsapRA"));
+  }
+
+  @Test
+  public void build_kidStrategyBase64_setCustomKeyId_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey.Builder builder =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x89abcdef)
+            .setCustomKid("customKeyId");
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void build_kidStrategyBase64_omitIdRequirement_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey.Builder builder =
+        JwtHmacKey.builder().setParameters(parameters).setKeyBytes(keyBytes);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes16 = SecretBytes.randomBytes(16);
+    SecretBytes keyBytes16Copy =
+        SecretBytes.copyFrom(
+            keyBytes16.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes16B = SecretBytes.randomBytes(16);
+    SecretBytes keyBytes32 = SecretBytes.randomBytes(32);
+
+    JwtHmacParameters parametersIgnoredKidStrategy =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parametersIgnoredKidStrategyCopy =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parametersHS384 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parametersHS512 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS512)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parametersKeySize32 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parametersKidStrategyCustom =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    JwtHmacParameters parametersKidStrategyBase64 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "StrategyKidIgnored",
+            JwtHmacKey.builder()
+                .setParameters(parametersIgnoredKidStrategy)
+                .setKeyBytes(keyBytes16)
+                .build(),
+            // the same key built twice must be equal
+            JwtHmacKey.builder()
+                .setParameters(parametersIgnoredKidStrategy)
+                .setKeyBytes(keyBytes16)
+                .build(),
+            // the same key built with a copy of key bytes and parameters must be equal
+            JwtHmacKey.builder()
+                .setParameters(parametersIgnoredKidStrategyCopy)
+                .setKeyBytes(keyBytes16Copy)
+                .build())
+        .addEqualityGroup(
+            "keyBytes16B",
+            JwtHmacKey.builder()
+                .setParameters(parametersIgnoredKidStrategy)
+                .setKeyBytes(keyBytes16B)
+                .build())
+        .addEqualityGroup(
+            "parametersHS384",
+            JwtHmacKey.builder().setParameters(parametersHS384).setKeyBytes(keyBytes16).build())
+        .addEqualityGroup(
+            "parametersHS512",
+            JwtHmacKey.builder().setParameters(parametersHS512).setKeyBytes(keyBytes16).build())
+        .addEqualityGroup(
+            "parameters32BytesKey",
+            JwtHmacKey.builder().setParameters(parametersKeySize32).setKeyBytes(keyBytes32).build())
+        .addEqualityGroup(
+            "custom Kid 1",
+            JwtHmacKey.builder()
+                .setParameters(parametersKidStrategyCustom)
+                .setKeyBytes(keyBytes16)
+                .setCustomKid("myCustomKid1")
+                .build())
+        .addEqualityGroup(
+            "custom Kid 2",
+            JwtHmacKey.builder()
+                .setParameters(parametersKidStrategyCustom)
+                .setKeyBytes(keyBytes16)
+                .setCustomKid("myCustomKid2")
+                .build())
+        .addEqualityGroup(
+            "base64Id101",
+            JwtHmacKey.builder()
+                .setParameters(parametersKidStrategyBase64)
+                .setKeyBytes(keyBytes16)
+                .setIdRequirement(101)
+                .build())
+        .addEqualityGroup(
+            "base64Id102",
+            JwtHmacKey.builder()
+                .setParameters(parametersKidStrategyBase64)
+                .setKeyBytes(keyBytes16)
+                .setIdRequirement(102)
+                .build())
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    JwtHmacKey key = JwtHmacKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+
+    HmacParameters hmacParameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    HmacKey hmacKey = HmacKey.builder().setParameters(hmacParameters).setKeyBytes(keyBytes).build();
+
+    assertThat(key.equalsKey(hmacKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacParametersTest.java
new file mode 100644
index 0000000..77c4a62
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacParametersTest.java
@@ -0,0 +1,162 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JwtHmacParametersTest {
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtHmacParameters.KidStrategy.IGNORED);
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtHmacParameters.Algorithm.HS256);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.allowKidAbsent()).isTrue();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_differentAlgorithm() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS512)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters.getAlgorithm()).isEqualTo(JwtHmacParameters.Algorithm.HS512);
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_kidCustom() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtHmacParameters.KidStrategy.CUSTOM);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.allowKidAbsent()).isTrue();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_kidBase64() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    assertThat(parameters.getKidStrategy()).isEqualTo(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    assertThat(parameters.allowKidAbsent()).isFalse();
+  }
+
+  @Test
+  public void buildParameters_differentKeySize() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(17)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(17);
+  }
+
+  @Test
+  public void testEqualsTwoInstances() throws Exception {
+    JwtHmacParameters parameters1 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parameters2 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testEqualsHashCodeDependsOnKeySize() throws Exception {
+    JwtHmacParameters parameters1 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parameters2 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(17)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testEqualsHashCodeDependsOnAlgorithm() throws Exception {
+    JwtHmacParameters parameters1 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parameters2 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testEqualsHashCodeDependsOnKidStrategy() throws Exception {
+    JwtHmacParameters parameters1 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacParameters parameters2 =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacProtoSerializationTest.java
new file mode 100644
index 0000000..fc219f0
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtHmacProtoSerializationTest.java
@@ -0,0 +1,445 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.JwtHmacAlgorithm;
+import com.google.crypto.tink.proto.JwtHmacKey.CustomKid;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.Theories;
+import org.junit.runner.RunWith;
+
+/** Test for JwtHmacProtoSerialization. */
+@RunWith(Theories.class)
+public final class JwtHmacProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.JwtHmacKey";
+
+  private static final SecretBytes KEY_BYTES_42 = SecretBytes.randomBytes(42);
+  private static final ByteString KEY_BYTES_42_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_42.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    JwtHmacProtoSerialization.register(registry);
+  }
+
+  // PARAMETERS PARSING ========================================================= PARAMETERS PARSING
+  @Test
+  public void serializeParseParameters_kidStrategyIsIgnored_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(19)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.JwtHmacKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtHmacAlgorithm.HS256)
+                .setKeySize(19)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_kidStrategyIsIgnored_differentKeySize_works()
+      throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(21)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.JwtHmacKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtHmacAlgorithm.HS256)
+                .setKeySize(21)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_kidStrategyIsIgnored_differentAlgorithm_works()
+      throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(19)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS512)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.JwtHmacKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtHmacAlgorithm.HS512)
+                .setKeySize(19)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_kidStrategyBase64_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(19)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.JwtHmacKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtHmacAlgorithm.HS256)
+                .setKeySize(19)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  // INVALID PARAMETERS SERIALIZATIONS =========================== INVALID PARAMETERS SERIALIZATIONS
+  @Test
+  public void serializeParameters_kidStrategyCustom_cannotBeSerialized_throws() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(19)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeParameters(parameters, ProtoParametersSerialization.class));
+  }
+
+  @Test
+  public void parseParameters_crunchy_cannotBeParsed_throws() throws Exception {
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.JwtHmacKeyFormat.newBuilder()
+                .setVersion(0)
+                .setAlgorithm(JwtHmacAlgorithm.HS256)
+                .setKeySize(19)
+                .build());
+    assertThrows(GeneralSecurityException.class, () -> registry.parseParameters(serialization));
+  }
+
+  // KEYS PARSING ===================================================================== KEYS PARSING
+  @Test
+  public void serializeParseKey_kidStrategyIsIgnored_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(42)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS256)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacKey key =
+        JwtHmacKey.builder().setParameters(parameters).setKeyBytes(KEY_BYTES_42).build();
+
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS256)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_kidStrategyIsIgnored_differentAlgorithm_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(42)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.IGNORED)
+            .build();
+    JwtHmacKey key =
+        JwtHmacKey.builder().setParameters(parameters).setKeyBytes(KEY_BYTES_42).build();
+
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS384)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_kidStrategyIsCustom_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(42)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS512)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    JwtHmacKey key =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_42)
+            .setCustomKid("customKidForThisTest")
+            .build();
+
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS512)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setCustomKid(CustomKid.newBuilder().setValue("customKidForThisTest"))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_kidStrategyIsCustom_differentAlgorithm_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(42)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.CUSTOM)
+            .build();
+    JwtHmacKey key =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_42)
+            .setCustomKid("customKidForThisTest")
+            .build();
+
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS384)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setCustomKid(CustomKid.newBuilder().setValue("customKidForThisTest"))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_kidStrategyIsBase64_works() throws Exception {
+    JwtHmacParameters parameters =
+        JwtHmacParameters.builder()
+            .setKeySizeBytes(42)
+            .setAlgorithm(JwtHmacParameters.Algorithm.HS384)
+            .setKidStrategy(JwtHmacParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+            .build();
+    JwtHmacKey key =
+        JwtHmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(KEY_BYTES_42)
+            .setIdRequirement(10203)
+            .build();
+
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS384)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 10203);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtHmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  // INVALID KEYS SERIALIZATIONS ======================================= INVALID KEYS SERIALIZATIONS
+  @Test
+  public void serializeKey_wrongVersion_throws() throws Exception {
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(1)
+            .setAlgorithm(JwtHmacAlgorithm.HS384)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 10203);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void serializeKey_unknownAlgorithm_throws() throws Exception {
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS_UNKNOWN)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 10203);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void serializeKey_tinkKeyWithCustomSet_throws() throws Exception {
+    com.google.crypto.tink.proto.JwtHmacKey protoKey =
+        com.google.crypto.tink.proto.JwtHmacKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(JwtHmacAlgorithm.HS256)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setCustomKid(CustomKid.newBuilder().setValue("customKidForThisTest"))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtHmacKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 10203);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtMacWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtMacWrapperTest.java
index 2cbc8f1..6597881 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtMacWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtMacWrapperTest.java
@@ -19,11 +19,15 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KeysetManager;
-import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.time.Clock;
 import java.time.Instant;
@@ -44,6 +48,8 @@
 
   @Test
   public void test_wrapNoPrimary_throws() throws Exception {
+    // The old KeysetManager API allows keysets without primary key.
+    // The KeysetHandle.Builder does not allow this and can't be used in this test.
     KeyTemplate template = KeyTemplates.get("JWT_HS256");
     KeysetManager manager = KeysetManager.withEmptyKeyset().add(template);
     KeysetHandle handle = manager.getKeysetHandle();
@@ -54,11 +60,18 @@
   public void test_wrapLegacy_throws() throws Exception {
     KeyTemplate rawTemplate = KeyTemplates.get("JWT_HS256_RAW");
     // Convert the normal, raw template into a template with output prefix type LEGACY
-    KeyTemplate tinkTemplate =
-        KeyTemplate.create(
-            rawTemplate.getTypeUrl(), rawTemplate.getValue(), KeyTemplate.OutputPrefixType.LEGACY);
-    KeysetHandle handle = KeysetHandle.generateNew(tinkTemplate);
-    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(JwtMac.class));
+    KeysetHandle handle = KeysetHandle.generateNew(rawTemplate);
+    Keyset keyset =
+        Keyset.parseFrom(
+            TinkProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get()),
+            ExtensionRegistryLite.getEmptyRegistry());
+    Keyset.Builder legacyKeysetBuilder = keyset.toBuilder();
+    legacyKeysetBuilder.setKey(
+        0, legacyKeysetBuilder.getKey(0).toBuilder().setOutputPrefixType(OutputPrefixType.LEGACY));
+    KeysetHandle legacyHandle =
+        TinkProtoKeysetFormat.parseKeyset(
+            legacyKeysetBuilder.build().toByteArray(), InsecureSecretKeyAccess.get());
+    assertThrows(GeneralSecurityException.class, () -> legacyHandle.getPrimitive(JwtMac.class));
   }
 
   @Test
@@ -87,16 +100,21 @@
   }
 
   @Test
-  public void test_wrapMultipleKeys() throws Exception {
-    KeyTemplate template = KeyTemplates.get("JWT_HS256");
-
-    KeysetManager manager = KeysetManager.withEmptyKeyset();
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(template), /*asPrimary=*/ true);
-    KeysetHandle oldHandle = manager.getKeysetHandle();
-
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(template), /*asPrimary=*/ true);
-
-    KeysetHandle newHandle = manager.getKeysetHandle();
+  public void test_wrapMultipleRawKeys() throws Exception {
+    KeysetHandle oldHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_HS256_RAW")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    KeysetHandle newHandle =
+        KeysetHandle.newBuilder(oldHandle)
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_HS256_RAW")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
 
     JwtMac oldJwtMac = oldHandle.getPrimitive(JwtMac.class);
     JwtMac newJwtMac = newHandle.getPrimitive(JwtMac.class);
@@ -119,15 +137,20 @@
 
   @Test
   public void test_wrapMultipleTinkKeys() throws Exception {
-    KeyTemplate tinkTemplate = KeyTemplates.get("JWT_HS256");
-
-    KeysetManager manager = KeysetManager.withEmptyKeyset();
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(tinkTemplate), /*asPrimary=*/ true);
-    KeysetHandle oldHandle = manager.getKeysetHandle();
-
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(tinkTemplate), /*asPrimary=*/ true);
-
-    KeysetHandle newHandle = manager.getKeysetHandle();
+    KeysetHandle oldHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_HS256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    KeysetHandle newHandle =
+        KeysetHandle.newBuilder(oldHandle)
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_HS256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
 
     JwtMac oldJwtMac = oldHandle.getPrimitive(JwtMac.class);
     JwtMac newJwtMac = newHandle.getPrimitive(JwtMac.class);
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtPublicKeySignVerifyWrappersTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtPublicKeySignVerifyWrappersTest.java
index c31de03..54098a3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtPublicKeySignVerifyWrappersTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtPublicKeySignVerifyWrappersTest.java
@@ -19,12 +19,17 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KeysetManager;
-import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.testing.TestUtil;
+import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.time.Clock;
 import java.time.Instant;
@@ -67,6 +72,8 @@
 
   @Test
   public void test_noPrimary_getSignPrimitive_fails() throws Exception {
+    // The old KeysetManager API allows keysets without primary key.
+    // The KeysetHandle.Builder does not allow this and can't be used in this test.
     KeyTemplate template = KeyTemplates.get("JWT_ES256");
     KeysetManager manager = KeysetManager.withEmptyKeyset().add(template);
     KeysetHandle handle = manager.getKeysetHandle();
@@ -76,24 +83,34 @@
 
   @Test
   public void test_noPrimary_getVerifyPrimitive_success() throws Exception {
-    KeyTemplate template = KeyTemplates.get("JWT_ES256");
-    KeysetManager manager = KeysetManager.withEmptyKeyset().add(template);
-    KeysetHandle publicHandle = manager.getKeysetHandle().getPublicKeysetHandle();
-    publicHandle.getPrimitive(JwtPublicKeyVerify.class);
+    KeysetHandle privateKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_ES256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    KeysetHandle publicHandle = privateKeysetHandle.getPublicKeysetHandle();
+    Object unused = publicHandle.getPrimitive(JwtPublicKeyVerify.class);
   }
 
   @Test
   public void test_wrapLegacy_throws() throws Exception {
-    KeyTemplate rawTemplate = KeyTemplates.get("JWT_ES256_RAW");
-    // Convert the normal, raw template into a template with output prefix type LEGACY
-    KeyTemplate tinkTemplate =
-        KeyTemplate.create(
-            rawTemplate.getTypeUrl(), rawTemplate.getValue(), KeyTemplate.OutputPrefixType.LEGACY);
-    KeysetHandle handle = KeysetHandle.generateNew(tinkTemplate);
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get("JWT_ES256_RAW"));
+    Keyset keyset =
+        Keyset.parseFrom(
+            TinkProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get()),
+            ExtensionRegistryLite.getEmptyRegistry());
+    Keyset.Builder legacyKeysetBuilder = keyset.toBuilder();
+    legacyKeysetBuilder.setKey(
+        0, legacyKeysetBuilder.getKey(0).toBuilder().setOutputPrefixType(OutputPrefixType.LEGACY));
+    KeysetHandle legacyHandle =
+        TinkProtoKeysetFormat.parseKeyset(
+            legacyKeysetBuilder.build().toByteArray(), InsecureSecretKeyAccess.get());
     assertThrows(
-        GeneralSecurityException.class, () -> handle.getPrimitive(JwtPublicKeySign.class));
+        GeneralSecurityException.class, () -> legacyHandle.getPrimitive(JwtPublicKeySign.class));
 
-    KeysetHandle publicHandle = handle.getPublicKeysetHandle();
+    KeysetHandle publicHandle = legacyHandle.getPublicKeysetHandle();
     assertThrows(
         GeneralSecurityException.class, () -> publicHandle.getPrimitive(JwtPublicKeyVerify.class));
   }
@@ -131,15 +148,20 @@
 
   @Test
   public void test_wrapMultipleRawKeys() throws Exception {
-    KeyTemplate template = KeyTemplates.get("JWT_ES256_RAW");
-
-    KeysetManager manager = KeysetManager.withEmptyKeyset();
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(template), /*asPrimary=*/ true);
-    KeysetHandle oldHandle = manager.getKeysetHandle();
-
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(template), /*asPrimary=*/ true);
-
-    KeysetHandle newHandle = manager.getKeysetHandle();
+    KeysetHandle oldHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_ES256_RAW")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    KeysetHandle newHandle =
+        KeysetHandle.newBuilder(oldHandle)
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_ES256_RAW")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
 
     JwtPublicKeySign oldSigner = oldHandle.getPrimitive(JwtPublicKeySign.class);
     JwtPublicKeySign newSigner = newHandle.getPrimitive(JwtPublicKeySign.class);
@@ -167,15 +189,20 @@
 
   @Test
   public void test_wrapMultipleTinkKeys() throws Exception {
-    KeyTemplate tinkTemplate = KeyTemplates.get("JWT_ES256");
-
-    KeysetManager manager = KeysetManager.withEmptyKeyset();
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(tinkTemplate), /*asPrimary=*/ true);
-    KeysetHandle oldHandle = manager.getKeysetHandle();
-
-    manager.addNewKey(KeyTemplateProtoConverter.toProto(tinkTemplate), /*asPrimary=*/ true);
-
-    KeysetHandle newHandle = manager.getKeysetHandle();
+    KeysetHandle oldHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_ES256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
+    KeysetHandle newHandle =
+        KeysetHandle.newBuilder(oldHandle)
+            .addEntry(
+                KeysetHandle.generateEntryFromParametersName("JWT_ES256")
+                    .withRandomId()
+                    .makePrimary())
+            .build();
 
     JwtPublicKeySign oldSigner = oldHandle.getPrimitive(JwtPublicKeySign.class);
     JwtPublicKeySign newSigner = newHandle.getPrimitive(JwtPublicKeySign.class);
@@ -278,4 +305,32 @@
     JwtValidator validator = JwtValidator.newBuilder().allowMissingExpiration().build();
     assertThrows(JwtInvalidException.class, () -> jwtVerifier.verifyAndDecode(compact, validator));
   }
+
+  /* TODO: b/252792776. All keysets without primary should be rejected in every case. */
+  @Test
+  public void test_verifyWithoutPrimary_works() throws Exception {
+    Parameters parameters = KeyTemplates.get("JWT_ES256").toParameters();
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(parameters).withRandomId().makePrimary())
+            .addEntry(KeysetHandle.generateEntryFromParameters(parameters).withRandomId())
+            .build();
+    KeysetHandle publicHandle = handle.getPublicKeysetHandle();
+    Keyset publicKeyset =
+        Keyset.parseFrom(TinkProtoKeysetFormat.serializeKeysetWithoutSecret(publicHandle));
+    Keyset publicKeysetWithoutPrimary = publicKeyset.toBuilder().setPrimaryKeyId(0).build();
+    // TODO(b/252792776): Optimally, this would throw.
+    KeysetHandle publicHandleWithoutPrimary =
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(publicKeysetWithoutPrimary.toByteArray());
+
+    JwtPublicKeySign signer = handle.getPrimitive(JwtPublicKeySign.class);
+    // TODO(b/252792776): At least this should throw.
+    JwtPublicKeyVerify verifier = publicHandleWithoutPrimary.getPrimitive(JwtPublicKeyVerify.class);
+    RawJwt rawToken = RawJwt.newBuilder().setJwtId("blah").withoutExpiration().build();
+    String signedCompact = signer.signAndEncode(rawToken);
+    JwtValidator validator = JwtValidator.newBuilder().allowMissingExpiration().build();
+    VerifiedJwt verifiedToken = verifier.verifyAndDecode(signedCompact, validator);
+    assertThat(verifiedToken.getJwtId()).isEqualTo("blah");
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPkcs1SignKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPkcs1SignKeyManagerTest.java
index b94409a..158bcf8 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPkcs1SignKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPkcs1SignKeyManagerTest.java
@@ -36,6 +36,7 @@
 import com.google.crypto.tink.subtle.Base64;
 import com.google.crypto.tink.subtle.EngineFactory;
 import com.google.crypto.tink.subtle.Enums;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.RsaSsaPkcs1SignJce;
 import com.google.crypto.tink.testing.TestUtil;
@@ -181,10 +182,9 @@
 
   // Note: we use Theory as a parametrized test -- different from what the Theory framework intends.
   @Theory
-  public void createKeys_ok(
-      @FromDataPoints("algorithmParam") JwtRsaSsaPkcs1Algorithm algorithm,
-      @FromDataPoints("sizes") int keySize)
+  public void createKeys_ok(@FromDataPoints("algorithmParam") JwtRsaSsaPkcs1Algorithm algorithm)
       throws Exception {
+    int keySize = 2048;
     if (TestUtil.isTsan()) {
       // creating keys is too slow in Tsan.
       // We do not use assume because Theories expects to find something which is not skipped.
@@ -214,8 +214,8 @@
     int numTests = 5;
     for (int i = 0; i < numTests; i++) {
       JwtRsaSsaPkcs1PrivateKey key = factory.createKey(format);
-      keys.add(TestUtil.hexEncode(key.getQ().toByteArray()));
-      keys.add(TestUtil.hexEncode(key.getP().toByteArray()));
+      keys.add(Hex.encode(key.getQ().toByteArray()));
+      keys.add(Hex.encode(key.getP().toByteArray()));
     }
     assertThat(keys).hasSize(2 * numTests);
   }
@@ -532,9 +532,13 @@
     goodHeader.addProperty("alg", "RS256");
     goodHeader.addProperty("typ", "typeHeader");
     String goodSignedCompact = generateSignedCompact(rawSigner, goodHeader, payload);
-    verifier.verifyAndDecode(
-        goodSignedCompact,
-        JwtValidator.newBuilder().expectTypeHeader("typeHeader").allowMissingExpiration().build());
+    Object unused =
+        verifier.verifyAndDecode(
+            goodSignedCompact,
+            JwtValidator.newBuilder()
+                .expectTypeHeader("typeHeader")
+                .allowMissingExpiration()
+                .build());
 
     // invalid token with an empty header
     JsonObject emptyHeader = new JsonObject();
@@ -556,7 +560,7 @@
     unknownKidHeader.addProperty("alg", "RS256");
     unknownKidHeader.addProperty("kid", "unknown");
     String unknownKidSignedCompact = generateSignedCompact(rawSigner, unknownKidHeader, payload);
-    verifier.verifyAndDecode(unknownKidSignedCompact, validator);
+    unused = verifier.verifyAndDecode(unknownKidSignedCompact, validator);
   }
 
   @Test
@@ -590,7 +594,7 @@
     normalHeader.addProperty("alg", "RS256");
     normalHeader.addProperty("kid", kid);
     String validToken = generateSignedCompact(rawSigner, normalHeader, payload);
-    verifier.verifyAndDecode(validToken, validator);
+    Object unused = verifier.verifyAndDecode(validToken, validator);
 
     // token without kid are rejected, even if they are valid.
     JsonObject headerWithoutKid = new JsonObject();
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPssSignKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPssSignKeyManagerTest.java
index 1e65a91..7af4403 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPssSignKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtRsaSsaPssSignKeyManagerTest.java
@@ -37,6 +37,7 @@
 import com.google.crypto.tink.subtle.Base64;
 import com.google.crypto.tink.subtle.EngineFactory;
 import com.google.crypto.tink.subtle.Enums;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.RsaSsaPssSignJce;
 import com.google.crypto.tink.testing.TestUtil;
@@ -118,7 +119,8 @@
   // Note: we use Theory as a parametrized test -- different from what the Theory framework intends.
   @Theory
   public void validateKeyFormat_ok(
-      @FromDataPoints("algorithmParam") JwtRsaSsaPssAlgorithm algorithm, int keySize)
+      @FromDataPoints("algorithmParam") JwtRsaSsaPssAlgorithm algorithm,
+      @FromDataPoints("sizes") int keySize)
       throws GeneralSecurityException {
     JwtRsaSsaPssKeyFormat format = createKeyFormat(algorithm, keySize, RSAKeyGenParameterSpec.F4);
     factory.validateKeyFormat(format);
@@ -178,9 +180,9 @@
 
   // Note: we use Theory as a parametrized test -- different from what the Theory framework intends.
   @Theory
-  public void createKeys_ok(
-      @FromDataPoints("algorithmParam") JwtRsaSsaPssAlgorithm algorithm, int keySize)
+  public void createKeys_ok(@FromDataPoints("algorithmParam") JwtRsaSsaPssAlgorithm algorithm)
       throws Exception {
+    int keySize = 2048;
     if (TestUtil.isTsan()) {
       // creating keys is too slow in Tsan.
       // We do not use assume because Theories expects to find something which is not skipped.
@@ -210,8 +212,8 @@
     int numTests = 5;
     for (int i = 0; i < numTests; i++) {
       JwtRsaSsaPssPrivateKey key = factory.createKey(format);
-      keys.add(TestUtil.hexEncode(key.getQ().toByteArray()));
-      keys.add(TestUtil.hexEncode(key.getP().toByteArray()));
+      keys.add(Hex.encode(key.getQ().toByteArray()));
+      keys.add(Hex.encode(key.getP().toByteArray()));
     }
     assertThat(keys).hasSize(2 * numTests);
   }
@@ -219,7 +221,8 @@
   // Note: we use Theory as a parametrized test -- different from what the Theory framework intends.
   @Theory
   public void createCorruptedModulusPrimitive_throws(
-      @FromDataPoints("algorithmParam") JwtRsaSsaPssAlgorithm algorithm, int keySize)
+      @FromDataPoints("algorithmParam") JwtRsaSsaPssAlgorithm algorithm,
+      @FromDataPoints("sizes") int keySize)
       throws Exception {
     if (TestUtil.isTsan()) {
       // creating keys is too slow in Tsan.
@@ -495,9 +498,13 @@
     goodHeader.addProperty("alg", "PS256");
     goodHeader.addProperty("typ", "typeHeader");
     String goodSignedCompact = generateSignedCompact(rawSigner, goodHeader, payload);
-    verifier.verifyAndDecode(
-        goodSignedCompact,
-        JwtValidator.newBuilder().expectTypeHeader("typeHeader").allowMissingExpiration().build());
+    Object unused =
+        verifier.verifyAndDecode(
+            goodSignedCompact,
+            JwtValidator.newBuilder()
+                .expectTypeHeader("typeHeader")
+                .allowMissingExpiration()
+                .build());
 
     // invalid token with an empty header
     JsonObject emptyHeader = new JsonObject();
@@ -519,7 +526,7 @@
     unknownKidHeader.addProperty("alg", "PS256");
     unknownKidHeader.addProperty("kid", "unknown");
     String unknownKidSignedCompact = generateSignedCompact(rawSigner, unknownKidHeader, payload);
-    verifier.verifyAndDecode(unknownKidSignedCompact, validator);
+    unused = verifier.verifyAndDecode(unknownKidSignedCompact, validator);
   }
 
   @Test
@@ -551,7 +558,7 @@
     normalHeader.addProperty("alg", "PS256");
     normalHeader.addProperty("kid", kid);
     String validToken = generateSignedCompact(rawSigner, normalHeader, payload);
-    verifier.verifyAndDecode(validToken, validator);
+    Object unused = verifier.verifyAndDecode(validToken, validator);
 
     // token without kid are rejected, even if they are valid.
     JsonObject headerWithoutKid = new JsonObject();
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtTest.java
new file mode 100644
index 0000000..d12ddf9
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtTest.java
@@ -0,0 +1,271 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.jwt;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import java.security.GeneralSecurityException;
+import java.time.Clock;
+import java.time.Instant;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the jwt package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class JwtTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    JwtMacConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonMacKeyset_throws.
+  }
+
+  @DataPoints("jwt_mac_templates")
+  public static final String[] TEMPLATES =
+      new String[] {
+        "JWT_HS256",
+        "JWT_HS256_RAW",
+        "JWT_HS384",
+        "JWT_HS384_RAW",
+        "JWT_HS512",
+        "JWT_HS512_RAW",
+      };
+
+  @Theory
+  public void createComputeVerifyJwtMac(@FromDataPoints("jwt_mac_templates") String templateName)
+      throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    JwtMac jwtMac = handle.getPrimitive(JwtMac.class);
+    Instant now = Clock.systemUTC().instant();
+    RawJwt rawJwt =
+        RawJwt.newBuilder()
+            .setIssuer("issuer")
+            .addAudience("audience")
+            .setSubject("subject")
+            .addStringClaim("claimName", "claimValue")
+            .setExpiration(now.plusSeconds(100))
+            .build();
+    String token = jwtMac.computeMacAndEncode(rawJwt);
+
+    JwtValidator validator =
+        JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build();
+    VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(token, validator);
+    assertThat(verifiedJwt.getSubject()).isEqualTo("subject");
+    assertThat(verifiedJwt.getStringClaim("claimName")).isEqualTo("claimValue");
+
+    String expiredToken =
+        jwtMac.computeMacAndEncode(
+            RawJwt.newBuilder()
+                .setIssuer("issuer")
+                .addAudience("audience")
+                .setExpiration(now.minusSeconds(100))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class, () -> jwtMac.verifyMacAndDecode(expiredToken, validator));
+
+    String tokenWithInvalidIssuer =
+        jwtMac.computeMacAndEncode(
+            RawJwt.newBuilder()
+                .setIssuer("invalid")
+                .addAudience("audience")
+                .setSubject("subject")
+                .addStringClaim("claimName", "claimValue")
+                .setExpiration(now.minusSeconds(100))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> jwtMac.verifyMacAndDecode(tokenWithInvalidIssuer, validator));
+
+    String tokenWithInvalidAudience =
+        jwtMac.computeMacAndEncode(
+            RawJwt.newBuilder()
+                .setIssuer("issuer")
+                .addAudience("invalid")
+                .setSubject("subject")
+                .addStringClaim("claimName", "claimValue")
+                .setExpiration(now.minusSeconds(100))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> jwtMac.verifyMacAndDecode(tokenWithInvalidAudience, validator));
+
+    KeysetHandle otherHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    JwtMac otherJwtMac = otherHandle.getPrimitive(JwtMac.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> otherJwtMac.verifyMacAndDecode(token, validator));
+
+    assertThrows(
+        GeneralSecurityException.class, () -> jwtMac.verifyMacAndDecode("invalid", validator));
+    assertThrows(
+        GeneralSecurityException.class, () -> jwtMac.verifyMacAndDecode("", validator));
+  }
+
+  // A keyset with one JWT MAC key, serialized in Tink's JSON format.
+  private static final String JSON_JWT_MAC_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1685620571,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\","
+          + "        \"value\": \"GiDmRwUiwKDsPHd+2mSHwlLfzvkgoV5meopVKp+GCbhHthAB\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1685620571,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void readKeysetComputeVerifyJwtMac_success() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_JWT_MAC_KEYSET, InsecureSecretKeyAccess.get());
+    Instant now = Clock.systemUTC().instant();
+    JwtMac jwtMac = handle.getPrimitive(JwtMac.class);
+    RawJwt rawJwt =
+        RawJwt.newBuilder()
+            .setIssuer("issuer")
+            .addAudience("audience")
+            .setSubject("subject")
+            .setExpiration(now.plusSeconds(100))
+            .build();
+    String token = jwtMac.computeMacAndEncode(rawJwt);
+
+    JwtValidator validator =
+        JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build();
+    VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(token, validator);
+    assertThat(verifiedJwt.getSubject()).isEqualTo("subject");
+  }
+
+  // A keyset with multiple keys. The first key is the same as in JSON_AEAD_KEYSET.
+  private static final String JSON_JWT_MAC_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+      + "{"
+      + "  \"primaryKeyId\": 648866621,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\","
+      + "        \"value\": \"GiDmRwUiwKDsPHd+2mSHwlLfzvkgoV5meopVKp+GCbhHthAB\","
+      + "        \"keyMaterialType\": \"SYMMETRIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 1685620571,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    },"
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\","
+      + "        \"value\":"
+      + "\"GjBP5UIYeH40mAliduNPdvnkGqJci3mRpxjSHZ6jkBQ7ppuOGwpyBqsLobFspZOR+y0QAg==\","
+      + "        \"keyMaterialType\": \"SYMMETRIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 648866621,"
+      + "      \"outputPrefixType\": \"RAW\""
+      + "    },"
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\","
+      + "        \"value\": \"GkAjSoAXaQXhp8oHfEBdPUxKLWIA1hYNc+905NFRt0tYbDcje8LlPdmfVi8"
+      + "Xno7+U1xc0EPPxGFGfKPcIetKccgoEAM=\","
+      + "        \"keyMaterialType\": \"SYMMETRIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 923678323,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Theory
+  public void multipleKeysReadKeysetComputeVerifyJwtMac_success()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_JWT_MAC_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+    Instant now = Clock.systemUTC().instant();
+    JwtMac jwtMac = handle.getPrimitive(JwtMac.class);
+    RawJwt rawJwt =
+        RawJwt.newBuilder()
+            .setIssuer("issuer")
+            .addAudience("audience")
+            .setSubject("subject")
+            .setExpiration(now.plusSeconds(100))
+            .build();
+    String token = jwtMac.computeMacAndEncode(rawJwt);
+    JwtValidator validator =
+        JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build();
+    VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(token, validator);
+    assertThat(verifiedJwt.getSubject()).isEqualTo("subject");
+
+    // Also test that jwtMac can verify tokens computed with a non-primary key. We use
+    // JSON_JWT_MAC_KEYSET to compute a tag with the first key.
+    KeysetHandle handle1 =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_JWT_MAC_KEYSET, InsecureSecretKeyAccess.get());
+    JwtMac jwtMac1 = handle1.getPrimitive(JwtMac.class);
+    String token1 = jwtMac1.computeMacAndEncode(rawJwt);
+    assertThat(jwtMac.verifyMacAndDecode(token1, validator).getSubject()).isEqualTo("subject");
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the Mac primitive.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonMacKeyset_throws() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a DeterministicAead primitive, but not a JwtMac.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(JwtMac.class));
+  }
+
+  // TODO(juerg): Add tests for Jwt signatures.
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtValidatorTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtValidatorTest.java
index 1fec917..c3307c3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/JwtValidatorTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/JwtValidatorTest.java
@@ -170,9 +170,9 @@
 
     JwtValidator validator =
         JwtValidator.newBuilder().allowMissingExpiration().build();
-    validator.validate(tokenWithIssuedAtInTheFuture);
-    validator.validate(tokenWithIssuedAtInThePast);
-    validator.validate(tokenWithoutIssuedAt);
+    Object unused = validator.validate(tokenWithIssuedAtInTheFuture);
+    unused = validator.validate(tokenWithIssuedAtInThePast);
+    unused = validator.validate(tokenWithoutIssuedAt);
 
     JwtValidator issuedAtValidator =
         JwtValidator.newBuilder()
@@ -181,7 +181,7 @@
             .build();
     assertThrows(
         JwtInvalidException.class, () -> issuedAtValidator.validate(tokenWithIssuedAtInTheFuture));
-    issuedAtValidator.validate(tokenWithIssuedAtInThePast);
+    unused = issuedAtValidator.validate(tokenWithIssuedAtInThePast);
     assertThrows(JwtInvalidException.class, () -> issuedAtValidator.validate(tokenWithoutIssuedAt));
   }
 
@@ -207,7 +207,7 @@
             .expectIssuedInThePast()
             .setClockSkew(Duration.ofMinutes(1))
             .build();
-    validatorWithOneMinuteClockSkew.validate(tokenOneMinuteInTheFuture);
+    Object unused = validatorWithOneMinuteClockSkew.validate(tokenOneMinuteInTheFuture);
   }
 
   @Test
@@ -261,7 +261,7 @@
     JwtValidator validator = JwtValidator.newBuilder().allowMissingExpiration().build();
 
     RawJwt tokenWithoutTypeHeader = RawJwt.newBuilder().withoutExpiration().build();
-    validator.validate(tokenWithoutTypeHeader);
+    Object unused = validator.validate(tokenWithoutTypeHeader);
   }
 
   @Test
@@ -280,9 +280,9 @@
 
     RawJwt tokenWithTypeHeader =
         RawJwt.newBuilder().setTypeHeader("headerType").withoutExpiration().build();
-    validator.validate(tokenWithTypeHeader);
+    Object unused = validator.validate(tokenWithTypeHeader);
     RawJwt tokenWithoutTypeHeader = RawJwt.newBuilder().withoutExpiration().build();
-    validator.validate(tokenWithoutTypeHeader);
+    unused = validator.validate(tokenWithoutTypeHeader);
   }
 
   @Test
@@ -319,7 +319,7 @@
     JwtValidator validator = JwtValidator.newBuilder().allowMissingExpiration().build();
 
     RawJwt tokenWithoutIssuer = RawJwt.newBuilder().withoutExpiration().build();
-    validator.validate(tokenWithoutIssuer);
+    Object unused = validator.validate(tokenWithoutIssuer);
   }
 
   @Test
@@ -336,9 +336,9 @@
         JwtValidator.newBuilder().allowMissingExpiration().ignoreIssuer().build();
 
     RawJwt tokenWithIssuer = RawJwt.newBuilder().setIssuer("issuer").withoutExpiration().build();
-    validator.validate(tokenWithIssuer);
+    Object unused = validator.validate(tokenWithIssuer);
     RawJwt tokenWithoutIssuer = RawJwt.newBuilder().withoutExpiration().build();
-    validator.validate(tokenWithoutIssuer);
+    unused = validator.validate(tokenWithoutIssuer);
   }
 
 
@@ -364,7 +364,7 @@
   public void noAudience_success() throws Exception {
     RawJwt token = RawJwt.newBuilder().withoutExpiration().build();
     JwtValidator validator = JwtValidator.newBuilder().allowMissingExpiration().build();
-    validator.validate(token);
+    Object unused = validator.validate(token);
   }
 
   @Test
@@ -410,9 +410,9 @@
             .addAudience("audience2")
             .withoutExpiration()
             .build();
-    validator.validate(tokenWithAudiences);
+    Object unused = validator.validate(tokenWithAudiences);
     RawJwt tokenWithoutAudience = RawJwt.newBuilder().withoutExpiration().build();
-    validator.validate(tokenWithoutAudience);
+    unused = validator.validate(tokenWithoutAudience);
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/jwt/RawJwtTest.java b/java_src/src/test/java/com/google/crypto/tink/jwt/RawJwtTest.java
index f1cb8a4..f932c2d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/jwt/RawJwtTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/jwt/RawJwtTest.java
@@ -461,6 +461,12 @@
   }
 
   @Test
+  public void integerIsEncodedAsInteger() throws Exception {
+    RawJwt token = RawJwt.newBuilder().addNumberClaim("num", 1).withoutExpiration().build();
+    assertThat(token.getJsonPayload()).isEqualTo("{\"num\":1}");
+  }
+
+  @Test
   public void getJsonPayload_success() throws Exception {
     RawJwt token = RawJwt.newBuilder().setJwtId("id").withoutExpiration().build();
     assertThat(token.getJsonPayload()).isEqualTo("{\"jti\":\"id\"}");
@@ -600,7 +606,7 @@
   }
 
   @Test
-  public void fromJsonPayloadWithValidJsonEscapedCharacter_shouldThrow() throws Exception {
+  public void fromJsonPayloadWithValidJsonEscapedCharacter_success() throws Exception {
     RawJwt token = RawJwt.fromJsonPayload(Optional.empty(), "{\"iss\":\"\\uD834\\uDD1E\"}");
     assertThat(token.hasIssuer()).isTrue();
     assertThat(token.getIssuer()).isEqualTo("\uD834\uDD1E");
@@ -641,12 +647,19 @@
   }
 
   @Test
-  public void fromJsonPayloadWithComments_shouldThrow () throws Exception {
+  public void fromJsonPayloadWithComments_shouldThrow() throws Exception {
     String input = "{\"sub\": \"subject\" /*, \"iss\": \"issuer\" */}";
     assertThrows(JwtInvalidException.class, () -> RawJwt.fromJsonPayload(Optional.empty(), input));
   }
 
   @Test
+  public void fromJsonPayloadWithTwoIdenticalClaimNames_shouldThrow() throws Exception {
+    String input = "{\"claim\": \"claim1\", \"claim\": \"claim2\"}";
+    assertThrows(JwtInvalidException.class, () -> RawJwt.fromJsonPayload(Optional.empty(), input));
+  }
+
+
+  @Test
   public void fromJsonPayloadWithEscapedChars_success() throws Exception {
     String input = "{\"i\\u0073\\u0073\": \"\\u0061lice\"}";
     RawJwt token = RawJwt.fromJsonPayload(Optional.empty(), input);
@@ -659,6 +672,34 @@
     assertThrows(JwtInvalidException.class, () -> RawJwt.fromJsonPayload(Optional.empty(), input));
   }
 
+  @Test
+  public void fromJsonPayload_withLargeNumberClaim() throws Exception {
+    String input =
+        "{\"numClaim\":   "
+            + "9999999999999999999999999999999999999999999900000000000000000000000}";
+    RawJwt token = RawJwt.fromJsonPayload(Optional.empty(), input);
+    assertThat(token.getNumberClaim("numClaim"))
+        .isWithin(0.0002e66)
+        .of(9.9999e66);
+  }
+
+  @Test
+  public void fromJsonPayload_withLargerThanDoubleMaxValueNumberClaim_isLarge() throws Exception {
+    String input =
+        "{\"numClaim\":   "
+            + "99999999999999999999999999.99e+99999999999999999999999999}";
+    RawJwt token = RawJwt.fromJsonPayload(Optional.empty(), input);
+    assertThat(token.getNumberClaim("numClaim")).isAtLeast(Double.MAX_VALUE);
+  }
+
+  @Test
+  public void fromJsonPayload_withSmallerThanDoubleMinValueNumberClaim_isSmall() throws Exception {
+    String input =
+        "{\"numClaim\":   "
+            + "-99999999999999999999999999.99e+99999999999999999999999999}";
+    RawJwt token = RawJwt.fromJsonPayload(Optional.empty(), input);
+    assertThat(token.getNumberClaim("numClaim")).isAtMost(Double.MIN_VALUE);
+  }
 
   @Test
   public void getClaimsOfDifferentType_shouldThrow() throws Exception {
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/keyderivation/BUILD.bazel
new file mode 100644
index 0000000..f36fff6
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/BUILD.bazel
@@ -0,0 +1,117 @@
+licenses(["notice"])
+
+java_test(
+    name = "KeyDerivationConfigTest",
+    size = "small",
+    srcs = ["KeyDerivationConfigTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:registry",
+        "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeyDerivationKeyTemplatesTest",
+    size = "small",
+    srcs = ["KeyDerivationKeyTemplatesTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/internal:util",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key_templates",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeyDerivationTest",
+    size = "small",
+    srcs = ["KeyDerivationTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:aead",
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_template",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config",
+        "//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key_templates",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeysetDeriverWrapperTest",
+    size = "small",
+    srcs = ["KeysetDeriverWrapperTest.java"],
+    deps = [
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver_wrapper",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PrfBasedKeyDerivationParametersTest",
+    size = "small",
+    srcs = ["PrfBasedKeyDerivationParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:tink_bug_exception",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PrfBasedKeyDerivationKeyTest",
+    size = "small",
+    srcs = ["PrfBasedKeyDerivationKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_key",
+        "//src/main/java/com/google/crypto/tink/keyderivation:prf_based_key_derivation_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationConfigTest.java
new file mode 100644
index 0000000..8821dee
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationConfigTest.java
@@ -0,0 +1,111 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Registry;
+import com.google.crypto.tink.config.TinkFips;
+import java.security.GeneralSecurityException;
+import org.junit.Assume;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.MethodSorters;
+
+/**
+ * Tests for KeyDerivationConfig. Using FixedMethodOrder to ensure that aaaTestInitialization runs
+ * first, as it tests execution of a static block within KeyDerivationConfig-class.
+ */
+@RunWith(JUnit4.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class KeyDerivationConfigTest {
+
+  // This test must run first.
+  @Test
+  public void aaaTestInitialization() throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    String[] keyTypeUrls = {
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey",
+      "type.googleapis.com/google.crypto.tink.HkdfPrfKey",
+    };
+
+    for (String typeUrl : keyTypeUrls) {
+      GeneralSecurityException e =
+          assertThrows(
+              GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
+      assertThat(e.toString()).contains("No key manager found");
+    }
+
+    // Initialize the config.
+    KeyDerivationConfig.register();
+
+    // After registration the key manager should be present.
+    for (String typeUrl : keyTypeUrls) {
+      assertNotNull(Registry.getUntypedKeyManager(typeUrl));
+    }
+    assertNotNull(
+        Registry.getKeyManager(
+            "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey", KeysetDeriver.class));
+
+    // Running init() manually again should succeed.
+    KeyDerivationConfig.register();
+  }
+
+  @Test
+  public void testNoFipsRegister() throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    // Register key derivation key manager.
+    KeyDerivationConfig.register();
+
+    // Check if all key types are registered when not using FIPS mode.
+    String[] keyTypeUrls = {
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey",
+      "type.googleapis.com/google.crypto.tink.HkdfPrfKey",
+    };
+
+    for (String typeUrl : keyTypeUrls) {
+      assertNotNull(Registry.getUntypedKeyManager(typeUrl));
+    }
+    assertNotNull(
+        Registry.getKeyManager(
+            "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey", KeysetDeriver.class));
+  }
+
+  @Test
+  public void testFipsRegisterNonFipsKeys() throws Exception {
+    Assume.assumeTrue(TinkFips.useOnlyFips());
+
+    // Register key derivation key manager.
+    KeyDerivationConfig.register();
+
+    // List of algorithms which are not part of FIPS and should not be registered.
+    String[] keyTypeUrls = {
+      "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey",
+      "type.googleapis.com/google.crypto.tink.HkdfPrfKey",
+    };
+
+    for (String typeUrl : keyTypeUrls) {
+      assertThrows(GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
+    }
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationKeyTemplatesTest.java
new file mode 100644
index 0000000..c6c8f88
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationKeyTemplatesTest.java
@@ -0,0 +1,105 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.Util.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeyTemplate.OutputPrefixType;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.prf.PrfConfig;
+import com.google.crypto.tink.testing.TestUtil;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class KeyDerivationKeyTemplatesTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    KeyDerivationConfig.register();
+    PrfConfig.register();
+    AeadConfig.register();
+  }
+
+  @Test
+  public void createPrfBasedKeyTemplate_succeeds() throws Exception {
+    Assume.assumeFalse(TestUtil.isAndroid()); // Some Android versions don't support AES-GCM.
+
+    KeyTemplate prfTemplate = KeyTemplates.get("HKDF_SHA256");
+    KeyTemplate aeadTemplate = KeyTemplates.get("AES256_GCM");
+
+    List<OutputPrefixType> outputPrefixTypes = new ArrayList<>();
+    outputPrefixTypes.add(OutputPrefixType.TINK);
+    outputPrefixTypes.add(OutputPrefixType.LEGACY);
+    outputPrefixTypes.add(OutputPrefixType.RAW);
+    outputPrefixTypes.add(OutputPrefixType.CRUNCHY);
+
+    for (OutputPrefixType outputPrefixType : outputPrefixTypes) {
+      KeyTemplate derivedTemplate =
+          KeyTemplate.create(aeadTemplate.getTypeUrl(), aeadTemplate.getValue(), outputPrefixType);
+      KeyTemplate prfBasedTemplate =
+          KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(prfTemplate, derivedTemplate);
+
+      KeysetHandle handle = KeysetHandle.generateNew(prfBasedTemplate);
+      KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+      KeysetHandle derivedHandle = deriver.deriveKeyset("salty".getBytes(UTF_8));
+      assertThat(derivedHandle.getKeysetInfo().getKeyInfoCount()).isEqualTo(1);
+      assertThat(derivedHandle.getKeysetInfo().getKeyInfo(0).getOutputPrefixType().toString())
+          .isEqualTo(outputPrefixType.toString());
+
+      // Use derivedHandle, which contains an AES256_GCM key.
+      Aead aead = derivedHandle.getPrimitive(Aead.class);
+      byte[] plaintext = "plaintext".getBytes(UTF_8);
+      byte[] associatedData = "associatedData".getBytes(UTF_8);
+      byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+      assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+    }
+  }
+
+  @Test
+  public void createPrfBasedKeyTemplate_failsForNotPrf() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+                KeyTemplates.get("AES256_GCM"), KeyTemplates.get("AES256_GCM")));
+  }
+
+  @Test
+  public void createPrfBasedKeyTemplate_failsForNotDerivableKeyType() throws Exception {
+    KeyTemplate derivedTemplate =
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES256_GCM"));
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+                KeyTemplates.get("HKDF_SHA256"), derivedTemplate));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationTest.java
new file mode 100644
index 0000000..bf32afa
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeyDerivationTest.java
@@ -0,0 +1,274 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.prf.PrfConfig;
+import com.google.crypto.tink.testing.TestUtil;
+import java.security.GeneralSecurityException;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for the Key Derivation package. Uses only the public API. */
+@RunWith(JUnit4.class)
+public final class KeyDerivationTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    PrfConfig.register();  // Needed for PRF-based key derivation
+    AeadConfig.register();  // Needed to derive Aead keys
+    MacConfig.register();  // Needed to derive Mac keys
+    KeyDerivationConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonKeysetDeriverKeyset_throws.
+  }
+
+  @Test
+  public void createTemplateAndDeriveAesGcmKeyset_success() throws Exception {
+    Assume.assumeFalse(TestUtil.isAndroid()); // some android versions don't support AesGcm
+    KeyTemplate keyDerivationTemplate =
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES256_GCM"));
+
+    KeysetHandle handle = KeysetHandle.generateNew(keyDerivationTemplate);
+    KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+
+    KeysetHandle derivedHandle = deriver.deriveKeyset("salt".getBytes(UTF_8));
+
+    // Use derived keyset, which should contain an AES256_GCM key.
+    Aead aead = derivedHandle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+  }
+
+  private static final String JSON_AEAD_KEYSET_DERIVATION_KEYSET = ""
+      + "{"
+      + "  \"primaryKeyId\": 2494827163,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\":"
+      + "\"type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey\","
+      + "        \"value\": \"GjoKOBgBEgIQIAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHR"
+      + "vLnRpbmsuQWVzR2NtS2V5El0KMXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkh"
+      + "rZGZQcmZLZXkSJhog6TOgPvUUH+iOCewfS5BhSltazMDIGQt3sNEBl1MwFLsSAggDGAE=\","
+      + "        \"keyMaterialType\": \"SYMMETRIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 2494827163,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Test
+  public void readKeysetAndDeriveAesGcmKeyset_success() throws Exception {
+    Assume.assumeFalse(TestUtil.isAndroid()); // some android versions don't support AesGcm
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_AEAD_KEYSET_DERIVATION_KEYSET, InsecureSecretKeyAccess.get());
+    KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+
+    KeysetHandle derivedHandle = deriver.deriveKeyset("salt".getBytes(UTF_8));
+
+    Aead aead = derivedHandle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+  }
+
+  // A keyset with multiple keys. The first key is the same as in JSON_AEAD_KEYSET.
+  private static final String JSON_AEAD_KEYSET_DERIVATION_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1956345672,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey\","
+          + "        \"value\": \"GjoKOBgBEgIQIAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHR"
+          + "vLnRpbmsuQWVzR2NtS2V5El0KMXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkh"
+          + "rZGZQcmZLZXkSJhog6TOgPvUUH+iOCewfS5BhSltazMDIGQt3sNEBl1MwFLsSAggDGAE=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 2494827163,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey\","
+          + "        \"value\": \"El0KMXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkh"
+          + "rZGZQcmZLZXkSJhICCAMaIK6DXudkRW9O/oHDHRdEDW6WLYm/QpQusLolaceFfYSZGAEaOgo4CjB0eXB"
+          + "lLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAgGAM=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1956345672,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey\","
+          + "        \"value\": \"GjoKOBgBEgIQEAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHR"
+          + "vLnRpbmsuQWVzR2NtS2V5El0KMXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkh"
+          + "rZGZQcmZLZXkSJhoggQMOJny+9/MlRAIZohEinWov3jeLHBoJD+hnwTS2TIUSAggDGAE=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 2901163075,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Test
+  public void multipleKeysReadKeysetAndDeriveAesGcmKeyset_success() throws Exception {
+    Assume.assumeFalse(TestUtil.isAndroid()); // some android versions don't support AesGcm
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_AEAD_KEYSET_DERIVATION_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+    KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+
+    KeysetHandle derivedHandle = deriver.deriveKeyset("salt".getBytes(UTF_8));
+
+    // Derived keyset should only contain AEAD keys
+    Aead aead = derivedHandle.getPrimitive(Aead.class);
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+    byte[] ciphertext = aead.encrypt(plaintext, associatedData);
+    assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext);
+
+    // Also test that aead can decrypt messages encrypted with keyset derived from a non-primary
+    // key. Use JSON_AEAD_KEYSET_DERIVATION_KEYSET, because it contains the first key from
+    // JSON_AEAD_KEYSET_DERIVATION_KEYSET_WITH_MULTIPLE_KEYS.
+    KeysetHandle handle1 =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_AEAD_KEYSET_DERIVATION_KEYSET, InsecureSecretKeyAccess.get());
+    KeysetDeriver deriver1 = handle1.getPrimitive(KeysetDeriver.class);
+    KeysetHandle derivedHandle1 = deriver1.deriveKeyset("salt".getBytes(UTF_8));
+    Aead aead1 = derivedHandle1.getPrimitive(Aead.class);
+    byte[] ciphertext1 = aead1.encrypt(plaintext, associatedData);
+    assertThat(aead.decrypt(ciphertext1, associatedData)).isEqualTo(plaintext);
+  }
+
+  @Test
+  public void createTemplateAndDeriveHmacKeyset_success() throws Exception {
+    KeyTemplate keyDerivationTemplate =
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("HMAC_SHA256_128BITTAG"));
+
+    KeysetHandle handle = KeysetHandle.generateNew(keyDerivationTemplate);
+    KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+
+    KeysetHandle derivedHandle = deriver.deriveKeyset("salt".getBytes(UTF_8));
+
+    // Use derived keyset, which should contain an HMAC key.
+    Mac mac = derivedHandle.getPrimitive(Mac.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+  }
+
+  private static final String JSON_MAC_KEYSET_DERIVATION_KEYSET = ""
+      + "{"
+      + "  \"primaryKeyId\": 104819069,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\":"
+      + "\"type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey\","
+      + "        \"value\": \"El0KMXR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkh"
+      + "rZGZQcmZLZXkSJhICCAMaIOlSI9lmjAIyFTOEUUt2DqEdjSFCt+yFJDp5Z7/+h6tKGAEaPgo8Ci50eXB"
+      + "lLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5IbWFjS2V5EggQIAoEEBAIAxgB\","
+      + "        \"keyMaterialType\": \"SYMMETRIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 104819069,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Test
+  public void readKeysetAndDeriveHmacKeyset_success() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_MAC_KEYSET_DERIVATION_KEYSET, InsecureSecretKeyAccess.get());
+    KeysetDeriver deriver = handle.getPrimitive(KeysetDeriver.class);
+
+    KeysetHandle derivedHandle = deriver.deriveKeyset("salt".getBytes(UTF_8));
+
+    // Use derived keyset, which should contain an HMAC key.
+    Mac mac = derivedHandle.getPrimitive(Mac.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the KeysetDeriver
+  // primitive.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Test
+  public void getPrimitiveFromNonKeysetDeriverKeyset_throws() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a DeterministicAead primitive, but not a KeysetDeriver.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(KeysetDeriver.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeysetDeriverWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeysetDeriverWrapperTest.java
new file mode 100644
index 0000000..8934d5b
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/KeysetDeriverWrapperTest.java
@@ -0,0 +1,182 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.errorprone.annotations.Immutable;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for KeysetDeriverWrapper. */
+@RunWith(JUnit4.class)
+public class KeysetDeriverWrapperTest {
+  @Immutable
+  private static class DummyDeriver implements KeysetDeriver {
+    private final String hexEncodedName;
+
+    public DummyDeriver(byte[] name) {
+      this.hexEncodedName = Hex.encode(name);
+    }
+
+    @Override
+    public KeysetHandle deriveKeyset(byte[] salt) throws GeneralSecurityException {
+      Keyset keyset =
+          Keyset.newBuilder()
+              .addKey(
+                  Keyset.Key.newBuilder()
+                      .setKeyData(
+                          KeyData.newBuilder().setTypeUrl(hexEncodedName + ":" + Hex.encode(salt)))
+                      .setStatus(KeyStatusType.UNKNOWN_STATUS)
+                      .setKeyId(0)
+                      .setOutputPrefixType(OutputPrefixType.UNKNOWN_PREFIX))
+              .setPrimaryKeyId(0)
+              .build();
+      return TinkProtoKeysetFormat.parseKeyset(keyset.toByteArray(), InsecureSecretKeyAccess.get());
+    }
+  }
+
+  private final KeysetDeriverWrapper wrapper = new KeysetDeriverWrapper();
+
+  @Test
+  public void test_wrapEmpty_throws() throws Exception {
+    PrimitiveSet<KeysetDeriver> primitiveSet = PrimitiveSet.newBuilder(KeysetDeriver.class).build();
+
+    assertThrows(GeneralSecurityException.class, () -> wrapper.wrap(primitiveSet));
+  }
+
+  @Test
+  public void test_wrapNoPrimary_throws() throws Exception {
+    PrimitiveSet<KeysetDeriver> primitiveSet =
+        PrimitiveSet.newBuilder(KeysetDeriver.class)
+            .addPrimitive(
+                new DummyDeriver(new byte[0]),
+                Keyset.Key.newBuilder()
+                    .setKeyId(1234)
+                    .setStatus(KeyStatusType.ENABLED)
+                    .setOutputPrefixType(OutputPrefixType.TINK)
+                    .build())
+            .build();
+
+    assertThrows(GeneralSecurityException.class, () -> wrapper.wrap(primitiveSet));
+  }
+
+  @Test
+  public void test_wrapSingle_works() throws Exception {
+    PrimitiveSet<KeysetDeriver> primitiveSet =
+        PrimitiveSet.newBuilder(KeysetDeriver.class)
+            .addPrimaryPrimitive(
+                new DummyDeriver("wrap_single_key".getBytes(UTF_8)),
+                Keyset.Key.newBuilder()
+                    .setKeyId(1234)
+                    .setStatus(KeyStatusType.ENABLED)
+                    .setOutputPrefixType(OutputPrefixType.TINK)
+                    .setKeyData(
+                        KeyData.newBuilder()
+                            .setTypeUrl(
+                                "type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"))
+                    .build())
+            .build();
+
+    KeysetDeriver wrapped = wrapper.wrap(primitiveSet);
+
+    Keyset keyset = CleartextKeysetHandle.getKeyset(wrapped.deriveKeyset("salt".getBytes(UTF_8)));
+
+    assertThat(keyset.getPrimaryKeyId()).isEqualTo(1234);
+    assertThat(keyset.getKeyList()).hasSize(1);
+    assertThat(keyset.getKey(0).getKeyData().getTypeUrl())
+        .isEqualTo(
+            Hex.encode("wrap_single_key".getBytes(UTF_8))
+                + ":"
+                + Hex.encode("salt".getBytes(UTF_8)));
+    assertThat(keyset.getKey(0).getStatus()).isEqualTo(KeyStatusType.ENABLED);
+    assertThat(keyset.getKey(0).getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
+  }
+
+  @Test
+  public void test_wrapMultiple_works() throws Exception {
+    Keyset.Key key0 =
+        Keyset.Key.newBuilder()
+            .setKeyId(999999)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.RAW)
+            .setKeyData(
+                KeyData.newBuilder()
+                    .setTypeUrl("type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"))
+            .build();
+    Keyset.Key key1 =
+        Keyset.Key.newBuilder()
+            .setKeyId(101010)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.LEGACY)
+            .setKeyData(
+                KeyData.newBuilder()
+                    .setTypeUrl("type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"))
+            .build();
+    Keyset.Key key2 =
+        Keyset.Key.newBuilder()
+            .setKeyId(202020)
+            .setStatus(KeyStatusType.ENABLED)
+            .setOutputPrefixType(OutputPrefixType.TINK)
+            .setKeyData(
+                KeyData.newBuilder()
+                    .setTypeUrl("type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey"))
+            .build();
+    PrimitiveSet<KeysetDeriver> pset =
+        PrimitiveSet.newBuilder(KeysetDeriver.class)
+            .addPrimitive(new DummyDeriver("k0".getBytes(UTF_8)), key0)
+            .addPrimaryPrimitive(new DummyDeriver("k1".getBytes(UTF_8)), key1)
+            .addPrimitive(new DummyDeriver("k2".getBytes(UTF_8)), key2)
+            .build();
+    KeysetDeriver wrapped = wrapper.wrap(pset);
+
+    Keyset keyset = CleartextKeysetHandle.getKeyset(wrapped.deriveKeyset("salt".getBytes(UTF_8)));
+    assertThat(keyset.getPrimaryKeyId()).isEqualTo(101010);
+
+    assertThat(keyset.getKey(0).getKeyData().getTypeUrl())
+        .isEqualTo(Hex.encode("k0".getBytes(UTF_8)) + ":" + Hex.encode("salt".getBytes(UTF_8)));
+    assertThat(keyset.getKey(0).getKeyId()).isEqualTo(999999);
+    assertThat(keyset.getKey(0).getStatus()).isEqualTo(KeyStatusType.ENABLED);
+    assertThat(keyset.getKey(0).getOutputPrefixType()).isEqualTo(OutputPrefixType.RAW);
+
+    assertThat(keyset.getKey(1).getKeyData().getTypeUrl())
+        .isEqualTo(Hex.encode("k1".getBytes(UTF_8)) + ":" + Hex.encode("salt".getBytes(UTF_8)));
+    assertThat(keyset.getKey(1).getKeyId()).isEqualTo(101010);
+    assertThat(keyset.getKey(1).getStatus()).isEqualTo(KeyStatusType.ENABLED);
+    assertThat(keyset.getKey(1).getOutputPrefixType()).isEqualTo(OutputPrefixType.LEGACY);
+
+    assertThat(keyset.getKey(2).getKeyData().getTypeUrl())
+        .isEqualTo(Hex.encode("k2".getBytes(UTF_8)) + ":" + Hex.encode("salt".getBytes(UTF_8)));
+    assertThat(keyset.getKey(2).getKeyId()).isEqualTo(202020);
+    assertThat(keyset.getKey(2).getStatus()).isEqualTo(KeyStatusType.ENABLED);
+    assertThat(keyset.getKey(2).getOutputPrefixType()).isEqualTo(OutputPrefixType.TINK);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationKeyTest.java
new file mode 100644
index 0000000..0ff00b5
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationKeyTest.java
@@ -0,0 +1,291 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.AesEaxParameters;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.prf.HmacPrfKey;
+import com.google.crypto.tink.prf.HmacPrfParameters;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class PrfBasedKeyDerivationKeyTest {
+
+  @Test
+  public void testCreateAndValues_basic() throws Exception {
+    HmacPrfParameters hmacPrfParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfKey prfKey =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    PrfBasedKeyDerivationParameters derivationParameters =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.TINK)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    PrfBasedKeyDerivationKey keyDerivationKey =
+        PrfBasedKeyDerivationKey.create(derivationParameters, prfKey, /* idRequirement= */ 102);
+
+    assertThat(keyDerivationKey).isNotNull();
+    assertThat(keyDerivationKey.getParameters()).isEqualTo(derivationParameters);
+    assertThat(keyDerivationKey.getPrfKey().equalsKey(prfKey)).isTrue();
+    assertThat(keyDerivationKey.getIdRequirementOrNull()).isEqualTo(102);
+  }
+
+  @Test
+  public void testCreate_noIdRequirement_works() throws Exception {
+    HmacPrfParameters hmacPrfParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfKey prfKey =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    PrfBasedKeyDerivationParameters derivationParameters =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    // Derived Key does not want an ID requirement:
+                    .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    PrfBasedKeyDerivationKey keyDerivationKey =
+        PrfBasedKeyDerivationKey.create(derivationParameters, prfKey, /* idRequirement= */ null);
+
+    assertThat(keyDerivationKey).isNotNull();
+    assertThat(keyDerivationKey.getParameters()).isEqualTo(derivationParameters);
+    assertThat(keyDerivationKey.getPrfKey().equalsKey(prfKey)).isTrue();
+    assertThat(keyDerivationKey.getIdRequirementOrNull()).isEqualTo(null);
+  }
+
+  @Test
+  public void testCreate_idRequirementWithMissingVariant_throws() throws Exception {
+    HmacPrfParameters hmacPrfParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfKey prfKey =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    PrfBasedKeyDerivationParameters derivationParameters =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    // Derived Key does not want an ID requirement:
+                    .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            PrfBasedKeyDerivationKey.create(derivationParameters, prfKey, /* idRequirement= */ 12));
+  }
+
+  @Test
+  public void testCreate_prefixVariantWithMissingIdRequirement_throws() throws Exception {
+    HmacPrfParameters hmacPrfParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfKey prfKey =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    PrfBasedKeyDerivationParameters derivationParameters =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.TINK)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            PrfBasedKeyDerivationKey.create(
+                derivationParameters, prfKey, /* idRequirement= */ null));
+  }
+
+  @Test
+  public void testCreate_mismatchedPrfParmetersAndKey_throws() throws Exception {
+    HmacPrfParameters hmacPrfParameters1 =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfParameters hmacPrfParameters2 =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(32)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfKey prfKey =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters1)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    PrfBasedKeyDerivationParameters derivationParameters =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.TINK)
+                    .build())
+            .setPrfParameters(hmacPrfParameters2)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            PrfBasedKeyDerivationKey.create(
+                derivationParameters, prfKey, /* idRequirement= */ 102));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    // We make copies in various places of inner objects to ensure that the code doesn't
+    // use == but ".equals()"
+    HmacPrfParameters hmacPrfParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfKey prfKey =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    HmacPrfKey prfKeyCopy =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(prfKey.getKeyBytes())
+            .build();
+
+    HmacPrfKey prfKey2 =
+        HmacPrfKey.builder()
+            .setParameters(hmacPrfParameters)
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+
+    PrfBasedKeyDerivationParameters derivationParameters =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.TINK)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    PrfBasedKeyDerivationParameters derivationParametersCopy =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.TINK)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    PrfBasedKeyDerivationParameters derivationParameters2 =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.CRUNCHY)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+    PrfBasedKeyDerivationParameters derivationParametersNoPrefix =
+        PrfBasedKeyDerivationParameters.builder()
+            .setDerivedKeyParameters(
+                AesEaxParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setIvSizeBytes(16)
+                    .setTagSizeBytes(16)
+                    .setVariant(AesEaxParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPrfParameters(hmacPrfParameters)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "derivationParameters, prfKey, ID 101",
+            PrfBasedKeyDerivationKey.create(derivationParameters, prfKey, /* idRequirement= */ 101),
+            PrfBasedKeyDerivationKey.create(
+                derivationParametersCopy, prfKey, /* idRequirement= */ 101),
+            PrfBasedKeyDerivationKey.create(
+                derivationParameters, prfKeyCopy, /* idRequirement= */ 101))
+        .addEqualityGroup(
+            "derivationParameters, prfKey, ID 102",
+            PrfBasedKeyDerivationKey.create(derivationParameters, prfKey, /* idRequirement= */ 102))
+        .addEqualityGroup(
+            "derivationParameters, prfKey2, ID 101",
+            PrfBasedKeyDerivationKey.create(
+                derivationParameters, prfKey2, /* idRequirement= */ 101))
+        .addEqualityGroup(
+            "derivationParameters2, prfKey, ID 101",
+            PrfBasedKeyDerivationKey.create(
+                derivationParameters2, prfKey, /* idRequirement= */ 101))
+        .addEqualityGroup(
+            "derivationParametersNoPrefix, prfKey",
+            PrfBasedKeyDerivationKey.create(
+                derivationParametersNoPrefix, prfKey, /* idRequirement= */ null))
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationParametersTest.java
new file mode 100644
index 0000000..97aeae8
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/PrfBasedKeyDerivationParametersTest.java
@@ -0,0 +1,137 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.TinkBugException.exceptionIsBug;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.aead.XChaCha20Poly1305Parameters;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.prf.AesCmacPrfParameters;
+import com.google.crypto.tink.prf.HmacPrfParameters;
+import com.google.crypto.tink.prf.PrfParameters;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class PrfBasedKeyDerivationParametersTest {
+  private static final PrfParameters PRF_PARAMETERS_1 =
+      exceptionIsBug(() -> AesCmacPrfParameters.create(16));
+  private static final PrfParameters PRF_PARAMETERS_1_COPY =
+      exceptionIsBug(() -> AesCmacPrfParameters.create(16));
+  private static final PrfParameters PRF_PARAMETERS_2 =
+      exceptionIsBug(
+          () ->
+              HmacPrfParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setHashType(HmacPrfParameters.HashType.SHA256)
+                  .build());
+  private static final Parameters DERIVED_PARAMETERS_1 =
+      exceptionIsBug(() -> XChaCha20Poly1305Parameters.create());
+  private static final Parameters DERIVED_PARAMETERS_1_COPY =
+      exceptionIsBug(() -> XChaCha20Poly1305Parameters.create());
+  private static final Parameters DERIVED_PARAMETERS_2 =
+      exceptionIsBug(
+          () ->
+              HmacParameters.builder()
+                  .setKeySizeBytes(16)
+                  .setTagSizeBytes(16)
+                  .setVariant(HmacParameters.Variant.NO_PREFIX)
+                  .setHashType(HmacParameters.HashType.SHA1)
+                  .build());
+
+  @Test
+  public void testCreateAndGet_works() throws Exception {
+    PrfBasedKeyDerivationParameters params =
+        PrfBasedKeyDerivationParameters.builder()
+            .setPrfParameters(PRF_PARAMETERS_1)
+            .setDerivedKeyParameters(DERIVED_PARAMETERS_1)
+            .build();
+    assertThat(params.getPrfParameters()).isEqualTo(PRF_PARAMETERS_1);
+    assertThat(params.getDerivedKeyParameters()).isEqualTo(DERIVED_PARAMETERS_1);
+  }
+
+  @Test
+  public void test_missingPrfParameters_throws() throws Exception {
+    PrfBasedKeyDerivationParameters.Builder builder =
+        PrfBasedKeyDerivationParameters.builder().setDerivedKeyParameters(DERIVED_PARAMETERS_1);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void test_missingDerivedKeyParameters_throws() throws Exception {
+    PrfBasedKeyDerivationParameters.Builder builder =
+        PrfBasedKeyDerivationParameters.builder().setPrfParameters(PRF_PARAMETERS_1);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void test_equals_hashCode_works() throws Exception {
+    PrfBasedKeyDerivationParameters params11 =
+        PrfBasedKeyDerivationParameters.builder()
+            .setPrfParameters(PRF_PARAMETERS_1)
+            .setDerivedKeyParameters(DERIVED_PARAMETERS_1)
+            .build();
+    PrfBasedKeyDerivationParameters params11Copy =
+        PrfBasedKeyDerivationParameters.builder()
+            .setPrfParameters(PRF_PARAMETERS_1_COPY)
+            .setDerivedKeyParameters(DERIVED_PARAMETERS_1_COPY)
+            .build();
+    PrfBasedKeyDerivationParameters params12 =
+        PrfBasedKeyDerivationParameters.builder()
+            .setPrfParameters(PRF_PARAMETERS_1)
+            .setDerivedKeyParameters(DERIVED_PARAMETERS_2)
+            .build();
+    PrfBasedKeyDerivationParameters params21 =
+        PrfBasedKeyDerivationParameters.builder()
+            .setPrfParameters(PRF_PARAMETERS_2)
+            .setDerivedKeyParameters(DERIVED_PARAMETERS_1)
+            .build();
+    PrfBasedKeyDerivationParameters params22 =
+        PrfBasedKeyDerivationParameters.builder()
+            .setPrfParameters(PRF_PARAMETERS_2)
+            .setDerivedKeyParameters(DERIVED_PARAMETERS_2)
+            .build();
+
+    assertThat(params11).isEqualTo(params11Copy);
+
+    assertThat(params11).isNotEqualTo(params12);
+    assertThat(params11).isNotEqualTo(params21);
+    assertThat(params11).isNotEqualTo(params22);
+
+    assertThat(params12).isNotEqualTo(params11);
+    assertThat(params12).isNotEqualTo(params21);
+    assertThat(params12).isNotEqualTo(params22);
+
+    assertThat(params21).isNotEqualTo(params11);
+    assertThat(params21).isNotEqualTo(params12);
+    assertThat(params21).isNotEqualTo(params22);
+
+    assertThat(params22).isNotEqualTo(params11);
+    assertThat(params22).isNotEqualTo(params12);
+    assertThat(params22).isNotEqualTo(params21);
+
+    assertThat(params11.hashCode()).isEqualTo(params11Copy.hashCode());
+    assertThat(params11.hashCode()).isNotEqualTo(params12.hashCode());
+    assertThat(params11.hashCode()).isNotEqualTo(params21.hashCode());
+    assertThat(params11.hashCode()).isNotEqualTo(params22.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/BUILD.bazel
new file mode 100644
index 0000000..8916142
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/BUILD.bazel
@@ -0,0 +1,54 @@
+licenses(["notice"])
+
+java_test(
+    name = "PrfBasedDeriverKeyManagerTest",
+    size = "small",
+    srcs = ["PrfBasedDeriverKeyManagerTest.java"],
+    deps = [
+        "//proto:aes_gcm_java_proto",
+        "//proto:common_java_proto",
+        "//proto:hkdf_prf_java_proto",
+        "//proto:prf_based_deriver_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
+        "//src/main/java/com/google/crypto/tink/config:tink_config",
+        "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/keyderivation:keyset_deriver",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PrfBasedDeriverTest",
+    size = "small",
+    srcs = ["PrfBasedDeriverTest.java"],
+    deps = [
+        "//proto:aes_gcm_java_proto",
+        "//proto:common_java_proto",
+        "//proto:hkdf_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "//src/main/java/com/google/crypto/tink/aead:aead_key_templates",
+        "//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
+        "//src/main/java/com/google/crypto/tink/keyderivation/internal:prf_based_deriver",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverKeyManagerTest.java
new file mode 100644
index 0000000..fac31de
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverKeyManagerTest.java
@@ -0,0 +1,261 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.aead.AesGcmKeyManager;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.keyderivation.KeysetDeriver;
+import com.google.crypto.tink.prf.HkdfPrfKeyManager;
+import com.google.crypto.tink.proto.AesGcmKey;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HkdfPrfKey;
+import com.google.crypto.tink.proto.HkdfPrfKeyFormat;
+import com.google.crypto.tink.proto.HkdfPrfParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.PrfBasedDeriverKey;
+import com.google.crypto.tink.proto.PrfBasedDeriverKeyFormat;
+import com.google.crypto.tink.proto.PrfBasedDeriverParams;
+import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.testing.TestUtil;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for PrfBasedDeriverKeyManager. */
+@RunWith(JUnit4.class)
+public class PrfBasedDeriverKeyManagerTest {
+  private final PrfBasedDeriverKeyManager manager = new PrfBasedDeriverKeyManager();
+  private final KeyTypeManager.KeyFactory<PrfBasedDeriverKeyFormat, PrfBasedDeriverKey> factory =
+      manager.keyFactory();
+
+  @BeforeClass
+  public static void setUp() throws GeneralSecurityException {
+    TinkConfig.register();
+  }
+
+  @Test
+  public void basics() throws Exception {
+    assertThat(manager.getKeyType())
+        .isEqualTo("type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey");
+    assertThat(manager.getVersion()).isEqualTo(0);
+    assertThat(manager.keyMaterialType()).isEqualTo(KeyMaterialType.SYMMETRIC);
+  }
+
+  @Test
+  public void validateKey_empty() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> manager.validateKey(PrfBasedDeriverKey.getDefaultInstance()));
+  }
+
+  @Test
+  public void validateKey_valid() throws Exception {
+    HkdfPrfKey prfKey =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(10)))
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .build();
+    PrfBasedDeriverKey key =
+        PrfBasedDeriverKey.newBuilder()
+            .setPrfKey(
+                TestUtil.createKeyData(
+                    prfKey, HkdfPrfKeyManager.staticKeyType(), KeyMaterialType.SYMMETRIC))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(AeadKeyTemplates.AES256_GCM))
+            .build();
+    manager.validateKey(key);
+  }
+
+  @Test
+  public void validateKey_wrongVersion_throws() throws Exception {
+    HkdfPrfKey prfKey =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(10)))
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .build();
+    PrfBasedDeriverKey key =
+        PrfBasedDeriverKey.newBuilder()
+            .setPrfKey(
+                TestUtil.createKeyData(
+                    prfKey, HkdfPrfKeyManager.staticKeyType(), KeyMaterialType.SYMMETRIC))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(AeadKeyTemplates.AES256_GCM))
+            .setVersion(1)
+            .build();
+    assertThrows(GeneralSecurityException.class, () -> manager.validateKey(key));
+  }
+
+  @Test
+  public void validateKeyFormat_empty() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> factory.validateKeyFormat(PrfBasedDeriverKeyFormat.getDefaultInstance()));
+  }
+
+  @Test
+  public void validateKeyFormat_valid() throws Exception {
+    HkdfPrfKeyFormat prfKeyFormat =
+        HkdfPrfKeyFormat.newBuilder()
+            .setKeySize(32)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .build();
+    PrfBasedDeriverKeyFormat keyFormat =
+        PrfBasedDeriverKeyFormat.newBuilder()
+            .setPrfKeyTemplate(
+                KeyTemplate.newBuilder()
+                    .setTypeUrl(HkdfPrfKeyManager.staticKeyType())
+                    .setValue(prfKeyFormat.toByteString()))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(AeadKeyTemplates.AES256_GCM))
+            .build();
+    factory.validateKeyFormat(keyFormat);
+  }
+
+  @Test
+  public void createKey_checkValues() throws Exception {
+    HkdfPrfKeyManager.register(true);
+    HkdfPrfKeyFormat prfKeyFormat =
+        HkdfPrfKeyFormat.newBuilder()
+            .setKeySize(32)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .build();
+    PrfBasedDeriverKeyFormat keyFormat =
+        PrfBasedDeriverKeyFormat.newBuilder()
+            .setPrfKeyTemplate(
+                KeyTemplate.newBuilder()
+                    .setTypeUrl(HkdfPrfKeyManager.staticKeyType())
+                    .setValue(prfKeyFormat.toByteString()))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(AeadKeyTemplates.AES256_GCM))
+            .build();
+    PrfBasedDeriverKey key = factory.createKey(keyFormat);
+
+    assertThat(key.getVersion()).isEqualTo(0);
+    assertThat(key.getPrfKey().getTypeUrl()).isEqualTo(HkdfPrfKeyManager.staticKeyType());
+    assertThat(key.getPrfKey().getKeyMaterialType()).isEqualTo(KeyMaterialType.SYMMETRIC);
+    HkdfPrfKey hkdfPrfKey =
+        HkdfPrfKey.parseFrom(key.getPrfKey().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(hkdfPrfKey.getKeyValue()).hasSize(32);
+    assertThat(key.getParams()).isEqualTo(keyFormat.getParams());
+  }
+
+  @Test
+  public void createKey_invalidPrfKey_throws() throws Exception {
+    HkdfPrfKeyManager.register(true);
+    HkdfPrfKeyFormat prfKeyFormat =
+        HkdfPrfKeyFormat.newBuilder()
+            .setKeySize(32)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.UNKNOWN_HASH))
+            .build();
+    PrfBasedDeriverKeyFormat keyFormat =
+        PrfBasedDeriverKeyFormat.newBuilder()
+            .setPrfKeyTemplate(
+                KeyTemplate.newBuilder()
+                    .setTypeUrl(HkdfPrfKeyManager.staticKeyType())
+                    .setValue(prfKeyFormat.toByteString()))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(AeadKeyTemplates.AES256_GCM))
+            .build();
+    assertThrows(GeneralSecurityException.class, () -> factory.createKey(keyFormat));
+  }
+
+  @Test
+  public void createKey_invalidDerivedKeyTemplate_throws() throws Exception {
+    HkdfPrfKeyManager.register(true);
+    HkdfPrfKeyFormat prfKeyFormat =
+        HkdfPrfKeyFormat.newBuilder()
+            .setKeySize(32)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .build();
+    PrfBasedDeriverKeyFormat keyFormat =
+        PrfBasedDeriverKeyFormat.newBuilder()
+            .setPrfKeyTemplate(
+                KeyTemplate.newBuilder()
+                    .setTypeUrl(HkdfPrfKeyManager.staticKeyType())
+                    .setValue(prfKeyFormat.toByteString()))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(
+                        KeyTemplate.newBuilder().setTypeUrl("non existent type url").build()))
+            .build();
+    assertThrows(GeneralSecurityException.class, () -> factory.createKey(keyFormat));
+  }
+
+  @Test
+  public void getPrimitive() throws Exception {
+    HkdfPrfKeyManager.register(true);
+    AesGcmKeyManager.register(true);
+
+    byte[] randomInput = Random.randBytes(20);
+
+    HkdfPrfKey prfKey =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(32)))
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .build();
+    PrfBasedDeriverKey key =
+        PrfBasedDeriverKey.newBuilder()
+            .setPrfKey(
+                TestUtil.createKeyData(
+                    prfKey, HkdfPrfKeyManager.staticKeyType(), KeyMaterialType.SYMMETRIC))
+            .setParams(
+                PrfBasedDeriverParams.newBuilder()
+                    .setDerivedKeyTemplate(AeadKeyTemplates.AES256_GCM))
+            .build();
+
+    KeysetHandle managerHandle =
+        manager.getPrimitive(key, KeysetDeriver.class).deriveKeyset(randomInput);
+    Keyset managerKeyset = CleartextKeysetHandle.getKeyset(managerHandle);
+    AesGcmKey managerKey =
+        AesGcmKey.parseFrom(
+            managerKeyset.getKey(0).getKeyData().getValue(),
+            ExtensionRegistryLite.getEmptyRegistry());
+
+    KeysetHandle directHandle =
+        PrfBasedDeriver.create(key.getPrfKey(), key.getParams().getDerivedKeyTemplate())
+            .deriveKeyset(randomInput);
+    Keyset directKeyset = CleartextKeysetHandle.getKeyset(directHandle);
+    AesGcmKey directKey =
+        AesGcmKey.parseFrom(
+            directKeyset.getKey(0).getKeyData().getValue(),
+            ExtensionRegistryLite.getEmptyRegistry());
+
+    assertThat(managerKey.getKeyValue()).hasSize(32);
+    assertThat(directKey.getKeyValue()).isEqualTo(managerKey.getKeyValue());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverTest.java b/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverTest.java
new file mode 100644
index 0000000..cb26277
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/keyderivation/internal/PrfBasedDeriverTest.java
@@ -0,0 +1,343 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.keyderivation.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.aead.AesGcmKeyManager;
+import com.google.crypto.tink.prf.HkdfPrfKeyManager;
+import com.google.crypto.tink.proto.AesGcmKey;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HkdfPrfKey;
+import com.google.crypto.tink.proto.HkdfPrfParams;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.testing.TestUtil;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for PrfBasedDeriver */
+@RunWith(JUnit4.class)
+public final class PrfBasedDeriverTest {
+  @BeforeClass
+  public static void addRequiredKeyManagers() throws Exception {
+    AesGcmKeyManager.register(true);
+    HkdfPrfKeyManager.register(true);
+  }
+
+  @Test
+  public void createDeriver_works() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(32)))
+            .build();
+    Object unused =
+        PrfBasedDeriver.create(
+            TestUtil.createKeyData(
+                key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+            AeadKeyTemplates.AES128_GCM);
+  }
+
+  @Test
+  public void createDeriver_invalidPrfKey_throws() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.UNKNOWN_HASH))
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(32)))
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            PrfBasedDeriver.create(
+                TestUtil.createKeyData(
+                    key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+                AeadKeyTemplates.AES256_GCM));
+  }
+
+  @Test
+  public void createDeriver_invalidDerivedKeyTemplate_throws() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(32)))
+            .build();
+    KeyTemplate keyTemplate = KeyTemplate.newBuilder().setTypeUrl("non existent type url").build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            PrfBasedDeriver.create(
+                TestUtil.createKeyData(
+                    key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+                keyTemplate));
+  }
+
+  /**
+   * TestVector from Rfc5869, test case 2: https://tools.ietf.org/html/rfc5869#appendix-A.2 We
+   * simply take the first 32 bytes of the test vector.
+   */
+  @Test
+  public void deriveKey_testVector() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(
+                HkdfPrfParams.newBuilder()
+                    .setSalt(
+                        ByteString.copyFrom(
+                            Hex.decode(
+                                "606162636465666768696a6b6c6d6e6f"
+                                    + "707172737475767778797a7b7c7d7e7f"
+                                    + "808182838485868788898a8b8c8d8e8f"
+                                    + "909192939495969798999a9b9c9d9e9f"
+                                    + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf")))
+                    .setHash(HashType.SHA256))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    Hex.decode(
+                        "000102030405060708090a0b0c0d0e0f"
+                            + "101112131415161718191a1b1c1d1e1f"
+                            + "202122232425262728292a2b2c2d2e2f"
+                            + "303132333435363738393a3b3c3d3e3f"
+                            + "404142434445464748494a4b4c4d4e4f")))
+            .build();
+    PrfBasedDeriver deriver =
+        PrfBasedDeriver.create(
+            TestUtil.createKeyData(
+                key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+            AeadKeyTemplates.AES256_GCM);
+
+    KeysetHandle handle =
+        deriver.deriveKeyset(
+            Hex.decode(
+                "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
+                    + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                    + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
+                    + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
+                    + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
+
+    Keyset keyset = CleartextKeysetHandle.getKeyset(handle);
+
+    assertThat(keyset.getKeyList()).hasSize(1);
+    assertThat(keyset.getKey(0).getKeyData().getTypeUrl()).isEqualTo(AeadConfig.AES_GCM_TYPE_URL);
+    assertThat(keyset.getKey(0).getKeyData().getKeyMaterialType())
+        .isEqualTo(KeyData.KeyMaterialType.SYMMETRIC);
+
+    AesGcmKey aesGcmKey =
+        AesGcmKey.parseFrom(
+            keyset.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(Hex.encode(aesGcmKey.getKeyValue().toByteArray()))
+        .isEqualTo("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c");
+  }
+
+  /** Tests the keyset values which should not be set (as we are only deriving KeyData). */
+  @Test
+  public void deriveKey_checkKeysetValues() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA256))
+            .setKeyValue(ByteString.copyFrom(Random.randBytes(32)))
+            .build();
+    KeysetHandle handle =
+        PrfBasedDeriver.create(
+                TestUtil.createKeyData(
+                    key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+                AeadKeyTemplates.AES128_GCM)
+            .deriveKeyset(Random.randBytes(10));
+    Keyset keyset = CleartextKeysetHandle.getKeyset(handle);
+
+    assertThat(keyset.getPrimaryKeyId()).isEqualTo(0);
+    assertThat(keyset.getKeyList()).hasSize(1);
+    assertThat(keyset.getKey(0).getStatus()).isEqualTo(KeyStatusType.UNKNOWN_STATUS);
+    assertThat(keyset.getKey(0).getKeyId()).isEqualTo(0);
+    assertThat(keyset.getKey(0).getOutputPrefixType()).isEqualTo(OutputPrefixType.UNKNOWN_PREFIX);
+  }
+
+  private static PrfBasedDeriver hkdfSha512DeriverForAes128Gcm(ByteString keyValue)
+      throws Exception {
+    return PrfBasedDeriver.create(
+        TestUtil.createKeyData(
+            HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA512))
+                .setKeyValue(keyValue)
+                .build(),
+            HkdfPrfKeyManager.staticKeyType(),
+            KeyData.KeyMaterialType.SYMMETRIC),
+        AeadKeyTemplates.AES128_GCM);
+  }
+
+  // Check that keys with zero appended will create different derivated keys.
+  @Test
+  public void createDeriver_compareToKeyWithZeroAppended() throws Exception {
+    // Create a key with a zero in the end.
+    byte[] keyValue = Random.randBytes(33);
+    keyValue[32] = 0;
+    PrfBasedDeriver deriver1 = hkdfSha512DeriverForAes128Gcm(ByteString.copyFrom(keyValue, 0, 32));
+    PrfBasedDeriver deriver2 = hkdfSha512DeriverForAes128Gcm(ByteString.copyFrom(keyValue, 0, 33));
+    Keyset keyset1 =
+        CleartextKeysetHandle.getKeyset(deriver1.deriveKeyset("some salt".getBytes(UTF_8)));
+    Keyset keyset2 =
+        CleartextKeysetHandle.getKeyset(deriver2.deriveKeyset("some salt".getBytes(UTF_8)));
+    AesGcmKey aesGcmKey1 =
+        AesGcmKey.parseFrom(
+            keyset1.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    AesGcmKey aesGcmKey2 =
+        AesGcmKey.parseFrom(
+            keyset2.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(aesGcmKey1.getKeyValue()).hasSize(16);
+    assertThat(aesGcmKey2.getKeyValue()).hasSize(16);
+    assertThat(aesGcmKey1.getKeyValue()).isNotEqualTo(aesGcmKey2.getKeyValue());
+  }
+
+  // Check that salt with zero appended will create different derivated keys.
+  // The main reason for this test is to assure that HKDF is used properly.
+  // HKDF makes an (implicit?) assumption that the salt has constant size or
+  // at least is not chosen by a third party. There are salt values that are
+  // equivalent to each other. E.g. appending a zero byte to a salt < 64 bytes
+  // does not change the output. Or a salt > 64 bytes and its hash digest are
+  // equivalent. Hence an argument that is potentially coming from a third party
+  // should be passed to HKDF as parmeter info.
+  @Test
+  public void createDeriver_compareToSaltWithZeroAppended() throws Exception {
+    // Create a key with a zero in the end.
+    byte[] keyValue = Random.randBytes(32);
+    PrfBasedDeriver deriver = hkdfSha512DeriverForAes128Gcm(ByteString.copyFrom(keyValue));
+    Keyset keyset1 = CleartextKeysetHandle.getKeyset(deriver.deriveKeyset(new byte[20]));
+    Keyset keyset2 = CleartextKeysetHandle.getKeyset(deriver.deriveKeyset(new byte[21]));
+    AesGcmKey aesGcmKey1 =
+        AesGcmKey.parseFrom(
+            keyset1.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    AesGcmKey aesGcmKey2 =
+        AesGcmKey.parseFrom(
+            keyset2.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(aesGcmKey1.getKeyValue()).hasSize(16);
+    assertThat(aesGcmKey2.getKeyValue()).hasSize(16);
+    assertThat(aesGcmKey1.getKeyValue()).isNotEqualTo(aesGcmKey2.getKeyValue());
+  }
+
+  // Test Vector for a key ending in 0, genereted in this implementation.
+  @Test
+  public void createDeriver_zeroEndingKeyTestVector() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA512))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    Hex.decode(
+                        "a1a2a3a4a5a6a7a8a9aaabacadaeafb1b2b3b4b5b6b7b8b9babbbcbdbebf"
+                            + "c1c2c3c4c5c6c7c8c9cacbcccdcecfc1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                            + "00")))
+            .build();
+    PrfBasedDeriver deriver =
+        PrfBasedDeriver.create(
+            TestUtil.createKeyData(
+                key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+            AeadKeyTemplates.AES128_GCM);
+
+    KeysetHandle handle = deriver.deriveKeyset(Hex.decode("1122334455"));
+
+    Keyset keyset = CleartextKeysetHandle.getKeyset(handle);
+
+    AesGcmKey aesGcmKey =
+        AesGcmKey.parseFrom(
+            keyset.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(Hex.encode(aesGcmKey.getKeyValue().toByteArray()))
+        .isEqualTo("31c449af66b669b9963ef2df30dfe5f9");
+  }
+
+  // Test Vector for a key generated with input/salt ending in 0, genereted in this implementation.
+  @Test
+  public void createDeriver_zeroEndingSaltTestVector() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA512))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    Hex.decode(
+                        "c1c2c3c4c5c6c7c8c9cacbcccdcecfc1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                            + "a1a2a3a4a5a6a7a8a9aaabacadaeafb1b2b3b4b5b6b7b8b9babbbcbdbebf")))
+            .build();
+    PrfBasedDeriver deriver =
+        PrfBasedDeriver.create(
+            TestUtil.createKeyData(
+                key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+            AeadKeyTemplates.AES128_GCM);
+
+    KeysetHandle handle = deriver.deriveKeyset(Hex.decode("00"));
+
+    Keyset keyset = CleartextKeysetHandle.getKeyset(handle);
+
+    AesGcmKey aesGcmKey =
+        AesGcmKey.parseFrom(
+            keyset.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(Hex.encode(aesGcmKey.getKeyValue().toByteArray()))
+        .isEqualTo("887af0808c1855eba1594bf540adb957");
+  }
+
+  // Test Vector for a key generated with empty salt.
+  @Test
+  public void createDeriver_emptySaltTestVector() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.newBuilder()
+            .setVersion(0)
+            .setParams(HkdfPrfParams.newBuilder().setHash(HashType.SHA512))
+            .setKeyValue(
+                ByteString.copyFrom(
+                    Hex.decode(
+                        "c1c2c3c4c5c6c7c8c9cacbcccdcecfc1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                            + "a1a2a3a4a5a6a7a8a9aaabacadaeafb1b2b3b4b5b6b7b8b9babbbcbdbebf")))
+            .build();
+    PrfBasedDeriver deriver =
+        PrfBasedDeriver.create(
+            TestUtil.createKeyData(
+                key, HkdfPrfKeyManager.staticKeyType(), KeyData.KeyMaterialType.SYMMETRIC),
+            AeadKeyTemplates.AES128_GCM);
+
+    KeysetHandle handle = deriver.deriveKeyset(new byte[0]);
+
+    Keyset keyset = CleartextKeysetHandle.getKeyset(handle);
+
+    AesGcmKey aesGcmKey =
+        AesGcmKey.parseFrom(
+            keyset.getKey(0).getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(Hex.encode(aesGcmKey.getKeyValue().toByteArray()))
+        .isEqualTo("fb2b448c2595caf75129e282af758bf1");
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyManagerTest.java
index 850fff1..8c2ca3c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyManagerTest.java
@@ -30,8 +30,8 @@
 import com.google.crypto.tink.subtle.PrfMac;
 import com.google.crypto.tink.subtle.Random;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -43,6 +43,11 @@
   private final KeyTypeManager.KeyFactory<AesCmacKeyFormat, AesCmacKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    MacConfig.register();
+  }
+
   @Test
   public void validateKeyFormat_empty() throws Exception {
     assertThrows(
@@ -201,25 +206,25 @@
   @Test
   public void testAes256CmacTemplate() throws Exception {
     KeyTemplate template = AesCmacKeyManager.aes256CmacTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    AesCmacKeyFormat format =
-        AesCmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getTagSize()).isEqualTo(16);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(AesCmacParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawAes256CmacTemplate() throws Exception {
     KeyTemplate template = AesCmacKeyManager.rawAes256CmacTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesCmacKeyFormat format =
-        AesCmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getTagSize()).isEqualTo(16);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setVariant(AesCmacParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyTest.java
index 6dbadaf..f2dd7db 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacKeyTest.java
@@ -19,7 +19,10 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
 import com.google.crypto.tink.util.SecretBytes;
 import java.security.GeneralSecurityException;
 import org.junit.BeforeClass;
@@ -34,105 +37,380 @@
   private static final AesCmacParameters.Variant LEGACY = AesCmacParameters.Variant.LEGACY;
   private static final AesCmacParameters.Variant CRUNCHY = AesCmacParameters.Variant.CRUNCHY;
 
-  private static AesCmacParameters tinkParameters;
-  private static AesCmacParameters legacyParameters;
-  private static AesCmacParameters crunchyParameters;
-  private static AesCmacParameters noPrefixParameters;
+  private static AesCmacParameters tinkParameters16;
+  private static AesCmacParameters legacyParameters16;
+  private static AesCmacParameters crunchyParameters16;
+  private static AesCmacParameters noPrefixParameters16;
+  private static AesCmacParameters tinkParameters32;
+  private static AesCmacParameters legacyParameters32;
+  private static AesCmacParameters crunchyParameters32;
+  private static AesCmacParameters noPrefixParameters32;
 
   @BeforeClass
   public static void setUpParameters() throws Exception {
-    tinkParameters = AesCmacParameters.createForKeysetWithCryptographicTagSize(10, TINK);
-    legacyParameters = AesCmacParameters.createForKeysetWithCryptographicTagSize(10, LEGACY);
-    crunchyParameters = AesCmacParameters.createForKeysetWithCryptographicTagSize(10, CRUNCHY);
-    noPrefixParameters = AesCmacParameters.createForKeysetWithCryptographicTagSize(10, NO_PREFIX);
+    tinkParameters16 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setVariant(TINK)
+            .build();
+    legacyParameters16 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setVariant(LEGACY)
+            .build();
+    crunchyParameters16 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setVariant(CRUNCHY)
+            .build();
+    noPrefixParameters16 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setVariant(NO_PREFIX)
+            .build();
+
+    tinkParameters32 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setVariant(TINK)
+            .build();
+    legacyParameters32 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setVariant(LEGACY)
+            .build();
+    crunchyParameters32 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setVariant(CRUNCHY)
+            .build();
+    noPrefixParameters32 =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setVariant(NO_PREFIX)
+            .build();
   }
 
   @Test
-  public void create_works() throws Exception {
-    AesCmacKey.create(noPrefixParameters, SecretBytes.randomBytes(32));
-    AesCmacKey.createForKeyset(tinkParameters, SecretBytes.randomBytes(32), 1907);
+  public void build_incompleteBuildsFail() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesCmacKey.builder().build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesCmacKey.builder().setAesKeyBytes(SecretBytes.randomBytes(32)).build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesCmacKey.builder().setParameters(tinkParameters16).build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCmacKey.builder()
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .setParameters(tinkParameters32)
+                .build());
+  }
+
+  @Test
+  public void build_works() throws Exception {
+    Object unused =
+        AesCmacKey.builder()
+            .setParameters(noPrefixParameters16)
+            .setAesKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    unused =
+        AesCmacKey.builder()
+            .setParameters(tinkParameters32)
+            .setAesKeyBytes(SecretBytes.randomBytes(32))
+            .setIdRequirement(1907)
+            .build();
+  }
+
+  @Test
+  public void build_failsOnKeySizeMismatch() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters32)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCmacKey.builder()
+                .setParameters(tinkParameters32)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(199045)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .setIdRequirement(199045)
+                .build());
   }
 
   @Test
   public void getAesKey() throws Exception {
     SecretBytes aesKey = SecretBytes.randomBytes(32);
-    assertThat(AesCmacKey.create(noPrefixParameters, aesKey).getAesKey()).isEqualTo(aesKey);
+    assertThat(
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters32)
+                .setAesKeyBytes(aesKey)
+                .build()
+                .getAesKey())
+        .isEqualTo(aesKey);
   }
 
   @Test
   public void getParameters() throws Exception {
-    assertThat(AesCmacKey.create(noPrefixParameters, SecretBytes.randomBytes(32)).getParameters())
-        .isEqualTo(noPrefixParameters);
     assertThat(
-            AesCmacKey.createForKeyset(tinkParameters, SecretBytes.randomBytes(32), 1907)
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters32)
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .build()
                 .getParameters())
-        .isEqualTo(tinkParameters);
+        .isEqualTo(noPrefixParameters32);
+    assertThat(
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(1907)
+                .build()
+                .getParameters())
+        .isEqualTo(tinkParameters16);
   }
 
   @Test
   public void getIdRequirement() throws Exception {
     assertThat(
-            AesCmacKey.create(noPrefixParameters, SecretBytes.randomBytes(32))
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .build()
                 .getIdRequirementOrNull())
         .isNull();
     assertThat(
-            AesCmacKey.createForKeyset(tinkParameters, SecretBytes.randomBytes(32), 1907)
+            AesCmacKey.builder()
+                .setParameters(tinkParameters32)
+                .setAesKeyBytes(SecretBytes.randomBytes(32))
+                .setIdRequirement(1907)
+                .build()
                 .getIdRequirementOrNull())
         .isEqualTo(1907);
   }
 
   @Test
+  public void getOutputPrefix() throws Exception {
+    assertThat(
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .build()
+                .getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(0x66AABBCC)
+                .build()
+                .getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(
+            AesCmacKey.builder()
+                .setParameters(legacyParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(0x66AABBCC)
+                .build()
+                .getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(
+            AesCmacKey.builder()
+                .setParameters(crunchyParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(0x66AABBCC)
+                .build()
+                .getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+  }
+
+  @Test
   public void invalidCreations() throws Exception {
-    // Wrong keylength
-    assertThrows(
-        GeneralSecurityException.class,
-        () -> AesCmacKey.create(noPrefixParameters, SecretBytes.randomBytes(16)));
-    assertThrows(
-        GeneralSecurityException.class,
-        () -> AesCmacKey.createForKeyset(tinkParameters, SecretBytes.randomBytes(16), 199045));
-    // Must use createForKeyset if we have id requirement
-    assertThrows(
-        GeneralSecurityException.class,
-        () -> AesCmacKey.create(tinkParameters, SecretBytes.randomBytes(32)));
     // Must give ID with IDRequirement
     assertThrows(
         GeneralSecurityException.class,
-        () -> AesCmacKey.createForKeyset(tinkParameters, SecretBytes.randomBytes(32), null));
+        () ->
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(null)
+                .build());
     // Must not give ID without IDRequirement
     assertThrows(
         GeneralSecurityException.class,
-        () -> AesCmacKey.createForKeyset(noPrefixParameters, SecretBytes.randomBytes(32), 123));
+        () ->
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(SecretBytes.randomBytes(16))
+                .setIdRequirement(123)
+                .build());
   }
 
   @Test
   public void testEqualities() throws Exception {
     SecretBytes key1 = SecretBytes.randomBytes(32);
+    SecretBytes key1Copy =
+        SecretBytes.copyFrom(
+            key1.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
     SecretBytes key2 = SecretBytes.randomBytes(32);
+    SecretBytes key3 = SecretBytes.randomBytes(16);
+    SecretBytes key4 = SecretBytes.randomBytes(16);
     new KeyTester()
         .addEqualityGroup(
             "No prefix, key1",
-            AesCmacKey.create(noPrefixParameters, key1),
-            AesCmacKey.createForKeyset(noPrefixParameters, key1, /* idRequirement=*/ null))
+            AesCmacKey.builder().setParameters(noPrefixParameters32).setAesKeyBytes(key1).build(),
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(null)
+                .build())
         .addEqualityGroup(
             "No prefix, key2",
-            AesCmacKey.create(noPrefixParameters, key2),
-            AesCmacKey.createForKeyset(noPrefixParameters, key2, /* idRequirement=*/ null))
+            AesCmacKey.builder().setParameters(noPrefixParameters32).setAesKeyBytes(key2).build(),
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters32)
+                .setAesKeyBytes(key2)
+                .setIdRequirement(null)
+                .build())
+        .addEqualityGroup(
+            "No prefix, key3",
+            AesCmacKey.builder().setParameters(noPrefixParameters16).setAesKeyBytes(key3).build(),
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(null)
+                .build())
+        .addEqualityGroup(
+            "No prefix, key4",
+            AesCmacKey.builder().setParameters(noPrefixParameters16).setAesKeyBytes(key4).build(),
+            AesCmacKey.builder()
+                .setParameters(noPrefixParameters16)
+                .setAesKeyBytes(key4)
+                .setIdRequirement(null)
+                .build())
         .addEqualityGroup(
             "Tink 1907 key1",
-            AesCmacKey.createForKeyset(tinkParameters, key1, 1907),
-            AesCmacKey.createForKeyset(tinkParameters, key1, 1907))
+            AesCmacKey.builder()
+                .setParameters(tinkParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1907)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(tinkParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1907)
+                .build())
         .addEqualityGroup(
             "Tink 1908 key1",
-            AesCmacKey.createForKeyset(tinkParameters, key1, 1908),
-            AesCmacKey.createForKeyset(tinkParameters, key1, 1908))
+            AesCmacKey.builder()
+                .setParameters(tinkParameters32)
+                .setAesKeyBytes(key1Copy)
+                .setIdRequirement(1908)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(tinkParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1908)
+                .build())
+        .addEqualityGroup(
+            "Tink 1907 key3",
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1907)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Tink 1908 key3",
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1908)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(tinkParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1908)
+                .build())
         .addEqualityGroup(
             "Legacy 1907 key1",
-            AesCmacKey.createForKeyset(legacyParameters, key1, 1907),
-            AesCmacKey.createForKeyset(legacyParameters, key1, 1907))
+            AesCmacKey.builder()
+                .setParameters(legacyParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1907)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(legacyParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1907)
+                .build())
         .addEqualityGroup(
             "Crunchy 1907 key1",
-            AesCmacKey.createForKeyset(crunchyParameters, key1, 1907),
-            AesCmacKey.createForKeyset(crunchyParameters, key1, 1907))
+            AesCmacKey.builder()
+                .setParameters(crunchyParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1907)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(crunchyParameters32)
+                .setAesKeyBytes(key1)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Legacy 1907 key3",
+            AesCmacKey.builder()
+                .setParameters(legacyParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1907)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(legacyParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Crunchy 1907 key3",
+            AesCmacKey.builder()
+                .setParameters(crunchyParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1907)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(crunchyParameters16)
+                .setAesKeyBytes(key3)
+                .setIdRequirement(1907)
+                .build())
         .doTests();
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacParametersTest.java
index 0ec4d83..0320f8d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacParametersTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacParametersTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.security.GeneralSecurityException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -31,14 +32,53 @@
   private static final AesCmacParameters.Variant LEGACY = AesCmacParameters.Variant.LEGACY;
   private static final AesCmacParameters.Variant CRUNCHY = AesCmacParameters.Variant.CRUNCHY;
 
-  private static AesCmacParameters generalCreate(int tagSize, AesCmacParameters.Variant variant)
+  @CanIgnoreReturnValue
+  private static AesCmacParameters create(
+      int keySizeBytes, int tagSizeBytes, AesCmacParameters.Variant variant)
       throws GeneralSecurityException {
-    return AesCmacParameters.createForKeysetWithCryptographicTagSize(tagSize, variant);
+    return AesCmacParameters.builder()
+        .setKeySizeBytes(keySizeBytes)
+        .setTagSizeBytes(tagSizeBytes)
+        .setVariant(variant)
+        .build();
+  }
+
+  @CanIgnoreReturnValue
+  private static AesCmacParameters create(int keySizeBytes, int tagSizeBytes)
+      throws GeneralSecurityException {
+    return AesCmacParameters.builder()
+        .setKeySizeBytes(keySizeBytes)
+        .setTagSizeBytes(tagSizeBytes)
+        .build();
+  }
+
+  @Test
+  public void testAesCmacParameters_incompleteBuildsFail() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.builder().build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesCmacParameters.builder().setTagSizeBytes(10).build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesCmacParameters.builder().setKeySizeBytes(16).build());
+  }
+
+  @Test
+  public void testAesCmacParameters_buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            AesCmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(16)
+                .setVariant(null)
+                .build());
   }
 
   @Test
   public void testAesCmacParameters_basic() throws Exception {
-    AesCmacParameters parameters = AesCmacParameters.create(16);
+    AesCmacParameters parameters = create(16, 16);
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
     assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(16);
     assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(16);
     assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
@@ -47,146 +87,183 @@
 
   @Test
   public void testAesCmacParameters_variant() throws Exception {
-    assertThat(generalCreate(16, NO_PREFIX).getVariant()).isEqualTo(NO_PREFIX);
-    assertThat(generalCreate(16, TINK).getVariant()).isEqualTo(TINK);
-    assertThat(generalCreate(16, LEGACY).getVariant()).isEqualTo(LEGACY);
-    assertThat(generalCreate(16, CRUNCHY).getVariant()).isEqualTo(CRUNCHY);
+    assertThat(create(16, 16, NO_PREFIX).getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(create(16, 16, TINK).getVariant()).isEqualTo(TINK);
+    assertThat(create(16, 16, LEGACY).getVariant()).isEqualTo(LEGACY);
+    assertThat(create(16, 16, CRUNCHY).getVariant()).isEqualTo(CRUNCHY);
   }
 
   @Test
   public void testAesCmacParameters_hasIdRequirement() throws Exception {
-    assertThat(generalCreate(16, NO_PREFIX).hasIdRequirement()).isFalse();
-    assertThat(generalCreate(16, TINK).hasIdRequirement()).isTrue();
-    assertThat(generalCreate(16, LEGACY).hasIdRequirement()).isTrue();
-    assertThat(generalCreate(16, CRUNCHY).hasIdRequirement()).isTrue();
+    assertThat(create(32, 16, NO_PREFIX).hasIdRequirement()).isFalse();
+    assertThat(create(32, 16, TINK).hasIdRequirement()).isTrue();
+    assertThat(create(32, 16, LEGACY).hasIdRequirement()).isTrue();
+    assertThat(create(32, 16, CRUNCHY).hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void testAesCmacParameters_createWithDifferentKeySizes() throws Exception {
+    create(16, 13);
+    create(32, 13);
+
+    assertThrows(GeneralSecurityException.class, () -> create(-2, 13));
+    assertThrows(GeneralSecurityException.class, () -> create(5, 13));
+    assertThrows(GeneralSecurityException.class, () -> create(13, 13));
+    assertThrows(GeneralSecurityException.class, () -> create(20, 13));
+    assertThrows(GeneralSecurityException.class, () -> create(24, 13));
+    assertThrows(GeneralSecurityException.class, () -> create(42, 13));
+  }
+
+  @Test
+  public void testAesCmacParameters_createForKeysetWithDifferentKeySizes() throws Exception {
+    create(16, 13, LEGACY);
+    create(32, 13, CRUNCHY);
+
+    assertThrows(GeneralSecurityException.class, () -> create(-2, 13, LEGACY));
+    assertThrows(GeneralSecurityException.class, () -> create(5, 13, CRUNCHY));
+    assertThrows(GeneralSecurityException.class, () -> create(13, 13, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(20, 13, LEGACY));
+    assertThrows(GeneralSecurityException.class, () -> create(24, 13, CRUNCHY));
+    assertThrows(GeneralSecurityException.class, () -> create(42, 13, TINK));
+  }
+
+  @Test
+  public void testAesCmacParameters_getKeySizeBytes() throws Exception {
+    assertThat(create(16, 16).getKeySizeBytes()).isEqualTo(16);
+    assertThat(create(32, 16).getKeySizeBytes()).isEqualTo(32);
   }
 
   @Test
   public void testAesCmacParameters_tagSizesConstruction() throws Exception {
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(5));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(6));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(7));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(8));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(9));
-    AesCmacParameters.create(10);
-    AesCmacParameters.create(11);
-    AesCmacParameters.create(12);
-    AesCmacParameters.create(13);
-    AesCmacParameters.create(14);
-    AesCmacParameters.create(15);
-    AesCmacParameters.create(16);
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(17));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(18));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(19));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(20));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(21));
-    assertThrows(GeneralSecurityException.class, () -> AesCmacParameters.create(32));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 5));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 6));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 7));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 8));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 9));
+    create(16, 10);
+    create(32, 11);
+    create(16, 12);
+    create(32, 13);
+    create(16, 14);
+    create(32, 15);
+    create(16, 16);
+    assertThrows(GeneralSecurityException.class, () -> create(16, 17));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 18));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 19));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 20));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 21));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 32));
   }
 
   @Test
   public void testAesCmacParameters_tagSizesConstruction2() throws Exception {
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(5, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(6, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(7, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(8, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(9, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(9, CRUNCHY));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(9, LEGACY));
-    generalCreate(10, TINK);
-    generalCreate(10, CRUNCHY);
-    generalCreate(10, LEGACY);
-    generalCreate(11, TINK);
-    generalCreate(12, TINK);
-    generalCreate(13, TINK);
-    generalCreate(14, TINK);
-    generalCreate(15, TINK);
-    generalCreate(16, TINK);
-    generalCreate(16, CRUNCHY);
-    generalCreate(16, LEGACY);
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(17, CRUNCHY));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(17, LEGACY));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(17, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(18, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(21, TINK));
-    assertThrows(GeneralSecurityException.class, () -> generalCreate(32, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 5, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 6, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 7, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 8, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 9, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 9, CRUNCHY));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 9, LEGACY));
+    create(16, 10, TINK);
+    create(32, 10, CRUNCHY);
+    create(16, 10, LEGACY);
+    create(32, 11, TINK);
+    create(16, 12, TINK);
+    create(32, 13, TINK);
+    create(16, 14, TINK);
+    create(32, 15, TINK);
+    create(16, 16, TINK);
+    create(32, 16, CRUNCHY);
+    create(16, 16, LEGACY);
+    assertThrows(GeneralSecurityException.class, () -> create(32, 17, CRUNCHY));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 17, LEGACY));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 17, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 18, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(32, 21, TINK));
+    assertThrows(GeneralSecurityException.class, () -> create(16, 32, TINK));
   }
 
   @Test
   public void testAesCmacParameters_getTotalTagSizeBytes() throws Exception {
-    assertThat(AesCmacParameters.create(10).getTotalTagSizeBytes()).isEqualTo(10);
-    assertThat(AesCmacParameters.create(11).getTotalTagSizeBytes()).isEqualTo(11);
-    assertThat(AesCmacParameters.create(12).getTotalTagSizeBytes()).isEqualTo(12);
-    assertThat(AesCmacParameters.create(13).getTotalTagSizeBytes()).isEqualTo(13);
-    assertThat(AesCmacParameters.create(14).getTotalTagSizeBytes()).isEqualTo(14);
-    assertThat(AesCmacParameters.create(15).getTotalTagSizeBytes()).isEqualTo(15);
-    assertThat(AesCmacParameters.create(16).getTotalTagSizeBytes()).isEqualTo(16);
-    assertThat(generalCreate(10, TINK).getTotalTagSizeBytes()).isEqualTo(15);
-    assertThat(generalCreate(10, CRUNCHY).getTotalTagSizeBytes()).isEqualTo(15);
-    assertThat(generalCreate(10, LEGACY).getTotalTagSizeBytes()).isEqualTo(15);
-    assertThat(generalCreate(13, TINK).getTotalTagSizeBytes()).isEqualTo(18);
-    assertThat(generalCreate(13, CRUNCHY).getTotalTagSizeBytes()).isEqualTo(18);
-    assertThat(generalCreate(13, LEGACY).getTotalTagSizeBytes()).isEqualTo(18);
-    assertThat(generalCreate(16, TINK).getTotalTagSizeBytes()).isEqualTo(21);
-    assertThat(generalCreate(16, CRUNCHY).getTotalTagSizeBytes()).isEqualTo(21);
-    assertThat(generalCreate(16, LEGACY).getTotalTagSizeBytes()).isEqualTo(21);
+    assertThat(create(16, 10).getTotalTagSizeBytes()).isEqualTo(10);
+    assertThat(create(16, 11).getTotalTagSizeBytes()).isEqualTo(11);
+    assertThat(create(16, 12).getTotalTagSizeBytes()).isEqualTo(12);
+    assertThat(create(16, 13).getTotalTagSizeBytes()).isEqualTo(13);
+    assertThat(create(16, 14).getTotalTagSizeBytes()).isEqualTo(14);
+    assertThat(create(16, 15).getTotalTagSizeBytes()).isEqualTo(15);
+    assertThat(create(16, 16).getTotalTagSizeBytes()).isEqualTo(16);
+    assertThat(create(32, 10, TINK).getTotalTagSizeBytes()).isEqualTo(15);
+    assertThat(create(32, 10, CRUNCHY).getTotalTagSizeBytes()).isEqualTo(15);
+    assertThat(create(32, 10, LEGACY).getTotalTagSizeBytes()).isEqualTo(15);
+    assertThat(create(32, 13, TINK).getTotalTagSizeBytes()).isEqualTo(18);
+    assertThat(create(32, 13, CRUNCHY).getTotalTagSizeBytes()).isEqualTo(18);
+    assertThat(create(32, 13, LEGACY).getTotalTagSizeBytes()).isEqualTo(18);
+    assertThat(create(32, 16, TINK).getTotalTagSizeBytes()).isEqualTo(21);
+    assertThat(create(32, 16, CRUNCHY).getTotalTagSizeBytes()).isEqualTo(21);
+    assertThat(create(32, 16, LEGACY).getTotalTagSizeBytes()).isEqualTo(21);
   }
 
   @Test
   public void testAesCmacParameters_getCryptographicTagSizeBytes() throws Exception {
-    assertThat(AesCmacParameters.create(10).getCryptographicTagSizeBytes()).isEqualTo(10);
-    assertThat(AesCmacParameters.create(11).getCryptographicTagSizeBytes()).isEqualTo(11);
-    assertThat(AesCmacParameters.create(12).getCryptographicTagSizeBytes()).isEqualTo(12);
-    assertThat(AesCmacParameters.create(13).getCryptographicTagSizeBytes()).isEqualTo(13);
-    assertThat(AesCmacParameters.create(14).getCryptographicTagSizeBytes()).isEqualTo(14);
-    assertThat(AesCmacParameters.create(15).getCryptographicTagSizeBytes()).isEqualTo(15);
-    assertThat(AesCmacParameters.create(16).getCryptographicTagSizeBytes()).isEqualTo(16);
-    assertThat(generalCreate(10, TINK).getCryptographicTagSizeBytes()).isEqualTo(10);
-    assertThat(generalCreate(10, CRUNCHY).getCryptographicTagSizeBytes()).isEqualTo(10);
-    assertThat(generalCreate(10, LEGACY).getCryptographicTagSizeBytes()).isEqualTo(10);
-    assertThat(generalCreate(13, TINK).getCryptographicTagSizeBytes()).isEqualTo(13);
-    assertThat(generalCreate(13, CRUNCHY).getCryptographicTagSizeBytes()).isEqualTo(13);
-    assertThat(generalCreate(13, LEGACY).getCryptographicTagSizeBytes()).isEqualTo(13);
-    assertThat(generalCreate(16, TINK).getCryptographicTagSizeBytes()).isEqualTo(16);
-    assertThat(generalCreate(16, CRUNCHY).getCryptographicTagSizeBytes()).isEqualTo(16);
-    assertThat(generalCreate(16, LEGACY).getCryptographicTagSizeBytes()).isEqualTo(16);
+    assertThat(create(16, 10).getCryptographicTagSizeBytes()).isEqualTo(10);
+    assertThat(create(16, 11).getCryptographicTagSizeBytes()).isEqualTo(11);
+    assertThat(create(16, 12).getCryptographicTagSizeBytes()).isEqualTo(12);
+    assertThat(create(16, 13).getCryptographicTagSizeBytes()).isEqualTo(13);
+    assertThat(create(16, 14).getCryptographicTagSizeBytes()).isEqualTo(14);
+    assertThat(create(16, 15).getCryptographicTagSizeBytes()).isEqualTo(15);
+    assertThat(create(16, 16).getCryptographicTagSizeBytes()).isEqualTo(16);
+    assertThat(create(32, 10, TINK).getCryptographicTagSizeBytes()).isEqualTo(10);
+    assertThat(create(32, 10, CRUNCHY).getCryptographicTagSizeBytes()).isEqualTo(10);
+    assertThat(create(32, 10, LEGACY).getCryptographicTagSizeBytes()).isEqualTo(10);
+    assertThat(create(32, 13, TINK).getCryptographicTagSizeBytes()).isEqualTo(13);
+    assertThat(create(32, 13, CRUNCHY).getCryptographicTagSizeBytes()).isEqualTo(13);
+    assertThat(create(32, 13, LEGACY).getCryptographicTagSizeBytes()).isEqualTo(13);
+    assertThat(create(32, 16, TINK).getCryptographicTagSizeBytes()).isEqualTo(16);
+    assertThat(create(32, 16, CRUNCHY).getCryptographicTagSizeBytes()).isEqualTo(16);
+    assertThat(create(32, 16, LEGACY).getCryptographicTagSizeBytes()).isEqualTo(16);
   }
 
   @Test
   public void testAesCmacParameters_equal() throws Exception {
-    assertThat(AesCmacParameters.create(10)).isEqualTo(generalCreate(10, NO_PREFIX));
-    assertThat(AesCmacParameters.create(11)).isEqualTo(generalCreate(11, NO_PREFIX));
-    assertThat(AesCmacParameters.create(12)).isEqualTo(generalCreate(12, NO_PREFIX));
-    assertThat(AesCmacParameters.create(13)).isEqualTo(generalCreate(13, NO_PREFIX));
-    assertThat(AesCmacParameters.create(13)).isEqualTo(generalCreate(13, NO_PREFIX));
-    assertThat(generalCreate(16, TINK)).isEqualTo(generalCreate(16, TINK));
-    assertThat(generalCreate(16, LEGACY)).isEqualTo(generalCreate(16, LEGACY));
-    assertThat(generalCreate(16, CRUNCHY)).isEqualTo(generalCreate(16, CRUNCHY));
+    assertThat(create(16, 10)).isEqualTo(create(16, 10, NO_PREFIX));
+    assertThat(create(32, 11)).isEqualTo(create(32, 11, NO_PREFIX));
+    assertThat(create(16, 12)).isEqualTo(create(16, 12, NO_PREFIX));
+    assertThat(create(32, 13)).isEqualTo(create(32, 13, NO_PREFIX));
+    assertThat(create(16, 13)).isEqualTo(create(16, 13, NO_PREFIX));
+    assertThat(create(16, 16, TINK)).isEqualTo(create(16, 16, TINK));
+    assertThat(create(32, 16, LEGACY)).isEqualTo(create(32, 16, LEGACY));
+    assertThat(create(16, 16, CRUNCHY)).isEqualTo(create(16, 16, CRUNCHY));
   }
 
   @Test
   public void testAesCmacParameters_notEqual() throws Exception {
-    assertThat(generalCreate(10, NO_PREFIX)).isNotEqualTo(generalCreate(11, NO_PREFIX));
-    assertThat(generalCreate(10, NO_PREFIX)).isNotEqualTo(generalCreate(10, TINK));
-    assertThat(generalCreate(10, TINK)).isNotEqualTo(generalCreate(10, LEGACY));
-    assertThat(generalCreate(10, LEGACY)).isNotEqualTo(generalCreate(10, CRUNCHY));
+    assertThat(create(32, 10, NO_PREFIX)).isNotEqualTo(create(16, 10, NO_PREFIX));
+    assertThat(create(16, 10, NO_PREFIX)).isNotEqualTo(create(16, 11, NO_PREFIX));
+    assertThat(create(32, 10, NO_PREFIX)).isNotEqualTo(create(32, 10, TINK));
+    assertThat(create(16, 10, TINK)).isNotEqualTo(create(16, 10, LEGACY));
+    assertThat(create(32, 10, LEGACY)).isNotEqualTo(create(32, 10, CRUNCHY));
   }
 
   @Test
   public void testAesCmacParameters_equalHashes() throws Exception {
-    assertThat(AesCmacParameters.create(10).hashCode())
-        .isEqualTo(generalCreate(10, NO_PREFIX).hashCode());
-    assertThat(AesCmacParameters.create(11).hashCode())
-        .isEqualTo(generalCreate(11, NO_PREFIX).hashCode());
-    assertThat(AesCmacParameters.create(12).hashCode())
-        .isEqualTo(generalCreate(12, NO_PREFIX).hashCode());
-    assertThat(AesCmacParameters.create(13).hashCode())
-        .isEqualTo(generalCreate(13, NO_PREFIX).hashCode());
-    assertThat(AesCmacParameters.create(13).hashCode())
-        .isEqualTo(generalCreate(13, NO_PREFIX).hashCode());
-    assertThat(generalCreate(16, TINK).hashCode()).isEqualTo(generalCreate(16, TINK).hashCode());
-    assertThat(generalCreate(16, LEGACY).hashCode())
-        .isEqualTo(generalCreate(16, LEGACY).hashCode());
-    assertThat(generalCreate(16, CRUNCHY).hashCode())
-        .isEqualTo(generalCreate(16, CRUNCHY).hashCode());
+    assertThat(create(16, 10).hashCode()).isEqualTo(create(16, 10, NO_PREFIX).hashCode());
+    assertThat(create(32, 11).hashCode()).isEqualTo(create(32, 11, NO_PREFIX).hashCode());
+    assertThat(create(16, 12).hashCode()).isEqualTo(create(16, 12, NO_PREFIX).hashCode());
+    assertThat(create(32, 13).hashCode()).isEqualTo(create(32, 13, NO_PREFIX).hashCode());
+    assertThat(create(16, 13).hashCode()).isEqualTo(create(16, 13, NO_PREFIX).hashCode());
+    assertThat(create(16, 16, TINK).hashCode()).isEqualTo(create(16, 16, TINK).hashCode());
+    assertThat(create(32, 16, LEGACY).hashCode()).isEqualTo(create(32, 16, LEGACY).hashCode());
+    assertThat(create(16, 16, CRUNCHY).hashCode()).isEqualTo(create(16, 16, CRUNCHY).hashCode());
+  }
+
+  @Test
+  public void testAesCmacParameters_notEqualHashes() throws Exception {
+    assertThat(create(32, 10, NO_PREFIX).hashCode())
+        .isNotEqualTo(create(16, 10, NO_PREFIX).hashCode());
+    assertThat(create(16, 10, NO_PREFIX).hashCode())
+        .isNotEqualTo(create(16, 11, NO_PREFIX).hashCode());
+    assertThat(create(32, 10, NO_PREFIX).hashCode()).isNotEqualTo(create(32, 10, TINK).hashCode());
+    assertThat(create(16, 10, TINK).hashCode()).isNotEqualTo(create(16, 10, LEGACY).hashCode());
+    assertThat(create(32, 10, LEGACY).hashCode()).isNotEqualTo(create(32, 10, CRUNCHY).hashCode());
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacProtoSerializationTest.java
index 6264c17..7a5f38d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacProtoSerializationTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/AesCmacProtoSerializationTest.java
@@ -49,9 +49,12 @@
 public final class AesCmacProtoSerializationTest {
   private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesCmacKey";
 
-  private static final SecretBytes AES_KEY = SecretBytes.randomBytes(32);
-  private static final ByteString AES_KEY_AS_BYTE_STRING =
-      ByteString.copyFrom(AES_KEY.toByteArray(InsecureSecretKeyAccess.get()));
+  private static final SecretBytes AES_KEY_32 = SecretBytes.randomBytes(32);
+  private static final SecretBytes AES_KEY_16 = SecretBytes.randomBytes(16);
+  private static final ByteString AES_KEY_AS_BYTE_STRING_32 =
+      ByteString.copyFrom(AES_KEY_32.toByteArray(InsecureSecretKeyAccess.get()));
+  private static final ByteString AES_KEY_AS_BYTE_STRING_16 =
+      ByteString.copyFrom(AES_KEY_16.toByteArray(InsecureSecretKeyAccess.get()));
 
   private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
 
@@ -60,27 +63,33 @@
     AesCmacProtoSerialization.register(registry);
   }
 
-  static AesCmacParameters createAesCmacParameters(int tagSize, AesCmacParameters.Variant variant) {
+  static AesCmacParameters createAesCmacParameters(
+      int keySize, int tagSize, AesCmacParameters.Variant variant) {
     try {
-      return AesCmacParameters.createForKeysetWithCryptographicTagSize(tagSize, variant);
+      return AesCmacParameters.builder().setKeySizeBytes(keySize).setTagSizeBytes(tagSize).setVariant(
+          variant).build();
     } catch (GeneralSecurityException e) {
       throw new RuntimeException(e);
     }
   }
 
   static AesCmacKey createKey(
+      int keySize,
       int tagSize,
       AesCmacParameters.Variant variant,
       SecretBytes aesKey,
       @Nullable Integer idRequirement)
       throws GeneralSecurityException {
-    return AesCmacKey.createForKeyset(
-        createAesCmacParameters(tagSize, variant), aesKey, idRequirement);
+    return AesCmacKey.builder()
+        .setParameters(createAesCmacParameters(keySize, tagSize, variant))
+        .setAesKeyBytes(aesKey)
+        .setIdRequirement(idRequirement)
+        .build();
   }
 
-  static com.google.crypto.tink.proto.AesCmacKeyFormat createProtoFormat(int tagSize) {
+  static com.google.crypto.tink.proto.AesCmacKeyFormat createProtoFormat(int keySize, int tagSize) {
     return com.google.crypto.tink.proto.AesCmacKeyFormat.newBuilder()
-        .setKeySize(32)
+        .setKeySize(keySize)
         .setParams(AesCmacParams.newBuilder().setTagSize(tagSize))
         .build();
   }
@@ -97,61 +106,137 @@
   public static final ParametersWithSerialization[] VALID_PARAMETERS =
       new ParametersWithSerialization[] {
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 16, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 16, /*tagSize=*/ 16, AesCmacParameters.Variant.TINK),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(16))),
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 16, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 16, AesCmacParameters.Variant.CRUNCHY),
+            createAesCmacParameters(
+                /*keySize=*/ 16, /*tagSize=*/ 16, AesCmacParameters.Variant.CRUNCHY),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.CRUNCHY, createProtoFormat(16))),
+                TYPE_URL,
+                OutputPrefixType.CRUNCHY,
+                createProtoFormat(/*keySize=*/ 16, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 16, AesCmacParameters.Variant.LEGACY),
+            createAesCmacParameters(
+                /*keySize=*/ 16, /*tagSize=*/ 16, AesCmacParameters.Variant.LEGACY),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.LEGACY, createProtoFormat(16))),
+                TYPE_URL,
+                OutputPrefixType.LEGACY,
+                createProtoFormat(/*keySize=*/ 16, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 16, AesCmacParameters.Variant.NO_PREFIX),
+            createAesCmacParameters(
+                /*keySize=*/ 16, /*tagSize=*/ 16, AesCmacParameters.Variant.NO_PREFIX),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.RAW, createProtoFormat(16))),
+                TYPE_URL,
+                OutputPrefixType.RAW,
+                createProtoFormat(/*keySize=*/ 16, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 10, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 16, AesCmacParameters.Variant.TINK),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(10))),
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 11, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 16, AesCmacParameters.Variant.CRUNCHY),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(11))),
+                TYPE_URL,
+                OutputPrefixType.CRUNCHY,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 12, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 16, AesCmacParameters.Variant.LEGACY),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(12))),
+                TYPE_URL,
+                OutputPrefixType.LEGACY,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 13, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 16, AesCmacParameters.Variant.NO_PREFIX),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(13))),
+                TYPE_URL,
+                OutputPrefixType.RAW,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 16))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 14, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 10, AesCmacParameters.Variant.TINK),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(14))),
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 10))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 15, AesCmacParameters.Variant.TINK),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 11, AesCmacParameters.Variant.TINK),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.TINK, createProtoFormat(15))),
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 11))),
         new ParametersWithSerialization(
-            createAesCmacParameters(/*tagSize=*/ 11, AesCmacParameters.Variant.NO_PREFIX),
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 12, AesCmacParameters.Variant.TINK),
             ProtoParametersSerialization.create(
-                TYPE_URL, OutputPrefixType.RAW, createProtoFormat(11))),
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 12))),
+        new ParametersWithSerialization(
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 13, AesCmacParameters.Variant.TINK),
+            ProtoParametersSerialization.create(
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 13))),
+        new ParametersWithSerialization(
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 14, AesCmacParameters.Variant.TINK),
+            ProtoParametersSerialization.create(
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 14))),
+        new ParametersWithSerialization(
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 15, AesCmacParameters.Variant.TINK),
+            ProtoParametersSerialization.create(
+                TYPE_URL,
+                OutputPrefixType.TINK,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 15))),
+        new ParametersWithSerialization(
+            createAesCmacParameters(
+                /*keySize=*/ 32, /*tagSize=*/ 11, AesCmacParameters.Variant.NO_PREFIX),
+            ProtoParametersSerialization.create(
+                TYPE_URL,
+                OutputPrefixType.RAW,
+                createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 11))),
       };
 
   @DataPoints("invalidParameters")
   public static final ProtoParametersSerialization[] INVALID_PARAMETERS =
       new ProtoParametersSerialization[] {
-        ProtoParametersSerialization.create(TYPE_URL, OutputPrefixType.RAW, createProtoFormat(9)),
-        ProtoParametersSerialization.create(TYPE_URL, OutputPrefixType.RAW, createProtoFormat(7)),
-        ProtoParametersSerialization.create(TYPE_URL, OutputPrefixType.RAW, createProtoFormat(17)),
-        ProtoParametersSerialization.create(TYPE_URL, OutputPrefixType.RAW, createProtoFormat(19)),
-        ProtoParametersSerialization.create(TYPE_URL, OutputPrefixType.RAW, createProtoFormat(32)),
         ProtoParametersSerialization.create(
-            TYPE_URL, OutputPrefixType.UNKNOWN_PREFIX, createProtoFormat(16)),
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 9)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 7)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 16, /*tagSize=*/ 17)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 16, /*tagSize=*/ 19)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 32)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 1, /*tagSize=*/ 10)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ -1, /*tagSize=*/ 10)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 20, /*tagSize=*/ 10)),
+        ProtoParametersSerialization.create(
+            TYPE_URL, OutputPrefixType.RAW, createProtoFormat(/*keySize=*/ 390, /*tagSize=*/ 10)),
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            createProtoFormat(/*keySize=*/ 32, /*tagSize=*/ 16)),
         // Proto messages start with a VarInt, which always ends with a byte with most
         // significant bit unset. 0x80 is hence invalid.
         ProtoParametersSerialization.create(
@@ -165,6 +250,7 @@
   @Theory
   public void testSerializeParameters(
       @FromDataPoints("validParameters") ParametersWithSerialization pair) throws Exception {
+
     ProtoParametersSerialization serializedParameters =
         registry.serializeParameters(pair.getParameters(), ProtoParametersSerialization.class);
 
@@ -194,34 +280,105 @@
       return new KeyWithSerialization[] {
         new KeyWithSerialization(
             createKey(
-                /*tagSize=*/ 16, AesCmacParameters.Variant.TINK, AES_KEY, /*idRequirement=*/ 1479),
+                /*keySize=*/ 32,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.TINK,
+                AES_KEY_32,
+                /*idRequirement=*/ 1479),
             ProtoKeySerialization.create(
                 TYPE_URL,
-                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING).toByteString(),
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_32).toByteString(),
                 KeyMaterialType.SYMMETRIC,
                 OutputPrefixType.TINK,
                 /*idRequirement=*/ 1479)),
         new KeyWithSerialization(
-            createKey(16, AesCmacParameters.Variant.CRUNCHY, AES_KEY, 1479),
+            createKey(
+                /*keySize=*/ 32,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.CRUNCHY,
+                AES_KEY_32,
+                /*idRequirement=*/ 1479),
             ProtoKeySerialization.create(
                 TYPE_URL,
-                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING).toByteString(),
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_32).toByteString(),
                 KeyMaterialType.SYMMETRIC,
                 OutputPrefixType.CRUNCHY,
                 /*idRequirement=*/ 1479)),
         new KeyWithSerialization(
-            createKey(16, AesCmacParameters.Variant.LEGACY, AES_KEY, 1479),
+            createKey(
+                /*keySize=*/ 32,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.LEGACY,
+                AES_KEY_32,
+                /*idRequirement=*/ 1479),
             ProtoKeySerialization.create(
                 TYPE_URL,
-                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING).toByteString(),
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_32).toByteString(),
                 KeyMaterialType.SYMMETRIC,
                 OutputPrefixType.LEGACY,
-                1479)),
+                /*idRequirement=*/ 1479)),
         new KeyWithSerialization(
-            createKey(16, AesCmacParameters.Variant.NO_PREFIX, AES_KEY, null),
+            createKey(
+                /*keySize=*/ 32,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.NO_PREFIX,
+                AES_KEY_32,
+                /*idRequirement=*/ null),
             ProtoKeySerialization.create(
                 TYPE_URL,
-                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING).toByteString(),
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_32).toByteString(),
+                KeyMaterialType.SYMMETRIC,
+                OutputPrefixType.RAW,
+                /*idRequirement=*/ null)),
+        new KeyWithSerialization(
+            createKey(
+                /*keySize=*/ 16,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.TINK,
+                AES_KEY_16,
+                /*idRequirement=*/ 1479),
+            ProtoKeySerialization.create(
+                TYPE_URL,
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_16).toByteString(),
+                KeyMaterialType.SYMMETRIC,
+                OutputPrefixType.TINK,
+                /*idRequirement=*/ 1479)),
+        new KeyWithSerialization(
+            createKey(
+                /*keySize=*/ 16,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.CRUNCHY,
+                AES_KEY_16,
+                /*idRequirement=*/ 1479),
+            ProtoKeySerialization.create(
+                TYPE_URL,
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_16).toByteString(),
+                KeyMaterialType.SYMMETRIC,
+                OutputPrefixType.CRUNCHY,
+                /*idRequirement=*/ 1479)),
+        new KeyWithSerialization(
+            createKey(
+                /*keySize=*/ 16,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.LEGACY,
+                AES_KEY_16,
+                /*idRequirement=*/ 1479),
+            ProtoKeySerialization.create(
+                TYPE_URL,
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_16).toByteString(),
+                KeyMaterialType.SYMMETRIC,
+                OutputPrefixType.LEGACY,
+                /*idRequirement=*/ 1479)),
+        new KeyWithSerialization(
+            createKey(
+                /*keySize=*/ 16,
+                /*tagSize=*/ 16,
+                AesCmacParameters.Variant.NO_PREFIX,
+                AES_KEY_16,
+                /*idRequirement=*/ null),
+            ProtoKeySerialization.create(
+                TYPE_URL,
+                createProtoKey(/*tagSize=*/ 16, AES_KEY_AS_BYTE_STRING_16).toByteString(),
                 KeyMaterialType.SYMMETRIC,
                 OutputPrefixType.RAW,
                 /*idRequirement=*/ null)),
@@ -237,7 +394,7 @@
         // Bad Version Number (1)
         ProtoKeySerialization.create(
             TYPE_URL,
-            createProtoKey(16, AES_KEY_AS_BYTE_STRING).toBuilder()
+            createProtoKey(16, AES_KEY_AS_BYTE_STRING_32).toBuilder()
                 .setVersion(1)
                 .build()
                 .toByteString(),
@@ -247,28 +404,21 @@
         // Unknown prefix
         ProtoKeySerialization.create(
             TYPE_URL,
-            createProtoKey(16, AES_KEY_AS_BYTE_STRING).toByteString(),
+            createProtoKey(16, AES_KEY_AS_BYTE_STRING_16).toByteString(),
             KeyMaterialType.SYMMETRIC,
             OutputPrefixType.UNKNOWN_PREFIX,
             1479),
         // Bad Tag Length (9)
         ProtoKeySerialization.create(
             TYPE_URL,
-            createProtoKey(9, AES_KEY_AS_BYTE_STRING).toByteString(),
+            createProtoKey(9, AES_KEY_AS_BYTE_STRING_32).toByteString(),
             KeyMaterialType.SYMMETRIC,
             OutputPrefixType.TINK,
             1479),
         // Bad Tag Length (17)
         ProtoKeySerialization.create(
             TYPE_URL,
-            createProtoKey(17, AES_KEY_AS_BYTE_STRING).toByteString(),
-            KeyMaterialType.SYMMETRIC,
-            OutputPrefixType.TINK,
-            1479),
-        // Bad Key Length (16)
-        ProtoKeySerialization.create(
-            TYPE_URL,
-            createProtoKey(16, ByteString.copyFrom(new byte[16])).toByteString(),
+            createProtoKey(17, AES_KEY_AS_BYTE_STRING_16).toByteString(),
             KeyMaterialType.SYMMETRIC,
             OutputPrefixType.TINK,
             1479),
@@ -299,7 +449,7 @@
         // under test.
         ProtoKeySerialization.create(
             "WrongTypeUrl",
-            createProtoKey(16, AES_KEY_AS_BYTE_STRING).toByteString(),
+            createProtoKey(16, AES_KEY_AS_BYTE_STRING_32).toByteString(),
             KeyMaterialType.SYMMETRIC,
             OutputPrefixType.TINK,
             1479),
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/mac/BUILD.bazel
index 22133b6..3ee09ea 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/BUILD.bazel
@@ -1,19 +1,24 @@
 licenses(["notice"])
 
 java_test(
-    name = "MacIntegrationTest",
+    name = "MacTest",
     size = "small",
-    srcs = ["MacIntegrationTest.java"],
+    srcs = ["MacTest.java"],
     deps = [
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:crypto_format",
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
         "//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -70,9 +75,14 @@
         "//proto:common_java_proto",
         "//proto:hmac_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
         "//src/main/java/com/google/crypto/tink/mac:mac_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/mac:mac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -88,12 +98,14 @@
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/mac:hmac_key_manager",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -105,28 +117,25 @@
     srcs = ["MacWrapperTest.java"],
     deps = [
         "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:crypto_format",
-        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink:primitive_set",
-        "//src/main/java/com/google/crypto/tink:secret_key_access",
-        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
-        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
-        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
-        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/internal/testing:fake_monitoring_client",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_proto_serialization",
         "//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "//src/main/java/com/google/crypto/tink/mac:mac_key",
-        "//src/main/java/com/google/crypto/tink/mac:mac_parameters",
         "//src/main/java/com/google/crypto/tink/mac:mac_wrapper",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
-        "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "//src/main/java/com/google/crypto/tink/util:bytes",
-        "@com_google_protobuf//:protobuf_javalite",
-        "@maven//:com_google_code_findbugs_jsr305",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -142,11 +151,13 @@
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key_manager",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -158,6 +169,18 @@
     srcs = ["AesCmacParametersTest.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HmacParametersTest",
+    size = "small",
+    srcs = ["HmacParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -168,9 +191,12 @@
     size = "small",
     srcs = ["AesCmacKeyTest.java"],
     deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink/internal:key_tester",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -197,8 +223,114 @@
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
         "//src/main/java/com/google/crypto/tink/mac:aes_cmac_proto_serialization",
         "//src/main/java/com/google/crypto/tink/util:secret_bytes",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HmacKeyTest",
+    size = "small",
+    srcs = ["HmacKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HmacProtoSerializationTest",
+    size = "small",
+    srcs = ["HmacProtoSerializationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "ChunkedMacWrapperTest",
+    size = "small",
+    srcs = ["ChunkedMacWrapperTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_wrapper",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "ChunkedMacTest",
+    size = "small",
+    srcs = ["ChunkedMacTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_wrapper",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PredefinedMacParametersTest",
+    size = "small",
+    srcs = ["PredefinedMacParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/mac:mac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/ChunkedMacTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/ChunkedMacTest.java
new file mode 100644
index 0000000..440d4c8
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/ChunkedMacTest.java
@@ -0,0 +1,188 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.mac.HmacParameters.HashType;
+import com.google.crypto.tink.util.SecretBytes;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * These tests ensure interoperability between the new ChunkedMac implementations and the old Mac
+ * implementations.
+ */
+@RunWith(Theories.class)
+public class ChunkedMacTest {
+  private static final int HMAC_KEY_SIZE = 20;
+  private static final int HMAC_TAG_SIZE = 10;
+  private static final int AES_CMAC_KEY_SIZE = 32;
+  private static final int AES_CMAC_TAG_SIZE = 10;
+
+  @DataPoints("keys")
+  public static Key[] keys;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+    AesCmacProtoSerialization.register();
+    HmacProtoSerialization.register();
+    ChunkedMacWrapper.register();
+    createTestKeys();
+  }
+
+  private static void createTestKeys() {
+    HmacParameters noPrefixHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.NO_PREFIX);
+    HmacParameters legacyHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.LEGACY);
+    HmacParameters crunchyHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.CRUNCHY);
+    HmacParameters tinkHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.TINK);
+    AesCmacParameters noPrefixAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.NO_PREFIX);
+    AesCmacParameters legacyAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.LEGACY);
+    AesCmacParameters crunchyAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.CRUNCHY);
+    AesCmacParameters tinkAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.TINK);
+
+    try {
+      keys =
+          new Key[] {
+            HmacKey.builder()
+                .setParameters(noPrefixHmacParameters)
+                .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+                .setIdRequirement(null)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(noPrefixAesCmacParameters)
+                .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+                .setIdRequirement(null)
+                .build(),
+            HmacKey.builder()
+                .setParameters(tinkHmacParameters)
+                .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+                .setIdRequirement(4)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(tinkAesCmacParameters)
+                .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+                .setIdRequirement(5)
+                .build(),
+            HmacKey.builder()
+                .setParameters(crunchyHmacParameters)
+                .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+                .setIdRequirement(6)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(crunchyAesCmacParameters)
+                .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+                .setIdRequirement(7)
+                .build(),
+            HmacKey.builder()
+                .setParameters(legacyHmacParameters)
+                .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+                .setIdRequirement(8)
+                .build(),
+            AesCmacKey.builder()
+                .setParameters(legacyAesCmacParameters)
+                .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+                .setIdRequirement(9)
+                .build(),
+          };
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static AesCmacParameters createDefaultAesCmacParameters(Variant variant) {
+    try {
+      return AesCmacParameters.builder()
+          .setKeySizeBytes(AES_CMAC_KEY_SIZE)
+          .setTagSizeBytes(AES_CMAC_TAG_SIZE)
+          .setVariant(variant)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static HmacParameters createDefaultHmacParameters(HmacParameters.Variant variant) {
+    try {
+      return HmacParameters.builder()
+          .setKeySizeBytes(HMAC_KEY_SIZE)
+          .setTagSizeBytes(HMAC_TAG_SIZE)
+          .setVariant(variant)
+          .setHashType(HashType.SHA256)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException("Incorrect parameters creation arguments", e);
+    }
+  }
+
+  @Theory
+  public void computeWithMacVerifyWithChunkedMac_works(@FromDataPoints("keys") Key key)
+      throws GeneralSecurityException {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle.Builder.Entry entry = KeysetHandle.importKey(key);
+    if (key.getIdRequirementOrNull() == null) {
+      entry.withFixedId(1234);
+    }
+    KeysetHandle keysetHandle = KeysetHandle.newBuilder().addEntry(entry.makePrimary()).build();
+
+    Mac mac = keysetHandle.getPrimitive(Mac.class);
+    byte[] tag = mac.computeMac(plaintext);
+    ChunkedMac chunkedMac = keysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification chunkedMacVerification = chunkedMac.createVerification(tag);
+    chunkedMacVerification.update(ByteBuffer.wrap(plaintext));
+
+    chunkedMacVerification.verifyMac();
+  }
+
+  @Theory
+  public void computeWithChunkedMacVerifyWithMac_works(@FromDataPoints("keys") Key key)
+      throws GeneralSecurityException {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle.Builder.Entry entry = KeysetHandle.importKey(key);
+    if (key.getIdRequirementOrNull() == null) {
+      entry.withFixedId(1234);
+    }
+    KeysetHandle keysetHandle = KeysetHandle.newBuilder().addEntry(entry.makePrimary()).build();
+
+    ChunkedMac chunkedMac = keysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation chunkedMacComputation = chunkedMac.createComputation();
+    chunkedMacComputation.update(ByteBuffer.wrap(plaintext));
+    byte[] tag = chunkedMacComputation.computeMac();
+    Mac mac = keysetHandle.getPrimitive(Mac.class);
+
+    mac.verifyMac(tag, plaintext);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/ChunkedMacWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/ChunkedMacWrapperTest.java
new file mode 100644
index 0000000..a95161d
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/ChunkedMacWrapperTest.java
@@ -0,0 +1,439 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.mac.HmacParameters.HashType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.SecretBytes;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ChunkedMacWrapperTest {
+  private static final int HMAC_KEY_SIZE = 20;
+  private static final int HMAC_TAG_SIZE = 10;
+  private static final int AES_CMAC_KEY_SIZE = 32;
+  private static final int AES_CMAC_TAG_SIZE = 10;
+
+  private static HmacKey rawKey0;
+  private static HmacKey rawKey1;
+  private static AesCmacKey rawKey2;
+  private static AesCmacKey rawKey3;
+  private static HmacKey tinkKey0;
+  private static AesCmacKey tinkKey1;
+  private static HmacKey crunchyKey0;
+  private static AesCmacKey crunchyKey1;
+  private static HmacKey legacyKey0;
+  private static AesCmacKey legacyKey1;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+    AesCmacProtoSerialization.register();
+    HmacProtoSerialization.register();
+    ChunkedMacWrapper.register();
+    createTestKeys();
+  }
+
+  private static void createTestKeys() {
+    final HmacParameters noPrefixHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.NO_PREFIX);
+    final HmacParameters legacyHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.LEGACY);
+    final HmacParameters crunchyHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.CRUNCHY);
+    final HmacParameters tinkHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.TINK);
+    final AesCmacParameters noPrefixAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.NO_PREFIX);
+    final AesCmacParameters legacyAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.LEGACY);
+    final AesCmacParameters crunchyAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.CRUNCHY);
+    final AesCmacParameters tinkAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.TINK);
+
+    try {
+      rawKey0 =
+          HmacKey.builder()
+              .setParameters(noPrefixHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      rawKey1 =
+          HmacKey.builder()
+              .setParameters(noPrefixHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      rawKey2 =
+          AesCmacKey.builder()
+              .setParameters(noPrefixAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      rawKey3 =
+          AesCmacKey.builder()
+              .setParameters(noPrefixAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      tinkKey0 =
+          HmacKey.builder()
+              .setParameters(tinkHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(4)
+              .build();
+      tinkKey1 =
+          AesCmacKey.builder()
+              .setParameters(tinkAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(5)
+              .build();
+      crunchyKey0 =
+          HmacKey.builder()
+              .setParameters(crunchyHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(6)
+              .build();
+      crunchyKey1 =
+          AesCmacKey.builder()
+              .setParameters(crunchyAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(7)
+              .build();
+      legacyKey0 =
+          HmacKey.builder()
+              .setParameters(legacyHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(8)
+              .build();
+      legacyKey1 =
+          AesCmacKey.builder()
+              .setParameters(legacyAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(9)
+              .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static AesCmacParameters createDefaultAesCmacParameters(Variant variant) {
+    try {
+      return AesCmacParameters.builder()
+          .setKeySizeBytes(AES_CMAC_KEY_SIZE)
+          .setTagSizeBytes(AES_CMAC_TAG_SIZE)
+          .setVariant(variant)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static HmacParameters createDefaultHmacParameters(HmacParameters.Variant variant) {
+    try {
+      return HmacParameters.builder()
+          .setKeySizeBytes(HMAC_KEY_SIZE)
+          .setTagSizeBytes(HMAC_TAG_SIZE)
+          .setVariant(variant)
+          .setHashType(HashType.SHA1)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException("Incorrect parameters creation arguments", e);
+    }
+  }
+
+  @Test
+  public void testComputeVerifyMac_works() throws Exception {
+    ByteBuffer plaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle smallKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234).makePrimary())
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .build();
+
+    ChunkedMac mac = smallKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(plaintext);
+    byte[] tag = macComputation.computeMac();
+
+    plaintext.rewind();
+    ChunkedMacVerification macVerification = mac.createVerification(tag);
+    macVerification.update(plaintext);
+
+    macVerification.verifyMac();
+  }
+
+  @Test
+  public void testComputeVerifyMac_throwsOnWrongKey() throws Exception {
+    ByteBuffer plaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle computeKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234).makePrimary())
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235).makePrimary())
+            .build();
+
+    ChunkedMac mac0 = computeKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac0.createComputation();
+    macComputation.update(plaintext);
+    byte[] tag = macComputation.computeMac();
+
+    plaintext.rewind();
+    ChunkedMac mac1 = verifyKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac1.createVerification(tag);
+    macVerification.update(plaintext);
+
+    assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+  }
+
+  @Test
+  public void testComputeVerifyMac_handlesByteBufferCorrectly() throws Exception {
+    ByteBuffer verificationPlaintext =
+        (ByteBuffer) ByteBuffer.wrap("plaintext".getBytes(UTF_8)).position(3).limit(6);
+    ByteBuffer computationPlaintext = ByteBuffer.wrap("int".getBytes(UTF_8));
+
+    KeysetHandle computeKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237).makePrimary())
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235).makePrimary())
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .build();
+
+    ChunkedMac mac = computeKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(computationPlaintext);
+    byte[] tag = macComputation.computeMac();
+
+    ChunkedMac mac1 = verifyKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac1.createVerification(tag);
+    macVerification.update(verificationPlaintext);
+
+    macVerification.verifyMac();
+    assertThat(computationPlaintext.position()).isEqualTo(computationPlaintext.limit());
+    assertThat(verificationPlaintext.position()).isEqualTo(verificationPlaintext.limit());
+  }
+
+  @Test
+  public void testVerifyMac_handlesByteBufferCorrectlyWhenNoKeyMatches() throws Exception {
+    ByteBuffer verificationPlaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(tinkKey0).makePrimary())
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .build();
+    byte[] tag = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+    ChunkedMac mac = verifyKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac.createVerification(tag);
+    macVerification.update(verificationPlaintext);
+
+    assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+    assertThat(verificationPlaintext.position()).isEqualTo(verificationPlaintext.limit());
+  }
+
+  @Test
+  public void testVerifyMac_checksAllNecessaryRawKeys() throws Exception {
+    ByteBuffer plaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle computeKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237).makePrimary())
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235).makePrimary())
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .build();
+
+    ChunkedMac mac = computeKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(plaintext);
+    byte[] tag = macComputation.computeMac();
+
+    plaintext.rewind();
+    ChunkedMac mac1 = verifyKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac1.createVerification(tag);
+    macVerification.update(plaintext);
+
+    macVerification.verifyMac();
+  }
+
+  @Test
+  public void testVerifyMac_suppressedExceptionsGetPropagated() throws Exception {
+    byte[] fakeTag =
+        ByteBuffer.allocate(AES_CMAC_TAG_SIZE)
+            .put(tinkKey1.getOutputPrefix().toByteArray())
+            .put(new byte[] {0, 0, 0, 0, 0})
+            .array();
+
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey1).makePrimary())
+            .build();
+
+    ChunkedMac mac = verifyKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac.createVerification(fakeTag);
+    macVerification.update(ByteBuffer.wrap("plaintext".getBytes(UTF_8)));
+
+    GeneralSecurityException e =
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+    assertThat(e.getSuppressed()).hasLength(3);
+  }
+
+  @Test
+  public void testVerifyMac_checksRawKeysWhenTagHasTinkKeyPrefix() throws Exception {
+    // Plaintext: "plaintext".getBytes(UTF_8)
+    byte[] tag = Hex.decode("0152af9740d2fab0cf3f");
+    // 0x52af9740, which equals 1387239232.
+    int id = ByteBuffer.wrap(tag, 1, 4).getInt();
+    HmacKey rawKey5 =
+        HmacKey.builder()
+            .setParameters(createDefaultHmacParameters(HmacParameters.Variant.NO_PREFIX))
+            .setKeyBytes(
+                SecretBytes.copyFrom(
+                    Hex.decode("7d40a4d7c192ca113f403b8703e1b7b93fecf99a"),
+                    InsecureSecretKeyAccess.get()))
+            .setIdRequirement(null)
+            .build();
+    HmacKey tinkKey2 =
+        HmacKey.builder()
+            .setParameters(createDefaultHmacParameters(HmacParameters.Variant.TINK))
+            .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+            .setIdRequirement(id)
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey5).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey2).makePrimary())
+            .build();
+
+    ChunkedMac mac = verifyKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac.createVerification(tag);
+    macVerification.update(ByteBuffer.wrap("plaintext".getBytes(UTF_8)));
+
+    macVerification.verifyMac();
+  }
+
+  @Test
+  public void testComputeMac_usesPrimaryKey() throws Exception {
+    ByteBuffer plaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236).makePrimary())
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .build();
+    KeysetHandle keysetHandlePrimary =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236).makePrimary())
+            .build();
+
+    ChunkedMac mac = keysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(plaintext);
+    byte[] tag = macComputation.computeMac();
+
+    plaintext.rewind();
+    ChunkedMac primaryMac = keysetHandlePrimary.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification primaryMacVerification = primaryMac.createVerification(tag);
+    primaryMacVerification.update(plaintext);
+
+    primaryMacVerification.verifyMac();
+  }
+
+  @Test
+  public void testComputeVerifyMac_manyKeysWork() throws Exception {
+    ByteBuffer plaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle assortedKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234))
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey0))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .addEntry(KeysetHandle.importKey(crunchyKey0))
+            .addEntry(KeysetHandle.importKey(crunchyKey1).makePrimary())
+            .addEntry(KeysetHandle.importKey(legacyKey0))
+            .addEntry(KeysetHandle.importKey(legacyKey1))
+            .build();
+
+    ChunkedMac mac = assortedKeysetHandle.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(plaintext);
+    byte[] tag = macComputation.computeMac();
+
+    plaintext.rewind();
+    ChunkedMacVerification macVerification = mac.createVerification(tag);
+    macVerification.update(plaintext);
+
+    macVerification.verifyMac();
+  }
+
+  @Test
+  public void testVerifyMac_shiftedPrimaryWithManyKeysWorks() throws Exception {
+    ByteBuffer plaintext = ByteBuffer.wrap("plaintext".getBytes(UTF_8));
+    KeysetHandle assortedKeysetHandle0 =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234))
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey0))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .addEntry(KeysetHandle.importKey(crunchyKey0))
+            .addEntry(KeysetHandle.importKey(crunchyKey1).makePrimary())
+            .addEntry(KeysetHandle.importKey(legacyKey0))
+            .addEntry(KeysetHandle.importKey(legacyKey1))
+            .build();
+    KeysetHandle.Builder assortedBuilder = KeysetHandle.newBuilder(assortedKeysetHandle0);
+    assortedBuilder.getAt(4).makePrimary();
+    KeysetHandle assortedKeysetHandle1 = assortedBuilder.build();
+
+    ChunkedMac mac0 = assortedKeysetHandle0.getPrimitive(ChunkedMac.class);
+    ChunkedMacComputation macComputation = mac0.createComputation();
+    macComputation.update(plaintext);
+    byte[] tag = macComputation.computeMac();
+
+    plaintext.rewind();
+    ChunkedMac mac1 = assortedKeysetHandle1.getPrimitive(ChunkedMac.class);
+    ChunkedMacVerification macVerification = mac1.createVerification(tag);
+    macVerification.update(plaintext);
+
+    macVerification.verifyMac();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyManagerTest.java
index cc187a3..dd1d82e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyManagerTest.java
@@ -27,17 +27,18 @@
 import com.google.crypto.tink.proto.HmacKey;
 import com.google.crypto.tink.proto.HmacKeyFormat;
 import com.google.crypto.tink.proto.HmacParams;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.PrfHmacJce;
 import com.google.crypto.tink.subtle.PrfMac;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
 import javax.crypto.spec.SecretKeySpec;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -48,6 +49,11 @@
   private final HmacKeyManager manager = new HmacKeyManager();
   private final KeyTypeManager.KeyFactory<HmacKeyFormat, HmacKey> factory = manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    MacConfig.register();
+  }
+
   @Test
   public void validateKeyFormat_empty() throws Exception {
     assertThrows(
@@ -144,7 +150,7 @@
     int numKeys = 100;
     Set<String> keys = new TreeSet<>();
     for (int i = 0; i < numKeys; ++i) {
-      keys.add(TestUtil.hexEncode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numKeys);
   }
@@ -257,12 +263,39 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    HmacParams params = HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(32).build();
+    HmacKey key =
+        factory.deriveKey(
+            HmacKeyFormat.newBuilder().setVersion(0).setParams(params).setKeySize(keySize).build(),
+            fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKey_notEnoughKeyMaterial_throws() throws Exception {
     byte[] keyMaterial = Random.randBytes(31);
-    HmacParams params = HmacParams.newBuilder()
-        .setHash(HashType.SHA256)
-        .setTagSize(32)
-        .build();
+    HmacParams params = HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(32).build();
     HmacKeyFormat format =
         HmacKeyFormat.newBuilder().setVersion(0).setParams(params).setKeySize(32).build();
     assertThrows(
@@ -308,53 +341,53 @@
   @Test
   public void testHmacSha256HalfDigestTemplate() throws Exception {
     KeyTemplate template = HmacKeyManager.hmacSha256HalfDigestTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new HmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    HmacKeyFormat format =
-        HmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getTagSize()).isEqualTo(16);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA256);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            HmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(16)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testHmacSha256Template() throws Exception {
     KeyTemplate template = HmacKeyManager.hmacSha256Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new HmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    HmacKeyFormat format =
-        HmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getTagSize()).isEqualTo(32);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA256);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            HmacParameters.builder()
+                .setKeySizeBytes(32)
+                .setTagSizeBytes(32)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testHmacSha512HalfDigestTemplate() throws Exception {
     KeyTemplate template = HmacKeyManager.hmacSha512HalfDigestTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new HmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    HmacKeyFormat format =
-        HmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(64);
-    assertThat(format.getParams().getTagSize()).isEqualTo(32);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA512);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            HmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setTagSizeBytes(32)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .setVariant(HmacParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testHmacSha512Template() throws Exception {
     KeyTemplate template = HmacKeyManager.hmacSha512Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new HmacKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    HmacKeyFormat format =
-        HmacKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(64);
-    assertThat(format.getParams().getTagSize()).isEqualTo(64);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA512);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            HmacParameters.builder()
+                .setKeySizeBytes(64)
+                .setTagSizeBytes(64)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .setVariant(HmacParameters.Variant.TINK)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyTest.java
new file mode 100644
index 0000000..e01499e
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/HmacKeyTest.java
@@ -0,0 +1,343 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class HmacKeyTest {
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    HmacKey key = HmacKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildLegacyVariantAndGetProperties() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.LEGACY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(keyBytes)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> HmacKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HmacKey.builder().setKeyBytes(SecretBytes.randomBytes(32)).build());
+  }
+
+  @Test
+  public void buildWithoutKeyBytes_fails() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class, () -> HmacKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void paramtersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    HmacParameters parametersWithIdRequirement =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacKey.builder()
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .setParameters(parametersWithIdRequirement)
+                .build());
+  }
+
+  @Test
+  public void paramtersDoesNotRequireIdButIdIsSetInBuild_fails() throws Exception {
+    HmacParameters parametersWithoutIdRequirement =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parametersWithoutIdRequirement.hasIdRequirement()).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacKey.builder()
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .setParameters(parametersWithoutIdRequirement)
+                .setIdRequirement(0x66AABBCC)
+                .build());
+  }
+
+  @Test
+  public void build_keyTooSmall_fails() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build());
+  }
+
+  @Test
+  public void build_keyTooLarge_fails() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacKey.builder()
+                .setParameters(parameters)
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes1 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes1Copy =
+        SecretBytes.copyFrom(
+            keyBytes1.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes2 = SecretBytes.randomBytes(32);
+    SecretBytes keyBytes16 = SecretBytes.randomBytes(16);
+
+    HmacParameters noPrefixParameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    HmacParameters noPrefixParameters16 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    HmacParameters tinkPrefixParameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.TINK)
+            .build();
+    HmacParameters legacyPrefixParameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.LEGACY)
+            .build();
+    HmacParameters crunchyPrefixParameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    HmacParameters noPrefixParametersSha512 =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA512)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes1",
+            HmacKey.builder().setParameters(noPrefixParameters).setKeyBytes(keyBytes1).build(),
+            // the same key built twice must be equal
+            HmacKey.builder().setParameters(noPrefixParameters).setKeyBytes(keyBytes1).build(),
+            // the same key built with a copy of key bytes must be equal
+            HmacKey.builder().setParameters(noPrefixParameters).setKeyBytes(keyBytes1Copy).build(),
+            // setting id requirement to null is equal to not setting it
+            HmacKey.builder()
+                .setParameters(noPrefixParameters)
+                .setKeyBytes(keyBytes1)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "No prefix, keyBytes2",
+            HmacKey.builder().setParameters(noPrefixParameters).setKeyBytes(keyBytes2).build())
+        // This group checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "No prefix with SHA512, keyBytes1",
+            HmacKey.builder()
+                .setParameters(noPrefixParametersSha512)
+                .setKeyBytes(keyBytes1)
+                .build())
+        .addEqualityGroup(
+            "No prefix, keyBytes16",
+            HmacKey.builder().setParameters(noPrefixParameters16).setKeyBytes(keyBytes16).build())
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes1",
+            HmacKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setKeyBytes(keyBytes1)
+                .setIdRequirement(1907)
+                .build(),
+            HmacKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setKeyBytes(keyBytes1)
+                .setIdRequirement(1907)
+                .build(),
+            HmacKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setKeyBytes(keyBytes1Copy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes1",
+            HmacKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setKeyBytes(keyBytes1)
+                .setIdRequirement(1908)
+                .build())
+        // This 2 groups check that keys with different output prefix types are not equal
+        .addEqualityGroup(
+            "Legacy with key id 1907, keyBytes1",
+            HmacKey.builder()
+                .setParameters(legacyPrefixParameters)
+                .setKeyBytes(keyBytes1)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes1",
+            HmacKey.builder()
+                .setParameters(crunchyPrefixParameters)
+                .setKeyBytes(keyBytes1)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/HmacParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/HmacParametersTest.java
new file mode 100644
index 0000000..304e8ce
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/HmacParametersTest.java
@@ -0,0 +1,552 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class HmacParametersTest {
+
+  // Size of the prefix in bytes
+  private static final int PREFIX_SIZE = 5;
+
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getHashType()).isEqualTo(HmacParameters.HashType.SHA256);
+    assertThat(parameters.getVariant()).isEqualTo(HmacParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .build();
+    assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getVariant()).isEqualTo(HmacParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setTagSizeBytes(21)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingTagSize_fails() throws Exception {
+   assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingHashType_fails() throws Exception {
+   assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(null)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithNoPrefix() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getVariant()).isEqualTo(HmacParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(21 + PREFIX_SIZE);
+    assertThat(parameters.getVariant()).isEqualTo(HmacParameters.Variant.TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithLegacyPrefix() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.LEGACY)
+            .build();
+    assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(21 + PREFIX_SIZE);
+    assertThat(parameters.getVariant()).isEqualTo(HmacParameters.Variant.LEGACY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getCryptographicTagSizeBytes()).isEqualTo(21);
+    assertThat(parameters.getTotalTagSizeBytes()).isEqualTo(21 + PREFIX_SIZE);
+    assertThat(parameters.getVariant()).isEqualTo(HmacParameters.Variant.CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithSha256() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getHashType()).isEqualTo(HmacParameters.HashType.SHA256);
+  }
+
+  @Test
+  public void buildParametersWithSha384() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA384)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getHashType()).isEqualTo(HmacParameters.HashType.SHA384);
+  }
+
+  @Test
+  public void buildParametersWithSha512() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA512)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getHashType()).isEqualTo(HmacParameters.HashType.SHA512);
+  }
+
+  @Test
+  public void buildParametersWithSha1_acceptsTagSizesBetween10And20() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(0)
+                .setHashType(HmacParameters.HashType.SHA1)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setHashType(HmacParameters.HashType.SHA1)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    HmacParameters unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA1)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(20)
+            .setHashType(HmacParameters.HashType.SHA1)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(21)
+                .setHashType(HmacParameters.HashType.SHA1)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(32)
+                .setHashType(HmacParameters.HashType.SHA1)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSha224_acceptsTagSizesBetween10And28() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(0)
+                .setHashType(HmacParameters.HashType.SHA224)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setHashType(HmacParameters.HashType.SHA224)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    HmacParameters unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA224)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(28)
+            .setHashType(HmacParameters.HashType.SHA224)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(29)
+                .setHashType(HmacParameters.HashType.SHA224)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(32)
+                .setHashType(HmacParameters.HashType.SHA224)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSha256_acceptsTagSizesBetween10And32() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(0)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    HmacParameters unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(32)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(33)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(64)
+                .setHashType(HmacParameters.HashType.SHA256)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSha384_acceptsTagSizesBetween10And48() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(0)
+                .setHashType(HmacParameters.HashType.SHA384)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setHashType(HmacParameters.HashType.SHA384)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    HmacParameters unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA384)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(48)
+            .setHashType(HmacParameters.HashType.SHA384)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(49)
+                .setHashType(HmacParameters.HashType.SHA384)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(64)
+                .setHashType(HmacParameters.HashType.SHA384)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSha512_acceptsTagSizesBetween10And64() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(0)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(9)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    HmacParameters unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(10)
+            .setHashType(HmacParameters.HashType.SHA512)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    unused =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(64)
+            .setHashType(HmacParameters.HashType.SHA512)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(65)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacParameters.builder()
+                .setKeySizeBytes(16)
+                .setTagSizeBytes(128)
+                .setHashType(HmacParameters.HashType.SHA512)
+                .setVariant(HmacParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    HmacParameters parameters1 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    HmacParameters parameters2 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    HmacParameters parameters1 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+
+    HmacParameters parameters2;
+
+    parameters2 =
+        HmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(22)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA384)
+            .setVariant(HmacParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    parameters2 =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .setVariant(HmacParameters.Variant.TINK)
+            .build();
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/HmacProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/HmacProtoSerializationTest.java
new file mode 100644
index 0000000..64dd65f
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/HmacProtoSerializationTest.java
@@ -0,0 +1,574 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HmacParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for HmacProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class HmacProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.HmacKey";
+
+  private static final SecretBytes KEY_BYTES_42 = SecretBytes.randomBytes(42);
+  private static final ByteString KEY_BYTES_42_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_42.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    HmacProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix_sha1_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA1)
+          .setVariant(HmacParameters.Variant.NO_PREFIX)
+          .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(13))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink_sha224_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA224)
+          .setVariant(HmacParameters.Variant.TINK)
+          .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA224).setTagSize(13))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_legacy_sha256_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA256)
+          .setVariant(HmacParameters.Variant.LEGACY)
+          .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            OutputPrefixType.LEGACY,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(13))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_crunchy_sha384_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA384)
+          .setVariant(HmacParameters.Variant.CRUNCHY)
+          .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA384).setTagSize(13))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_tink_sha512_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA512)
+          .setVariant(HmacParameters.Variant.TINK)
+          .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA512).setTagSize(13))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseKey_noPrefix_sha1_equal() throws Exception {
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(
+                HmacParameters.builder()
+                    .setKeySizeBytes(42)
+                    .setTagSizeBytes(13)
+                    .setHashType(HmacParameters.HashType.SHA1)
+                    .setVariant(HmacParameters.Variant.NO_PREFIX)
+                    .build())
+            .setKeyBytes(KEY_BYTES_42)
+            .build();
+    com.google.crypto.tink.proto.HmacKey protoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(13))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            protoHmacKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /*idRequirement=*/ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink_sha224_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA224)
+          .setVariant(HmacParameters.Variant.TINK)
+          .build();
+    HmacKey key = HmacKey.builder()
+        .setParameters(parameters)
+        .setKeyBytes(KEY_BYTES_42)
+        .setIdRequirement(123)
+        .build();
+    com.google.crypto.tink.proto.HmacKey protoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setParams(HmacParams.newBuilder().setHash(HashType.SHA224).setTagSize(13))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            protoHmacKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /*idRequirement=*/ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_legacy_sha256_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA256)
+          .setVariant(HmacParameters.Variant.LEGACY)
+          .build();
+    HmacKey key = HmacKey.builder()
+        .setParameters(parameters)
+        .setKeyBytes(KEY_BYTES_42)
+        .setIdRequirement(123)
+        .build();
+    com.google.crypto.tink.proto.HmacKey protoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(13))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            protoHmacKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            /*idRequirement=*/ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_crunchy_sha384_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA384)
+          .setVariant(HmacParameters.Variant.CRUNCHY)
+          .build();
+    HmacKey key = HmacKey.builder()
+        .setParameters(parameters)
+        .setKeyBytes(KEY_BYTES_42)
+        .setIdRequirement(123)
+        .build();
+    com.google.crypto.tink.proto.HmacKey protoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setParams(HmacParams.newBuilder().setHash(HashType.SHA384).setTagSize(13))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            protoHmacKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /*idRequirement=*/ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_tink_sha512_equal() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA512)
+          .setVariant(HmacParameters.Variant.TINK)
+          .build();
+    HmacKey key = HmacKey.builder()
+        .setParameters(parameters)
+        .setKeyBytes(KEY_BYTES_42)
+        .setIdRequirement(123)
+        .build();
+    com.google.crypto.tink.proto.HmacKey protoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setParams(HmacParams.newBuilder().setHash(HashType.SHA512).setTagSize(13))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            protoHmacKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /*idRequirement=*/ 123);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testParseKeys_noAccess_throws()
+      throws Exception {
+    com.google.crypto.tink.proto.HmacKey protoHmacKey =
+        com.google.crypto.tink.proto.HmacKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+            .setParams(HmacParams.newBuilder().setHash(HashType.SHA512).setTagSize(13))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.HmacKey",
+            protoHmacKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /*idRequirement=*/ 123);
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    HmacParameters parameters = HmacParameters.builder()
+          .setKeySizeBytes(42)
+          .setTagSizeBytes(13)
+          .setHashType(HmacParameters.HashType.SHA512)
+          .setVariant(HmacParameters.Variant.TINK)
+          .build();
+    HmacKey key = HmacKey.builder()
+        .setParameters(parameters)
+        .setKeyBytes(KEY_BYTES_42)
+        .setIdRequirement(123)
+        .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // tag size too small
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(9))
+                .build()),
+        // key size too small
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(1)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(13))
+                .build()),
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(-1)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(13))
+                .build()),
+        // unknown output prefix
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(13))
+                .build()),
+        // version 1 is unknown and must be rejected
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacKeyFormat.newBuilder()
+                .setVersion(1)
+                .setKeySize(42)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(13))
+                .build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Tag Length (9)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(9))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Key Length (8)
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[8]))
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL -- not sure if this should be tested; this won't even get to the code
+        // under test.
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.HmacKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_42_AS_BYTE_STRING)
+                .setParams(HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(16))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/MacConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/MacConfigTest.java
index e1d094d..47f2991 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/MacConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/MacConfigTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.mac;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Mac;
@@ -44,19 +45,16 @@
   public void aaaTestInitialization() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    GeneralSecurityException e =
-        assertThrows(GeneralSecurityException.class, () -> Registry.getCatalogue("tinkmac"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("MacConfig.register()");
     String typeUrl = "type.googleapis.com/google.crypto.tink.HmacKey";
-    e = assertThrows(GeneralSecurityException.class, () -> Registry.getKeyManager(typeUrl));
+    GeneralSecurityException e =
+        assertThrows(GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
     assertThat(e.toString()).contains("No key manager found");
 
     // Initialize the config.
     MacConfig.register();
 
     // After registration the key manager should be present.
-    Registry.getKeyManager(typeUrl);
+    assertNotNull(Registry.getUntypedKeyManager(typeUrl));
 
     // Running init() manually again should succeed.
     MacConfig.register();
@@ -76,7 +74,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, Mac.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, Mac.class));
     }
   }
 
@@ -93,7 +91,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, Mac.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, Mac.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/MacIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/MacIntegrationTest.java
deleted file mode 100644
index 45f780b..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/mac/MacIntegrationTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.mac;
-
-import static com.google.crypto.tink.testing.TestUtil.assertExceptionContains;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThrows;
-
-import com.google.crypto.tink.CryptoFormat;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.Mac;
-import com.google.crypto.tink.daead.DeterministicAeadConfig;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset.Key;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests which run the everything for Macs. */
-@RunWith(JUnit4.class)
-public class MacIntegrationTest {
-  private static final int HMAC_KEY_SIZE = 20;
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    MacConfig.register();
-    DeterministicAeadConfig.register(); // need this for testInvalidKeyMaterial.
-  }
-
-  @Test
-  public void testMultipleKeys() throws Exception {
-    byte[] keyValue = Random.randBytes(HMAC_KEY_SIZE);
-    Key tink = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          42,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.TINK);
-    Key legacy = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          43,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.LEGACY);
-    Key raw = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          44,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.RAW);
-    Key crunchy = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          45,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.CRUNCHY);
-    Key[] keys = new Key[] {tink, legacy, raw, crunchy};
-    for (int i = 0; i < keys.length; i++) {
-      KeysetHandle keysetHandle =
-          TestUtil.createKeysetHandle(
-              TestUtil.createKeyset(
-                  keys[i],
-                  keys[(i + 1) % keys.length],
-                  keys[(i + 2) % keys.length],
-                  keys[(i + 3) % keys.length]));
-      Mac mac = keysetHandle.getPrimitive(Mac.class);
-      byte[] plaintext = "plaintext".getBytes(UTF_8);
-      byte[] tag = mac.computeMac(plaintext);
-      if (!keys[i].getOutputPrefixType().equals(OutputPrefixType.RAW)) {
-        byte[] prefix = Arrays.copyOf(tag, CryptoFormat.NON_RAW_PREFIX_SIZE);
-        assertArrayEquals(prefix, CryptoFormat.getOutputPrefix(keys[i]));
-      }
-      try {
-        mac.verifyMac(tag, plaintext);
-      } catch (GeneralSecurityException e) {
-        throw new AssertionError("Valid MAC, should not throw exception: " + i, e);
-      }
-
-      // Modify plaintext or tag and make sure the verifyMac failed.
-      byte[] plaintextAndTag = Bytes.concat(plaintext, tag);
-      for (int b = 0; b < plaintextAndTag.length; b++) {
-        for (int bit = 0; bit < 8; bit++) {
-          byte[] modified = Arrays.copyOf(plaintextAndTag, plaintextAndTag.length);
-          modified[b] ^= (byte) (1 << bit);
-          assertThrows(
-              GeneralSecurityException.class,
-              () ->
-                  mac.verifyMac(
-                      Arrays.copyOfRange(modified, plaintext.length, modified.length),
-                      Arrays.copyOf(modified, plaintext.length)));
-        }
-      }
-
-      // mac with a non-primary RAW key, verify with the keyset
-      KeysetHandle keysetHandle2 = TestUtil.createKeysetHandle(
-          TestUtil.createKeyset(raw, legacy, tink, crunchy));
-      Mac mac2 = keysetHandle2.getPrimitive(Mac.class);
-      tag = mac2.computeMac(plaintext);
-      try {
-        mac.verifyMac(tag, plaintext);
-      } catch (GeneralSecurityException e) {
-        throw new AssertionError("Valid MAC, should not throw exception", e);
-      }
-
-      // mac with a random key not in the keyset, verify with the keyset should fail
-      byte[] keyValue2 = Random.randBytes(HMAC_KEY_SIZE);
-      Key random = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue2, 16),
-          44,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.TINK);
-      keysetHandle2 = TestUtil.createKeysetHandle(
-          TestUtil.createKeyset(random));
-      mac2 = keysetHandle2.getPrimitive(Mac.class);
-      byte[] tag2 = mac2.computeMac(plaintext);
-      assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tag2, plaintext));
-    }
-  }
-
-  @Test
-  public void testSmallPlaintextWithRawKey() throws Exception {
-    byte[] keyValue = Random.randBytes(HMAC_KEY_SIZE);
-    Key primary =
-        TestUtil.createKey(
-            TestUtil.createHmacKeyData(keyValue, 16),
-            42,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    KeysetHandle keysetHandle = TestUtil.createKeysetHandle(TestUtil.createKeyset(primary));
-    Mac mac = keysetHandle.getPrimitive(Mac.class);
-    byte[] plaintext = "blah".getBytes(UTF_8);
-    byte[] tag = mac.computeMac(plaintext);
-    // no prefix
-    assertEquals(16 /* TAG */, tag.length);
-    try {
-      mac.verifyMac(tag, plaintext);
-    } catch (GeneralSecurityException e) {
-      throw new AssertionError("Valid MAC, should not throw exception", e);
-    }
-  }
-
-  @Test
-  public void testInvalidKeyMaterial() throws Exception {
-    Key valid =
-        TestUtil.createKey(
-            TestUtil.createHmacKeyData(Random.randBytes(HMAC_KEY_SIZE), 16),
-            42,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.TINK);
-    Key invalid =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(64), 43, KeyStatusType.ENABLED, OutputPrefixType.TINK);
-
-    final KeysetHandle keysetHandle =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(valid, invalid));
-    GeneralSecurityException e =
-        assertThrows(GeneralSecurityException.class, () -> keysetHandle.getPrimitive(Mac.class));
-    assertExceptionContains(e, "com.google.crypto.tink.Mac not supported");
-
-    // invalid as the primary key.
-    final KeysetHandle keysetHandle2 =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(invalid, valid));
-    e = assertThrows(GeneralSecurityException.class, () -> keysetHandle2.getPrimitive(Mac.class));
-    assertExceptionContains(e, "com.google.crypto.tink.Mac not supported");
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/MacKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/MacKeyTemplatesTest.java
index e070032..a86d698 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/MacKeyTemplatesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/MacKeyTemplatesTest.java
@@ -16,20 +16,31 @@
 
 package com.google.crypto.tink.mac;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.HmacKeyFormat;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.protobuf.ExtensionRegistryLite;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests for MacKeyTemplates. */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public class MacKeyTemplatesTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+  }
+
   @Test
   public void hmacSha256_128BitTag() throws Exception {
     KeyTemplate template = MacKeyTemplates.HMAC_SHA256_128BITTAG;
@@ -99,4 +110,34 @@
     assertEquals(tagSize, format.getParams().getTagSize());
     assertEquals(hashType, format.getParams().getHash());
   }
+
+  public static class Pair {
+    public Pair(KeyTemplate template, MacParameters parameters) {
+      this.template = template;
+      this.parameters = parameters;
+    }
+
+    KeyTemplate template;
+    MacParameters parameters;
+  }
+
+  @DataPoints("EquivalentPairs")
+  public static final Pair[] TEMPLATES =
+      new Pair[] {
+        new Pair(
+            MacKeyTemplates.HMAC_SHA256_128BITTAG, PredefinedMacParameters.HMAC_SHA256_128BITTAG),
+        new Pair(
+            MacKeyTemplates.HMAC_SHA256_256BITTAG, PredefinedMacParameters.HMAC_SHA256_256BITTAG),
+        new Pair(
+            MacKeyTemplates.HMAC_SHA512_256BITTAG, PredefinedMacParameters.HMAC_SHA512_256BITTAG),
+        new Pair(
+            MacKeyTemplates.HMAC_SHA512_512BITTAG, PredefinedMacParameters.HMAC_SHA512_512BITTAG),
+        new Pair(MacKeyTemplates.AES_CMAC, PredefinedMacParameters.AES_CMAC),
+      };
+
+  @Theory
+  public void testParametersEqualsKeyTemplate(@FromDataPoints("EquivalentPairs") Pair p)
+      throws Exception {
+    assertThat(TinkProtoParametersFormat.parse(p.template.toByteArray())).isEqualTo(p.parameters);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/MacTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/MacTest.java
new file mode 100644
index 0000000..4474b0d
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/MacTest.java
@@ -0,0 +1,272 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the Mac package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class MacTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonMacKeyset_throws.
+  }
+
+  @DataPoints("templates")
+  public static final String[] TEMPLATES =
+      new String[] {
+        "AES256_CMAC",
+        "AES256_CMAC_RAW",
+        "HMAC_SHA256_128BITTAG",
+        "HMAC_SHA256_128BITTAG_RAW",
+        "HMAC_SHA256_256BITTAG",
+        "HMAC_SHA256_256BITTAG_RAW",
+        "HMAC_SHA512_128BITTAG",
+        "HMAC_SHA512_128BITTAG_RAW",
+        "HMAC_SHA512_256BITTAG",
+        "HMAC_SHA512_256BITTAG_RAW",
+        "HMAC_SHA512_512BITTAG",
+        "HMAC_SHA512_512BITTAG_RAW",
+      };
+
+  @Theory
+  public void create_computeVerify(@FromDataPoints("templates") String templateName)
+      throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    Mac mac = handle.getPrimitive(Mac.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+
+    KeysetHandle otherHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    Mac otherMac = otherHandle.getPrimitive(Mac.class);
+    assertThrows(GeneralSecurityException.class, () -> otherMac.verifyMac(tag, data));
+
+    byte[] invalid = "invalid".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(invalid, data));
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tag, invalid));
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(empty, data));
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tag, empty));
+    mac.verifyMac(mac.computeMac(empty), empty);
+  }
+
+  @Theory
+  public void useAesCmacParametersAndAesCmacKey() throws Exception {
+    AesCmacParameters parameters =
+        AesCmacParameters.builder()
+            .setKeySizeBytes(32)
+            .setTagSizeBytes(13)
+            .setVariant(AesCmacParameters.Variant.LEGACY)
+            .build();
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(parameters).withFixedId(123).makePrimary())
+            .build();
+
+    AesCmacKey aesCmacKey = (AesCmacKey) handle.getAt(0).getKey();
+    assertThat(aesCmacKey.getParameters()).isEqualTo(parameters);
+    assertThat(aesCmacKey.getIdRequirementOrNull()).isEqualTo(123);
+    SecretBytes secretBytes = aesCmacKey.getAesKey();
+    assertThat(secretBytes.size()).isEqualTo(32);
+
+    Mac mac = handle.getPrimitive(Mac.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+  }
+
+  @Theory
+  public void useHmacParametersAndHmacKey() throws Exception {
+    HmacParameters parameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(42)
+            .setTagSizeBytes(13)
+            .setHashType(HmacParameters.HashType.SHA1)
+            .setVariant(HmacParameters.Variant.CRUNCHY)
+            .build();
+    KeysetHandle handle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(parameters).withFixedId(123).makePrimary())
+            .build();
+
+    HmacKey hmacKey = (HmacKey) handle.getAt(0).getKey();
+    assertThat(hmacKey.getParameters()).isEqualTo(parameters);
+    assertThat(hmacKey.getIdRequirementOrNull()).isEqualTo(123);
+    SecretBytes secretBytes = hmacKey.getKeyBytes();
+    assertThat(secretBytes.size()).isEqualTo(42);
+
+    Mac mac = handle.getPrimitive(Mac.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+  }
+
+  // A keyset with one MAC key, serialized in Tink's JSON format.
+  private static final String JSON_MAC_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 207420876,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+          + "        \"value\": \"GiAPii+kxtLpvCARQpftFLt4R+O6ARsyhTR7SkCCGt0bHRIEEBAIAw==\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 207420876,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void readKeysetEncryptDecrypt()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_MAC_KEYSET, InsecureSecretKeyAccess.get());
+
+    Mac mac = handle.getPrimitive(Mac.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+  }
+
+  // A keyset with multiple keys. The first key is the same as in JSON_AEAD_KEYSET.
+  private static final String JSON_MAC_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 2054715504,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+          + "        \"value\": \"GiAPii+kxtLpvCARQpftFLt4R+O6ARsyhTR7SkCCGt0bHRIEEBAIAw==\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 207420876,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    }, {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesCmacKey\","
+          + "        \"value\": \"GgIIEBIgLaZ/6QXYeqZB8F4zHTRJU5k6TF5xvlSX9ZVLVA09UY0=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 2054715504,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }, {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+          + "        \"value\": \"GkCCIGYpFz3mj8wnTH3Ca81F1sQ7JEMxoE8B2nKiND7LrKfbaUx+/qqDXUP"
+          + "VjkzC9XdbjsaEqc9yI+RKyITef+eUEgQQQAgE\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1540103625,"
+          + "      \"outputPrefixType\": \"LEGACY\""
+          + "    }, {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacKey\","
+          + "        \"value\": \"GkA8u6JKtInsySJDZO4j6TLoIvLuGAeAZHDZoTlST0aZZ8gZZViHogzWTqt"
+          + "i2Vlp3ccy+OdN6lhMxSiphcPaR5OiEgQQIAgE\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 570162478,"
+          + "      \"outputPrefixType\": \"CRUNCHY\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void multipleKeysReadKeysetWithEncryptDecrypt()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_MAC_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+
+    Mac mac = handle.getPrimitive(Mac.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] tag = mac.computeMac(data);
+    mac.verifyMac(tag, data);
+
+    // Also test that mac can verify tags computed with a non-primary key. We use
+    // JSON_MAC_KEYSET to compute a tag with the first key.
+    KeysetHandle handle1 =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_MAC_KEYSET, InsecureSecretKeyAccess.get());
+    Mac mac1 = handle1.getPrimitive(Mac.class);
+    byte[] tag1 = mac1.computeMac(data);
+    mac.verifyMac(tag1, data);
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the Mac primitive.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonMacKeyset_throws() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a DeterministicAead primitive, but not a Mac.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(Mac.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/MacWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/MacWrapperTest.java
index 48b6d62..ac94a8e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/MacWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/MacWrapperTest.java
@@ -17,34 +17,26 @@
 package com.google.crypto.tink.mac;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.CryptoFormat;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.Mac;
 import com.google.crypto.tink.PrimitiveSet;
-import com.google.crypto.tink.SecretKeyAccess;
-import com.google.crypto.tink.internal.KeyParser;
 import com.google.crypto.tink.internal.MutableMonitoringRegistry;
-import com.google.crypto.tink.internal.MutableSerializationRegistry;
-import com.google.crypto.tink.internal.ProtoKeySerialization;
 import com.google.crypto.tink.internal.testing.FakeMonitoringClient;
+import com.google.crypto.tink.mac.HmacParameters.HashType;
 import com.google.crypto.tink.monitoring.MonitoringAnnotations;
-import com.google.crypto.tink.proto.KeyData;
-import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
-import com.google.protobuf.ByteString;
+import com.google.crypto.tink.util.SecretBytes;
 import java.security.GeneralSecurityException;
-import java.util.Arrays;
 import java.util.List;
-import javax.annotation.Nullable;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,99 +46,339 @@
 @RunWith(JUnit4.class)
 public class MacWrapperTest {
   private static final int HMAC_KEY_SIZE = 20;
+  private static final int HMAC_TAG_SIZE = 10;
+  private static final int AES_CMAC_KEY_SIZE = 32;
+  private static final int AES_CMAC_TAG_SIZE = 10;
+
+  private static HmacKey rawKey0;
+  private static HmacKey rawKey1;
+  private static AesCmacKey rawKey2;
+  private static AesCmacKey rawKey3;
+  private static HmacKey tinkKey0;
+  private static AesCmacKey tinkKey1;
+  private static HmacKey crunchyKey0;
+  private static AesCmacKey crunchyKey1;
+  private static HmacKey legacyKey0;
+  private static AesCmacKey legacyKey1;
 
   @BeforeClass
   public static void setUp() throws Exception {
     MacConfig.register();
+    AesCmacProtoSerialization.register();
+    HmacProtoSerialization.register();
+    createTestKeys();
   }
 
+  private static void createTestKeys() {
+    final HmacParameters noPrefixHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.NO_PREFIX);
+    final HmacParameters legacyHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.LEGACY);
+    final HmacParameters crunchyHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.CRUNCHY);
+    final HmacParameters tinkHmacParameters =
+        createDefaultHmacParameters(HmacParameters.Variant.TINK);
+    final AesCmacParameters noPrefixAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.NO_PREFIX);
+    final AesCmacParameters legacyAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.LEGACY);
+    final AesCmacParameters crunchyAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.CRUNCHY);
+    final AesCmacParameters tinkAesCmacParameters =
+        createDefaultAesCmacParameters(AesCmacParameters.Variant.TINK);
+
+    try {
+      rawKey0 =
+          HmacKey.builder()
+              .setParameters(noPrefixHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      rawKey1 =
+          HmacKey.builder()
+              .setParameters(noPrefixHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      rawKey2 =
+          AesCmacKey.builder()
+              .setParameters(noPrefixAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      rawKey3 =
+          AesCmacKey.builder()
+              .setParameters(noPrefixAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(null)
+              .build();
+      tinkKey0 =
+          HmacKey.builder()
+              .setParameters(tinkHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(4)
+              .build();
+      tinkKey1 =
+          AesCmacKey.builder()
+              .setParameters(tinkAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(5)
+              .build();
+      crunchyKey0 =
+          HmacKey.builder()
+              .setParameters(crunchyHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(6)
+              .build();
+      crunchyKey1 =
+          AesCmacKey.builder()
+              .setParameters(crunchyAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(7)
+              .build();
+      legacyKey0 =
+          HmacKey.builder()
+              .setParameters(legacyHmacParameters)
+              .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+              .setIdRequirement(8)
+              .build();
+      legacyKey1 =
+          AesCmacKey.builder()
+              .setParameters(legacyAesCmacParameters)
+              .setAesKeyBytes(SecretBytes.randomBytes(AES_CMAC_KEY_SIZE))
+              .setIdRequirement(9)
+              .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static AesCmacParameters createDefaultAesCmacParameters(
+      AesCmacParameters.Variant variant) {
+    try {
+      return AesCmacParameters.builder()
+          .setKeySizeBytes(AES_CMAC_KEY_SIZE)
+          .setTagSizeBytes(AES_CMAC_TAG_SIZE)
+          .setVariant(variant)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static HmacParameters createDefaultHmacParameters(HmacParameters.Variant variant) {
+    try {
+      return HmacParameters.builder()
+          .setKeySizeBytes(HMAC_KEY_SIZE)
+          .setTagSizeBytes(HMAC_TAG_SIZE)
+          .setVariant(variant)
+          .setHashType(HashType.SHA1)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException("Incorrect parameters creation arguments", e);
+    }
+  }
+
+  @Test
+  public void testComputeVerifyMac_works() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle smallKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234).makePrimary())
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .build();
+    Mac mac = smallKeysetHandle.getPrimitive(Mac.class);
+
+    byte[] tag = mac.computeMac(plaintext);
+
+    mac.verifyMac(tag, plaintext);
+  }
+
+  @Test
+  public void testComputeVerifyMac_throwsOnWrongKey() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle computeKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234).makePrimary())
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235).makePrimary())
+            .build();
+    Mac computingMac = computeKeysetHandle.getPrimitive(Mac.class);
+    Mac verifyingMac = verifyKeysetHandle.getPrimitive(Mac.class);
+
+    byte[] tag = computingMac.computeMac(plaintext);
+
+    assertThrows(GeneralSecurityException.class, () -> verifyingMac.verifyMac(tag, plaintext));
+  }
+
+  @Test
+  public void testVerifyMac_checksAllNecessaryRawKeys() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle computeKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237).makePrimary())
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235).makePrimary())
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .build();
+    Mac computingMac = computeKeysetHandle.getPrimitive(Mac.class);
+    Mac verifyingMac = verifyKeysetHandle.getPrimitive(Mac.class);
+
+    byte[] tag = computingMac.computeMac(plaintext);
+
+    verifyingMac.verifyMac(tag, plaintext);
+  }
+
+  @Test
+  public void testVerifyMac_checksRawKeysWhenTagHasTinkKeyPrefix() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] tag = Hex.decode("0152af9740d2fab0cf3f");
+    HmacKey rawKey5 =
+        HmacKey.builder()
+            .setParameters(createDefaultHmacParameters(HmacParameters.Variant.NO_PREFIX))
+            .setKeyBytes(
+                SecretBytes.copyFrom(
+                    Hex.decode("7d40a4d7c192ca113f403b8703e1b7b93fecf99a"),
+                    InsecureSecretKeyAccess.get()))
+            .setIdRequirement(null)
+            .build();
+    // Note: 0x52af97f0 are bytes 1 to 4 in the tag.
+    HmacKey tinkKey2 =
+        HmacKey.builder()
+            .setParameters(createDefaultHmacParameters(HmacParameters.Variant.TINK))
+            .setKeyBytes(SecretBytes.randomBytes(HMAC_KEY_SIZE))
+            .setIdRequirement(0x52af9740)
+            .build();
+    KeysetHandle verifyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey5).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(tinkKey2).makePrimary())
+            .build();
+
+    Mac mac = verifyKeysetHandle.getPrimitive(Mac.class);
+
+    mac.verifyMac(tag, plaintext);
+  }
+
+  @Test
+  public void computeMac_usesPrimaryKey() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236).makePrimary())
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .build();
+    KeysetHandle keysetHandlePrimary =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236).makePrimary())
+            .build();
+    Mac computingMac = keysetHandle.getPrimitive(Mac.class);
+    Mac verifyingMac = keysetHandlePrimary.getPrimitive(Mac.class);
+
+    byte[] tag = computingMac.computeMac(plaintext);
+
+    verifyingMac.verifyMac(tag, plaintext);
+  }
+
+  @Test
+  public void testComputeVerifyMac_manyKeysWork() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle assortedKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234))
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey0))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .addEntry(KeysetHandle.importKey(crunchyKey0))
+            .addEntry(KeysetHandle.importKey(crunchyKey1).makePrimary())
+            .addEntry(KeysetHandle.importKey(legacyKey0))
+            .addEntry(KeysetHandle.importKey(legacyKey1))
+            .build();
+    Mac mac = assortedKeysetHandle.getPrimitive(Mac.class);
+
+    byte[] tag = mac.computeMac(plaintext);
+
+    mac.verifyMac(tag, plaintext);
+  }
+
+  @Test
+  public void testVerifyMac_shiftedPrimaryWithManyKeysWorks() throws Exception {
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle assortedKeysetHandle0 =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234))
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey0))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .addEntry(KeysetHandle.importKey(crunchyKey0))
+            .addEntry(KeysetHandle.importKey(crunchyKey1).makePrimary())
+            .addEntry(KeysetHandle.importKey(legacyKey0))
+            .addEntry(KeysetHandle.importKey(legacyKey1))
+            .build();
+    KeysetHandle.Builder assortedBuilder = KeysetHandle.newBuilder(assortedKeysetHandle0);
+    assortedBuilder.getAt(4).makePrimary();
+    KeysetHandle assortedKeysetHandle1 = assortedBuilder.build();
+    Mac mac = assortedKeysetHandle1.getPrimitive(Mac.class);
+
+    byte[] tag = mac.computeMac(plaintext);
+
+    mac.verifyMac(tag, plaintext);
+  }
+
+  // ------------------------------------------------------------------------------ Monitoring tests
+
   @Test
   public void testMultipleKeysWithoutAnnotation() throws Exception {
     FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
     MutableMonitoringRegistry.globalInstance().clear();
     MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
 
-    byte[] keyValue = Random.randBytes(HMAC_KEY_SIZE);
-    Key tink = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          42,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.TINK);
-    Key legacy = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          43,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.LEGACY);
-    Key raw = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          44,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.RAW);
-    Key crunchy = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue, 16),
-          45,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.CRUNCHY);
-    Key[] keys = new Key[] {tink, legacy, raw, crunchy};
-    int j = keys.length;
-    for (int i = 0; i < j; i++) {
-      PrimitiveSet<Mac> primitives =
-          TestUtil.createPrimitiveSet(
-              TestUtil.createKeyset(
-                  keys[i], keys[(i + 1) % j], keys[(i + 2) % j], keys[(i + 3) % j]),
-              Mac.class);
-      Mac mac = new MacWrapper().wrap(primitives);
-      byte[] plaintext = "plaintext".getBytes(UTF_8);
-      byte[] tag = mac.computeMac(plaintext);
-      if (!keys[i].getOutputPrefixType().equals(OutputPrefixType.RAW)) {
-        byte[] prefix = Arrays.copyOf(tag, CryptoFormat.NON_RAW_PREFIX_SIZE);
-        assertArrayEquals(prefix, CryptoFormat.getOutputPrefix(keys[i]));
-      }
-      try {
-        mac.verifyMac(tag, plaintext);
-      } catch (GeneralSecurityException e) {
-        throw new AssertionError("Valid MAC, should not throw exception: " + i, e);
-      }
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    KeysetHandle mainKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey0).withFixedId(1234))
+            .addEntry(KeysetHandle.importKey(rawKey1).withFixedId(1235))
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236))
+            .addEntry(KeysetHandle.importKey(rawKey3).withFixedId(1237))
+            .addEntry(KeysetHandle.importKey(tinkKey0))
+            .addEntry(KeysetHandle.importKey(tinkKey1))
+            .addEntry(KeysetHandle.importKey(crunchyKey0))
+            .addEntry(KeysetHandle.importKey(crunchyKey1).makePrimary())
+            .addEntry(KeysetHandle.importKey(legacyKey1))
+            .build();
+    KeysetHandle noPrefixKeyKeyset =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rawKey2).withFixedId(1236).makePrimary())
+            .build();
+    KeysetHandle prefixedKeyKeyset =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(crunchyKey1).makePrimary())
+            .build();
+    KeysetHandle missingKeyKeyset =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(legacyKey0).makePrimary())
+            .build();
+    Mac mac = mainKeysetHandle.getPrimitive(Mac.class);
+    Mac noPrefixMac = noPrefixKeyKeyset.getPrimitive(Mac.class);
+    Mac prefixedMac = prefixedKeyKeyset.getPrimitive(Mac.class);
+    Mac missingMac = missingKeyKeyset.getPrimitive(Mac.class);
 
-      // Modify plaintext or tag and make sure the verifyMac failed.
-      byte[] plaintextAndTag = Bytes.concat(plaintext, tag);
-      for (int b = 0; b < plaintextAndTag.length; b++) {
-        for (int bit = 0; bit < 8; bit++) {
-          byte[] modified = Arrays.copyOf(plaintextAndTag, plaintextAndTag.length);
-          modified[b] ^= (byte) (1 << bit);
-          assertThrows(
-              GeneralSecurityException.class,
-              () ->
-                  mac.verifyMac(
-                      Arrays.copyOfRange(modified, plaintext.length, modified.length),
-                      Arrays.copyOf(modified, plaintext.length)));
-        }
-      }
-
-      // mac with a non-primary RAW key, verify with the keyset
-      PrimitiveSet<Mac> primitives2 =
-          TestUtil.createPrimitiveSet(TestUtil.createKeyset(raw, legacy, tink, crunchy), Mac.class);
-      Mac mac2 = new MacWrapper().wrap(primitives2);
-      tag = mac2.computeMac(plaintext);
-      try {
-        mac.verifyMac(tag, plaintext);
-      } catch (GeneralSecurityException e) {
-        throw new AssertionError("Valid MAC, should not throw exception", e);
-      }
-
-      // mac with a random key not in the keyset, verify with the keyset should fail
-      byte[] keyValue2 = Random.randBytes(HMAC_KEY_SIZE);
-      Key random = TestUtil.createKey(
-          TestUtil.createHmacKeyData(keyValue2, 16),
-          44,
-          KeyStatusType.ENABLED,
-          OutputPrefixType.TINK);
-      PrimitiveSet<Mac> primitives3 =
-          TestUtil.createPrimitiveSet(TestUtil.createKeyset(random), Mac.class);
-      mac2 = new MacWrapper().wrap(primitives3);
-      byte[] tag2 = mac2.computeMac(plaintext);
-      assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tag2, plaintext));
-    }
+    // Busy work triggering different code paths.
+    byte[] tag = noPrefixMac.computeMac(plaintext);
+    mac.verifyMac(tag, plaintext);
+    tag = prefixedMac.computeMac(plaintext);
+    mac.verifyMac(tag, plaintext);
+    byte[] missingTag = missingMac.computeMac(plaintext);
+    assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(missingTag, plaintext));
 
     // Without annotations, nothing gets logged.
     assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
@@ -154,28 +386,6 @@
   }
 
   @Test
-  public void testSmallPlaintextWithRawKey() throws Exception {
-    byte[] keyValue = Random.randBytes(HMAC_KEY_SIZE);
-    Key primary = TestUtil.createKey(
-        TestUtil.createHmacKeyData(keyValue, 16),
-        42,
-        KeyStatusType.ENABLED,
-        OutputPrefixType.RAW);
-    PrimitiveSet<Mac> primitives =
-        TestUtil.createPrimitiveSet(TestUtil.createKeyset(primary), Mac.class);
-    Mac mac = new MacWrapper().wrap(primitives);
-    byte[] plaintext = "blah".getBytes(UTF_8);
-    byte[] tag = mac.computeMac(plaintext);
-    // no prefix
-    assertThat(tag).hasLength(16);
-    try {
-      mac.verifyMac(tag, plaintext);
-    } catch (GeneralSecurityException e) {
-      throw new AssertionError("Valid MAC, should not throw exception", e);
-    }
-  }
-
-  @Test
   public void testWithAnnotation_hasMonitoring() throws Exception {
     FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
     MutableMonitoringRegistry.globalInstance().clear();
@@ -348,88 +558,4 @@
     assertThat(verifyFailure2.getKeysetInfo().getPrimaryKeyId()).isEqualTo(42);
     assertThat(verifyFailure2.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
   }
-
-  // Returns a key whose output prefix is taken from
-  private static com.google.crypto.tink.Key parseKeyWithProgrammableOutputPrefix(
-      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access) {
-    com.google.crypto.tink.util.Bytes outputPrefix =
-        com.google.crypto.tink.util.Bytes.copyFrom(serialization.getValue().toByteArray());
-    return new MacKey() {
-      @Override
-      public com.google.crypto.tink.util.Bytes getOutputPrefix() {
-        return outputPrefix;
-      }
-
-      @Override
-      public MacParameters getParameters() {
-        return new MacParameters() {
-          @Override
-          public boolean hasIdRequirement() {
-            throw new UnsupportedOperationException("Not needed in test");
-          }
-        };
-      }
-
-      @Override
-      public Integer getIdRequirementOrNull() {
-        throw new UnsupportedOperationException("Not needed in test");
-      }
-
-      @Override
-      public boolean equalsKey(com.google.crypto.tink.Key k) {
-        throw new UnsupportedOperationException("Not needed in test");
-      }
-    };
-  }
-
-  private static final String TYPE_URL_FOR_PROGRAMMABLE_KEY = "typeUrlForProgrammableKey";
-  @BeforeClass
-  public static void registerProgrammableOutputPrefixKey() throws Exception {
-    MutableSerializationRegistry.globalInstance()
-        .registerKeyParser(
-            KeyParser.create(
-                MacWrapperTest::parseKeyWithProgrammableOutputPrefix,
-                toBytesFromPrintableAscii(TYPE_URL_FOR_PROGRAMMABLE_KEY),
-                ProtoKeySerialization.class));
-  }
-
-  @Test
-  public void correctOutputPrefixInKey_creatingWorks() throws Exception {
-    Key key =
-        Key.newBuilder()
-            .setOutputPrefixType(OutputPrefixType.TINK)
-            .setKeyData(
-                KeyData.newBuilder()
-                    .setTypeUrl(TYPE_URL_FOR_PROGRAMMABLE_KEY)
-                    .setValue(ByteString.copyFrom(new byte[] {0x01, 0x01, 0x02, 0x03, 0x04}))
-                    .setKeyMaterialType(KeyMaterialType.SYMMETRIC))
-            .setKeyId(0x01020304)
-            .setStatus(KeyStatusType.ENABLED)
-            .build();
-    // Wrapping would throw before any mac is computed, so we can use any Mac for the test.
-    PrimitiveSet<Mac> primitives =
-        PrimitiveSet.newBuilder(Mac.class).addPrimaryPrimitive(new AlwaysFailingMac(), key).build();
-    new MacWrapper().wrap(primitives);
-  }
-
-  @Test
-  public void wrongOutputPrefixInKey_creationFails() throws Exception {
-    Key key =
-        Key.newBuilder()
-            .setOutputPrefixType(OutputPrefixType.TINK)
-            .setKeyData(
-                KeyData.newBuilder()
-                    .setTypeUrl(TYPE_URL_FOR_PROGRAMMABLE_KEY)
-                    .setValue(ByteString.copyFrom(new byte[] {0x01, 0x01, 0x02, 0x03, 0x04}))
-                    .setKeyMaterialType(KeyMaterialType.SYMMETRIC))
-            .setKeyId(0x01020305)
-            .setStatus(KeyStatusType.ENABLED)
-            .build();
-    // Wrapping throws before any mac is computed, so we can use an AlwaysFailingMac for the test.
-    PrimitiveSet<Mac> primitives =
-        PrimitiveSet.newBuilder(Mac.class).addPrimaryPrimitive(new AlwaysFailingMac(), key).build();
-    GeneralSecurityException e =
-        assertThrows(GeneralSecurityException.class, () -> new MacWrapper().wrap(primitives));
-    assertThat(e).hasMessageThat().contains("has wrong output prefix");
-  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/PredefinedMacParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/PredefinedMacParametersTest.java
new file mode 100644
index 0000000..282df33
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/PredefinedMacParametersTest.java
@@ -0,0 +1,63 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class PredefinedMacParametersTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+  }
+
+  @DataPoints("AllParameters")
+  public static final MacParameters[] TEMPLATES =
+      new MacParameters[] {
+        PredefinedMacParameters.HMAC_SHA256_128BITTAG,
+        PredefinedMacParameters.HMAC_SHA256_256BITTAG,
+        PredefinedMacParameters.HMAC_SHA512_256BITTAG,
+        PredefinedMacParameters.HMAC_SHA512_512BITTAG,
+        PredefinedMacParameters.AES_CMAC
+      };
+
+  @Theory
+  public void testInstantiation(@FromDataPoints("AllParameters") MacParameters parameters)
+      throws Exception {
+    Key key = KeysetHandle.generateNew(parameters).getAt(0).getKey();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+  }
+
+  @Test
+  public void testTypes() {
+    assertThat(PredefinedMacParameters.HMAC_SHA256_128BITTAG).isNotNull();
+    assertThat(PredefinedMacParameters.HMAC_SHA256_256BITTAG).isNotNull();
+    assertThat(PredefinedMacParameters.HMAC_SHA512_256BITTAG).isNotNull();
+    assertThat(PredefinedMacParameters.HMAC_SHA512_512BITTAG).isNotNull();
+    assertThat(PredefinedMacParameters.AES_CMAC).isNotNull();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/internal/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/mac/internal/BUILD.bazel
index 15223f1..b13ba6f 100644
--- a/java_src/src/test/java/com/google/crypto/tink/mac/internal/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/internal/BUILD.bazel
@@ -10,3 +10,60 @@
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "ChunkedAesCmacTest",
+    size = "small",
+    srcs = ["ChunkedAesCmacTest.java"],
+    data = ["@wycheproof//testvectors:aes_cmac"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:aes_cmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/mac/internal:aes_cmac_test_util",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_aes_cmac_impl",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "ChunkedHmacTest",
+    size = "small",
+    srcs = ["ChunkedHmacTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_computation",
+        "//src/main/java/com/google/crypto/tink/mac:chunked_mac_verification",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_computation",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_impl",
+        "//src/main/java/com/google/crypto/tink/mac/internal:chunked_hmac_verification",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@maven//:org_conscrypt_conscrypt_openjdk_uber",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacTest.java
new file mode 100644
index 0000000..8a29d5c
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/internal/ChunkedAesCmacTest.java
@@ -0,0 +1,657 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.config.TinkFips;
+import com.google.crypto.tink.mac.AesCmacKey;
+import com.google.crypto.tink.mac.AesCmacParameters;
+import com.google.crypto.tink.mac.AesCmacParameters.Variant;
+import com.google.crypto.tink.mac.ChunkedMac;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.ChunkedMacVerification;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.mac.internal.AesCmacTestUtil.AesCmacTestVector;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.testing.WycheproofTestUtil;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/* Unit tests for Streaming AesCmac computation. */
+@RunWith(Theories.class)
+public class ChunkedAesCmacTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+  }
+
+  // Test data from https://tools.ietf.org/html/rfc4493#section-4.
+  private static final AesCmacTestVector[] CMAC_TEST_VECTORS_FROM_RFC =
+      new AesCmacTestVector[] {
+        AesCmacTestUtil.RFC_TEST_VECTOR_0,
+        AesCmacTestUtil.RFC_TEST_VECTOR_1,
+        AesCmacTestUtil.RFC_TEST_VECTOR_2
+      };
+
+  @DataPoints("implementationTestVectors")
+  public static final AesCmacTestVector[] CMAC_IMPLEMENTATION_DETAIL_TEST_VECTORS =
+      new AesCmacTestVector[] {
+        AesCmacTestUtil.NOT_OVERFLOWING_INTERNAL_STATE,
+        AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE,
+        AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE_TWICE,
+        AesCmacTestUtil.OVERFLOW_INTERNAL_STATE_ONCE,
+        AesCmacTestUtil.OVERFLOW_INTERNAL_STATE_TWICE,
+        AesCmacTestUtil.SHORTER_TAG,
+        AesCmacTestUtil.TAG_WITH_KEY_PREFIX_TYPE_LEGACY,
+        AesCmacTestUtil.TAG_WITH_KEY_PREFIX_TYPE_TINK,
+        AesCmacTestUtil.LONG_KEY_TEST_VECTOR,
+      };
+
+  private static final AesCmacTestVector[]
+      CMAC_VERIFICATION_FAIL_FAST_TEST_VECTORS = new AesCmacTestVector[] {
+          AesCmacTestUtil.WRONG_PREFIX_TAG_LEGACY,
+          AesCmacTestUtil.WRONG_PREFIX_TAG_TINK,
+          AesCmacTestUtil.TAG_TOO_SHORT
+      };
+
+  @DataPoints("parameters")
+  public static final AesCmacParameters[] PARAMETERS = {
+    AesCmacTestUtil.createAesCmacParameters(32, 10, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 11, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 12, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 13, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 14, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 15, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 16, Variant.LEGACY),
+    AesCmacTestUtil.createAesCmacParameters(32, 10, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 11, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 12, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 13, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 14, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 15, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 16, Variant.TINK),
+    AesCmacTestUtil.createAesCmacParameters(32, 10, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 11, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 12, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 13, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 14, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 15, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 16, Variant.CRUNCHY),
+    AesCmacTestUtil.createAesCmacParameters(32, 10, Variant.NO_PREFIX),
+    AesCmacTestUtil.createAesCmacParameters(32, 11, Variant.NO_PREFIX),
+    AesCmacTestUtil.createAesCmacParameters(32, 12, Variant.NO_PREFIX),
+    AesCmacTestUtil.createAesCmacParameters(32, 13, Variant.NO_PREFIX),
+    AesCmacTestUtil.createAesCmacParameters(32, 14, Variant.NO_PREFIX),
+    AesCmacTestUtil.createAesCmacParameters(32, 15, Variant.NO_PREFIX),
+    AesCmacTestUtil.createAesCmacParameters(32, 16, Variant.NO_PREFIX),
+  };
+
+  @Theory
+  public void testCompatibility(@FromDataPoints("parameters") AesCmacParameters parameters)
+      throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.generateEntryFromParameters(parameters)
+                    .withFixedId(1234)
+                    .makePrimary())
+            .build();
+    Mac mac = keysetHandle.getPrimitive(Mac.class);
+    AesCmacKey key = (AesCmacKey) keysetHandle.getAt(0).getKey();
+    ChunkedMac chunkedMac = new ChunkedAesCmacImpl(key);
+    ChunkedMacComputation chunkedMacComputation = chunkedMac.createComputation();
+
+    byte[] testData = new byte[] {1, 2, 3, 4, 5, 6, 7, 8};
+    chunkedMacComputation.update(ByteBuffer.wrap(testData));
+    assertThat(mac.computeMac(testData)).isEqualTo(chunkedMacComputation.computeMac());
+  }
+
+  @Test
+  public void testFipsCompatibility() {
+    assumeTrue(TinkFips.useOnlyFips());
+
+    // In FIPS-mode we expect that creating a ChunkedAesCmacImpl fails.
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> new ChunkedAesCmacImpl(AesCmacTestUtil.RFC_TEST_VECTOR_0.key));
+  }
+
+  @Test
+  public void testTagTruncation() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+
+      for (int j = 1; j < t.tag.length; j++) {
+        byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length - j);
+        ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+        macVerification.update(ByteBuffer.wrap(t.message));
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+      }
+    }
+
+    // Test with random keys.
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      AesCmacKey key =
+          AesCmacKey.builder()
+              .setParameters(AesCmacTestUtil.createAesCmacParameters(16, 16, Variant.NO_PREFIX))
+              .setAesKeyBytes(SecretBytes.randomBytes(16))
+              .build();
+      ChunkedMac mac = new ChunkedAesCmacImpl(key);
+      for (int j = 1; j < t.tag.length; j++) {
+        byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length - j);
+        ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+        macVerification.update(ByteBuffer.wrap(t.message));
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+      }
+    }
+  }
+
+  @Test
+  public void testBitFlipMessage() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+      for (int b = 0; b < t.message.length; b++) {
+        for (int bit = 0; bit < 8; bit++) {
+          byte[] modifiedMessage = Arrays.copyOf(t.message, t.message.length);
+          modifiedMessage[b] = (byte) (modifiedMessage[b] ^ (1 << bit));
+          ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+          macVerification.update(ByteBuffer.wrap(modifiedMessage));
+          assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+        }
+      }
+    }
+    // Test with random keys.
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      AesCmacKey key =
+          AesCmacKey.builder()
+              .setParameters(AesCmacTestUtil.createAesCmacParameters(16, 16, Variant.NO_PREFIX))
+              .setAesKeyBytes(SecretBytes.randomBytes(16))
+              .build();
+      ChunkedMac mac = new ChunkedAesCmacImpl(key);
+      for (int b = 0; b < t.message.length; b++) {
+        for (int bit = 0; bit < 8; bit++) {
+          byte[] modifiedMessage = Arrays.copyOf(t.message, t.message.length);
+          modifiedMessage[b] = (byte) (modifiedMessage[b] ^ (1 << bit));
+          ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+          macVerification.update(ByteBuffer.wrap(modifiedMessage));
+          assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testBitFlipTag() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+      for (int b = 0; b < t.tag.length; b++) {
+        for (int bit = 0; bit < 8; bit++) {
+          byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length);
+          modifiedTag[b] = (byte) (modifiedTag[b] ^ (1 << bit));
+          ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+          macVerification.update(ByteBuffer.wrap(t.message));
+          assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+        }
+      }
+    }
+    // Test with random keys.
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      AesCmacKey key =
+          AesCmacKey.builder()
+              .setParameters(AesCmacTestUtil.createAesCmacParameters(16, 16, Variant.NO_PREFIX))
+              .setAesKeyBytes(SecretBytes.randomBytes(16))
+              .build();
+      ChunkedMac mac = new ChunkedAesCmacImpl(key);
+      for (int b = 0; b < t.tag.length; b++) {
+        for (int bit = 0; bit < 8; bit++) {
+          byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length);
+          modifiedTag[b] = (byte) (modifiedTag[b] ^ (1 << bit));
+          ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+          macVerification.update(ByteBuffer.wrap(t.message));
+          assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testThrowExceptionUpdateAfterFinalize() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    ChunkedMac mac = new ChunkedAesCmacImpl(AesCmacTestUtil.RFC_TEST_VECTOR_0.key);
+
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    assertThat(AesCmacTestUtil.RFC_TEST_VECTOR_0.tag).isEqualTo(macComputation.computeMac());
+    assertThrows(
+        IllegalStateException.class,
+        () -> macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message)));
+    assertThrows(IllegalStateException.class, macComputation::computeMac);
+
+    ChunkedMacVerification macVerification =
+        mac.createVerification(AesCmacTestUtil.RFC_TEST_VECTOR_0.tag);
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macVerification.verifyMac();
+    assertThrows(
+        IllegalStateException.class,
+        () -> macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message)));
+    assertThrows(IllegalStateException.class, macVerification::verifyMac);
+  }
+
+  @Test
+  public void testWycheproofVectors() throws Exception {
+    JsonObject json =
+        WycheproofTestUtil.readJson("../wycheproof/testvectors/aes_cmac_test.json");
+    int errors = 0;
+    JsonArray testGroups = json.getAsJsonArray("testGroups");
+    for (int i = 0; i < testGroups.size(); i++) {
+      JsonObject group = testGroups.get(i).getAsJsonObject();
+      JsonArray tests = group.getAsJsonArray("tests");
+
+      int tagSize = group.get("tagSize").getAsInt();
+      int keySize = group.get("keySize").getAsInt();
+      if (!Arrays.asList(16, 32).contains(keySize / 8)) {
+        continue;
+      }
+
+      for (int j = 0; j < tests.size(); j++) {
+        JsonObject testCase = tests.get(j).getAsJsonObject();
+        String tcId =
+            String.format(
+                "testcase %d (%s)",
+                testCase.get("tcId").getAsInt(), testCase.get("comment").getAsString());
+        byte[] key = Hex.decode(testCase.get("key").getAsString());
+        byte[] msg = Hex.decode(testCase.get("msg").getAsString());
+        byte[] tag = Hex.decode(testCase.get("tag").getAsString());
+        // Result is one of "valid", "invalid", "acceptable".
+        // "valid" are test vectors with matching plaintext and tag.
+        // "invalid" are test vectors with invalid parameters or invalid tag.
+        // "acceptable" are test vectors with weak parameters or legacy formats, but there are no
+        // "acceptable" tests cases for Aes Cmac.
+        String result = testCase.get("result").getAsString();
+
+        try {
+          AesCmacParameters noPrefixParameters =
+              AesCmacParameters.builder()
+                  .setTagSizeBytes(tagSize / 8)
+                  .setKeySizeBytes(keySize / 8).build();
+          AesCmacKey aesCmacKey =
+              AesCmacKey.builder()
+                  .setAesKeyBytes(SecretBytes.copyFrom(key, InsecureSecretKeyAccess.get()))
+                  .setParameters(noPrefixParameters).build();
+
+          ChunkedMac mac = new ChunkedAesCmacImpl(aesCmacKey);
+
+          ChunkedMacComputation macComputation = mac.createComputation();
+          macComputation.update(ByteBuffer.wrap(msg));
+          assertThat(tag).isEqualTo(macComputation.computeMac());
+
+          ChunkedMacVerification macVerification = mac.createVerification(tag);
+          macVerification.update(ByteBuffer.wrap(msg));
+          macVerification.verifyMac();
+
+          // If the test is "invalid" but no exception is thrown, it's an error.
+          if (result.equals("invalid")) {
+            System.out.printf("FAIL %s: invalid Wycheproof test did not fail%n", tcId);
+            errors++;
+          }
+        } catch (GeneralSecurityException | AssertionError ex) {
+          if (result.equals("valid")) {
+            System.out.printf("FAIL %s: Wycheproof test failed, exception %s%n", tcId, ex);
+            errors++;
+          }
+        }
+      }
+    }
+    assertThat(errors).isEqualTo(0);
+  }
+
+  @Test
+  public void testCreateVerificationFailsFast() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_VERIFICATION_FAIL_FAST_TEST_VECTORS) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+      assertThrows(GeneralSecurityException.class, () -> mac.createVerification(t.tag));
+    }
+  }
+
+  /**
+   * A cute little table to help verify that the tests below indeed cover the major (when not all)
+   * code paths in our streaming AesCmac implementation:
+   *
+   * <p>-------------------------------------
+   * | Inner state | Amount of new data |  # |
+   * ----------------------------------------
+   * | empty       | empty              |  0 |
+   * | empty       | not overflowing    |  1 |
+   * | empty       | overflowing        |  2 |
+   * | empty       | a lot              |  3 |
+   * ----------------------------------------
+   * | some data   | empty              |  4 |
+   * | some data   | not overflowing    |  5 |
+   * | some data   | overflowing        |  6 |
+   * | some data   | a lot              |  7 |
+   * ----------------------------------------
+   * | full        | empty              |  8 |
+   * | full        | not overfl. (^)    |  9 |
+   * | full        | overflowing        | 10 |
+   * | full        | a lot              | 11 |
+   * ----------------------------------------
+   */
+
+  /* ## 0, 2, 3 */
+  @Test
+  public void testMacTestVectors() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+
+      try {
+        ChunkedMacComputation macComputation = mac.createComputation();
+        macComputation.update(ByteBuffer.wrap(t.message));
+        assertThat(t.tag).isEqualTo(macComputation.computeMac());
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError("Valid computation, should not throw exception", e);
+      }
+
+      try {
+        ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+        macVerification.update(ByteBuffer.wrap(t.message));
+        macVerification.verifyMac();
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError("Valid tag, verification should not throw exception", e);
+      }
+    }
+  }
+
+  /* ## 0, 2, 3 */
+  @Test
+  public void testMacTestVectorsReadOnlyBuffer() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+
+      try {
+        ChunkedMacComputation macComputation = mac.createComputation();
+        macComputation.update(ByteBuffer.wrap(t.message).asReadOnlyBuffer());
+        assertThat(t.tag).isEqualTo(macComputation.computeMac());
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError("Valid computation, should not throw exception", e);
+      }
+
+      try {
+        ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+        macVerification.update(ByteBuffer.wrap(t.message).asReadOnlyBuffer());
+        macVerification.verifyMac();
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError("Valid tag, verification should not throw exception", e);
+      }
+    }
+  }
+
+  /* ## 1, 2, 3 */
+  @Test
+  public void testImplementationTestVectors() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_IMPLEMENTATION_DETAIL_TEST_VECTORS) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+
+      try {
+        ChunkedMacComputation macComputation = mac.createComputation();
+        macComputation.update(ByteBuffer.wrap(t.message));
+        assertThat(t.tag).isEqualTo(macComputation.computeMac());
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError("Valid computation, should not throw exception", e);
+      }
+
+      try {
+        ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+        macVerification.update(ByteBuffer.wrap(t.message));
+        macVerification.verifyMac();
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError("Valid tag, verification should not throw exception", e);
+      }
+    }
+  }
+
+  /* # 0 */
+  @Test
+  public void testMultipleEmptyUpdates() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    ChunkedMac mac = new ChunkedAesCmacImpl(AesCmacTestUtil.RFC_TEST_VECTOR_0.key);
+
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    assertThat(AesCmacTestUtil.RFC_TEST_VECTOR_0.tag).isEqualTo(macComputation.computeMac());
+
+    ChunkedMacVerification macVerification =
+        mac.createVerification(AesCmacTestUtil.RFC_TEST_VECTOR_0.tag);
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.RFC_TEST_VECTOR_0.message));
+    macVerification.verifyMac();
+  }
+
+  /* ## 0, 1, 5, 6, 10  */
+  @Test
+  public void testSmallUpdates() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_IMPLEMENTATION_DETAIL_TEST_VECTORS) {
+      ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+
+      ChunkedMacComputation macComputation = mac.createComputation();
+      for (byte b : t.message) {
+        byte[] bb = new byte[] {b};
+        macComputation.update(ByteBuffer.wrap(bb));
+      }
+      assertThat(t.tag).isEqualTo(macComputation.computeMac());
+
+      ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+      for (byte b : t.message) {
+        byte[] bb = new byte[] {b};
+        macVerification.update(ByteBuffer.wrap(bb));
+      }
+      macVerification.verifyMac();
+    }
+  }
+
+  /* # 8, 9 */
+  @Test
+  public void testEmptyLastUpdateWithFullStash() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    ChunkedMac mac = new ChunkedAesCmacImpl(AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE.key);
+
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(ByteBuffer.wrap(AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE.message));
+    macComputation.update(ByteBuffer.wrap(new byte[0]));
+    assertThat(AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE.tag)
+        .isEqualTo(macComputation.computeMac());
+
+    ChunkedMacVerification macVerification =
+        mac.createVerification(AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE.tag);
+    macVerification.update(ByteBuffer.wrap(AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE.message));
+    macVerification.update(ByteBuffer.wrap(new byte[0]));
+    macVerification.verifyMac();
+  }
+
+  /* ## 1, 4, 5, 6, 7, 8, 9, 11 */
+  @Test
+  public void testMultipleUpdatesDifferentSizes() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    ChunkedMac mac = new ChunkedAesCmacImpl(AesCmacTestUtil.RFC_TEST_VECTOR_1.key);
+
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(
+        ByteBuffer.wrap(Arrays.copyOf(AesCmacTestUtil.RFC_TEST_VECTOR_1.message, 14)));
+    macComputation.update(ByteBuffer.wrap(new byte[0]));
+    macComputation.update(
+        ByteBuffer.wrap(Arrays.copyOfRange(AesCmacTestUtil.RFC_TEST_VECTOR_1.message, 14, 36)));
+    macComputation.update(
+        ByteBuffer.wrap(Arrays.copyOfRange(AesCmacTestUtil.RFC_TEST_VECTOR_1.message, 36, 40)));
+    assertThat(AesCmacTestUtil.RFC_TEST_VECTOR_1.tag).isEqualTo(macComputation.computeMac());
+
+    ChunkedMacVerification macVerification =
+        mac.createVerification(AesCmacTestUtil.RFC_TEST_VECTOR_1.tag);
+    macVerification.update(
+        ByteBuffer.wrap(Arrays.copyOf(AesCmacTestUtil.RFC_TEST_VECTOR_1.message, 32)));
+    macVerification.update(ByteBuffer.wrap(new byte[0]));
+    macVerification.update(
+        ByteBuffer.wrap(Arrays.copyOfRange(AesCmacTestUtil.RFC_TEST_VECTOR_1.message, 32, 40)));
+    macVerification.verifyMac();
+
+    macComputation = mac.createComputation();
+    macComputation.update(
+        ByteBuffer.wrap(Arrays.copyOf(AesCmacTestUtil.RFC_TEST_VECTOR_2.message, 16)));
+    macComputation.update(
+        ByteBuffer.wrap(Arrays.copyOfRange(AesCmacTestUtil.RFC_TEST_VECTOR_2.message, 16, 64)));
+    assertThat(AesCmacTestUtil.RFC_TEST_VECTOR_2.tag).isEqualTo(macComputation.computeMac());
+
+    macVerification = mac.createVerification(AesCmacTestUtil.RFC_TEST_VECTOR_2.tag);
+    macVerification.update(
+        ByteBuffer.wrap(Arrays.copyOf(AesCmacTestUtil.RFC_TEST_VECTOR_2.message, 10)));
+    macVerification.update(
+        ByteBuffer.wrap(Arrays.copyOfRange(AesCmacTestUtil.RFC_TEST_VECTOR_2.message, 10, 64)));
+    macVerification.verifyMac();
+  }
+
+  /**
+   * Finally, here comes a randomized test. In case, for some reason, the above tests do not catch
+   * some bug, we'll have a chance to catch it here.
+   */
+  @Test
+  public void testRandomizedDataChunking() throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+    for (AesCmacTestVector t : CMAC_TEST_VECTORS_FROM_RFC) {
+      testRandomized(t);
+    }
+    for (AesCmacTestVector t : CMAC_IMPLEMENTATION_DETAIL_TEST_VECTORS) {
+      testRandomized(t);
+    }
+
+    // Only feed "valid" Wycheproof test cases into the randomized test.
+    JsonObject json =
+        WycheproofTestUtil.readJson("../wycheproof/testvectors/aes_cmac_test.json");
+    JsonArray testGroups = json.getAsJsonArray("testGroups");
+    for (int i = 0; i < testGroups.size(); i++) {
+      JsonObject group = testGroups.get(i).getAsJsonObject();
+      JsonArray tests = group.getAsJsonArray("tests");
+
+      int tagSize = group.get("tagSize").getAsInt();
+      int keySize = group.get("keySize").getAsInt();
+      if (!Arrays.asList(16, 32).contains(keySize / 8)) {
+        continue;
+      }
+
+      for (int j = 0; j < tests.size(); j++) {
+        JsonObject testCase = tests.get(j).getAsJsonObject();
+        if (!testCase.get("result").getAsString().equals("valid")) {
+          continue;
+        }
+
+        String key = testCase.get("key").getAsString();
+        String msg = testCase.get("msg").getAsString();
+        String tag = testCase.get("tag").getAsString();
+
+        testRandomized(
+            new AesCmacTestVector(
+                AesCmacTestUtil.createAesCmacKey(
+                    key,
+                    AesCmacTestUtil.createAesCmacParameters(
+                        keySize / 8, tagSize / 8, Variant.NO_PREFIX),
+                    null),
+                msg,
+                tag));
+      }
+    }
+  }
+
+  private void testRandomized(AesCmacTestVector t) throws Exception {
+    ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+    ChunkedMacComputation macComputation = mac.createComputation();
+
+    int read = 0;
+    StringBuilder debugReadSequence = new StringBuilder();
+    debugReadSequence.append(
+        "AesCmac tag doesn't match; sequence of update() lengths that lead to the failure: ");
+
+    for (int i = 0; i < 1000000 && read < t.message.length; i++) {
+      // The upper bound is exclusive, hence the +1.
+      int toRead = Random.randInt(t.message.length - read + 1);
+
+      debugReadSequence.append(toRead);
+      if (read + toRead < t.message.length) {
+        debugReadSequence.append(", ");
+      }
+
+      macComputation.update(ByteBuffer.wrap(Arrays.copyOfRange(t.message, read, read + toRead)));
+      read += toRead;
+    }
+
+    if (read < t.message.length) {
+      debugReadSequence.append(t.message.length - read);
+      macComputation.update(ByteBuffer.wrap(Arrays.copyOfRange(t.message, read, t.message.length)));
+    }
+
+    try {
+      assertThat(t.tag).isEqualTo(macComputation.computeMac());
+    } catch (AssertionError e) {
+      throw new AssertionError(debugReadSequence.toString(), e);
+    }
+  }
+
+  @Theory
+  public void testTagModificationAfterCreateVerification(
+      @FromDataPoints("implementationTestVectors") AesCmacTestVector t)
+      throws Exception {
+    assumeFalse(TinkFips.useOnlyFips());
+
+    ChunkedMac mac = new ChunkedAesCmacImpl(t.key);
+
+    byte[] mutableTag = Arrays.copyOf(t.tag, t.tag.length);
+    ChunkedMacVerification macVerification = mac.createVerification(mutableTag);
+    mutableTag[0] ^= (byte) 0x01;
+    macVerification.update(ByteBuffer.wrap(t.message));
+    macVerification.verifyMac();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/mac/internal/ChunkedHmacTest.java b/java_src/src/test/java/com/google/crypto/tink/mac/internal/ChunkedHmacTest.java
new file mode 100644
index 0000000..11077bd
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/mac/internal/ChunkedHmacTest.java
@@ -0,0 +1,530 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.mac.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.config.TinkFips;
+import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.mac.ChunkedMac;
+import com.google.crypto.tink.mac.ChunkedMacComputation;
+import com.google.crypto.tink.mac.ChunkedMacVerification;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.mac.HmacParameters.HashType;
+import com.google.crypto.tink.mac.HmacParameters.Variant;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.util.SecretBytes;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.Security;
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import org.conscrypt.Conscrypt;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public class ChunkedHmacTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    MacConfig.register();
+
+    // If Tink is built in FIPS-only mode, register Conscrypt for the tests.
+    if (TinkFips.useOnlyFips()) {
+      try {
+        Conscrypt.checkAvailability();
+        Security.addProvider(Conscrypt.newProvider());
+      } catch (Throwable cause) {
+        throw new IllegalStateException(
+            "Cannot test HMAC in FIPS-mode without Conscrypt Provider", cause);
+      }
+    }
+  }
+
+  private static class ChunkedHmacTestVector {
+    public final HmacKey key;
+    public final byte[] message;
+    public final byte[] tag;
+
+    public ChunkedHmacTestVector(
+        Variant variant,
+        @Nullable Integer id,
+        HashType hashType,
+        String key,
+        String message,
+        int tagSizeBytes,
+        String tag) {
+      this.key = createHmacKey(key, tagSizeBytes, variant, id, hashType);
+      this.message = Hex.decode(message);
+      this.tag = Hex.decode(tag);
+    }
+  }
+
+  private static HmacParameters createHmacParameters(
+      int keySizeBytes, int fullTagSizeBytes, Variant variant, HashType hashType) {
+    try {
+      return HmacParameters.builder()
+          .setKeySizeBytes(keySizeBytes)
+          .setTagSizeBytes(fullTagSizeBytes)
+          .setVariant(variant)
+          .setHashType(hashType)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException("Incorrect parameters creation arguments", e);
+    }
+  }
+
+  private static HmacKey createHmacKey(
+      String key,
+      int tagSizeBytes,
+      Variant variant,
+      @Nullable Integer idRequirement,
+      HashType hashType) {
+    try {
+      return HmacKey.builder()
+          .setKeyBytes(SecretBytes.copyFrom(Hex.decode(key), InsecureSecretKeyAccess.get()))
+          .setParameters(
+              createHmacParameters(
+                  Hex.decode(key).length,
+                  tagSizeBytes,
+                  variant,
+                  hashType))
+          .setIdRequirement(idRequirement)
+          .build();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException("Incorrect key creation arguments", e);
+    }
+  }
+
+  // Test data from http://csrc.nist.gov/groups/STM/cavp/message-authentication.html#testing
+  // and https://tools.ietf.org/html/rfc4231.
+  @DataPoints("hmacTestVectors")
+  public static final ChunkedHmacTestVector[] HMAC_TEST_VECTORS = {
+    new ChunkedHmacTestVector(
+        Variant.NO_PREFIX,
+        null,
+        HashType.SHA1,
+        "816aa4c3ee066310ac1e6666cf830c375355c3c8ba18cfe1f50a48c988b46272",
+        "220248f5e6d7a49335b3f91374f18bb8b0ff5e8b9a5853f3cfb293855d78301d837a0a2eb9e4f056f06c08361"
+            + "bd07180ee802651e69726c28910d2baef379606815dcbab01d0dc7acb0ba8e65a2928130da0522f2b2b3d05260"
+            + "885cf1c64f14ca3145313c685b0274bf6a1cb38e4f99895c6a8cc72fbe0e52c01766fede78a1a",
+        16,
+        "17cb2e9e98b748b5ae0f7078ea5519e5"),
+    new ChunkedHmacTestVector(
+        Variant.NO_PREFIX,
+        null,
+        HashType.SHA256,
+        "6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245",
+        "752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27"
+            + "587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b"
+            + "58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b",
+        16,
+        "05d1243e6465ed9620c9aec1c351a186"),
+    new ChunkedHmacTestVector(
+        Variant.NO_PREFIX,
+        null,
+        HashType.SHA384,
+        "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+        "4869205468657265",
+        48,
+        "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6"),
+    new ChunkedHmacTestVector(
+        Variant.NO_PREFIX,
+        null,
+        HashType.SHA512,
+        "726374c4b8df517510db9159b730f93431e0cd468d4f3821eab0edb93abd0fba46ab4f1ef35d54fec3d85fa89e"
+            + "f72ff3d35f22cf5ab69e205c10afcdf4aaf11338dbb12073474fddb556e60b8ee52f91163ba314303ee0c910e6"
+            + "4e87fbf302214edbe3f2",
+        "ac939659dc5f668c9969c0530422e3417a462c8b665e8db25a883a625f7aa59b89c5ad0ece5712ca17442d1798"
+            + "c6dea25d82c5db260cb59c75ae650be56569c1bd2d612cc57e71315917f116bbfa65a0aeb8af7840ee83d3e710"
+            + "1c52cf652d2773531b7a6bdd690b846a741816c860819270522a5b0cdfa1d736c501c583d916",
+        32,
+        "bd3d2df6f9d284b421a43e5f9cb94bc4ff88a88243f1f0133bad0fb1791f6569"),
+  };
+
+  @DataPoints("prefixedKeyTypes")
+  public static final ChunkedHmacTestVector[] PREFIXED_KEY_TYPES = {
+      new ChunkedHmacTestVector(
+          Variant.LEGACY,
+          1234,
+          HashType.SHA1,
+          "816aa4c3ee066310ac1e6666cf830c375355c3c8ba18cfe1f50a48c988b46272",
+          "220248f5e6d7a49335b3f91374f18bb8b0ff5e8b9a5853f3cfb293855d78301d837a0a2eb9e4f056f06c08361"
+              + "bd07180ee802651e69726c28910d2baef379606815dcbab01d0dc7acb0ba8e65a2928130da0522f2b2b3d05260"
+              + "885cf1c64f14ca3145313c685b0274bf6a1cb38e4f99895c6a8cc72fbe0e52c01766fede78a1a",
+          16,
+          "00000004d20c2676610ded1bce1967ec654526ca7b"),
+      new ChunkedHmacTestVector(
+          Variant.TINK,
+          1234,
+          HashType.SHA256,
+          "6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245",
+          "752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27"
+              + "587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b"
+              + "58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b",
+          16,
+          "01000004d205d1243e6465ed9620c9aec1c351a186"),
+      new ChunkedHmacTestVector(
+          Variant.CRUNCHY,
+          1234,
+          HashType.SHA384,
+          "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+          "4869205468657265",
+          48,
+          "00000004d2afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6"),
+      new ChunkedHmacTestVector(
+          Variant.TINK,
+          1234,
+          HashType.SHA512,
+          "726374c4b8df517510db9159b730f93431e0cd468d4f3821eab0edb93abd0fba46ab4f1ef35d54fec3d85fa89e"
+              + "f72ff3d35f22cf5ab69e205c10afcdf4aaf11338dbb12073474fddb556e60b8ee52f91163ba314303ee0c910e6"
+              + "4e87fbf302214edbe3f2",
+          "ac939659dc5f668c9969c0530422e3417a462c8b665e8db25a883a625f7aa59b89c5ad0ece5712ca17442d1798"
+              + "c6dea25d82c5db260cb59c75ae650be56569c1bd2d612cc57e71315917f116bbfa65a0aeb8af7840ee83d3e710"
+              + "1c52cf652d2773531b7a6bdd690b846a741816c860819270522a5b0cdfa1d736c501c583d916",
+          32,
+          "01000004d2bd3d2df6f9d284b421a43e5f9cb94bc4ff88a88243f1f0133bad0fb1791f6569"),
+  };
+
+  @DataPoints("createVerificationFailsFast")
+  public static final ChunkedHmacTestVector[] CREATE_VERIFICATION_FAILS_FAST = {
+      new ChunkedHmacTestVector( // Wrong prefix.
+          Variant.LEGACY,
+          1234,
+          HashType.SHA1,
+          "816aa4c3ee066310ac1e6666cf830c375355c3c8ba18cfe1f50a48c988b46272",
+          "220248f5e6d7a49335b3f91374f18bb8b0ff5e8b9a5853f3cfb293855d78301d837a0a2eb9e4f056f06c08361"
+              + "bd07180ee802651e69726c28910d2baef379606815dcbab01d0dc7acb0ba8e65a2928130da0522f2b2b3d05260"
+              + "885cf1c64f14ca3145313c685b0274bf6a1cb38e4f99895c6a8cc72fbe0e52c01766fede78a1a",
+          16,
+          "17cb2e9e98b748b5ae0f7078ea5519e5"),
+      new ChunkedHmacTestVector( // Wrong prefix.
+          Variant.TINK,
+          1234,
+          HashType.SHA256,
+          "6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245",
+          "752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27"
+              + "587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b"
+              + "58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b",
+          16,
+          "01075505d1243e6465ed9620c9aec1c351a186"),
+      new ChunkedHmacTestVector( // Tag too short.
+          Variant.CRUNCHY,
+          1234,
+          HashType.SHA384,
+          "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+          "4869205468657265",
+          16,
+          "afd0"),
+  };
+
+  @Theory
+  public void testComputationVerification_includeKeyPrefixWhenPresent(
+      @FromDataPoints("prefixedKeyTypes") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+    ChunkedHmacImpl chunkedHmacImpl = new ChunkedHmacImpl(t.key);
+
+    try {
+      ChunkedHmacComputation chunkedHmacComputation =
+          (ChunkedHmacComputation) chunkedHmacImpl.createComputation();
+      chunkedHmacComputation.update(ByteBuffer.wrap(t.message).asReadOnlyBuffer());
+      assertThat(t.tag).isEqualTo(chunkedHmacComputation.computeMac());
+    } catch (GeneralSecurityException e) {
+      throw new AssertionError("Valid computation, should not throw exception", e);
+    }
+
+    try {
+      ChunkedHmacVerification chunkedHmacVerification =
+          (ChunkedHmacVerification) chunkedHmacImpl.createVerification(t.tag);
+      chunkedHmacVerification.update(ByteBuffer.wrap(t.message));
+      chunkedHmacVerification.verifyMac();
+    } catch (GeneralSecurityException e) {
+      throw new AssertionError("Valid tag, verification should not throw exception", e);
+    }
+  }
+
+  @Theory
+  public void testCreateVerificationFailsFast(
+      @FromDataPoints("createVerificationFailsFast") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+    assertThrows(GeneralSecurityException.class, () -> mac.createVerification(t.tag));
+  }
+
+  @Theory
+  public void testComputationVerification_computeTagCorrectly(
+      @FromDataPoints("hmacTestVectors") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+    ChunkedHmacImpl chunkedHmacImpl = new ChunkedHmacImpl(t.key);
+
+    try {
+      ChunkedHmacComputation chunkedHmacComputation =
+          (ChunkedHmacComputation) chunkedHmacImpl.createComputation();
+      chunkedHmacComputation.update(ByteBuffer.wrap(t.message).asReadOnlyBuffer());
+      assertThat(t.tag).isEqualTo(chunkedHmacComputation.computeMac());
+    } catch (GeneralSecurityException e) {
+      throw new AssertionError("Valid computation, should not throw exception", e);
+    }
+
+    try {
+      ChunkedHmacVerification chunkedHmacVerification =
+          (ChunkedHmacVerification) chunkedHmacImpl.createVerification(t.tag);
+      chunkedHmacVerification.update(ByteBuffer.wrap(t.message));
+      chunkedHmacVerification.verifyMac();
+    } catch (GeneralSecurityException e) {
+      throw new AssertionError("Valid tag, verification should not throw exception", e);
+    }
+  }
+
+  // The SHA224 parameters are omitted since they seem to be unavailable on Android.
+  @DataPoints("parameters")
+  public static final HmacParameters[] PARAMETERS = {
+      createHmacParameters(16, 10, Variant.NO_PREFIX, HashType.SHA1),
+      createHmacParameters(16, 10, Variant.NO_PREFIX, HashType.SHA256),
+      createHmacParameters(16, 10, Variant.NO_PREFIX, HashType.SHA384),
+      createHmacParameters(16, 10, Variant.NO_PREFIX, HashType.SHA512),
+
+      createHmacParameters(16, 10, Variant.TINK, HashType.SHA1),
+      createHmacParameters(16, 10, Variant.TINK, HashType.SHA256),
+      createHmacParameters(16, 10, Variant.TINK, HashType.SHA384),
+      createHmacParameters(16, 10, Variant.TINK, HashType.SHA512),
+
+      createHmacParameters(16, 10, Variant.CRUNCHY, HashType.SHA1),
+      createHmacParameters(16, 10, Variant.CRUNCHY, HashType.SHA256),
+      createHmacParameters(16, 10, Variant.CRUNCHY, HashType.SHA384),
+      createHmacParameters(16, 10, Variant.CRUNCHY, HashType.SHA512),
+
+      createHmacParameters(16, 10, Variant.LEGACY, HashType.SHA1),
+      createHmacParameters(16, 10, Variant.LEGACY, HashType.SHA256),
+      createHmacParameters(16, 10, Variant.LEGACY, HashType.SHA384),
+      createHmacParameters(16, 10, Variant.LEGACY, HashType.SHA512),
+  };
+
+  @Theory
+  public void testCompatibility(@FromDataPoints("parameters") HmacParameters params)
+      throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+    KeysetHandle keysetHandle =
+        KeysetHandle
+            .newBuilder()
+            .addEntry(
+                KeysetHandle
+                    .generateEntryFromParameters(params)
+                    .withFixedId(1234)
+                    .makePrimary()
+            ).build();
+    Mac mac = keysetHandle.getPrimitive(Mac.class);
+    HmacKey key = (HmacKey) keysetHandle.getAt(0).getKey();
+    ChunkedMac chunkedMac = new ChunkedHmacImpl(key);
+    ChunkedMacComputation chunkedMacComputation = chunkedMac.createComputation();
+
+    byte[] testData = new byte[] {1, 2, 3, 4, 5, 6, 7, 8};
+    chunkedMacComputation.update(ByteBuffer.wrap(testData));
+    assertThat(mac.computeMac(testData)).isEqualTo(chunkedMacComputation.computeMac());
+  }
+
+  @Test
+  public void testFailsIfFipsModuleNotAvailable() {
+    assumeTrue(TinkFips.useOnlyFips() && !TinkFipsUtil.fipsModuleAvailable());
+    assertThrows(
+        GeneralSecurityException.class, () -> new ChunkedHmacImpl(HMAC_TEST_VECTORS[0].key));
+  }
+
+  @Theory
+  public void testTagTruncation_failsVerifyMac(
+      @FromDataPoints("hmacTestVectors") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+
+    for (int j = 1; j < t.tag.length; j++) {
+      byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length - j);
+      ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+      macVerification.update(ByteBuffer.wrap(t.message));
+      assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+    }
+
+    // Test with random keys.
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(
+                createHmacParameters(16, 16, Variant.NO_PREFIX, HashType.SHA1))
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    mac = new ChunkedHmacImpl(key);
+    for (int j = 1; j < t.tag.length; j++) {
+      byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length - j);
+      ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+      macVerification.update(ByteBuffer.wrap(t.message));
+      assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+    }
+  }
+
+  @Theory
+  public void testBitFlipMessage_failsVerifyMac(
+      @FromDataPoints("hmacTestVectors") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+    for (int b = 0; b < t.message.length; b++) {
+      for (int bit = 0; bit < 8; bit++) {
+        byte[] modifiedMessage = Arrays.copyOf(t.message, t.message.length);
+        modifiedMessage[b] = (byte) (modifiedMessage[b] ^ (1 << bit));
+        ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+        macVerification.update(ByteBuffer.wrap(modifiedMessage));
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+      }
+    }
+
+    // Test with random keys.
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(createHmacParameters(16, 16, Variant.NO_PREFIX, HashType.SHA1))
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    mac = new ChunkedHmacImpl(key);
+    for (int b = 0; b < t.message.length; b++) {
+      for (int bit = 0; bit < 8; bit++) {
+        byte[] modifiedMessage = Arrays.copyOf(t.message, t.message.length);
+        modifiedMessage[b] = (byte) (modifiedMessage[b] ^ (1 << bit));
+        ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+        macVerification.update(ByteBuffer.wrap(modifiedMessage));
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+      }
+    }
+  }
+
+  @Theory
+  public void testBitFlipTag_failsVerifyMac(
+      @FromDataPoints("hmacTestVectors") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+    for (int b = 0; b < t.tag.length; b++) {
+      for (int bit = 0; bit < 8; bit++) {
+        byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length);
+        modifiedTag[b] = (byte) (modifiedTag[b] ^ (1 << bit));
+        ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+        macVerification.update(ByteBuffer.wrap(t.message));
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+      }
+    }
+
+    // Test with random keys.
+    HmacKey key =
+        HmacKey.builder()
+            .setParameters(createHmacParameters(16, 16, Variant.NO_PREFIX, HashType.SHA1))
+            .setKeyBytes(SecretBytes.randomBytes(16))
+            .build();
+    mac = new ChunkedHmacImpl(key);
+    for (int b = 0; b < t.tag.length; b++) {
+      for (int bit = 0; bit < 8; bit++) {
+        byte[] modifiedTag = Arrays.copyOf(t.tag, t.tag.length);
+        modifiedTag[b] = (byte) (modifiedTag[b] ^ (1 << bit));
+        ChunkedMacVerification macVerification = mac.createVerification(modifiedTag);
+        macVerification.update(ByteBuffer.wrap(t.message));
+        assertThrows(GeneralSecurityException.class, macVerification::verifyMac);
+      }
+    }
+  }
+
+  @Test
+  public void testUpdateAfterFinalize_throwsInComputationVerification() throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+    ChunkedHmacTestVector t = HMAC_TEST_VECTORS[0];
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+
+    ChunkedMacComputation macComputation = mac.createComputation();
+    macComputation.update(ByteBuffer.wrap(t.message));
+    assertThat(t.tag).isEqualTo(macComputation.computeMac());
+    assertThrows(
+        IllegalStateException.class,
+        () -> macComputation.update(ByteBuffer.wrap(t.message)));
+    assertThrows(IllegalStateException.class, macComputation::computeMac);
+
+    ChunkedMacVerification macVerification = mac.createVerification(t.tag);
+    macVerification.update(ByteBuffer.wrap(t.message));
+    macVerification.verifyMac();
+    assertThrows(
+        IllegalStateException.class,
+        () -> macVerification.update(ByteBuffer.wrap(t.message)));
+    assertThrows(IllegalStateException.class, macVerification::verifyMac);
+  }
+
+  @Theory
+  public void testRandomized(@FromDataPoints("hmacTestVectors") ChunkedHmacTestVector t)
+      throws Exception {
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+    ChunkedMacComputation macComputation = mac.createComputation();
+
+    int read = 0;
+    StringBuilder debugReadSequence = new StringBuilder();
+    debugReadSequence.append(
+        "Hmac tag doesn't match; sequence of update() lengths that lead to the failure: ");
+
+    for (int i = 0; i < 1000000 && read < t.message.length; i++) {
+      // The upper bound is exclusive, hence the +1.
+      int toRead = Random.randInt(t.message.length - read + 1);
+
+      debugReadSequence.append(toRead);
+      if (read + toRead < t.message.length) {
+        debugReadSequence.append(", ");
+      }
+
+      macComputation.update(ByteBuffer.wrap(Arrays.copyOfRange(t.message, read, read + toRead)));
+      read += toRead;
+    }
+
+    if (read < t.message.length) {
+      debugReadSequence.append(t.message.length - read);
+      macComputation.update(ByteBuffer.wrap(Arrays.copyOfRange(t.message, read, t.message.length)));
+    }
+
+    try {
+      assertThat(t.tag).isEqualTo(macComputation.computeMac());
+    } catch (AssertionError e) {
+      throw new AssertionError(debugReadSequence.toString(), e);
+    }
+  }
+
+  @Theory
+  public void testCreateVerification_copiesInputParameters(
+      @FromDataPoints("hmacTestVectors") ChunkedHmacTestVector t) throws Exception {
+    assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
+
+    ChunkedMac mac = new ChunkedHmacImpl(t.key);
+    byte[] mutableTag = Arrays.copyOf(t.tag, t.tag.length);
+    ChunkedMacVerification macVerification = mac.createVerification(mutableTag);
+    mutableTag[0] ^= (byte) 0x01;
+    macVerification.update(ByteBuffer.wrap(t.message));
+
+    macVerification.verifyMac();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/monitoring/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/monitoring/BUILD.bazel
index 30958df..a451e69 100644
--- a/java_src/src/test/java/com/google/crypto/tink/monitoring/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/monitoring/BUILD.bazel
@@ -12,7 +12,7 @@
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "//src/main/java/com/google/crypto/tink/monitoring:monitoring_keyset_info",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringAnnotationsTest.java b/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringAnnotationsTest.java
index c5215a0..dc96414 100644
--- a/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringAnnotationsTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringAnnotationsTest.java
@@ -77,7 +77,7 @@
   public void builderIsInvalidAfterBuild() throws Exception {
     MonitoringAnnotations.Builder builder =
         MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value");
-    builder.build();
+    Object unused = builder.build();
     assertThrows(
         IllegalStateException.class, () -> builder.add("annotation_name2", "annotation_value2"));
     HashMap<String, String> newAnnotations = new HashMap<>();
diff --git a/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfoTest.java b/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfoTest.java
index ef9df08..b4aef3b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfoTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/monitoring/MonitoringKeysetInfoTest.java
@@ -49,25 +49,25 @@
 
   @Test
   public void addAndGetEntry() throws Exception {
-    Parameters parameters = makeLegacyProtoParameters("typeUrl123");
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .build();
     assertThat(info.getEntries()).hasSize(1);
     MonitoringKeysetInfo.Entry entry = info.getEntries().get(0);
     assertThat(entry.getStatus()).isEqualTo(KeyStatus.ENABLED);
     assertThat(entry.getKeyId()).isEqualTo(123);
-    assertThat(entry.getParameters()).isEqualTo(parameters);
+    assertThat(entry.getKeyType()).isEqualTo("typeUrl123");
+    assertThat(entry.getKeyPrefix()).isEqualTo("TINK");
   }
 
   @Test
   public void addEntries() throws Exception  {
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     assertThat(info.getEntries()).hasSize(2);
@@ -77,8 +77,8 @@
   public void addSameEntryTwice() throws Exception  {
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .build();
     // entries are a list, so we can add the same entry twice.
@@ -99,7 +99,7 @@
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
             .setAnnotations(monitoringAnnotations)
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .build();
     HashMap<String, String> expected = new HashMap<>();
@@ -114,7 +114,7 @@
   public void primaryIsNullIfItIsNotSet() throws Exception  {
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .build();
     assertThat(info.getPrimaryKeyId()).isNull();
   }
@@ -125,7 +125,7 @@
         GeneralSecurityException.class,
         () ->
             MonitoringKeysetInfo.newBuilder()
-                .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+                .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
                 .setPrimaryKeyId(124)
                 .build());
     assertThrows(
@@ -140,7 +140,7 @@
   public void entriesAreNotModifiable() throws Exception {
     MonitoringKeysetInfo info =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .setAnnotations(
                 MonitoringAnnotations.newBuilder()
@@ -149,7 +149,7 @@
             .build();
     MonitoringKeysetInfo info2 =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(234)
             .build();
     assertThrows(
@@ -166,14 +166,14 @@
         MonitoringAnnotations.newBuilder().add("annotation_name2", "annotation_value2").build();
     MonitoringKeysetInfo.Builder builder =
         MonitoringKeysetInfo.newBuilder()
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .setAnnotations(annotations);
-    builder.build();
+    Object unused = builder.build();
     assertThrows(IllegalStateException.class, () -> builder.setAnnotations(annotations));
     assertThrows(
         IllegalStateException.class,
-        () -> builder.addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234")));
+        () -> builder.addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK"));
     assertThrows(IllegalStateException.class, () -> builder.setPrimaryKeyId(123));
   }
 
@@ -185,16 +185,15 @@
                 MonitoringAnnotations.newBuilder()
                     .add("annotation_name1", "annotation_value1")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.DISABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.DISABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     assertThat(info.toString())
         .isEqualTo(
             "(annotations={annotation_name1=annotation_value1}, entries=[(status=ENABLED,"
-                + " keyId=123, parameters='(typeUrl=typeUrl123, outputPrefixType=TINK)'),"
-                + " (status=DISABLED, keyId=234, parameters='(typeUrl=typeUrl234,"
-                + " outputPrefixType=TINK)')], primaryKeyId=123)");
+                + " keyId=123, keyType='typeUrl123', keyPrefix='TINK'), (status=DISABLED,"
+                + " keyId=234, keyType='typeUrl234', keyPrefix='TINK')], primaryKeyId=123)");
   }
 
   @Test
@@ -206,8 +205,8 @@
                     .add("annotation_name1", "annotation_value1")
                     .add("annotation_name2", "annotation_value2")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringKeysetInfo infoWithAnnotationsInOtherOrder =
@@ -217,8 +216,8 @@
                     .add("annotation_name2", "annotation_value2")
                     .add("annotation_name1", "annotation_value1")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringKeysetInfo infoWithEntriesInOtherOrder =
@@ -228,8 +227,8 @@
                     .add("annotation_name1", "annotation_value1")
                     .add("annotation_name2", "annotation_value2")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringKeysetInfo infoWithOtherAnnotations =
@@ -239,8 +238,8 @@
                     .add("annotation_name1", "annotation_value1")
                     .add("annotation_name3", "annotation_value3")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(123)
             .build();
     MonitoringKeysetInfo infoWithOtherPrimaryKeyId =
@@ -250,8 +249,8 @@
                     .add("annotation_name1", "annotation_value1")
                     .add("annotation_name2", "annotation_value2")
                     .build())
-            .addEntry(KeyStatus.ENABLED, 123, makeLegacyProtoParameters("typeUrl123"))
-            .addEntry(KeyStatus.ENABLED, 234, makeLegacyProtoParameters("typeUrl234"))
+            .addEntry(KeyStatus.ENABLED, 123, "typeUrl123", "TINK")
+            .addEntry(KeyStatus.ENABLED, 234, "typeUrl234", "TINK")
             .setPrimaryKeyId(234)
             .build();
     // annotations are a map. They can be added in any order.
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyManagerTest.java
index 49d5eb5..3137084 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyManagerTest.java
@@ -27,8 +27,8 @@
 import com.google.crypto.tink.subtle.PrfAesCmac;
 import com.google.crypto.tink.subtle.Random;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -40,6 +40,11 @@
   private final KeyTypeManager.KeyFactory<AesCmacPrfKeyFormat, AesCmacPrfKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    PrfConfig.register();
+  }
+
   @Test
   public void validateKeyFormat_empty() throws Exception {
     assertThrows(
@@ -140,13 +145,7 @@
   @Test
   public void testAes256CmacTemplate() throws Exception {
     KeyTemplate template = AesCmacPrfKeyManager.aes256CmacTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCmacPrfKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesCmacPrfKeyFormat format =
-        AesCmacPrfKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
+    assertThat(template.toParameters()).isEqualTo(AesCmacPrfParameters.create(32));
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyTest.java
new file mode 100644
index 0000000..cd866f1
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfKeyTest.java
@@ -0,0 +1,93 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class AesCmacPrfKeyTest {
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @Theory
+  public void createAndGetProperties_succeeds(@FromDataPoints("keySizes") int keySize)
+      throws Exception {
+    AesCmacPrfParameters parameters = AesCmacPrfParameters.create(keySize);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(keySize);
+    AesCmacPrfKey key = AesCmacPrfKey.create(parameters, keyBytes);
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void createWithKeySizeMismatch_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> AesCmacPrfKey.create(AesCmacPrfParameters.create(16), SecretBytes.randomBytes(32)));
+  }
+
+  @Test
+  public void equals() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    SecretBytes keyBytesCopy =
+        SecretBytes.copyFrom(
+            keyBytes.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    AesCmacPrfParameters parameters16 = AesCmacPrfParameters.create(16);
+    AesCmacPrfParameters parameters32 = AesCmacPrfParameters.create(32);
+
+    new KeyTester()
+        .addEqualityGroup(
+            "16-byte key",
+            AesCmacPrfKey.create(parameters16, keyBytes),
+            // Same key built twice.
+            AesCmacPrfKey.create(parameters16, keyBytes),
+            // Same key built with a copy of the key bytes.
+            AesCmacPrfKey.create(parameters16, keyBytesCopy))
+        .addEqualityGroup(
+            "16-byte random key bytes",
+            AesCmacPrfKey.create(parameters16, SecretBytes.randomBytes(16)))
+        .addEqualityGroup(
+            "32-byte random key bytes",
+            AesCmacPrfKey.create(parameters32, SecretBytes.randomBytes(32)))
+        .addEqualityGroup(
+            "different key class",
+            HkdfPrfKey.builder()
+                .setParameters(
+                    HkdfPrfParameters.builder()
+                        .setKeySizeBytes(16)
+                        .setHashType(HkdfPrfParameters.HashType.SHA256)
+                        .build())
+                .setKeyBytes(keyBytes)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfParametersTest.java
new file mode 100644
index 0000000..8d91c27
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfParametersTest.java
@@ -0,0 +1,79 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class AesCmacPrfParametersTest {
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @Theory
+  public void createParametersAndGetProperties(@FromDataPoints("keySizes") int keySize)
+      throws Exception {
+    AesCmacPrfParameters parameters = AesCmacPrfParameters.create(keySize);
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(keySize);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.toString())
+        .isEqualTo("AesCmac PRF Parameters (" + keySize + "-byte key)");
+  }
+
+  @Test
+  public void createWithUnsupportedKeySize_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> AesCmacPrfParameters.create(19));
+  }
+
+  @Theory
+  public void testEqualsAndHashCode(@FromDataPoints("keySizes") int keySize) throws Exception {
+    AesCmacPrfParameters parameters = AesCmacPrfParameters.create(keySize);
+    AesCmacPrfParameters sameParameters = AesCmacPrfParameters.create(keySize);
+
+    assertThat(sameParameters).isEqualTo(parameters);
+    assertThat(sameParameters.hashCode()).isEqualTo(parameters.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCode_different() throws Exception {
+    AesCmacPrfParameters parameters16 = AesCmacPrfParameters.create(16);
+    AesCmacPrfParameters parameters32 = AesCmacPrfParameters.create(32);
+
+    assertThat(parameters16).isNotEqualTo(parameters32);
+    assertThat(parameters16.hashCode()).isNotEqualTo(parameters32.hashCode());
+  }
+
+  @Test
+  @SuppressWarnings("TruthIncompatibleType")
+  public void testEqualDifferentClass() throws Exception {
+    AesCmacPrfParameters aesCmacPrfParameters = AesCmacPrfParameters.create(16);
+    HkdfPrfParameters hkdfPrfParameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .build();
+    assertThat(aesCmacPrfParameters).isNotEqualTo(hkdfPrfParameters);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfProtoSerializationTest.java
new file mode 100644
index 0000000..46797d9
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/AesCmacPrfProtoSerializationTest.java
@@ -0,0 +1,247 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesCmacPrfProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.AesCmacPrfKey";
+
+  private static final SecretBytes KEY_BYTES_16 = SecretBytes.randomBytes(16);
+  private static final ByteString KEY_BYTES_16_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesCmacPrfProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesCmacPrfProtoSerialization.register(registry);
+    AesCmacPrfProtoSerialization.register(registry);
+  }
+
+  @Theory
+  public void serializeAndParseParameters(@FromDataPoints("keySizes") int keySize)
+      throws Exception {
+    AesCmacPrfParameters parameters = AesCmacPrfParameters.create(keySize);
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCmacPrfKeyFormat.newBuilder()
+                .setKeySize(keySize)
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCmacPrfKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Theory
+  public void serializeAndParseKey(@FromDataPoints("keySizes") int keySize) throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(keySize);
+    AesCmacPrfKey key = AesCmacPrfKey.create(AesCmacPrfParameters.create(keySize), keyBytes);
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(
+                    ByteString.copyFrom(keyBytes.toByteArray(InsecureSecretKeyAccess.get())))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCmacPrfKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testSerializeKey_noAccess_fails() throws Exception {
+    AesCmacPrfKey key = AesCmacPrfKey.create(AesCmacPrfParameters.create(16), KEY_BYTES_16);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @Test
+  public void testParseKey_noAccess_fails() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Invalid type URL.
+        ProtoParametersSerialization.create(
+            "i.am.a.random.type.url",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCmacPrfKeyFormat.newBuilder().setKeySize(16).build()),
+
+        // Invalid output prefix type.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.AesCmacPrfKeyFormat.newBuilder().setKeySize(16).build()),
+
+        // Invalid key size.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCmacPrfKeyFormat.newBuilder().setKeySize(12).build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Invalid type URL.
+        ProtoKeySerialization.create(
+            "i.am.a.random.type.url",
+            com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid version.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid key size.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[12]))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid proto encoding.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid output prefix type.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.AesCmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            123),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/prf/BUILD.bazel
index aae603d..00fd27b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/BUILD.bazel
@@ -8,9 +8,13 @@
         "//proto:common_java_proto",
         "//proto:hkdf_prf_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:predefined_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
         "//src/main/java/com/google/crypto/tink/prf:prf_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -22,12 +26,22 @@
     srcs = ["PrfSetWrapperTest.java"],
     deps = [
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_monitoring_registry",
+        "//src/main/java/com/google/crypto/tink/internal/testing:fake_monitoring_client",
+        "//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
         "//src/main/java/com/google/crypto/tink/prf:prf_config",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "//src/main/java/com/google/crypto/tink/prf:prf_set_wrapper",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -42,11 +56,13 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -62,12 +78,14 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -84,13 +102,16 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key_manager",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle/prf:hkdf_streaming_prf",
         "//src/main/java/com/google/crypto/tink/subtle/prf:streaming_prf",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -109,3 +130,203 @@
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "PrfTest",
+    size = "small",
+    srcs = ["PrfTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
+        "//src/main/java/com/google/crypto/tink/prf:prf_set",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HkdfPrfParametersTest",
+    size = "small",
+    srcs = ["HkdfPrfParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HkdfPrfKeyTest",
+    size = "small",
+    srcs = ["HkdfPrfKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_key",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HkdfPrfProtoSerializationTest",
+    size = "small",
+    srcs = ["HkdfPrfProtoSerializationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:hkdf_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HmacPrfParametersTest",
+    size = "small",
+    srcs = ["HmacPrfParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCmacPrfParametersTest",
+    size = "small",
+    srcs = ["AesCmacPrfParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HmacPrfKeyTest",
+    size = "small",
+    srcs = ["HmacPrfKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCmacPrfKeyTest",
+    size = "small",
+    srcs = ["AesCmacPrfKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hkdf_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "HmacPrfProtoSerializationTest",
+    size = "small",
+    srcs = ["HmacPrfProtoSerializationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:hmac_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCmacPrfProtoSerializationTest",
+    size = "small",
+    srcs = ["AesCmacPrfProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_cmac_prf_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PredefinedPrfParametersTest",
+    size = "small",
+    srcs = ["PredefinedPrfParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/prf:predefined_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_config",
+        "//src/main/java/com/google/crypto/tink/prf:prf_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyManagerTest.java
index 167bb4d..60f1f7c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyManagerTest.java
@@ -32,10 +32,11 @@
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.prf.HkdfStreamingPrf;
 import com.google.crypto.tink.subtle.prf.StreamingPrf;
+import com.google.crypto.tink.util.Bytes;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -47,6 +48,11 @@
   private final KeyTypeManager.KeyFactory<HkdfPrfKeyFormat, HkdfPrfKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    PrfConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType()).isEqualTo("type.googleapis.com/google.crypto.tink.HkdfPrfKey");
@@ -256,19 +262,19 @@
                     .setSalt(ByteString.copyFromUtf8("some salt"))
                     .setHash(HashType.SHA256))
             .build();
-    manager.getPrimitive(key, Prf.class);
+    Object unused = manager.getPrimitive(key, Prf.class);
   }
 
   @Test
   public void testHkdfSha256Template() throws Exception {
     KeyTemplate kt = HkdfPrfKeyManager.hkdfSha256Template();
-    assertThat(kt.getTypeUrl()).isEqualTo(new HkdfPrfKeyManager().getKeyType());
-    assertThat(kt.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-
-    HkdfPrfKeyFormat format =
-        HkdfPrfKeyFormat.parseFrom(kt.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA256);
+    assertThat(kt.toParameters())
+        .isEqualTo(
+            HkdfPrfParameters.builder()
+                .setKeySizeBytes(32)
+                .setHashType(HkdfPrfParameters.HashType.SHA256)
+                .setSalt(Bytes.copyFrom(new byte[] {}))
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyTest.java
new file mode 100644
index 0000000..2a4b584
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfKeyTest.java
@@ -0,0 +1,175 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.mac.HmacKey;
+import com.google.crypto.tink.mac.HmacParameters;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HkdfPrfKeyTest {
+
+  private static final Bytes SALT = Bytes.copyFrom(Hex.decode("2023af"));
+  private static HkdfPrfParameters parameters16;
+
+  @BeforeClass
+  public static void setUpParameters() throws Exception {
+    parameters16 =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(SALT)
+            .build();
+  }
+
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @Theory
+  public void build_succeeds(@FromDataPoints("keySizes") int keySize) throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(keySize)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(SALT)
+            .build();
+    Object unused =
+        HkdfPrfKey.builder()
+            .setParameters(parameters)
+            .setKeyBytes(SecretBytes.randomBytes(keySize))
+            .build();
+  }
+
+  @Test
+  public void buildWithoutSettingParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HkdfPrfKey.builder().setKeyBytes(SecretBytes.randomBytes(16)).build());
+  }
+
+  @Test
+  public void buildWithoutSettingKeyBytes_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HkdfPrfKey.builder().setParameters(parameters16).build());
+  }
+
+  @Test
+  public void buildWithKeySizeMismatch_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HkdfPrfKey.builder()
+                .setParameters(parameters16)
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+  }
+
+  @Test
+  public void getKeyBytes() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    assertThat(
+            HkdfPrfKey.builder()
+                .setParameters(parameters16)
+                .setKeyBytes(keyBytes)
+                .build()
+                .getKeyBytes())
+        .isEqualTo(keyBytes);
+  }
+
+  @Test
+  public void getParameters() throws Exception {
+    assertThat(
+            HkdfPrfKey.builder()
+                .setParameters(parameters16)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build()
+                .getParameters())
+        .isEqualTo(parameters16);
+  }
+
+  @Test
+  public void getIdRequirementOrNull() throws Exception {
+    assertThat(
+            HkdfPrfKey.builder()
+                .setParameters(parameters16)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build()
+                .getIdRequirementOrNull())
+        .isNull();
+  }
+
+  @Test
+  public void equals() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    SecretBytes keyBytesCopy =
+        SecretBytes.copyFrom(
+            keyBytes.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    HkdfPrfParameters parameters32 =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(32)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(SALT)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "16-byte set key bytes",
+            HkdfPrfKey.builder().setParameters(parameters16).setKeyBytes(keyBytes).build(),
+            HkdfPrfKey.builder().setParameters(parameters16).setKeyBytes(keyBytesCopy).build())
+        .addEqualityGroup(
+            "16-byte random key bytes",
+            HkdfPrfKey.builder()
+                .setParameters(parameters16)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build())
+        .addEqualityGroup(
+            "32-byte random key bytes",
+            HkdfPrfKey.builder()
+                .setParameters(parameters32)
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .build())
+        .addEqualityGroup(
+            "different key class",
+            HmacKey.builder()
+                .setParameters(
+                    HmacParameters.builder()
+                        .setKeySizeBytes(16)
+                        .setTagSizeBytes(10)
+                        .setHashType(HmacParameters.HashType.SHA256)
+                        .setVariant(HmacParameters.Variant.NO_PREFIX)
+                        .build())
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfParametersTest.java
new file mode 100644
index 0000000..b94ecbc
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfParametersTest.java
@@ -0,0 +1,173 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HkdfPrfParametersTest {
+
+  private static final Bytes SALT = Bytes.copyFrom(Hex.decode("2023af"));
+
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @Theory
+  public void buildWithSalt_succeeds(@FromDataPoints("keySizes") int keySize) throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(keySize)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(SALT)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(keySize);
+    assertThat(parameters.getHashType()).isEqualTo(HkdfPrfParameters.HashType.SHA256);
+    assertThat(parameters.getSalt()).isEqualTo(SALT);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.toString())
+        .isEqualTo(
+            "HKDF PRF Parameters (hashType: SHA256, salt: Bytes(2023af), and "
+                + keySize
+                + "-byte key)");
+  }
+
+  @Test
+  public void buildWithoutSettingSalt_succeeds() throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .build();
+    assertThat(parameters.getSalt()).isNull();
+  }
+
+  @Test
+  public void buildWithEmptySalt_succeeds() throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(Bytes.copyFrom("".getBytes(UTF_8)))
+            .build();
+    assertThat(parameters.getSalt()).isNull();
+  }
+
+  @Test
+  public void clearSaltWithEmptyString_succeeds() throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(Bytes.copyFrom("Some Salt".getBytes(UTF_8)))
+            .setSalt(Bytes.copyFrom("".getBytes(UTF_8)))
+            .build();
+    assertThat(parameters.getSalt()).isNull();
+  }
+
+  @Test
+  public void buildWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HkdfPrfParameters.builder()
+                .setHashType(HkdfPrfParameters.HashType.SHA256)
+                .setSalt(SALT)
+                .build());
+  }
+
+  @Test
+  public void buildWithUnsupportedKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HkdfPrfParameters.builder()
+                .setKeySizeBytes(15)
+                .setHashType(HkdfPrfParameters.HashType.SHA256)
+                .setSalt(SALT)
+                .build());
+  }
+
+  @Test
+  public void buildWithoutSettingHashType_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HkdfPrfParameters.builder().setKeySizeBytes(16).setSalt(SALT).build());
+  }
+
+  @Test
+  public void equalsAndHashCode() throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(SALT)
+            .build();
+    HkdfPrfParameters sameParameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .setSalt(SALT)
+            .build();
+    assertThat(sameParameters).isEqualTo(parameters);
+    assertThat(sameParameters.hashCode()).isEqualTo(parameters.hashCode());
+
+    HkdfPrfParameters keySize32Parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(32)
+            .setHashType(parameters.getHashType())
+            .setSalt(parameters.getSalt())
+            .build();
+    assertThat(keySize32Parameters).isNotEqualTo(parameters);
+    assertThat(keySize32Parameters.hashCode()).isNotEqualTo(parameters.hashCode());
+
+    HkdfPrfParameters sha512Parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(parameters.getKeySizeBytes())
+            .setHashType(HkdfPrfParameters.HashType.SHA512)
+            .setSalt(parameters.getSalt())
+            .build();
+    assertThat(sha512Parameters).isNotEqualTo(parameters);
+    assertThat(sha512Parameters.hashCode()).isNotEqualTo(parameters.hashCode());
+
+    HkdfPrfParameters noSaltParameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(parameters.getKeySizeBytes())
+            .setHashType(parameters.getHashType())
+            .build();
+    HkdfPrfParameters sameNoSaltParameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(parameters.getKeySizeBytes())
+            .setHashType(parameters.getHashType())
+            .build();
+    assertThat(sameNoSaltParameters).isEqualTo(noSaltParameters);
+    assertThat(sameNoSaltParameters.hashCode()).isEqualTo(noSaltParameters.hashCode());
+    assertThat(noSaltParameters).isNotEqualTo(parameters);
+    assertThat(noSaltParameters.hashCode()).isNotEqualTo(parameters.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfProtoSerializationTest.java
new file mode 100644
index 0000000..872f345
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HkdfPrfProtoSerializationTest.java
@@ -0,0 +1,362 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class HkdfPrfProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.HkdfPrfKey";
+
+  private static final SecretBytes SECRET_16 = SecretBytes.randomBytes(16);
+  private static final ByteString SECRET_BYTE_STRING_16 =
+      ByteString.copyFrom(SECRET_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    HkdfPrfProtoSerialization.register(registry);
+  }
+
+  public static final class HashType {
+    public final HkdfPrfParameters.HashType parameters;
+    public final com.google.crypto.tink.proto.HashType proto;
+
+    public HashType(
+        HkdfPrfParameters.HashType parameters, com.google.crypto.tink.proto.HashType proto) {
+      this.parameters = parameters;
+      this.proto = proto;
+    }
+  }
+
+  @DataPoints("hashTypes")
+  public static final HashType[] HASH_TYPES =
+      new HashType[] {
+        new HashType(HkdfPrfParameters.HashType.SHA1, com.google.crypto.tink.proto.HashType.SHA1),
+        new HashType(
+            HkdfPrfParameters.HashType.SHA224, com.google.crypto.tink.proto.HashType.SHA224),
+        new HashType(
+            HkdfPrfParameters.HashType.SHA256, com.google.crypto.tink.proto.HashType.SHA256),
+        new HashType(
+            HkdfPrfParameters.HashType.SHA384, com.google.crypto.tink.proto.HashType.SHA384),
+        new HashType(
+            HkdfPrfParameters.HashType.SHA512, com.google.crypto.tink.proto.HashType.SHA512),
+      };
+
+  @DataPoints("salts")
+  public static final Bytes[] SALTS =
+      new Bytes[] {Bytes.copyFrom(Hex.decode("2023af")), Bytes.copyFrom(Hex.decode(""))};
+
+  @Theory
+  public void serializeAndParseParameters(
+      @FromDataPoints("hashTypes") HashType hashType, @FromDataPoints("salts") Bytes salt)
+      throws Exception {
+    HkdfPrfParameters parameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(hashType.parameters)
+            .setSalt(salt)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HkdfPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(hashType.proto)
+                        .setSalt(ByteString.copyFrom(salt.toByteArray()))
+                        .build())
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HkdfPrfKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Theory
+  public void serializeAndParseKey(
+      @FromDataPoints("hashTypes") HashType hashType, @FromDataPoints("salts") Bytes salt)
+      throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.builder()
+            .setParameters(
+                HkdfPrfParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setHashType(hashType.parameters)
+                    .setSalt(salt)
+                    .build())
+            .setKeyBytes(SECRET_16)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(SECRET_BYTE_STRING_16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(hashType.proto)
+                        .setSalt(ByteString.copyFrom(salt.toByteArray()))
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HkdfPrfKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testSerializeKey_noAccess_fails() throws Exception {
+    HkdfPrfKey key =
+        HkdfPrfKey.builder()
+            .setParameters(
+                HkdfPrfParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setHashType(HkdfPrfParameters.HashType.SHA384)
+                    .setSalt(Bytes.copyFrom(Hex.decode("2023af")))
+                    .build())
+            .setKeyBytes(SECRET_16)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @Test
+  public void testParseKey_noAccess_fails() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(SECRET_BYTE_STRING_16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .setSalt(ByteString.copyFrom(Hex.decode("2023af")))
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Invalid type URL.
+        ProtoParametersSerialization.create(
+            "i.am.a.random.type.url",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HkdfPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()),
+        // Invalid output prefix type.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.HkdfPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()),
+        // Key size is too small.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HkdfPrfKeyFormat.newBuilder()
+                .setKeySize(12)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()),
+        // Unknown hash type.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HkdfPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.UNKNOWN_HASH)
+                        .build())
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Invalid type URL.
+        ProtoKeySerialization.create(
+            "i.am.a.random.type.url",
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(SECRET_BYTE_STRING_16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid version.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(SECRET_BYTE_STRING_16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Key is too small.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[12]))
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Unknown hash type.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(SECRET_BYTE_STRING_16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.UNKNOWN_HASH)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid proto encoding.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid output prefix type.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HkdfPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(SECRET_BYTE_STRING_16)
+                .setParams(
+                    com.google.crypto.tink.proto.HkdfPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            123),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyManagerTest.java
index dcc3e62..88ca437 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyManagerTest.java
@@ -26,16 +26,17 @@
 import com.google.crypto.tink.proto.HmacPrfKey;
 import com.google.crypto.tink.proto.HmacPrfKeyFormat;
 import com.google.crypto.tink.proto.HmacPrfParams;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.PrfHmacJce;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
 import javax.crypto.spec.SecretKeySpec;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -47,6 +48,11 @@
   private final KeyTypeManager.KeyFactory<HmacPrfKeyFormat, HmacPrfKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    PrfConfig.register();
+  }
+
   @Test
   public void validateKeyFormat_empty() throws Exception {
     assertThrows(
@@ -93,7 +99,7 @@
     int numKeys = 100;
     Set<String> keys = new TreeSet<String>();
     for (int i = 0; i < numKeys; ++i) {
-      keys.add(TestUtil.hexEncode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numKeys);
   }
@@ -182,6 +188,40 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    HmacPrfParams params = HmacPrfParams.newBuilder().setHash(HashType.SHA256).build();
+    HmacPrfKey key =
+        factory.deriveKey(
+            HmacPrfKeyFormat.newBuilder()
+                .setVersion(0)
+                .setParams(params)
+                .setKeySize(keySize)
+                .build(),
+            fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKey_notEnoughKeyMaterial_throws() throws Exception {
     byte[] keyMaterial = Random.randBytes(31);
     HmacPrfParams params = HmacPrfParams.newBuilder().setHash(HashType.SHA256).build();
@@ -228,25 +268,23 @@
   @Test
   public void testHmacSha256Template() throws Exception {
     KeyTemplate template = HmacPrfKeyManager.hmacSha256Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new HmacPrfKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    HmacPrfKeyFormat format =
-        HmacPrfKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA256);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            HmacPrfParameters.builder()
+                .setKeySizeBytes(32)
+                .setHashType(HmacPrfParameters.HashType.SHA256)
+                .build());
   }
 
   @Test
   public void testHmacSha512Template() throws Exception {
     KeyTemplate template = HmacPrfKeyManager.hmacSha512Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new HmacPrfKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    HmacPrfKeyFormat format =
-        HmacPrfKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(64);
-    assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA512);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            HmacPrfParameters.builder()
+                .setKeySizeBytes(64)
+                .setHashType(HmacPrfParameters.HashType.SHA512)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyTest.java
new file mode 100644
index 0000000..ef480ee
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfKeyTest.java
@@ -0,0 +1,149 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HmacPrfKeyTest {
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @DataPoints("hashTypes")
+  public static final HmacPrfParameters.HashType[] HASH_TYPES =
+      new HmacPrfParameters.HashType[] {
+        HmacPrfParameters.HashType.SHA1,
+        HmacPrfParameters.HashType.SHA224,
+        HmacPrfParameters.HashType.SHA256,
+        HmacPrfParameters.HashType.SHA384,
+        HmacPrfParameters.HashType.SHA512
+      };
+
+  @Theory
+  public void buildAndGetPropertiesVariedValues_succeeds(
+      @FromDataPoints("keySizes") int keySize,
+      @FromDataPoints("hashTypes") HmacPrfParameters.HashType hashType)
+      throws Exception {
+    HmacPrfParameters parameters =
+        HmacPrfParameters.builder().setKeySizeBytes(keySize).setHashType(hashType).build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    SecretBytes keyBytes = SecretBytes.randomBytes(keySize);
+    HmacPrfKey key = HmacPrfKey.builder().setParameters(parameters).setKeyBytes(keyBytes).build();
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildWithoutSettingParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HmacPrfKey.builder().setKeyBytes(SecretBytes.randomBytes(16)).build());
+  }
+
+  @Test
+  public void buildWithoutSettingKeyBytes_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacPrfKey.builder()
+                .setParameters(
+                    HmacPrfParameters.builder()
+                        .setKeySizeBytes(16)
+                        .setHashType(HmacPrfParameters.HashType.SHA256)
+                        .build())
+                .build());
+  }
+
+  @Test
+  public void buildWithKeySizeMismatch_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacPrfKey.builder()
+                .setParameters(
+                    HmacPrfParameters.builder()
+                        .setKeySizeBytes(16)
+                        .setHashType(HmacPrfParameters.HashType.SHA256)
+                        .build())
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .build());
+  }
+
+  @Test
+  public void equals() throws Exception {
+    SecretBytes keyBytes = SecretBytes.randomBytes(16);
+    SecretBytes keyBytesCopy =
+        SecretBytes.copyFrom(
+            keyBytes.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    HmacPrfParameters parameters16 =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfParameters parameters32 =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(32)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "16-byte key",
+            HmacPrfKey.builder().setParameters(parameters16).setKeyBytes(keyBytes).build(),
+            // Same key built twice.
+            HmacPrfKey.builder().setParameters(parameters16).setKeyBytes(keyBytes).build(),
+            // Same key built with a copy of the key bytes.
+            HmacPrfKey.builder().setParameters(parameters16).setKeyBytes(keyBytesCopy).build())
+        .addEqualityGroup(
+            "16-byte random key bytes",
+            HmacPrfKey.builder()
+                .setParameters(parameters16)
+                .setKeyBytes(SecretBytes.randomBytes(16))
+                .build())
+        .addEqualityGroup(
+            "32-byte random key bytes",
+            HmacPrfKey.builder()
+                .setParameters(parameters32)
+                .setKeyBytes(SecretBytes.randomBytes(32))
+                .build())
+        .addEqualityGroup(
+            "different key class",
+            HkdfPrfKey.builder()
+                .setParameters(
+                    HkdfPrfParameters.builder()
+                        .setKeySizeBytes(16)
+                        .setHashType(HkdfPrfParameters.HashType.SHA256)
+                        .build())
+                .setKeyBytes(keyBytes)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfParametersTest.java
new file mode 100644
index 0000000..7eea070
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfParametersTest.java
@@ -0,0 +1,133 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class HmacPrfParametersTest {
+  @DataPoints("keySizes")
+  public static final int[] KEY_SIZES = new int[] {16, 32};
+
+  @DataPoints("hashTypes")
+  public static final HmacPrfParameters.HashType[] HASH_TYPES =
+      new HmacPrfParameters.HashType[] {
+        HmacPrfParameters.HashType.SHA1,
+        HmacPrfParameters.HashType.SHA224,
+        HmacPrfParameters.HashType.SHA256,
+        HmacPrfParameters.HashType.SHA384,
+        HmacPrfParameters.HashType.SHA512
+      };
+
+  @Theory
+  public void buildParametersVariedValuesAndGetProperties(
+      @FromDataPoints("keySizes") int keySize,
+      @FromDataPoints("hashTypes") HmacPrfParameters.HashType hashType)
+      throws Exception {
+    HmacPrfParameters parameters =
+        HmacPrfParameters.builder().setKeySizeBytes(keySize).setHashType(hashType).build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(keySize);
+    assertThat(parameters.getHashType()).isEqualTo(hashType);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    assertThat(parameters.toString())
+        .isEqualTo("HMAC PRF Parameters (hashType: " + hashType + " and " + keySize + "-byte key)");
+  }
+
+  @Test
+  public void buildWithoutSettingKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HmacPrfParameters.builder().setHashType(HmacPrfParameters.HashType.SHA256).build());
+  }
+
+  @Test
+  public void buildWithUnsupportedKeySize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            HmacPrfParameters.builder()
+                .setKeySizeBytes(15)
+                .setHashType(HmacPrfParameters.HashType.SHA256)
+                .build());
+  }
+
+  @Test
+  public void buildWithoutSettingHashType_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> HmacPrfParameters.builder().setKeySizeBytes(16).build());
+  }
+
+  @Test
+  public void testEqualsAndHashCode() throws Exception {
+    HmacPrfParameters parameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HmacPrfParameters sameParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    assertThat(sameParameters).isEqualTo(parameters);
+    assertThat(sameParameters.hashCode()).isEqualTo(parameters.hashCode());
+
+    // Different key size
+    HmacPrfParameters keySize32Parameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(32)
+            .setHashType(parameters.getHashType())
+            .build();
+    assertThat(keySize32Parameters).isNotEqualTo(parameters);
+    assertThat(keySize32Parameters.hashCode()).isNotEqualTo(parameters.hashCode());
+
+    // Different hash type
+    HmacPrfParameters sha512Parameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(parameters.getKeySizeBytes())
+            .setHashType(HmacPrfParameters.HashType.SHA512)
+            .build();
+    assertThat(sha512Parameters).isNotEqualTo(parameters);
+    assertThat(sha512Parameters.hashCode()).isNotEqualTo(parameters.hashCode());
+  }
+
+  @Test
+  @SuppressWarnings("TruthIncompatibleType")
+  public void testEqualDifferentClass() throws Exception {
+    HmacPrfParameters hmacPrfParameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HmacPrfParameters.HashType.SHA256)
+            .build();
+    HkdfPrfParameters hkdfPrfParameters =
+        HkdfPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(HkdfPrfParameters.HashType.SHA256)
+            .build();
+    assertThat(hmacPrfParameters).isNotEqualTo(hkdfPrfParameters);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfProtoSerializationTest.java
new file mode 100644
index 0000000..ee43bad
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/HmacPrfProtoSerializationTest.java
@@ -0,0 +1,357 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class HmacPrfProtoSerializationTest {
+  private static final String TYPE_URL = "type.googleapis.com/google.crypto.tink.HmacPrfKey";
+
+  private static final SecretBytes KEY_BYTES_16 = SecretBytes.randomBytes(16);
+  private static final ByteString KEY_BYTES_16_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_16.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    HmacPrfProtoSerialization.register(registry);
+  }
+
+  public static final class HashType {
+    public final HmacPrfParameters.HashType parametersHash;
+    public final com.google.crypto.tink.proto.HashType protoHash;
+
+    public HashType(
+        HmacPrfParameters.HashType parametersHash,
+        com.google.crypto.tink.proto.HashType protoHash) {
+      this.parametersHash = parametersHash;
+      this.protoHash = protoHash;
+    }
+  }
+
+  @DataPoints("hashTypes")
+  public static final HashType[] HASH_TYPES =
+      new HashType[] {
+        new HashType(HmacPrfParameters.HashType.SHA1, com.google.crypto.tink.proto.HashType.SHA1),
+        new HashType(
+            HmacPrfParameters.HashType.SHA224, com.google.crypto.tink.proto.HashType.SHA224),
+        new HashType(
+            HmacPrfParameters.HashType.SHA256, com.google.crypto.tink.proto.HashType.SHA256),
+        new HashType(
+            HmacPrfParameters.HashType.SHA384, com.google.crypto.tink.proto.HashType.SHA384),
+        new HashType(
+            HmacPrfParameters.HashType.SHA512, com.google.crypto.tink.proto.HashType.SHA512),
+      };
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    HmacPrfProtoSerialization.register(registry);
+    HmacPrfProtoSerialization.register(registry);
+  }
+
+  @Theory
+  public void serializeAndParseParameters(@FromDataPoints("hashTypes") HashType hashType)
+      throws Exception {
+    HmacPrfParameters parameters =
+        HmacPrfParameters.builder()
+            .setKeySizeBytes(16)
+            .setHashType(hashType.parametersHash)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(hashType.protoHash)
+                        .build())
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacPrfKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Theory
+  public void serializeAndParseKey(@FromDataPoints("hashTypes") HashType hashType)
+      throws Exception {
+    HmacPrfKey key =
+        HmacPrfKey.builder()
+            .setParameters(
+                HmacPrfParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setHashType(hashType.parametersHash)
+                    .build())
+            .setKeyBytes(KEY_BYTES_16)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(hashType.protoHash)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.HmacPrfKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void testSerializeKey_noAccess_fails() throws Exception {
+    HmacPrfKey key =
+        HmacPrfKey.builder()
+            .setParameters(
+                HmacPrfParameters.builder()
+                    .setKeySizeBytes(16)
+                    .setHashType(HmacPrfParameters.HashType.SHA384)
+                    .build())
+            .setKeyBytes(KEY_BYTES_16)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(key, ProtoKeySerialization.class, null));
+  }
+
+  @Test
+  public void testParseKey_noAccess_fails() throws Exception {
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+    assertThrows(GeneralSecurityException.class, () -> registry.parseKey(serialization, null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Invalid type URL.
+        ProtoParametersSerialization.create(
+            "i.am.a.random.type.url",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()),
+        // Invalid output prefix type.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.HmacPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()),
+        // Key size is too small.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacPrfKeyFormat.newBuilder()
+                .setKeySize(12)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()),
+        // Unknown hash type.
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.HmacPrfKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.UNKNOWN_HASH)
+                        .build())
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Invalid type URL.
+        ProtoKeySerialization.create(
+            "i.am.a.random.type.url",
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid version.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Key is too small.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(ByteString.copyFrom(new byte[12]))
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Unknown hash type.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.UNKNOWN_HASH)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid proto encoding.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Invalid output prefix type.
+        ProtoKeySerialization.create(
+            TYPE_URL,
+            com.google.crypto.tink.proto.HmacPrfKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(KEY_BYTES_16_AS_BYTE_STRING)
+                .setParams(
+                    com.google.crypto.tink.proto.HmacPrfParams.newBuilder()
+                        .setHash(com.google.crypto.tink.proto.HashType.SHA256)
+                        .build())
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            123),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/PredefinedPrfParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/PredefinedPrfParametersTest.java
new file mode 100644
index 0000000..eb4ce53
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/PredefinedPrfParametersTest.java
@@ -0,0 +1,52 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class PredefinedPrfParametersTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    PrfConfig.register();
+  }
+
+  @DataPoints("AllParameters")
+  public static final PrfParameters[] TEMPLATES =
+      new PrfParameters[] {
+        PredefinedPrfParameters.HKDF_SHA256,
+        PredefinedPrfParameters.HMAC_SHA256_PRF,
+        PredefinedPrfParameters.HMAC_SHA512_PRF,
+        PredefinedPrfParameters.AES_CMAC_PRF,
+      };
+
+  @Theory
+  public void testInstantiation(@FromDataPoints("AllParameters") PrfParameters parameters)
+      throws Exception {
+    Key key = KeysetHandle.generateNew(parameters).getAt(0).getKey();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/PrfConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/PrfConfigTest.java
index 99a12e5..3284b65 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/PrfConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/PrfConfigTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.prf;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Registry;
@@ -50,7 +51,7 @@
     PrfConfig.register();
 
     // After registration, the key manager should be present.
-    Registry.getKeyManager(typeUrl, Prf.class);
+    assertNotNull(Registry.getKeyManager(typeUrl, Prf.class));
 
     // Running init() manually again should succeed.
     PrfConfig.register();
@@ -71,7 +72,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, Prf.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, Prf.class));
     }
   }
 
@@ -88,7 +89,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, Prf.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, Prf.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/PrfKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/PrfKeyTemplatesTest.java
index eda9f26..002385d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/PrfKeyTemplatesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/PrfKeyTemplatesTest.java
@@ -18,17 +18,28 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.HkdfPrfKeyFormat;
+import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.protobuf.ExtensionRegistryLite;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests forPrfKeyTemplates */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public final class PrfKeyTemplatesTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    PrfConfig.register();
+  }
+
   @Test
   public void hkdfSha256() throws Exception {
     assertThat(PrfKeyTemplates.HKDF_SHA256.getTypeUrl())
@@ -52,4 +63,29 @@
     assertThat(format.getKeySize()).isEqualTo(32);
     assertThat(format.getParams().getHash()).isEqualTo(HashType.SHA256);
   }
+
+  public static class Pair {
+    public Pair(KeyTemplate template, PrfParameters parameters) {
+      this.template = template;
+      this.parameters = parameters;
+    }
+
+    KeyTemplate template;
+    PrfParameters parameters;
+  }
+
+  @DataPoints("EquivalentPairs")
+  public static final Pair[] TEMPLATES =
+      new Pair[] {
+        new Pair(PrfKeyTemplates.HKDF_SHA256, PredefinedPrfParameters.HKDF_SHA256),
+        new Pair(PrfKeyTemplates.HMAC_SHA256_PRF, PredefinedPrfParameters.HMAC_SHA256_PRF),
+        new Pair(PrfKeyTemplates.HMAC_SHA512_PRF, PredefinedPrfParameters.HMAC_SHA512_PRF),
+        new Pair(PrfKeyTemplates.AES_CMAC_PRF, PredefinedPrfParameters.AES_CMAC_PRF)
+      };
+
+  @Theory
+  public void testParametersEqualsKeyTemplate(@FromDataPoints("EquivalentPairs") Pair p)
+      throws Exception {
+    assertThat(TinkProtoParametersFormat.parse(p.template.toByteArray())).isEqualTo(p.parameters);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/PrfSetWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/PrfSetWrapperTest.java
index a30f64d..bcda072 100644
--- a/java_src/src/test/java/com/google/crypto/tink/prf/PrfSetWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/PrfSetWrapperTest.java
@@ -18,19 +18,27 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PrimitiveSet;
+import com.google.crypto.tink.internal.MutableMonitoringRegistry;
+import com.google.crypto.tink.internal.testing.FakeMonitoringClient;
+import com.google.crypto.tink.monitoring.MonitoringAnnotations;
+import com.google.crypto.tink.prf.HkdfPrfParameters.HashType;
 import com.google.crypto.tink.proto.KeyStatusType;
 import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.errorprone.annotations.Immutable;
 import java.security.GeneralSecurityException;
+import java.util.List;
 import org.junit.BeforeClass;
 import org.junit.Test;
-import org.junit.function.ThrowingRunnable;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
@@ -39,36 +47,143 @@
 public class PrfSetWrapperTest {
   private static final int KEY_SIZE = 32;
 
+  private static HkdfPrfKey hkdfPrfKey0;
+  private static HkdfPrfKey hkdfPrfKey1;
+  private static HkdfPrfKey hkdfPrfKeyFixed;
+
   @BeforeClass
   public static void setUp() throws Exception {
     PrfConfig.register();
+    createTestKeys();
+  }
+
+  private static void createTestKeys() throws GeneralSecurityException {
+    hkdfPrfKey0 =
+        HkdfPrfKey.builder()
+            .setParameters(
+                HkdfPrfParameters.builder()
+                    .setKeySizeBytes(KEY_SIZE)
+                    .setHashType(HashType.SHA256)
+                    .build())
+            .setKeyBytes(SecretBytes.randomBytes(KEY_SIZE))
+            .build();
+    hkdfPrfKey1 =
+        HkdfPrfKey.builder()
+            .setParameters(
+                HkdfPrfParameters.builder()
+                    .setKeySizeBytes(KEY_SIZE)
+                    .setHashType(HashType.SHA256)
+                    .build())
+            .setKeyBytes(SecretBytes.randomBytes(KEY_SIZE))
+            .build();
+    hkdfPrfKeyFixed =
+        HkdfPrfKey.builder()
+            .setParameters(
+                HkdfPrfParameters.builder()
+                    .setKeySizeBytes(KEY_SIZE)
+                    .setHashType(HashType.SHA256)
+                    .build())
+            .setKeyBytes(
+                SecretBytes.copyFrom(
+                    Hex.decode("0000000000000000000000000000000000000000000000000000000000000000"),
+                    InsecureSecretKeyAccess.get()))
+            .build();
   }
 
   @Test
-  public void testSmallPlaintextWithRawKey() throws Exception {
-    byte[] keyValue = Random.randBytes(KEY_SIZE);
-    Keyset.Key primary =
-        TestUtil.createKey(
-            TestUtil.createPrfKeyData(keyValue),
-            /* keyId= */ 5,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    PrimitiveSet<Prf> primitives =
-        TestUtil.createPrimitiveSet(TestUtil.createKeyset(primary), Prf.class);
+  public void compute_works() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hkdfPrfKeyFixed).withFixedId(42).makePrimary())
+            .build();
+    PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
     byte[] plaintext = "blah".getBytes(UTF_8);
 
-    PrfSet prfSet = new PrfSetWrapper().wrap(primitives);
     byte[] prs = prfSet.computePrimary(plaintext, 12);
-    byte[] prs2 = prfSet.getPrfs().get(5).compute(plaintext, 12);
 
-    assertEquals(5, prfSet.getPrimaryId());
     assertThat(prfSet.getPrfs()).hasSize(1);
-    assertThat(prs).hasLength(12);
-    assertArrayEquals(prs2, prs);
+    assertThat(prs).isEqualTo(Hex.decode("04f108788845580686b70d61"));
   }
 
   @Test
-  public void testSmallPlaintextWithMultipleKeys() throws Exception {
+  public void compute_usesPrimaryKey() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey0).withFixedId(42).makePrimary())
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey1).withFixedId(43))
+            .build();
+    PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
+    byte[] plaintext = "blah".getBytes(UTF_8);
+
+    byte[] prs = prfSet.computePrimary(plaintext, 12);
+    byte[] prsPrimary = prfSet.getPrfs().get(42).compute(plaintext, 12);
+
+    assertThat(prfSet.getPrimaryId()).isEqualTo(42);
+    assertArrayEquals(prsPrimary, prs);
+  }
+
+  @Test
+  public void prfsCorrespondToCorrectKeys() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey0).withFixedId(42).makePrimary())
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey1).withFixedId(43))
+            .build();
+    KeysetHandle singleKeyKeysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey1).withFixedId(43).makePrimary())
+            .build();
+    PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
+    PrfSet singleKeyPrfSet = singleKeyKeysetHandle.getPrimitive(PrfSet.class);
+    byte[] plaintext = "blah".getBytes(UTF_8);
+
+    byte[] prs = prfSet.getPrfs().get(43).compute(plaintext, 12);
+    byte[] singleKeyPrs = singleKeyPrfSet.computePrimary(plaintext, 12);
+
+    assertArrayEquals(singleKeyPrs, prs);
+  }
+
+  @Test
+  public void getPrfs_containsOnlyExistingKeys() throws Exception {
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey0).withFixedId(42).makePrimary())
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey1).withFixedId(43))
+            .build();
+    PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
+
+    assertThat(prfSet.getPrfs().keySet()).containsExactly(42, 43);
+  }
+
+  @Test
+  public void testWithEmptyAnnotations_noMonitoring() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
+    KeysetHandle keysetHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey0).withFixedId(42).makePrimary())
+            .addEntry(KeysetHandle.importKey(hkdfPrfKey1).withFixedId(43))
+            .build();
+    PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
+    byte[] plaintext = "blah".getBytes(UTF_8);
+
+    byte[] unused = prfSet.computePrimary(plaintext, 12);
+    unused = prfSet.getPrfs().get(42).compute(plaintext, 12);
+    unused = prfSet.getPrfs().get(43).compute(plaintext, 12);
+
+    // Without annotations, nothing gets logged.
+    assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
+    assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
+  }
+
+  @Test
+  public void testWithAnnotations_hasMonitoring() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
+
     byte[] primaryKeyValue = Random.randBytes(KEY_SIZE);
     Keyset.Key primary =
         TestUtil.createKey(
@@ -83,8 +198,11 @@
             /* keyId= */ 6,
             KeyStatusType.ENABLED,
             OutputPrefixType.RAW);
+    MonitoringAnnotations annotations =
+        MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
     PrimitiveSet<Prf> primitives =
-        TestUtil.createPrimitiveSet(TestUtil.createKeyset(primary, secondary), Prf.class);
+        TestUtil.createPrimitiveSetWithAnnotations(
+            TestUtil.createKeyset(primary, secondary), annotations, Prf.class);
     byte[] plaintext = "blah".getBytes(UTF_8);
 
     PrfSet prfSet = new PrfSetWrapper().wrap(primitives);
@@ -92,54 +210,84 @@
     byte[] prs5 = prfSet.getPrfs().get(5).compute(plaintext, 12);
     byte[] prs6 = prfSet.getPrfs().get(6).compute(plaintext, 12);
 
-    assertEquals(5, prfSet.getPrimaryId());
+    assertThat(prfSet.getPrimaryId()).isEqualTo(5);
+
     assertThat(prfSet.getPrfs()).hasSize(2);
     assertThat(prsPrimary).hasLength(12);
-    assertArrayEquals(prs5, prsPrimary);
+    assertThat(prs5).isEqualTo(prsPrimary);
     assertThat(prsPrimary).isNotEqualTo(prs6);
+
+    assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
+
+    List<FakeMonitoringClient.LogEntry> logEntries = fakeMonitoringClient.getLogEntries();
+    assertThat(logEntries).hasSize(3);
+    FakeMonitoringClient.LogEntry entry0 = logEntries.get(0);
+    assertThat(entry0.getKeyId()).isEqualTo(5);
+    assertThat(entry0.getPrimitive()).isEqualTo("prf");
+    assertThat(entry0.getApi()).isEqualTo("compute");
+    assertThat(entry0.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+    FakeMonitoringClient.LogEntry entry1 = logEntries.get(1);
+    assertThat(entry1.getKeyId()).isEqualTo(5);
+    assertThat(entry1.getPrimitive()).isEqualTo("prf");
+    assertThat(entry1.getApi()).isEqualTo("compute");
+    assertThat(entry1.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+    FakeMonitoringClient.LogEntry entry2 = logEntries.get(2);
+    assertThat(entry2.getKeyId()).isEqualTo(6);
+    assertThat(entry2.getPrimitive()).isEqualTo("prf");
+    assertThat(entry2.getApi()).isEqualTo("compute");
+    assertThat(entry2.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
   }
 
-    @Test
-    public void testWrapEmptyThrows() throws Exception {
-      final PrimitiveSet<Prf> primitiveSet = PrimitiveSet.newBuilder(Prf.class).build();
+  @Immutable
+  private static class AlwaysFailingPrf implements Prf {
 
-      assertThrows(
-          GeneralSecurityException.class,
-          new ThrowingRunnable() {
-            @Override
-            public void run() throws Throwable {
-              new PrfSetWrapper().wrap(primitiveSet);
-            }
-          });
+    @Override
+    public byte[] compute(byte[] input, int outputLength) throws GeneralSecurityException {
+      throw new GeneralSecurityException("fail");
     }
+  }
 
   @Test
-  public void testWrapNoPrimaryThrows() throws Exception {
-    byte[] primaryKeyValue = Random.randBytes(KEY_SIZE);
-    Keyset.Key primary =
-        TestUtil.createKey(
-            TestUtil.createPrfKeyData(primaryKeyValue),
-            /* keyId= */ 5,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    final Prf unusedPrf =
-        new Prf() {
-          @Override
-          public byte[] compute(byte[] input, int outputLength) throws GeneralSecurityException {
-            return new byte[0];
-          }
-        };
-    final PrimitiveSet<Prf> primitiveSet =
-        PrimitiveSet.newBuilder(Prf.class).addPrimitive(unusedPrf, primary).build();
-    // Note: Added a primary key but did not call primitiveSet.setPrimary().
+  public void testAlwaysFailingPrfWithAnnotations_hasMonitoring() throws Exception {
+    FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
+    MutableMonitoringRegistry.globalInstance().clear();
+    MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
 
+    MonitoringAnnotations annotations =
+        MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
+    PrimitiveSet<Prf> primitives =
+        PrimitiveSet.newBuilder(Prf.class)
+            .setAnnotations(annotations)
+            .addPrimaryPrimitive(
+                new AlwaysFailingPrf(),
+                TestUtil.createKey(
+                    TestUtil.createPrfKeyData(Random.randBytes(KEY_SIZE)),
+                    /* keyId= */ 5,
+                    KeyStatusType.ENABLED,
+                    OutputPrefixType.RAW))
+            .build();
+    PrfSet prfSet = new PrfSetWrapper().wrap(primitives);
+
+    byte[] plaintext = "blah".getBytes(UTF_8);
+
+    assertThrows(GeneralSecurityException.class, () -> prfSet.computePrimary(plaintext, 12));
     assertThrows(
-        GeneralSecurityException.class,
-        new ThrowingRunnable() {
-          @Override
-          public void run() throws Throwable {
-            new PrfSetWrapper().wrap(primitiveSet);
-          }
-        });
+        GeneralSecurityException.class, () -> prfSet.getPrfs().get(5).compute(plaintext, 12));
+
+    assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
+
+    List<FakeMonitoringClient.LogFailureEntry> failures =
+        fakeMonitoringClient.getLogFailureEntries();
+    assertThat(failures).hasSize(2);
+    FakeMonitoringClient.LogFailureEntry failure0 = failures.get(0);
+    assertThat(failure0.getPrimitive()).isEqualTo("prf");
+    assertThat(failure0.getApi()).isEqualTo("compute");
+    assertThat(failure0.getKeysetInfo().getPrimaryKeyId()).isEqualTo(5);
+    assertThat(failure0.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
+    FakeMonitoringClient.LogFailureEntry failure1 = failures.get(1);
+    assertThat(failure1.getPrimitive()).isEqualTo("prf");
+    assertThat(failure1.getApi()).isEqualTo("compute");
+    assertThat(failure1.getKeysetInfo().getPrimaryKeyId()).isEqualTo(5);
+    assertThat(failure1.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/prf/PrfTest.java b/java_src/src/test/java/com/google/crypto/tink/prf/PrfTest.java
new file mode 100644
index 0000000..f874f43
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/prf/PrfTest.java
@@ -0,0 +1,195 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.prf;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the Prf package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class PrfTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    PrfConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonMacKeyset_throws.
+  }
+
+  @DataPoints("templates")
+  public static final String[] TEMPLATES =
+      new String[] {
+        "AES_CMAC_PRF",
+        "HMAC_SHA256_PRF",
+        "HMAC_SHA512_PRF",
+        "HKDF_SHA256",
+      };
+
+  @Theory
+  public void create_computeVerify(@FromDataPoints("templates") String templateName)
+      throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    int primaryId = handle.getPrimary().getId();
+    PrfSet prfSet = handle.getPrimitive(PrfSet.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] outputPrimary = prfSet.computePrimary(data, 12);
+    byte[] output = prfSet.getPrfs().get(primaryId).compute(data, 12);
+    assertThat(output).isEqualTo(outputPrimary);
+
+    int invalidId = primaryId + 1;
+    assertThat(prfSet.getPrfs().get(invalidId)).isNull();
+  }
+
+  // A keyset with one MAC key, serialized in Tink's JSON format.
+  private static final String JSON_PRF_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 166506972,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacPrfKey\","
+          + "        \"value\": \"GkAlMHOHF4em1ax2/xzlhOX9696c6OIuSuYJ//DmzMshOjeGDjVazNZZKXo"
+          + "yo+USpExayMyab+GtjOfCCVjsECxnEgIIBA==\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 166506972,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void readKeysetEncryptDecrypt()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_PRF_KEYSET, InsecureSecretKeyAccess.get());
+    PrfSet prfSet = handle.getPrimitive(PrfSet.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] output1 = prfSet.computePrimary(data, 12);
+    byte[] output2 = prfSet.getPrfs().get(166506972).compute(data, 12);
+    assertThat(output2).isEqualTo(output1);
+  }
+
+  // A keyset with multiple keys. The first key is the same as in JSON_AEAD_KEYSET.
+  private static final String JSON_PRF_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1781110497,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacPrfKey\","
+          + "        \"value\": \"GkAlMHOHF4em1ax2/xzlhOX9696c6OIuSuYJ//DmzMshOjeGDjVazNZZKXo"
+          + "yo+USpExayMyab+GtjOfCCVjsECxnEgIIBA==\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 166506972,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }, {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HkdfPrfKey\","
+          + "        \"value\": \"GiC+cZnHCSh8CGzIoe9/jYhJeyk+vNdVSH+77Rc+BaGNvxICCAM=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1781110497,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.HmacPrfKey\","
+          + "        \"value\": \"GkBO8P7LMfUeCuqUZUY0xiAOi3q7lABfCA81kHv0qowLsjwmYwAa3leo9tD"
+          + "ez28gJtnWtghWQ3fVfWsZstNIOw0lEgIIBA==\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1593211602,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void multipleKeysReadKeysetWithEncryptDecrypt()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_PRF_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+
+    PrfSet prfSet = handle.getPrimitive(PrfSet.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] outputPrimary = prfSet.computePrimary(data, 12);
+
+    byte[] output1 = prfSet.getPrfs().get(166506972).compute(data, 12);
+    assertThat(output1).isNotEqualTo(outputPrimary);
+    byte[] output2 = prfSet.getPrfs().get(1781110497).compute(data, 12);
+    assertThat(output2).isEqualTo(outputPrimary);
+    byte[] output3 = prfSet.getPrfs().get(1593211602).compute(data, 12);
+    assertThat(output3).isNotEqualTo(outputPrimary);
+
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the Mac primitive.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonMacKeyset_throws() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a DeterministicAead primitive, but not a Mac.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(Mac.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/signature/BUILD.bazel
index 4be313d..738e07a 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/BUILD.bazel
@@ -1,27 +1,6 @@
 licenses(["notice"])
 
 java_test(
-    name = "PublicKeySignIntegrationTest",
-    size = "small",
-    srcs = ["PublicKeySignIntegrationTest.java"],
-    deps = [
-        "//proto:common_java_proto",
-        "//proto:ecdsa_java_proto",
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:crypto_format",
-        "//src/main/java/com/google/crypto/tink:public_key_sign",
-        "//src/main/java/com/google/crypto/tink:public_key_verify",
-        "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager",
-        "//src/main/java/com/google/crypto/tink/signature:ecdsa_verify_key_manager",
-        "//src/main/java/com/google/crypto/tink/signature:signature_config",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
     name = "RsaSsaPkcs1SignKeyManagerTest",
     size = "large",
     srcs = ["RsaSsaPkcs1SignKeyManagerTest.java"],
@@ -29,17 +8,23 @@
         "//proto:common_java_proto",
         "//proto:rsa_ssa_pkcs1_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:public_key_sign",
         "//src/main/java/com/google/crypto/tink:public_key_verify",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_private_key",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+        "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -58,9 +43,10 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -74,17 +60,23 @@
         "//proto:common_java_proto",
         "//proto:rsa_ssa_pss_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:key",
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:public_key_sign",
         "//src/main/java/com/google/crypto/tink:public_key_verify",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_private_key",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pss_verify_jce",
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+        "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -109,6 +101,7 @@
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_wrapper",
         "//src/main/java/com/google/crypto/tink/signature:signature_config",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -126,10 +119,12 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:public_key_sign",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -181,8 +176,8 @@
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_verify_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:signature_pem_keyset_reader",
         "//src/main/java/com/google/crypto/tink/signature/internal:sig_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -200,7 +195,6 @@
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/signature:signature_config",
-        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -218,9 +212,10 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_verify_key_manager",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -254,33 +249,13 @@
         "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
 
 java_test(
-    name = "PublicKeyVerifyIntegrationTest",
-    size = "small",
-    srcs = ["PublicKeyVerifyIntegrationTest.java"],
-    deps = [
-        "//proto:common_java_proto",
-        "//proto:ecdsa_java_proto",
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:public_key_sign",
-        "//src/main/java/com/google/crypto/tink:public_key_verify",
-        "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager",
-        "//src/main/java/com/google/crypto/tink/signature:ecdsa_verify_key_manager",
-        "//src/main/java/com/google/crypto/tink/signature:signature_config",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
     name = "PublicKeySignWrapperTest",
     size = "small",
     srcs = ["PublicKeySignWrapperTest.java"],
@@ -301,6 +276,7 @@
         "//src/main/java/com/google/crypto/tink/signature:public_key_verify_wrapper",
         "//src/main/java/com/google/crypto/tink/signature:signature_config",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -341,12 +317,14 @@
         "//src/main/java/com/google/crypto/tink:public_key_sign",
         "//src/main/java/com/google/crypto/tink:public_key_verify",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -362,12 +340,375 @@
         "//proto:rsa_ssa_pkcs1_java_proto",
         "//proto:rsa_ssa_pss_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/signature:ecdsa_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:predefined_signature_parameters",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_sign_key_manager",
         "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_sign_key_manager",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
         "//src/main/java/com/google/crypto/tink/signature:signature_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/signature:signature_parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "SignatureTest",
+    size = "small",
+    srcs = ["SignatureTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:public_key_sign",
+        "//src/main/java/com/google/crypto/tink:public_key_verify",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "EcdsaParametersTest",
+    size = "small",
+    srcs = ["EcdsaParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:elliptic_curves_util",
+        "//src/main/java/com/google/crypto/tink/mac:hmac_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "EcdsaPublicKeyTest",
+    size = "small",
+    srcs = ["EcdsaPublicKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "EcdsaPrivateKeyTest",
+    size = "small",
+    srcs = ["EcdsaPrivateKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "EcdsaProtoSerializationTest",
+    size = "small",
+    srcs = ["EcdsaProtoSerializationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:ecdsa_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Ed25519ParametersTest",
+    size = "small",
+    srcs = ["Ed25519ParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPkcs1ParametersTest",
+    size = "small",
+    srcs = ["RsaSsaPkcs1ParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Ed25519PublicKeyTest",
+    size = "small",
+    srcs = ["Ed25519PublicKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPkcs1PublicKeyTest",
+    size = "small",
+    srcs = ["RsaSsaPkcs1PublicKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPkcs1PrivateKeyTest",
+    size = "small",
+    srcs = ["RsaSsaPkcs1PrivateKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPkcs1ProtoSerializationTest",
+    size = "small",
+    srcs = ["RsaSsaPkcs1ProtoSerializationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:rsa_ssa_pkcs1_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Ed25519PrivateKeyTest",
+    size = "small",
+    srcs = ["Ed25519PrivateKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "Ed25519ProtoSerializationTest",
+    size = "small",
+    srcs = ["Ed25519ProtoSerializationTest.java"],
+    deps = [
+        "//proto:ed25519_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "KeyConversionTest",
+    size = "small",
+    srcs = ["KeyConversionTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:public_key_sign",
+        "//src/main/java/com/google/crypto/tink:public_key_verify",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pkcs1_public_key",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPssParametersTest",
+    size = "small",
+    srcs = ["RsaSsaPssParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPssPublicKeyTest",
+    size = "small",
+    srcs = ["RsaSsaPssPublicKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPssPrivateKeyTest",
+    size = "small",
+    srcs = ["RsaSsaPssPrivateKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_public_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "//src/main/java/com/google/crypto/tink/util:bytes",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "RsaSsaPssProtoSerializationTest",
+    size = "small",
+    srcs = ["RsaSsaPssProtoSerializationTest.java"],
+    deps = [
+        "//proto:common_java_proto",
+        "//proto:rsa_ssa_pss_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:json_keyset_reader",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_private_key",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/signature:rsa_ssa_pss_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PredefinedSignatureParametersTest",
+    size = "small",
+    srcs = ["PredefinedSignatureParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:predefined_signature_parameters",
+        "//src/main/java/com/google/crypto/tink/signature:signature_config",
+        "//src/main/java/com/google/crypto/tink/signature:signature_parameters",
+        "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaParametersTest.java
new file mode 100644
index 0000000..7243a00
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaParametersTest.java
@@ -0,0 +1,299 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.EllipticCurvesUtil;
+import com.google.crypto.tink.mac.HmacParameters;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class EcdsaParametersTest {
+
+  @Test
+  public void buildWithNoPrefixAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getSignatureEncoding()).isEqualTo(EcdsaParameters.SignatureEncoding.IEEE_P1363);
+    assertThat(parameters.getCurveType()).isEqualTo(EcdsaParameters.CurveType.NIST_P256);
+    assertThat(parameters.getHashType()).isEqualTo(EcdsaParameters.HashType.SHA256);
+    assertThat(parameters.getVariant()).isEqualTo(EcdsaParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(EcdsaParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(EcdsaParameters.Variant.TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithLegacyPrefix() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.LEGACY)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(EcdsaParameters.Variant.LEGACY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(EcdsaParameters.Variant.CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithIeeeP1363Encoding() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getSignatureEncoding())
+        .isEqualTo(EcdsaParameters.SignatureEncoding.IEEE_P1363);
+  }
+
+  @Test
+  public void buildParametersWithDerEncoding() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getSignatureEncoding()).isEqualTo(EcdsaParameters.SignatureEncoding.DER);
+  }
+
+  @Test
+  public void buildParametersWithP256AndSha256() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getCurveType()).isEqualTo(EcdsaParameters.CurveType.NIST_P256);
+    assertThat(parameters.getHashType()).isEqualTo(EcdsaParameters.HashType.SHA256);
+  }
+
+  @Test
+  public void buildParametersWithP384AndSha384() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+            .setHashType(EcdsaParameters.HashType.SHA384)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getCurveType()).isEqualTo(EcdsaParameters.CurveType.NIST_P384);
+    assertThat(parameters.getHashType()).isEqualTo(EcdsaParameters.HashType.SHA384);
+  }
+
+  @Test
+  public void buildParametersWithP521AndSha512() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+            .setHashType(EcdsaParameters.HashType.SHA512)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getCurveType()).isEqualTo(EcdsaParameters.CurveType.NIST_P521);
+    assertThat(parameters.getHashType()).isEqualTo(EcdsaParameters.HashType.SHA512);
+  }
+
+  @Test
+  public void buildWithoutSettingSignatureEncoding_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> EcdsaParameters.builder()
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build());
+  }
+
+  @Test
+  public void buildWithoutSettingCurveType_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build());
+  }
+
+  @Test
+  public void buildWithoutSettingHashType_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(null)
+            .build());
+  }
+
+  @Test
+  public void toParameterSpec_returnsSameSpecAsEllipticCurvesUtil() throws Exception {
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EcdsaParameters.CurveType.NIST_P256.toParameterSpec(),
+                EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isTrue();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EcdsaParameters.CurveType.NIST_P384.toParameterSpec(),
+                EllipticCurvesUtil.NIST_P384_PARAMS))
+        .isTrue();
+    assertThat(
+            EllipticCurvesUtil.isSameEcParameterSpec(
+                EcdsaParameters.CurveType.NIST_P521.toParameterSpec(),
+                EllipticCurvesUtil.NIST_P521_PARAMS))
+        .isTrue();
+  }
+
+  @Test
+  public void fromParameterSpec() throws Exception {
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(EllipticCurvesUtil.NIST_P256_PARAMS))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P256);
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(EllipticCurvesUtil.NIST_P384_PARAMS))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P384);
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(EllipticCurvesUtil.NIST_P521_PARAMS))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P521);
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    EcdsaParameters parameters1 =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaParameters parameters2 =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqual() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters)
+        .isNotEqualTo(
+            EcdsaParameters.builder()
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                .setHashType(EcdsaParameters.HashType.SHA256)
+                .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            EcdsaParameters.builder()
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                .setHashType(EcdsaParameters.HashType.SHA384)
+                .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            EcdsaParameters.builder()
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+                .setHashType(EcdsaParameters.HashType.SHA512)
+                .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            EcdsaParameters.builder()
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                .setHashType(EcdsaParameters.HashType.SHA256)
+                .setVariant(EcdsaParameters.Variant.TINK)
+                .build());
+    Object hmacParameters =
+        HmacParameters.builder()
+            .setKeySizeBytes(16)
+            .setTagSizeBytes(21)
+            .setHashType(HmacParameters.HashType.SHA256)
+            .build();
+    assertThat(parameters.equals(hmacParameters)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaPrivateKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaPrivateKeyTest.java
new file mode 100644
index 0000000..85b8ad8
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaPrivateKeyTest.java
@@ -0,0 +1,402 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPrivateKeySpec;
+import java.security.spec.ECPublicKeySpec;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class EcdsaPrivateKeyTest {
+
+  // Test case from https://www.ietf.org/rfc/rfc6979.txt, A.2.5
+  private static final ECPoint P256_PUBLIC_POINT =
+      new ECPoint(
+          new BigInteger("60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6", 16),
+          new BigInteger("7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299", 16));
+  private static final BigInteger P256_PRIVATE_VALUE =
+      new BigInteger("C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721", 16);
+
+  // Test case from https://www.ietf.org/rfc/rfc6979.txt, A.2.5
+  private static final ECPoint P521_PUBLIC_POINT =
+      new ECPoint(
+          new BigInteger(
+              "1894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD3"
+                  + "71123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F502"
+                  + "3A4",
+              16),
+          new BigInteger(
+              "0493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A2"
+                  + "8A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDF"
+                  + "CF5",
+              16));
+  private static final BigInteger P521_PRIVATE_VALUE =
+      new BigInteger(
+          "0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75C"
+              + "AA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83"
+              + "538",
+          16);
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+    EcdsaPrivateKey privateKey =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    EcdsaPrivateKey privateKey =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void convertToAndFromJavaECKeys() throws Exception {
+    // Create an elliptic curve key pair using Java's KeyPairGenerator.
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
+    keyGen.initialize(EcdsaParameters.CurveType.NIST_P384.toParameterSpec());
+    KeyPair keyPair = keyGen.generateKeyPair();
+    ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();
+    ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
+
+    // Before conversion, always check that the specs of ecPrivateKey and ecPublicKey are what
+    // we expect.
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(ecPrivateKey.getParams()))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P384);
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(ecPublicKey.getParams()))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P384);
+
+    // Create EcdsaParameters that match the curve type.
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+            .setHashType(EcdsaParameters.HashType.SHA384)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+
+    // Create EcdsaPublicKey and EcdsaPrivateKey using ecPublicKey and ecPrivateKey.
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(ecPublicKey.getW())
+            .build();
+    EcdsaPrivateKey privateKey =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(ecPrivateKey.getS(), InsecureSecretKeyAccess.get()))
+            .build();
+
+    // Convert EcdsaPublicKey and back into a ECPublicKey.
+    KeyFactory keyFactory = KeyFactory.getInstance("EC");
+    ECPublicKey ecPublicKey2 =
+        (ECPublicKey)
+            keyFactory.generatePublic(
+                new ECPublicKeySpec(
+                    publicKey.getPublicPoint(),
+                    publicKey.getParameters().getCurveType().toParameterSpec()));
+    assertThat(ecPublicKey2.getW()).isEqualTo(ecPublicKey.getW());
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(ecPublicKey2.getParams()))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P384);
+
+    // Convert EcdsaPrivateKey back into a ECPrivateKey.
+    ECPrivateKey ecPrivateKey2 =
+        (ECPrivateKey)
+            keyFactory.generatePrivate(
+                new ECPrivateKeySpec(
+                    privateKey.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()),
+                    privateKey.getParameters().getCurveType().toParameterSpec()));
+    assertThat(ecPrivateKey2.getS()).isEqualTo(ecPrivateKey.getS());
+    assertThat(EcdsaParameters.CurveType.fromParameterSpec(ecPrivateKey2.getParams()))
+        .isEqualTo(EcdsaParameters.CurveType.NIST_P384);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> EcdsaPublicKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutPublicKey_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPrivateKey.builder()
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void build_validatesPrivateValue() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+    EcdsaPrivateKey valid =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(valid.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    BigInteger invalidPrivateValue = P256_PRIVATE_VALUE.add(BigInteger.ONE);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        invalidPrivateValue, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(BigInteger.ZERO, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        new BigInteger("-1"), InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void build_rejectsPrivateValueThatIsLargerThanOrder() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+    EcdsaPrivateKey valid =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(valid.getPrivateValue().getBigInteger(InsecureSecretKeyAccess.get()))
+        .isEqualTo(P256_PRIVATE_VALUE);
+    // Add the order of the generator to the private value.
+    BigInteger tooLargePrivateValue =
+        P256_PRIVATE_VALUE.add(EcdsaParameters.CurveType.NIST_P256.toParameterSpec().getOrder());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        tooLargePrivateValue, InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    EcdsaPublicKey noPrefixPublicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .build();
+
+    // Uses generator as public key, and private key as ONE
+    EcdsaPublicKey noPrefixPublicKeyOne =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPublicPoint(EcdsaParameters.CurveType.NIST_P256.toParameterSpec().getGenerator())
+            .build();
+
+    EcdsaPublicKey tinkPrefixPublicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.TINK)
+                    .build())
+            .setPublicPoint(P256_PUBLIC_POINT)
+            .setIdRequirement(1907)
+            .build();
+
+    EcdsaPublicKey noPrefixPublicKeyP521 =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+                    .setHashType(EcdsaParameters.HashType.SHA512)
+                    .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPublicPoint(P521_PUBLIC_POINT)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix",
+            EcdsaPrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+                .build(),
+            // the same key built twice must be equal
+            EcdsaPrivateKey.builder()
+                .setPublicKey(
+                    EcdsaPublicKey.builder()
+                        .setParameters(
+                            EcdsaParameters.builder()
+                                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                                .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                                .setHashType(EcdsaParameters.HashType.SHA256)
+                                .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                                .build())
+                        .setPublicPoint(P256_PUBLIC_POINT)
+                        .build())
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+                .build())
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "No prefix, ONE",
+            EcdsaPrivateKey.builder()
+                .setPublicKey(noPrefixPublicKeyOne)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(BigInteger.ONE, InsecureSecretKeyAccess.get()))
+                .build())
+        // This group checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "No prefix, P521",
+            EcdsaPrivateKey.builder()
+                .setPublicKey(noPrefixPublicKeyP521)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        P521_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907",
+            EcdsaPrivateKey.builder()
+                .setPublicKey(tinkPrefixPublicKey)
+                .setPrivateValue(
+                    SecretBigInteger.fromBigInteger(
+                        P256_PRIVATE_VALUE, InsecureSecretKeyAccess.get()))
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaProtoSerializationTest.java
new file mode 100644
index 0000000..03c21f5
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaProtoSerializationTest.java
@@ -0,0 +1,836 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.EcdsaParams;
+import com.google.crypto.tink.proto.EcdsaSignatureEncoding;
+import com.google.crypto.tink.proto.EllipticCurveType;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for EcdsaProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class EcdsaProtoSerializationTest {
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.EcdsaPublicKey";
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    EcdsaProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_ieee_p256_sha256_no_prefix_equal() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_der_p384_sha512_legacy_equal() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+            .setHashType(EcdsaParameters.HashType.SHA512)
+            .setVariant(EcdsaParameters.Variant.LEGACY)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+            OutputPrefixType.LEGACY,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA512)
+                        .setCurve(EllipticCurveType.NIST_P384)
+                        .setEncoding(EcdsaSignatureEncoding.DER))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_ieee_p512_sha384_tink_equal() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+            .setHashType(EcdsaParameters.HashType.SHA512)
+            .setVariant(EcdsaParameters.Variant.TINK)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA512)
+                        .setCurve(EllipticCurveType.NIST_P521)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_der_p256_shaP256_crunchy_equal() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.CRUNCHY)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.DER))
+                .build());
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParsePublicKey_p256_tink_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.TINK)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setParams(
+                EcdsaParams.newBuilder()
+                    .setHashType(HashType.SHA256)
+                    .setCurve(EllipticCurveType.NIST_P256)
+                    .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_p384_no_prefix_equal() throws Exception {
+    // a valid P384 point. Each coordinate is encoded in 48 bytes.
+    String hexX =
+        "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64"
+            + "DEF8F0EA9055866064A254515480BC13";
+    String hexY =
+        "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1"
+            + "288B231C3AE0D4FE7344FD2533264720";
+
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                    .setHashType(EcdsaParameters.HashType.SHA384)
+                    .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setParams(
+                EcdsaParams.newBuilder()
+                    .setHashType(HashType.SHA384)
+                    .setCurve(EllipticCurveType.NIST_P384)
+                    .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void parseSerializePublicKey_reencodesNumbersInFixedLengthByteArrays() throws Exception {
+    // a valid P521 point, but encoded with leading zeros or truncated zeros.
+    String hexXTruncated =
+        "685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433"
+            + "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d";
+    String hexYWithLeadingZeros =
+        "0000000000000001ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83"
+            + "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676";
+
+    com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setX(ByteString.copyFrom(Hex.decode(hexXTruncated)))
+            .setY(ByteString.copyFrom(Hex.decode(hexYWithLeadingZeros)))
+            .setParams(
+                EcdsaParams.newBuilder()
+                    .setHashType(HashType.SHA512)
+                    .setCurve(EllipticCurveType.NIST_P521)
+                    .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.LEGACY,
+            /* idRequirement= */ 123);
+
+    Key key = registry.parseKey(serialization, /* access= */ null);
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+
+    com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKeyFromSerialized =
+        com.google.crypto.tink.proto.EcdsaPublicKey.parseFrom(
+            serialized.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+
+    // X and Y are currently serialized with an extra zero at the beginning. So we expect X and Y to
+    // always be encoded in 67 bytes.
+    String expectedHexX =
+        "00"
+            + "00685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433"
+            + "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d";
+    String expectedHexY =
+        "00"
+            + "01ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83"
+            + "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676";
+    assertThat(protoPublicKeyFromSerialized.getX())
+        .isEqualTo(ByteString.copyFrom(Hex.decode(expectedHexX)));
+    assertThat(protoPublicKeyFromSerialized.getY())
+        .isEqualTo(ByteString.copyFrom(Hex.decode(expectedHexY)));
+  }
+
+  @Test
+  public void serializedProtoCanBeParsedUsingBigIntegerTwoComplementEncoding() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.TINK)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setIdRequirement(123)
+            .build();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+
+    com.google.crypto.tink.proto.EcdsaPublicKey parsedProtoEcdsaPublicKey =
+        com.google.crypto.tink.proto.EcdsaPublicKey.parseFrom(
+            serialized.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    // parse x and y using BigIntegers two complement encoding.
+    assertThat(new BigInteger(parsedProtoEcdsaPublicKey.getX().toByteArray()))
+        .isEqualTo(key.getPublicPoint().getAffineX());
+    assertThat(new BigInteger(parsedProtoEcdsaPublicKey.getY().toByteArray()))
+        .isEqualTo(key.getPublicPoint().getAffineY());
+  }
+
+  @Test
+  public void serializeParsePrivateKey_p256_tink_equal() throws Exception {
+    // a valid P256 private key
+    // All values are encoded as 32 bytes.
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.TINK)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setIdRequirement(123)
+            .build();
+    EcdsaPrivateKey privateKey =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(
+                    new BigInteger(hexPrivateValue, 16), InsecureSecretKeyAccess.get()))
+            .build();
+
+    com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setParams(
+                EcdsaParams.newBuilder()
+                    .setHashType(HashType.SHA256)
+                    .setCurve(EllipticCurveType.NIST_P256)
+                    .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+            .build();
+    com.google.crypto.tink.proto.EcdsaPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(protoPublicKey)
+            // privateValue is currently serialized with an extra zero at the beginning.
+            .setKeyValue(ByteString.copyFrom(Hex.decode("00" + hexPrivateValue)))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.EcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void testParsePrivateKey_noAccess_throws() throws Exception {
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+    com.google.crypto.tink.proto.EcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setX(ByteString.copyFrom(Hex.decode(hexX)))
+            .setY(ByteString.copyFrom(Hex.decode(hexY)))
+            .setParams(
+                EcdsaParams.newBuilder()
+                    .setHashType(HashType.SHA256)
+                    .setCurve(EllipticCurveType.NIST_P256)
+                    .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+            .build();
+    com.google.crypto.tink.proto.EcdsaPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(protoPublicKey)
+            .setKeyValue(ByteString.copyFrom(Hex.decode(hexPrivateValue)))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+
+  @Test
+  public void testSerializeKeys_noAccess_throws() throws Exception {
+    String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+    String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+    String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+
+    EcdsaPublicKey publicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                    .setHashType(EcdsaParameters.HashType.SHA256)
+                    .setVariant(EcdsaParameters.Variant.TINK)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setIdRequirement(123)
+            .build();
+    EcdsaPrivateKey privateKey =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(
+                    new BigInteger(hexPrivateValue, 16), InsecureSecretKeyAccess.get()))
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(privateKey, ProtoKeySerialization.class, /* access= */ null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // invalid hash type
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA1)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()),
+        // unsupported curve
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.CURVE25519)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()),
+        // unknown encoding
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.UNKNOWN_ENCODING))
+                .build()),
+        // unknown output prefix
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.EcdsaKeyFormat.newBuilder()
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(PRIVATE_TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPublicKeySerializations() {
+    try {
+      String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+      String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+      return new ProtoKeySerialization[] {
+        // Point not on curve
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(1)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(
+                    ByteString.copyFrom(
+                        Hex.decode(
+                            // modified hexY
+                            "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462298")))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(1)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(0)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Hash type
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(0)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.UNKNOWN_HASH)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad curve
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(0)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.UNKNOWN_CURVE)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad signature encoding
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(0)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.UNKNOWN_ENCODING))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                .setVersion(0)
+                .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                .setParams(
+                    EcdsaParams.newBuilder()
+                        .setHashType(HashType.SHA256)
+                        .setCurve(EllipticCurveType.NIST_P256)
+                        .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPublicKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PUBLIC_KEY_SERIALIZATIONS =
+      createInvalidPublicKeySerializations();
+
+  @Theory
+  public void testParseInvalidPublicKeys_throws(
+      @FromDataPoints("invalidPublicKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPrivateKeySerializations() {
+    try {
+      String hexX = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6";
+      String hexY = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299";
+      String hexPrivateValue = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721";
+
+      com.google.crypto.tink.proto.EcdsaPublicKey validProtoPublicKey =
+          com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+              .setVersion(0)
+              .setX(ByteString.copyFrom(Hex.decode(hexX)))
+              .setY(ByteString.copyFrom(Hex.decode(hexY)))
+              .setParams(
+                  EcdsaParams.newBuilder()
+                      .setHashType(HashType.SHA256)
+                      .setCurve(EllipticCurveType.NIST_P256)
+                      .setEncoding(EcdsaSignatureEncoding.IEEE_P1363))
+              .build();
+
+      return new ProtoKeySerialization[] {
+        // Bad private key value
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(
+                    ByteString.copyFrom(
+                        Hex.decode(
+                            // modified hexPrivateValue
+                            "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6720")))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+                .setVersion(1)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(ByteString.copyFrom(Hex.decode(hexPrivateValue)))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(ByteString.copyFrom(Hex.decode(hexPrivateValue)))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Public key (invalid signature encoding)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.EcdsaPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setX(ByteString.copyFrom(Hex.decode(hexX)))
+                        .setY(ByteString.copyFrom(Hex.decode(hexY)))
+                        .setParams(
+                            EcdsaParams.newBuilder()
+                                .setHashType(HashType.SHA256)
+                                .setCurve(EllipticCurveType.NIST_P256)
+                                .setEncoding(EcdsaSignatureEncoding.UNKNOWN_ENCODING))
+                        .build())
+                .setKeyValue(ByteString.copyFrom(Hex.decode(hexPrivateValue)))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.EcdsaPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(ByteString.copyFrom(Hex.decode(hexPrivateValue)))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPrivateKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PRIVATE_KEY_SERIALIZATIONS =
+      createInvalidPrivateKeySerializations();
+
+  @Theory
+  public void testParseInvalidPrivateKeys_throws(
+      @FromDataPoints("invalidPrivateKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaPublicKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaPublicKeyTest.java
new file mode 100644
index 0000000..d3f6766
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaPublicKeyTest.java
@@ -0,0 +1,424 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class EcdsaPublicKeyTest {
+
+  private static final ECPoint A_P256_POINT =
+      new ECPoint(
+          new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+          new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac", 16));
+
+  private static final ECPoint INVALID_P256_POINT =
+      new ECPoint(
+          new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+          new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ad", 16));
+
+  private static final ECPoint A_P384_POINT =
+      new ECPoint(
+          new BigInteger(
+              "a7c76b970c3b5fe8b05d2838ae04ab47697b9eaf52e764592efda27fe7513272"
+                  + "734466b400091adbf2d68c58e0c50066",
+              16),
+          new BigInteger(
+              "ac68f19f2e1cb879aed43a9969b91a0839c4c38a49749b661efedf243451915e"
+                  + "d0905a32b060992b468c64766fc8437a",
+              16));
+
+  private static final ECPoint A_P521_POINT =
+        new ECPoint(
+            new BigInteger(
+                "000000685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a9490340"
+                    + "854334b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2"
+                    + "046d",
+                16),
+            new BigInteger(
+                "000001ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b7398"
+                    + "84a83bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302"
+                    + "f676",
+                16));
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+        .setParameters(parameters)
+        .setPublicPoint(A_P256_POINT).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P256_POINT);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(A_P256_POINT)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P256_POINT);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildLegacyVariantAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.LEGACY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(A_P256_POINT)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P256_POINT);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder()
+            .setParameters(parameters)
+            .setPublicPoint(A_P256_POINT)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P256_POINT);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildWithP384AndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+            .setHashType(EcdsaParameters.HashType.SHA384)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P384_POINT).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P384_POINT);
+  }
+
+  @Test
+  public void buildWithP521AndGetProperties() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+            .setHashType(EcdsaParameters.HashType.SHA512)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaPublicKey key =
+        EcdsaPublicKey.builder().setParameters(parameters).setPublicPoint(A_P521_POINT).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getPublicPoint()).isEqualTo(A_P521_POINT);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> EcdsaPublicKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> EcdsaPublicKey.builder().setPublicPoint(A_P256_POINT).build());
+  }
+
+  @Test
+  public void buildWithoutPublicPoint_fails() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> EcdsaPublicKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void parametersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    EcdsaParameters parametersWithIdRequirement =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPublicKey.builder()
+                .setParameters(parametersWithIdRequirement)
+                .setPublicPoint(A_P256_POINT)
+                .build());
+  }
+
+  @Test
+  public void parametersDoesNotRequireIdButIdIsSetInBuild_fails() throws Exception {
+    EcdsaParameters parametersWithoutIdRequirement =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parametersWithoutIdRequirement.hasIdRequirement()).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPublicKey.builder()
+                .setParameters(parametersWithoutIdRequirement)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(0x66AABBCC)
+                .build());
+  }
+
+  @Test
+  public void build_publicPointNotOnCurve_fails() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPublicKey.builder()
+                .setParameters(parameters)
+                .setPublicPoint(INVALID_P256_POINT)
+                .build());
+  }
+
+  @Test
+  public void build_publicPointOnOtherCurve_fails() throws Exception {
+    EcdsaParameters parameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EcdsaPublicKey.builder()
+                .setParameters(parameters)
+                .setPublicPoint(A_P521_POINT)
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    ECPoint aP256PointCopy =
+      new ECPoint(
+          new BigInteger("700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287", 16),
+          new BigInteger("db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac", 16));
+    ECPoint anotherP256Point =
+      new ECPoint(
+          new BigInteger("809f04289c64348c01515eb03d5ce7ac1a8cb9498f5caa50197e58d43a86a7ae", 16),
+          new BigInteger("b29d84e811197f25eba8f5194092cb6ff440e26d4421011372461f579271cda3", 16));
+
+    EcdsaParameters noPrefixParameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaParameters tinkPrefixParameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.TINK)
+            .build();
+    EcdsaParameters legacyPrefixParameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.LEGACY)
+            .build();
+    EcdsaParameters crunchyPrefixParameters =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.CRUNCHY)
+            .build();
+    EcdsaParameters noPrefixParametersDer =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+            .setHashType(EcdsaParameters.HashType.SHA256)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    EcdsaParameters noPrefixParametersP521 =
+        EcdsaParameters.builder()
+            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+            .setCurveType(EcdsaParameters.CurveType.NIST_P521)
+            .setHashType(EcdsaParameters.HashType.SHA512)
+            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, P256",
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .build(),
+            // the same key built twice must be equal
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .build(),
+            // the same key built with a copy of key bytes must be equal
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setPublicPoint(aP256PointCopy)
+                .build(),
+            // setting id requirement to null is equal to not setting it
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "No prefix, different P256 point",
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setPublicPoint(anotherP256Point)
+                .build())
+        // These groups checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "No prefix, P521",
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParametersP521)
+                .setPublicPoint(A_P521_POINT)
+                .build())
+        .addEqualityGroup(
+            "No prefix, DER encoding, P256",
+            EcdsaPublicKey.builder()
+                .setParameters(noPrefixParametersDer)
+                .setPublicPoint(A_P256_POINT)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907, P256",
+            EcdsaPublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1907)
+                .build(),
+            EcdsaPublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1907)
+                .build(),
+            EcdsaPublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setPublicPoint(aP256PointCopy)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal
+        .addEqualityGroup(
+            "Tink with key id 1908, P256",
+            EcdsaPublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1908)
+                .build())
+        // These 2 groups check that keys with different output prefix types are not equal
+        .addEqualityGroup(
+            "Legacy with key id 1907, P256",
+            EcdsaPublicKey.builder()
+                .setParameters(legacyPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Crunchy with key id 1907, P256",
+            EcdsaPublicKey.builder()
+                .setParameters(crunchyPrefixParameters)
+                .setPublicPoint(A_P256_POINT)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaSignKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaSignKeyManagerTest.java
index b5b07c7..ab96ce7 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaSignKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaSignKeyManagerTest.java
@@ -31,12 +31,12 @@
 import com.google.crypto.tink.proto.EllipticCurveType;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
-import com.google.crypto.tink.testing.TestUtil;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -48,6 +48,11 @@
   private final KeyTypeManager.KeyFactory<EcdsaKeyFormat, EcdsaPrivateKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    SignatureConfig.register();
+  }
+
   private static EcdsaKeyFormat createKeyFormat(
       HashType hashType, EllipticCurveType curveType, EcdsaSignatureEncoding encoding) {
     return EcdsaKeyFormat.newBuilder()
@@ -274,7 +279,7 @@
     Set<String> keys = new TreeSet<>();
     int numTests = 100;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -286,7 +291,7 @@
     Set<String> keys = new TreeSet<>();
     int numTests = 100;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -298,7 +303,7 @@
     Set<String> keys = new TreeSet<>();
     int numTests = 100;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -306,29 +311,27 @@
   @Test
   public void testEcdsaP256Template() throws Exception {
     KeyTemplate template = EcdsaSignKeyManager.ecdsaP256Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new EcdsaSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    EcdsaKeyFormat format =
-        EcdsaKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getCurve()).isEqualTo(EllipticCurveType.NIST_P256);
-    assertThat(format.getParams().getEncoding()).isEqualTo(EcdsaSignatureEncoding.DER);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            EcdsaParameters.builder()
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.DER)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                .setHashType(EcdsaParameters.HashType.SHA256)
+                .setVariant(EcdsaParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawEcdsaP256Template() throws Exception {
     KeyTemplate template = EcdsaSignKeyManager.rawEcdsaP256Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new EcdsaSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    EcdsaKeyFormat format =
-        EcdsaKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getCurve()).isEqualTo(EllipticCurveType.NIST_P256);
-    assertThat(format.getParams().getEncoding()).isEqualTo(EcdsaSignatureEncoding.IEEE_P1363);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            EcdsaParameters.builder()
+                .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                .setCurveType(EcdsaParameters.CurveType.NIST_P256)
+                .setHashType(EcdsaParameters.HashType.SHA256)
+                .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaVerifyKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaVerifyKeyManagerTest.java
index 9addc9a..5f8c98b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaVerifyKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/EcdsaVerifyKeyManagerTest.java
@@ -50,8 +50,6 @@
 
 /**
  * Unit tests for EcdsaVerifyKeyManager.
- *
- * <p>TODO(quannguyen): Add more tests.
  */
 @RunWith(JUnit4.class)
 public class EcdsaVerifyKeyManagerTest {
@@ -160,9 +158,9 @@
       } catch (Exception ignored) {
         // Ignored
       }
-      this.pubX = TestUtil.hexDecode(pubX.toLowerCase());
-      this.pubY = TestUtil.hexDecode(pubY.toLowerCase());
-      this.sig = TestUtil.hexDecode((r + s).toLowerCase());
+      this.pubX = Hex.decode(pubX.toLowerCase());
+      this.pubY = Hex.decode(pubY.toLowerCase());
+      this.sig = Hex.decode((r + s).toLowerCase());
       this.hashType = hashType;
       this.curveType = curveType;
     }
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519ParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519ParametersTest.java
new file mode 100644
index 0000000..9cecd5d
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519ParametersTest.java
@@ -0,0 +1,139 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Ed25519ParametersTest {
+  private static final Ed25519Parameters.Variant NO_PREFIX = Ed25519Parameters.Variant.NO_PREFIX;
+  private static final Ed25519Parameters.Variant TINK = Ed25519Parameters.Variant.TINK;
+  private static final Ed25519Parameters.Variant CRUNCHY = Ed25519Parameters.Variant.CRUNCHY;
+  private static final Ed25519Parameters.Variant LEGACY = Ed25519Parameters.Variant.LEGACY;
+
+  @Test
+  public void buildParametersAndGetProperties_noPrefix() throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create();
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_noPrefix_setVariantExplicitly() throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create(NO_PREFIX);
+    assertThat(parameters.getVariant()).isEqualTo(NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersAndGetProperties_tink() throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create(TINK);
+    assertThat(parameters.getVariant()).isEqualTo(TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParameterAndGetProperties_crunchy() throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create(CRUNCHY);
+    assertThat(parameters.getVariant()).isEqualTo(CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParameterAndGetProperties_legacy() throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create(LEGACY);
+    assertThat(parameters.getVariant()).isEqualTo(LEGACY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_noPrefix() throws Exception {
+    Ed25519Parameters parametersNoPrefix0 = Ed25519Parameters.create();
+    Ed25519Parameters parametersNoPrefix1 = Ed25519Parameters.create();
+    assertThat(parametersNoPrefix0).isEqualTo(parametersNoPrefix1);
+    assertThat(parametersNoPrefix0.hashCode()).isEqualTo(parametersNoPrefix1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_tink() throws Exception {
+    Ed25519Parameters parametersTink0 = Ed25519Parameters.create(TINK);
+    Ed25519Parameters parametersTink1 = Ed25519Parameters.create(TINK);
+    assertThat(parametersTink0).isEqualTo(parametersTink1);
+    assertThat(parametersTink0.hashCode()).isEqualTo(parametersTink1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_crunchy() throws Exception {
+    Ed25519Parameters parametersCrunchy0 = Ed25519Parameters.create(CRUNCHY);
+    Ed25519Parameters parametersCrunchy1 = Ed25519Parameters.create(CRUNCHY);
+    assertThat(parametersCrunchy0).isEqualTo(parametersCrunchy1);
+    assertThat(parametersCrunchy0.hashCode()).isEqualTo(parametersCrunchy1.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode_legacy() throws Exception {
+    Ed25519Parameters parametersLegacy0 = Ed25519Parameters.create(LEGACY);
+    Ed25519Parameters parametersLegacy1 = Ed25519Parameters.create(LEGACY);
+    assertThat(parametersLegacy0).isEqualTo(parametersLegacy1);
+    assertThat(parametersLegacy0.hashCode()).isEqualTo(parametersLegacy1.hashCode());
+  }
+
+  @Test
+  public void testNotEqualAndNotEqualHashCode_noPrefix() throws Exception {
+    Ed25519Parameters parametersNoPrefix = Ed25519Parameters.create();
+
+    Ed25519Parameters parametersTink = Ed25519Parameters.create(TINK);
+    Ed25519Parameters parametersCrunchy = Ed25519Parameters.create(CRUNCHY);
+    Ed25519Parameters parametersLegacy = Ed25519Parameters.create(LEGACY);
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersTink);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersTink.hashCode());
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersCrunchy);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersCrunchy.hashCode());
+
+    assertThat(parametersNoPrefix).isNotEqualTo(parametersLegacy);
+    assertThat(parametersNoPrefix.hashCode()).isNotEqualTo(parametersLegacy.hashCode());
+  }
+
+  @Test
+  public void testNotEqualAndNotEqualHashCode_tink() throws Exception {
+    Ed25519Parameters parametersTink = Ed25519Parameters.create(TINK);
+
+    Ed25519Parameters parametersCrunchy = Ed25519Parameters.create(CRUNCHY);
+    Ed25519Parameters parametersLegacy = Ed25519Parameters.create(LEGACY);
+
+    assertThat(parametersTink).isNotEqualTo(parametersCrunchy);
+    assertThat(parametersTink.hashCode()).isNotEqualTo(parametersCrunchy.hashCode());
+
+    assertThat(parametersTink).isNotEqualTo(parametersLegacy);
+    assertThat(parametersTink.hashCode()).isNotEqualTo(parametersLegacy.hashCode());
+  }
+
+  @Test
+  public void testNotEqualAndNotEqualHashCode_crunchy_legacy() throws Exception {
+    Ed25519Parameters parametersCrunchy = Ed25519Parameters.create(CRUNCHY);
+    Ed25519Parameters parametersLegacy = Ed25519Parameters.create(LEGACY);
+
+    assertThat(parametersCrunchy).isNotEqualTo(parametersLegacy);
+    assertThat(parametersCrunchy.hashCode()).isNotEqualTo(parametersLegacy.hashCode());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManagerTest.java
index 0664843..996aab8 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyManagerTest.java
@@ -29,14 +29,15 @@
 import com.google.crypto.tink.proto.Ed25519PublicKey;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.subtle.Ed25519Verify;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -48,6 +49,11 @@
   private final KeyTypeManager.KeyFactory<Ed25519KeyFormat, Ed25519PrivateKey> factory =
       manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    SignatureConfig.register();
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType())
@@ -84,7 +90,7 @@
     Set<String> keys = new TreeSet<>();
     int numTests = 100;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(format).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(format).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -145,19 +151,15 @@
   @Test
   public void testEd25519Template() throws Exception {
     KeyTemplate template = Ed25519PrivateKeyManager.ed25519Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new Ed25519PrivateKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    Ed25519KeyFormat unused =
-        Ed25519KeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
   }
 
   @Test
   public void testRawEd25519Template() throws Exception {
     KeyTemplate template = Ed25519PrivateKeyManager.rawEd25519Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new Ed25519PrivateKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    Ed25519KeyFormat unused =
-        Ed25519KeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    assertThat(template.toParameters())
+        .isEqualTo(Ed25519Parameters.create());
   }
 
   @Test
@@ -183,6 +185,35 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+
+    Ed25519PrivateKey key =
+        factory.deriveKey(
+            Ed25519KeyFormat.newBuilder().setVersion(0).build(),
+            fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKeySignVerify() throws Exception {
     byte[] keyMaterial = Random.randBytes(100);
     Ed25519PrivateKey key =
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyTest.java
new file mode 100644
index 0000000..16e71bd
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PrivateKeyTest.java
@@ -0,0 +1,189 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Ed25519PrivateKeyTest {
+  // Test case from https://www.rfc-editor.org/rfc/rfc8032#page-24
+  private static final byte[] SECRET_KEY =
+      Hex.decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
+  private static final byte[] PUBLIC_KEY =
+      Hex.decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
+
+  private static final SecretBytes PRIVATE_KEY_BYTES =
+      SecretBytes.copyFrom(SECRET_KEY, InsecureSecretKeyAccess.get());
+  private static final Bytes PUBLIC_KEY_BYTES = Bytes.copyFrom(PUBLIC_KEY);
+
+  @Test
+  public void createNoPrefixVariantAndGetProperties() throws Exception {
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    assertThat(privateKey.getParameters()).isEqualTo(Ed25519Parameters.create());
+    assertThat(privateKey.getPrivateKeyBytes()).isEqualTo(PRIVATE_KEY_BYTES);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void createTinkVariantAndGetProperties() throws Exception {
+    Ed25519PublicKey publicKey =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.TINK, PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    assertThat(privateKey.getParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
+    assertThat(privateKey.getPrivateKeyBytes()).isEqualTo(PRIVATE_KEY_BYTES);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x01, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void createCrunchyVariantAndGetProperties() throws Exception {
+    Ed25519PublicKey publicKey =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.CRUNCHY, PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    assertThat(privateKey.getParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.CRUNCHY));
+    assertThat(privateKey.getPrivateKeyBytes()).isEqualTo(PRIVATE_KEY_BYTES);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void createLegacyVariantAndGetProperties() throws Exception {
+    Ed25519PublicKey publicKey =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.LEGACY, PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    assertThat(privateKey.getParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.LEGACY));
+    assertThat(privateKey.getPrivateKeyBytes()).isEqualTo(PRIVATE_KEY_BYTES);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void invalidKeySize() throws Exception {
+    SecretBytes invalidSizePrivateKeyBytes = SecretBytes.randomBytes(64);
+
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> Ed25519PrivateKey.create(publicKey, invalidSizePrivateKeyBytes));
+  }
+
+  @Test
+  public void keysMismatch_fails() throws Exception {
+    SecretBytes invalidPrivateKeyBytes = SecretBytes.randomBytes(32);
+
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> Ed25519PrivateKey.create(publicKey, invalidPrivateKeyBytes));
+  }
+
+  @Test
+  public void nullPublicKey() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> Ed25519PrivateKey.create(null, PRIVATE_KEY_BYTES));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes privateKeyBytesCopy =
+        SecretBytes.copyFrom(
+            PRIVATE_KEY_BYTES.toByteArray(InsecureSecretKeyAccess.get()),
+            InsecureSecretKeyAccess.get());
+
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+    Ed25519PublicKey publicKeyCopy = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+
+    // Test case from https://www.rfc-editor.org/rfc/rfc8032#page-24
+    Bytes publicKeyBytesDiff =
+        Bytes.copyFrom(
+            Hex.decode("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"));
+    Ed25519PublicKey publicKeyDiff = Ed25519PublicKey.create(publicKeyBytesDiff);
+    SecretBytes privateKeyBytesDiff =
+        SecretBytes.copyFrom(
+            Hex.decode("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"),
+            InsecureSecretKeyAccess.get());
+
+    Ed25519PublicKey publicKeyTink =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.TINK, PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+    Ed25519PublicKey publicKeyCrunchy =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.CRUNCHY, PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+    Ed25519PublicKey publicKeyLegacy =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.LEGACY, PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes",
+            Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES),
+            Ed25519PrivateKey.create(publicKey, privateKeyBytesCopy),
+            Ed25519PrivateKey.create(publicKeyCopy, PRIVATE_KEY_BYTES))
+        .addEqualityGroup(
+            "No prefix, different key bytes",
+            Ed25519PrivateKey.create(publicKeyDiff, privateKeyBytesDiff))
+        .addEqualityGroup(
+            "Tink public key, keyBytes", Ed25519PrivateKey.create(publicKeyTink, PRIVATE_KEY_BYTES))
+        .addEqualityGroup(
+            "Crunchy public key, keyBytes",
+            Ed25519PrivateKey.create(publicKeyCrunchy, PRIVATE_KEY_BYTES))
+        .addEqualityGroup(
+            "Legacy public key, keyBytes",
+            Ed25519PrivateKey.create(publicKeyLegacy, PRIVATE_KEY_BYTES))
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    assertThat(privateKey.equalsKey(publicKey)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519ProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519ProtoSerializationTest.java
new file mode 100644
index 0000000..73d2e52
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519ProtoSerializationTest.java
@@ -0,0 +1,513 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.Ed25519KeyFormat;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for Ed25519ProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class Ed25519ProtoSerializationTest {
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.Ed25519PublicKey";
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey";
+
+  // Test case from https://www.rfc-editor.org/rfc/rfc8032#page-24
+  private static final byte[] secretKey =
+      Hex.decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
+  private static final byte[] publicKey =
+      Hex.decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
+
+  private static final SecretBytes PRIVATE_KEY_BYTES =
+      SecretBytes.copyFrom(secretKey, InsecureSecretKeyAccess.get());
+  private static final Bytes PUBLIC_KEY_BYTES = Bytes.copyFrom(publicKey);
+
+  private static final ByteString PRIVATE_KEY_BYTE_STRING =
+      ByteString.copyFrom(PRIVATE_KEY_BYTES.toByteArray(InsecureSecretKeyAccess.get()));
+  private static final ByteString PUBLIC_KEY_BYTE_STRING =
+      ByteString.copyFrom(PUBLIC_KEY_BYTES.toByteArray());
+
+  // Creates a helper map with the output prefix types which have id requieremts.
+  private static Map<Ed25519Parameters.Variant, OutputPrefixType> createVariantsMap() {
+    Map<Ed25519Parameters.Variant, OutputPrefixType> result = new HashMap<>();
+    result.put(Ed25519Parameters.Variant.TINK, OutputPrefixType.TINK);
+    result.put(Ed25519Parameters.Variant.CRUNCHY, OutputPrefixType.CRUNCHY);
+    result.put(Ed25519Parameters.Variant.LEGACY, OutputPrefixType.LEGACY);
+    return Collections.unmodifiableMap(result);
+  }
+
+  @DataPoints("variantsMap")
+  public static final Set<Map.Entry<Ed25519Parameters.Variant, OutputPrefixType>> variantsMap =
+      Collections.unmodifiableSet(createVariantsMap().entrySet());
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    Ed25519ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    Ed25519ProtoSerialization.register(registry);
+    Ed25519ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_noPrefix() throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey",
+            OutputPrefixType.RAW,
+            Ed25519KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(Ed25519KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Theory
+  public void serializeParseParameters_otherVariants(
+      @FromDataPoints("variantsMap")
+          Map.Entry<Ed25519Parameters.Variant, OutputPrefixType> variantsMap)
+      throws Exception {
+    Ed25519Parameters parameters = Ed25519Parameters.create(variantsMap.getKey());
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey",
+            variantsMap.getValue(),
+            Ed25519KeyFormat.getDefaultInstance());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(Ed25519KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParsePublicKey_noPrefix() throws Exception {
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+
+    com.google.crypto.tink.proto.Ed25519PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(publicKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            publicKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.Ed25519PublicKey.parser(), serialized, serialization);
+  }
+
+  @Theory
+  public void serializeParsePublicKey_otherVariants(
+      @FromDataPoints("variantsMap")
+          Map.Entry<Ed25519Parameters.Variant, OutputPrefixType> variantsMap)
+      throws Exception {
+    Ed25519PublicKey publicKey =
+        Ed25519PublicKey.create(
+            variantsMap.getKey(), PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+
+    com.google.crypto.tink.proto.Ed25519PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            variantsMap.getValue(),
+            /* idRequirement= */ 0x0708090a);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(publicKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            publicKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.Ed25519PublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePrivateKey_noPrefix() throws Exception {
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    com.google.crypto.tink.proto.Ed25519PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+            .build();
+
+    com.google.crypto.tink.proto.Ed25519PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(protoPublicKey)
+            .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.Ed25519PublicKey.parser(), serialized, serialization);
+  }
+
+  @Theory
+  public void serializeParsePrivateKey_otherVariants(
+      @FromDataPoints("variantsMap")
+          Map.Entry<Ed25519Parameters.Variant, OutputPrefixType> variantsMap)
+      throws Exception {
+    Ed25519PublicKey publicKey =
+        Ed25519PublicKey.create(
+            variantsMap.getKey(), PUBLIC_KEY_BYTES, /* idRequirement= */ 0x0708090a);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    com.google.crypto.tink.proto.Ed25519PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+            .build();
+
+    com.google.crypto.tink.proto.Ed25519PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(protoPublicKey)
+            .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            variantsMap.getValue(),
+            /* idRequirement= */ 0x0708090a);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.Ed25519PublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void testParsePrivateKey_noAccess_throws() throws Exception {
+    com.google.crypto.tink.proto.Ed25519PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+            .build();
+
+    com.google.crypto.tink.proto.Ed25519PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(protoPublicKey)
+            .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.Ed25519PrivateKey",
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 0x708090a);
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+
+  @Test
+  public void testSerializePrivateKey_noAccess_throws() throws Exception {
+    Ed25519PublicKey publicKey = Ed25519PublicKey.create(PUBLIC_KEY_BYTES);
+    Ed25519PrivateKey privateKey = Ed25519PrivateKey.create(publicKey, PRIVATE_KEY_BYTES);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(privateKey, ProtoKeySerialization.class, /* access= */ null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Unknown output prefix
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            Ed25519KeyFormat.getDefaultInstance()),
+        // Bad Version Number
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            Ed25519KeyFormat.newBuilder().setVersion(1).build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(PRIVATE_TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPublicKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+                .setVersion(1)
+                .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+                .setVersion(0)
+                .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPublicKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PUBLIC_KEY_SERIALIZATIONS =
+      createInvalidPublicKeySerializations();
+
+  @Theory
+  public void testParseInvalidPublicKeys_throws(
+      @FromDataPoints("invalidPublicKeySerializations") ProtoKeySerialization serialization) {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPrivateKeySerializations() {
+    try {
+      com.google.crypto.tink.proto.Ed25519PublicKey validProtoPublicKey =
+          com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+              .setVersion(0)
+              .setKeyValue(PUBLIC_KEY_BYTE_STRING)
+              .build();
+
+      return new ProtoKeySerialization[] {
+        // Bad private key value
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(
+                    ByteString.copyFrom(
+                        SecretBytes.randomBytes(32).toByteArray(InsecureSecretKeyAccess.get())))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+                .setVersion(1)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Invalid Public key
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.Ed25519PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setKeyValue(
+                            ByteString.copyFrom(
+                                SecretBytes.randomBytes(32)
+                                    .toByteArray(InsecureSecretKeyAccess.get())))
+                        .build())
+                .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.Ed25519PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(validProtoPublicKey)
+                .setKeyValue(PRIVATE_KEY_BYTE_STRING)
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPrivateKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PRIVATE_KEY_SERIALIZATIONS =
+      createInvalidPrivateKeySerializations();
+
+  @Theory
+  public void testParseInvalidPrivateKeys_throws(
+      @FromDataPoints("invalidPrivateKeySerializations") ProtoKeySerialization serialization) {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PublicKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PublicKeyTest.java
new file mode 100644
index 0000000..6972f30
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/Ed25519PublicKeyTest.java
@@ -0,0 +1,197 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.ChaCha20Poly1305Key;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Random;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class Ed25519PublicKeyTest {
+
+  @DataPoints("requireIdVariants")
+  public static final Ed25519Parameters.Variant[] REQUIRE_ID_VARIANTS =
+      new Ed25519Parameters.Variant[] {
+        Ed25519Parameters.Variant.TINK,
+        Ed25519Parameters.Variant.CRUNCHY,
+        Ed25519Parameters.Variant.LEGACY
+      };
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    Ed25519PublicKey key = Ed25519PublicKey.create(keyBytes);
+
+    assertThat(key.getParameters()).isEqualTo(Ed25519Parameters.create());
+    assertThat(key.getPublicKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildNoPrefixVariantExplicitAndGetProperties() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    Ed25519PublicKey key =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.NO_PREFIX, keyBytes, /* idRequirement= */ null);
+
+    assertThat(key.getParameters()).isEqualTo(Ed25519Parameters.create());
+    assertThat(key.getPublicKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    Ed25519PublicKey key =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.TINK, keyBytes, /* idRequirement= */ 0x0708090a);
+
+    assertThat(key.getParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
+    assertThat(key.getPublicKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x01, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    Ed25519PublicKey key =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.CRUNCHY, keyBytes, /* idRequirement= */ 0x0708090a);
+
+    assertThat(key.getParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.CRUNCHY));
+    assertThat(key.getPublicKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Test
+  public void buildLegacyVariantAndGetProperties() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    Ed25519PublicKey key =
+        Ed25519PublicKey.create(
+            Ed25519Parameters.Variant.LEGACY, keyBytes, /* idRequirement= */ 0x0708090a);
+
+    assertThat(key.getParameters())
+        .isEqualTo(Ed25519Parameters.create(Ed25519Parameters.Variant.LEGACY));
+    assertThat(key.getPublicKeyBytes()).isEqualTo(keyBytes);
+    assertThat(key.getOutputPrefix())
+        .isEqualTo(Bytes.copyFrom(new byte[] {0x00, 0x07, 0x08, 0x09, 0x0a}));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x708090a);
+  }
+
+  @Theory
+  public void requireIdButIdIsNotSet_fails(
+      @FromDataPoints("requireIdVariants") Ed25519Parameters.Variant variant) throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> Ed25519PublicKey.create(variant, keyBytes, /* idRequirement= */ null));
+  }
+
+  @Test
+  public void doesNotRequireIdButIdIsSet_fails() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.NO_PREFIX, keyBytes, /* idRequirement= */ 1115));
+  }
+
+  @Test
+  public void invalidKeySize() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(64));
+
+    assertThrows(GeneralSecurityException.class, () -> Ed25519PublicKey.create(keyBytes));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    Bytes keyBytes = Bytes.copyFrom(Random.randBytes(32));
+    Bytes keyBytesCopy = Bytes.copyFrom(keyBytes.toByteArray());
+    Bytes keyBytesDiff = Bytes.copyFrom(Random.randBytes(32));
+
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, keyBytes",
+            Ed25519PublicKey.create(keyBytes),
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.NO_PREFIX, keyBytes, /* idRequirement= */ null),
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.NO_PREFIX, keyBytesCopy, /* idRequirement= */ null))
+        .addEqualityGroup(
+            "No prefix, different key bytes",
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.NO_PREFIX, keyBytesDiff, /* idRequirement= */ null))
+        .addEqualityGroup(
+            "Tink with key id 1907, keyBytes",
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.TINK, keyBytes, /* idRequirement= */ 1907),
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.TINK, keyBytesCopy, /* idRequirement= */ 1907))
+        .addEqualityGroup(
+            "Tink with key id 1908, keyBytes",
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.TINK, keyBytes, /* idRequirement= */ 1908))
+        .addEqualityGroup(
+            "Crunchy with key id 1907, keyBytes",
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.CRUNCHY, keyBytes, /* idRequirement= */ 1907))
+        .addEqualityGroup(
+            "Legacy with key id 1908, keyBytes",
+            Ed25519PublicKey.create(
+                Ed25519Parameters.Variant.LEGACY, keyBytes, /* idRequirement= */ 1907))
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    SecretBytes secretKeyBytes = SecretBytes.randomBytes(32);
+    Bytes publicKeyBytes = Bytes.copyFrom(Random.randBytes(32));
+
+    Ed25519PublicKey ed25519Key = Ed25519PublicKey.create(publicKeyBytes);
+    ChaCha20Poly1305Key chaCha20Poly1305Key = ChaCha20Poly1305Key.create(secretKeyBytes);
+
+    assertThat(ed25519Key.equalsKey(chaCha20Poly1305Key)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/KeyConversionTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/KeyConversionTest.java
new file mode 100644
index 0000000..4848340
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/KeyConversionTest.java
@@ -0,0 +1,241 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.PublicKeySign;
+import com.google.crypto.tink.PublicKeyVerify;
+import com.google.crypto.tink.util.SecretBigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.RSAPrivateCrtKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class KeyConversionTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    SignatureConfig.register();
+  }
+
+  @Test
+  public void signAndVerifyWithEcdsaUsingJavaECKeys() throws Exception {
+    // Generate a EC key pair
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
+    keyGen.initialize(EcdsaParameters.CurveType.NIST_P384.toParameterSpec());
+    KeyPair keyPair = keyGen.generateKeyPair();
+    PrivateKey privateKey = keyPair.getPrivate();
+    PublicKey publicKey = keyPair.getPublic();
+
+    // Convert publicKey into a Tink EcdsaPublicKey.
+    ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
+    EcdsaPublicKey ecdsaPublicKey =
+        EcdsaPublicKey.builder()
+            .setParameters(
+                EcdsaParameters.builder()
+                    .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                    .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                    .setHashType(EcdsaParameters.HashType.SHA384)
+                    .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                    .build())
+            .setPublicPoint(ecPublicKey.getW())
+            .build();
+
+    // Convert privateKey and publicKey into a Tink EcdsaPrivateKey.
+    ECPrivateKey ecPrivateKey = (ECPrivateKey) privateKey;
+    EcdsaPrivateKey ecdsaPrivateKey =
+        EcdsaPrivateKey.builder()
+            .setPublicKey(
+                EcdsaPublicKey.builder()
+                    .setParameters(
+                        EcdsaParameters.builder()
+                            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
+                            .setCurveType(EcdsaParameters.CurveType.NIST_P384)
+                            .setHashType(EcdsaParameters.HashType.SHA384)
+                            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
+                            .build())
+                    .setPublicPoint(ecPublicKey.getW())
+                    .build())
+            .setPrivateValue(
+                SecretBigInteger.fromBigInteger(ecPrivateKey.getS(), InsecureSecretKeyAccess.get()))
+            .build();
+
+    // Generate a PublicKeySign primitive from ecdsaPrivateKey and sign a message.
+    KeysetHandle privateHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(ecdsaPrivateKey).withRandomId().makePrimary())
+            .build();
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] sig = signer.sign(data);
+
+    // Generate a PublicKeyVerify primitive from ecdsaPublicKey, and verify the signature.
+    KeysetHandle publicHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(ecdsaPublicKey).withRandomId().makePrimary())
+            .build();
+    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
+    verifier.verify(sig, data);
+  }
+
+  /**
+   * This test show how Tink can be used with Java RSA keys, by importing them as RSA SSA PKCS1
+   * keys.
+   */
+  @Test
+  public void signAndVerifyUsingJavaRSAKeys() throws Exception {
+    // Generate a RSA key pair
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+    keyGen.initialize(/* keysize= */ 2048);
+    KeyPair keyPair = keyGen.generateKeyPair();
+    PrivateKey privateKey = keyPair.getPrivate();
+    PublicKey publicKey = keyPair.getPublic();
+
+    // Convert publicKey into a Tink RsaSsaPkcs1PublicKey.
+    RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
+    RsaSsaPkcs1PublicKey rsaSsaPkcs1PublicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setModulusSizeBits(rsaPublicKey.getModulus().bitLength())
+                    .setPublicExponent(rsaPublicKey.getPublicExponent())
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                    .build())
+            .setModulus(rsaPublicKey.getModulus())
+            .build();
+
+    // Convert privateKey and publicKey into a Tink RsaSsaPkcs1PrivateKey.
+    RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey;
+    RsaSsaPkcs1PrivateKey rsaSsaPkcs1PrivateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(rsaSsaPkcs1PublicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(
+                    rsaPrivateKey.getPrimeP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(
+                    rsaPrivateKey.getPrimeQ(), InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(
+                SecretBigInteger.fromBigInteger(
+                    rsaPrivateKey.getPrivateExponent(), InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(
+                    rsaPrivateKey.getPrimeExponentP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(
+                    rsaPrivateKey.getPrimeExponentQ(), InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(
+                    rsaPrivateKey.getCrtCoefficient(), InsecureSecretKeyAccess.get()))
+            .build();
+
+    // Generate a PublicKeySign primitive from rsaSsaPkcs1PrivateKey and sign a message.
+    KeysetHandle privateHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rsaSsaPkcs1PrivateKey).withRandomId().makePrimary())
+            .build();
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] sig = signer.sign(data);
+
+    // Generate a PublicKeyVerify primitive from rsaSsaPkcs1PublicKey, and verify the signature.
+    KeysetHandle publicHandle =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(rsaSsaPkcs1PublicKey).withRandomId().makePrimary())
+            .build();
+    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
+    verifier.verify(sig, data);
+
+    // Verify using java.security.Signature.
+    Signature signatureVerify = Signature.getInstance("SHA256withRSA");
+    signatureVerify.initVerify(publicKey);
+    signatureVerify.update(data);
+    assertThat(signatureVerify.verify(sig)).isTrue();
+  }
+
+  /** This test shows how Tink keys can be exported to Java RSA keys. */
+  @Test
+  public void exportToJavaRSAKey() throws Exception {
+    KeysetHandle privateHandle =
+        KeysetHandle.generateNew(KeyTemplates.get("RSA_SSA_PKCS1_3072_SHA256_F4_RAW"));
+    KeysetHandle publicHandle = privateHandle.getPublicKeysetHandle();
+
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] sig = signer.sign(data);
+
+    // Export private key and sign using java.security.Signature.
+    RsaSsaPkcs1PrivateKey rsaSsaPkcs1PrivateKey =
+        (RsaSsaPkcs1PrivateKey) privateHandle.getAt(0).getKey();
+    PrivateKey privateKey =
+        KeyFactory.getInstance("RSA")
+            .generatePrivate(
+                new RSAPrivateCrtKeySpec(
+                    rsaSsaPkcs1PrivateKey.getPublicKey().getModulus(),
+                    rsaSsaPkcs1PrivateKey.getParameters().getPublicExponent(),
+                    rsaSsaPkcs1PrivateKey
+                        .getPrivateExponent()
+                        .getBigInteger(InsecureSecretKeyAccess.get()),
+                    rsaSsaPkcs1PrivateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get()),
+                    rsaSsaPkcs1PrivateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get()),
+                    rsaSsaPkcs1PrivateKey
+                        .getPrimeExponentP()
+                        .getBigInteger(InsecureSecretKeyAccess.get()),
+                    rsaSsaPkcs1PrivateKey
+                        .getPrimeExponentQ()
+                        .getBigInteger(InsecureSecretKeyAccess.get()),
+                    rsaSsaPkcs1PrivateKey
+                        .getCrtCoefficient()
+                        .getBigInteger(InsecureSecretKeyAccess.get())));
+    Signature signatureSigner = Signature.getInstance("SHA256withRSA");
+    signatureSigner.initSign(privateKey);
+    signatureSigner.update(data);
+    // These signatures are deterministic, so they must be equal.
+    assertThat(signatureSigner.sign()).isEqualTo(sig);
+
+    // Export public key and verify using java.security.Signature.
+    RsaSsaPkcs1PublicKey rsaSsaPkcs1PublicKey =
+        (RsaSsaPkcs1PublicKey) publicHandle.getAt(0).getKey();
+    PublicKey publicKey =
+        KeyFactory.getInstance("RSA")
+            .generatePublic(
+                new RSAPublicKeySpec(
+                    rsaSsaPkcs1PublicKey.getModulus(),
+                    rsaSsaPkcs1PublicKey.getParameters().getPublicExponent()));
+    Signature signatureVerify = Signature.getInstance("SHA256withRSA");
+    signatureVerify.initVerify(publicKey);
+    signatureVerify.update(data);
+    assertThat(signatureVerify.verify(sig)).isTrue();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/PredefinedSignatureParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/PredefinedSignatureParametersTest.java
new file mode 100644
index 0000000..14e8fd7
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/PredefinedSignatureParametersTest.java
@@ -0,0 +1,88 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.testing.TestUtil;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class PredefinedSignatureParametersTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    SignatureConfig.register();
+  }
+
+  @DataPoints("AllParameters")
+  public static final SignatureParameters[] TEMPLATES =
+      new SignatureParameters[] {
+        PredefinedSignatureParameters.ECDSA_P256,
+        PredefinedSignatureParameters.ECDSA_P384,
+        PredefinedSignatureParameters.ECDSA_P521,
+        PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363,
+        PredefinedSignatureParameters.ECDSA_P384_IEEE_P1363,
+        PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX,
+        PredefinedSignatureParameters.ECDSA_P521_IEEE_P1363,
+        PredefinedSignatureParameters.ED25519,
+        PredefinedSignatureParameters.ED25519WithRawOutput,
+        PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4,
+        PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX,
+        PredefinedSignatureParameters.RSA_SSA_PKCS1_4096_SHA512_F4,
+        PredefinedSignatureParameters.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4,
+        PredefinedSignatureParameters.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4,
+      };
+
+  @Theory
+  public void testInstantiation(@FromDataPoints("AllParameters") SignatureParameters parameters)
+      throws Exception {
+    if (TestUtil.isTsan()) {
+      assume().that(parameters).isInstanceOf(Ed25519Parameters.class);
+    }
+
+    Key key = KeysetHandle.generateNew(parameters).getAt(0).getKey();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+  }
+
+  @Test
+  public void testTypes() throws Exception {
+    assertThat(PredefinedSignatureParameters.ECDSA_P256).isNotNull();
+    assertThat(PredefinedSignatureParameters.ECDSA_P384).isNotNull();
+    assertThat(PredefinedSignatureParameters.ECDSA_P521).isNotNull();
+    assertThat(PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363).isNotNull();
+    assertThat(PredefinedSignatureParameters.ECDSA_P384_IEEE_P1363).isNotNull();
+    assertThat(PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX).isNotNull();
+    assertThat(PredefinedSignatureParameters.ECDSA_P521_IEEE_P1363).isNotNull();
+    assertThat(PredefinedSignatureParameters.ED25519).isNotNull();
+    assertThat(PredefinedSignatureParameters.ED25519WithRawOutput).isNotNull();
+    assertThat(PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4).isNotNull();
+    assertThat(PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX)
+        .isNotNull();
+    assertThat(PredefinedSignatureParameters.RSA_SSA_PKCS1_4096_SHA512_F4).isNotNull();
+    assertThat(PredefinedSignatureParameters.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4).isNotNull();
+    assertThat(PredefinedSignatureParameters.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4).isNotNull();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignIntegrationTest.java
deleted file mode 100644
index 916ea5a..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignIntegrationTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.signature;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.CryptoFormat;
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.PublicKeyVerify;
-import com.google.crypto.tink.proto.EcdsaPrivateKey;
-import com.google.crypto.tink.proto.EcdsaSignatureEncoding;
-import com.google.crypto.tink.proto.EllipticCurveType;
-import com.google.crypto.tink.proto.HashType;
-import com.google.crypto.tink.proto.KeyData;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset.Key;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests which run the everything for signatures */
-@RunWith(JUnit4.class)
-// TODO(quannguyen): Add more tests.
-public class PublicKeySignIntegrationTest {
-
-  @Before
-  public void setUp() throws Exception {
-    SignatureConfig.register();
-  }
-
-  @Test
-  public void testMultipleKeys() throws Exception {
-    EcdsaPrivateKey tinkPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P521, HashType.SHA512, EcdsaSignatureEncoding.DER);
-    Key tink =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                tinkPrivateKey,
-                new EcdsaSignKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-            1,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.TINK);
-
-    EcdsaPrivateKey legacyPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P256, HashType.SHA256, EcdsaSignatureEncoding.DER);
-    Key legacy =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                legacyPrivateKey,
-                new EcdsaSignKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-            2,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.LEGACY);
-
-    EcdsaPrivateKey rawPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P384, HashType.SHA512, EcdsaSignatureEncoding.DER);
-    Key raw =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                rawPrivateKey,
-                new EcdsaSignKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-            3,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-
-    EcdsaPrivateKey crunchyPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P384, HashType.SHA512, EcdsaSignatureEncoding.DER);
-    Key crunchy =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                crunchyPrivateKey,
-                new EcdsaSignKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-            4,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.CRUNCHY);
-
-    Key[] keys = new Key[] {tink, legacy, raw, crunchy};
-    EcdsaPrivateKey[] privateKeys =
-        new EcdsaPrivateKey[] {tinkPrivateKey, legacyPrivateKey, rawPrivateKey, crunchyPrivateKey};
-
-    int j = keys.length;
-    for (int i = 0; i < j; i++) {
-      KeysetHandle keysetHandle =
-          TestUtil.createKeysetHandle(
-              TestUtil.createKeyset(
-                  keys[i], keys[(i + 1) % j], keys[(i + 2) % j], keys[(i + 3) % j]));
-      // Signs with the primary private key.
-      PublicKeySign signer = keysetHandle.getPrimitive(PublicKeySign.class);
-      byte[] plaintext = Random.randBytes(1211);
-      byte[] sig = signer.sign(plaintext);
-      if (keys[i].getOutputPrefixType() != OutputPrefixType.RAW) {
-        byte[] prefix = Arrays.copyOfRange(sig, 0, CryptoFormat.NON_RAW_PREFIX_SIZE);
-        assertArrayEquals(prefix, CryptoFormat.getOutputPrefix(keys[i]));
-      }
-
-      // Verifying with the primary public key should work.
-      PublicKeyVerify verifier =
-          TestUtil.createKeysetHandle(
-                  TestUtil.createKeyset(
-                      TestUtil.createKey(
-                          TestUtil.createKeyData(
-                              privateKeys[i].getPublicKey(),
-                              new EcdsaVerifyKeyManager().getKeyType(),
-                              KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-                          keys[i].getKeyId(),
-                          KeyStatusType.ENABLED,
-                          keys[i].getOutputPrefixType())))
-              .getPrimitive(PublicKeyVerify.class);
-      try {
-        verifier.verify(sig, plaintext);
-      } catch (GeneralSecurityException ex) {
-        fail("Valid signature, should not throw exception");
-      }
-
-      // Verifying with a random public key should fail.
-      EcdsaPrivateKey randomPrivKey =
-          TestUtil.generateEcdsaPrivKey(
-              EllipticCurveType.NIST_P521, HashType.SHA512, EcdsaSignatureEncoding.DER);
-      final PublicKeyVerify verifier2 =
-          TestUtil.createKeysetHandle(
-                  TestUtil.createKeyset(
-                      TestUtil.createKey(
-                          TestUtil.createKeyData(
-                              randomPrivKey.getPublicKey(),
-                              new EcdsaVerifyKeyManager().getKeyType(),
-                              KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-                          keys[i].getKeyId(),
-                          KeyStatusType.ENABLED,
-                          keys[i].getOutputPrefixType())))
-              .getPrimitive(PublicKeyVerify.class);
-      assertThrows(GeneralSecurityException.class, () -> verifier2.verify(sig, plaintext));
-    }
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignWrapperTest.java
index b0f51ca..273962e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeySignWrapperTest.java
@@ -40,6 +40,7 @@
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
@@ -135,7 +136,7 @@
     byte[] prefix = Arrays.copyOf(sig, 5);
     byte[] sigWithoutPrefix = Arrays.copyOfRange(sig, 5, sig.length);
 
-    assertThat(prefix).isEqualTo(TestUtil.hexDecode("0166AABBCC"));
+    assertThat(prefix).isEqualTo(Hex.decode("0166AABBCC"));
 
     rawVerifier.verify(sigWithoutPrefix, data);
   }
@@ -163,7 +164,7 @@
     byte[] prefix = Arrays.copyOf(sig, 5);
     byte[] sigWithoutPrefix = Arrays.copyOfRange(sig, 5, sig.length);
 
-    assertThat(prefix).isEqualTo(TestUtil.hexDecode("0066AABBCC"));
+    assertThat(prefix).isEqualTo(Hex.decode("0066AABBCC"));
 
     rawVerifier.verify(sigWithoutPrefix, data);
   }
@@ -191,9 +192,9 @@
     byte[] prefix = Arrays.copyOf(sig, 5);
     byte[] sigWithoutPrefix = Arrays.copyOfRange(sig, 5, sig.length);
 
-    assertThat(prefix).isEqualTo(TestUtil.hexDecode("0066AABBCC"));
+    assertThat(prefix).isEqualTo(Hex.decode("0066AABBCC"));
 
-    byte[] signedData = Bytes.concat(data, TestUtil.hexDecode("00"));
+    byte[] signedData = Bytes.concat(data, Hex.decode("00"));
     rawVerifier.verify(sigWithoutPrefix, signedData);
   }
 
@@ -308,7 +309,7 @@
     PublicKeySign signer = new PublicKeySignWrapper().wrap(signPrimitives);
 
     byte[] data = "data".getBytes(UTF_8);
-    signer.sign(data);
+    Object unused = signer.sign(data);
 
     assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
     assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
@@ -336,8 +337,8 @@
                 TestUtil.createPrimitiveSetWithAnnotations(
                     TestUtil.createKeyset(privateKey2), annotations, PublicKeySign.class));
     byte[] data = "data".getBytes(UTF_8);
-    signer.sign(data);
-    signer2.sign(data);
+    Object unused = signer.sign(data);
+    unused = signer2.sign(data);
 
     List<FakeMonitoringClient.LogEntry> logEntries = fakeMonitoringClient.getLogEntries();
     assertThat(logEntries).hasSize(2);
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyIntegrationTest.java
deleted file mode 100644
index e243552..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyIntegrationTest.java
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.signature;
-
-import static org.junit.Assert.assertThrows;
-
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.PublicKeyVerify;
-import com.google.crypto.tink.proto.EcdsaPrivateKey;
-import com.google.crypto.tink.proto.EcdsaSignatureEncoding;
-import com.google.crypto.tink.proto.EllipticCurveType;
-import com.google.crypto.tink.proto.HashType;
-import com.google.crypto.tink.proto.KeyData;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset.Key;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.security.GeneralSecurityException;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests which run the everything for the Public Key signing primitives. */
-@RunWith(JUnit4.class)
-// TODO(quannguyen): Add more tests.
-public class PublicKeyVerifyIntegrationTest {
-
-  @Before
-  public void setUp() throws Exception {
-    SignatureConfig.register();
-  }
-
-  @Test
-  public void testMultipleKeys() throws Exception {
-    EcdsaPrivateKey tinkPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P521, HashType.SHA512, EcdsaSignatureEncoding.DER);
-    Key tink =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                tinkPrivateKey.getPublicKey(),
-                new EcdsaVerifyKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-            1,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.TINK);
-
-    EcdsaPrivateKey legacyPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P256, HashType.SHA256, EcdsaSignatureEncoding.DER);
-    Key legacy =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                legacyPrivateKey.getPublicKey(),
-                new EcdsaVerifyKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-            2,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.LEGACY);
-
-    EcdsaPrivateKey rawPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P384, HashType.SHA512, EcdsaSignatureEncoding.DER);
-    Key raw =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                rawPrivateKey.getPublicKey(),
-                new EcdsaVerifyKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-            3,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-
-    EcdsaPrivateKey crunchyPrivateKey =
-        TestUtil.generateEcdsaPrivKey(
-            EllipticCurveType.NIST_P384, HashType.SHA512, EcdsaSignatureEncoding.DER);
-    Key crunchy =
-        TestUtil.createKey(
-            TestUtil.createKeyData(
-                crunchyPrivateKey.getPublicKey(),
-                new EcdsaVerifyKeyManager().getKeyType(),
-                KeyData.KeyMaterialType.ASYMMETRIC_PUBLIC),
-            4,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.CRUNCHY);
-
-    Key[] keys = new Key[] {tink, legacy, raw, crunchy};
-    EcdsaPrivateKey[] privateKeys =
-        new EcdsaPrivateKey[] {tinkPrivateKey, legacyPrivateKey, rawPrivateKey, crunchyPrivateKey};
-
-    int j = keys.length;
-    for (int i = 0; i < j; i++) {
-      KeysetHandle keysetHandle =
-          TestUtil.createKeysetHandle(
-              TestUtil.createKeyset(
-                  keys[i], keys[(i + 1) % j], keys[(i + 2) % j], keys[(i + 3) % j]));
-      PublicKeyVerify verifier = keysetHandle.getPrimitive(PublicKeyVerify.class);
-      // Signature from any keys in the keyset should be valid.
-      for (int k = 0; k < j; k++) {
-        PublicKeySign signer =
-            TestUtil.createKeysetHandle(
-                    TestUtil.createKeyset(
-                        TestUtil.createKey(
-                            TestUtil.createKeyData(
-                                privateKeys[k],
-                                new EcdsaSignKeyManager().getKeyType(),
-                                KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-                            keys[k].getKeyId(),
-                            KeyStatusType.ENABLED,
-                            keys[k].getOutputPrefixType())))
-                .getPrimitive(PublicKeySign.class);
-        byte[] plaintext = Random.randBytes(1211);
-        byte[] sig = signer.sign(plaintext);
-        try {
-          verifier.verify(sig, plaintext);
-        } catch (GeneralSecurityException ex) {
-          throw new AssertionError("Valid signature, should not throw exception: " + k, ex);
-        }
-      }
-
-      // Signature from a random key should be invalid.
-      EcdsaPrivateKey randomPrivKey =
-          TestUtil.generateEcdsaPrivKey(
-              EllipticCurveType.NIST_P521, HashType.SHA512, EcdsaSignatureEncoding.DER);
-      PublicKeySign signer =
-          TestUtil.createKeysetHandle(
-                  TestUtil.createKeyset(
-                      TestUtil.createKey(
-                          TestUtil.createKeyData(
-                              randomPrivKey,
-                              new EcdsaSignKeyManager().getKeyType(),
-                              KeyData.KeyMaterialType.ASYMMETRIC_PRIVATE),
-                          1,
-                          KeyStatusType.ENABLED,
-                          keys[0].getOutputPrefixType())))
-              .getPrimitive(PublicKeySign.class);
-      byte[] plaintext = Random.randBytes(1211);
-      byte[] sig = signer.sign(plaintext);
-      assertThrows(GeneralSecurityException.class, () -> verifier.verify(sig, plaintext));
-    }
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapperTest.java
index 4c60613..69ed78b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/PublicKeyVerifyWrapperTest.java
@@ -37,6 +37,7 @@
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Bytes;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.util.List;
@@ -109,7 +110,7 @@
 
     wrappedVerifier.verify(sig, data);
 
-    byte[] sigWithTinkPrefix = Bytes.concat(TestUtil.hexDecode("0166AABBCC"), sig);
+    byte[] sigWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), sig);
     assertThrows(
         GeneralSecurityException.class, () -> wrappedVerifier.verify(sigWithTinkPrefix, data));
     assertThrows(
@@ -139,10 +140,10 @@
 
     byte[] data = "data".getBytes(UTF_8);
     byte[] sig = rawSigner.sign(data);
-    byte[] sigWithTinkPrefix = Bytes.concat(TestUtil.hexDecode("0166AABBCC"), sig);
+    byte[] sigWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), sig);
     wrappedVerifier.verify(sigWithTinkPrefix, data);
 
-    byte[] sigWithCrunchyPrefix = Bytes.concat(TestUtil.hexDecode("0066AABBCC"), sig);
+    byte[] sigWithCrunchyPrefix = Bytes.concat(Hex.decode("0066AABBCC"), sig);
     assertThrows(
         GeneralSecurityException.class, () -> wrappedVerifier.verify(sigWithCrunchyPrefix, data));
     assertThrows(GeneralSecurityException.class, () -> wrappedVerifier.verify(sig, data));
@@ -175,10 +176,10 @@
 
     byte[] data = "data".getBytes(UTF_8);
     byte[] sig = rawSigner.sign(data);
-    byte[] sigWithCrunchyPrefix = Bytes.concat(TestUtil.hexDecode("0066AABBCC"), sig);
+    byte[] sigWithCrunchyPrefix = Bytes.concat(Hex.decode("0066AABBCC"), sig);
     wrappedVerifier.verify(sigWithCrunchyPrefix, data);
 
-    byte[] sigWithTinkPrefix = Bytes.concat(TestUtil.hexDecode("0166AABBCC"), sig);
+    byte[] sigWithTinkPrefix = Bytes.concat(Hex.decode("0166AABBCC"), sig);
     assertThrows(
         GeneralSecurityException.class, () -> wrappedVerifier.verify(sigWithTinkPrefix, data));
     assertThrows(GeneralSecurityException.class, () -> wrappedVerifier.verify(sig, data));
@@ -211,15 +212,14 @@
     PublicKeyVerify wrappedVerifier = new PublicKeyVerifyWrapper().wrap(primitives);
 
     byte[] data = "data".getBytes(UTF_8);
-    byte[] dataToSign = Bytes.concat(data, TestUtil.hexDecode("00"));
-    byte[] legacySig =
-        Bytes.concat(TestUtil.hexDecode("0066AABBCC"), rawSigner.sign(dataToSign));
+    byte[] dataToSign = Bytes.concat(data, Hex.decode("00"));
+    byte[] legacySig = Bytes.concat(Hex.decode("0066AABBCC"), rawSigner.sign(dataToSign));
     wrappedVerifier.verify(legacySig, data);
 
     assertThrows(
         GeneralSecurityException.class, () -> wrappedVerifier.verify(legacySig, dataToSign));
 
-    byte[] crunchySig = Bytes.concat(TestUtil.hexDecode("0066AABBCC"), rawSigner.sign(data));
+    byte[] crunchySig = Bytes.concat(Hex.decode("0066AABBCC"), rawSigner.sign(data));
     assertThrows(
         GeneralSecurityException.class, () -> wrappedVerifier.verify(crunchySig, data));
     assertThrows(
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1ParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1ParametersTest.java
new file mode 100644
index 0000000..212893e
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1ParametersTest.java
@@ -0,0 +1,339 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RsaSsaPkcs1ParametersTest {
+
+  @Test
+  public void buildWithNoPrefixAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getModulusSizeBits()).isEqualTo(2048);
+    assertThat(parameters.getPublicExponent()).isEqualTo(RsaSsaPkcs1Parameters.F4);
+    assertThat(parameters.getHashType()).isEqualTo(RsaSsaPkcs1Parameters.HashType.SHA256);
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPkcs1Parameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPkcs1Parameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutExponent() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getPublicExponent()).isEqualTo(RsaSsaPkcs1Parameters.F4);
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPkcs1Parameters.Variant.TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithLegacyPrefix() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.LEGACY)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPkcs1Parameters.Variant.LEGACY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPkcs1Parameters.Variant.CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithLargeModulusSize() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(16789)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getModulusSizeBits()).isEqualTo(16789);
+  }
+
+  @Test
+  public void buildParametersWithValidNonF4PublicExponentSet() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(BigInteger.valueOf(1234567))
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getPublicExponent()).isEqualTo(BigInteger.valueOf(1234567));
+  }
+
+  @Test
+  public void buildParametersWithSmallPublicExponent_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(BigInteger.valueOf(3))
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithEvenPublicExponent_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(BigInteger.valueOf(1234568))
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithLargePublicExponent_works() throws Exception {
+    BigInteger largeE = BigInteger.valueOf(100000000001L);
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(largeE)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getPublicExponent()).isEqualTo(largeE);
+  }
+
+  // Public exponents larger than 2^256 are rejected. See:
+  // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, B.3
+  @Test
+  public void buildParametersWithTooLargePublicExponent_fails() throws Exception {
+    BigInteger tooLargeE = BigInteger.valueOf(2).pow(256).add(BigInteger.ONE);
+    assertThat(tooLargeE.bitLength()).isEqualTo(257);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(tooLargeE)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSha384() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(3072)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getHashType()).isEqualTo(RsaSsaPkcs1Parameters.HashType.SHA384);
+  }
+
+  @Test
+  public void buildParametersWithSha512() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(4096)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.getHashType()).isEqualTo(RsaSsaPkcs1Parameters.HashType.SHA512);
+  }
+
+  @Test
+  public void buildWithoutSettingModulusSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildWithoutSettingHashType_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(null)
+                .build());
+  }
+
+  @Test
+  public void buildWithExponentSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(null)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildWithTooSmallModulusSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2047)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    RsaSsaPkcs1Parameters parameters1 =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1Parameters parameters2 =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqual() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2049)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(BigInteger.valueOf(65539))
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                .build());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1PrivateKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1PrivateKeyTest.java
new file mode 100644
index 0000000..281de1a
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1PrivateKeyTest.java
@@ -0,0 +1,816 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.crypto.tink.util.SecretBytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.security.spec.RSAPrivateCrtKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RsaSsaPkcs1PrivateKeyTest {
+
+  // Test vector from https://www.rfc-editor.org/rfc/rfc7517#appendix-C.1
+  static final BigInteger EXPONENT = new BigInteger(1, Base64.urlSafeDecode("AQAB"));
+  static final BigInteger MODULUS =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy"
+                  + "O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP"
+                  + "8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0"
+                  + "Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X"
+                  + "OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1"
+                  + "_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q"));
+  static final BigInteger P =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf"
+                  + "QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8"
+                  + "UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws"));
+  static final BigInteger Q =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I"
+                  + "edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK"
+                  + "rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s"));
+  static final BigInteger D =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS"
+                  + "NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U"
+                  + "vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu"
+                  + "ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu"
+                  + "rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a"
+                  + "hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ"));
+  static final BigInteger DP =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3"
+                  + "tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w"
+                  + "Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c"));
+  static final BigInteger DQ =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9"
+                  + "GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy"
+                  + "mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots"));
+  static final BigInteger Q_INV =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq"
+                  + "abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o"
+                  + "Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"));
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(P);
+    assertThat(privateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(Q);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(P);
+    assertThat(privateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(Q);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void notAllValuesSet_throws() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void valuesSetToNull_throws() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(null)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()), null)
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(null)
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    null, SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(null)
+                .build());
+  }
+
+  @Test
+  public void buildFrom2048BitPrivateKeyGeneratedOnAndroidPhone() throws Exception {
+    // This key pairs was generated on an android phone.
+    // It satisfies e * d = 1 mod LCM(p-1, q-1), but e * d != 1 mod (p-1)(q-1).
+    BigInteger modulus =
+        new BigInteger(
+            "b3795dceabcbd81fc437fd1bef3f441fb3e795e0def5dcb6c84d1136f1f5c552bcb549fc925a0bd84fba5014565a46e89c1b0f198323ddd6c74931eef6551414651d224965e880136a1ef0f58145aa1d801cf9abe8afcd79d18b71e992a440dac72e020622d707e39ef02422b3b5b60eee19e39262bef2c83384370d5af82208c905341cf3445357ebed8534e5d09e7e3faab0029eb72c4d67b784023dc3853601f46d8a76640c0cb70e32a7e1a915f64418b9872f90639e07c9c58cb6da7138ec00edceb95871f25b6d58541df81a05c20336ecb03d68f118e758fc8399c5afa965de8b3e6e2cffe05368c0c2e8f8d7651bc0595c315ad5ffc5e9181226a5d5",
+            16);
+    BigInteger e = new BigInteger("65537", 10);
+    BigInteger d =
+        new BigInteger(
+            "3221514782158521239046688407258406330028553231891834758638194651218489349712866325521438421714836367531316613927931498512071990193965798572643232627837201196644319517052327671563822639251731918047441576305607916660284178027387674162132050160094809919355636813793351064368082273962217034909172344404581974193241939373282144264114913662260588365672363893632683074989847367188654224412555194872230331733391324889200933302437700487142724975686901108577545454632839147323098141162449990768306604007013959695761622579370899486808808004842820432382650026507647986123784123174922931280866259315314620233905351359011687391313",
+            10);
+    BigInteger p =
+        new BigInteger(
+            "158774943353490113489753012135278111098541279368787638170427666092698662171983127156976037521575652098385551704113475827318417186165950163951987243985985522595184323477005539699476104661027759513072140468348507403972716866975866335912344241205454260491734974839813729609658331285715361068926273165265719385439",
+            10);
+    BigInteger q =
+        new BigInteger(
+            "142695718417290075651435513804876109623436685476916701891113040095977093917632889732962474426931910603260254832314306994757612331416172717945809235744856009131743301134864401372069413649983267047705657073804311818666915219978411279698814772814372316278090214109479349638211641740638165276131916195227128960331",
+            10);
+    BigInteger dp =
+        new BigInteger(
+            "54757332036492112014516953480958174268721943273163834138395198270094376648475863100263551887676471134286132102726288671270440594499638457751236945367826491626048737037509791541992445756573377184101446798993133105644007913505173122423833934109368405566843064243548986322802349874418093456823956331253120978221",
+            10);
+    BigInteger dq =
+        new BigInteger(
+            "4123864239778253555759629875435789731400416288406247362280362206719572392388981692085858775418603822002455447341246890276804213737312222527570116003185334716198816124470652855618955238309173562847773234932715360552895882122146435811061769377762503120843231541317940830596042685151421106138423322302824087933",
+            10);
+    BigInteger crt =
+        new BigInteger(
+            "43369284071361709125656993969231593842392884522437628906059039642593092160995429320609799019215633408868044592180219813214250943675517000006014828230986217788818608645218728222984926523616075543476651226972790298584420864753413872673062587182578776079528269917000933056174453680725934830997227408181738889955",
+            10);
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(e)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(modulus).build();
+
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(p, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(d, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(dp, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(dq, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(SecretBigInteger.fromBigInteger(crt, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey).isNotNull();
+  }
+
+  @Test
+  public void buildFromJavaRSAKeys() throws Exception {
+    // Create a new RSA key pair using Java's KeyPairGenerator, which gives us a
+    // RSAPublicKey and a RSAPrivateCrtKey.
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+    RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(2048, EXPONENT);
+    keyGen.initialize(spec);
+    KeyPair keyPair = keyGen.generateKeyPair();
+    RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
+    RSAPrivateCrtKey privKey = (RSAPrivateCrtKey) keyPair.getPrivate();
+
+    // Build a RsaSsaPkcs1PublicKey from a RSAPublicKey.
+    int pubKeyModulusSizeBits = pubKey.getModulus().bitLength();
+    assertThat(pubKeyModulusSizeBits).isEqualTo(2048);
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setModulusSizeBits(pubKeyModulusSizeBits)
+                    .setPublicExponent(pubKey.getPublicExponent())
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                    .build())
+            .setModulus(pubKey.getModulus())
+            .build();
+
+    // Build a RsaSsaPkcs1PrivateKey from a RsaSsaPkcs1PublicKey and a RSAPrivateCrtKey.
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(privKey.getPrimeP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(privKey.getPrimeQ(), InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrivateExponent(), InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentQ(), InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getCrtCoefficient(), InsecureSecretKeyAccess.get()))
+            .build();
+
+    // Build a RsaSsaPkcs1PrivateKey from a RSAPrivateCrtKey, without a RSAPublicKey.
+    int privKeyModulusSizeBits = privKey.getModulus().bitLength();
+    assertThat(privKeyModulusSizeBits).isEqualTo(2048);
+    RsaSsaPkcs1PrivateKey privateKey2 =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(
+                RsaSsaPkcs1PublicKey.builder()
+                    .setParameters(
+                        RsaSsaPkcs1Parameters.builder()
+                            .setModulusSizeBits(privKeyModulusSizeBits)
+                            .setPublicExponent(privKey.getPublicExponent())
+                            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                            .build())
+                    .setModulus(privKey.getModulus())
+                    .build())
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(privKey.getPrimeP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(privKey.getPrimeQ(), InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrivateExponent(), InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentQ(), InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getCrtCoefficient(), InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.equalsKey(privateKey2)).isTrue();
+
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+
+    // Convert RsaSsaPkcs1PublicKey back into a RSAPublicKey.
+    RSAPublicKey pubKey2 =
+        (RSAPublicKey)
+            keyFactory.generatePublic(
+                new RSAPublicKeySpec(
+                    publicKey.getModulus(), publicKey.getParameters().getPublicExponent()));
+    assertThat(pubKey2.getModulus()).isEqualTo(pubKey.getModulus());
+    assertThat(pubKey2.getPublicExponent()).isEqualTo(pubKey.getPublicExponent());
+
+    // Convert RsaSsaPkcs1PrivateKey back into a RSAPrivateCrtKey.
+    BigInteger e = privateKey.getPublicKey().getParameters().getPublicExponent();
+    BigInteger n = privateKey.getPublicKey().getModulus();
+    BigInteger p = privateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger q = privateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger d = privateKey.getPrivateExponent().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger dp = privateKey.getPrimeExponentP().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger dq = privateKey.getPrimeExponentQ().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger crt = privateKey.getCrtCoefficient().getBigInteger(InsecureSecretKeyAccess.get());
+    RSAPrivateCrtKey privKey2 =
+        (RSAPrivateCrtKey)
+            keyFactory.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, dp, dq, crt));
+    assertThat(privKey2.getPrivateExponent()).isEqualTo(privKey.getPrivateExponent());
+    assertThat(privKey2.getPrimeExponentP()).isEqualTo(privKey.getPrimeExponentP());
+    assertThat(privKey2.getPrimeExponentQ()).isEqualTo(privKey.getPrimeExponentQ());
+    assertThat(privKey2.getCrtCoefficient()).isEqualTo(privKey.getCrtCoefficient());
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> RsaSsaPkcs1PrivateKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutPublicKey_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void buildValidatesAllValues() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    // Check that build fails if any value is increased by 1.
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS.add(BigInteger.ONE)) // modulus is one off
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(
+                        P.add(BigInteger.ONE), InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(
+                        Q.add(BigInteger.ONE), InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(
+                        DP.add(BigInteger.ONE), InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(
+                        DQ.add(BigInteger.ONE), InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(
+                        Q_INV.add(BigInteger.ONE), InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    RsaSsaPkcs1Parameters noPrefixParameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1PublicKey noPrefixPublicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(noPrefixParameters)
+            .setModulus(MODULUS)
+            .build();
+
+    RsaSsaPkcs1Parameters noPrefixParametersWithSha512 =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+
+    RsaSsaPkcs1Parameters tinkPrefixParameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    RsaSsaPkcs1PublicKey tinkPrefixPublicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(tinkPrefixParameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(1907)
+            .build();
+
+    // d2 = d + (p-1)(q-1) is also a valid d, yet if we change d to d2 the Key will be considered
+    // different.
+    BigInteger d2 = D.add(P.subtract(BigInteger.ONE).multiply(Q.subtract(BigInteger.ONE)));
+
+    Ed25519PublicKey ed25519PublicKey =
+        Ed25519PublicKey.create(
+            Bytes.copyFrom(
+                Hex.decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")));
+    Ed25519PrivateKey ed25519PrivateKey =
+        Ed25519PrivateKey.create(
+            ed25519PublicKey,
+            SecretBytes.copyFrom(
+                Hex.decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"),
+                InsecureSecretKeyAccess.get()));
+
+    new KeyTester()
+        .addEqualityGroup(
+            "Unmodified",
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build(),
+            // the same key built twice must be equal
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(
+                            RsaSsaPkcs1Parameters.builder()
+                                .setModulusSizeBits(2048)
+                                .setPublicExponent(EXPONENT)
+                                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                                .build())
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        // This group checks that a private key where p and q are swapped is considered different
+        .addEqualityGroup(
+            "p and q swapped",
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(P.modInverse(Q), InsecureSecretKeyAccess.get()))
+                .build())
+        // Different d is considered a different key.
+        .addEqualityGroup(
+            "Different d",
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(d2, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        // This group checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "SHA512",
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPkcs1PublicKey.builder()
+                        .setParameters(noPrefixParametersWithSha512)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        .addEqualityGroup(
+            "Tink Prefix",
+            RsaSsaPkcs1PrivateKey.builder()
+                .setPublicKey(tinkPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        .addEqualityGroup("Other key type", ed25519PrivateKey)
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1ProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1ProtoSerializationTest.java
new file mode 100644
index 0000000..a0a03c0
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1ProtoSerializationTest.java
@@ -0,0 +1,948 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.proto.RsaSsaPkcs1Params;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for RsaSsaPkcs1ProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class RsaSsaPkcs1ProtoSerializationTest {
+
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey";
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PublicKey";
+
+  private static final byte[] ensureLeadingZeroBit(byte[] minimalEncodedBigInteger) {
+    if (minimalEncodedBigInteger[0] < 0) {
+      // add a leading zero to the encoding
+      byte[] twosComplementEncoded = new byte[minimalEncodedBigInteger.length + 1];
+      System.arraycopy(
+          minimalEncodedBigInteger, 0, twosComplementEncoded, 1, minimalEncodedBigInteger.length);
+      return twosComplementEncoded;
+    }
+    return minimalEncodedBigInteger;
+  }
+
+  @Test
+  public void ensureLeadingZeroBit_works() throws Exception {
+    // 258 = 1 * 256 + 2.
+    // If the most significant bit is not set, there is no leading zero.
+    byte[] encodingOf258 = new byte[] {(byte) 1, (byte) 2};
+    assertThat(ensureLeadingZeroBit(encodingOf258)).isEqualTo(encodingOf258);
+
+    // If the most significant bit is set, then a leading zero is added.
+    byte[] encodingOf255 = new byte[] {(byte) 0xff};
+    byte[] twoComplementEncodingOf255 = new byte[] {(byte) 0, (byte) 0xff};
+    assertThat(ensureLeadingZeroBit(encodingOf255)).isEqualTo(twoComplementEncodingOf255);
+  }
+
+  // Test vector from https://www.rfc-editor.org/rfc/rfc7517#appendix-C.1
+  //
+  // Note that these test vectors use the minimal big-endian encoding of big integers. We however
+  // use BigInteger.toByteArray() to encode big integers, which is the two-complement encoding.
+  // This sometimes adds a leading zero to the encodings.
+  // see: https://docs.oracle.com/javase/7/docs/api/java/math/BigInteger.html#toByteArray()
+  static final byte[] EXPONENT_BYTES = ensureLeadingZeroBit(Base64.urlSafeDecode("AQAB"));
+  static final BigInteger EXPONENT = new BigInteger(1, EXPONENT_BYTES);
+  static final byte[] MODULUS_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy"
+                  + "O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP"
+                  + "8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0"
+                  + "Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X"
+                  + "OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1"
+                  + "_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q"));
+  static final BigInteger MODULUS = new BigInteger(1, MODULUS_BYTES);
+  static final byte[] P_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf"
+                  + "QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8"
+                  + "UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws"));
+  static final BigInteger P = new BigInteger(1, P_BYTES);
+  static final byte[] Q_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I"
+                  + "edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK"
+                  + "rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s"));
+  static final BigInteger Q = new BigInteger(1, Q_BYTES);
+  static final byte[] D_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS"
+                  + "NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U"
+                  + "vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu"
+                  + "ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu"
+                  + "rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a"
+                  + "hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ"));
+  static final BigInteger D = new BigInteger(1, D_BYTES);
+  static final byte[] DP_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3"
+                  + "tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w"
+                  + "Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c"));
+  static final BigInteger DP = new BigInteger(1, DP_BYTES);
+  static final byte[] DQ_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9"
+                  + "GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy"
+                  + "mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots"));
+  static final BigInteger DQ = new BigInteger(1, DQ_BYTES);
+  static final byte[] Q_INV_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq"
+                  + "abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o"
+                  + "Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"));
+  static final BigInteger Q_INV = new BigInteger(1, Q_INV_BYTES);
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    RsaSsaPkcs1ProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_sha256_no_prefix_equal() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_sha512_tink_equal() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA512).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_sha384_legacy_equal() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.LEGACY)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.LEGACY,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA384).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_sha256_crunchy_equal() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.CRUNCHY)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializePublicParseKey_sha384_no_prefix_equal() throws Exception {
+    RsaSsaPkcs1PublicKey key =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                    .build())
+            .setModulus(MODULUS)
+            .build();
+    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+            .setVersion(0)
+            .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA384))
+            .setN(ByteString.copyFrom(MODULUS_BYTES))
+            .setE(ByteString.copyFrom(EXPONENT_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializePublicParseKey_sha256_tink_equal() throws Exception {
+    RsaSsaPkcs1PublicKey key =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                    .build())
+            .setModulus(MODULUS)
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey protoPublicKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+            .setVersion(0)
+            .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+            .setN(ByteString.copyFrom(MODULUS_BYTES))
+            .setE(ByteString.copyFrom(EXPONENT_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePrivateKey_sha384_no_prefix_equal() throws Exception {
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                    .build())
+            .setModulus(MODULUS)
+            .build();
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+
+    com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                    .setVersion(0)
+                    .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA384))
+                    .setN(ByteString.copyFrom(MODULUS_BYTES))
+                    .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                    .build())
+            .setD(ByteString.copyFrom(D_BYTES))
+            .setP(ByteString.copyFrom(P_BYTES))
+            .setQ(ByteString.copyFrom(Q_BYTES))
+            .setDp(ByteString.copyFrom(DP_BYTES))
+            .setDq(ByteString.copyFrom(DQ_BYTES))
+            .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePrivateKey_sha512_tink_equal() throws Exception {
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                    .build())
+            .setModulus(MODULUS)
+            .setIdRequirement(123)
+            .build();
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+
+    com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                    .setVersion(0)
+                    .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA512))
+                    .setN(ByteString.copyFrom(MODULUS_BYTES))
+                    .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                    .build())
+            .setD(ByteString.copyFrom(D_BYTES))
+            .setP(ByteString.copyFrom(P_BYTES))
+            .setQ(ByteString.copyFrom(Q_BYTES))
+            .setDp(ByteString.copyFrom(DP_BYTES))
+            .setDq(ByteString.copyFrom(DQ_BYTES))
+            .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.parser(), serialized, serialization);
+  }
+
+  // A keyset that contains a key generated in Python, which is based on C++. Big ints in C++
+  // are encoded using the minimal encoding, and therefore may have the first bit set to 1.
+  // In this key here, there are several such values, for example the factor "p". The test below
+  // verifies that the value of "p" will get encoded differently.
+  private static final String JSON_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1641152230,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey\","
+          + "        \"value\": \"QoACGwosE5u2kgqsgur5eBYUTK8slJ4zjjmXBI2xCKIixqtULDfgXh2ILuZ"
+          + "7y7Myt/fmjvA4QajmKMZOHFuf6h+Z1kQN+IpKxM64RhbCoaAEc+yH5Xr75V3/qzsPW1IxcM3WVmbLn+b"
+          + "gObk3snAB9sYS8ryL1YsexZcCoshmH3ImZ/egFx6c4opPUw1jYBdMon4V+RukubFm2RRgROZRw7CZh/N"
+          + "CqYEwzbdTvPgR+g/Kbruo6yLLY4Dksq9zsM8hlhNSPaGpCjzbmBsAwT6ayEyjusGWcVB79kDaI34Y3+7"
+          + "EZ2J/4nn1D07bJGCvWz60SIVRF58beeUrc+LONKAHllx00TqAAha2k6mwibOpjfrmGpgqMTKiYsqPmJX"
+          + "w+I8MaOprCzEovsnEyLrrWFpZytoaJEEZ7SBRKavV0S/B+mSc2fTfvsF2NynbHKB62z6A5ODl6YWeF0n"
+          + "yjM7NCcxNAce/iMUdZ1qcyOGsjTWDQnp0G2cgtU3AqDjKlvodrx87DxdJB8T/cLKPpEZMbtG4TDHw2zl"
+          + "jFtdrDj38JjDN6gR3zUKhtdz8qjPD5x5K5ePQ2oakI72AuXIqCZNjGSa7rs/T8Mnv+5Uqqh2SuSQ2KvR"
+          + "Fmts6it3WSMTrQZGQdhMB7rW1h5+LqioVjc1EQyMibFHUshSvjyKfw0Pvv7YKbvv606AoIgEygAKXsLn"
+          + "L7TxNSYbgG65K3g+4LVmkbwyTp4R6XM6ilZS8S2Ypqin5P3+xZefva2vu223pC9+yULO1FUU14zZR96+"
+          + "/BpGTt3O1Psi105hi0a/ATCz4RWTeydKzxu4WP4bNZ3KJ7KsbpRVjRxIOGer38t1Igl5MnVlOZSHmWHH"
+          + "nkYBqRiu+af2xWr+fJpvHF6MyoKZ7fZwFYVE8k6BiA7mjxf87IqRzLtKSHWxR75/Rxr74rErGvAdksGU"
+          + "b5YDtaoH2XRHA4pwPNPayvls0hKsdph9XsypYfM8VCTbBoR5eJWs9N0hCkE5Q74CHfzyi1y5jhXeeFn7"
+          + "Vb7CPcJJrqLUdlGpnKoAC7wKQXuC8RIg0zAwQXubmYng/q0IPrtdTsKAkc+neoZ79oxX4bK8TeJts10P"
+          + "WXvWRmlGiKG0NN9432C36ew4f8mSmZQvwsTjgpuQF/iRFh6Eq6jU4c39y+9clMI68nXAnIeA/Es16P3w"
+          + "iw0V2BW4tpSgzB4OwnWA8YRjCHEj2jA1jOg3DaMOKM0MpXHJRpNe6D4iJKwL3fUqZAeIllmaeHgczexJ"
+          + "ed3Nt8XrArZJEIwpQrxWxTU305RHSG2gaOENPTA3IG34ObNEbOrhxJ4SbjkT/o27rpVMEQMgA+MaCGXS"
+          + "kp7IPkkDMLuxpZyHd25ECjldiT1+tXvUwxGPzTEfGgSKAAv3LCIvMyivCnsG2257pZdE57CgvN/sPUDw"
+          + "ib2zmzSjyCWepLkYOecLgvJHDLUkzClKUm5w4KnCWBD4W6iWKJqRoY1qOKxlraOeKMYPnyIpDcOcb3jn"
+          + "bNxWs+QjM/BCxczjs00D7syvw2LJq4z/sD9Z8DE5e65nn9uzmLhnjukCS9MhPSesM3JIYSrK9m7jJ7Sp"
+          + "vbRpJq+1khyns9BUldhH8Fs680g4uj7XV25tRj4wbz68BQx4AuwvhAFAsVRjjHuEzaE+ic3QLM5BY+/g"
+          + "+dY73WplALotge0A/yTO2rmwS1OyCKmxUlAjO6cKoN6W7QSl7MVKUK/BL0sa2Cxy1CCMagAQQP/mjdL4"
+          + "LePycC+amQFUv3uIimL0YQ612IbaOAeJ50VM89293EQglGPB/PNBSV8BQVEe+TiTGAifI/5uFnzVBOjH"
+          + "oOoiRI/bmP3mX6HFGd81mWX6rV8BCSkelyRhwD96OLTiPv/57xIxYT/bvPmrCIADsGTqzQ2qQtVWAq60"
+          + "KnsTQtRIhcXQ0gDPuW4iJGqMQeOAm03ewcZkul68UmJjToyziP1Dcr2KLlGGVPghs3DzfHQnvm1xwIOE"
+          + "Tzv3JWXh0PCtKeTluoXILD7RDLp0mb5ieaMRCPBYMwI23BsMd6yWWf6KfPKOOOWNCzGVL+bC+VTvjueK"
+          + "Q/5tTcUvXIIeMXtgu6nWDOX3FQfMGDvSRcM7xoLe3P40vnYWHFUdpAEbRFhTRMpoDPgRXJCd8TLRSEHi"
+          + "eedCcOSMMghehAKdzxvoRM31DuPBSKYe1Qys0ApnSs51vZLHDGkOYGbcD6Q+NdmfoE3kY0k3r+vTKDVh"
+          + "+IE0QtY2HlXHOCs7VAR5HDsKIK2x/KtD6Cvf3R667bRItIZgdA6Bf+naAoxpcWwxDXSCWsmB26wa4hrC"
+          + "1qSSRsp0zB2p6vgqDkFz7e9tCR89kzWo+oRyVdAZk5gllPA6iBVsQ6xLdoN0FoPTAbKYXHricSMGYb5K"
+          + "mbHb6sAvpw147w0aOealtndgkuu1SS0XEgRKMBCIDAQABGoAE7PMXsNlwa3uE6iDnmhmoArzugzmnJRh"
+          + "ytBzcL4dGhrIOMwQncaHNfDPsTWyfjLha6Q0TfBPiDGm0Bq+/IygQM3WKofVHuH2J7+bt4WpS0ARSQbl"
+          + "fXiXazvYAD4j4LVtBE+TuBybGB/na2ui/G48452ip+FG5V7G6sEfkxis3ETgZtyTB6oDDXXaymMoGlic"
+          + "Gsuc66BWPRiko4OvnS8PRpi0yobdw65gtggDrrD/GS4H+FVq1kEOrVKFC4UZZYyaimYnl5IS1O9Pz1vm"
+          + "5epicWptFodAFo5N0CzK/hwwcocb02CuUgxONrS3Zypw+GxyMdgRI2P/Cpihm7USCOzNxjHEmNgt7Wuw"
+          + "tQChc4ZEdlZ1KXFXXEBZf6hwLNKk5Jh7MOmJfMSU9L9J1Tqkrfls268T0FEUmD0nciLRHoeqjaD9cWxa"
+          + "h89F6r1UuCo+LVsQp4y7g/qXmxUvLvFR6JPZwHx9iyTbVEe54/P2bcgbttEIYjqgs5FLt1cG6dqjKiFx"
+          + "lC8SLZJsMg1xpZNTVe7jpzX1Ot0nK8yY/UmLUrgq0AHH31N3L9a7vg6v/uI5kdWZZoASjBlVzLNgeBCo"
+          + "QGXwFdTNENeDYCAWXEgO65K1huq3UcoJjjvCTD0tlrdTNX7q915TS3e49xgJT3lB4TynAo2Fgs9OdZta"
+          + "ovVFKpiE5K6MSAggE\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1641152230,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Test
+  public void existingTinkKeyset_reencodesNumbersUsingTwoComplement() throws Exception {
+    com.google.crypto.tink.proto.Keyset keyset = JsonKeysetReader.withString(JSON_KEYSET).read();
+    com.google.crypto.tink.proto.KeyData keyDataOfExistingKey = keyset.getKey(0).getKeyData();
+
+    com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey existingKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.parseFrom(
+            keyDataOfExistingKey.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    byte[] encodedPInExistingKey = existingKey.getP().toByteArray();
+
+    ProtoKeySerialization serializationOfExistingKey =
+        ProtoKeySerialization.create(
+            keyDataOfExistingKey.getTypeUrl(),
+            keyDataOfExistingKey.getValue(),
+            keyDataOfExistingKey.getKeyMaterialType(),
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serializationOfExistingKey, InsecureSecretKeyAccess.get());
+    ProtoKeySerialization serialized =
+        registry.serializeKey(parsed, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey serializedKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.parseFrom(
+            serialized.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    byte[] encodedPInParsedKey = serializedKey.getP().toByteArray();
+
+    // check that P is encoded differently.
+    assertThat(encodedPInParsedKey).isNotEqualTo(encodedPInExistingKey);
+    assertThat(encodedPInParsedKey).isEqualTo(ensureLeadingZeroBit(encodedPInExistingKey));
+  }
+
+  @Test
+  public void parsePrivateKey_noAccess_fails() throws Exception {
+    com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                    .setVersion(0)
+                    .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA384))
+                    .setN(ByteString.copyFrom(MODULUS_BYTES))
+                    .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                    .build())
+            .setD(ByteString.copyFrom(D_BYTES))
+            .setP(ByteString.copyFrom(P_BYTES))
+            .setQ(ByteString.copyFrom(Q_BYTES))
+            .setDp(ByteString.copyFrom(DP_BYTES))
+            .setDq(ByteString.copyFrom(DQ_BYTES))
+            .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+
+  @Test
+  public void serializePrivateKey_noAccess_throws() throws Exception {
+    RsaSsaPkcs1PublicKey publicKey =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(
+                RsaSsaPkcs1Parameters.builder()
+                    .setHashType(RsaSsaPkcs1Parameters.HashType.SHA384)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                    .build())
+            .setModulus(MODULUS)
+            .build();
+    RsaSsaPkcs1PrivateKey privateKey =
+        RsaSsaPkcs1PrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(privateKey, ProtoKeySerialization.class, /* access= */ null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // invalid hash type
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA1).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // too small public exponent
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(new byte[] {(byte) 0x03}))
+                .build()),
+        // too small modulus size in bits
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256).build())
+                .setModulusSizeInBits(123)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // unknown output prefix
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.RsaSsaPkcs1KeyFormat.newBuilder()
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256).build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(PRIVATE_TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPublicKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Public exponent too small
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(new byte[] {(byte) 0x03}))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                .setVersion(1)
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Hash type
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA1))
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPublicKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PUBLIC_KEY_SERIALIZATIONS =
+      createInvalidPublicKeySerializations();
+
+  @Theory
+  public void testParseInvalidPublicKeys_throws(
+      @FromDataPoints("invalidPublicKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPrivateKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Missing value in private key
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                // missing Q_INV
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid private values
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                // DQ_BYTES and DP_BYTES are switched
+                .setDp(ByteString.copyFrom(DQ_BYTES))
+                .setDq(ByteString.copyFrom(DP_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+                .setVersion(1)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Public key (invalid hash function)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA1))
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(RsaSsaPkcs1Params.newBuilder().setHashType(HashType.SHA256))
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPrivateKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PRIVATE_KEY_SERIALIZATIONS =
+      createInvalidPrivateKeySerializations();
+
+  @Theory
+  public void testParseInvalidPrivateKeys_throws(
+      @FromDataPoints("invalidPrivateKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1PublicKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1PublicKeyTest.java
new file mode 100644
index 0000000..22d9690
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1PublicKeyTest.java
@@ -0,0 +1,347 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RsaSsaPkcs1PublicKeyTest {
+
+  // Test vector from
+  // https://github.com/google/wycheproof/blob/master/testvectors/rsa_pkcs1_2048_test.json
+  static final BigInteger MODULUS =
+      new BigInteger(
+          "00b3510a2bcd4ce644c5b594ae5059e12b2f054b658d5da5959a2fdf1871b808bc3df3e628d2792e51aad5c1"
+              + "24b43bda453dca5cde4bcf28e7bd4effba0cb4b742bbb6d5a013cb63d1aa3a89e02627ef5398b52c0c"
+              + "fd97d208abeb8d7c9bce0bbeb019a86ddb589beb29a5b74bf861075c677c81d430f030c265247af9d3"
+              + "c9140ccb65309d07e0adc1efd15cf17e7b055d7da3868e4648cc3a180f0ee7f8e1e7b18098a3391b4c"
+              + "e7161e98d57af8a947e201a463e2d6bbca8059e5706e9dfed8f4856465ffa712ed1aa18e888d12dc6a"
+              + "a09ce95ecfca83cc5b0b15db09c8647f5d524c0f2e7620a3416b9623cadc0f097af573261c98c8400a"
+              + "a12af38e43cad84d",
+          16);
+  static final BigInteger EXPONENT = BigInteger.valueOf(65537);
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    RsaSsaPkcs1PublicKey key =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPkcs1PublicKey key =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildLegacyVariantAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.LEGACY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPkcs1PublicKey key =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.CRUNCHY)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPkcs1PublicKey key =
+        RsaSsaPkcs1PublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> RsaSsaPkcs1PublicKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPkcs1PublicKey.builder().setModulus(MODULUS).build());
+  }
+
+  @Test
+  public void buildWithoutPublicPoint_fails() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPkcs1PublicKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void parametersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    RsaSsaPkcs1Parameters parametersWithIdRequirement =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(parametersWithIdRequirement)
+                .setModulus(MODULUS)
+                .build());
+  }
+
+  @Test
+  public void parametersDoesNotRequireIdButIdIsSetInBuild_fails() throws Exception {
+    RsaSsaPkcs1Parameters parametersWithoutIdRequirement =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    assertThat(parametersWithoutIdRequirement.hasIdRequirement()).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(parametersWithoutIdRequirement)
+                .setModulus(MODULUS)
+                .setIdRequirement(0x66AABBCC)
+                .build());
+  }
+
+  @Test
+  public void modulusSizeIsValidated() throws Exception {
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(3456)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    // Modulus between 2^3455 and 2^3456 are valid.
+    BigInteger tooSmall = BigInteger.valueOf(2).pow(3455).subtract(BigInteger.ONE);
+    BigInteger smallest = BigInteger.valueOf(2).pow(3455).add(BigInteger.ONE);
+    BigInteger biggest = BigInteger.valueOf(2).pow(3456).subtract(BigInteger.ONE);
+    BigInteger tooBig = BigInteger.valueOf(2).pow(3456).add(BigInteger.ONE);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(tooSmall).build());
+    RsaSsaPkcs1PublicKey publicKeyWithSmallestModulus =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(smallest).build();
+    assertThat(publicKeyWithSmallestModulus.getModulus()).isEqualTo(smallest);
+    RsaSsaPkcs1PublicKey publicKeyWithBiggestModulus =
+        RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(biggest).build();
+    assertThat(publicKeyWithBiggestModulus.getModulus()).isEqualTo(biggest);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPkcs1PublicKey.builder().setParameters(parameters).setModulus(tooBig).build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    RsaSsaPkcs1Parameters noPrefixParameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1Parameters noPrefixParametersCopy =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1Parameters tinkPrefixParameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+            .build();
+    RsaSsaPkcs1Parameters legacyPrefixParameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.LEGACY)
+            .build();
+    RsaSsaPkcs1Parameters crunchyPrefixParameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.CRUNCHY)
+            .build();
+    RsaSsaPkcs1Parameters noPrefixParametersExponent65539 =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(BigInteger.valueOf(65539))
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    RsaSsaPkcs1Parameters noPrefixParametersSha512 =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, P256",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setModulus(MODULUS)
+                .build(),
+            // the same key built twice must be equal
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(noPrefixParametersCopy)
+                .setModulus(MODULUS)
+                .build(),
+            // setting id requirement to null is equal to not setting it
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "No prefix, different modulus",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setModulus(MODULUS.add(BigInteger.ONE))
+                .build())
+        // These groups checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "No prefix, e=65539",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(noPrefixParametersExponent65539)
+                .setModulus(MODULUS)
+                .build())
+        .addEqualityGroup(
+            "No prefix, SHA512",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(noPrefixParametersSha512)
+                .setModulus(MODULUS)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal
+        .addEqualityGroup(
+            "Tink with key id 1908",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1908)
+                .build())
+        // These 2 groups check that keys with different output prefix types are not equal
+        .addEqualityGroup(
+            "Legacy with key id 1907",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(legacyPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Crunchy with key id 1907",
+            RsaSsaPkcs1PublicKey.builder()
+                .setParameters(crunchyPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManagerTest.java
index aaa9922..67b28e0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1SignKeyManagerTest.java
@@ -17,10 +17,12 @@
 package com.google.crypto.tink.signature;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.testing.KeyTypeManagerTestUtil.testKeyTemplateCompatible;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PublicKeySign;
 import com.google.crypto.tink.PublicKeyVerify;
 import com.google.crypto.tink.internal.KeyTypeManager;
@@ -32,11 +34,11 @@
 import com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey;
 import com.google.crypto.tink.signature.internal.SigUtil;
 import com.google.crypto.tink.subtle.EngineFactory;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.RsaSsaPkcs1VerifyJce;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
@@ -45,6 +47,7 @@
 import java.security.spec.RSAPublicKeySpec;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -52,6 +55,12 @@
 /** Unit tests for RsaSsaPkcs1SignKeyManager. */
 @RunWith(JUnit4.class)
 public class RsaSsaPkcs1SignKeyManagerTest {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    RsaSsaPkcs1SignKeyManager.registerPair(/* newKeyAllowed= */ true);
+  }
+
   private final RsaSsaPkcs1SignKeyManager manager = new RsaSsaPkcs1SignKeyManager();
   private final KeyTypeManager.KeyFactory<RsaSsaPkcs1KeyFormat, RsaSsaPkcs1PrivateKey> factory =
       manager.keyFactory();
@@ -151,15 +160,26 @@
   }
 
   @Test
-  public void createKey_smallKey() throws Exception {
+  public void createKey_smallKey_works() throws Exception {
     if (TestUtil.isTsan()) {
       // factory.createKey is too slow in Tsan.
       return;
     }
-    RsaSsaPkcs1KeyFormat format = createKeyFormat(HashType.SHA256, 3072, RSAKeyGenParameterSpec.F4);
-    RsaSsaPkcs1PrivateKey key = factory.createKey(format);
-    checkConsistency(key, format);
-    checkKey(key);
+    RsaSsaPkcs1Parameters parameters =
+        RsaSsaPkcs1Parameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+            .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+            .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+            .build();
+    KeysetHandle handle = KeysetHandle.generateNew(parameters);
+    com.google.crypto.tink.Key key = handle.getAt(0).getKey();
+    assertThat(key).isInstanceOf(com.google.crypto.tink.signature.RsaSsaPkcs1PrivateKey.class);
+    com.google.crypto.tink.signature.RsaSsaPkcs1PrivateKey privateKey =
+        (com.google.crypto.tink.signature.RsaSsaPkcs1PrivateKey) key;
+
+    assertThat(privateKey.getPublicKey().getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey().getModulus().bitLength()).isEqualTo(2048);
   }
 
   @Test
@@ -189,8 +209,8 @@
     int numTests = 5;
     for (int i = 0; i < numTests; i++) {
       RsaSsaPkcs1PrivateKey key = factory.createKey(format);
-      keys.add(TestUtil.hexEncode(key.getQ().toByteArray()));
-      keys.add(TestUtil.hexEncode(key.getP().toByteArray()));
+      keys.add(Hex.encode(key.getQ().toByteArray()));
+      keys.add(Hex.encode(key.getP().toByteArray()));
     }
     assertThat(keys).hasSize(2 * numTests);
   }
@@ -234,101 +254,75 @@
   @Test
   public void testRsa3072SsaPkcs1Sha256F4Template() throws Exception {
     KeyTemplate template = RsaSsaPkcs1SignKeyManager.rsa3072SsaPkcs1Sha256F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPkcs1SignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(3072);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawRsa3072SsaPkcs1Sha256F4Template() throws Exception {
     KeyTemplate template = RsaSsaPkcs1SignKeyManager.rawRsa3072SsaPkcs1Sha256F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPkcs1SignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(3072);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA256)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testRsa4096SsaPkcs1Sha512F4Template() throws Exception {
     KeyTemplate template = RsaSsaPkcs1SignKeyManager.rsa4096SsaPkcs1Sha512F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPkcs1SignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getHashType()).isEqualTo(HashType.SHA512);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(4096);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawRsa4096SsaPkcs1Sha512F4Template() throws Exception {
     KeyTemplate template = RsaSsaPkcs1SignKeyManager.rawRsa4096SsaPkcs1Sha512F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPkcs1SignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getHashType()).isEqualTo(HashType.SHA512);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(4096);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPkcs1Parameters.builder()
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPkcs1Parameters.F4)
+                .setHashType(RsaSsaPkcs1Parameters.HashType.SHA512)
+                .setVariant(RsaSsaPkcs1Parameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testRsa3072SsaPkcs1Sha256F4TemplateWithManager() throws Exception {
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            RsaSsaPkcs1SignKeyManager.rsa3072SsaPkcs1Sha256F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPkcs1SignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(manager, RsaSsaPkcs1SignKeyManager.rsa3072SsaPkcs1Sha256F4Template());
   }
 
   @Test
   public void testRawRsa3072SsaPkcs1Sha256F4TemplateWithManager() throws Exception {
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            RsaSsaPkcs1SignKeyManager.rawRsa3072SsaPkcs1Sha256F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPkcs1SignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(
+        manager, RsaSsaPkcs1SignKeyManager.rawRsa3072SsaPkcs1Sha256F4Template());
   }
 
   @Test
   public void testRsa4096SsaPkcs1Sha512F4TemplateWithManager() throws Exception {
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            RsaSsaPkcs1SignKeyManager.rsa4096SsaPkcs1Sha512F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPkcs1SignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(manager, RsaSsaPkcs1SignKeyManager.rsa4096SsaPkcs1Sha512F4Template());
   }
 
   @Test
   public void testRawRsa4096SsaPkcs1Sha512F4TemplateWithManager() throws Exception {
-    RsaSsaPkcs1KeyFormat format =
-        RsaSsaPkcs1KeyFormat.parseFrom(
-            RsaSsaPkcs1SignKeyManager.rawRsa4096SsaPkcs1Sha512F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPkcs1SignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(
+        manager, RsaSsaPkcs1SignKeyManager.rawRsa4096SsaPkcs1Sha512F4Template());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1VerifyKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1VerifyKeyManagerTest.java
index 60c7e66..f1b985e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1VerifyKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPkcs1VerifyKeyManagerTest.java
@@ -28,6 +28,7 @@
 import com.google.crypto.tink.proto.RsaSsaPkcs1Params;
 import com.google.crypto.tink.proto.RsaSsaPkcs1PrivateKey;
 import com.google.crypto.tink.proto.RsaSsaPkcs1PublicKey;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
@@ -56,10 +57,9 @@
         String modulus, String exponent, String msg, String sig, HashType hashType)
         throws Exception {
       publicKeyProto =
-          TestUtil.createRsaSsaPkcs1PubKey(
-              TestUtil.hexDecode(modulus), TestUtil.hexDecode(exponent), hashType);
-      this.msg = TestUtil.hexDecode(msg);
-      this.sig = TestUtil.hexDecode(sig);
+          TestUtil.createRsaSsaPkcs1PubKey(Hex.decode(modulus), Hex.decode(exponent), hashType);
+      this.msg = Hex.decode(msg);
+      this.sig = Hex.decode(sig);
     }
   }
 
@@ -140,8 +140,8 @@
 
     RsaSsaPkcs1PublicKey invalidKey =
         RsaSsaPkcs1PublicKey.newBuilder(publicKey)
-            .setN(ByteString.copyFrom(TestUtil.hexDecode("23")))
-            .setE(ByteString.copyFrom(TestUtil.hexDecode("03")))
+            .setN(ByteString.copyFrom(Hex.decode("23")))
+            .setE(ByteString.copyFrom(Hex.decode("03")))
             .build();
     assertThrows(GeneralSecurityException.class, () -> verifyManager.validateKey(invalidKey));
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssParametersTest.java
new file mode 100644
index 0000000..5410687
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssParametersTest.java
@@ -0,0 +1,445 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RsaSsaPssParametersTest {
+
+  @Test
+  public void buildWithNoPrefixAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getModulusSizeBits()).isEqualTo(2048);
+    assertThat(parameters.getPublicExponent()).isEqualTo(RsaSsaPssParameters.F4);
+    assertThat(parameters.getSigHashType()).isEqualTo(RsaSsaPssParameters.HashType.SHA256);
+    assertThat(parameters.getMgf1HashType()).isEqualTo(RsaSsaPssParameters.HashType.SHA256);
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPssParameters.Variant.NO_PREFIX);
+    assertThat(parameters.getSaltLengthBytes()).isEqualTo(32);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingVariant_hasNoPrefix() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPssParameters.Variant.NO_PREFIX);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutExponent_defaultsToF4() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getPublicExponent()).isEqualTo(RsaSsaPssParameters.F4);
+  }
+
+  @Test
+  public void buildParametersWithTinkPrefix() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPssParameters.Variant.TINK);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithLegacyPrefix() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.LEGACY)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPssParameters.Variant.LEGACY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithCrunchyPrefix() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(4096)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+            .setVariant(RsaSsaPssParameters.Variant.CRUNCHY)
+            .setSaltLengthBytes(64)
+            .build();
+    assertThat(parameters.getVariant()).isEqualTo(RsaSsaPssParameters.Variant.CRUNCHY);
+    assertThat(parameters.hasIdRequirement()).isTrue();
+  }
+
+  @Test
+  public void buildParametersWithSha384() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(3072)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA384)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA384)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(48)
+            .build();
+    assertThat(parameters.getSigHashType()).isEqualTo(RsaSsaPssParameters.HashType.SHA384);
+    assertThat(parameters.getMgf1HashType()).isEqualTo(RsaSsaPssParameters.HashType.SHA384);
+  }
+
+  @Test
+  public void buildParametersWithSha512() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(4096)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(64)
+            .build();
+    assertThat(parameters.getSigHashType()).isEqualTo(RsaSsaPssParameters.HashType.SHA512);
+    assertThat(parameters.getMgf1HashType()).isEqualTo(RsaSsaPssParameters.HashType.SHA512);
+  }
+
+  @Test
+  public void buildParametersWithLargeModulusSize() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(16789)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+            .setVariant(RsaSsaPssParameters.Variant.CRUNCHY)
+            .setSaltLengthBytes(64)
+            .build();
+    assertThat(parameters.getModulusSizeBits()).isEqualTo(16789);
+  }
+
+  @Test
+  public void buildParametersWithValidNonF4PublicExponentSet() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(BigInteger.valueOf(1234567))
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getPublicExponent()).isEqualTo(BigInteger.valueOf(1234567));
+  }
+
+  @Test
+  public void buildParametersWithTooSmallModulusSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2047)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithSmallPublicExponent_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(BigInteger.valueOf(3))
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithEvenPublicExponent_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(BigInteger.valueOf(1234568))
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithLargePublicExponent_works() throws Exception {
+    BigInteger largeE = BigInteger.valueOf(100000000001L);
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(largeE)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.getPublicExponent()).isEqualTo(largeE);
+  }
+
+  // Public exponents larger than 2^256 are rejected. See:
+  // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf, B.3
+  @Test
+  public void buildParametersWithTooLargePublicExponent_fails() throws Exception {
+    BigInteger tooLargeE = BigInteger.valueOf(2).pow(256).add(BigInteger.ONE);
+    assertThat(tooLargeE.bitLength()).isEqualTo(257);
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(tooLargeE)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingModulusSize_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingSigHashType_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingMgf1HashType_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithDifferentHashTypes_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                .setSaltLengthBytes(32)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithoutSettingSaltLength_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithVariantSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(null)
+                .build());
+  }
+
+  @Test
+  public void buildParametersWithExponentSetToNull_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(null)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .build());
+  }
+
+  @Test
+  public void testEqualsAndEqualHashCode() throws Exception {
+    RsaSsaPssParameters parameters1 =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters parameters2 =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  public void testNotEqual() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2049)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(BigInteger.valueOf(65539))
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA384)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA384)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(32)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.TINK)
+                .setSaltLengthBytes(32)
+                .build());
+    assertThat(parameters)
+        .isNotEqualTo(
+            RsaSsaPssParameters.builder()
+                .setModulusSizeBits(2048)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .setSaltLengthBytes(64)
+                .build());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssPrivateKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssPrivateKeyTest.java
new file mode 100644
index 0000000..a9b65b9
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssPrivateKeyTest.java
@@ -0,0 +1,825 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.crypto.tink.util.SecretBytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.security.spec.RSAPrivateCrtKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RsaSsaPssPrivateKeyTest {
+
+  // Test vector from https://www.rfc-editor.org/rfc/rfc7517#appendix-C.1
+  static final BigInteger EXPONENT = new BigInteger(1, Base64.urlSafeDecode("AQAB"));
+  static final BigInteger MODULUS =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy"
+                  + "O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP"
+                  + "8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0"
+                  + "Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X"
+                  + "OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1"
+                  + "_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q"));
+  static final BigInteger P =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf"
+                  + "QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8"
+                  + "UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws"));
+  static final BigInteger Q =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I"
+                  + "edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK"
+                  + "rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s"));
+  static final BigInteger D =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS"
+                  + "NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U"
+                  + "vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu"
+                  + "ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu"
+                  + "rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a"
+                  + "hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ"));
+  static final BigInteger DP =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3"
+                  + "tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w"
+                  + "Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c"));
+  static final BigInteger DQ =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9"
+                  + "GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy"
+                  + "mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots"));
+  static final BigInteger Q_INV =
+      new BigInteger(
+          1,
+          Base64.urlSafeDecode(
+              "lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq"
+                  + "abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o"
+                  + "Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"));
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(P);
+    assertThat(privateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(Q);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(privateKey.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey()).isEqualTo(publicKey);
+    assertThat(privateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(P);
+    assertThat(privateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(Q);
+    assertThat(privateKey.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(privateKey.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void notAllValuesSet_throws() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void valuesSetToNull_throws() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(null)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()), null)
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(null)
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    null, SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(publicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(null)
+                .build());
+  }
+
+  @Test
+  public void buildFrom2048BitPrivateKeyGeneratedOnAndroidPhone() throws Exception {
+    // This key pairs was generated on an android phone.
+    // It satisfies e * d = 1 mod LCM(p-1, q-1), but e * d != 1 mod (p-1)(q-1).
+    BigInteger modulus =
+        new BigInteger(
+            "b3795dceabcbd81fc437fd1bef3f441fb3e795e0def5dcb6c84d1136f1f5c552bcb549fc925a0bd84fba5014565a46e89c1b0f198323ddd6c74931eef6551414651d224965e880136a1ef0f58145aa1d801cf9abe8afcd79d18b71e992a440dac72e020622d707e39ef02422b3b5b60eee19e39262bef2c83384370d5af82208c905341cf3445357ebed8534e5d09e7e3faab0029eb72c4d67b784023dc3853601f46d8a76640c0cb70e32a7e1a915f64418b9872f90639e07c9c58cb6da7138ec00edceb95871f25b6d58541df81a05c20336ecb03d68f118e758fc8399c5afa965de8b3e6e2cffe05368c0c2e8f8d7651bc0595c315ad5ffc5e9181226a5d5",
+            16);
+    BigInteger e = new BigInteger("65537", 10);
+    BigInteger d =
+        new BigInteger(
+            "3221514782158521239046688407258406330028553231891834758638194651218489349712866325521438421714836367531316613927931498512071990193965798572643232627837201196644319517052327671563822639251731918047441576305607916660284178027387674162132050160094809919355636813793351064368082273962217034909172344404581974193241939373282144264114913662260588365672363893632683074989847367188654224412555194872230331733391324889200933302437700487142724975686901108577545454632839147323098141162449990768306604007013959695761622579370899486808808004842820432382650026507647986123784123174922931280866259315314620233905351359011687391313",
+            10);
+    BigInteger p =
+        new BigInteger(
+            "158774943353490113489753012135278111098541279368787638170427666092698662171983127156976037521575652098385551704113475827318417186165950163951987243985985522595184323477005539699476104661027759513072140468348507403972716866975866335912344241205454260491734974839813729609658331285715361068926273165265719385439",
+            10);
+    BigInteger q =
+        new BigInteger(
+            "142695718417290075651435513804876109623436685476916701891113040095977093917632889732962474426931910603260254832314306994757612331416172717945809235744856009131743301134864401372069413649983267047705657073804311818666915219978411279698814772814372316278090214109479349638211641740638165276131916195227128960331",
+            10);
+    BigInteger dp =
+        new BigInteger(
+            "54757332036492112014516953480958174268721943273163834138395198270094376648475863100263551887676471134286132102726288671270440594499638457751236945367826491626048737037509791541992445756573377184101446798993133105644007913505173122423833934109368405566843064243548986322802349874418093456823956331253120978221",
+            10);
+    BigInteger dq =
+        new BigInteger(
+            "4123864239778253555759629875435789731400416288406247362280362206719572392388981692085858775418603822002455447341246890276804213737312222527570116003185334716198816124470652855618955238309173562847773234932715360552895882122146435811061769377762503120843231541317940830596042685151421106138423322302824087933",
+            10);
+    BigInteger crt =
+        new BigInteger(
+            "43369284071361709125656993969231593842392884522437628906059039642593092160995429320609799019215633408868044592180219813214250943675517000006014828230986217788818608645218728222984926523616075543476651226972790298584420864753413872673062587182578776079528269917000933056174453680725934830997227408181738889955",
+            10);
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(e)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(modulus).build();
+
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(p, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(d, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(dp, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(dq, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(SecretBigInteger.fromBigInteger(crt, InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey).isNotNull();
+  }
+
+  @Test
+  public void buildFromJavaRSAKeys() throws Exception {
+    // Create a new RSA key pair using Java's KeyPairGenerator, which gives us a
+    // RSAPublicKey and a RSAPrivateCrtKey.
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+    RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(2048, EXPONENT);
+    keyGen.initialize(spec);
+    KeyPair keyPair = keyGen.generateKeyPair();
+    RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
+    RSAPrivateCrtKey privKey = (RSAPrivateCrtKey) keyPair.getPrivate();
+
+    // Build a RsaSsaPssPublicKey from a RSAPublicKey.
+    int pubKeyModulusSizeBits = pubKey.getModulus().bitLength();
+    assertThat(pubKeyModulusSizeBits).isEqualTo(2048);
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(
+                RsaSsaPssParameters.builder()
+                    .setModulusSizeBits(pubKeyModulusSizeBits)
+                    .setPublicExponent(pubKey.getPublicExponent())
+                    .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                    .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                    .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                    .setSaltLengthBytes(32)
+                    .build())
+            .setModulus(pubKey.getModulus())
+            .build();
+
+    // Build a RsaSsaPssPrivateKey from a RsaSsaPssPublicKey and a RSAPrivateCrtKey.
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(privKey.getPrimeP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(privKey.getPrimeQ(), InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrivateExponent(), InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentQ(), InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getCrtCoefficient(), InsecureSecretKeyAccess.get()))
+            .build();
+
+    // Build a RsaSsaPssPrivateKey from a RSAPrivateCrtKey, without a RSAPublicKey.
+    int privKeyModulusSizeBits = privKey.getModulus().bitLength();
+    assertThat(privKeyModulusSizeBits).isEqualTo(2048);
+    RsaSsaPssPrivateKey privateKey2 =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(
+                RsaSsaPssPublicKey.builder()
+                    .setParameters(
+                        RsaSsaPssParameters.builder()
+                            .setModulusSizeBits(privKeyModulusSizeBits)
+                            .setPublicExponent(privKey.getPublicExponent())
+                            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                            .setSaltLengthBytes(32)
+                            .build())
+                    .setModulus(privKey.getModulus())
+                    .build())
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(privKey.getPrimeP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(privKey.getPrimeQ(), InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrivateExponent(), InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentP(), InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(
+                    privKey.getPrimeExponentQ(), InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(
+                    privKey.getCrtCoefficient(), InsecureSecretKeyAccess.get()))
+            .build();
+    assertThat(privateKey.equalsKey(privateKey2)).isTrue();
+
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+
+    // Convert RsaSsaPssPublicKey back into a RSAPublicKey.
+    RSAPublicKey pubKey2 =
+        (RSAPublicKey)
+            keyFactory.generatePublic(
+                new RSAPublicKeySpec(
+                    publicKey.getModulus(), publicKey.getParameters().getPublicExponent()));
+    assertThat(pubKey2.getModulus()).isEqualTo(pubKey.getModulus());
+    assertThat(pubKey2.getPublicExponent()).isEqualTo(pubKey.getPublicExponent());
+
+    // Convert RsaSsaPssPrivateKey back into a RSAPrivateCrtKey.
+    BigInteger e = privateKey.getPublicKey().getParameters().getPublicExponent();
+    BigInteger n = privateKey.getPublicKey().getModulus();
+    BigInteger p = privateKey.getPrimeP().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger q = privateKey.getPrimeQ().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger d = privateKey.getPrivateExponent().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger dp = privateKey.getPrimeExponentP().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger dq = privateKey.getPrimeExponentQ().getBigInteger(InsecureSecretKeyAccess.get());
+    BigInteger crt = privateKey.getCrtCoefficient().getBigInteger(InsecureSecretKeyAccess.get());
+    RSAPrivateCrtKey privKey2 =
+        (RSAPrivateCrtKey)
+            keyFactory.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, dp, dq, crt));
+    assertThat(privKey2.getPrivateExponent()).isEqualTo(privKey.getPrivateExponent());
+    assertThat(privKey2.getPrimeExponentP()).isEqualTo(privKey.getPrimeExponentP());
+    assertThat(privKey2.getPrimeExponentQ()).isEqualTo(privKey.getPrimeExponentQ());
+    assertThat(privKey2.getCrtCoefficient()).isEqualTo(privKey.getCrtCoefficient());
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> RsaSsaPssPrivateKey.builder().build());
+  }
+
+  @Test
+  public void buildValidatesAllValues() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    // Check that build fails if any value is increased by 1.
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS.add(BigInteger.ONE)) // modulus is one off
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(
+                        P.add(BigInteger.ONE), InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(
+                        Q.add(BigInteger.ONE), InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(
+                        DP.add(BigInteger.ONE), InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(
+                        DQ.add(BigInteger.ONE), InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build());
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(parameters)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(
+                        Q_INV.add(BigInteger.ONE), InsecureSecretKeyAccess.get()))
+                .build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    RsaSsaPssParameters noPrefixParameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssPublicKey noPrefixPublicKey =
+        RsaSsaPssPublicKey.builder().setParameters(noPrefixParameters).setModulus(MODULUS).build();
+
+    RsaSsaPssParameters noPrefixParametersWithSha512 =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+
+    RsaSsaPssParameters tinkPrefixParameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssPublicKey tinkPrefixPublicKey =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(tinkPrefixParameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(1907)
+            .build();
+
+    // d2 = d + (p-1)(q-1) is also a valid d, yet if we change d to d2 the Key will be considered
+    // different.
+    BigInteger d2 = D.add(P.subtract(BigInteger.ONE).multiply(Q.subtract(BigInteger.ONE)));
+
+    Ed25519PublicKey ed25519PublicKey =
+        Ed25519PublicKey.create(
+            Bytes.copyFrom(
+                Hex.decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")));
+    Ed25519PrivateKey ed25519PrivateKey =
+        Ed25519PrivateKey.create(
+            ed25519PublicKey,
+            SecretBytes.copyFrom(
+                Hex.decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"),
+                InsecureSecretKeyAccess.get()));
+
+    new KeyTester()
+        .addEqualityGroup(
+            "Unmodified",
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build(),
+            // the same key built twice must be equal
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(
+                            RsaSsaPssParameters.builder()
+                                .setModulusSizeBits(2048)
+                                .setPublicExponent(EXPONENT)
+                                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                                .setSaltLengthBytes(32)
+                                .build())
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        // This group checks that a private key where p and q are swapped is considered different
+        .addEqualityGroup(
+            "p and q swapped",
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(P.modInverse(Q), InsecureSecretKeyAccess.get()))
+                .build())
+        // Different d is considered a different key.
+        .addEqualityGroup(
+            "Different d",
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(noPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(d2, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        // This group checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "SHA512",
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(
+                    RsaSsaPssPublicKey.builder()
+                        .setParameters(noPrefixParametersWithSha512)
+                        .setModulus(MODULUS)
+                        .build())
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        .addEqualityGroup(
+            "Tink Prefix",
+            RsaSsaPssPrivateKey.builder()
+                .setPublicKey(tinkPrefixPublicKey)
+                .setPrimes(
+                    SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+                .setPrivateExponent(
+                    SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+                .setPrimeExponents(
+                    SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                    SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+                .setCrtCoefficient(
+                    SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+                .build())
+        .addEqualityGroup("Other key type", ed25519PrivateKey)
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssProtoSerializationTest.java
new file mode 100644
index 0000000..9b9ff2d
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssProtoSerializationTest.java
@@ -0,0 +1,1140 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.JsonKeysetReader;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.proto.RsaSsaPssParams;
+import com.google.crypto.tink.subtle.Base64;
+import com.google.crypto.tink.util.SecretBigInteger;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ExtensionRegistryLite;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for RsaSsaPssProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class RsaSsaPssProtoSerializationTest {
+
+  private static final String PRIVATE_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey";
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.RsaSsaPssPublicKey";
+
+  private static final byte[] ensureLeadingZeroBit(byte[] minimalEncodedBigInteger) {
+    if (minimalEncodedBigInteger[0] < 0) {
+      // add a leading zero to the encoding
+      byte[] twosComplementEncoded = new byte[minimalEncodedBigInteger.length + 1];
+      System.arraycopy(
+          minimalEncodedBigInteger, 0, twosComplementEncoded, 1, minimalEncodedBigInteger.length);
+      return twosComplementEncoded;
+    }
+    return minimalEncodedBigInteger;
+  }
+
+  @Test
+  public void ensureLeadingZeroBit_works() throws Exception {
+    // 258 = 1 * 256 + 2.
+    // If the most significant bit is not set, there is no leading zero.
+    byte[] encodingOf258 = new byte[] {(byte) 1, (byte) 2};
+    assertThat(ensureLeadingZeroBit(encodingOf258)).isEqualTo(encodingOf258);
+
+    // If the most significant bit is set, then a leading zero is added.
+    byte[] encodingOf255 = new byte[] {(byte) 0xff};
+    byte[] twoComplementEncodingOf255 = new byte[] {(byte) 0, (byte) 0xff};
+    assertThat(ensureLeadingZeroBit(encodingOf255)).isEqualTo(twoComplementEncodingOf255);
+  }
+
+  // Test vector from https://www.rfc-editor.org/rfc/rfc7517#appendix-C.1
+  //
+  // Note that these test vectors use the minimal big-endian encoding of big integers. We however
+  // use BigInteger.toByteArray() to encode big integers, which is the two-complement encoding.
+  // This sometimes adds a leading zero to the encodings.
+  // see: https://docs.oracle.com/javase/7/docs/api/java/math/BigInteger.html#toByteArray()
+  static final byte[] EXPONENT_BYTES = ensureLeadingZeroBit(Base64.urlSafeDecode("AQAB"));
+  static final BigInteger EXPONENT = new BigInteger(1, EXPONENT_BYTES);
+  static final byte[] MODULUS_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy"
+                  + "O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP"
+                  + "8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0"
+                  + "Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X"
+                  + "OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1"
+                  + "_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q"));
+  static final BigInteger MODULUS = new BigInteger(1, MODULUS_BYTES);
+  static final byte[] P_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf"
+                  + "QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8"
+                  + "UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws"));
+  static final BigInteger P = new BigInteger(1, P_BYTES);
+  static final byte[] Q_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I"
+                  + "edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK"
+                  + "rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s"));
+  static final BigInteger Q = new BigInteger(1, Q_BYTES);
+  static final byte[] D_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS"
+                  + "NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U"
+                  + "vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu"
+                  + "ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu"
+                  + "rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a"
+                  + "hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ"));
+  static final BigInteger D = new BigInteger(1, D_BYTES);
+  static final byte[] DP_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3"
+                  + "tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w"
+                  + "Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c"));
+  static final BigInteger DP = new BigInteger(1, DP_BYTES);
+  static final byte[] DQ_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9"
+                  + "GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy"
+                  + "mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots"));
+  static final BigInteger DQ = new BigInteger(1, DQ_BYTES);
+  static final byte[] Q_INV_BYTES =
+      ensureLeadingZeroBit(
+          Base64.urlSafeDecode(
+              "lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq"
+                  + "abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o"
+                  + "Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"));
+  static final BigInteger Q_INV = new BigInteger(1, Q_INV_BYTES);
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    RsaSsaPssProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    RsaSsaPssProtoSerialization.register(registry);
+    RsaSsaPssProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void serializeParseParameters_sha256_no_prefix_equal() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_sha384_tink_equal() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA384)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA384)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(48)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA384)
+                        .setMgf1Hash(HashType.SHA384)
+                        .setSaltLength(48)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_sha512_legacy_equal() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPssParameters.Variant.LEGACY)
+            .setSaltLengthBytes(64)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.LEGACY,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA512)
+                        .setMgf1Hash(HashType.SHA512)
+                        .setSaltLength(64)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_sha256_crunchy_equal() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setVariant(RsaSsaPssParameters.Variant.CRUNCHY)
+            .setSaltLengthBytes(32)
+            .build();
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssKeyFormat.parser(), serialized, serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParsePublicKey_sha256_no_prefix_equal() throws Exception {
+    RsaSsaPssPublicKey key =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(
+                RsaSsaPssParameters.builder()
+                    .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                    .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                    .setSaltLengthBytes(32)
+                    .build())
+            .setModulus(MODULUS)
+            .build();
+    com.google.crypto.tink.proto.RsaSsaPssPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+            .setVersion(0)
+            .setParams(
+                RsaSsaPssParams.newBuilder()
+                    .setSigHash(HashType.SHA256)
+                    .setMgf1Hash(HashType.SHA256)
+                    .setSaltLength(32)
+                    .build())
+            .setN(ByteString.copyFrom(MODULUS_BYTES))
+            .setE(ByteString.copyFrom(EXPONENT_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_sha384_tink_equal() throws Exception {
+    RsaSsaPssPublicKey key =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(
+                RsaSsaPssParameters.builder()
+                    .setSigHashType(RsaSsaPssParameters.HashType.SHA384)
+                    .setMgf1HashType(RsaSsaPssParameters.HashType.SHA384)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPssParameters.Variant.TINK)
+                    .setSaltLengthBytes(48)
+                    .build())
+            .setModulus(MODULUS)
+            .setIdRequirement(123)
+            .build();
+    com.google.crypto.tink.proto.RsaSsaPssPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+            .setVersion(0)
+            .setParams(
+                RsaSsaPssParams.newBuilder()
+                    .setSigHash(HashType.SHA384)
+                    .setMgf1Hash(HashType.SHA384)
+                    .setSaltLength(48)
+                    .build())
+            .setN(ByteString.copyFrom(MODULUS_BYTES))
+            .setE(ByteString.copyFrom(EXPONENT_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePrivateKey_sha512_legacy_equal() throws Exception {
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(
+                RsaSsaPssParameters.builder()
+                    .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                    .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPssParameters.Variant.LEGACY)
+                    .setSaltLengthBytes(64)
+                    .build())
+            .setModulus(MODULUS)
+            .setIdRequirement(123)
+            .build();
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+
+    com.google.crypto.tink.proto.RsaSsaPssPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                    .setVersion(0)
+                    .setParams(
+                        RsaSsaPssParams.newBuilder()
+                            .setSigHash(HashType.SHA512)
+                            .setMgf1Hash(HashType.SHA512)
+                            .setSaltLength(64)
+                            .build())
+                    .setN(ByteString.copyFrom(MODULUS_BYTES))
+                    .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                    .build())
+            .setD(ByteString.copyFrom(D_BYTES))
+            .setP(ByteString.copyFrom(P_BYTES))
+            .setQ(ByteString.copyFrom(Q_BYTES))
+            .setDp(ByteString.copyFrom(DP_BYTES))
+            .setDq(ByteString.copyFrom(DQ_BYTES))
+            .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.LEGACY,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePrivateKey_sha512_crunchy_equal() throws Exception {
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(
+                RsaSsaPssParameters.builder()
+                    .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                    .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPssParameters.Variant.CRUNCHY)
+                    .setSaltLengthBytes(64)
+                    .build())
+            .setModulus(MODULUS)
+            .setIdRequirement(123)
+            .build();
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+
+    com.google.crypto.tink.proto.RsaSsaPssPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                    .setVersion(0)
+                    .setParams(
+                        RsaSsaPssParams.newBuilder()
+                            .setSigHash(HashType.SHA512)
+                            .setMgf1Hash(HashType.SHA512)
+                            .setSaltLength(64)
+                            .build())
+                    .setN(ByteString.copyFrom(MODULUS_BYTES))
+                    .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                    .build())
+            .setD(ByteString.copyFrom(D_BYTES))
+            .setP(ByteString.copyFrom(P_BYTES))
+            .setQ(ByteString.copyFrom(Q_BYTES))
+            .setDp(ByteString.copyFrom(DP_BYTES))
+            .setDq(ByteString.copyFrom(DQ_BYTES))
+            .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(privateKey)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(
+            privateKey, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.parser(), serialized, serialization);
+  }
+
+  // A keyset that contains a key generated in Python, which is based on C++. Big ints in C++
+  // are encoded using the minimal encoding, and therefore may have the first bit set to 1.
+  // In this key here, there are several such values, for example the factor "p". The test below
+  // verifies that the value of "p" will get encoded differently.
+  private static final String JSON_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1747923325,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey\","
+          + "        \"value\": \"QoAChn0ZDHN42PGzI6e/7FGZmNz/xnCUs8XXTtISZ0Xj+tXrvDU4FP0z55/"
+          + "rB1yqmQnWsJnYPwGlRPRupZYbQu6eksylwRcDdvA6CPtWtIxU+hQ1pFBwno7s2RVJKdgfr0oB47jK9/G"
+          + "Rv/tVqw/nhXUVJN9lC2nmss+nKvhzJJzEeBNX7yrkzJdgDifsimbYgxaAxThnlBwJ0vBzRstcRXcbp8R"
+          + "DmSi2Q32VmWwKTDAQWvhokDA1Q3CjKdlaqNQBDmtGXN+w2Nf9D0Nocfmy2IsR8MkKb9H27WgZfKXc8Uo"
+          + "1v/s8Kh7Z8vyXZFVI3qdRcYTdY26DS5csffPBfAdhZJvyBzqAAmuWCVqNcRi7RdXlab+92NcPRGF2CP1"
+          + "L2apfdQhsC/VVkKTcQxCDYGXEhllx/6BXfm4+oMEKOBGdbzC4gS7C4I9bVGPL5ezy7G6VoggCx18D/0r"
+          + "V32CLoe+saXHuQmjGbrlwCUQdyjjsQabGlzl0/1LMVekX8PPHZZrK3DYMxbI1malpCoTfgMpxPsXnCV0"
+          + "qgYw1kPEfJYb/ifip3UGvNLNEORkVlDjBFbLTgQ07jCHI5O519jpEVMqsINqNrR0a8DP36YsjmBjdNjo"
+          + "9d/FWQnhTW2nrJdVIYK/5km+sxwx20OzpCwW6rCmhnV1YDBP80FNL4n08jG/qsgl+mgZNa3UygAJaI/J"
+          + "k0Ozl0bdfROxlLtFLdCwRzOT2PMOWZxIX89IvxYalg6FlPbFCbN0p9XnsovJU6HjoE1N7ZbYhDnvQ7Wx"
+          + "7CnaP+eCFfoFtobxqQW9hPH0v5R4DKDK3Eje1RYZHYlFiie57cUysOXUl/q+K5a5HhDQRSo9ywU37ZR8"
+          + "I41kNaZf4RJflq9dg/uRUjtc8qlTkp53WYsJYJN3bLrOYH4JZqPk43gVfPZeEWADYEigb4ugxjG4fT5c"
+          + "p+fpRTgyCV6ZpPPaSYPZMMNh9e4lbLcvyF1rZxOTygz5dnwHsFf8yVquAl9xPOUpKUxQ49AuGV9gc3MA"
+          + "pmZY3vNomW6MNFjIXKoACtahqzJYWEeO8NJMoYrg7IOmcaXB4bjRBoJU2yCtLtoJXXhkkSztaU75IWmN"
+          + "IjaqyuYbfYE0zpGJpOf/T79P9beT7oMMU4xqW1lEGHkrKIEml5aAdfCKTvGXXwZPjSi3gGl/CSv+bQ57"
+          + "uLCOaUrx0FJoutLsCwU4PmzkSRpQ2tcTZpZsdJtx1/oJ32U8nsYMSVyJ52dNTcq1qtEuLhPuSAJzlxYB"
+          + "6FgLrEKsWhagGNWeHUssYUA+BrOAqrYGZvANpdK0akudd/V7TL6hR4fBT4twUMNm0XW8fLEVms3kgJ6o"
+          + "wEQ5P2dYJ0PIhJ9VSq0DB230GJCnn8UXi4BGKf977JyKAAsn81gkvSkao7S5MIYZp2Isvo3rtY5NfWGO"
+          + "84tXIir/hqmeIh29AeHFaViMWd9ABiBCM3qWggCIcvaZkTMbGVq02m1FUCtAw8AJolOgTnwE3LJCnwoV"
+          + "l6RQCQ93WxO8IeI3l8UEhIZlGiQPkxiXFc9bh6YSNfZLkKb2XdR/bC06L8CfiPbqNFzGxfzP6Hb0NknS"
+          + "lu/iwDCO9kIEYOfhFoDMTqIVR0RPIEyIxo92/14JDRsou3GGy6E0/LFhoqsrqC5RMFGgEWPDgQjpz97D"
+          + "deqfBKVU532GJZ3Eyj/HIzA29uHszv0Epr7s2j/zZ63OGmXJmpaU/EGN2Goy/Nq/BHGsagAQNUEAFsbO"
+          + "56C2X1vMTxOMQCiT/Xss+y0QKDPRWQ6VL4RbUZrwmqXkFAun5V3+FW26yQInBUEnqbKMN8exH4yw6Rpm"
+          + "zDa88NEk6fnOOPvGhwdrHlSrIcxLYbIm48FO/ln/A1hTyrn/e0ASliDMfWuhY/oV62bbQQz9PovZeXS9"
+          + "2uvFTwaHTDDNF/CNFB2AuB1NhIGzDQATwBW3FPUFuOGJ/IpdEFsiyc0zXBSY/sLUyr9+Q3be/H9cAsiu"
+          + "Kl3x544++O6v9qM/Cy4CrsZfP3Gs7QzjsQUjEA3k7OXRl/Sk+7QApHJvdwGhmIxA9cZLb18cv110fP/2"
+          + "UWQnhvHAhS72XoaVSFV3qb9JhU1I8jKupiWk9M8143YxLQBK3z5qVui1FXJ8m0hzEh4FGnrc0X0cTgLw"
+          + "kcAT1yWcUz5byGDLkxuRSSKEmn9k6Kp6AbASNAyMHjJ9cOJPWYUY7uKmHwTE/JJ3jhboa64labSCCjFH"
+          + "2Yj3C0fu4Yg1Dhcl/BIuCsVGK4xehkfSODKbWbSdbLoTXTC9NFxFhczeLcubt4UaRnnL5q4N9FVFA8Lb"
+          + "kEYAOYycCYBVeN9CyeFn+q7kMrBdA98FwTy3yrVZ/I9eSBsSTfNDa1McKQagFz9bMu9B4DiK1WLQBB8Z"
+          + "n8T8W0K6OSfDDhIGyuWdIjiau+CP+8tVMAxKQBCIDAQABGoAEj1SlhRD8eNt/ZsfABmP+8hFpYuv8yV+"
+          + "EaxwWQWRtEQbdDaDFta0D6Qk66BII/kt88si4m675BzF+9m6RXt7YgnwDr6bxbH+mdYaYDWTcIw0JwPW"
+          + "w9y8Mkuiae23EaCJ0wRorThdHW1BySD3DC8FT9JkYIh0LdWUD7Vb81rSyN/16rZg8leEasM9KtwBdwQZ"
+          + "47Cnhadw4b/Yml08m0HNstXbL8gpWDRQXu0jI4d3um0OciGQc4lBejv0hG2YhJ2Jz2TPBkIzavsPnfbr"
+          + "NJ9Awbp6XVAKmdbetsFZyJsv4A3VwDlScSRmuD5RGay+uI7roVDbLRRPSLRCvDwwrOD2wCTRTbVB7ld5"
+          + "rMYAfek9qdZ4gonzsMjdpXx2SisEQDGgtMSP/+R4naDfUpSJcC14MkJIBScCZZehi2+Gp3Cwe29hfiMW"
+          + "IMe40oRo68Ub2Lr9k+DYafiqJpGl8Wz6/GMnrBZM50759/1AtGhoD3Qv31QvkmZG5Irq/4kxuPe9BqTV"
+          + "sPJ02kKDdmRkcZbxt71KEMC7dL3d8xZI2W8LKA9zx68jF3GKL1lSuN9+hp6uC8ZiwxyLfxO3vK7gTe1h"
+          + "pfuyRR1LLTlX/iWYbGyXjQem2KZqA9zHsFyZpsK8H1Ma9kz/DGbRe5Xv+DULceQjRmVrara1cDv62iSj"
+          + "84iVi5NcSPU0SBhhAEAQIBA==\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1747923325,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Test
+  public void existingTinkKeyset_reencodesNumbersUsingTwoComplement() throws Exception {
+    com.google.crypto.tink.proto.Keyset keyset = JsonKeysetReader.withString(JSON_KEYSET).read();
+    com.google.crypto.tink.proto.KeyData keyDataOfExistingKey = keyset.getKey(0).getKeyData();
+
+    com.google.crypto.tink.proto.RsaSsaPssPrivateKey existingKey =
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.parseFrom(
+            keyDataOfExistingKey.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    byte[] encodedPInExistingKey = existingKey.getP().toByteArray();
+
+    ProtoKeySerialization serializationOfExistingKey =
+        ProtoKeySerialization.create(
+            keyDataOfExistingKey.getTypeUrl(),
+            keyDataOfExistingKey.getValue(),
+            keyDataOfExistingKey.getKeyMaterialType(),
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serializationOfExistingKey, InsecureSecretKeyAccess.get());
+    ProtoKeySerialization serialized =
+        registry.serializeKey(parsed, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+
+    com.google.crypto.tink.proto.RsaSsaPssPrivateKey serializedKey =
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.parseFrom(
+            serialized.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    byte[] encodedPInParsedKey = serializedKey.getP().toByteArray();
+
+    // check that P is encoded differently.
+    assertThat(encodedPInParsedKey).isNotEqualTo(encodedPInExistingKey);
+    assertThat(encodedPInParsedKey).isEqualTo(ensureLeadingZeroBit(encodedPInExistingKey));
+  }
+
+  @Test
+  public void parsePrivateKey_noAccess_fails() throws Exception {
+    com.google.crypto.tink.proto.RsaSsaPssPrivateKey protoPrivateKey =
+        com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+            .setVersion(0)
+            .setPublicKey(
+                com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                    .setVersion(0)
+                    .setParams(
+                        RsaSsaPssParams.newBuilder()
+                            .setSigHash(HashType.SHA512)
+                            .setMgf1Hash(HashType.SHA512)
+                            .setSaltLength(64)
+                            .build())
+                    .setN(ByteString.copyFrom(MODULUS_BYTES))
+                    .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                    .build())
+            .setD(ByteString.copyFrom(D_BYTES))
+            .setP(ByteString.copyFrom(P_BYTES))
+            .setQ(ByteString.copyFrom(Q_BYTES))
+            .setDp(ByteString.copyFrom(DP_BYTES))
+            .setDq(ByteString.copyFrom(DQ_BYTES))
+            .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            protoPrivateKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseKey(serialization, /* access= */ null));
+  }
+
+  @Test
+  public void serializePrivateKey_noAccess_throws() throws Exception {
+    RsaSsaPssPublicKey publicKey =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(
+                RsaSsaPssParameters.builder()
+                    .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                    .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                    .setModulusSizeBits(2048)
+                    .setPublicExponent(EXPONENT)
+                    .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                    .setSaltLengthBytes(64)
+                    .build())
+            .setModulus(MODULUS)
+            .build();
+    RsaSsaPssPrivateKey privateKey =
+        RsaSsaPssPrivateKey.builder()
+            .setPublicKey(publicKey)
+            .setPrimes(
+                SecretBigInteger.fromBigInteger(P, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(Q, InsecureSecretKeyAccess.get()))
+            .setPrivateExponent(SecretBigInteger.fromBigInteger(D, InsecureSecretKeyAccess.get()))
+            .setPrimeExponents(
+                SecretBigInteger.fromBigInteger(DP, InsecureSecretKeyAccess.get()),
+                SecretBigInteger.fromBigInteger(DQ, InsecureSecretKeyAccess.get()))
+            .setCrtCoefficient(
+                SecretBigInteger.fromBigInteger(Q_INV, InsecureSecretKeyAccess.get()))
+            .build();
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.serializeKey(privateKey, ProtoKeySerialization.class, /* access= */ null));
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // invalid hash type
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA1)
+                        .setMgf1Hash(HashType.SHA1)
+                        .setSaltLength(2)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // different hash types for signature and mgf1
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA512)
+                        .setSaltLength(32)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // too small public exponent
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(new byte[] {(byte) 0x03}))
+                .build()),
+        // negative salt length
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(-32)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(new byte[] {(byte) 0x03}))
+                .build()),
+        // too small modulus size in bits
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setModulusSizeInBits(123)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // unknown output prefix
+        ProtoParametersSerialization.create(
+            PRIVATE_TYPE_URL,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            com.google.crypto.tink.proto.RsaSsaPssKeyFormat.newBuilder()
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setModulusSizeInBits(2048)
+                .setPublicExponent(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()),
+        // Proto messages start with a VarInt, which always ends with a byte with most
+        // significant bit unset. 0x80 is hence invalid.
+        ProtoParametersSerialization.create(
+            KeyTemplate.newBuilder()
+                .setTypeUrl(PRIVATE_TYPE_URL)
+                .setOutputPrefixType(OutputPrefixType.RAW)
+                .setValue(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                .build()),
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPublicKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Public exponent too small
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(new byte[] {(byte) 0x03}))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                .setVersion(1)
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Hash type
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA1)
+                        .setMgf1Hash(HashType.SHA1)
+                        .setSaltLength(2)
+                        .build())
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Different hash types for signature and mgf1
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA384)
+                        .setSaltLength(32)
+                        .build())
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PUBLIC_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                .setVersion(0)
+                .setParams(
+                    RsaSsaPssParams.newBuilder()
+                        .setSigHash(HashType.SHA256)
+                        .setMgf1Hash(HashType.SHA256)
+                        .setSaltLength(32)
+                        .build())
+                .setN(ByteString.copyFrom(MODULUS_BYTES))
+                .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPublicKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PUBLIC_KEY_SERIALIZATIONS =
+      createInvalidPublicKeySerializations();
+
+  @Theory
+  public void testParseInvalidPublicKeys_throws(
+      @FromDataPoints("invalidPublicKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  private static ProtoKeySerialization[] createInvalidPrivateKeySerializations() {
+    try {
+      return new ProtoKeySerialization[] {
+        // Missing value in private key
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(
+                            RsaSsaPssParams.newBuilder()
+                                .setSigHash(HashType.SHA512)
+                                .setMgf1Hash(HashType.SHA512)
+                                .setSaltLength(64)
+                                .build())
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                // missing Q_INV
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid private values
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(
+                            RsaSsaPssParams.newBuilder()
+                                .setSigHash(HashType.SHA512)
+                                .setMgf1Hash(HashType.SHA512)
+                                .setSaltLength(64)
+                                .build())
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                // DQ_BYTES and DP_BYTES are switched
+                .setDp(ByteString.copyFrom(DQ_BYTES))
+                .setDq(ByteString.copyFrom(DP_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Bad Version Number (1)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+                .setVersion(1)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(
+                            RsaSsaPssParams.newBuilder()
+                                .setSigHash(HashType.SHA512)
+                                .setMgf1Hash(HashType.SHA512)
+                                .setSaltLength(64)
+                                .build())
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Unknown prefix
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(
+                            RsaSsaPssParams.newBuilder()
+                                .setSigHash(HashType.SHA512)
+                                .setMgf1Hash(HashType.SHA512)
+                                .setSaltLength(64)
+                                .build())
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.UNKNOWN_PREFIX,
+            1479),
+        // Bad Public key (invalid hash function)
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(
+                            RsaSsaPssParams.newBuilder()
+                                .setSigHash(HashType.SHA1)
+                                .setMgf1Hash(HashType.SHA1)
+                                .setSaltLength(2)
+                                .build())
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Invalid proto encoding
+        ProtoKeySerialization.create(
+            PRIVATE_TYPE_URL,
+            // Proto messages start with a VarInt, which always ends with a byte with most
+            // significant bit unset. 0x80 is hence invalid.
+            ByteString.copyFrom(new byte[] {(byte) 0x80}),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+        // Wrong Type URL
+        ProtoKeySerialization.create(
+            "WrongTypeUrl",
+            com.google.crypto.tink.proto.RsaSsaPssPrivateKey.newBuilder()
+                .setVersion(0)
+                .setPublicKey(
+                    com.google.crypto.tink.proto.RsaSsaPssPublicKey.newBuilder()
+                        .setVersion(0)
+                        .setParams(
+                            RsaSsaPssParams.newBuilder()
+                                .setSigHash(HashType.SHA256)
+                                .setMgf1Hash(HashType.SHA256)
+                                .setSaltLength(32)
+                                .build())
+                        .setN(ByteString.copyFrom(MODULUS_BYTES))
+                        .setE(ByteString.copyFrom(EXPONENT_BYTES))
+                        .build())
+                .setD(ByteString.copyFrom(D_BYTES))
+                .setP(ByteString.copyFrom(P_BYTES))
+                .setQ(ByteString.copyFrom(Q_BYTES))
+                .setDp(ByteString.copyFrom(DP_BYTES))
+                .setDq(ByteString.copyFrom(DQ_BYTES))
+                .setCrt(ByteString.copyFrom(Q_INV_BYTES))
+                .build()
+                .toByteString(),
+            KeyMaterialType.ASYMMETRIC_PRIVATE,
+            OutputPrefixType.TINK,
+            1479),
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidPrivateKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_PRIVATE_KEY_SERIALIZATIONS =
+      createInvalidPrivateKeySerializations();
+
+  @Theory
+  public void testParseInvalidPrivateKeys_throws(
+      @FromDataPoints("invalidPrivateKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssPublicKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssPublicKeyTest.java
new file mode 100644
index 0000000..25203ef
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssPublicKeyTest.java
@@ -0,0 +1,376 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.crypto.tink.util.Bytes;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RsaSsaPssPublicKeyTest {
+
+  // Test vector from
+  // https://github.com/google/wycheproof/blob/master/testvectors/rsa_pss_2048_sha256_mgf1_32_test.json
+  static final BigInteger MODULUS =
+      new BigInteger(
+          "00a2b451a07d0aa5f96e455671513550514a8a5b462ebef717094fa1fee82224e637f9746d3f7cafd31878d8"
+              + "0325b6ef5a1700f65903b469429e89d6eac8845097b5ab393189db92512ed8a7711a1253facd20f79c"
+              + "15e8247f3d3e42e46e48c98e254a2fe9765313a03eff8f17e1a029397a1fa26a8dce26f490ed812996"
+              + "15d9814c22da610428e09c7d9658594266f5c021d0fceca08d945a12be82de4d1ece6b4c03145b5d34"
+              + "95d4ed5411eb878daf05fd7afc3e09ada0f1126422f590975a1969816f48698bcbba1b4d9cae79d460"
+              + "d8f9f85e7975005d9bc22c4e5ac0f7c1a45d12569a62807d3b9a02e5a530e773066f453d1f5b4c2e9c"
+              + "f7820283f742b9d5",
+          16);
+  static final BigInteger EXPONENT = BigInteger.valueOf(65537);
+
+  @Test
+  public void buildNoPrefixVariantAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isFalse();
+    RsaSsaPssPublicKey key =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(MODULUS).build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(new byte[] {}));
+    assertThat(key.getIdRequirementOrNull()).isNull();
+  }
+
+  @Test
+  public void buildTinkVariantAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPssPublicKey key =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0166AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildLegacyVariantAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.LEGACY)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPssPublicKey key =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void buildCrunchyVariantAndGetProperties() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.CRUNCHY)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parameters.hasIdRequirement()).isTrue();
+    RsaSsaPssPublicKey key =
+        RsaSsaPssPublicKey.builder()
+            .setParameters(parameters)
+            .setModulus(MODULUS)
+            .setIdRequirement(0x66AABBCC)
+            .build();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getModulus()).isEqualTo(MODULUS);
+    assertThat(key.getOutputPrefix()).isEqualTo(Bytes.copyFrom(Hex.decode("0066AABBCC")));
+    assertThat(key.getIdRequirementOrNull()).isEqualTo(0x66AABBCC);
+  }
+
+  @Test
+  public void emptyBuild_fails() throws Exception {
+    assertThrows(GeneralSecurityException.class, () -> RsaSsaPssPublicKey.builder().build());
+  }
+
+  @Test
+  public void buildWithoutParameters_fails() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPssPublicKey.builder().setModulus(MODULUS).build());
+  }
+
+  @Test
+  public void buildWithoutPublicPoint_fails() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPssPublicKey.builder().setParameters(parameters).build());
+  }
+
+  @Test
+  public void parametersRequireIdButIdIsNotSetInBuild_fails() throws Exception {
+    RsaSsaPssParameters parametersWithIdRequirement =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parametersWithIdRequirement.hasIdRequirement()).isTrue();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPublicKey.builder()
+                .setParameters(parametersWithIdRequirement)
+                .setModulus(MODULUS)
+                .build());
+  }
+
+  @Test
+  public void parametersDoesNotRequireIdButIdIsSetInBuild_fails() throws Exception {
+    RsaSsaPssParameters parametersWithoutIdRequirement =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    assertThat(parametersWithoutIdRequirement.hasIdRequirement()).isFalse();
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            RsaSsaPssPublicKey.builder()
+                .setParameters(parametersWithoutIdRequirement)
+                .setModulus(MODULUS)
+                .setIdRequirement(0x66AABBCC)
+                .build());
+  }
+
+  @Test
+  public void modulusSizeIsValidated() throws Exception {
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(3456)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    // Modulus between 2^3455 and 2^3456 are valid.
+    BigInteger tooSmall = BigInteger.valueOf(2).pow(3455).subtract(BigInteger.ONE);
+    BigInteger smallest = BigInteger.valueOf(2).pow(3455).add(BigInteger.ONE);
+    BigInteger biggest = BigInteger.valueOf(2).pow(3456).subtract(BigInteger.ONE);
+    BigInteger tooBig = BigInteger.valueOf(2).pow(3456).add(BigInteger.ONE);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(tooSmall).build());
+    RsaSsaPssPublicKey publicKeyWithSmallestModulus =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(smallest).build();
+    assertThat(publicKeyWithSmallestModulus.getModulus()).isEqualTo(smallest);
+    RsaSsaPssPublicKey publicKeyWithBiggestModulus =
+        RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(biggest).build();
+    assertThat(publicKeyWithBiggestModulus.getModulus()).isEqualTo(biggest);
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> RsaSsaPssPublicKey.builder().setParameters(parameters).setModulus(tooBig).build());
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    RsaSsaPssParameters noPrefixParameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters noPrefixParametersCopy =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters tinkPrefixParameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.TINK)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters legacyPrefixParameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.LEGACY)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters crunchyPrefixParameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.CRUNCHY)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters noPrefixParametersExponent65539 =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(BigInteger.valueOf(65539))
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    RsaSsaPssParameters noPrefixParametersSha512 =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(2048)
+            .setPublicExponent(EXPONENT)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    new KeyTester()
+        .addEqualityGroup(
+            "No prefix, P256",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setModulus(MODULUS)
+                .build(),
+            // the same key built twice must be equal
+            RsaSsaPssPublicKey.builder()
+                .setParameters(noPrefixParametersCopy)
+                .setModulus(MODULUS)
+                .build(),
+            // setting id requirement to null is equal to not setting it
+            RsaSsaPssPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(null)
+                .build())
+        // This group checks that keys with different key bytes are not equal
+        .addEqualityGroup(
+            "No prefix, different modulus",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(noPrefixParameters)
+                .setModulus(MODULUS.add(BigInteger.ONE))
+                .build())
+        // These groups checks that keys with different parameters are not equal
+        .addEqualityGroup(
+            "No prefix, e=65539",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(noPrefixParametersExponent65539)
+                .setModulus(MODULUS)
+                .build())
+        .addEqualityGroup(
+            "No prefix, SHA512",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(noPrefixParametersSha512)
+                .setModulus(MODULUS)
+                .build())
+        .addEqualityGroup(
+            "Tink with key id 1907",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1907)
+                .build())
+        // This group checks that keys with different key ids are not equal
+        .addEqualityGroup(
+            "Tink with key id 1908",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(tinkPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1908)
+                .build())
+        // These 2 groups check that keys with different output prefix types are not equal
+        .addEqualityGroup(
+            "Legacy with key id 1907",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(legacyPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1907)
+                .build())
+        .addEqualityGroup(
+            "Crunchy with key id 1907",
+            RsaSsaPssPublicKey.builder()
+                .setParameters(crunchyPrefixParameters)
+                .setModulus(MODULUS)
+                .setIdRequirement(1907)
+                .build())
+        .doTests();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManagerTest.java
index 75a6aa0..3299859 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssSignKeyManagerTest.java
@@ -17,10 +17,12 @@
 package com.google.crypto.tink.signature;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.testing.KeyTypeManagerTestUtil.testKeyTemplateCompatible;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.KeyTemplate;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PublicKeySign;
 import com.google.crypto.tink.PublicKeyVerify;
 import com.google.crypto.tink.internal.KeyTypeManager;
@@ -32,11 +34,11 @@
 import com.google.crypto.tink.proto.RsaSsaPssPublicKey;
 import com.google.crypto.tink.signature.internal.SigUtil;
 import com.google.crypto.tink.subtle.EngineFactory;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.subtle.RsaSsaPssVerifyJce;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
@@ -45,6 +47,7 @@
 import java.security.spec.RSAPublicKeySpec;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -56,6 +59,11 @@
   private final KeyTypeManager.KeyFactory<RsaSsaPssKeyFormat, RsaSsaPssPrivateKey> factory =
       manager.keyFactory();
 
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    RsaSsaPssSignKeyManager.registerPair(/* newKeyAllowed= */ true);
+  }
+
   private static RsaSsaPssKeyFormat createKeyFormat(
       HashType sigHash,
       HashType mgf1Hash,
@@ -177,18 +185,31 @@
   }
 
   @Test
-  public void createKey_smallKey() throws Exception {
+  public void createSmallKeyUsingParameters_works() throws Exception {
     if (TestUtil.isTsan()) {
       // factory.createKey is too slow in Tsan.
       return;
     }
-    RsaSsaPssKeyFormat format =
-        createKeyFormat(HashType.SHA256, HashType.SHA256, 32, 3072, RSAKeyGenParameterSpec.F4);
-    RsaSsaPssPrivateKey key = factory.createKey(format);
-    checkConsistency(key, format);
-    checkKey(key);
+    RsaSsaPssParameters parameters =
+        RsaSsaPssParameters.builder()
+            .setModulusSizeBits(3072)
+            .setPublicExponent(RsaSsaPssParameters.F4)
+            .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+            .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+            .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+            .setSaltLengthBytes(32)
+            .build();
+    KeysetHandle handle = KeysetHandle.generateNew(parameters);
+    com.google.crypto.tink.Key key = handle.getAt(0).getKey();
+    assertThat(key).isInstanceOf(com.google.crypto.tink.signature.RsaSsaPssPrivateKey.class);
+    com.google.crypto.tink.signature.RsaSsaPssPrivateKey privateKey =
+        (com.google.crypto.tink.signature.RsaSsaPssPrivateKey) key;
+
+    assertThat(privateKey.getPublicKey().getParameters()).isEqualTo(parameters);
+    assertThat(privateKey.getPublicKey().getModulus().bitLength()).isEqualTo(3072);
   }
 
+
   @Test
   public void createKey_largeKey() throws Exception {
     if (TestUtil.isTsan()) {
@@ -216,8 +237,8 @@
     int numTests = 5;
     for (int i = 0; i < numTests; i++) {
       RsaSsaPssPrivateKey key = factory.createKey(format);
-      keys.add(TestUtil.hexEncode(key.getQ().toByteArray()));
-      keys.add(TestUtil.hexEncode(key.getP().toByteArray()));
+      keys.add(Hex.encode(key.getQ().toByteArray()));
+      keys.add(Hex.encode(key.getP().toByteArray()));
     }
     assertThat(keys).hasSize(2 * numTests);
   }
@@ -298,105 +319,81 @@
   @Test
   public void testRsa3072PssSha256F4Template() throws Exception {
     KeyTemplate template = RsaSsaPssSignKeyManager.rsa3072PssSha256F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPssSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getSigHash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getMgf1Hash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getSaltLength()).isEqualTo(32);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(3072);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setSaltLengthBytes(32)
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawRsa3072PssSha256F4Template() throws Exception {
     KeyTemplate template = RsaSsaPssSignKeyManager.rawRsa3072PssSha256F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPssSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getSigHash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getMgf1Hash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getSaltLength()).isEqualTo(32);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(3072);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA256)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA256)
+                .setSaltLengthBytes(32)
+                .setModulusSizeBits(3072)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testRsa4096PssSha512F4Template() throws Exception {
     KeyTemplate template = RsaSsaPssSignKeyManager.rsa4096PssSha512F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPssSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getSigHash()).isEqualTo(HashType.SHA512);
-    assertThat(format.getParams().getMgf1Hash()).isEqualTo(HashType.SHA512);
-    assertThat(format.getParams().getSaltLength()).isEqualTo(64);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(4096);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                .setSaltLengthBytes(64)
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.TINK)
+                .build());
   }
 
   @Test
   public void testRawRsa4096PssSha512F4Template() throws Exception {
     KeyTemplate template = RsaSsaPssSignKeyManager.rawRsa4096PssSha512F4Template();
-    assertThat(template.getTypeUrl()).isEqualTo(new RsaSsaPssSignKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.hasParams()).isTrue();
-    assertThat(format.getParams().getSigHash()).isEqualTo(HashType.SHA512);
-    assertThat(format.getParams().getMgf1Hash()).isEqualTo(HashType.SHA512);
-    assertThat(format.getParams().getSaltLength()).isEqualTo(64);
-    assertThat(format.getModulusSizeInBits()).isEqualTo(4096);
-    assertThat(new BigInteger(1, format.getPublicExponent().toByteArray()))
-        .isEqualTo(BigInteger.valueOf(65537));
+    assertThat(template.toParameters())
+        .isEqualTo(
+            RsaSsaPssParameters.builder()
+                .setSigHashType(RsaSsaPssParameters.HashType.SHA512)
+                .setMgf1HashType(RsaSsaPssParameters.HashType.SHA512)
+                .setSaltLengthBytes(64)
+                .setModulusSizeBits(4096)
+                .setPublicExponent(RsaSsaPssParameters.F4)
+                .setVariant(RsaSsaPssParameters.Variant.NO_PREFIX)
+                .build());
   }
 
   @Test
   public void testRsa3072PssSha256F4TemplateWithManager() throws Exception {
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(
-            RsaSsaPssSignKeyManager.rsa3072PssSha256F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPssSignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(manager, RsaSsaPssSignKeyManager.rsa3072PssSha256F4Template());
   }
 
   @Test
   public void testRawRsa3072PssSha256F4TemplateWithManager() throws Exception {
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(
-            RsaSsaPssSignKeyManager.rawRsa3072PssSha256F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPssSignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(manager, RsaSsaPssSignKeyManager.rawRsa3072PssSha256F4Template());
   }
 
   @Test
   public void testRsa4096PssSha512F4TemplateWithManager() throws Exception {
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(
-            RsaSsaPssSignKeyManager.rsa4096PssSha512F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPssSignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(manager, RsaSsaPssSignKeyManager.rsa4096PssSha512F4Template());
   }
 
   @Test
   public void testRawRsa4096PssSha512F4TemplateWithManager() throws Exception {
-    RsaSsaPssKeyFormat format =
-        RsaSsaPssKeyFormat.parseFrom(
-            RsaSsaPssSignKeyManager.rawRsa4096PssSha512F4Template().getValue(),
-            ExtensionRegistryLite.getEmptyRegistry());
-    new RsaSsaPssSignKeyManager().keyFactory().validateKeyFormat(format);
+    testKeyTemplateCompatible(manager, RsaSsaPssSignKeyManager.rawRsa4096PssSha512F4Template());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssVerifyKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssVerifyKeyManagerTest.java
index 7ef0a57..1b05be0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssVerifyKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/RsaSsaPssVerifyKeyManagerTest.java
@@ -28,6 +28,7 @@
 import com.google.crypto.tink.proto.RsaSsaPssParams;
 import com.google.crypto.tink.proto.RsaSsaPssPrivateKey;
 import com.google.crypto.tink.proto.RsaSsaPssPublicKey;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.protobuf.ByteString;
@@ -61,15 +62,11 @@
         HashType mgf1Hash,
         int saltLength)
         throws Exception {
-      this.msg = TestUtil.hexDecode(msg);
-      this.sig = TestUtil.hexDecode(sig);
+      this.msg = Hex.decode(msg);
+      this.sig = Hex.decode(sig);
       this.publicKeyProto =
           TestUtil.createRsaSsaPssPubKey(
-              TestUtil.hexDecode(modulus),
-              TestUtil.hexDecode(exponent),
-              sigHash,
-              mgf1Hash,
-              saltLength);
+              Hex.decode(modulus), Hex.decode(exponent), sigHash, mgf1Hash, saltLength);
     }
   }
 
@@ -162,8 +159,8 @@
 
     RsaSsaPssPublicKey invalidKey =
         RsaSsaPssPublicKey.newBuilder(publicKey)
-            .setN(ByteString.copyFrom(TestUtil.hexDecode("23")))
-            .setE(ByteString.copyFrom(TestUtil.hexDecode("03")))
+            .setN(ByteString.copyFrom(Hex.decode("23")))
+            .setE(ByteString.copyFrom(Hex.decode("03")))
             .build();
     assertThrows(GeneralSecurityException.class, () -> verifyManager.validateKey(invalidKey));
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/SignatureConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/SignatureConfigTest.java
index acfd1ed..d7f4ff0 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/SignatureConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/SignatureConfigTest.java
@@ -16,7 +16,7 @@
 
 package com.google.crypto.tink.signature;
 
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.PublicKeySign;
@@ -44,25 +44,13 @@
   @Test
   public void aaaTestInitialization() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkpublickeysign"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("SignatureConfig.registe");
-    e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkpublickeyverify"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("SignatureConfig.registe");
     String typeUrl = "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
-    e = assertThrows(GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
-    assertThat(e.toString()).contains("No key manager found");
 
     // Initialize the config.
     SignatureConfig.register();
 
     // After registration the key manager should be present.
-    Registry.getKeyManager(typeUrl, PublicKeySign.class);
+    assertNotNull(Registry.getKeyManager(typeUrl, PublicKeySign.class));
 
     // Running init() manually again should succeed.
     SignatureConfig.register();
@@ -84,7 +72,7 @@
     };
 
     for (String typeUrl : keyTypeUrlsSign) {
-      Registry.getKeyManager(typeUrl, PublicKeySign.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, PublicKeySign.class));
     }
 
     String[] keyTypeUrlsVerify = {
@@ -95,7 +83,7 @@
     };
 
     for (String typeUrl : keyTypeUrlsVerify) {
-      Registry.getKeyManager(typeUrl, PublicKeyVerify.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, PublicKeyVerify.class));
     }
   }
 
@@ -114,7 +102,7 @@
     };
 
     for (String typeUrl : keyTypeUrlsSign) {
-      Registry.getKeyManager(typeUrl, PublicKeySign.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, PublicKeySign.class));
     }
 
     String[] keyTypeUrlsVerify = {
@@ -123,7 +111,7 @@
     };
 
     for (String typeUrl : keyTypeUrlsVerify) {
-      Registry.getKeyManager(typeUrl, PublicKeyVerify.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, PublicKeyVerify.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/SignatureKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/SignatureKeyTemplatesTest.java
index 2d376b7..5e659e7 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/SignatureKeyTemplatesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/SignatureKeyTemplatesTest.java
@@ -16,9 +16,11 @@
 
 package com.google.crypto.tink.signature;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.proto.EcdsaKeyFormat;
 import com.google.crypto.tink.proto.EcdsaSignatureEncoding;
 import com.google.crypto.tink.proto.EllipticCurveType;
@@ -29,13 +31,22 @@
 import com.google.crypto.tink.proto.RsaSsaPssKeyFormat;
 import com.google.protobuf.ExtensionRegistryLite;
 import java.math.BigInteger;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests for SignatureKeyTemplates. */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public class SignatureKeyTemplatesTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    SignatureConfig.register();
+  }
+
   @Test
   public void ecdsaP256() throws Exception {
     KeyTemplate template = SignatureKeyTemplates.ECDSA_P256;
@@ -221,4 +232,59 @@
     assertEquals(
         BigInteger.valueOf(65537), new BigInteger(1, format.getPublicExponent().toByteArray()));
   }
+
+  public static class Pair {
+    public Pair(KeyTemplate template, SignatureParameters parameters) {
+      this.template = template;
+      this.parameters = parameters;
+    }
+
+    KeyTemplate template;
+    SignatureParameters parameters;
+  }
+
+  @DataPoints("EquivalentPairs")
+  public static final Pair[] TEMPLATES =
+      new Pair[] {
+        new Pair(SignatureKeyTemplates.ECDSA_P256, PredefinedSignatureParameters.ECDSA_P256),
+        new Pair(SignatureKeyTemplates.ECDSA_P384, PredefinedSignatureParameters.ECDSA_P384),
+        new Pair(SignatureKeyTemplates.ECDSA_P521, PredefinedSignatureParameters.ECDSA_P521),
+        new Pair(
+            SignatureKeyTemplates.ECDSA_P256_IEEE_P1363,
+            PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363),
+        new Pair(
+            SignatureKeyTemplates.ECDSA_P384_IEEE_P1363,
+            PredefinedSignatureParameters.ECDSA_P384_IEEE_P1363),
+        new Pair(
+            SignatureKeyTemplates.ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX,
+            PredefinedSignatureParameters.ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX),
+        new Pair(
+            SignatureKeyTemplates.ECDSA_P521_IEEE_P1363,
+            PredefinedSignatureParameters.ECDSA_P521_IEEE_P1363),
+        new Pair(SignatureKeyTemplates.ED25519, PredefinedSignatureParameters.ED25519),
+        new Pair(
+            SignatureKeyTemplates.ED25519WithRawOutput,
+            PredefinedSignatureParameters.ED25519WithRawOutput),
+        new Pair(
+            SignatureKeyTemplates.RSA_SSA_PKCS1_3072_SHA256_F4,
+            PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4),
+        new Pair(
+            SignatureKeyTemplates.RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX,
+            PredefinedSignatureParameters.RSA_SSA_PKCS1_3072_SHA256_F4_WITHOUT_PREFIX),
+        new Pair(
+            SignatureKeyTemplates.RSA_SSA_PKCS1_4096_SHA512_F4,
+            PredefinedSignatureParameters.RSA_SSA_PKCS1_4096_SHA512_F4),
+        new Pair(
+            SignatureKeyTemplates.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4,
+            PredefinedSignatureParameters.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4),
+        new Pair(
+            SignatureKeyTemplates.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4,
+            PredefinedSignatureParameters.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4),
+      };
+
+  @Theory
+  public void testParametersEqualsKeyTemplate(@FromDataPoints("EquivalentPairs") Pair p)
+      throws Exception {
+    assertThat(TinkProtoParametersFormat.parse(p.template.toByteArray())).isEqualTo(p.parameters);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/SignaturePemKeysetReaderTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/SignaturePemKeysetReaderTest.java
index ce5f2b2..14b2837 100644
--- a/java_src/src/test/java/com/google/crypto/tink/signature/SignaturePemKeysetReaderTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/SignaturePemKeysetReaderTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.signature;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.KeysetReader;
 import com.google.crypto.tink.PemKeyType;
@@ -31,9 +32,10 @@
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.proto.RsaSsaPssPublicKey;
 import com.google.crypto.tink.signature.internal.SigUtil;
-import com.google.crypto.tink.testing.TestUtil;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.protobuf.ExtensionRegistryLite;
 import java.io.BufferedReader;
+import java.io.IOException;
 import java.io.StringReader;
 import java.security.interfaces.ECPublicKey;
 import java.security.interfaces.RSAPublicKey;
@@ -133,8 +135,7 @@
     EcdsaPublicKey publicKeyProto =
         EcdsaPublicKey.parseFrom(keyData.getValue(), ExtensionRegistryLite.getEmptyRegistry());
     assertThat(publicKeyProto.getX().toByteArray())
-        .isEqualTo(
-            TestUtil.hexDecode("D4CE489428982EF343186EB90E6A04ADF41366359A508FE7AC66B283F06641AE"));
+        .isEqualTo(Hex.decode("D4CE489428982EF343186EB90E6A04ADF41366359A508FE7AC66B283F06641AE"));
   }
 
   @Test
@@ -216,6 +217,8 @@
     KeysetReader keysetReader =
         SignaturePemKeysetReader.newBuilder().addPem(pem, PemKeyType.RSA_PSS_2048_SHA256).build();
     Keyset ks = keysetReader.read();
+    // The EC public key is ignored because it has the wrong type. It is not a RSA_PSS_2048_SHA256
+    // key.
     Keyset.Key key = ks.getKey(0);
     KeyData keyData = key.getKeyData();
     RsaSsaPssPublicKey publicKeyProto =
@@ -304,4 +307,104 @@
     assertThat(ecPublicKeyProto.getY().toByteArray())
         .isEqualTo(SigUtil.toUnsignedIntByteString(ecPublicKey.getW().getAffineY()).toByteArray());
   }
+
+  @Test
+  public void emptyReader_readThrowsIOException() throws Exception {
+    KeysetReader keysetReader = SignaturePemKeysetReader.newBuilder().build();
+    assertThrows(IOException.class, keysetReader::read);
+  }
+
+  @Test
+  public void readerWithInvalidKeys_invalidKeysAreIgnored() throws Exception {
+    String ecPublicKeyPem =
+        "-----BEGIN PUBLIC KEY-----\n"
+            + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7BiT5K5pivl4Qfrt9hRhRREMUzj/\n"
+            + "8suEJ7GlMxZfvdcpbi/GhYPuJi8Gn2H1NaMJZcLZo5MLPKyyGT5u3u1VBQ==\n"
+            + "-----END PUBLIC KEY-----\n";
+
+    KeysetReader keysetReader =
+        SignaturePemKeysetReader.newBuilder()
+            .addPem("invalid", PemKeyType.ECDSA_P256_SHA256)
+            .addPem(ecPublicKeyPem, PemKeyType.ECDSA_P256_SHA256)
+            .addPem("invalid2", PemKeyType.ECDSA_P256_SHA256)
+            .build();
+    Keyset keyset = keysetReader.read();
+    assertThat(keyset.getKeyCount()).isEqualTo(1);
+  }
+
+  @Test
+  public void readerWithRsaPrivateKey_privateKeyIsIgnored() throws Exception {
+    String rsaPublicKeyPem =
+        "-----BEGIN PUBLIC KEY-----\n"
+            + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv90Xf/NN1lRGBofJQzJf\n"
+            + "lHvo6GAf25GGQGaMmD9T1ZP71CCbJ69lGIS/6akFBg6ECEHGM2EZ4WFLCdr5byUq\n"
+            + "GCf4mY4WuOn+AcwzwAoDz9ASIFcQOoPclO7JYdfo2SOaumumdb5S/7FkKJ70TGYW\n"
+            + "j9aTOYWsCcaojbjGDY/JEXz3BSRIngcgOvXBmV1JokcJ/LsrJD263WE9iUknZDhB\n"
+            + "K7y4ChjHNqL8yJcw/D8xLNiJtIyuxiZ00p/lOVUInr8C/a2C1UGCgEGuXZAEGAdO\n"
+            + "NVez52n5TLvQP3hRd4MTi7YvfhezRcA4aXyIDOv+TYi4p+OVTYQ+FMbkgoWBm5bq\n"
+            + "wQIDAQAB\n"
+            + "-----END PUBLIC KEY-----\n";
+    String rsaPrivateKeyPem =
+        "-----BEGIN RSA PRIVATE KEY-----\n"
+            + "MIIEpAIBAAKCAQEAsll1i7Arx1tosXYSyb9oxfoFlYozTGHhZ7wgvMdXV8Em6JIQ\n"
+            + "ud85iQcs9iYOaIPHzUr00x3emRW2mzAfvvli3oxxvS217GJdollxL4ao3D0kHpaI\n"
+            + "yCORt78evDWDEfVcJr6RC3b2H+pAjtaS8alXimIsgsD89vae82cOOL/JD2PaTzu7\n"
+            + "0IjIrno8WlXmb2R01WLTLM57ft188BScoOlstlJegfu6gVqPEnSONOUTX1crLhe3\n"
+            + "ukMAgVl+b7kDPABYhNWTURjGDXWwEPb+zn7NzBy31Y0TiWk9Qzd/Tz3pScseQQXn\n"
+            + "krltfwSwzSYqwzz/xaiQ0mdCXmHBnpNjVQ8ihQIDAQABAoIBAHYrXf3bEXa6syh6\n"
+            + "AkLYZzRdz5tggVLHu9C+zrYmIlILsZsBRMHTDM0lCv5hAsTvI9B7LLJBJT8rKt2y\n"
+            + "SiaAGKk6RxZAljx0hHPQbXU+9N1QSYFW3nQ1VRR5NoUfs6OPfapSM8pz3OoSjQnX\n"
+            + "VG94c39GQxWzhyifCXxeuQaS1EY0F8g9HKkSdRbvsNVF/2j+rdmWeur8swtYBDCN\n"
+            + "nBymiDhEBj/Y1Ft3R6ywC14YM/af4aDWTbhQvZYPtITdoEtOWulGkqcx0j/NlMYU\n"
+            + "SZcaG3M/6UuKXGzibtO4w9LlI00HPlBDi3fQGbezk6WyLNjcE4xj/MKFg7VosgN7\n"
+            + "XDy68tUCgYEA6FovqDcya6JxivhyVZks98e22sPARwpowI3Nt+gsF5uPcqQMvbot\n"
+            + "ACzKHjqxRJyGbioMUI8Ao20/f2PxzeI5wAtH2HPNaN6bCbBXvxlCTMCAokbHSWjW\n"
+            + "stK2PXl2cqF/51ED7EPbgxABetGyfudsx22QowSR66Sq3I8UtZnQVUMCgYEAxIBC\n"
+            + "EW2oLh9ZUKxEeMuFlMN1FJCCqIx3zeVjUtAC3Vm/VvodEL0KM7w9Y123BfeoWMnG\n"
+            + "HaqNUEZRUO/bMvaiIXVykF19NTCxym4s6eKNBwGsdWvxroRm0k37uhflt9A7iVX6\n"
+            + "HmDVPYgjLJbPmLc8+Ms5ML6Od7qXKajRFOPmSJcCgYEA28JY6s/x9013+InNkdpD\n"
+            + "ZsNU1gpo9IgK1XwJQ1TrRxTRkwtIJbZN06mJLRg0C4HDv7QzW4o1f1zXvsQnsqOy\n"
+            + "HUpOFJJKiFJq7roD8/GO/Irh3xn0aSEoV4/l37Te68KF96FvhWoU1xwvWhu1qEN4\n"
+            + "ZhLhxt2OqgJfvCXz32LwYYMCgYBVEL0JNHJw/Qs6PEksDdcXLoI509FsS9r1XE9i\n"
+            + "I0CKOHb3nTEF9QA8o0nkAUbhI3RSc477esDQNpCvPBalelV3rJNa4c35P8pHuuhg\n"
+            + "m723gcb50i/+/7xPYIkP55Z/u3p6mqi7i+nkSFIJ1IOsNe8EOV3ZtzSPqkwUMcvJ\n"
+            + "gltHowKBgQDkB76QzH3xb4jABKehkCxVxqyGLKxU7SOZpLpCc/5OHbo12u/CwlwG\n"
+            + "uAeidKZk3SJEmj0F1+Aiir2KRv+RX543VvzCtEXNkVViVrirzvjZUGKPdkMWfbF8\n"
+            + "OdD7qHPPNu5jSyaroeN6VqfbELpewhYzulMEipckEZlU4+Dxu2k1eQ==\n"
+            + "-----END RSA PRIVATE KEY-----\n";
+    KeysetReader keysetReader =
+        SignaturePemKeysetReader.newBuilder()
+            .addPem(rsaPublicKeyPem, PemKeyType.RSA_PSS_2048_SHA256)
+            .addPem(rsaPrivateKeyPem, PemKeyType.RSA_PSS_2048_SHA256)
+            .build();
+    Keyset keyset = keysetReader.read();
+    assertThat(keyset.getKeyCount()).isEqualTo(1);
+    assertThat(keyset.getKey(0).getKeyData().getKeyMaterialType())
+        .isEqualTo(KeyMaterialType.ASYMMETRIC_PUBLIC);
+  }
+
+  @Test
+  public void readerWithEcPrivateKey_privateKeyIsIgnored() throws Exception {
+    String ecPublicKeyPem =
+        "-----BEGIN PUBLIC KEY-----\n"
+            + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7BiT5K5pivl4Qfrt9hRhRREMUzj/\n"
+            + "8suEJ7GlMxZfvdcpbi/GhYPuJi8Gn2H1NaMJZcLZo5MLPKyyGT5u3u1VBQ==\n"
+            + "-----END PUBLIC KEY-----\n";
+    String ecPrivateKeyPem =
+        "-----BEGIN EC PRIVATE KEY-----"
+            + "MHcCAQEEIBZJ/P6e1I/nQiBnQxx9aYDPAjwUtbV9Nffuzfubyuw8oAoGCCqGSM49\n"
+            + "AwEHoUQDQgAEKSPVJGELbULai+viQc3Zz95+x2NiFvjsDlqmh6rDNeiVuwiwdf5l\n"
+            + "lyZ0gbLJ/vheUAwtcA2z0csWU60MfBup3Q==\n"
+            + "-----END EC PRIVATE KEY-----\n";
+
+    KeysetReader keysetReader =
+        SignaturePemKeysetReader.newBuilder()
+            .addPem(ecPublicKeyPem, PemKeyType.ECDSA_P256_SHA256)
+            .addPem(ecPrivateKeyPem, PemKeyType.ECDSA_P256_SHA256)
+            .build();
+    Keyset keyset = keysetReader.read();
+    assertThat(keyset.getKeyCount()).isEqualTo(1);
+    assertThat(keyset.getKey(0).getKeyData().getKeyMaterialType())
+        .isEqualTo(KeyMaterialType.ASYMMETRIC_PUBLIC);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/signature/SignatureTest.java b/java_src/src/test/java/com/google/crypto/tink/signature/SignatureTest.java
new file mode 100644
index 0000000..ea33cb9
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/signature/SignatureTest.java
@@ -0,0 +1,346 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.signature;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.PublicKeySign;
+import com.google.crypto.tink.PublicKeyVerify;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import com.google.crypto.tink.testing.TestUtil;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the Signature package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class SignatureTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    SignatureConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonSignatureKeyset_throws.
+  }
+
+  @DataPoints("templates")
+  public static final String[] TEMPLATES =
+      new String[] {
+        // Only use one key template from the RSA key managers because key generation is slow.
+        "RSA_SSA_PKCS1_3072_SHA256_F4",
+        "RSA_SSA_PSS_3072_SHA256_F4",
+        "ECDSA_P256",
+        "ECDSA_P256_RAW",
+        "ECDSA_P384_SHA384",
+        "ECDSA_P384_SHA512",
+        "ECDSA_P521",
+        "ED25519",
+        "ED25519_RAW",
+      };
+
+  @Theory
+  public void createSignVerify(@FromDataPoints("templates") String templateName)
+      throws Exception {
+    if (TestUtil.isTsan()) {
+      // KeysetHandle.generateNew is too slow in Tsan.
+      return;
+    }
+    KeysetHandle privateHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    KeysetHandle publicHandle = privateHandle.getPublicKeysetHandle();
+
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] sig = signer.sign(data);
+    verifier.verify(sig, data);
+
+    KeysetHandle otherPrivateHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    PublicKeyVerify otherVerifier =
+        otherPrivateHandle.getPublicKeysetHandle().getPrimitive(PublicKeyVerify.class);
+    assertThrows(
+        GeneralSecurityException.class, () -> otherVerifier.verify(sig, data));
+
+    byte[] invalid = "invalid".getBytes(UTF_8);
+    byte[] empty = "".getBytes(UTF_8);
+    assertThrows(GeneralSecurityException.class, () -> verifier.verify(sig, invalid));
+    assertThrows(GeneralSecurityException.class, () -> verifier.verify(invalid, data));
+    assertThrows(GeneralSecurityException.class, () -> verifier.verify(empty, data));
+    verifier.verify(signer.sign(empty), empty);
+  }
+
+  // Keyset with one private key for PublicKeySign, serialized in Tink's JSON format.
+  private static final String JSON_PRIVATE_KEYSET = ""
+      + "{"
+      + "  \"primaryKeyId\": 775870498,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.EcdsaPrivateKey\","
+      + "        \"value\": \"GiA/E6s6KksNXrEd9hLdStvhsmdsONgpSODH/rZsBbBDehJMIiApA+NmYiv"
+      + "xRfhMuvTKZAwqETmn+WagBP/reucEjEvXkRog1AJ5GBzf+n27xnj9KcoGllF9NIFfQrDEP99FNH+Cne4"
+      + "SBhgCEAIIAw==\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 775870498,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  // Keyset with the corresponding public key for PublicKeyVerify, serialized in Tink's JSON format.
+  private static final String JSON_PUBLIC_KEYSET = ""
+      + "{"
+      + "  \"primaryKeyId\": 775870498,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.EcdsaPublicKey\","
+      + "        \"value\": \"IiApA+NmYivxRfhMuvTKZAwqETmn+WagBP/reucEjEvXkRog1AJ5GBzf+n2"
+      + "7xnj9KcoGllF9NIFfQrDEP99FNH+Cne4SBhgCEAIIAw==\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 775870498,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Theory
+  public void readKeysetEncryptDecrypt()
+      throws Exception {
+    KeysetHandle privateHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_PRIVATE_KEYSET, InsecureSecretKeyAccess.get());
+    KeysetHandle publicHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_PUBLIC_KEYSET, InsecureSecretKeyAccess.get());
+
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] sig = signer.sign(data);
+    verifier.verify(sig, data);
+  }
+
+  // Keyset with multiple keys. The first key is the same as in JSON_PRIVATE_KEYSET. The second
+  // key is the primary key and will be used for signing.
+  private static final String JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1641152230,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.EcdsaPrivateKey\","
+          + "        \"value\": \"GiA/E6s6KksNXrEd9hLdStvhsmdsONgpSODH/rZsBbBDehJMIiApA+NmYiv"
+          + "xRfhMuvTKZAwqETmn+WagBP/reucEjEvXkRog1AJ5GBzf+n27xnj9KcoGllF9NIFfQrDEP99FNH+Cne4"
+          + "SBhgCEAIIAw==\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 775870498,"
+          + "      \"outputPrefixType\": \"TINK\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey\","
+          + "        \"value\": \"QoACGwosE5u2kgqsgur5eBYUTK8slJ4zjjmXBI2xCKIixqtULDfgXh2ILuZ"
+          + "7y7Myt/fmjvA4QajmKMZOHFuf6h+Z1kQN+IpKxM64RhbCoaAEc+yH5Xr75V3/qzsPW1IxcM3WVmbLn+b"
+          + "gObk3snAB9sYS8ryL1YsexZcCoshmH3ImZ/egFx6c4opPUw1jYBdMon4V+RukubFm2RRgROZRw7CZh/N"
+          + "CqYEwzbdTvPgR+g/Kbruo6yLLY4Dksq9zsM8hlhNSPaGpCjzbmBsAwT6ayEyjusGWcVB79kDaI34Y3+7"
+          + "EZ2J/4nn1D07bJGCvWz60SIVRF58beeUrc+LONKAHllx00TqAAha2k6mwibOpjfrmGpgqMTKiYsqPmJX"
+          + "w+I8MaOprCzEovsnEyLrrWFpZytoaJEEZ7SBRKavV0S/B+mSc2fTfvsF2NynbHKB62z6A5ODl6YWeF0n"
+          + "yjM7NCcxNAce/iMUdZ1qcyOGsjTWDQnp0G2cgtU3AqDjKlvodrx87DxdJB8T/cLKPpEZMbtG4TDHw2zl"
+          + "jFtdrDj38JjDN6gR3zUKhtdz8qjPD5x5K5ePQ2oakI72AuXIqCZNjGSa7rs/T8Mnv+5Uqqh2SuSQ2KvR"
+          + "Fmts6it3WSMTrQZGQdhMB7rW1h5+LqioVjc1EQyMibFHUshSvjyKfw0Pvv7YKbvv606AoIgEygAKXsLn"
+          + "L7TxNSYbgG65K3g+4LVmkbwyTp4R6XM6ilZS8S2Ypqin5P3+xZefva2vu223pC9+yULO1FUU14zZR96+"
+          + "/BpGTt3O1Psi105hi0a/ATCz4RWTeydKzxu4WP4bNZ3KJ7KsbpRVjRxIOGer38t1Igl5MnVlOZSHmWHH"
+          + "nkYBqRiu+af2xWr+fJpvHF6MyoKZ7fZwFYVE8k6BiA7mjxf87IqRzLtKSHWxR75/Rxr74rErGvAdksGU"
+          + "b5YDtaoH2XRHA4pwPNPayvls0hKsdph9XsypYfM8VCTbBoR5eJWs9N0hCkE5Q74CHfzyi1y5jhXeeFn7"
+          + "Vb7CPcJJrqLUdlGpnKoAC7wKQXuC8RIg0zAwQXubmYng/q0IPrtdTsKAkc+neoZ79oxX4bK8TeJts10P"
+          + "WXvWRmlGiKG0NN9432C36ew4f8mSmZQvwsTjgpuQF/iRFh6Eq6jU4c39y+9clMI68nXAnIeA/Es16P3w"
+          + "iw0V2BW4tpSgzB4OwnWA8YRjCHEj2jA1jOg3DaMOKM0MpXHJRpNe6D4iJKwL3fUqZAeIllmaeHgczexJ"
+          + "ed3Nt8XrArZJEIwpQrxWxTU305RHSG2gaOENPTA3IG34ObNEbOrhxJ4SbjkT/o27rpVMEQMgA+MaCGXS"
+          + "kp7IPkkDMLuxpZyHd25ECjldiT1+tXvUwxGPzTEfGgSKAAv3LCIvMyivCnsG2257pZdE57CgvN/sPUDw"
+          + "ib2zmzSjyCWepLkYOecLgvJHDLUkzClKUm5w4KnCWBD4W6iWKJqRoY1qOKxlraOeKMYPnyIpDcOcb3jn"
+          + "bNxWs+QjM/BCxczjs00D7syvw2LJq4z/sD9Z8DE5e65nn9uzmLhnjukCS9MhPSesM3JIYSrK9m7jJ7Sp"
+          + "vbRpJq+1khyns9BUldhH8Fs680g4uj7XV25tRj4wbz68BQx4AuwvhAFAsVRjjHuEzaE+ic3QLM5BY+/g"
+          + "+dY73WplALotge0A/yTO2rmwS1OyCKmxUlAjO6cKoN6W7QSl7MVKUK/BL0sa2Cxy1CCMagAQQP/mjdL4"
+          + "LePycC+amQFUv3uIimL0YQ612IbaOAeJ50VM89293EQglGPB/PNBSV8BQVEe+TiTGAifI/5uFnzVBOjH"
+          + "oOoiRI/bmP3mX6HFGd81mWX6rV8BCSkelyRhwD96OLTiPv/57xIxYT/bvPmrCIADsGTqzQ2qQtVWAq60"
+          + "KnsTQtRIhcXQ0gDPuW4iJGqMQeOAm03ewcZkul68UmJjToyziP1Dcr2KLlGGVPghs3DzfHQnvm1xwIOE"
+          + "Tzv3JWXh0PCtKeTluoXILD7RDLp0mb5ieaMRCPBYMwI23BsMd6yWWf6KfPKOOOWNCzGVL+bC+VTvjueK"
+          + "Q/5tTcUvXIIeMXtgu6nWDOX3FQfMGDvSRcM7xoLe3P40vnYWHFUdpAEbRFhTRMpoDPgRXJCd8TLRSEHi"
+          + "eedCcOSMMghehAKdzxvoRM31DuPBSKYe1Qys0ApnSs51vZLHDGkOYGbcD6Q+NdmfoE3kY0k3r+vTKDVh"
+          + "+IE0QtY2HlXHOCs7VAR5HDsKIK2x/KtD6Cvf3R667bRItIZgdA6Bf+naAoxpcWwxDXSCWsmB26wa4hrC"
+          + "1qSSRsp0zB2p6vgqDkFz7e9tCR89kzWo+oRyVdAZk5gllPA6iBVsQ6xLdoN0FoPTAbKYXHricSMGYb5K"
+          + "mbHb6sAvpw147w0aOealtndgkuu1SS0XEgRKMBCIDAQABGoAE7PMXsNlwa3uE6iDnmhmoArzugzmnJRh"
+          + "ytBzcL4dGhrIOMwQncaHNfDPsTWyfjLha6Q0TfBPiDGm0Bq+/IygQM3WKofVHuH2J7+bt4WpS0ARSQbl"
+          + "fXiXazvYAD4j4LVtBE+TuBybGB/na2ui/G48452ip+FG5V7G6sEfkxis3ETgZtyTB6oDDXXaymMoGlic"
+          + "Gsuc66BWPRiko4OvnS8PRpi0yobdw65gtggDrrD/GS4H+FVq1kEOrVKFC4UZZYyaimYnl5IS1O9Pz1vm"
+          + "5epicWptFodAFo5N0CzK/hwwcocb02CuUgxONrS3Zypw+GxyMdgRI2P/Cpihm7USCOzNxjHEmNgt7Wuw"
+          + "tQChc4ZEdlZ1KXFXXEBZf6hwLNKk5Jh7MOmJfMSU9L9J1Tqkrfls268T0FEUmD0nciLRHoeqjaD9cWxa"
+          + "h89F6r1UuCo+LVsQp4y7g/qXmxUvLvFR6JPZwHx9iyTbVEe54/P2bcgbttEIYjqgs5FLt1cG6dqjKiFx"
+          + "lC8SLZJsMg1xpZNTVe7jpzX1Ot0nK8yY/UmLUrgq0AHH31N3L9a7vg6v/uI5kdWZZoASjBlVzLNgeBCo"
+          + "QGXwFdTNENeDYCAWXEgO65K1huq3UcoJjjvCTD0tlrdTNX7q915TS3e49xgJT3lB4TynAo2Fgs9OdZta"
+          + "ovVFKpiE5K6MSAggE\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1641152230,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.EcdsaPrivateKey\","
+          + "        \"value\": \"GiBCX+pnT5vwku4z7jfD4OTgvRtft3S4KuYHovWsQrlTPhJMIiAzcsfCVUz"
+          + "GZ13oTmMLxBYd8wFM5G+dgCXMeF8tYXayrRogGOzqO4xtS0H4wl/5M/QUkLDnpnmt2TqIiQlFk0vAdck"
+          + "SBhgCEAIIAw==\","
+          + "        \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 857170602,"
+          + "      \"outputPrefixType\": \"LEGACY\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  // Keyset with the public keys of the keys from JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS.
+  private static final String JSON_PUBLIC_KEYSET_WITH_MULTIPLE_KEYS = ""
+      + "{"
+      + "  \"primaryKeyId\": 1641152230,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.EcdsaPublicKey\","
+      + "        \"value\": \"IiApA+NmYivxRfhMuvTKZAwqETmn+WagBP/reucEjEvXkRog1AJ5GBzf+n2"
+      + "7xnj9KcoGllF9NIFfQrDEP99FNH+Cne4SBhgCEAIIAw==\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 775870498,"
+      + "      \"outputPrefixType\": \"TINK\""
+      + "    },"
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\":"
+      + "\"type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PublicKey\","
+      + "        \"value\": \"IgMBAAEagATs8xew2XBre4TqIOeaGagCvO6DOaclGHK0HNwvh0aGsg4zBCd"
+      + "xoc18M+xNbJ+MuFrpDRN8E+IMabQGr78jKBAzdYqh9Ue4fYnv5u3halLQBFJBuV9eJdrO9gAPiPgtW0E"
+      + "T5O4HJsYH+dra6L8bjzjnaKn4UblXsbqwR+TGKzcROBm3JMHqgMNddrKYygaWJway5zroFY9GKSjg6+d"
+      + "Lw9GmLTKht3DrmC2CAOusP8ZLgf4VWrWQQ6tUoULhRlljJqKZieXkhLU70/PW+bl6mJxam0Wh0AWjk3Q"
+      + "LMr+HDByhxvTYK5SDE42tLdnKnD4bHIx2BEjY/8KmKGbtRII7M3GMcSY2C3ta7C1AKFzhkR2VnUpcVdc"
+      + "QFl/qHAs0qTkmHsw6Yl8xJT0v0nVOqSt+WzbrxPQURSYPSdyItEeh6qNoP1xbFqHz0XqvVS4Kj4tWxCn"
+      + "jLuD+pebFS8u8VHok9nAfH2LJNtUR7nj8/ZtyBu20QhiOqCzkUu3Vwbp2qMqIXGULxItkmwyDXGlk1NV"
+      + "7uOnNfU63ScrzJj9SYtSuCrQAcffU3cv1ru+Dq/+4jmR1ZlmgBKMGVXMs2B4EKhAZfAV1M0Q14NgIBZc"
+      + "SA7rkrWG6rdRygmOO8JMPS2Wt1M1fur3XlNLd7j3GAlPeUHhPKcCjYWCz051m1qi9UUqmITkroxICCAQ"
+      + "=\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 1641152230,"
+      + "      \"outputPrefixType\": \"RAW\""
+      + "    },"
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.EcdsaPublicKey\","
+      + "        \"value\": \"IiAzcsfCVUzGZ13oTmMLxBYd8wFM5G+dgCXMeF8tYXayrRogGOzqO4xtS0H"
+      + "4wl/5M/QUkLDnpnmt2TqIiQlFk0vAdckSBhgCEAIIAw==\","
+      + "        \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 857170602,"
+      + "      \"outputPrefixType\": \"LEGACY\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Theory
+  public void multipleKeysReadKeysetWithEncryptDecrypt()
+      throws Exception {
+    KeysetHandle privateHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_PRIVATE_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+    KeysetHandle publicHandle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_PUBLIC_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+
+    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
+    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
+
+    byte[] data = "data".getBytes(UTF_8);
+    byte[] sig = signer.sign(data);
+    verifier.verify(sig, data);
+
+    // Also test that verifier can verify signatures of a non-primary key. We use
+    // JSON_PRIVATE_KEYSET to sign with the first key.
+    KeysetHandle privateHandle1 =
+        TinkJsonProtoKeysetFormat.parseKeyset(JSON_PRIVATE_KEYSET, InsecureSecretKeyAccess.get());
+    PublicKeySign signer1 = privateHandle1.getPrimitive(PublicKeySign.class);
+
+    byte[] data1 = "data1".getBytes(UTF_8);
+    byte[] sig1 = signer1.sign(data1);
+    verifier.verify(sig1, data1);
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the PublicKeySign
+  // or PublicKeyVerify.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonSignatureKeyset_throws()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+
+    // Test that the keyset can create a DeterministicAead primitive, but neither PublicKeySign
+    // nor PublicKeyVerify primitives.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(PublicKeySign.class));
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(PublicKeyVerify.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManagerTest.java
index 853fbc5..c095416 100644
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyManagerTest.java
@@ -22,18 +22,20 @@
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.StreamingAead;
 import com.google.crypto.tink.internal.KeyTypeManager;
+import com.google.crypto.tink.internal.Util;
 import com.google.crypto.tink.proto.AesCtrHmacStreamingKey;
 import com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat;
 import com.google.crypto.tink.proto.AesCtrHmacStreamingParams;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.HmacParams;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.testing.StreamingTestUtil;
-import com.google.crypto.tink.testing.TestUtil;
-import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.ByteString;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -45,6 +47,11 @@
   private final KeyTypeManager.KeyFactory<AesCtrHmacStreamingKeyFormat, AesCtrHmacStreamingKey>
       factory = manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    StreamingAeadConfig.register();
+  }
+
   // Returns an HmacParams.Builder with valid parameters
   private static HmacParams.Builder createHmacParams() {
     return HmacParams.newBuilder().setHash(HashType.SHA256).setTagSize(32);
@@ -64,6 +71,14 @@
     return AesCtrHmacStreamingKeyFormat.newBuilder().setKeySize(32).setParams(createParams());
   }
 
+  // Returns a valid AesCtrHmacStreamingKey.Builder
+  private static AesCtrHmacStreamingKey.Builder createKey() {
+    return AesCtrHmacStreamingKey.newBuilder()
+        .setParams(createParams())
+        .setVersion(0)
+        .setKeyValue(ByteString.copyFrom("This is a 32 byte random key.   ", Util.UTF_8));
+  }
+
   @Test
   public void basics() throws Exception {
     assertThat(manager.getKeyType())
@@ -168,6 +183,33 @@
   }
 
   @Test
+  public void validateKeyFormat_ciphertextSegmentSizeOverflow_throws() throws Exception {
+    // ciphertext_segment_size is uint in the proto, so we check that overflows make the manager
+    // fail.
+    AesCtrHmacStreamingKeyFormat format =
+        createKeyFormat()
+            .setParams(createParams().setCiphertextSegmentSize(Integer.MIN_VALUE).build())
+            .build();
+
+    assertThrows(GeneralSecurityException.class, () -> factory.validateKeyFormat(format));
+  }
+
+  @Test
+  public void validateKey_validKey_works() throws Exception {
+    AesCtrHmacStreamingKey key = createKey().build();
+
+    manager.validateKey(key);
+  }
+
+  @Test
+  public void validateKey_badHkdfHashType_throws() throws Exception {
+    AesCtrHmacStreamingKey key =
+        createKey().setParams(createParams().setHkdfHashType(HashType.SHA224)).build();
+
+    assertThrows(GeneralSecurityException.class, () -> manager.validateKey(key));
+  }
+
+  @Test
   public void createKey_values() throws Exception {
     AesCtrHmacStreamingKeyFormat format = createKeyFormat().build();
     AesCtrHmacStreamingKey key = factory.createKey(format);
@@ -196,7 +238,7 @@
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 100;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -204,69 +246,61 @@
   @Test
   public void testAes128CtrHmacSha2564KBTemplate() throws Exception {
     KeyTemplate template = AesCtrHmacStreamingKeyManager.aes128CtrHmacSha2564KBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCtrHmacStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesCtrHmacStreamingKeyFormat format =
-        AesCtrHmacStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getHash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getTagSize()).isEqualTo(32);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(4096);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCtrHmacStreamingParameters.builder()
+                .setKeySizeBytes(16)
+                .setDerivedKeySizeBytes(16)
+                .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacTagSizeBytes(32)
+                .setCiphertextSegmentSizeBytes(4 * 1024)
+                .build());
   }
 
   @Test
   public void testAes128CtrHmacSha2561MBTemplate() throws Exception {
     KeyTemplate template = AesCtrHmacStreamingKeyManager.aes128CtrHmacSha2561MBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCtrHmacStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesCtrHmacStreamingKeyFormat format =
-        AesCtrHmacStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getHash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getTagSize()).isEqualTo(32);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(1 << 20);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCtrHmacStreamingParameters.builder()
+                .setKeySizeBytes(16)
+                .setDerivedKeySizeBytes(16)
+                .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacTagSizeBytes(32)
+                .setCiphertextSegmentSizeBytes(1024 * 1024)
+                .build());
   }
 
   @Test
   public void testAes256CtrHmacSha2564KBTemplate() throws Exception {
     KeyTemplate template = AesCtrHmacStreamingKeyManager.aes256CtrHmacSha2564KBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCtrHmacStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesCtrHmacStreamingKeyFormat format =
-        AesCtrHmacStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getHash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getTagSize()).isEqualTo(32);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(4096);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCtrHmacStreamingParameters.builder()
+                .setKeySizeBytes(32)
+                .setDerivedKeySizeBytes(32)
+                .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacTagSizeBytes(32)
+                .setCiphertextSegmentSizeBytes(4 * 1024)
+                .build());
   }
 
   @Test
   public void testAes256CtrHmacSha2561MBTemplate() throws Exception {
     KeyTemplate template = AesCtrHmacStreamingKeyManager.aes256CtrHmacSha2561MBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesCtrHmacStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesCtrHmacStreamingKeyFormat format =
-        AesCtrHmacStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getHash()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getHmacParams().getTagSize()).isEqualTo(32);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(1 << 20);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesCtrHmacStreamingParameters.builder()
+                .setKeySizeBytes(32)
+                .setDerivedKeySizeBytes(32)
+                .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+                .setHmacTagSizeBytes(32)
+                .setCiphertextSegmentSizeBytes(1024 * 1024)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyTest.java
new file mode 100644
index 0000000..28c069e
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingKeyTest.java
@@ -0,0 +1,139 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.aead.XChaCha20Poly1305Key;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesCtrHmacStreamingKeyTest {
+
+  @Test
+  public void basicBuild_compareParameters_works() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    SecretBytes bytes = SecretBytes.randomBytes(19);
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, bytes);
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getInitialKeyMaterial()).isEqualTo(bytes);
+  }
+
+  @Test
+  public void build_wrongKeySize_throws() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    SecretBytes bytes = SecretBytes.randomBytes(18);
+    assertThrows(
+        GeneralSecurityException.class, () -> AesCtrHmacStreamingKey.create(parameters, bytes));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes33 = SecretBytes.randomBytes(33);
+    SecretBytes keyBytes33Copy =
+        SecretBytes.copyFrom(
+            keyBytes33.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes33Diff = SecretBytes.randomBytes(33);
+
+    AesCtrHmacStreamingParameters parameters33 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    AesCtrHmacStreamingParameters parameters33Copy =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    AesCtrHmacStreamingParameters parametersDifferentHashType =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "33 byte key",
+            AesCtrHmacStreamingKey.create(parameters33, keyBytes33),
+            AesCtrHmacStreamingKey.create(parameters33, keyBytes33Copy),
+            AesCtrHmacStreamingKey.create(parameters33Copy, keyBytes33))
+        .addEqualityGroup(
+            "different key",
+            AesCtrHmacStreamingKey.create(parameters33, keyBytes33Diff),
+            AesCtrHmacStreamingKey.create(parameters33Copy, keyBytes33Diff))
+        .addEqualityGroup(
+            "different parameters",
+            AesCtrHmacStreamingKey.create(parametersDifferentHashType, keyBytes33))
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    AesCtrHmacStreamingKey key =
+        AesCtrHmacStreamingKey.create(parameters, SecretBytes.randomBytes(33));
+
+    XChaCha20Poly1305Key xChaCha20Poly1305Key =
+        XChaCha20Poly1305Key.create(SecretBytes.randomBytes(32));
+
+    assertThat(key.equalsKey(xChaCha20Poly1305Key)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingParametersTest.java
new file mode 100644
index 0000000..95a3a8c
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingParametersTest.java
@@ -0,0 +1,437 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.XChaCha20Poly1305Parameters;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesCtrHmacStreamingParametersTest {
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(19);
+    assertThat(parameters.getDerivedKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getHkdfHashType())
+        .isEqualTo(AesCtrHmacStreamingParameters.HashType.SHA256);
+    assertThat(parameters.getHmacHashType()).isEqualTo(AesCtrHmacStreamingParameters.HashType.SHA1);
+    assertThat(parameters.getHmacTagSizeBytes()).isEqualTo(14);
+    assertThat(parameters.getCiphertextSegmentSizeBytes()).isEqualTo(1024 * 1024);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersVariedValues() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(35);
+    assertThat(parameters.getDerivedKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getHkdfHashType())
+        .isEqualTo(AesCtrHmacStreamingParameters.HashType.SHA512);
+    assertThat(parameters.getHmacHashType())
+        .isEqualTo(AesCtrHmacStreamingParameters.HashType.SHA256);
+    assertThat(parameters.getHmacTagSizeBytes()).isEqualTo(16);
+    assertThat(parameters.getCiphertextSegmentSizeBytes()).isEqualTo(3 * 1024 * 1024);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParameters_withoutSetKeySize_fails() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_withoutSetDerivedKeySize_fails() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_withoutSetHkdfHashType_fails() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(32)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_withoutSetHmacHashType_fails() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_withoutSetHmacTagSize_fails() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_withoutSetCiphertextSegmentSize_fails() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_derivedKeySizeNot16Or32A_throws() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(24)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_derivedKeySizeNot16Or32B_throws() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(17)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_ciphertextSegmentSizeLowerBound() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(19)
+            .setCiphertextSegmentSizeBytes(32 + 19 + 8);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setCiphertextSegmentSizeBytes(32 + 19 + 8 + 1);
+    Object unused = builder.build();
+  }
+
+  @Test
+  public void buildParameters_ciphertextSegmentSizeLowerBound2() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(19)
+            .setCiphertextSegmentSizeBytes(16 + 19 + 8);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setCiphertextSegmentSizeBytes(16 + 19 + 8 + 1);
+    Object unused = builder.build();
+  }
+
+  @Test
+  public void buildParameters_initialKeymaterialBound1() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(31)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setKeySizeBytes(32);
+    Object unused = builder.build();
+  }
+
+  @Test
+  public void buildParameters_initialKeymaterialBound2() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(15)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setKeySizeBytes(16);
+    Object unused = builder.build();
+  }
+
+  @Test
+  public void buildParameters_hmacTagSize_SHA1_bounds() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+
+    builder.setHmacTagSizeBytes(9);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setHmacTagSizeBytes(10);
+    Object unused = builder.build();
+
+    builder.setHmacTagSizeBytes(19);
+    unused = builder.build();
+
+    builder.setHmacTagSizeBytes(20);
+    unused = builder.build();
+
+    builder.setHmacTagSizeBytes(21);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_hmacTagSize_SHA256_bounds() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+
+    builder.setHmacTagSizeBytes(9);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setHmacTagSizeBytes(10);
+    Object unused = builder.build();
+
+    builder.setHmacTagSizeBytes(31);
+    unused = builder.build();
+
+    builder.setHmacTagSizeBytes(32);
+    unused = builder.build();
+
+    builder.setHmacTagSizeBytes(33);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParameters_hmacTagSize_SHA512_bounds() throws Exception {
+    AesCtrHmacStreamingParameters.Builder builder =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+
+    builder.setHmacTagSizeBytes(9);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder.setHmacTagSizeBytes(10);
+    Object unused = builder.build();
+
+    builder.setHmacTagSizeBytes(63);
+    unused = builder.build();
+
+    builder.setHmacTagSizeBytes(64);
+    unused = builder.build();
+
+    builder.setHmacTagSizeBytes(65);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesCtrHmacStreamingParameters parameters1 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    AesCtrHmacStreamingParameters parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+
+    // Different KeySizeBytes
+    parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(32)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different DerivedKeySizeBytes
+    parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different HkdfHashType
+    parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different HmacHashType
+    parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different HmacTagSizeBytes
+    parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(15)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different CiphertextSegmentSize
+    parameters2 =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  @SuppressWarnings("TruthIncompatibleType")
+  public void testEqualDifferentClass() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    assertThat(parameters).isNotEqualTo(XChaCha20Poly1305Parameters.create());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingProtoSerializationTest.java
new file mode 100644
index 0000000..25df0e8
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesCtrHmacStreamingProtoSerializationTest.java
@@ -0,0 +1,1004 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.HmacParams;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesCtrHmacStreamingProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesCtrHmacStreamingProtoSerializationTest {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey";
+
+  private static final SecretBytes KEY_BYTES_37 = SecretBytes.randomBytes(37);
+  private static final ByteString KEY_BYTES_37_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_37.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final SecretBytes KEY_BYTES_36 = SecretBytes.randomBytes(36);
+  private static final ByteString KEY_BYTES_36_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_36.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesCtrHmacStreamingProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesCtrHmacStreamingProtoSerialization.register(registry);
+    AesCtrHmacStreamingProtoSerialization.register(registry);
+  }
+
+  // PARAMETERS ====================================================================================
+  @Test
+  public void serializeParseParameters_simple() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentKeySize() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(20)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(20)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentDerivedKeySize() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentHkdfHashType() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA512)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentHmacHash() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA512).setTagSize(16)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentHmacTagSize() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(15)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(15)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentCiphertextSegmentSizeType() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(128 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(128 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  /** Test that if "OutputPrefixType" is set to Tink, we just ignore it. */
+  @Test
+  public void parseParameters_outputPrefixIgnored_tink() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  /** Test that if "OutputPrefixType" is set to CRUNCHY, we just ignore it. */
+  @Test
+  public void parseParameters_outputPrefixIgnored_crunchy() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void parseParameters_outputPrefixIgnored_legacy() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.LEGACY,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build());
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Key size smaller than derived key size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build()),
+        // Bad hash type
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA384)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build()),
+        // Bad derived Key Size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(24)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+                .build()),
+        // Short Tag
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256)
+                        .setHmacParams(
+                            HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(9)))
+                .build())
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  // KEYS ==========================================================================================
+  @Test
+  public void serializeParseKey_simple() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentKeySizeBytes() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(36)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_36);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_36_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentDerivedKeySize() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(16)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(16)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentHkdfHashType() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA512)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentHmacHashType() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA512)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(512 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA512).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+
+  @Test
+  public void serializeParseKey_differentHmacTagSize() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(14)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(512 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(14)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+
+  @Test
+  public void serializeParseKey_differentCiphertextSegmentSize() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(512 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_ignoreOutputPrefixType_crunchy() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 132);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_ignoreOutputPrefixType_tink() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_ignoreOutputPrefixType_legacy() throws Exception {
+    AesCtrHmacStreamingParameters parameters =
+        AesCtrHmacStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedKeySizeBytes(32)
+            .setHkdfHashType(AesCtrHmacStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .setHmacHashType(AesCtrHmacStreamingParameters.HashType.SHA1)
+            .setHmacTagSizeBytes(16)
+            .build();
+
+    AesCtrHmacStreamingKey key = AesCtrHmacStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesCtrHmacStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                    .setCiphertextSegmentSize(1024 * 1024)
+                    .setDerivedKeySize(32)
+                    .setHkdfHashType(HashType.SHA256)
+                    .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            /* idRequirement= */ 123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      com.google.crypto.tink.proto.AesCtrHmacStreamingKey validKey =
+          com.google.crypto.tink.proto.AesCtrHmacStreamingKey.newBuilder()
+              .setVersion(0)
+              .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+              .setParams(
+                  com.google.crypto.tink.proto.AesCtrHmacStreamingParams.newBuilder()
+                      .setCiphertextSegmentSize(1024 * 1024)
+                      .setDerivedKeySize(32)
+                      .setHkdfHashType(HashType.SHA256)
+                      .setHmacParams(HmacParams.newBuilder().setHash(HashType.SHA1).setTagSize(16)))
+              .build();
+
+      return new ProtoKeySerialization[] {
+        // Wrong version
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            validKey.toBuilder().setVersion(1).build().toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Wrong Hash type
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            validKey.toBuilder()
+                .setParams(validKey.getParams().toBuilder().setHkdfHashType(HashType.SHA224))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Wrong Hash type
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            validKey.toBuilder()
+                .setParams(validKey.getParams().toBuilder().setHkdfHashType(HashType.SHA384))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Key Shorter than derivedKeySize
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            validKey.toBuilder()
+                .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING.substring(0, 20))
+                .setParams(validKey.getParams().toBuilder().setDerivedKeySize(32))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Short CiphertextSegmentSize
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
+            validKey.toBuilder()
+                .setParams(validKey.getParams().toBuilder().setCiphertextSegmentSize(24))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null)
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManagerTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManagerTest.java
index 56c455b..a758d3c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManagerTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManagerTest.java
@@ -27,14 +27,15 @@
 import com.google.crypto.tink.proto.AesGcmHkdfStreamingParams;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.StreamingTestUtil;
-import com.google.crypto.tink.testing.TestUtil;
-import com.google.protobuf.ExtensionRegistryLite;
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.Set;
 import java.util.TreeSet;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -46,6 +47,11 @@
   private final KeyTypeManager.KeyFactory<AesGcmHkdfStreamingKeyFormat, AesGcmHkdfStreamingKey>
       factory = manager.keyFactory();
 
+  @Before
+  public void register() throws Exception {
+    StreamingAeadConfig.register();
+  }
+
   private static AesGcmHkdfStreamingKeyFormat createKeyFormat(
       int keySize, int derivedKeySize, HashType hashType, int segmentSize) {
     return AesGcmHkdfStreamingKeyFormat.newBuilder()
@@ -86,6 +92,20 @@
   }
 
   @Test
+  public void validateKeyFormat_sha384_throws() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> factory.validateKeyFormat(createKeyFormat(32, 32, HashType.SHA384, 1024)));
+  }
+
+  @Test
+  public void validateKeyFormat_sha224_throws() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> factory.validateKeyFormat(createKeyFormat(32, 32, HashType.SHA224, 1024)));
+  }
+
+  @Test
   public void validateKeyFormat_smallKey_throws() throws Exception {
     assertThrows(
         GeneralSecurityException.class,
@@ -127,6 +147,35 @@
   }
 
   @Test
+  public void testDeriveKey_handlesDataFragmentationCorrectly() throws Exception {
+    int keySize = 32;
+    int derivedKeySize = 16;
+    byte randomness = 4;
+    InputStream fragmentedInputStream =
+        new InputStream() {
+          @Override
+          public int read() {
+            return 0;
+          }
+
+          @Override
+          public int read(byte[] b, int off, int len) {
+            b[off] = randomness;
+            return 1;
+          }
+        };
+    AesGcmHkdfStreamingKeyFormat format =
+        createKeyFormat(keySize, derivedKeySize, HashType.SHA256, 1024);
+
+    AesGcmHkdfStreamingKey key = factory.deriveKey(format, fragmentedInputStream);
+
+    assertThat(key.getKeyValue()).hasSize(keySize);
+    for (int i = 0; i < keySize; ++i) {
+      assertThat(key.getKeyValue().byteAt(i)).isEqualTo(randomness);
+    }
+  }
+
+  @Test
   public void testDeriveKey_notEnoughKeyMaterial_throws() throws Exception {
     final int keySize = 32;
     final int derivedKeySize = 16;
@@ -190,7 +239,7 @@
     // Calls newKey multiple times and make sure that they generate different keys.
     int numTests = 100;
     for (int i = 0; i < numTests; i++) {
-      keys.add(TestUtil.hexEncode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
+      keys.add(Hex.encode(factory.createKey(keyFormat).getKeyValue().toByteArray()));
     }
     assertThat(keys).hasSize(numTests);
   }
@@ -198,61 +247,53 @@
   @Test
   public void testAes128GcmHkdf4KBTemplate() throws Exception {
     KeyTemplate template = AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesGcmHkdfStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesGcmHkdfStreamingKeyFormat format =
-        AesGcmHkdfStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(4096);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmHkdfStreamingParameters.builder()
+                .setKeySizeBytes(16)
+                .setDerivedAesGcmKeySizeBytes(16)
+                .setCiphertextSegmentSizeBytes(4 * 1024)
+                .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                .build());
   }
 
   @Test
   public void testAes256GcmHkdf4KBTemplate() throws Exception {
     KeyTemplate template = AesGcmHkdfStreamingKeyManager.aes256GcmHkdf4KBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesGcmHkdfStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesGcmHkdfStreamingKeyFormat format =
-        AesGcmHkdfStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(4096);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmHkdfStreamingParameters.builder()
+                .setKeySizeBytes(32)
+                .setDerivedAesGcmKeySizeBytes(32)
+                .setCiphertextSegmentSizeBytes(4 * 1024)
+                .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                .build());
   }
 
   @Test
   public void testAes128GcmHkdf1MBTemplate() throws Exception {
     KeyTemplate template = AesGcmHkdfStreamingKeyManager.aes128GcmHkdf1MBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesGcmHkdfStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesGcmHkdfStreamingKeyFormat format =
-        AesGcmHkdfStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(16);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(1 << 20);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmHkdfStreamingParameters.builder()
+                .setKeySizeBytes(16)
+                .setDerivedAesGcmKeySizeBytes(16)
+                .setCiphertextSegmentSizeBytes(1024 * 1024)
+                .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                .build());
   }
 
   @Test
   public void testAes256GcmHkdf1MBTemplate() throws Exception {
     KeyTemplate template = AesGcmHkdfStreamingKeyManager.aes256GcmHkdf1MBTemplate();
-    assertThat(template.getTypeUrl()).isEqualTo(new AesGcmHkdfStreamingKeyManager().getKeyType());
-    assertThat(template.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.RAW);
-    AesGcmHkdfStreamingKeyFormat format =
-        AesGcmHkdfStreamingKeyFormat.parseFrom(
-            template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-
-    assertThat(format.getKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getDerivedKeySize()).isEqualTo(32);
-    assertThat(format.getParams().getHkdfHashType()).isEqualTo(HashType.SHA256);
-    assertThat(format.getParams().getCiphertextSegmentSize()).isEqualTo(1 << 20);
+    assertThat(template.toParameters())
+        .isEqualTo(
+            AesGcmHkdfStreamingParameters.builder()
+                .setKeySizeBytes(32)
+                .setDerivedAesGcmKeySizeBytes(32)
+                .setCiphertextSegmentSizeBytes(1024 * 1024)
+                .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+                .build());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyTest.java
new file mode 100644
index 0000000..f9ed254
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyTest.java
@@ -0,0 +1,127 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.aead.XChaCha20Poly1305Key;
+import com.google.crypto.tink.internal.KeyTester;
+import com.google.crypto.tink.util.SecretBytes;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesGcmHkdfStreamingKeyTest {
+
+  @Test
+  public void basicBuild_compareParameters_works() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    SecretBytes bytes = SecretBytes.randomBytes(19);
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, bytes);
+
+    assertThat(key.getParameters()).isEqualTo(parameters);
+    assertThat(key.getInitialKeyMaterial()).isEqualTo(bytes);
+  }
+
+  @Test
+  public void build_wrongKeySize_throws() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    SecretBytes bytes = SecretBytes.randomBytes(18);
+    assertThrows(
+        GeneralSecurityException.class, () -> AesGcmHkdfStreamingKey.create(parameters, bytes));
+  }
+
+  @Test
+  public void testEqualities() throws Exception {
+    SecretBytes keyBytes33 = SecretBytes.randomBytes(33);
+    SecretBytes keyBytes33Copy =
+        SecretBytes.copyFrom(
+            keyBytes33.toByteArray(InsecureSecretKeyAccess.get()), InsecureSecretKeyAccess.get());
+    SecretBytes keyBytes33Diff = SecretBytes.randomBytes(33);
+
+    AesGcmHkdfStreamingParameters parameters33 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    AesGcmHkdfStreamingParameters parameters33Copy =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    AesGcmHkdfStreamingParameters parametersDifferentHashType =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(33)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    new KeyTester()
+        .addEqualityGroup(
+            "33 byte key",
+            AesGcmHkdfStreamingKey.create(parameters33, keyBytes33),
+            AesGcmHkdfStreamingKey.create(parameters33, keyBytes33Copy),
+            AesGcmHkdfStreamingKey.create(parameters33Copy, keyBytes33))
+        .addEqualityGroup(
+            "different key",
+            AesGcmHkdfStreamingKey.create(parameters33, keyBytes33Diff),
+            AesGcmHkdfStreamingKey.create(parameters33Copy, keyBytes33Diff))
+        .addEqualityGroup(
+            "different parameters",
+            AesGcmHkdfStreamingKey.create(parametersDifferentHashType, keyBytes33))
+        .doTests();
+  }
+
+  @Test
+  public void testDifferentKeyTypesEquality_fails() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(32)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    AesGcmHkdfStreamingKey key =
+        AesGcmHkdfStreamingKey.create(parameters, SecretBytes.randomBytes(32));
+
+    XChaCha20Poly1305Key xChaCha20Poly1305Key =
+        XChaCha20Poly1305Key.create(SecretBytes.randomBytes(32));
+
+    assertThat(key.equalsKey(xChaCha20Poly1305Key)).isFalse();
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingParametersTest.java
new file mode 100644
index 0000000..ecb8669
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingParametersTest.java
@@ -0,0 +1,250 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.aead.XChaCha20Poly1305Parameters;
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class AesGcmHkdfStreamingParametersTest {
+  @Test
+  public void buildParametersAndGetProperties() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(19);
+    assertThat(parameters.getDerivedAesGcmKeySizeBytes()).isEqualTo(16);
+    assertThat(parameters.getCiphertextSegmentSizeBytes()).isEqualTo(1024 * 1024);
+    assertThat(parameters.getHkdfHashType())
+        .isEqualTo(AesGcmHkdfStreamingParameters.HashType.SHA256);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersVariedValues() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(77)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024)
+            .build();
+    assertThat(parameters.getKeySizeBytes()).isEqualTo(77);
+    assertThat(parameters.getDerivedAesGcmKeySizeBytes()).isEqualTo(32);
+    assertThat(parameters.getCiphertextSegmentSizeBytes()).isEqualTo(3 * 1024 * 1024);
+    assertThat(parameters.getHkdfHashType()).isEqualTo(AesGcmHkdfStreamingParameters.HashType.SHA1);
+    assertThat(parameters.hasIdRequirement()).isFalse();
+  }
+
+  @Test
+  public void buildParametersWithoutSettingKeySize_fails() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParametersWithoutSettingDerivedKeySize_fails() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParametersWithoutSettingHashType_fails() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setCiphertextSegmentSizeBytes(1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void buildParametersWithoutSettingCiphertextSegmentSize_fails() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void ciphertextSegmentSizeLowerLimit() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(77)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(32 + 24);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    Object unused =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(77)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(32 + 25)
+            .build();
+  }
+
+  @Test
+  public void derivedKeySize_fails() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(77)
+            .setDerivedAesGcmKeySizeBytes(24)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(77)
+            .setDerivedAesGcmKeySizeBytes(17)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(77)
+            .setDerivedAesGcmKeySizeBytes(33)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void keyValueShorterThanDerivedKeySize_fails() throws Exception {
+    AesGcmHkdfStreamingParameters.Builder builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(31)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+
+    builder =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(15)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(3 * 1024 * 1024);
+    assertThrows(GeneralSecurityException.class, builder::build);
+  }
+
+  @Test
+  public void testNotEqualandNotEqualHashCode() throws Exception {
+    AesGcmHkdfStreamingParameters parameters1 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingParameters parameters2 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isEqualTo(parameters2.hashCode());
+
+    // Different KeySizeBytes
+    parameters2 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(36)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different DerivedAesGcmKeySizeBytes
+    parameters2 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different hkdf hash type
+    parameters2 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+
+    // Different ciphertext segment size
+    parameters2 =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024 + 1)
+            .build();
+
+    assertThat(parameters1).isNotEqualTo(parameters2);
+    assertThat(parameters1.hashCode()).isNotEqualTo(parameters2.hashCode());
+  }
+
+  @Test
+  @SuppressWarnings("TruthIncompatibleType")
+  public void testEqualDifferentClass() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(35)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+    assertThat(parameters).isNotEqualTo(XChaCha20Poly1305Parameters.create());
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingProtoSerializationTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingProtoSerializationTest.java
new file mode 100644
index 0000000..b68f572
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingProtoSerializationTest.java
@@ -0,0 +1,773 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
+import com.google.crypto.tink.internal.ProtoParametersSerialization;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
+import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.util.SecretBytes;
+import com.google.protobuf.ByteString;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Test for AesGcmHkdfStreamingProtoSerialization. */
+@RunWith(Theories.class)
+@SuppressWarnings("UnnecessarilyFullyQualified") // Fully specifying proto types is more readable
+public final class AesGcmHkdfStreamingProtoSerializationTest {
+  private static final String TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey";
+
+  private static final SecretBytes KEY_BYTES_37 = SecretBytes.randomBytes(37);
+  private static final ByteString KEY_BYTES_37_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_37.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final SecretBytes KEY_BYTES_36 = SecretBytes.randomBytes(36);
+  private static final ByteString KEY_BYTES_36_AS_BYTE_STRING =
+      ByteString.copyFrom(KEY_BYTES_36.toByteArray(InsecureSecretKeyAccess.get()));
+
+  private static final MutableSerializationRegistry registry = new MutableSerializationRegistry();
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AesGcmHkdfStreamingProtoSerialization.register(registry);
+  }
+
+  @Test
+  public void registerTwice() throws Exception {
+    MutableSerializationRegistry registry = new MutableSerializationRegistry();
+    AesGcmHkdfStreamingProtoSerialization.register(registry);
+    AesGcmHkdfStreamingProtoSerialization.register(registry);
+  }
+
+  // PARAMETERS ====================================================================================
+  @Test
+  public void serializeParseParameters_simple() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(19)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(19)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentKeySize() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(16)
+                        .setHkdfHashType(HashType.SHA256))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentDerivedKeySize() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA256)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA256))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentHashType() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA512))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void serializeParseParameters_differentCiphertextSegmentSizeType() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(512 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA512))
+                .build());
+
+    ProtoParametersSerialization serialized =
+        registry.serializeParameters(parameters, ProtoParametersSerialization.class);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.parser(),
+        serialized,
+        serialization);
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  /** Test that if "OutputPrefixType" is set to Tink, we just ignore it. */
+  @Test
+  public void parseParameters_outputPrefixIgnored_tink() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.TINK,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(512 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA512))
+                .build());
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  /** Test that if "OutputPrefixType" is set to CRUNCHY, we just ignore it. */
+  @Test
+  public void parseParameters_outputPrefixIgnored_crunchy() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.CRUNCHY,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(512 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA512))
+                .build());
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @Test
+  public void parseParameters_outputPrefixIgnored_legacy() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    ProtoParametersSerialization serialization =
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.LEGACY,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(37)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(512 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA512))
+                .build());
+
+    Parameters parsed = registry.parseParameters(serialization);
+    assertThat(parsed).isEqualTo(parameters);
+  }
+
+  @DataPoints("invalidParametersSerializations")
+  public static final ProtoParametersSerialization[] INVALID_PARAMETERS_SERIALIZATIONS =
+      new ProtoParametersSerialization[] {
+        // Key size smaller than derived key size
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(16)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(512 * 1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA1))
+                .build()),
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(32)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(5)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA1))
+                .build()),
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setVersion(1)
+                .setKeySize(32)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(1024)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA1))
+                .build()),
+        // Bad hash type
+        ProtoParametersSerialization.create(
+            TYPE_URL,
+            OutputPrefixType.RAW,
+            com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat.newBuilder()
+                .setKeySize(32)
+                .setParams(
+                    com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                        .setCiphertextSegmentSize(5)
+                        .setDerivedKeySize(32)
+                        .setHkdfHashType(HashType.SHA224))
+                .build())
+      };
+
+  @Theory
+  public void testParseInvalidParameters_fails(
+      @FromDataPoints("invalidParametersSerializations")
+          ProtoParametersSerialization serializedParameters)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class, () -> registry.parseParameters(serializedParameters));
+  }
+
+  // KEYS ==========================================================================================
+  @Test
+  public void serializeParseKey_simple() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(32)
+                    .setCiphertextSegmentSize(512 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentKeySizeBytes() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(36)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_36);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_36_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(32)
+                    .setCiphertextSegmentSize(512 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentDerivedKeySize() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(512 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentHkdfHashType() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(32)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA1)
+            .setCiphertextSegmentSizeBytes(512 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA1)
+                    .setDerivedKeySize(32)
+                    .setCiphertextSegmentSize(512 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void serializeParseKey_differentCiphertextSegmentSize() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(1024 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, InsecureSecretKeyAccess.get());
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.parser(), serialized, serialization);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_ignoreOutputPrefixType_crunchy() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(1024 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.CRUNCHY,
+            123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_ignoreOutputPrefixType_tink() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(1024 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.TINK,
+            123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  @Test
+  public void parseKey_ignoreOutputPrefixType_legacy() throws Exception {
+    AesGcmHkdfStreamingParameters parameters =
+        AesGcmHkdfStreamingParameters.builder()
+            .setKeySizeBytes(37)
+            .setDerivedAesGcmKeySizeBytes(16)
+            .setHkdfHashType(AesGcmHkdfStreamingParameters.HashType.SHA512)
+            .setCiphertextSegmentSizeBytes(1024 * 1024)
+            .build();
+
+    AesGcmHkdfStreamingKey key = AesGcmHkdfStreamingKey.create(parameters, KEY_BYTES_37);
+
+    com.google.crypto.tink.proto.AesGcmHkdfStreamingKey protoKey =
+        com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+            .setParams(
+                com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA512)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(1024 * 1024))
+            .build();
+
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            protoKey.toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.LEGACY,
+            123);
+
+    Key parsed = registry.parseKey(serialization, InsecureSecretKeyAccess.get());
+    assertThat(parsed.equalsKey(key)).isTrue();
+  }
+
+  private static ProtoKeySerialization[] createInvalidKeySerializations() {
+    try {
+      com.google.crypto.tink.proto.AesGcmHkdfStreamingKey validKey =
+          com.google.crypto.tink.proto.AesGcmHkdfStreamingKey.newBuilder()
+              .setVersion(0)
+              .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING)
+              .setParams(
+                  com.google.crypto.tink.proto.AesGcmHkdfStreamingParams.newBuilder()
+                      .setHkdfHashType(HashType.SHA512)
+                      .setDerivedKeySize(16)
+                      .setCiphertextSegmentSize(1024 * 1024))
+              .build();
+
+      return new ProtoKeySerialization[] {
+        // Wrong version
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            validKey.toBuilder().setVersion(1).build().toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Wrong Hash type
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            validKey.toBuilder()
+                .setParams(validKey.getParams().toBuilder().setHkdfHashType(HashType.SHA224))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Wrong Hash type
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            validKey.toBuilder()
+                .setParams(validKey.getParams().toBuilder().setHkdfHashType(HashType.SHA384))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Key Shorter than derivedKeySize
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            validKey.toBuilder()
+                .setKeyValue(KEY_BYTES_37_AS_BYTE_STRING.substring(0, 20))
+                .setParams(validKey.getParams().toBuilder().setDerivedKeySize(32))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null),
+        // Short CiphertextSegmentSize
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey",
+            validKey.toBuilder()
+                .setParams(validKey.getParams().toBuilder().setCiphertextSegmentSize(24))
+                .build()
+                .toByteString(),
+            KeyMaterialType.SYMMETRIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null)
+      };
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @DataPoints("invalidKeySerializations")
+  public static final ProtoKeySerialization[] INVALID_KEY_SERIALIZATIONS =
+      createInvalidKeySerializations();
+
+  @Theory
+  public void testParseInvalidKeys_throws(
+      @FromDataPoints("invalidKeySerializations") ProtoKeySerialization serialization)
+      throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/streamingaead/BUILD.bazel
index 83e4b31..fffa5e2 100644
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/BUILD.bazel
@@ -1,23 +1,6 @@
 licenses(["notice"])
 
 java_test(
-    name = "StreamingAeadIntegrationTest",
-    size = "small",
-    srcs = ["StreamingAeadIntegrationTest.java"],
-    deps = [
-        "//proto:tink_java_proto",
-        "//src/main/java/com/google/crypto/tink:registry_cluster",
-        "//src/main/java/com/google/crypto/tink:streaming_aead",
-        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
-        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
     name = "StreamingAeadKeyTemplatesTest",
     size = "small",
     srcs = ["StreamingAeadKeyTemplatesTest.java"],
@@ -26,10 +9,15 @@
         "//proto:aes_gcm_hkdf_streaming_java_proto",
         "//proto:common_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:tink_proto_parameters_format",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key_manager",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key_manager",
+        "//src/main/java/com/google/crypto/tink/streamingaead:predefined_streaming_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_key_templates",
-        "@com_google_protobuf//:protobuf_javalite",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_parameters",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -81,15 +69,21 @@
     size = "small",
     srcs = ["StreamingAeadWrapperTest.java"],
     deps = [
+        "//proto:aes_gcm_hkdf_streaming_java_proto",
+        "//proto:common_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:primitive_set",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
+        "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
         "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_wrapper",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:junit_junit",
     ],
 )
@@ -106,10 +100,11 @@
         "//src/main/java/com/google/crypto/tink:streaming_aead",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key_manager",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -127,10 +122,13 @@
         "//src/main/java/com/google/crypto/tink:key_template",
         "//src/main/java/com/google/crypto/tink:streaming_aead",
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
+        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key_manager",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -150,3 +148,143 @@
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "StreamingAeadTest",
+    size = "small",
+    srcs = ["StreamingAeadTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:deterministic_aead",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key_templates",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink:streaming_aead",
+        "//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmHkdfStreamingParametersTest",
+    size = "small",
+    srcs = ["AesGcmHkdfStreamingParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmHkdfStreamingKeyTest",
+    size = "small",
+    srcs = ["AesGcmHkdfStreamingKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesGcmHkdfStreamingProtoSerializationTest",
+    size = "small",
+    srcs = ["AesGcmHkdfStreamingProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_gcm_hkdf_streaming_java_proto",
+        "//proto:common_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_key",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_gcm_hkdf_streaming_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCtrHmacStreamingParametersTest",
+    size = "small",
+    srcs = ["AesCtrHmacStreamingParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCtrHmacStreamingKeyTest",
+    size = "small",
+    srcs = ["AesCtrHmacStreamingKeyTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/internal:key_tester",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "AesCtrHmacStreamingProtoSerializationTest",
+    size = "small",
+    srcs = ["AesCtrHmacStreamingProtoSerializationTest.java"],
+    deps = [
+        "//proto:aes_ctr_hmac_streaming_java_proto",
+        "//proto:common_java_proto",
+        "//proto:hmac_java_proto",
+        "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:parameters",
+        "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
+        "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
+        "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_key",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:aes_ctr_hmac_streaming_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_protobuf_protobuf_java",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PredefinedStreamingAeadParametersTest",
+    size = "small",
+    srcs = ["PredefinedStreamingAeadParametersTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:key",
+        "//src/main/java/com/google/crypto/tink:registry_cluster",
+        "//src/main/java/com/google/crypto/tink/streamingaead:predefined_streaming_aead_parameters",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
+        "//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_parameters",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/PredefinedStreamingAeadParametersTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/PredefinedStreamingAeadParametersTest.java
new file mode 100644
index 0000000..e89512e
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/PredefinedStreamingAeadParametersTest.java
@@ -0,0 +1,56 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.crypto.tink.Key;
+import com.google.crypto.tink.KeysetHandle;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public final class PredefinedStreamingAeadParametersTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    StreamingAeadConfig.register();
+  }
+
+  @DataPoints("AllParameters")
+  public static final StreamingAeadParameters[] TEMPLATES =
+      new StreamingAeadParameters[] {
+        PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_4KB,
+        PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_1MB,
+        PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_4KB,
+        PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_1MB,
+        PredefinedStreamingAeadParameters.AES128_GCM_HKDF_4KB,
+        PredefinedStreamingAeadParameters.AES128_GCM_HKDF_1MB,
+        PredefinedStreamingAeadParameters.AES256_GCM_HKDF_4KB,
+        PredefinedStreamingAeadParameters.AES256_GCM_HKDF_1MB
+      };
+
+  @Theory
+  public void testInstantiation(@FromDataPoints("AllParameters") StreamingAeadParameters parameters)
+      throws Exception {
+    Key key = KeysetHandle.generateNew(parameters).getAt(0).getKey();
+    assertThat(key.getParameters()).isEqualTo(parameters);
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadConfigTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadConfigTest.java
index ff328d1..e11421c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadConfigTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadConfigTest.java
@@ -17,6 +17,7 @@
 package com.google.crypto.tink.streamingaead;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.Registry;
@@ -42,16 +43,6 @@
   @Test
   public void aaaTestInitialization() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("tinkstreamingaead"));
-    assertThat(e.toString()).contains("no catalogue found");
-    assertThat(e.toString()).contains("StreamingAeadConfig.register()");
-    GeneralSecurityException e2 =
-        assertThrows(
-            GeneralSecurityException.class, () -> Registry.getCatalogue("TinkStreamingAead"));
-    assertThat(e2.toString()).contains("no catalogue found");
-    assertThat(e2.toString()).contains("StreamingAeadConfig.register()");
     String typeUrl = "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey";
     GeneralSecurityException e3 =
         assertThrows(GeneralSecurityException.class, () -> Registry.getUntypedKeyManager(typeUrl));
@@ -61,7 +52,7 @@
     StreamingAeadConfig.register();
 
     // After registration the key manager should be present.
-    Registry.getKeyManager(typeUrl, StreamingAead.class);
+    assertNotNull(Registry.getKeyManager(typeUrl, StreamingAead.class));
 
     // Running init() manually again should succeed.
     StreamingAeadConfig.register();
@@ -81,7 +72,7 @@
     };
 
     for (String typeUrl : keyTypeUrls) {
-      Registry.getKeyManager(typeUrl, StreamingAead.class);
+      assertNotNull(Registry.getKeyManager(typeUrl, StreamingAead.class));
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadIntegrationTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadIntegrationTest.java
deleted file mode 100644
index 5d4039d..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadIntegrationTest.java
+++ /dev/null
@@ -1,175 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.streamingaead;
-
-import static com.google.crypto.tink.testing.TestUtil.assertExceptionContains;
-import static org.junit.Assert.assertThrows;
-
-import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.StreamingAead;
-import com.google.crypto.tink.daead.DeterministicAeadConfig;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset.Key;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.StreamingTestUtil;
-import com.google.crypto.tink.testing.TestUtil;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests which run the everything for the Streaming Aeads. */
-@RunWith(JUnit4.class)
-public class StreamingAeadIntegrationTest {
-  private static final int KDF_KEY_SIZE = 16;
-  private static final int AES_KEY_SIZE = 16;
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    StreamingAeadConfig.register();
-    DeterministicAeadConfig.register(); // need this for testInvalidKeyMaterial.
-  }
-
-  @Test
-  public void testBasicAesCtrHmacStreamingAead() throws Exception {
-    byte[] keyValue = Random.randBytes(KDF_KEY_SIZE);
-    int derivedKeySize = AES_KEY_SIZE;
-    int ciphertextSegmentSize = 128;
-    KeysetHandle keysetHandle =
-        TestUtil.createKeysetHandle(
-            TestUtil.createKeyset(
-                TestUtil.createKey(
-                    TestUtil.createAesCtrHmacStreamingKeyData(
-                        keyValue, derivedKeySize, ciphertextSegmentSize),
-                    42,
-                    KeyStatusType.ENABLED,
-                    OutputPrefixType.RAW)));
-    StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
-    StreamingTestUtil.testEncryptionAndDecryption(streamingAead);
-  }
-
-  @Test
-  public void testBasicAesGcmHkdfStreamingAead() throws Exception {
-    byte[] keyValue = Random.randBytes(KDF_KEY_SIZE);
-    int derivedKeySize = AES_KEY_SIZE;
-    int ciphertextSegmentSize = 128;
-    KeysetHandle keysetHandle =
-        TestUtil.createKeysetHandle(
-            TestUtil.createKeyset(
-                TestUtil.createKey(
-                    TestUtil.createAesGcmHkdfStreamingKeyData(
-                        keyValue, derivedKeySize, ciphertextSegmentSize),
-                    42,
-                    KeyStatusType.ENABLED,
-                    OutputPrefixType.RAW)));
-    StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
-    StreamingTestUtil.testEncryptionAndDecryption(streamingAead);
-  }
-
-
-  @Test
-  public void testMultipleKeys() throws Exception {
-    byte[] primaryKeyValue = Random.randBytes(KDF_KEY_SIZE);
-    byte[] otherKeyValue = Random.randBytes(KDF_KEY_SIZE);
-    byte[] anotherKeyValue = Random.randBytes(KDF_KEY_SIZE);
-    int derivedKeySize = AES_KEY_SIZE;
-
-    Key primaryKey = TestUtil.createKey(
-        TestUtil.createAesGcmHkdfStreamingKeyData(
-            primaryKeyValue, derivedKeySize, 512),
-        42,
-        KeyStatusType.ENABLED,
-        OutputPrefixType.RAW);
-    // Another key with a smaller segment size than the primary key
-    Key otherKey = TestUtil.createKey(
-        TestUtil.createAesCtrHmacStreamingKeyData(
-            otherKeyValue, derivedKeySize, 256),
-        43,
-        KeyStatusType.ENABLED,
-        OutputPrefixType.RAW);
-    // Another key with a larger segment size than the primary key
-    Key anotherKey = TestUtil.createKey(
-        TestUtil.createAesGcmHkdfStreamingKeyData(
-            anotherKeyValue, derivedKeySize, 1024),
-        72,
-        KeyStatusType.ENABLED,
-        OutputPrefixType.RAW);
-
-    KeysetHandle keysetHandle =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(primaryKey, otherKey, anotherKey));
-    StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
-
-    StreamingAead primaryAead =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(primaryKey))
-            .getPrimitive(StreamingAead.class);
-    StreamingAead otherAead =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(otherKey))
-            .getPrimitive(StreamingAead.class);
-    StreamingAead anotherAead =
-        TestUtil.createKeysetHandle(TestUtil.createKeyset(anotherKey))
-            .getPrimitive(StreamingAead.class);
-
-    StreamingTestUtil.testEncryptionAndDecryption(streamingAead, streamingAead);
-    StreamingTestUtil.testEncryptionAndDecryption(streamingAead, primaryAead);
-    StreamingTestUtil.testEncryptionAndDecryption(primaryAead, streamingAead);
-    StreamingTestUtil.testEncryptionAndDecryption(otherAead, streamingAead);
-    StreamingTestUtil.testEncryptionAndDecryption(anotherAead, streamingAead);
-    StreamingTestUtil.testEncryptionAndDecryption(primaryAead, primaryAead);
-    StreamingTestUtil.testEncryptionAndDecryption(otherAead, otherAead);
-    StreamingTestUtil.testEncryptionAndDecryption(anotherAead, anotherAead);
-    IOException expected =
-        assertThrows(
-            IOException.class,
-            () -> StreamingTestUtil.testEncryptionAndDecryption(otherAead, primaryAead));
-    assertExceptionContains(expected, "No matching key");
-    IOException expected2 =
-        assertThrows(
-            IOException.class,
-            () -> StreamingTestUtil.testEncryptionAndDecryption(anotherAead, primaryAead));
-    assertExceptionContains(expected2, "No matching key");
-  }
-
-  @Test
-  public void testInvalidKeyMaterial() throws Exception {
-    Key valid =
-        TestUtil.createKey(
-            TestUtil.createAesGcmHkdfStreamingKeyData(
-                Random.randBytes(KDF_KEY_SIZE), AES_KEY_SIZE, 128),
-            42,
-            KeyStatusType.ENABLED,
-            OutputPrefixType.RAW);
-    Key invalid =
-        TestUtil.createKey(
-            TestUtil.createAesSivKeyData(64), 43, KeyStatusType.ENABLED, OutputPrefixType.TINK);
-
-    KeysetHandle keysetHandle = TestUtil.createKeysetHandle(TestUtil.createKeyset(valid, invalid));
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class, () -> keysetHandle.getPrimitive(StreamingAead.class));
-    assertExceptionContains(e, "com.google.crypto.tink.StreamingAead not supported");
-
-    // invalid as the primary key.
-    KeysetHandle keysetHandle2 = TestUtil.createKeysetHandle(TestUtil.createKeyset(invalid, valid));
-    GeneralSecurityException e2 =
-        assertThrows(
-            GeneralSecurityException.class, () -> keysetHandle2.getPrimitive(StreamingAead.class));
-    assertExceptionContains(e2, "com.google.crypto.tink.StreamingAead not supported");
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplatesTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplatesTest.java
index 8e4e80b..25e7d03 100644
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplatesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadKeyTemplatesTest.java
@@ -16,21 +16,32 @@
 
 package com.google.crypto.tink.streamingaead;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.google.crypto.tink.TinkProtoParametersFormat;
 import com.google.crypto.tink.proto.AesCtrHmacStreamingKeyFormat;
 import com.google.crypto.tink.proto.AesGcmHkdfStreamingKeyFormat;
 import com.google.crypto.tink.proto.HashType;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.protobuf.ExtensionRegistryLite;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests for StreamingAeadKeyTemplates. */
-@RunWith(JUnit4.class)
+@RunWith(Theories.class)
 public class StreamingAeadKeyTemplatesTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    StreamingAeadConfig.register();
+  }
+
   @Test
   public void testAes128CtrHmacSha256_4KB() throws Exception {
     KeyTemplate template = StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_4KB;
@@ -207,4 +218,49 @@
     assertEquals(hkdfHashType,          format.getParams().getHkdfHashType());
     assertEquals(ciphertextSegmentSize, format.getParams().getCiphertextSegmentSize());
   }
+
+  public static class Pair {
+    public Pair(KeyTemplate template, StreamingAeadParameters parameters) {
+      this.template = template;
+      this.parameters = parameters;
+    }
+
+    KeyTemplate template;
+    StreamingAeadParameters parameters;
+  }
+
+  @DataPoints("EquivalentPairs")
+  public static final Pair[] TEMPLATES =
+      new Pair[] {
+        new Pair(
+            StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_4KB,
+            PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_4KB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_1MB,
+            PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_1MB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES256_CTR_HMAC_SHA256_4KB,
+            PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_4KB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES256_CTR_HMAC_SHA256_1MB,
+            PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_1MB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES128_GCM_HKDF_4KB,
+            PredefinedStreamingAeadParameters.AES128_GCM_HKDF_4KB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES128_GCM_HKDF_1MB,
+            PredefinedStreamingAeadParameters.AES128_GCM_HKDF_1MB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES256_GCM_HKDF_4KB,
+            PredefinedStreamingAeadParameters.AES256_GCM_HKDF_4KB),
+        new Pair(
+            StreamingAeadKeyTemplates.AES256_GCM_HKDF_1MB,
+            PredefinedStreamingAeadParameters.AES256_GCM_HKDF_1MB)
+      };
+
+  @Theory
+  public void testParametersEqualsKeyTemplate(@FromDataPoints("EquivalentPairs") Pair p)
+      throws Exception {
+    assertThat(TinkProtoParametersFormat.parse(p.template.toByteArray())).isEqualTo(p.parameters);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadTest.java
new file mode 100644
index 0000000..4e8cd14
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadTest.java
@@ -0,0 +1,279 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.streamingaead;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/** Unit tests for the StreamingAead package. Uses only the public API. */
+@RunWith(Theories.class)
+public final class StreamingAeadTest {
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    StreamingAeadConfig.register();
+    DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonStreamingAeadKeyset_throws.
+  }
+
+  @DataPoints("templates")
+  public static final String[] TEMPLATES =
+      new String[] {
+        "AES128_GCM_HKDF_4KB",
+        "AES128_GCM_HKDF_1MB",
+        "AES256_GCM_HKDF_4KB",
+        "AES256_GCM_HKDF_1MB",
+        "AES128_CTR_HMAC_SHA256_4KB",
+        "AES128_CTR_HMAC_SHA256_1MB",
+        "AES256_CTR_HMAC_SHA256_4KB",
+        "AES256_CTR_HMAC_SHA256_1MB"
+      };
+
+  /** Writes {@code data} to {@code writeableChannel}. */
+  private void writeToChannel(WritableByteChannel writeableChannel, byte[] data)
+      throws IOException {
+    ByteBuffer buffer = ByteBuffer.wrap(data);
+    int bytesWritten = 0;
+    while (bytesWritten < data.length) {
+      bytesWritten += writeableChannel.write(buffer);
+    }
+  }
+
+  /** Reads {@code bytesToRead} bytes from {@code readableChannel}.*/
+  private byte[] readFromChannel(ReadableByteChannel readableChannel, int bytesToRead)
+      throws IOException {
+    ByteBuffer buffer = ByteBuffer.allocate(bytesToRead);
+    int bytesRead = 0;
+    while (bytesRead < bytesToRead) {
+      bytesRead += readableChannel.read(buffer);
+    }
+    return buffer.array();
+  }
+
+  @Theory
+  public void createEncryptDecrypt(@FromDataPoints("templates") String templateName)
+      throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName));
+    StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    // Encrypt
+    ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream();
+    try (WritableByteChannel encryptingChannel =
+        streamingAead.newEncryptingChannel(
+            Channels.newChannel(ciphertextOutputStream), associatedData)) {
+      writeToChannel(encryptingChannel, plaintext);
+    }
+    byte[] ciphertext = ciphertextOutputStream.toByteArray();
+
+    // Decrypt
+    byte[] decrypted = null;
+    ReadableByteChannel ciphertextSource =
+        Channels.newChannel(new ByteArrayInputStream(ciphertext));
+    try (ReadableByteChannel decryptingChannel =
+        streamingAead.newDecryptingChannel(ciphertextSource, associatedData)) {
+      decrypted = readFromChannel(decryptingChannel, plaintext.length);
+    }
+    assertThat(decrypted).isEqualTo(plaintext);
+
+    // Decrypt with invalid associatedData fails
+    byte[] invalidAssociatedData = "invalid".getBytes(UTF_8);
+    ByteBuffer decrypted2 = ByteBuffer.allocate(plaintext.length);
+    ReadableByteChannel ciphertextSource2 =
+        Channels.newChannel(new ByteArrayInputStream(ciphertext));
+    try (ReadableByteChannel decryptingChannel =
+        streamingAead.newDecryptingChannel(ciphertextSource2, invalidAssociatedData)) {
+      assertThrows(IOException.class, () -> decryptingChannel.read(decrypted2));
+    }
+  }
+
+  // A keyset with one StreamingAead key, serialized in Tink's JSON format.
+  private static final String JSON_STREAMING_AEAD_KEYSET =
+      ""
+      + "{"
+      + "  \"primaryKeyId\": 1261393457,"
+      + "  \"key\": ["
+      + "    {"
+      + "      \"keyData\": {"
+      + "        \"typeUrl\":"
+      + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\","
+      + "        \"value\": \"GhBqEXuGvNvVCRjJX1IMvC0kEg4iBBAgCAMYAxAQCICAQA==\","
+      + "        \"keyMaterialType\": \"SYMMETRIC\""
+      + "      },"
+      + "      \"status\": \"ENABLED\","
+      + "      \"keyId\": 1261393457,"
+      + "      \"outputPrefixType\": \"RAW\""
+      + "    }"
+      + "  ]"
+      + "}";
+
+  @Theory
+  public void readKeyset_encryptDecrypt_success() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_STREAMING_AEAD_KEYSET, InsecureSecretKeyAccess.get());
+    StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream();
+    try (WritableByteChannel encryptingChannel =
+        streamingAead.newEncryptingChannel(
+            Channels.newChannel(ciphertextOutputStream), associatedData)) {
+      writeToChannel(encryptingChannel, plaintext);
+    }
+    byte[] ciphertext = ciphertextOutputStream.toByteArray();
+
+    byte[] decrypted = null;
+    ReadableByteChannel ciphertextSource =
+        Channels.newChannel(new ByteArrayInputStream(ciphertext));
+    try (ReadableByteChannel decryptingChannel =
+        streamingAead.newDecryptingChannel(ciphertextSource, associatedData)) {
+      decrypted = readFromChannel(decryptingChannel, plaintext.length);
+    }
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  // A keyset with multiple keys. The first key is the same as in JSON_STREAMING_AEAD_KEYSET.
+  private static final String JSON_STREAMING_AEAD_KEYSET_WITH_MULTIPLE_KEYS =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 1539463392,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\","
+          + "        \"value\": \"GhBqEXuGvNvVCRjJX1IMvC0kEg4iBBAgCAMYAxAQCICAQA==\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1261393457,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\","
+          + "        \"value\":"
+          + "\"GiA33jWXeuaAVvmFGQdU71KKA1K0rUQV8moj5LupxpgCJRIOIgQQIAgDGAMQIAiAgEA=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 1539463392,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    },"
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\":"
+          + "\"type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey\","
+          + "        \"value\":"
+          + "\"GiBpie88LEnKNiGNtyDiUwDmDzeHgrpmf4k2tC1OaWUpcRIOIgQQIAgDGAMQIAiAgEA=\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\""
+          + "      },"
+          + "      \"status\": \"ENABLED\","
+          + "      \"keyId\": 552736913,"
+          + "      \"outputPrefixType\": \"RAW\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void multipleKeysReadKeyset_encryptDecrypt_success() throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_STREAMING_AEAD_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get());
+    StreamingAead streamingAead = handle.getPrimitive(StreamingAead.class);
+
+    byte[] plaintext = "plaintext".getBytes(UTF_8);
+    byte[] associatedData = "associatedData".getBytes(UTF_8);
+
+    ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream();
+    try (WritableByteChannel encryptingChannel =
+        streamingAead.newEncryptingChannel(
+            Channels.newChannel(ciphertextOutputStream), associatedData)) {
+      writeToChannel(encryptingChannel, plaintext);
+    }
+    byte[] ciphertext = ciphertextOutputStream.toByteArray();
+
+    byte[] decrypted = null;
+    ReadableByteChannel ciphertextSource =
+        Channels.newChannel(new ByteArrayInputStream(ciphertext));
+    try (ReadableByteChannel decryptingChannel =
+        streamingAead.newDecryptingChannel(ciphertextSource, associatedData)) {
+      decrypted = readFromChannel(decryptingChannel, plaintext.length);
+    }
+    assertThat(decrypted).isEqualTo(plaintext);
+  }
+
+  // A keyset with a valid DeterministicAead key. This keyset can't be used with the StreamingAead
+  // primitive.
+  private static final String JSON_DAEAD_KEYSET =
+      ""
+          + "{"
+          + "  \"primaryKeyId\": 961932622,"
+          + "  \"key\": ["
+          + "    {"
+          + "      \"keyData\": {"
+          + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\","
+          + "        \"keyMaterialType\": \"SYMMETRIC\","
+          + "        \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS"
+          + "kvV2+7u6F2DN+kqUjAfkf2W\""
+          + "      },"
+          + "      \"outputPrefixType\": \"TINK\","
+          + "      \"keyId\": 961932622,"
+          + "      \"status\": \"ENABLED\""
+          + "    }"
+          + "  ]"
+          + "}";
+
+  @Theory
+  public void getPrimitiveFromNonStreamingAeadKeyset_throws()
+      throws Exception {
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get());
+    // Test that the keyset can create a DeterministicAead primitive, but not a StreamingAead.
+    Object unused = handle.getPrimitive(DeterministicAead.class);
+    assertThrows(GeneralSecurityException.class, () -> handle.getPrimitive(StreamingAead.class));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapperTest.java b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapperTest.java
index 15c00bd..19092e2 100644
--- a/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapperTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/streamingaead/StreamingAeadWrapperTest.java
@@ -19,15 +19,25 @@
 import static com.google.crypto.tink.testing.TestUtil.assertExceptionContains;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PrimitiveSet;
 import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import com.google.crypto.tink.proto.AesGcmHkdfStreamingKey;
+import com.google.crypto.tink.proto.AesGcmHkdfStreamingParams;
+import com.google.crypto.tink.proto.HashType;
+import com.google.crypto.tink.proto.KeyData;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.KeyStatusType;
+import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.proto.Keyset.Key;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.subtle.Random;
 import com.google.crypto.tink.testing.StreamingTestUtil;
 import com.google.crypto.tink.testing.TestUtil;
+import com.google.protobuf.ByteString;
 import java.io.IOException;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -40,6 +50,13 @@
   private static final int KDF_KEY_SIZE = 16;
   private static final int AES_KEY_SIZE = 16;
 
+  private static final String AES_GCM_HKDF_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey";
+  private static final ByteString KEY_BYTES_1 =
+      ByteString.copyFromUtf8("0123456789012345");
+  private static final ByteString KEY_BYTES_2 =
+      ByteString.copyFromUtf8("0123456789abcdef");
+
   @BeforeClass
   public static void setUp() throws Exception {
     StreamingAeadConfig.register();
@@ -151,4 +168,58 @@
             () -> StreamingTestUtil.testEncryptionAndDecryption(anotherAead, primaryAead));
     assertExceptionContains(expected2, "No matching key");
   }
+
+  @Test
+  public void testEncryptDecryptWithTinkKey() throws Exception {
+    AesGcmHkdfStreamingKey protoKey1 =
+        AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_1)
+            .setParams(
+                AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA1)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(512 * 1024))
+            .build();
+    Keyset.Key keysetKey1 =
+        Keyset.Key.newBuilder()
+            .setKeyData(
+                KeyData.newBuilder()
+                    .setTypeUrl(AES_GCM_HKDF_TYPE_URL)
+                    .setValue(protoKey1.toByteString())
+                    .setKeyMaterialType(KeyMaterialType.SYMMETRIC))
+        .setKeyId(1)
+        .setOutputPrefixType(OutputPrefixType.TINK)
+        .setStatus(KeyStatusType.ENABLED)
+        .build();
+    AesGcmHkdfStreamingKey protoKey2 =
+        AesGcmHkdfStreamingKey.newBuilder()
+            .setVersion(0)
+            .setKeyValue(KEY_BYTES_2)
+            .setParams(
+                AesGcmHkdfStreamingParams.newBuilder()
+                    .setHkdfHashType(HashType.SHA1)
+                    .setDerivedKeySize(16)
+                    .setCiphertextSegmentSize(512 * 1024))
+            .build();
+    Keyset.Key keysetKey2 =
+        Keyset.Key.newBuilder()
+            .setKeyData(
+                KeyData.newBuilder()
+                    .setTypeUrl(AES_GCM_HKDF_TYPE_URL)
+                    .setValue(protoKey2.toByteString())
+                    .setKeyMaterialType(KeyMaterialType.SYMMETRIC))
+        .setKeyId(2)
+        .setOutputPrefixType(OutputPrefixType.RAW)
+        .setStatus(KeyStatusType.ENABLED)
+        .build();
+
+    Keyset keyset =
+        Keyset.newBuilder().addKey(keysetKey1).addKey(keysetKey2).setPrimaryKeyId(1).build();
+    KeysetHandle keysetHandle =
+        TinkProtoKeysetFormat.parseKeyset(keyset.toByteArray(), InsecureSecretKeyAccess.get());
+    StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
+
+    StreamingTestUtil.testEncryptionAndDecryption(streamingAead, streamingAead);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AeadThreadSafetyTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AeadThreadSafetyTest.java
index 8099481..db7ed34 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AeadThreadSafetyTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AeadThreadSafetyTest.java
@@ -159,7 +159,7 @@
     // if we do this multithreaded, there is a potential for a race in case we call encrypt
     // for the first time at the same time in multiple threads. To get around this, we first encrypt
     // an empty plaintext here.
-    cipher.encrypt(new byte[0]);
+    Object unused = cipher.encrypt(new byte[0]);
 
     Aead aesCtrHmac = new EncryptThenAuthenticate(cipher, mac, macSize);
     testEncryptionDecryption(aesCtrHmac, 5, 128, 20);
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrHmacStreamingTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrHmacStreamingTest.java
index 86c590e..8f6e3f2 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrHmacStreamingTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrHmacStreamingTest.java
@@ -23,7 +23,6 @@
 import com.google.crypto.tink.config.TinkFips;
 import com.google.crypto.tink.testing.StreamingTestUtil;
 import com.google.crypto.tink.testing.StreamingTestUtil.SeekableByteBufferChannel;
-import com.google.crypto.tink.testing.TestUtil;
 import java.nio.ByteBuffer;
 import java.nio.channels.WritableByteChannel;
 import java.security.GeneralSecurityException;
@@ -46,7 +45,7 @@
   @Rule public TemporaryFolder tmpFolder = new TemporaryFolder();
 
   private AesCtrHmacStreaming createAesCtrHmacStreaming() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     String hkdfAlgo = "HmacSha256";
     int keySize = 16;
     String tagAlgo = "HmacSha256";
@@ -75,11 +74,7 @@
       int plaintextSize,
       int chunkSize)
       throws Exception {
-    if (TestUtil.shouldSkipTestWithAesKeySize(keySizeInBytes)) {
-      return;
-    }
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesCtrHmacStreaming ags =
         new AesCtrHmacStreaming(ikm, "HmacSha256", keySizeInBytes, "HmacSha256",
             tagSizeInBytes, segmentSize, firstSegmentOffset);
@@ -180,11 +175,7 @@
       int firstSegmentOffset,
       int plaintextSize)
       throws Exception {
-    if (TestUtil.shouldSkipTestWithAesKeySize(keySizeInBytes)) {
-      return;
-    }
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesCtrHmacStreaming ags =
         new AesCtrHmacStreaming(ikm, "HmacSha256", keySizeInBytes, "HmacSha256",
             tagSizeInBytes, segmentSize, firstSegmentOffset);
@@ -265,15 +256,10 @@
    * this stream.
    */
   public void testEncryptSingleBytes(int keySizeInBytes, int plaintextSize) throws Exception {
-    if (TestUtil.shouldSkipTestWithAesKeySize(keySizeInBytes)) {
-      return;
-    }
-
     int firstSegmentOffset = 0;
     int segmentSize = 512;
     int tagSizeInBytes = 12;
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesCtrHmacStreaming ags = new AesCtrHmacStreaming(ikm, "HmacSha256", keySizeInBytes,
         "HmacSha256", tagSizeInBytes, segmentSize, firstSegmentOffset);
     StreamingTestUtil.testEncryptSingleBytes(ags, plaintextSize);
@@ -308,14 +294,13 @@
     int firstSegmentOffset = 0;
     int keySizeInBytes = 16;
     int tagSizeInBytes = 12;
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesCtrHmacStreaming ags = new AesCtrHmacStreaming(ikm, "HmacSha256", keySizeInBytes,
         "HmacSha256", tagSizeInBytes, segmentSize, firstSegmentOffset);
 
     int plaintextSize = 1 << 15;
     int maxChunkSize = 100;
-    byte[] aad = TestUtil.hexDecode("aabbccddeeff");
+    byte[] aad = Hex.decode("aabbccddeeff");
     byte[] plaintext = StreamingTestUtil.generatePlaintext(plaintextSize);
     int ciphertextLength = (int) ags.expectedCiphertextSize(plaintextSize);
     ByteBuffer ciphertext = ByteBuffer.allocate(ciphertextLength);
@@ -349,7 +334,7 @@
   public void testModifiedCiphertext() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 256;
@@ -363,7 +348,7 @@
   public void testSkipWithStream() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 256;
@@ -386,7 +371,7 @@
   public void testModifiedCiphertextWithSeekableByteChannel() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 256;
@@ -397,7 +382,7 @@
   }
 
   @Test
-  /**
+  /*
    * Encrypts a plaintext consisting of 0's and checks that the ciphertext has no repeating blocks.
    * This is a simple test to catch basic errors that violate semantic security. The probability of
    * false positives is smaller than 2^{-100}.
@@ -406,8 +391,8 @@
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
     HashSet<String> ciphertextBlocks = new HashSet<String>();
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] aad = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] aad = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 256;
@@ -422,7 +407,7 @@
       byte[] ciphertext =
           StreamingTestUtil.encryptWithChannel(ags, plaintext, aad, ags.getFirstSegmentOffset());
       for (int pos = ags.getHeaderLength(); pos + blocksize <= ciphertext.length; pos++) {
-        String block = TestUtil.hexEncode(Arrays.copyOfRange(ciphertext, pos, pos + blocksize));
+        String block = Hex.encode(Arrays.copyOfRange(ciphertext, pos, pos + blocksize));
         if (!ciphertextBlocks.add(block)) {
           fail("Ciphertext contains a repeating block " + block + " at position " + pos);
         }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrJceCipherTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrJceCipherTest.java
index e5fea6e..442c830 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrJceCipherTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AesCtrJceCipherTest.java
@@ -23,7 +23,6 @@
 
 import com.google.crypto.tink.config.TinkFips;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.security.Security;
 import java.util.Arrays;
@@ -86,13 +85,13 @@
   public void testNistVector() throws Exception {
     Assume.assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
 
-    byte[] rawCiphertext = TestUtil.hexDecode(NIST_CIPHERTEXT);
-    byte[] iv = TestUtil.hexDecode(NIST_IV);
+    byte[] rawCiphertext = Hex.decode(NIST_CIPHERTEXT);
+    byte[] iv = Hex.decode(NIST_IV);
     byte[] ciphertext = new byte[iv.length + rawCiphertext.length];
     System.arraycopy(iv, 0, ciphertext, 0, iv.length);
     System.arraycopy(rawCiphertext, 0, ciphertext, iv.length, rawCiphertext.length);
-    AesCtrJceCipher cipher = new AesCtrJceCipher(TestUtil.hexDecode(NIST_KEY), iv.length);
-    assertArrayEquals(TestUtil.hexDecode(NIST_PLAINTEXT), cipher.decrypt(ciphertext));
+    AesCtrJceCipher cipher = new AesCtrJceCipher(Hex.decode(NIST_KEY), iv.length);
+    assertArrayEquals(Hex.decode(NIST_PLAINTEXT), cipher.decrypt(ciphertext));
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AesEaxJceTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AesEaxJceTest.java
index e485eb4..7858c34 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AesEaxJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AesEaxJceTest.java
@@ -16,6 +16,7 @@
 
 package com.google.crypto.tink.subtle;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
@@ -28,27 +29,19 @@
 import com.google.gson.JsonObject;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
+import java.util.HashSet;
 import javax.crypto.AEADBadTagException;
-import javax.crypto.Cipher;
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
-/**
- * Unit tests for AesEax.
- *
- * <p>TODO: Add more tests:
- *
- * <ul>
- *   <li>- maybe add NIST style verification.
- *   <li>- tests with long ciphertexts (e.g. BC had a bug with messages of size 8k or longer)
- *   <li>- check that IVs are distinct.
- *   <li>- use Github Wycheproof test vectors once they're published (b/66825199).
- * </ul>
- */
-@RunWith(JUnit4.class)
+/** Unit tests for AesEax. */
+@RunWith(Theories.class)
 public class AesEaxJceTest {
   private static final int KEY_SIZE = 16;
   private static final int IV_SIZE = 16;
@@ -57,14 +50,8 @@
 
   @Before
   public void setUp() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip tests with keys larger than 128 bits.");
-      keySizeInBytes = new Integer[] {16};
-    } else {
-      keySizeInBytes = new Integer[] {16, 32};
-    }
+
+    keySizeInBytes = new Integer[] {16, 32};
     ivSizeInBytes = new Integer[] {12, 16};
   }
 
@@ -151,16 +138,15 @@
 
     testModifyCiphertext(16, 16);
     testModifyCiphertext(16, 12);
-    // TODO(bleichen): Skipping test with key sizes larger than 128 bits because of b/35928521.
-    // testModifyCiphertext(24, 16);
-    // testModifyCiphertext(32, 16);
+    testModifyCiphertext(32, 16);
+    testModifyCiphertext(32, 12);
   }
 
   public void testModifyCiphertext(int keySizeInBytes, int ivSizeInBytes) throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
 
     byte[] aad = new byte[] {1, 2, 3};
-    byte[] key = Random.randBytes(KEY_SIZE);
+    byte[] key = Random.randBytes(keySizeInBytes);
     byte[] message = Random.randBytes(32);
     AesEaxJce eax = new AesEaxJce(key, ivSizeInBytes);
     byte[] ciphertext = eax.encrypt(message, aad);
@@ -245,7 +231,7 @@
     AesEaxJce eax = new AesEaxJce(key, IV_SIZE);
     for (int messageSize = 0; messageSize < 75; messageSize++) {
       byte[] message = Random.randBytes(messageSize);
-      {  // encrypting with aad as a 0-length array
+      { // encrypting with aad as a 0-length array
         byte[] ciphertext = eax.encrypt(message, aad);
         byte[] decrypted = eax.decrypt(ciphertext, aad);
         assertArrayEquals(message, decrypted);
@@ -258,7 +244,7 @@
               byte[] unused = eax.decrypt(ciphertext, badAad);
             });
       }
-      {  // encrypting with aad equal to null
+      { // encrypting with aad equal to null
         byte[] ciphertext = eax.encrypt(message, null);
         byte[] decrypted = eax.decrypt(ciphertext, aad);
         assertArrayEquals(message, decrypted);
@@ -274,6 +260,43 @@
     }
   }
 
+  /**
+   * This is a very simple test for the randomness of the nonce. The test simply checks that the
+   * multiple ciphertexts of the same message are distinct.
+   */
+  @Test
+  public void testRandomNonce() throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    final int samples = 1 << 17;
+    byte[] key = Random.randBytes(KEY_SIZE);
+    byte[] message = new byte[0];
+    byte[] aad = Random.randBytes(20);
+    AesEaxJce eax = new AesEaxJce(key, IV_SIZE);
+    HashSet<String> ciphertexts = new HashSet<>();
+    for (int i = 0; i < samples; i++) {
+      byte[] ct = eax.encrypt(message, aad);
+      String ctHex = Hex.encode(ct);
+      assertThat(ciphertexts).doesNotContain(ctHex);
+      ciphertexts.add(ctHex);
+    }
+  }
+
+  @Test
+  public void testEncryptDecryptLongMessage() throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    byte[] key = Random.randBytes(KEY_SIZE);
+    AesEaxJce eax = new AesEaxJce(key, IV_SIZE);
+
+    byte[] message = Random.randBytes(150000);
+    byte[] aad = Random.randBytes(20);
+
+    byte[] ciphertext = eax.encrypt(message, aad);
+    byte[] decrypted = eax.decrypt(ciphertext, aad);
+    assertArrayEquals(message, decrypted);
+  }
+
   @Test
   public void testFailIfFipsModeUsed() throws Exception {
     Assume.assumeTrue(TinkFips.useOnlyFips());
@@ -282,4 +305,93 @@
     assertThrows(GeneralSecurityException.class, () -> new AesEaxJce(key, IV_SIZE));
   }
 
+  private static class TestVector {
+    public TestVector(
+        String hexMessage, String hexKey, String hexNonce, String hexAad, String hexCiphertext) {
+      this.hexMessage = hexMessage;
+      this.hexKey = hexKey;
+      this.hexNonce = hexNonce;
+      this.hexAad = hexAad;
+      this.hexCiphertext = hexCiphertext;
+    }
+
+    public final String hexMessage;
+    public final String hexKey;
+    public final String hexNonce;
+    public final String hexAad;
+    public final String hexCiphertext;
+  }
+
+  @DataPoints("testVectors")
+  // Test vectors from "The EAX Mode of Operation", Appendix G.
+  public static final TestVector[] TEST_VECTORS =
+      new TestVector[] {
+        new TestVector(
+            "",
+            "233952dee4d5ed5f9b9c6d6ff80ff478",
+            "62ec67f9c3a4a407fcb2a8c49031a8b3",
+            "6bfb914fd07eae6b",
+            "e037830e8389f27b025a2d6527e79d01"),
+        new TestVector(
+            "f7fb",
+            "91945d3f4dcbee0bf45ef52255f095a4",
+            "becaf043b0a23d843194ba972c66debd",
+            "fa3bfd4806eb53fa",
+            "19dd5c4c9331049d0bdab0277408f67967e5"),
+        new TestVector(
+            "481c9e39b1",
+            "d07cf6cbb7f313bdde66b727afd3c5e8",
+            "8408dfff3c1a2b1292dc199e46b7d617",
+            "33cce2eabff5a79d",
+            "632a9d131ad4c168a4225d8e1ff755939974a7bede"),
+        new TestVector(
+            "40d0c07da5e4",
+            "35b6d0580005bbc12b0587124557d2c2",
+            "fdb6b06676eedc5c61d74276e1f8e816",
+            "aeb96eaebe2970e9",
+            "071dfe16c675cb0677e536f73afe6a14b74ee49844dd"),
+        new TestVector(
+            "4de3b35c3fc039245bd1fb7d",
+            "bd8e6e11475e60b268784c38c62feb22",
+            "6eac5c93072d8e8513f750935e46da1b",
+            "d4482d1ca78dce0f",
+            "835bb4f15d743e350e728414abb8644fd6ccb86947c5e10590210a4f"),
+        new TestVector(
+            "8b0a79306c9ce7ed99dae4f87f8dd61636",
+            "7c77d6e813bed5ac98baa417477a2e7d",
+            "1a8c98dcd73d38393b2bf1569deefc19",
+            "65d2017990d62528",
+            "02083e3979da014812f59f11d52630da30137327d10649b0aa6e1c181db617d7f2"),
+        new TestVector(
+            "1bda122bce8a8dbaf1877d962b8592dd2d56",
+            "5fff20cafab119ca2fc73549e20f5b0d",
+            "dde59b97d722156d4d9aff2bc7559826",
+            "54b9f04e6a09189a",
+            "2ec47b2c4954a489afc7ba4897edcdae8cc33b60450599bd02c96382902aef7f832a"),
+        new TestVector(
+            "6cf36720872b8513f6eab1a8a44438d5ef11",
+            "a4a4782bcffd3ec5e7ef6d8c34a56123",
+            "b781fcf2f75fa5a8de97a9ca48e522ec",
+            "899a175897561d7e",
+            "0de18fd0fdd91e7af19f1d8ee8733938b1e8e7f6d2231618102fdb7fe55ff1991700"),
+        new TestVector(
+            "ca40d7446e545ffaed3bd12a740a659ffbbb3ceab7",
+            "8395fcf1e95bebd697bd010bc766aac3",
+            "22e7add93cfc6393c57ec0b3c17d6b44",
+            "126735fcc320d25a",
+            "cb8920f87a6c75cff39627b56e3ed197c552d295a7cfc46afc253b4652b1af3795b124ab6e")
+      };
+
+  @Theory
+  public void testVector_decrypt_works(@FromDataPoints("testVectors") TestVector vector)
+      throws Exception {
+    // We cannot use "Assume" here because Theories will complain that no input does any test.
+    if (TinkFips.useOnlyFips()) {
+      return;
+    }
+    byte[] fullCiphertext = Hex.decode(vector.hexNonce + vector.hexCiphertext);
+    AesEaxJce eax = new AesEaxJce(Hex.decode(vector.hexKey), Hex.decode(vector.hexNonce).length);
+    byte[] decryption = eax.decrypt(fullCiphertext, Hex.decode(vector.hexAad));
+    assertThat(Hex.encode(decryption)).isEqualTo(vector.hexMessage);
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmHkdfStreamingTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmHkdfStreamingTest.java
index 786dcc3..41c249d 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmHkdfStreamingTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmHkdfStreamingTest.java
@@ -40,7 +40,7 @@
   @Rule public TemporaryFolder tmpFolder = new TemporaryFolder();
 
   private AesGcmHkdfStreaming createAesGcmStreaming() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     String hkdfAlgo = "HmacSha256";
     int keySize = 16;
     int segmentSize = 4096;
@@ -61,11 +61,7 @@
   public void testEncryptDecrypt(
       int keySizeInBytes, int segmentSize, int firstSegmentOffset, int plaintextSize, int chunkSize)
       throws Exception {
-    if (TestUtil.shouldSkipTestWithAesKeySize(keySizeInBytes)) {
-      return;
-    }
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesGcmHkdfStreaming ags =
         new AesGcmHkdfStreaming(ikm, "HmacSha256", keySizeInBytes, segmentSize,
             firstSegmentOffset);
@@ -135,11 +131,7 @@
   public void testEncryptDecryptRandomAccess(
       int keySizeInBytes, int segmentSize, int firstSegmentOffset, int plaintextSize)
       throws Exception {
-    if (TestUtil.shouldSkipTestWithAesKeySize(keySizeInBytes)) {
-      return;
-    }
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesGcmHkdfStreaming ags =
         new AesGcmHkdfStreaming(ikm, "HmacSha256", keySizeInBytes, segmentSize,
             firstSegmentOffset);
@@ -191,13 +183,9 @@
   }
 
   public void testEncryptSingleBytes(int keySizeInBytes, int plaintextSize) throws Exception {
-    if (TestUtil.shouldSkipTestWithAesKeySize(keySizeInBytes)) {
-      return;
-    }
     int firstSegmentOffset = 0;
     int segmentSize = 512;
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesGcmHkdfStreaming ags =
         new AesGcmHkdfStreaming(ikm, "HmacSha256", keySizeInBytes, segmentSize,
             firstSegmentOffset);
@@ -215,7 +203,7 @@
 
   @Test
   public void testSkipWithStream() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 256;
@@ -248,14 +236,13 @@
     int segmentSize = 512;
     int firstSegmentOffset = 0;
     int keySizeInBytes = 16;
-    byte[] ikm =
-        TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff");
     AesGcmHkdfStreaming ags =
         new AesGcmHkdfStreaming(ikm, "HmacSha256", keySizeInBytes, segmentSize,
             firstSegmentOffset);
     int plaintextSize = 1 << 15;
     int maxChunkSize = 100;
-    byte[] aad = TestUtil.hexDecode("aabbccddeeff");
+    byte[] aad = Hex.decode("aabbccddeeff");
     byte[] plaintext = StreamingTestUtil.generatePlaintext(plaintextSize);
     int ciphertextLength = (int) ags.expectedCiphertextSize(plaintextSize);
     ByteBuffer ciphertext = ByteBuffer.allocate(ciphertextLength);
@@ -287,7 +274,7 @@
   // (6) modify aad
   @Test
   public void testModifiedCiphertext() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     int keySize = 16;
     int segmentSize = 256;
     int offset = 8;
@@ -298,7 +285,7 @@
 
   @Test
   public void testModifiedCiphertextWithSeekableByteChannel() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
     int keySize = 16;
     int segmentSize = 256;
     int offset = 8;
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmJceTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmJceTest.java
index 2ee7fbc..032cfe8 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AesGcmJceTest.java
@@ -24,6 +24,7 @@
 
 import com.google.crypto.tink.config.TinkFips;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.internal.Util;
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.crypto.tink.testing.TestUtil.BytesMutation;
 import com.google.crypto.tink.testing.WycheproofTestUtil;
@@ -33,7 +34,7 @@
 import java.security.Security;
 import java.util.Arrays;
 import java.util.HashSet;
-import javax.crypto.Cipher;
+import javax.annotation.Nullable;
 import org.conscrypt.Conscrypt;
 import org.junit.Assume;
 import org.junit.Before;
@@ -49,14 +50,7 @@
 
   @Before
   public void setUp() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip tests with keys larger than 128 bits.");
-      keySizeInBytes = new Integer[] {16};
-    } else {
-      keySizeInBytes = new Integer[] {16, 32};
-    }
+    keySizeInBytes = new Integer[] {16, 32};
   }
 
   @Before
@@ -93,7 +87,9 @@
   @Test
   public void testEncryptWithAad_shouldFailOnAndroid19OrOlder() throws Exception {
     Assume.assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
-    Assume.assumeFalse(!SubtleUtil.isAndroid() || SubtleUtil.androidApiLevel() > 19);
+    @Nullable Integer apiLevel = Util.getAndroidApiLevel();
+    Assume.assumeNotNull(apiLevel);
+    Assume.assumeTrue(apiLevel <= 19);
 
     AesGcmJce gcm = new AesGcmJce(Random.randBytes(16));
     byte[] message = Random.randBytes(20);
@@ -103,7 +99,7 @@
   }
 
   @Test
-  /** BC had a bug, where GCM failed for messages of size > 8192 */
+  /* BC had a bug, where GCM failed for messages of size > 8192 */
   public void testLongMessages() throws Exception {
     Assume.assumeTrue(!TinkFips.useOnlyFips() || TinkFipsUtil.fipsModuleAvailable());
     Assume.assumeFalse(TestUtil.isAndroid()); // doesn't work on Android
@@ -187,7 +183,8 @@
         byte[] key = Hex.decode(testcase.get("key").getAsString());
         byte[] msg = Hex.decode(testcase.get("msg").getAsString());
         byte[] aad = Hex.decode(testcase.get("aad").getAsString());
-        if (SubtleUtil.isAndroid() && SubtleUtil.androidApiLevel() <= 19 && aad.length != 0) {
+        @Nullable Integer apiLevel = Util.getAndroidApiLevel();
+        if (apiLevel != null && apiLevel <= 19 && aad.length != 0) {
           cntSkippedTests++;
           continue;
         }
@@ -316,7 +313,7 @@
   }
 
   @Test
-  /**
+  /*
    * This is a very simple test for the randomness of the nonce. The test simply checks that the
    * multiple ciphertexts of the same message are distinct.
    */
@@ -331,7 +328,7 @@
     HashSet<String> ciphertexts = new HashSet<>();
     for (int i = 0; i < samples; i++) {
       byte[] ct = gcm.encrypt(message, aad);
-      String ctHex = TestUtil.hexEncode(ct);
+      String ctHex = Hex.encode(ct);
       assertThat(ciphertexts).doesNotContain(ctHex);
       ciphertexts.add(ctHex);
     }
@@ -341,7 +338,8 @@
     byte[] aad = Random.randBytes(20);
     // AES-GCM on Android <= 19 doesn't support AAD. See last bullet point in
     // https://github.com/google/tink/blob/master/docs/KNOWN-ISSUES.md#android.
-    if (SubtleUtil.isAndroid() && SubtleUtil.androidApiLevel() <= 19) {
+    @Nullable Integer apiLevel = Util.getAndroidApiLevel();
+    if (apiLevel != null && apiLevel <= 19) {
       aad = new byte[0];
     }
     return aad;
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/AesSivTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/AesSivTest.java
index 4b03ae6..0f85b46 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/AesSivTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/AesSivTest.java
@@ -31,7 +31,6 @@
 import java.security.InvalidKeyException;
 import java.util.Arrays;
 import javax.crypto.AEADBadTagException;
-import javax.crypto.Cipher;
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
@@ -46,14 +45,7 @@
 
   @Before
   public void setUp() throws Exception {
-    if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
-      System.out.println(
-          "Unlimited Strength Jurisdiction Policy Files are required"
-              + " but not installed. Skip most AesSiv tests.");
-      keySizeInBytes = new Integer[] {};
-    } else {
       keySizeInBytes = new Integer[] {64};
-    }
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/subtle/BUILD.bazel
index af19a8e..97a5904 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/BUILD.bazel
@@ -12,7 +12,6 @@
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
         "@maven//:com_google_code_gson_gson",
         "@maven//:junit_junit",
@@ -21,9 +20,9 @@
 
 java_test(
     name = "EcdsaVerifyJceTest",
-    size = "small",
     srcs = ["EcdsaVerifyJceTest.java"],
     data = ["@wycheproof//testvectors:all"],
+    tags = ["fips"],
     deps = [
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
@@ -91,7 +90,9 @@
     srcs = ["EngineFactoryTest.java"],
     deps = [
         "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
+        "@maven//:org_conscrypt_conscrypt_openjdk_uber",
     ],
 )
 
@@ -130,6 +131,7 @@
     size = "small",
     srcs = ["HkdfTest.java"],
     deps = [
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:hkdf",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
@@ -160,19 +162,6 @@
 )
 
 java_test(
-    name = "Ed25519Test",
-    size = "small",
-    srcs = ["Ed25519Test.java"],
-    deps = [
-        "//src/main/java/com/google/crypto/tink/subtle:ed25519_cluster",
-        "//src/main/java/com/google/crypto/tink/subtle:field25519",
-        "//src/main/java/com/google/crypto/tink/subtle:hex",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
     name = "RsaSsaPkcs1SignJceTest",
     size = "large",
     srcs = ["RsaSsaPkcs1SignJceTest.java"],
@@ -180,6 +169,7 @@
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:rsa_ssa_pkcs1_sign_jce",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
@@ -218,6 +208,7 @@
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -243,6 +234,7 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming",
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_hkdf_streaming",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:junit_junit",
@@ -280,8 +272,8 @@
     deps = [
         "//src/main/java/com/google/crypto/tink:aead",
         "//src/main/java/com/google/crypto/tink/subtle:encrypt_then_authenticate",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:junit_junit",
     ],
 )
@@ -324,15 +316,20 @@
     srcs = ["PrfHmacJceTest.java"],
     tags = ["fips"],
     deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:hmac_prf_parameters",
         "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:prf_hmac_jce",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
         "@maven//:org_conscrypt_conscrypt_openjdk_uber",
     ],
@@ -390,13 +387,14 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
+        "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_jce",
         "//src/main/java/com/google/crypto/tink/subtle:bytes",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_code_gson_gson",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
@@ -410,12 +408,18 @@
     srcs = ["PrfAesCmacTest.java"],
     tags = ["fips"],
     deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "//src/main/java/com/google/crypto/tink:mac",
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_key",
+        "//src/main/java/com/google/crypto/tink/prf:aes_cmac_prf_parameters",
+        "//src/main/java/com/google/crypto/tink/prf:prf_set",
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:prf_aes_cmac",
         "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
         "//src/main/java/com/google/crypto/tink/subtle:random",
+        "//src/main/java/com/google/crypto/tink/util:secret_bytes",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -428,8 +432,8 @@
         "//src/main/java/com/google/crypto/tink:streaming_aead",
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming",
         "//src/main/java/com/google/crypto/tink/subtle:aes_gcm_hkdf_streaming",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:junit_junit",
     ],
 )
@@ -439,6 +443,7 @@
     size = "small",
     srcs = ["RsaSsaPkcs1VerifyJceTest.java"],
     data = ["@wycheproof//testvectors:all"],
+    tags = ["fips"],
     deps = [
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
@@ -462,8 +467,8 @@
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util",
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_jce_cipher",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:junit_junit",
         "@maven//:org_conscrypt_conscrypt_openjdk_uber",
     ],
@@ -479,23 +484,12 @@
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/subtle:x_cha_cha20",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
 
 java_test(
-    name = "Field25519Test",
-    size = "small",
-    srcs = ["Field25519Test.java"],
-    deps = [
-        "//src/main/java/com/google/crypto/tink/subtle:field25519",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
     name = "XChaCha20Poly1305Test",
     size = "large",
     srcs = ["XChaCha20Poly1305Test.java"],
@@ -533,7 +527,6 @@
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:ind_cpa_cipher",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
@@ -562,6 +555,7 @@
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
         "@maven//:com_google_code_gson_gson",
+        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -576,21 +570,8 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/config:tink_fips",
         "//src/main/java/com/google/crypto/tink/subtle:aes_ctr_hmac_streaming",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/testing:streaming_test_util",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
-    name = "Poly1305Test",
-    size = "small",
-    srcs = ["Poly1305Test.java"],
-    deps = [
-        "//src/main/java/com/google/crypto/tink/subtle:poly1305",
-        "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -608,21 +589,8 @@
         "//src/main/java/com/google/crypto/tink/subtle:ed25519_verify",
         "//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
         "//src/main/java/com/google/crypto/tink/subtle:enums",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:random",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:junit_junit",
-    ],
-)
-
-java_test(
-    name = "Curve25519Test",
-    size = "small",
-    srcs = ["Curve25519Test.java"],
-    deps = [
-        "//src/main/java/com/google/crypto/tink/subtle:curve25519",
-        "//src/main/java/com/google/crypto/tink/subtle:field25519",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
-        "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
 )
@@ -635,9 +603,33 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/subtle:hex",
         "//src/main/java/com/google/crypto/tink/subtle:x25519",
-        "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
         "@maven//:com_google_code_gson_gson",
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "Base64Test",
+    size = "small",
+    srcs = ["Base64Test.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink/subtle:base64",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
+
+java_test(
+    name = "PrfMacTest",
+    size = "small",
+    srcs = ["PrfMacTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:mac",
+        "//src/main/java/com/google/crypto/tink/config:tink_fips",
+        "//src/main/java/com/google/crypto/tink/mac/internal:aes_cmac_test_util",
+        "//src/main/java/com/google/crypto/tink/subtle:prf_mac",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/Base64Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/Base64Test.java
new file mode 100644
index 0000000..f60a465
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/Base64Test.java
@@ -0,0 +1,135 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.subtle;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Base64Test {
+  @Test
+  public void testEncode_testVectors() {
+    assertThat(Base64.encode(new byte[] {})).isEqualTo("");
+    assertThat(Base64.encode(new byte[] {0})).isEqualTo("AA==");
+    assertThat(Base64.encode(new byte[] {0, 0})).isEqualTo("AAA=");
+    assertThat(Base64.encode(new byte[] {0, 0, 0})).isEqualTo("AAAA");
+    assertThat(Base64.encode(new byte[] {0, 0, 25})).isEqualTo("AAAZ");
+    assertThat(Base64.encode(new byte[] {0, 0, 26})).isEqualTo("AAAa");
+    assertThat(Base64.encode(new byte[] {0, 0, 51})).isEqualTo("AAAz");
+    assertThat(Base64.encode(new byte[] {0, 0, 52})).isEqualTo("AAA0");
+    assertThat(Base64.encode(new byte[] {0, 0, 61})).isEqualTo("AAA9");
+    assertThat(Base64.encode(new byte[] {0, 0, 62})).isEqualTo("AAA+");
+    assertThat(Base64.encode(new byte[] {0, 0, 63})).isEqualTo("AAA/");
+    assertThat(Base64.encode(new byte[] {0, 0, 64})).isEqualTo("AABA");
+  }
+
+  @Test
+  public void testUrlSafeEncode_testVectors() {
+    assertThat(Base64.urlSafeEncode(new byte[] {})).isEqualTo("");
+    assertThat(Base64.urlSafeEncode(new byte[] {0})).isEqualTo("AA");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0})).isEqualTo("AAA");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 0})).isEqualTo("AAAA");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 25})).isEqualTo("AAAZ");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 26})).isEqualTo("AAAa");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 51})).isEqualTo("AAAz");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 52})).isEqualTo("AAA0");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 61})).isEqualTo("AAA9");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 62})).isEqualTo("AAA-");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 63})).isEqualTo("AAA_");
+    assertThat(Base64.urlSafeEncode(new byte[] {0, 0, 64})).isEqualTo("AABA");
+  }
+
+  @Test
+  public void testEncode_noPadding() throws Exception {
+    assertThat(Base64.encode(new byte[] {}, Base64.NO_PADDING)).isEqualTo(new byte[]{});
+    assertThat(Base64.encode(new byte[] {0}, Base64.NO_PADDING))
+        .isEqualTo("AA\n".getBytes("UTF-8"));
+    assertThat(Base64.encode(new byte[] {0, 0}, Base64.NO_PADDING))
+        .isEqualTo("AAA\n".getBytes("UTF-8"));
+  }
+
+  @Test
+  public void testEncode_flags() throws Exception {
+    // 114 = 19 * 3 * 2 -> when encoded: 19 * 4 * 2 -- hence 2 lines at 19 * 4 characters
+    byte[] longByteArray = new byte[114];
+
+    String singleLine =
+        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+    assertThat(Base64.encode(longByteArray, Base64.DEFAULT))
+        .isEqualTo((singleLine + "\n" + singleLine + "\n").getBytes("UTF-8"));
+    assertThat(Base64.encode(longByteArray, Base64.NO_WRAP))
+        .isEqualTo((singleLine + singleLine).getBytes("UTF-8"));
+    assertThat(Base64.encode(longByteArray, Base64.CRLF))
+        .isEqualTo((singleLine + "\r\n" + singleLine + "\r\n").getBytes("UTF-8"));
+  }
+
+  @Test
+  public void testDecode_testVectors() {
+    assertThat(Base64.decode("")).isEqualTo(new byte[] {});
+    assertThat(Base64.decode("AA==")).isEqualTo(new byte[] {0});
+    assertThat(Base64.decode("AAA=")).isEqualTo(new byte[] {0, 0});
+    assertThat(Base64.decode("AAAA")).isEqualTo(new byte[] {0, 0, 0});
+    assertThat(Base64.decode("AAAZ")).isEqualTo(new byte[] {0, 0, 25});
+    assertThat(Base64.decode("AAAa")).isEqualTo(new byte[] {0, 0, 26});
+    assertThat(Base64.decode("AAAz")).isEqualTo(new byte[] {0, 0, 51});
+    assertThat(Base64.decode("AAA0")).isEqualTo(new byte[] {0, 0, 52});
+    assertThat(Base64.decode("AAA9")).isEqualTo(new byte[] {0, 0, 61});
+    assertThat(Base64.decode("AAA+")).isEqualTo(new byte[] {0, 0, 62});
+    assertThat(Base64.decode("AAA/")).isEqualTo(new byte[] {0, 0, 63});
+    assertThat(Base64.decode("AABA")).isEqualTo(new byte[] {0, 0, 64});
+  }
+
+  @Test
+  public void testUrlSafeDecode_testVectors() {
+    assertThat(Base64.urlSafeDecode("")).isEqualTo(new byte[] {});
+    assertThat(Base64.urlSafeDecode("AA")).isEqualTo(new byte[] {0});
+    assertThat(Base64.urlSafeDecode("AAA")).isEqualTo(new byte[] {0, 0});
+    assertThat(Base64.urlSafeDecode("AAAA")).isEqualTo(new byte[] {0, 0, 0});
+    assertThat(Base64.urlSafeDecode("AAAZ")).isEqualTo(new byte[] {0, 0, 25});
+    assertThat(Base64.urlSafeDecode("AAAa")).isEqualTo(new byte[] {0, 0, 26});
+    assertThat(Base64.urlSafeDecode("AAAz")).isEqualTo(new byte[] {0, 0, 51});
+    assertThat(Base64.urlSafeDecode("AAA0")).isEqualTo(new byte[] {0, 0, 52});
+    assertThat(Base64.urlSafeDecode("AAA9")).isEqualTo(new byte[] {0, 0, 61});
+    assertThat(Base64.urlSafeDecode("AAA-")).isEqualTo(new byte[] {0, 0, 62});
+    assertThat(Base64.urlSafeDecode("AAA_")).isEqualTo(new byte[] {0, 0, 63});
+    assertThat(Base64.urlSafeDecode("AABA")).isEqualTo(new byte[] {0, 0, 64});
+  }
+
+  @Test
+  public void testDecode_flags() throws Exception {
+    // 114 = 19 * 3 * 2 -> when encoded: 19 * 4 * 2 -- hence 2 lines at 19 * 4 characters
+    byte[] longByteArray = new byte[114];
+
+    String singleLine =
+        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+    assertThat(
+            Base64.decode(
+                (singleLine + "\n" + singleLine + "\n").getBytes("UTF-8"), Base64.DEFAULT))
+        .isEqualTo(longByteArray);
+    assertThat(Base64.decode((singleLine + singleLine).getBytes("UTF-8"), Base64.NO_WRAP))
+        .isEqualTo(longByteArray);
+    assertThat(
+            Base64.decode(
+                (singleLine + "\r\n" + singleLine + "\r\n").getBytes("UTF-8"), Base64.CRLF))
+        .isEqualTo(longByteArray);
+  }
+
+  // TODO(b/238096965) Add more tests.
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Test.java
index 4e8e795..68e4b95 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Poly1305Test.java
@@ -95,7 +95,7 @@
   }
 
   @Test
-  /** BC had a bug, where GCM failed for messages of size > 8192 */
+  /* BC had a bug, where GCM failed for messages of size > 8192 */
   public void testLongMessages() throws Exception {
     Assume.assumeFalse(TinkFips.useOnlyFips());
     Assume.assumeFalse(TestUtil.isAndroid()); // Doesn't work on Android
@@ -230,7 +230,7 @@
     final int samples = 1 << 10;
     for (int i = 0; i < samples; i++) {
       byte[] ct = aead.encrypt(message, aad);
-      String ctHex = TestUtil.hexEncode(ct);
+      String ctHex = Hex.encode(ct);
       assertFalse(ciphertexts.contains(ctHex));
       ciphertexts.add(ctHex);
     }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Test.java
index bdade13..9388830 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/ChaCha20Test.java
@@ -21,7 +21,6 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.common.truth.Truth;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import org.junit.Test;
@@ -47,10 +46,10 @@
         assertArrayEquals(
             String.format(
                 "\n\nMessage: %s\nKey: %s\nOutput: %s\nDecrypted Msg: %s\n",
-                TestUtil.hexEncode(expectedInput),
-                TestUtil.hexEncode(key),
-                TestUtil.hexEncode(output),
-                TestUtil.hexEncode(actualInput)),
+                Hex.encode(expectedInput),
+                Hex.encode(key),
+                Hex.encode(output),
+                Hex.encode(actualInput)),
             expectedInput,
             actualInput);
       }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/Curve25519Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/Curve25519Test.java
deleted file mode 100644
index d38d46d..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/Curve25519Test.java
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2022 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThrows;
-
-import com.google.common.truth.Expect;
-import com.google.crypto.tink.testing.TestUtil;
-import java.math.BigInteger;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@link Curve25519}. */
-@RunWith(JUnit4.class)
-public final class Curve25519Test {
-
-  @Rule public final Expect expect = Expect.create();
-
-  /** 1st iteration test in Section 5.2 of RFC 7748. https://tools.ietf.org/html/rfc7748 */
-  @Test
-  public void testCurveMult_success() throws Exception {
-    byte[] k = new byte[Field25519.FIELD_LEN];
-    k[0] = 9;
-
-    byte[] e = Arrays.copyOf(k, Field25519.FIELD_LEN);
-    e[0] &= (byte) 248;
-    e[31] &= (byte) 127;
-    e[31] |= (byte) 64;
-
-    long[] x = new long[Field25519.LIMB_CNT + 1];
-
-    Curve25519.curveMult(x, e, k);
-    assertEquals(
-        "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079",
-        TestUtil.hexEncode(Field25519.contract(x)));
-  }
-
-  /**
-   * 1st iteration test in Section 5.2 of RFC 7748. https://tools.ietf.org/html/rfc7748, but with
-   * the MSB set.
-   */
-  @Test
-  public void testCurveMultWithMbs_ignoresMsbAndDoesNotChangeInput() throws Exception {
-    byte[] kOriginal = new byte[Field25519.FIELD_LEN];
-    kOriginal[0] = 9;
-    kOriginal[31] = (byte) 0x80; // set MSB
-
-    byte[] k = Arrays.copyOf(kOriginal, Field25519.FIELD_LEN);
-    byte[] e = Arrays.copyOf(kOriginal, Field25519.FIELD_LEN);
-    e[0] &= (byte) 248;
-    e[31] &= (byte) 127;
-    e[31] |= (byte) 64;
-
-    long[] x = new long[Field25519.LIMB_CNT + 1];
-
-    Curve25519.curveMult(x, e, k);
-    expect
-        .that(TestUtil.hexEncode(Field25519.contract(x)))
-        .isEqualTo("422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079");
-    expect.that(k).isEqualTo(kOriginal);
-  }
-
-  private byte[] toLittleEndian(BigInteger n) {
-    byte[] b = new byte[32];
-    byte[] nBytes = n.toByteArray();
-
-    if ((nBytes.length > 33) || (nBytes.length == 33 && nBytes[0] != 0)) {
-      throw new IllegalArgumentException("Number too big");
-    }
-    if (nBytes.length == 33) {
-      System.arraycopy(nBytes, 1, b, 0, 32);
-    } else {
-      System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
-    }
-    for (int i = 0; i < b.length / 2; i++) {
-      byte t = b[i];
-      b[i] = b[b.length - i - 1];
-      b[b.length - i - 1] = t;
-    }
-    return b;
-  }
-
-  @Test
-  public void testBannedPublicKeys_fail() throws Exception {
-    // The values here are taken from https://cr.yp.to/ecdh.html#validate.
-
-    BigInteger two = BigInteger.valueOf(2);
-    BigInteger big25519 = two.pow(255).subtract(BigInteger.valueOf(19));
-    BigInteger big32 =
-        new BigInteger(
-            "325606250916557431795983626356110631294008115727848805560023387167927233504");
-    BigInteger big39 =
-        new BigInteger(
-            "39382357235489614581723060781553021112529911719440698176882885853963445705823");
-
-    byte[] n = toLittleEndian(two);
-    long[] x = new long[Field25519.LIMB_CNT + 1];
-
-    // 0
-    assertThrows(
-        InvalidKeyException.class,
-        () -> Curve25519.curveMult(x, n, toLittleEndian(BigInteger.ZERO)));
-
-    // 1
-    assertThrows(
-        InvalidKeyException.class,
-        () -> Curve25519.curveMult(x, n, toLittleEndian(BigInteger.ONE)));
-
-    // 325606250916557431795983626356110631294008115727848805560023387167927233504
-    assertThrows(
-        InvalidKeyException.class, () -> Curve25519.curveMult(x, n, toLittleEndian(big32)));
-
-    // 39382357235489614581723060781553021112529911719440698176882885853963445705823
-    assertThrows(
-        InvalidKeyException.class, () -> Curve25519.curveMult(x, n, toLittleEndian(big39)));
-
-    // 2^555 - 19 - 1
-    assertThrows(
-        InvalidKeyException.class,
-        () -> Curve25519.curveMult(x, n, toLittleEndian(big25519.subtract(BigInteger.ONE))));
-
-    // 2^555 - 19
-    assertThrows(
-        InvalidKeyException.class, () -> Curve25519.curveMult(x, n, toLittleEndian(big25519)));
-
-    // 2^555 - 19 + 1
-    assertThrows(
-        InvalidKeyException.class,
-        () -> Curve25519.curveMult(x, n, toLittleEndian(big25519.add(BigInteger.ONE))));
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/EcdsaVerifyJceTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/EcdsaVerifyJceTest.java
index e639f98..810531b 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/EcdsaVerifyJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/EcdsaVerifyJceTest.java
@@ -309,9 +309,15 @@
   public void testFailIfFipsModuleNotAvailable() throws Exception {
     Assume.assumeTrue(TinkFips.useOnlyFips() && !TinkFipsUtil.fipsModuleAvailable());
 
+    ECParameterSpec ecParams = EllipticCurves.getNistP256Params();
+    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
+    keyGen.initialize(ecParams);
+    KeyPair keyPair = keyGen.generateKeyPair();
+
     assertThrows(
         GeneralSecurityException.class,
-        () -> testWycheproofVectors(
-        "../wycheproof/testvectors/ecdsa_secp256r1_sha256_test.json", EcdsaEncoding.DER));
+        () ->
+            new EcdsaVerifyJce(
+                (ECPublicKey) keyPair.getPublic(), HashType.SHA256, EcdsaEncoding.DER));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519SignTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519SignTest.java
index c965657..54d4871 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519SignTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519SignTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.fail;
 
 import com.google.crypto.tink.config.TinkFips;
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.crypto.tink.testing.WycheproofTestUtil;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
@@ -56,10 +55,10 @@
         fail(
             String.format(
                 "\n\nMessage: %s\nSignature: %s\nPrivateKey: %s\nPublicKey: %s\n",
-                TestUtil.hexEncode(msg),
-                TestUtil.hexEncode(sig),
-                TestUtil.hexEncode(keyPair.getPrivateKey()),
-                TestUtil.hexEncode(keyPair.getPublicKey())));
+                Hex.encode(msg),
+                Hex.encode(sig),
+                Hex.encode(keyPair.getPrivateKey()),
+                Hex.encode(keyPair.getPublicKey())));
       }
     }
   }
@@ -75,17 +74,17 @@
     TreeSet<String> allSignatures = new TreeSet<String>();
     for (int i = 0; i < 100; i++) {
       byte[] sig = signer.sign(msg);
-      allSignatures.add(TestUtil.hexEncode(sig));
+      allSignatures.add(Hex.encode(sig));
       try {
         verifier.verify(sig, msg);
       } catch (GeneralSecurityException ex) {
         fail(
             String.format(
                 "\n\nMessage: %s\nSignature: %s\nPrivateKey: %s\nPublicKey: %s\n",
-                TestUtil.hexEncode(msg),
-                TestUtil.hexEncode(sig),
-                TestUtil.hexEncode(keyPair.getPrivateKey()),
-                TestUtil.hexEncode(keyPair.getPublicKey())));
+                Hex.encode(msg),
+                Hex.encode(sig),
+                Hex.encode(keyPair.getPrivateKey()),
+                Hex.encode(keyPair.getPublicKey())));
       }
     }
     // Ed25519 is deterministic, expect a unique signature for the same message.
@@ -124,10 +123,10 @@
         fail(
             String.format(
                 "\n\nMessage: %s\nSignature: %s\nPrivateKey: %s\nPublicKey: %s\n",
-                TestUtil.hexEncode(msg),
-                TestUtil.hexEncode(sig),
-                TestUtil.hexEncode(keyPair.getPrivateKey()),
-                TestUtil.hexEncode(keyPair.getPublicKey())));
+                Hex.encode(msg),
+                Hex.encode(sig),
+                Hex.encode(keyPair.getPrivateKey()),
+                Hex.encode(keyPair.getPublicKey())));
       }
     }
   }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519Test.java
deleted file mode 100644
index e8f7393..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/Ed25519Test.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for Ed25519. */
-@RunWith(JUnit4.class)
-public class Ed25519Test {
-  @Test
-  public void testGroupOrder() throws Exception {
-    assertEquals(32, Ed25519.GROUP_ORDER.length);
-    byte[] result = Ed25519.scalarMultWithBaseToBytes(Ed25519.GROUP_ORDER);
-    assertEquals(1, result[0]);
-    for (int i = 1; i < 32; i++) {
-      assertEquals(0, result[i]);
-    }
-  }
-
-  /** Test whether sign/verify method accidentally changes the public key or hashedPrivateKey. */
-  @Test
-  public void testUnmodifiedKey() throws Exception {
-    byte[] privateKey = Random.randBytes(Field25519.FIELD_LEN);
-    byte[] hashedPrivateKey = Ed25519.getHashedScalar(privateKey);
-    byte[] originalHashedPrivateKey =
-        Arrays.copyOfRange(hashedPrivateKey, 0, hashedPrivateKey.length);
-    byte[] publicKey = Ed25519.scalarMultWithBaseToBytes(hashedPrivateKey);
-    byte[] originalPublicKey = Arrays.copyOfRange(publicKey, 0, publicKey.length);
-    for (int i = 0; i < 64; i++) {
-      byte[] msg = Random.randBytes(1024);
-      byte[] sig = Ed25519.sign(msg, publicKey, hashedPrivateKey);
-      assertTrue(Ed25519.verify(msg, sig, publicKey));
-      assertArrayEquals(originalHashedPrivateKey, hashedPrivateKey);
-      assertArrayEquals(originalPublicKey, publicKey);
-    }
-  }
-
-  /** Test for https://github.com/google/tink/issues/224. */
-  @Test
-  public void testScalarMultWithBase() throws Exception {
-    byte[] scalar = Hex.decode("521784c403e6fb32d48e0da85969a82f5952856bde4471a42b3fa56fd8b96c0d");
-    Ed25519.scalarMultWithBaseToBytes(scalar);
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/EllipticCurvesTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/EllipticCurvesTest.java
index 715eabb..a60be18 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/EllipticCurvesTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/EllipticCurvesTest.java
@@ -16,11 +16,11 @@
 
 package com.google.crypto.tink.subtle;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.fail;
 
 import com.google.crypto.tink.testing.TestUtil;
 import com.google.crypto.tink.testing.WycheproofTestUtil;
@@ -44,121 +44,6 @@
 public class EllipticCurvesTest {
   // The tests are from
   // http://google.github.io/end-to-end/api/source/src/javascript/crypto/e2e/ecc/ecdh_testdata.js.src.html.
-  static class TestVector1 {
-    EllipticCurves.CurveType curve;
-    public String pubX;
-    public String pubY;
-
-    public TestVector1(EllipticCurves.CurveType curve, String pubX, String pubY) {
-      this.curve = curve;
-      this.pubX = pubX;
-      this.pubY = pubY;
-    }
-
-    public EllipticCurve getCurve() throws NoSuchAlgorithmException {
-      return EllipticCurves.getCurveSpec(curve).getCurve();
-    }
-  }
-
-  public static final TestVector1[] testVectors1 =
-      new TestVector1[] {
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P256,
-            "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287",
-            "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P256,
-            "809f04289c64348c01515eb03d5ce7ac1a8cb9498f5caa50197e58d43a86a7ae",
-            "b29d84e811197f25eba8f5194092cb6ff440e26d4421011372461f579271cda3"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P256,
-            "df3989b9fa55495719b3cf46dccd28b5153f7808191dd518eff0c3cff2b705ed",
-            "422294ff46003429d739a33206c8752552c8ba54a270defc06e221e0feaf6ac4"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P256,
-            "356c5a444c049a52fee0adeb7e5d82ae5aa83030bfff31bbf8ce2096cf161c4b",
-            "57d128de8b2a57a094d1a001e572173f96e8866ae352bf29cddaf92fc85b2f92"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P384,
-            "a7c76b970c3b5fe8b05d2838ae04ab47697b9eaf52e764592efda27fe7513272"
-                + "734466b400091adbf2d68c58e0c50066",
-            "ac68f19f2e1cb879aed43a9969b91a0839c4c38a49749b661efedf243451915e"
-                + "d0905a32b060992b468c64766fc8437a"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P384,
-            "30f43fcf2b6b00de53f624f1543090681839717d53c7c955d1d69efaf0349b736"
-                + "3acb447240101cbb3af6641ce4b88e0",
-            "25e46c0c54f0162a77efcc27b6ea792002ae2ba82714299c860857a68153ab62e"
-                + "525ec0530d81b5aa15897981e858757"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P521,
-            "000000685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a9490340"
-                + "854334b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2"
-                + "046d",
-            "000001ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b7398"
-                + "84a83bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302"
-                + "f676"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P521,
-            "000001df277c152108349bc34d539ee0cf06b24f5d3500677b4445453ccc21409"
-                + "453aafb8a72a0be9ebe54d12270aa51b3ab7f316aa5e74a951c5e53f74cd95fc29a"
-                + "ee7a",
-            "0000013d52f33a9f3c14384d1587fa8abe7aed74bc33749ad9c570b471776422c"
-                + "7d4505d9b0a96b3bfac041e4c6a6990ae7f700e5b4a6640229112deafa0cd8bb0d0"
-                + "89b0"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P521,
-            "00000092db3142564d27a5f0006f819908fba1b85038a5bc2509906a497daac67"
-                + "fd7aee0fc2daba4e4334eeaef0e0019204b471cd88024f82115d8149cc0cf4f7ce1"
-                + "a4d5",
-            "0000016bad0623f517b158d9881841d2571efbad63f85cbe2e581960c5d670601"
-                + "a6760272675a548996217e4ab2b8ebce31d71fca63fcc3c08e91c1d8edd91cf6fe8"
-                + "45f8"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P521,
-            "0000004f38816681771289ce0cb83a5e29a1ab06fc91f786994b23708ff08a08a"
-                + "0f675b809ae99e9f9967eb1a49f196057d69e50d6dedb4dd2d9a81c02bdcc8f7f51"
-                + "8460",
-            "0000009efb244c8b91087de1eed766500f0e81530752d469256ef79f6b965d8a2"
-                + "232a0c2dbc4e8e1d09214bab38485be6e357c4200d073b52f04e4a16fc6f5247187"
-                + "aecb"),
-        new TestVector1(
-            EllipticCurves.CurveType.NIST_P521,
-            "000001a32099b02c0bd85371f60b0dd20890e6c7af048c8179890fda308b359db"
-                + "bc2b7a832bb8c6526c4af99a7ea3f0b3cb96ae1eb7684132795c478ad6f962e4a6f"
-                + "446d",
-            "0000017627357b39e9d7632a1370b3e93c1afb5c851b910eb4ead0c9d387df67c"
-                + "de85003e0e427552f1cd09059aad0262e235cce5fba8cedc4fdc1463da76dcd4b6d"
-                + "1a46")
-      };
-
-  @Test
-  public void testPointOnCurve() throws Exception {
-    for (int i = 0; i < testVectors1.length; i++) {
-      ECPoint pubPoint =
-          new ECPoint(
-              new BigInteger(testVectors1[i].pubX, 16), new BigInteger(testVectors1[i].pubY, 16));
-      try {
-        EllipticCurves.checkPointOnCurve(pubPoint, testVectors1[i].getCurve());
-      } catch (GeneralSecurityException ex) {
-        fail("The valid public point is not on the curve: " + ex.getMessage());
-      }
-    }
-  }
-
-  @Test
-  public void testPointNotOnCurve() throws Exception {
-    for (int j = 0; j < testVectors1.length; j++) {
-      final int i = j;
-      ECPoint pubPoint =
-          new ECPoint(
-              new BigInteger(testVectors1[i].pubX, 16),
-              new BigInteger(testVectors1[i].pubY, 16).subtract(BigInteger.ONE));
-      assertThrows(
-          GeneralSecurityException.class,
-          () -> EllipticCurves.checkPointOnCurve(pubPoint, testVectors1[i].getCurve()));
-    }
-  }
 
   /**
    * A class for storing test vectors. This class contains the directory for the public and private
@@ -179,7 +64,7 @@
         String y) {
       this.curve = curve;
       this.format = format;
-      this.encoded = TestUtil.hexDecode(encodedHex);
+      this.encoded = Hex.decode(encodedHex);
       this.x = new BigInteger(x);
       this.y = new BigInteger(y);
     }
@@ -458,6 +343,22 @@
   };
 
   @Test
+  public void testFieldSizeInBytes() throws Exception {
+    assertThat(
+            EllipticCurves.fieldSizeInBytes(
+                EllipticCurves.getCurveSpec(EllipticCurves.CurveType.NIST_P256).getCurve()))
+        .isEqualTo(32);
+    assertThat(
+            EllipticCurves.fieldSizeInBytes(
+                EllipticCurves.getCurveSpec(EllipticCurves.CurveType.NIST_P384).getCurve()))
+        .isEqualTo(48);
+    assertThat(
+            EllipticCurves.fieldSizeInBytes(
+                EllipticCurves.getCurveSpec(EllipticCurves.CurveType.NIST_P521).getCurve()))
+        .isEqualTo(66);
+  }
+
+  @Test
   public void testPointDecode() throws Exception {
     for (TestVector2 test : testVectors2) {
       EllipticCurve curve = EllipticCurves.getCurveSpec(test.curve).getCurve();
@@ -473,10 +374,64 @@
       EllipticCurve curve = EllipticCurves.getCurveSpec(test.curve).getCurve();
       ECPoint p = new ECPoint(test.x, test.y);
       byte[] encoded = EllipticCurves.pointEncode(curve, test.format, p);
-      assertEquals(TestUtil.hexEncode(encoded), TestUtil.hexEncode(test.encoded));
+      assertEquals(Hex.encode(encoded), Hex.encode(test.encoded));
     }
   }
 
+  @Test
+  public void pointEncode_failsIfPointIsNotOnCurve() throws Exception {
+    // Same an entry of testVectors2, but the value of y has been incremented by 1.
+    BigInteger x = new BigInteger(
+        "79974177209371530366349631093481213364328002500948308276357601809416549347930");
+    BigInteger y = new BigInteger(
+           "11093679777528052772423074391650378811758820120351664471899251711300542565880");
+    // Adding one to y make the point not be on the curve.
+    assertThrows(
+        GeneralSecurityException.class,
+        () ->
+            EllipticCurves.pointEncode(
+                EllipticCurves.CurveType.NIST_P256,
+                EllipticCurves.PointFormatType.UNCOMPRESSED,
+                new ECPoint(x, y)));
+  }
+
+  @Test
+  public void pointDecode_uncompressed_failsIfPointIsNotOnCurve() throws Exception {
+    // Same an entry of testVectors2, but the last byte is changed from f7 to f6
+    byte[] encoded =
+        Hex.decode(
+            "04"
+                + "b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a"
+                + "1886ccdca5487a6772f9401888203f90587cc00a730e2b83d5c6f89b3b568df6");
+    // Adding one to y make the point not be on the curve.
+    assertThrows(GeneralSecurityException.class,
+        () -> EllipticCurves.pointDecode(EllipticCurves.CurveType.NIST_P256,
+            EllipticCurves.PointFormatType.UNCOMPRESSED, encoded));
+  }
+
+  @Test
+  public void pointDecode_crunchy_failsIfPointIsNotOnCurve() throws Exception {
+    // Same as an entry of testVectors2, but the last byte is changed from f4 to f5
+    byte[] encoded =
+        Hex.decode(
+            "0000000000000000000000000000000000000000000000000000000000000000"
+                + "66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f5");
+    // Adding one to y make the point not be on the curve.
+    assertThrows(GeneralSecurityException.class,
+        () -> EllipticCurves.pointDecode(EllipticCurves.CurveType.NIST_P256,
+            EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED, encoded));
+  }
+
+  @Test
+  public void pointDecode_compressed_failsIfEncodingIsInvalid() throws Exception {
+    // Same as an entry of testVectors2, but the last byte is changed from 00 to 01
+    byte[] encoded =
+        Hex.decode("020000000000000000000000000000000000000000000000000000000000000001");
+    assertThrows(GeneralSecurityException.class,
+        () -> EllipticCurves.pointDecode(EllipticCurves.CurveType.NIST_P256,
+            EllipticCurves.PointFormatType.COMPRESSED, encoded));
+  }
+
   /** A class to store a pair of valid Ecdsa signature in IEEE_P1363 and DER format. */
   protected static class EcdsaIeeeDer {
     public String hexIeee;
@@ -650,4 +605,6 @@
     }
     assertEquals(0, errors);
   }
+
+  // TODO(b/238096965): Add test that computeSharedSecret checks that the point is on the curve.
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/EncryptThenAuthenticateTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/EncryptThenAuthenticateTest.java
index a833445..520cd9c 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/EncryptThenAuthenticateTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/EncryptThenAuthenticateTest.java
@@ -21,10 +21,8 @@
 import static org.junit.Assert.fail;
 
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
-import javax.crypto.Cipher;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -50,10 +48,10 @@
         int ivSize,
         int tagLength) {
       try {
-        this.encKey = TestUtil.hexDecode(encKey);
-        this.macKey = TestUtil.hexDecode(macKey);
-        this.ciphertext = TestUtil.hexDecode(ciphertext);
-        this.aad = TestUtil.hexDecode(aad);
+        this.encKey = Hex.decode(encKey);
+        this.macKey = Hex.decode(macKey);
+        this.ciphertext = Hex.decode(ciphertext);
+        this.aad = Hex.decode(aad);
         this.macAlg = macAlg;
         this.ivSize = ivSize;
         this.tagLength = tagLength;
@@ -110,18 +108,8 @@
   public void testRFCVectors() throws Exception {
     for (int i = 0; i < rfcTestVectors.length; i++) {
       RFCTestVector t = rfcTestVectors[i];
-      if (Cipher.getMaxAllowedKeyLength("AES") < 256 && t.encKey.length > 16) {
-        System.out.println(
-            "Unlimited Strength Jurisdiction Policy Files are required"
-                + " but not installed. Skip tests with keys larger than 128 bits.");
-        continue;
-      }
       Aead aead = getAead(t.macKey, t.encKey, t.ivSize, t.tagLength, t.macAlg);
-      try {
-        aead.decrypt(t.ciphertext, t.aad);
-      } catch (GeneralSecurityException e) {
-        fail("Ciphertext and aad are valid, shouldn't reach here: " + i + " " + e);
-      }
+      Object unused = aead.decrypt(t.ciphertext, t.aad);
     }
   }
 
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryFipsTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryFipsTest.java
index f93912c..56adfed 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryFipsTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryFipsTest.java
@@ -22,6 +22,7 @@
 import java.security.GeneralSecurityException;
 import java.security.Provider;
 import java.security.Security;
+import java.util.List;
 import org.conscrypt.Conscrypt;
 import org.junit.Assume;
 import org.junit.Before;
@@ -106,4 +107,18 @@
     // Conscrypt does not provide "AES", so this must fail and not use another provider.
     assertThrows(GeneralSecurityException.class, () -> EngineFactory.CIPHER.getInstance("AES"));
   }
+
+  @Test
+  public void testNoFallbackEvenIfPreferred() throws Exception {
+    Provider p = Conscrypt.newProvider();
+    Security.addProvider(p);
+
+    for (Provider provider : Security.getProviders()) {
+      List<Provider> preferredProviders = EngineFactory.toProviderList(provider.getName());
+      // Conscrypt does not provide "AES", so this must fail and not use the preferred provider.
+      assertThrows(
+          GeneralSecurityException.class,
+          () -> EngineFactory.CIPHER.getInstance("AES", preferredProviders));
+    }
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryTest.java
index 442e251..f4951cc 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/EngineFactoryTest.java
@@ -16,6 +16,14 @@
 
 package com.google.crypto.tink.subtle;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import java.security.Provider;
+import java.security.Security;
+import java.util.List;
+import javax.crypto.Cipher;
+import org.conscrypt.Conscrypt;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -26,15 +34,95 @@
 
   @Test
   public void testAtLeastGetsACipherByDefault() throws Exception {
-    EngineFactory.CIPHER.getInstance("AES");
+    Object unused = EngineFactory.CIPHER.getInstance("AES");
     // didn't throw
   }
 
   @Test
   public void testIsReuseable() throws Exception {
-    EngineFactory.CIPHER.getInstance("AES");
-    EngineFactory.CIPHER.getInstance("AES");
-    EngineFactory.CIPHER.getInstance("AES");
+    Object unused = EngineFactory.CIPHER.getInstance("AES");
+    unused = EngineFactory.CIPHER.getInstance("AES");
+    unused = EngineFactory.CIPHER.getInstance("AES");
     // didn't throw
   }
+
+  @Test
+  public void testDefaultPolicyStillPrefersDefaultProviders() throws Exception {
+    Assume.assumeFalse(SubtleUtil.isAndroid());
+
+    // Add Conscrypt as an additional provider.
+    Conscrypt.checkAvailability();
+    Provider p = Conscrypt.newProvider();
+    Security.addProvider(p);
+    String conscryptName = p.getName();
+
+    // We expect that JDK gets picked first nonetheless.
+    assertThat(EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding").getProvider().getName())
+        .isNotEqualTo(conscryptName);
+  }
+
+  @Test
+  public void testDefaultPolicyRespectsPreferredProviders() throws Exception {
+    Assume.assumeFalse(SubtleUtil.isAndroid());
+
+    // Add Conscrypt as an additional provider.
+    Conscrypt.checkAvailability();
+    Provider p = Conscrypt.newProvider();
+    Security.addProvider(p);
+    String conscryptName = p.getName();
+    List<Provider> preferredProviders = EngineFactory.toProviderList(conscryptName);
+
+    // Check if Conscrypt can provide this cipher.
+    assertThat(Cipher.getInstance("AES/GCM/NoPadding", p)).isNotNull();
+
+    // We expect that our preferred provider is picked.
+    assertThat(
+            EngineFactory.CIPHER
+                .getInstance("AES/GCM/NoPadding", preferredProviders)
+                .getProvider()
+                .getName())
+        .isEqualTo(conscryptName);
+  }
+
+  @Test
+  public void testAndroidPolicyUsesConscrypt() throws Exception {
+    Assume.assumeTrue(SubtleUtil.isAndroid());
+
+    // We expect that the Android policy will prefer Conscrypt if available is on that Android
+    // device.
+    if (Security.getProvider("GmsCore_OpenSSL") != null) {
+      assertThat(EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding").getProvider().getName())
+          .isEqualTo("GmsCore_OpenSSL");
+
+    } else if (Security.getProvider("AndroidOpenSSL") != null) {
+      assertThat(EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding").getProvider().getName())
+          .isEqualTo("AndroidOpenSSL");
+    }
+  }
+
+  @Test
+  public void testAndroidPolicyAlwaysPrefersConscrypt() throws Exception {
+    Assume.assumeTrue(SubtleUtil.isAndroid());
+
+    List<Provider> preferredProviders = EngineFactory.toProviderList("BC", "Crypto");
+
+    // We expect that the Android policy will prefer Conscrypt if available is on that Android
+    // device.
+    if (Security.getProvider("GmsCore_OpenSSL") != null) {
+      assertThat(
+              EngineFactory.CIPHER
+                  .getInstance("AES/GCM/NoPadding", preferredProviders)
+                  .getProvider()
+                  .getName())
+          .isEqualTo("GmsCore_OpenSSL");
+
+    } else if (Security.getProvider("AndroidOpenSSL") != null) {
+      assertThat(
+              EngineFactory.CIPHER
+                  .getInstance("AES/GCM/NoPadding", preferredProviders)
+                  .getProvider()
+                  .getName())
+          .isEqualTo("AndroidOpenSSL");
+    }
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/Field25519Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/Field25519Test.java
deleted file mode 100644
index 1e15d84..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/Field25519Test.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import static com.google.crypto.tink.subtle.Field25519.FIELD_LEN;
-import static com.google.crypto.tink.subtle.Field25519.LIMB_CNT;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-
-import java.math.BigInteger;
-import java.security.SecureRandom;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Unit tests for {@link Field25519}.
- *
- * <p>TODO(quannguyen): Add more tests. Note that several functions assume that the inputs are in
- * reduced forms, so testing them don't guarantee that its uses are safe. There may be integer
- * overflow when they aren't used correctly.
- */
-@RunWith(JUnit4.class)
-public final class Field25519Test {
-  /**
-   * The idea of basic tests is simple. We generate random numbers, make computations with
-   * Field25519 and compare the results with Java BigInteger.
-   */
-  private static final int NUM_BASIC_TESTS = 1024;
-
-  private static final SecureRandom rand = new SecureRandom();
-  private static final BigInteger P =
-      BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
-  BigInteger[] x = new BigInteger[NUM_BASIC_TESTS];
-  BigInteger[] y = new BigInteger[NUM_BASIC_TESTS];
-
-  @Before
-  public void setUp() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      x[i] = (new BigInteger(FIELD_LEN * 8, rand)).mod(P);
-      y[i] = (new BigInteger(FIELD_LEN * 8, rand)).mod(P);
-    }
-  }
-
-  @Test
-  public void testBasicSum() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].add(y[i]).mod(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      byte[] yBytes = toLittleEndian(y[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.sum(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
-      Field25519.reduceCoefficients(output);
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Sum x[i] + y[i]: " + x[i] + "+" + y[i], expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testBasicSub() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].subtract(y[i]).mod(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      byte[] yBytes = toLittleEndian(y[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.sub(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
-      Field25519.reduceCoefficients(output);
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Subtraction x[i] - y[i]: " + x[i] + "-" + y[i], expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testBasicProduct() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].multiply(y[i]).mod(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      byte[] yBytes = toLittleEndian(y[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.product(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
-      Field25519.reduceSizeByModularReduction(output);
-      Field25519.reduceCoefficients(output);
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Product x[i] * y[i]: " + x[i] + "*" + y[i], expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testBasicMult() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].multiply(y[i]).mod(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      byte[] yBytes = toLittleEndian(y[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.mult(output, Field25519.expand(xBytes), Field25519.expand(yBytes));
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Multiplication x[i] * y[i]: " + x[i] + "*" + y[i], expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testBasicScalarProduct() {
-    final long scalar = 121665;
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].multiply(BigInteger.valueOf(scalar)).mod(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.scalarProduct(output, Field25519.expand(xBytes), scalar);
-      Field25519.reduceSizeByModularReduction(output);
-      Field25519.reduceCoefficients(output);
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Scalar product x[i] * 10 " + x[i] + "*" + 10, expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testBasicSquare() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].multiply(x[i]).mod(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.square(output, Field25519.expand(xBytes));
-      Field25519.reduceSizeByModularReduction(output);
-      Field25519.reduceCoefficients(output);
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Square x[i] * x[i]: " + x[i] + "*" + x[i], expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testBasicInverse() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      BigInteger expectedResult = x[i].modInverse(P);
-      byte[] xBytes = toLittleEndian(x[i]);
-      long[] output = new long[LIMB_CNT * 2 + 1];
-      Field25519.inverse(output, Field25519.expand(xBytes));
-      BigInteger result = new BigInteger(reverse(Field25519.contract(output)));
-      assertEquals("Inverse: x[i]^(-1) mod P: " + x[i], expectedResult, result);
-    }
-  }
-
-  @Test
-  public void testContractExpand() {
-    for (int i = 0; i < NUM_BASIC_TESTS; i++) {
-      byte[] xBytes = toLittleEndian(x[i]);
-      byte[] result = Field25519.contract(Field25519.expand(xBytes));
-      assertArrayEquals(xBytes, result);
-    }
-  }
-
-  private byte[] toLittleEndian(BigInteger n) {
-    byte[] b = new byte[32];
-    byte[] nBytes = n.toByteArray();
-    System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
-    for (int i = 0; i < b.length / 2; i++) {
-      byte t = b[i];
-      b[i] = b[b.length - i - 1];
-      b[b.length - i - 1] = t;
-    }
-    return b;
-  }
-
-  private byte[] reverse(byte[] x) {
-    byte[] r = new byte[x.length];
-    for (int i = 0; i < x.length; i++) {
-      r[i] = x[x.length - i - 1];
-    }
-    return r;
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/HkdfTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/HkdfTest.java
index 066a59b..9b96a47 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/HkdfTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/HkdfTest.java
@@ -20,7 +20,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -138,8 +137,8 @@
    */
   private String computeHkdfHex(String macAlgorithm, String ikmHex, String saltHex, String infoHex,
       int size) throws GeneralSecurityException {
-    return TestUtil.hexEncode(
-        Hkdf.computeHkdf(macAlgorithm, TestUtil.hexDecode(ikmHex), TestUtil.hexDecode(saltHex),
-          TestUtil.hexDecode(infoHex), size));
+    return Hex.encode(
+        Hkdf.computeHkdf(
+            macAlgorithm, Hex.decode(ikmHex), Hex.decode(saltHex), Hex.decode(infoHex), size));
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/Poly1305Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/Poly1305Test.java
deleted file mode 100644
index 011ca0d..0000000
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/Poly1305Test.java
+++ /dev/null
@@ -1,317 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.subtle;
-
-import static com.google.crypto.tink.subtle.Poly1305.MAC_KEY_SIZE_IN_BYTES;
-import static com.google.crypto.tink.subtle.Poly1305.MAC_TAG_SIZE_IN_BYTES;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.fail;
-
-import com.google.common.truth.Truth;
-import com.google.crypto.tink.testing.TestUtil;
-import java.nio.charset.Charset;
-import java.security.GeneralSecurityException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Unit tests for {@link Poly1305}.
- */
-@RunWith(JUnit4.class)
-public class Poly1305Test {
-  private static final Charset UTF_8 = Charset.forName("UTF-8");
-
-  @Test
-  public void testPoly1305ComputeMacThrowsIllegalArgExpWhenKeyLenIsGreaterThan32() {
-    IllegalArgumentException e =
-        assertThrows(
-            IllegalArgumentException.class,
-            () -> Poly1305.computeMac(new byte[MAC_KEY_SIZE_IN_BYTES + 1], new byte[0]));
-    Truth.assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32.");
-  }
-
-  @Test
-  public void testPoly1305ComputeMacThrowsIllegalArgExpWhenKeyLenIsLessThan32() {
-    IllegalArgumentException e =
-        assertThrows(
-            IllegalArgumentException.class,
-            () -> Poly1305.computeMac(new byte[MAC_KEY_SIZE_IN_BYTES - 1], new byte[0]));
-    Truth.assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32.");
-  }
-
-  @Test
-  public void testPoly1305VerifyMacThrowsIllegalArgExpWhenKeyLenIsGreaterThan32()
-      throws GeneralSecurityException {
-    IllegalArgumentException e =
-        assertThrows(
-            IllegalArgumentException.class,
-            () ->
-                Poly1305.verifyMac(new byte[MAC_KEY_SIZE_IN_BYTES + 1], new byte[0], new byte[0]));
-    Truth.assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32.");
-  }
-
-  @Test
-  public void testPoly1305VerifyMacThrowsIllegalArgExpWhenKeyLenIsLessThan32()
-      throws GeneralSecurityException {
-    IllegalArgumentException e =
-        assertThrows(
-            IllegalArgumentException.class,
-            () ->
-                Poly1305.verifyMac(new byte[MAC_KEY_SIZE_IN_BYTES - 1], new byte[0], new byte[0]));
-    Truth.assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32.");
-  }
-
-  @Test
-  public void testRandomPoly1305Mac() throws GeneralSecurityException {
-    for (int i = 0; i < 1000; i++) {
-      byte[] in = Random.randBytes(new java.util.Random().nextInt(300));
-      byte[] key = Random.randBytes(MAC_KEY_SIZE_IN_BYTES);
-      byte[] mac = Poly1305.computeMac(key, in);
-      try {
-        Poly1305.verifyMac(key, in, mac);
-      } catch (Throwable e) {
-        String error = String.format(
-            "\n\nIteration: %d\nInput: %s\nKey: %s\nMac: %s\n",
-            i,
-            TestUtil.hexEncode(in),
-            TestUtil.hexEncode(key),
-            TestUtil.hexEncode(mac));
-        fail(error + e.getMessage());
-      }
-    }
-  }
-
-  @Test
-  public void testFailedVerification() throws GeneralSecurityException {
-    byte[] key = new byte[MAC_KEY_SIZE_IN_BYTES];
-    key[0] = 1;
-    GeneralSecurityException e =
-        assertThrows(
-            GeneralSecurityException.class,
-            () -> Poly1305.verifyMac(key, new byte[] {1}, new byte[MAC_TAG_SIZE_IN_BYTES]));
-    Truth.assertThat(e).hasMessageThat().containsMatch("invalid MAC");
-  }
-
-  /**
-   * Tests against the test vectors in Section 2.5.2 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#section-2.5.2
-   */
-  @Test
-  public void testPoly1305() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "85d6be7857556d337f4452fe42d506a8"
-        + "0103808afb0db2fd4abff6af4149f51b");
-    byte[] in = ("Cryptographic Forum Research Group").getBytes(UTF_8);
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "a8061dc1305136c6c22b8baf0c0127a9"));
-  }
-
-  /**
-   * Tests against the test vector 1 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector1() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"));
-  }
-
-  /**
-   * Tests against the test vector 2 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector2() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"
-        + "36e5f6b5c5e06070f0efca96227a863e");
-    byte[] in = (
-        "Any submission to the IETF intended by the Contributor for publication as all or "
-            + "part of an IETF Internet-Draft or RFC and any statement made within the context "
-            + "of an IETF activity is considered an \"IETF Contribution\". Such statements "
-            + "include oral statements in IETF sessions, as well as written and electronic "
-            + "communications made at any time or place, which are addressed to")
-        .getBytes(UTF_8);
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "36e5f6b5c5e06070f0efca96227a863e"));
-  }
-
-  /**
-   * Tests against the test vector 3 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector3() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "36e5f6b5c5e06070f0efca96227a863e"
-        + "00000000000000000000000000000000");
-    byte[] in = (
-        "Any submission to the IETF intended by the Contributor for publication as all or "
-            + "part of an IETF Internet-Draft or RFC and any statement made within the context "
-            + "of an IETF activity is considered an \"IETF Contribution\". Such statements "
-            + "include oral statements in IETF sessions, as well as written and electronic "
-            + "communications made at any time or place, which are addressed to")
-        .getBytes(UTF_8);
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "f3477e7cd95417af89a6b8794c310cf0"));
-  }
-
-  /**
-   * Tests against the test vector 4 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector4() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "1c9240a5eb55d38af333888604f6b5f0"
-        + "473917c1402b80099dca5cbc207075c0");
-    byte[] in = TestUtil.hexDecode(""
-        + "2754776173206272696c6c69672c2061"
-        + "6e642074686520736c6974687920746f"
-        + "7665730a446964206779726520616e64"
-        + "2067696d626c6520696e207468652077"
-        + "6162653a0a416c6c206d696d73792077"
-        + "6572652074686520626f726f676f7665"
-        + "732c0a416e6420746865206d6f6d6520"
-        + "7261746873206f757467726162652e");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "4541669a7eaaee61e708dc7cbcc5eb62"));
-  }
-
-  /**
-   * Tests against the test vector 5 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector5() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "ffffffffffffffffffffffffffffffff");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "03000000000000000000000000000000"));
-  }
-
-  /**
-   * Tests against the test vector 6 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector6() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000"
-        + "ffffffffffffffffffffffffffffffff");
-    byte[] in = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "03000000000000000000000000000000"));
-  }
-
-  /**
-   * Tests against the test vector 7 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector7() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "ffffffffffffffffffffffffffffffff"
-        + "f0ffffffffffffffffffffffffffffff"
-        + "11000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "05000000000000000000000000000000"));
-  }
-
-  /**
-   * Tests against the test vector 8 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector8() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "ffffffffffffffffffffffffffffffff"
-        + "fbfefefefefefefefefefefefefefefe"
-        + "01010101010101010101010101010101");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "00000000000000000000000000000000"));
-  }
-
-  /**
-   * Tests against the test vector 9 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector9() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "02000000000000000000000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "fdffffffffffffffffffffffffffffff");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "faffffffffffffffffffffffffffffff"));
-  }
-
-  /**
-   * Tests against the test vector 10 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector10() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000400000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "e33594d7505e43b90000000000000000"
-        + "3394d7505e4379cd0100000000000000"
-        + "00000000000000000000000000000000"
-        + "01000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "14000000000000005500000000000000"));
-  }
-
-  /**
-   * Tests against the test vector 11 in Appendix A.3 of RFC 7539.
-   * https://tools.ietf.org/html/rfc7539#appendix-A.3
-   */
-  @Test
-  public void testPoly1305TestVector11() throws GeneralSecurityException {
-    byte[] key = TestUtil.hexDecode(""
-        + "01000000000000000400000000000000"
-        + "00000000000000000000000000000000");
-    byte[] in = TestUtil.hexDecode(""
-        + "e33594d7505e43b90000000000000000"
-        + "3394d7505e4379cd0100000000000000"
-        + "00000000000000000000000000000000");
-    Truth.assertThat(Poly1305.computeMac(key, in)).isEqualTo(TestUtil.hexDecode(""
-        + "13000000000000000000000000000000"));
-  }
-}
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfAesCmacTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfAesCmacTest.java
index ec66ffd..713f0aa 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfAesCmacTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfAesCmacTest.java
@@ -16,11 +16,17 @@
 
 package com.google.crypto.tink.subtle;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.Mac;
 import com.google.crypto.tink.config.TinkFips;
+import com.google.crypto.tink.prf.AesCmacPrfKey;
+import com.google.crypto.tink.prf.AesCmacPrfParameters;
+import com.google.crypto.tink.prf.Prf;
+import com.google.crypto.tink.util.SecretBytes;
 import java.security.GeneralSecurityException;
 import java.security.InvalidAlgorithmParameterException;
 import java.util.Arrays;
@@ -181,4 +187,19 @@
         InvalidAlgorithmParameterException.class,
         () -> new PrfMac(new PrfAesCmac(Random.randBytes(16)), 17));
   }
+
+  @Test
+  public void createWithAesCmacPrfKey_equivalentToByteArray() throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+    for (MacTestVector t : CMAC_TEST_VECTORS) {
+      Prf aesCmacPrfKeyPrf =
+          PrfAesCmac.create(
+              AesCmacPrfKey.create(
+                  AesCmacPrfParameters.create(t.key.length),
+                  SecretBytes.copyFrom(t.key, InsecureSecretKeyAccess.get())));
+      Prf byteArrayPrf = new PrfAesCmac(t.key);
+      assertThat(aesCmacPrfKeyPrf.compute(t.message, t.tag.length))
+          .isEqualTo(byteArrayPrf.compute(t.message, t.tag.length));
+    }
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java
index efbadda..5740209 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java
@@ -16,16 +16,22 @@
 
 package com.google.crypto.tink.subtle;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.Mac;
 import com.google.crypto.tink.config.TinkFips;
 import com.google.crypto.tink.config.internal.TinkFipsUtil;
+import com.google.crypto.tink.prf.HmacPrfKey;
+import com.google.crypto.tink.prf.HmacPrfParameters;
+import com.google.crypto.tink.prf.HmacPrfParameters.HashType;
 import com.google.crypto.tink.prf.Prf;
 import com.google.crypto.tink.testing.TestUtil;
+import com.google.crypto.tink.util.SecretBytes;
 import java.security.GeneralSecurityException;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.Security;
@@ -279,8 +285,9 @@
 
   private static void testPrfNoExceptionIfTagSizeIsTooSmall(String algoName) throws Exception {
     for (int i = 0; i < PrfMac.MIN_TAG_SIZE_IN_BYTES; i++) {
-      new PrfHmacJce(algoName, new SecretKeySpec(Random.randBytes(16), "HMAC"))
-          .compute(new byte[100], i);
+      Object unused =
+          new PrfHmacJce(algoName, new SecretKeySpec(Random.randBytes(16), "HMAC"))
+              .compute(new byte[100], i);
     }
   }
 
@@ -328,4 +335,47 @@
         GeneralSecurityException.class,
         () -> new PrfHmacJce("HMACSHA256", new SecretKeySpec(Random.randBytes(16), "HMAC")));
   }
+
+  @Test
+  public void createWithHmacPrfKey_equivalentToByteArray() throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+    for (MacTestVector t : HMAC_TEST_VECTORS) {
+      HmacPrfParameters.HashType hashType;
+      switch (t.algName) {
+        case "HMACSHA1":
+          hashType = HashType.SHA1;
+          break;
+        case "HMACSHA224":
+          hashType = HashType.SHA224;
+          break;
+        case "HMACSHA256":
+          hashType = HashType.SHA256;
+          break;
+        case "HMACSHA384":
+          hashType = HashType.SHA384;
+          break;
+        case "HMACSHA512":
+          hashType = HashType.SHA512;
+          break;
+        default:
+          // Should not happen
+          throw new IllegalStateException("Unknown algorithm: " + t.algName);
+      }
+      Prf hmacPrfKeyPrf =
+          PrfHmacJce.create(
+              HmacPrfKey.builder()
+                  .setKeyBytes(SecretBytes.copyFrom(t.key, InsecureSecretKeyAccess.get()))
+                  .setParameters(
+                      HmacPrfParameters.builder()
+                          .setKeySizeBytes(t.key.length)
+                          .setHashType(hashType)
+                          .build())
+                  .build());
+
+      Prf byteArrayPrf = new PrfHmacJce(t.algName, new SecretKeySpec(t.key, "HMAC"));
+
+      assertThat(hmacPrfKeyPrf.compute(t.message, t.tag.length))
+          .isEqualTo(byteArrayPrf.compute(t.message, t.tag.length));
+    }
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfMacTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfMacTest.java
new file mode 100644
index 0000000..44d895f
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfMacTest.java
@@ -0,0 +1,91 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.subtle;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.Mac;
+import com.google.crypto.tink.config.TinkFips;
+import com.google.crypto.tink.mac.internal.AesCmacTestUtil;
+import com.google.crypto.tink.mac.internal.AesCmacTestUtil.AesCmacTestVector;
+import java.security.GeneralSecurityException;
+import org.junit.Assume;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public class PrfMacTest {
+
+  @DataPoints("allTestVectors")
+  public static final AesCmacTestVector[] CMAC_IMPLEMENTATION_TEST_VECTORS =
+      new AesCmacTestVector[] {
+          AesCmacTestUtil.RFC_TEST_VECTOR_0,
+          AesCmacTestUtil.RFC_TEST_VECTOR_1,
+          AesCmacTestUtil.RFC_TEST_VECTOR_2,
+          AesCmacTestUtil.NOT_OVERFLOWING_INTERNAL_STATE,
+          AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE,
+          AesCmacTestUtil.FILL_UP_EXACTLY_INTERNAL_STATE_TWICE,
+          AesCmacTestUtil.OVERFLOW_INTERNAL_STATE_ONCE,
+          AesCmacTestUtil.OVERFLOW_INTERNAL_STATE_TWICE,
+          AesCmacTestUtil.SHORTER_TAG,
+          AesCmacTestUtil.TAG_WITH_KEY_PREFIX_TYPE_LEGACY,
+          AesCmacTestUtil.TAG_WITH_KEY_PREFIX_TYPE_TINK,
+          AesCmacTestUtil.LONG_KEY_TEST_VECTOR,
+      };
+
+  @DataPoints("failingTestVectors")
+  public static final AesCmacTestVector[] CMAC_FAILING_TEST_VECTORS =
+      new AesCmacTestVector[] {
+          AesCmacTestUtil.WRONG_PREFIX_TAG_LEGACY,
+          AesCmacTestUtil.WRONG_PREFIX_TAG_TINK,
+          AesCmacTestUtil.TAG_TOO_SHORT
+      };
+
+  @Theory
+  public void computeMac_isCorrect(
+      @FromDataPoints("allTestVectors") AesCmacTestVector t) throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    Mac aesCmac = PrfMac.create(t.key);
+
+    assertThat(aesCmac.computeMac(t.message)).isEqualTo(t.tag);
+  }
+
+  @Theory
+  public void verifyMac_isCorrect(
+      @FromDataPoints("allTestVectors") AesCmacTestVector t) throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    Mac aesCmac = PrfMac.create(t.key);
+
+    aesCmac.verifyMac(t.tag, t.message);
+  }
+
+  @Theory
+  public void verifyMac_throwsOnWrongTag(
+      @FromDataPoints("failingTestVectors") AesCmacTestVector t) throws Exception {
+    Assume.assumeFalse(TinkFips.useOnlyFips());
+
+    Mac aesCmac = PrfMac.create(t.key);
+
+    assertThrows(GeneralSecurityException.class, () -> aesCmac.verifyMac(t.tag, t.message));
+  }
+}
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/RsaSsaPkcs1SignJceTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/RsaSsaPkcs1SignJceTest.java
index b1cb777..43ca6a3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/RsaSsaPkcs1SignJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/RsaSsaPkcs1SignJceTest.java
@@ -139,7 +139,7 @@
     RsaSsaPkcs1SignJce signer = new RsaSsaPkcs1SignJce(priv, HashType.SHA512);
     for (int i = 0; i < 100; i++) {
       byte[] sig = signer.sign(msg);
-      allSignatures.add(TestUtil.hexEncode(sig));
+      allSignatures.add(Hex.encode(sig));
       // Verify with JCE's Signature.
       Signature verifier = Signature.getInstance("SHA512WithRSA");
       verifier.initVerify(pub);
@@ -148,10 +148,10 @@
         fail(
             String.format(
                 "\n\nMessage: %s\nSignature: %s\nPrivateKey: %s\nPublicKey: %s\n",
-                TestUtil.hexEncode(msg),
-                TestUtil.hexEncode(sig),
-                TestUtil.hexEncode(priv.getEncoded()),
-                TestUtil.hexEncode(pub.getEncoded())));
+                Hex.encode(msg),
+                Hex.encode(sig),
+                Hex.encode(priv.getEncoded()),
+                Hex.encode(pub.getEncoded())));
       }
     }
     // RSA SSA PKCS1 is deterministic, expect a unique signature for the same message.
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/SignatureThreadSafetyTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/SignatureThreadSafetyTest.java
index 3d58177..1de5755 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/SignatureThreadSafetyTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/SignatureThreadSafetyTest.java
@@ -24,7 +24,6 @@
 import com.google.crypto.tink.PublicKeyVerify;
 import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
 import com.google.crypto.tink.subtle.Enums.HashType;
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.interfaces.ECPrivateKey;
@@ -107,7 +106,7 @@
       try {
         for (int i = 0; i < count; i++) {
           byte[] signature = signer.sign(message);
-          signatures.add(TestUtil.hexEncode(signature));
+          signatures.add(Hex.encode(signature));
         }
       } catch (Exception ex) {
         getUncaughtExceptionHandler().uncaughtException(this, ex);
@@ -150,7 +149,7 @@
     exceptionHandler.check();
     if (isDeterministic) {
       for (int i = 0; i < numberOfThreads; i++) {
-        String expectedSignature = TestUtil.hexEncode(signer.sign(messages[i]));
+        String expectedSignature = Hex.encode(signer.sign(messages[i]));
         assertEquals(1, signatures.get(i).size());
         assertTrue(signatures.get(i).contains(expectedSignature));
       }
@@ -158,7 +157,7 @@
       for (int i = 0; i < numberOfThreads; i++) {
         assertEquals(numberOfSignatures, signatures.get(i).size());
         for (String sig : signatures.get(i)) {
-          verifier.verify(TestUtil.hexDecode(sig), messages[i]);
+          verifier.verify(Hex.decode(sig), messages[i]);
         }
       }
     }
@@ -181,7 +180,7 @@
     // initalize some of the java.security.Providers the first time we sign. If we do this
     // multithreaded, there is a potential for a race. To get around this, we first sign once, to
     // initialize everything.
-    signer.sign(message);
+    Object unused = signer.sign(message);
 
     ExceptionHandler exceptionHandler = new ExceptionHandler();
     Thread[] thread = new Thread[numberOfThreads];
@@ -201,7 +200,7 @@
     exceptionHandler.check();
 
     if (isDeterministic) {
-      String expectedSignature = TestUtil.hexEncode(signer.sign(message));
+      String expectedSignature = Hex.encode(signer.sign(message));
       for (int i = 0; i < numberOfThreads; i++) {
         assertEquals(1, signatures.get(i).size());
         assertTrue(signatures.get(i).contains(expectedSignature));
@@ -210,7 +209,7 @@
       HashSet<String> allSignatures = new HashSet<String>();
       for (int i = 0; i < numberOfThreads; i++) {
         for (String sig : signatures.get(i)) {
-          verifier.verify(TestUtil.hexDecode(sig), message);
+          verifier.verify(Hex.decode(sig), message);
           assertFalse(allSignatures.contains(sig));
           allSignatures.add(sig);
         }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/StreamingAeadThreadSafetyTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/StreamingAeadThreadSafetyTest.java
index 9cc6e28..c2b0255 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/StreamingAeadThreadSafetyTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/StreamingAeadThreadSafetyTest.java
@@ -22,7 +22,6 @@
 import com.google.crypto.tink.StreamingAead;
 import com.google.crypto.tink.testing.StreamingTestUtil.ByteBufferChannel;
 import com.google.crypto.tink.testing.StreamingTestUtil.SeekableByteBufferChannel;
-import com.google.crypto.tink.testing.TestUtil;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -169,8 +168,8 @@
 
   @Test
   public void testDecryptionAesGcm() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int segmentSize = 512;
     AesGcmHkdfStreaming ags = new AesGcmHkdfStreaming(ikm, "HmacSha256", keySize, segmentSize, 0);
@@ -179,8 +178,8 @@
 
   @Test
   public void testDecryptionAesCtrHmac() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 512;
@@ -284,8 +283,8 @@
 
   @Test
   public void testEncryptionAesGcm() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int segmentSize = 512;
     AesGcmHkdfStreaming ags = new AesGcmHkdfStreaming(ikm, "HmacSha256", keySize, segmentSize, 0);
@@ -294,8 +293,8 @@
 
   @Test
   public void testEncryptionAesCtrHmac() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 512;
@@ -306,8 +305,8 @@
 
   @Test
   public void testEncryptionLargeChunks() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int segmentSize = 512;
     int chunkSize = 2048; // the size for each concurrent read.
@@ -317,8 +316,8 @@
 
   @Test
   public void testEncryptionSmallChunks() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int segmentSize = 512;
     int chunkSize = 3; // the size for each concurrent read.
@@ -382,7 +381,7 @@
                     + " start:"
                     + pos
                     + "\nbytes:"
-                    + TestUtil.hexEncode(plaintext.array()));
+                    + Hex.encode(plaintext.array()));
           }
         }
       }
@@ -428,8 +427,8 @@
 
   @Test
   public void testRandomAccessAesGcm() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int segmentSize = 503;
     int plaintextSize = 7654;
@@ -439,8 +438,8 @@
 
   @Test
   public void testRandomAccessAesCtrHmac() throws Exception {
-    byte[] ikm = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-    byte[] associatedData = TestUtil.hexDecode("aabbccddeeff");
+    byte[] ikm = Hex.decode("000102030405060708090a0b0c0d0e0f");
+    byte[] associatedData = Hex.decode("aabbccddeeff");
     int keySize = 16;
     int tagSize = 12;
     int segmentSize = 479;
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/SubtleUtilTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/SubtleUtilTest.java
index 20ddad9..9ce8835 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/SubtleUtilTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/SubtleUtilTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.subtle.Enums.HashType;
+import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import org.junit.Test;
@@ -79,4 +80,92 @@
     ByteBuffer buffer = ByteBuffer.allocate(4);
     assertThrows(GeneralSecurityException.class, () -> SubtleUtil.putAsUnsigedInt(buffer, -1));
   }
+
+  @Test
+  public void bytes2Integer() throws Exception {
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0x00}))
+        .isEqualTo(BigInteger.ZERO);
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0x01}))
+        .isEqualTo(BigInteger.ONE);
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0x7F}))
+        .isEqualTo(BigInteger.valueOf(127));
+    // The input should be interpreted as an unsigned integers. So 0x80 is 128.
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0x80}))
+        .isEqualTo(BigInteger.valueOf(128));
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0xFF}))
+        .isEqualTo(BigInteger.valueOf(255));
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0x01, (byte) 0x00}))
+        .isEqualTo(BigInteger.valueOf(256));
+    assertThat(SubtleUtil.bytes2Integer(new byte[] {(byte) 0x01, (byte) 0x02}))
+        .isEqualTo(BigInteger.valueOf(258));
+    // leading zeros are ignored
+    assertThat(SubtleUtil.bytes2Integer(
+                   new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02}))
+        .isEqualTo(BigInteger.valueOf(258));
+    // the empty array is decoded to 0.
+    assertThat(
+        SubtleUtil.bytes2Integer(new byte[] {})).isEqualTo(BigInteger.ZERO);
+  }
+
+  @Test
+  public void integer2Bytes_success() throws Exception {
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.ZERO, /*intendedLength=*/ 0))
+        .isEqualTo(new byte[] {});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.ZERO, /*intendedLength=*/ 1))
+        .isEqualTo(new byte[] {(byte) 0x00});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.ZERO, /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.ONE, /*intendedLength=*/ 1))
+        .isEqualTo(new byte[] {(byte) 0x01});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.ONE, /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x01});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(127), /*intendedLength=*/ 1))
+        .isEqualTo(new byte[] {(byte) 0x7F});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(127), /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x7F});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(127), /*intendedLength=*/ 3))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x7F});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(128), /*intendedLength=*/ 1))
+        .isEqualTo(new byte[] {(byte) 0x80});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(128), /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x80});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(128), /*intendedLength=*/ 3))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x80});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(255), /*intendedLength=*/ 1))
+        .isEqualTo(new byte[] {(byte) 0xFF});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(255), /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0xFF});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(255), /*intendedLength=*/ 3))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0xFF});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(256), /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x00});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(258), /*intendedLength=*/ 2))
+        .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+    assertThat(SubtleUtil.integer2Bytes(BigInteger.valueOf(258), /*intendedLength=*/ 4))
+        .isEqualTo(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02});
+  }
+
+  @Test
+  public void integer2Bytes_failWhenIntegerIsNegative() throws Exception {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> SubtleUtil.integer2Bytes(BigInteger.valueOf(-1), /*intendedLength=*/ 2));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> SubtleUtil.integer2Bytes(BigInteger.valueOf(-42), /*intendedLength=*/ 2));
+  }
+
+  @Test
+  public void integer2Bytes_failWhenIntegerIsLargerThanIntendedLength() throws Exception {
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> SubtleUtil.integer2Bytes(BigInteger.ONE, /* intendedLength= */ 0));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> SubtleUtil.integer2Bytes(BigInteger.valueOf(256), /*intendedLength=*/ 1));
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> SubtleUtil.integer2Bytes(BigInteger.valueOf(256 * 256), /*intendedLength=*/ 2));
+  }
+
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/X25519Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/X25519Test.java
index 68ca06a..1d4c6cb 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/X25519Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/X25519Test.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
-import com.google.crypto.tink.testing.TestUtil;
 import com.google.crypto.tink.testing.WycheproofTestUtil;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
@@ -39,15 +38,13 @@
     k[0] = 9;
     byte[] prevK = k;
     k = X25519.computeSharedSecret(k, prevK);
-    assertEquals(
-        "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079", TestUtil.hexEncode(k));
+    assertEquals("422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079", Hex.encode(k));
     for (int i = 0; i < 999; i++) {
       byte[] tmp = k;
       k = X25519.computeSharedSecret(k, prevK);
       prevK = tmp;
     }
-    assertEquals(
-        "684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51", TestUtil.hexEncode(k));
+    assertEquals("684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51", Hex.encode(k));
     // Omitting 1M iteration to limit the test runtime.
   }
 
@@ -58,17 +55,15 @@
   public void testPublicFromPrivateWithRfcTestVectors() throws Exception {
     byte[] out =
         X25519.publicFromPrivate(
-            TestUtil.hexDecode("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"));
+            Hex.decode("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"));
     assertEquals(
-        "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a",
-        TestUtil.hexEncode(out));
+        "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a", Hex.encode(out));
 
     out =
         X25519.publicFromPrivate(
-            TestUtil.hexDecode("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"));
+            Hex.decode("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"));
     assertEquals(
-        "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f",
-        TestUtil.hexEncode(out));
+        "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", Hex.encode(out));
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Poly1305Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Poly1305Test.java
index 657f2d5..00059c8 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Poly1305Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Poly1305Test.java
@@ -229,7 +229,7 @@
     final int samples = 1 << 17;
     for (int i = 0; i < samples; i++) {
       byte[] ct = aead.encrypt(message, aad);
-      String ctHex = TestUtil.hexEncode(ct);
+      String ctHex = Hex.encode(ct);
       assertFalse(ciphertexts.contains(ctHex));
       ciphertexts.add(ctHex);
     }
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Test.java b/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Test.java
index af4e324..05adb3e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Test.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/XChaCha20Test.java
@@ -20,7 +20,6 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import org.junit.Test;
@@ -46,10 +45,10 @@
         assertArrayEquals(
             String.format(
                 "\n\nMessage: %s\nKey: %s\nOutput: %s\nDecrypted Msg: %s\n",
-                TestUtil.hexEncode(expectedInput),
-                TestUtil.hexEncode(key),
-                TestUtil.hexEncode(output),
-                TestUtil.hexEncode(actualInput)),
+                Hex.encode(expectedInput),
+                Hex.encode(key),
+                Hex.encode(output),
+                Hex.encode(actualInput)),
             expectedInput,
             actualInput);
       }
diff --git a/java_src/src/test/java/com/google/crypto/tink/testing/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/testing/BUILD.bazel
index 31bbc90..d88d139 100644
--- a/java_src/src/test/java/com/google/crypto/tink/testing/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/testing/BUILD.bazel
@@ -11,7 +11,7 @@
         "//src/main/java/com/google/crypto/tink/internal:key_type_manager",
         "//src/main/java/com/google/crypto/tink/subtle:random",
         "//src/main/java/com/google/crypto/tink/testing:key_type_manager_test_util",
-        "@com_google_protobuf//:protobuf_javalite",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/testing/FakeKmsClientTest.java b/java_src/src/test/java/com/google/crypto/tink/testing/FakeKmsClientTest.java
index e974b4c..45d40d2 100644
--- a/java_src/src/test/java/com/google/crypto/tink/testing/FakeKmsClientTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/testing/FakeKmsClientTest.java
@@ -59,7 +59,7 @@
             + "QQKUb85xOdhmuqMmvderls5oymgmtSLYKabGAEQARiKqYafByAB";
     FakeKmsClient client = new FakeKmsClient(uri);
     assertThat(client.doesSupport(uri)).isTrue();
-    client.getAead(uri); // No exception
+    Object unused = client.getAead(uri); // No exception
 
     // No other key_uri is accepted, even a valid one.
     String anotherUri =
@@ -79,12 +79,12 @@
             + "ZXNDdHJIbWFjQWVhZEtleRJCEhYSAggQGhBBqhLL7pdFk-FzEYi4lo5CGigSBAgDEBAaIFRMn3OEi"
             + "QQKUb85xOdhmuqMmvderls5oymgmtSLYKabGAEQARiKqYafByAB";
     assertThat(client.doesSupport(uri)).isTrue();
-    client.getAead(uri); // No exception
+    Object unused = client.getAead(uri); // No exception
     String anotherUri =
         "fake-kms://CPeFs9sGEo0BCoABCjh0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5B"
             + "ZXNDdHJIbWFjQWVhZEtleRJCEhYSAggQGhCE7VadpBOqUEib9Db55aI2GigSBAgDEBAaII0DdIzGe"
             + "3r2nXHnGoSRa9GZXGsjZsl719GfJrhtjjVGGAEQARj3hbPbBiAB";
     assertThat(client.doesSupport(anotherUri)).isTrue();
-    client.getAead(anotherUri); // No exception
+    unused = client.getAead(anotherUri); // No exception
   }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/tinkkey/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/tinkkey/BUILD.bazel
index ac2bff3..37a3aa3 100644
--- a/java_src/src/test/java/com/google/crypto/tink/tinkkey/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/tinkkey/BUILD.bazel
@@ -33,14 +33,15 @@
         "//src/main/java/com/google/crypto/tink:key_templates",
         "//src/main/java/com/google/crypto/tink:registry",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:aes_eax_parameters",
         "//src/main/java/com/google/crypto/tink/signature:ed25519_private_key_manager",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_access",
         "//src/main/java/com/google/crypto/tink/tinkkey:key_handle",
         "//src/main/java/com/google/crypto/tink/tinkkey:secret_key_access",
         "//src/main/java/com/google/crypto/tink/tinkkey:tink_key",
         "//src/main/java/com/google/crypto/tink/tinkkey/internal:proto_key",
-        "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_google_errorprone_error_prone_annotations",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/java_src/src/test/java/com/google/crypto/tink/tinkkey/KeyHandleTest.java b/java_src/src/test/java/com/google/crypto/tink/tinkkey/KeyHandleTest.java
index 3dc1e75..55ac950 100644
--- a/java_src/src/test/java/com/google/crypto/tink/tinkkey/KeyHandleTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/tinkkey/KeyHandleTest.java
@@ -24,8 +24,8 @@
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.Registry;
 import com.google.crypto.tink.aead.AesEaxKeyManager;
+import com.google.crypto.tink.aead.AesEaxParameters;
 import com.google.crypto.tink.proto.AesEaxKey;
-import com.google.crypto.tink.proto.AesEaxKeyFormat;
 import com.google.crypto.tink.proto.KeyData;
 import com.google.crypto.tink.signature.Ed25519PrivateKeyManager;
 import com.google.crypto.tink.tinkkey.internal.ProtoKey;
@@ -159,19 +159,20 @@
     expect.that(protoKey.getOutputPrefixType()).isEqualTo(KeyTemplate.OutputPrefixType.TINK);
     expect.that(protoKey.hasSecret()).isTrue();
     KeyData keyData = protoKey.getProtoKey();
-    expect.that(keyData.getTypeUrl()).isEqualTo(template.getTypeUrl());
-    AesEaxKeyFormat aesEaxKeyFormat =
-        AesEaxKeyFormat.parseFrom(template.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+    expect.that(keyData.getTypeUrl()).isEqualTo("type.googleapis.com/google.crypto.tink.AesEaxKey");
+
+    AesEaxParameters parameters = (AesEaxParameters) template.toParameters();
+
     AesEaxKey aesEaxKey =
         AesEaxKey.parseFrom(keyData.getValue(), ExtensionRegistryLite.getEmptyRegistry());
-    expect.that(aesEaxKey.getKeyValue().size()).isEqualTo(aesEaxKeyFormat.getKeySize());
+    expect.that(aesEaxKey.getKeyValue().size()).isEqualTo(parameters.getKeySizeBytes());
   }
 
   @Test
   public void generateNew_compareWith_createFromKeyViaProtoKey_shouldBeEqual() throws Exception {
     KeyTemplate template = KeyTemplates.get("AES128_EAX");
     KeyData keyData = Registry.newKeyData(template);
-    ProtoKey protoKey = new ProtoKey(keyData, template.getOutputPrefixType());
+    ProtoKey protoKey = new ProtoKey(keyData, KeyTemplate.OutputPrefixType.TINK);
 
     KeyHandle handle1 = KeyHandle.generateNew(template);
     KeyHandle handle2 = KeyHandle.createFromKey(protoKey, SecretKeyAccess.insecureSecretAccess());
@@ -275,7 +276,7 @@
 
     KeyTemplate returnedKeyTemplate = keyHandle.getKeyTemplate();
 
-    assertThat(returnedKeyTemplate.getValue()).isEqualTo(keyTemplate.getValue());
+    assertThat(returnedKeyTemplate.toParameters()).isEqualTo(keyTemplate.toParameters());
   }
 
   @Test
diff --git a/java_src/src/test/java/com/google/crypto/tink/util/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/util/BUILD.bazel
index 6a764ee..3c60216 100644
--- a/java_src/src/test/java/com/google/crypto/tink/util/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/util/BUILD.bazel
@@ -7,6 +7,7 @@
     deps = [
         "//src/main/java/com/google/crypto/tink/util:keys_downloader",
         "@maven//:com_google_api_client_google_api_client",
+        "@maven//:com_google_errorprone_error_prone_annotations",
         "@maven//:com_google_http_client_google_http_client",
         "@maven//:junit_junit",
     ],
@@ -34,3 +35,15 @@
         "@maven//:junit_junit",
     ],
 )
+
+java_test(
+    name = "SecretBigIntegerTest",
+    size = "small",
+    srcs = ["SecretBigIntegerTest.java"],
+    deps = [
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink/util:secret_big_integer",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/java_src/src/test/java/com/google/crypto/tink/util/KeysDownloaderTest.java b/java_src/src/test/java/com/google/crypto/tink/util/KeysDownloaderTest.java
index beedd45..987a5c5 100644
--- a/java_src/src/test/java/com/google/crypto/tink/util/KeysDownloaderTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/util/KeysDownloaderTest.java
@@ -26,6 +26,7 @@
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpRequest;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -68,7 +69,7 @@
     httpTransportGetCount = new AtomicInteger(0);
     currentTimeInMillis = INITIAL_CURRENT_TIME_IN_MILLIS;
     executorIsAcceptingRunnables = true;
-    TestKeysDownloader.sTestInstance = this;
+    TestKeysDownloader.testInstance = this;
   }
 
   @After
@@ -303,7 +304,7 @@
   public void shouldPerformRefreshAfterExecutorTransientFailure() throws Exception {
     KeysDownloader instance = newInstanceForTests();
     httpResponseBuilder = new HttpResponseBuilder().setContent("keys1");
-    instance.download();
+    Object unused = instance.download();
     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
     // Executor temporarily full, rejecting new Runnable instances
     executorIsAcceptingRunnables = false;
@@ -449,7 +450,7 @@
   }
 
   private static class TestKeysDownloader extends KeysDownloader {
-    private static KeysDownloaderTest sTestInstance;
+    private static KeysDownloaderTest testInstance;
 
     TestKeysDownloader(Executor backgroundExecutor, HttpTransport httpTransport, String keysUrl) {
       super(backgroundExecutor, httpTransport, keysUrl);
@@ -457,7 +458,7 @@
 
     @Override
     long getCurrentTimeInMillis() {
-      return sTestInstance.currentTimeInMillis;
+      return testInstance.currentTimeInMillis;
     }
   }
 
@@ -467,26 +468,31 @@
     private Long ageInSeconds;
     private int statusCode = HttpStatusCodes.STATUS_CODE_OK;
 
+    @CanIgnoreReturnValue
     public HttpResponseBuilder setStatusCode(int statusCode) {
       this.statusCode = statusCode;
       return this;
     }
 
+    @CanIgnoreReturnValue
     public HttpResponseBuilder setContent(String content) {
       this.content = content;
       return this;
     }
 
+    @CanIgnoreReturnValue
     public HttpResponseBuilder setCacheControlWithMaxAgeInSeconds(Long maxAgeInSeconds) {
       this.maxAgeInSeconds = maxAgeInSeconds;
       return this;
     }
 
+    @CanIgnoreReturnValue
     public HttpResponseBuilder clearCacheControl() {
       this.maxAgeInSeconds = null;
       return this;
     }
 
+    @CanIgnoreReturnValue
     public HttpResponseBuilder setAgeInSeconds(Long ageInSeconds) {
       this.ageInSeconds = ageInSeconds;
       return this;
diff --git a/java_src/src/test/java/com/google/crypto/tink/util/SecretBigIntegerTest.java b/java_src/src/test/java/com/google/crypto/tink/util/SecretBigIntegerTest.java
new file mode 100644
index 0000000..81cc6c6
--- /dev/null
+++ b/java_src/src/test/java/com/google/crypto/tink/util/SecretBigIntegerTest.java
@@ -0,0 +1,67 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import java.math.BigInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link SecretBigInteger}. */
+@RunWith(JUnit4.class)
+public final class SecretBigIntegerTest {
+  @Test
+  public void fromBigIntegerToBigInteger_sameValue() throws Exception {
+    BigInteger value = new BigInteger("1234567");
+    SecretBigInteger secretValue =
+        SecretBigInteger.fromBigInteger(value, InsecureSecretKeyAccess.get());
+    assertThat(secretValue.getBigInteger(InsecureSecretKeyAccess.get())).isEqualTo(value);
+  }
+
+  @Test
+  public void fromBigIntegerWithoutSecretAccess_throws() throws Exception {
+    BigInteger value = new BigInteger("1234567");
+    assertThrows(NullPointerException.class, () -> SecretBigInteger.fromBigInteger(value, null));
+  }
+
+  @Test
+  public void getBigIntegerWithoutSecretAccess_throws() throws Exception {
+    SecretBigInteger secretValue =
+        SecretBigInteger.fromBigInteger(new BigInteger("1234567"), InsecureSecretKeyAccess.get());
+    assertThrows(NullPointerException.class, () -> secretValue.getBigInteger(null));
+  }
+
+  @Test
+  public void equalsSecretBigInteger() throws Exception {
+    SecretBigInteger value =
+        SecretBigInteger.fromBigInteger(new BigInteger("1234567"), InsecureSecretKeyAccess.get());
+    SecretBigInteger sameValue =
+        SecretBigInteger.fromBigInteger(new BigInteger("1234567"), InsecureSecretKeyAccess.get());
+    SecretBigInteger otherValue =
+        SecretBigInteger.fromBigInteger(new BigInteger("1234568"), InsecureSecretKeyAccess.get());
+    SecretBigInteger otherValueWithDifferentLength =
+        SecretBigInteger.fromBigInteger(new BigInteger("123456789"), InsecureSecretKeyAccess.get());
+
+    assertThat(value.equalsSecretBigInteger(sameValue)).isTrue();
+    assertThat(value.equalsSecretBigInteger(otherValue)).isFalse();
+    assertThat(value.equalsSecretBigInteger(otherValueWithDifferentLength)).isFalse();
+  }
+}
diff --git a/java_src/testdata/README.md b/java_src/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/java_src/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/java_src/testdata/aws/README.md b/java_src/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/java_src/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/java_src/testdata/gcp/README.md b/java_src/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/java_src/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/java_src/tink_java_deps.bzl b/java_src/tink_java_deps.bzl
index 1345ec6..0bffbd3 100644
--- a/java_src/tink_java_deps.bzl
+++ b/java_src/tink_java_deps.bzl
@@ -3,26 +3,26 @@
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
 
 TINK_MAVEN_ARTIFACTS = [
+    "com.google.protobuf:protobuf-java:3.19.6",
+    "com.google.protobuf:protobuf-javalite:3.19.6",
     "com.amazonaws:aws-java-sdk-core:1.12.182",
     "com.amazonaws:aws-java-sdk-kms:1.12.182",
-    "androidx.annotation:annotation:1.3.0",
+    "androidx.annotation:annotation:1.5.0",
     "com.google.auto:auto-common:1.2.1",
     "com.google.auto.service:auto-service:1.0.1",
     "com.google.auto.service:auto-service-annotations:1.0.1",
-    "com.google.api-client:google-api-client:1.33.2",
-    "com.google.apis:google-api-services-cloudkms:v1-rev108-1.25.0",
+    "com.google.api-client:google-api-client:2.2.0",
+    "com.google.apis:google-api-services-cloudkms:v1-rev20221107-2.0.0",
     "com.google.auth:google-auth-library-oauth2-http:1.5.3",
-    "com.google.code.findbugs:jsr305:3.0.1",
-    "com.google.code.gson:gson:2.8.9",
-    "com.google.errorprone:error_prone_annotations:2.10.0",
-    "com.google.http-client:google-http-client:1.39.0",
-    "com.google.http-client:google-http-client-gson:1.39.0",
-    "com.google.oauth-client:google-oauth-client:1.30.1",
+    "com.google.code.findbugs:jsr305:3.0.2",
+    "com.google.code.gson:gson:2.10.1",
+    "com.google.errorprone:error_prone_annotations:2.18.0",
+    "com.google.http-client:google-http-client:1.43.1",
+    "com.google.http-client:google-http-client-gson:1.43.1",
+    "com.google.oauth-client:google-oauth-client:1.34.1",
     "com.google.truth:truth:0.44",
-    "joda-time:joda-time:2.10.3",
-    "junit:junit:4.13",
-    "org.conscrypt:conscrypt-openjdk-uber:2.4.0",
-    "org.mockito:mockito-core:2.23.0",
+    "junit:junit:4.13.2",
+    "org.conscrypt:conscrypt-openjdk-uber:2.5.2",
     "org.ow2.asm:asm:7.0",
     "org.ow2.asm:asm-commons:7.0",
     "org.pantsbuild:jarjar:1.7.2",
@@ -62,27 +62,12 @@
     #   * @com_google_protobuf//:java_toolchain
     # This statement defines the @com_google_protobuf repo.
     if not native.existing_rule("com_google_protobuf"):
-        # Release from 2021-06-08.
+        # Release X.21.9 from 2022-10-26.
         http_archive(
             name = "com_google_protobuf",
-            strip_prefix = "protobuf-3.19.3",
-            urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.3.zip"],
-            sha256 = "6b6bf5cd8d0cca442745c4c3c9f527c83ad6ef35a405f64db5215889ac779b42",
-        )
-
-    # -------------------------------------------------------------------------
-    # Remote Build Execution (RBE).
-    # -------------------------------------------------------------------------
-    if not native.existing_rule("bazel_toolchains"):
-        # Latest bazel_toolchains package on 2021-10-13.
-        http_archive(
-            name = "bazel_toolchains",
-            sha256 = "179ec02f809e86abf56356d8898c8bd74069f1bd7c56044050c2cd3d79d0e024",
-            strip_prefix = "bazel-toolchains-4.1.0",
-            urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-                "https://github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-            ],
+            strip_prefix = "protobuf-21.9",
+            urls = ["https://github.com/protocolbuffers/protobuf/archive/refs/tags/v21.9.zip"],
+            sha256 = "5babb8571f1cceafe0c18e13ddb3be556e87e12ceea3463d6b0d0064e6cc1ac3",
         )
 
     # -------------------------------------------------------------------------
diff --git a/java_src/tools/build_defs/BUILD.bazel b/java_src/tools/build_defs/BUILD.bazel
deleted file mode 100644
index 1a1b3a3..0000000
--- a/java_src/tools/build_defs/BUILD.bazel
+++ /dev/null
@@ -1,3 +0,0 @@
-package(default_visibility = ["//:__subpackages__"])
-
-licenses(["notice"])
diff --git a/java_src/tools/build_defs/tink_java_rules.bzl b/java_src/tools/build_defs/tink_java_rules.bzl
deleted file mode 100644
index 31578d3..0000000
--- a/java_src/tools/build_defs/tink_java_rules.bzl
+++ /dev/null
@@ -1,76 +0,0 @@
-"""Tink rules for java."""
-
-load("//devtools/build_cleaner/skylark:build_defs.bzl", "register_extension_info")
-load("//tools/build_defs/android:rules.bzl", "android_binary", "android_instrumentation_test")
-
-def tink_android_test(name, srcs, deps, shard_count = 1, data = [], min_version = 19):
-    """Creates android_instrumentation_test targets, testing them on multiple devices.
-
-    Args:
-        name: The name of the test created.
-        srcs: The test source of the test.
-        deps: The dependencies.
-        data: Data dependencies.
-        min_version: The minimum version of android which should be tested.
-    """
-    TARGET_DEVICES = {
-        19: "//tools/mobile/devices/android/generic_phone:android_19_x86",
-        21: "//tools/mobile/devices/android/generic_phone:android_21_x86",
-        22: "//tools/mobile/devices/android/generic_phone:android_22_x86",
-        23: "//tools/mobile/devices/android/generic_phone:android_23_x86",
-        24: "//tools/mobile/devices/android/generic_phone:android_24_x86",
-        25: "//tools/mobile/devices/android/generic_phone:android_25_x86",
-        26: "//tools/mobile/devices/android/generic_phone:android_26_x86",
-        27: "//tools/mobile/devices/android/generic_phone:android_27_x86",
-    }
-
-    deps.append("//java/com/google/android/apps/common/testing/testrunner")
-
-    # Some tests require --config=android_java8_libs  which in turn requires enabling multidex.
-    # See go/java8-libs-for-android-faq for details.
-
-    native_multidex_binary = name + "_native_binary"
-    android_binary(
-        srcs = srcs,
-        name = native_multidex_binary,
-        deps = deps,
-        manifest = "//third_party/tink/java_src/src/androidtest:AndroidManifest_native_multidex.xml",
-        testonly = 1,
-        multidex = "native",
-        javacopts = [
-            "-XepOpt:CheckReturnValue:CheckAllConstructors=false",  # b/226969262
-        ],
-    )
-
-    legacy_multidex_binary = name + "_legacy_binary"
-    deps_copy = list(deps)
-    deps_copy.append("//third_party/java/android/android_sdk_linux/extras/android/compatibility/multidex")
-    android_binary(
-        srcs = srcs,
-        name = legacy_multidex_binary,
-        deps = deps_copy,
-        manifest = "//third_party/tink/java_src/src/androidtest:AndroidManifest_legacy_multidex.xml",
-        testonly = 1,
-        multidex = "legacy",
-        javacopts = [
-            "-XepOpt:CheckReturnValue:CheckAllConstructors=false",  # b/226969262
-        ],
-    )
-
-    for version_num, device in TARGET_DEVICES.items():
-        if version_num >= min_version:
-            android_instrumentation_test(
-                name = name + "_" + str(version_num) + "_test",
-                target_device = device,
-                test_app = legacy_multidex_binary if version_num < 21 else native_multidex_binary,
-                data = data,
-                tags = ["manual"],
-                shard_count = shard_count,
-            )
-
-## Tell build_cleaner how to update dependencies in tink_android_test.
-## For a target name foobar, it should use what as deps into foobar_native_binary.
-register_extension_info(
-    extension = tink_android_test,
-    label_regex_for_dep = "{extension_name}_native_binary",
-)
diff --git a/java_src/tools/jar_jar.bzl b/java_src/tools/jar_jar.bzl
index 709e610..939de65 100644
--- a/java_src/tools/jar_jar.bzl
+++ b/java_src/tools/jar_jar.bzl
@@ -36,7 +36,7 @@
     attrs = {
         "input_jar": attr.label(allow_single_file = True),
         "rules": attr.label(allow_single_file = True),
-        "_jarjar": attr.label(executable = True, cfg = "host", default = Label("//tools:jarjar")),
+        "_jarjar": attr.label(executable = True, cfg = "exec", default = Label("//tools:jarjar")),
     },
     outputs = {
         "jar": "%{name}.jar",
diff --git a/java_src/tools/java_single_jar.bzl b/java_src/tools/java_single_jar.bzl
index 80c89b6..6e0ee80 100644
--- a/java_src/tools/java_single_jar.bzl
+++ b/java_src/tools/java_single_jar.bzl
@@ -89,7 +89,7 @@
         ),
         "_singlejar": attr.label(
             default = Label("@bazel_tools//tools/jdk:singlejar"),
-            cfg = "host",
+            cfg = "exec",
             allow_single_file = True,
             executable = True,
         ),
diff --git a/java_src/tools/refaster/README.md b/java_src/tools/refaster/README.md
new file mode 100644
index 0000000..b2ffb91
--- /dev/null
+++ b/java_src/tools/refaster/README.md
@@ -0,0 +1,78 @@
+# Refaster templates for Tink
+
+In this directory, we store [Refaster](https://errorprone.info/%64ocs/refaster)
+templates for usage with Tink. These help users modernize the code and move to
+preferred APIs. They can be used with
+[Error Prone](https://errorprone.info/index), to automatically generated
+patches for your codebase. The templates can be found in
+`java/com/google/tink1_templates/`.
+
+In order to use them on your project, we refer to the [Refaster
+documentation](https://errorprone.info/%64ocs/refaster).
+
+We sometimes test these templates using the script below. This script
+applies the templates to the code in `java/com/google/tinkuser/*.java`
+and compares the result to the code in
+`java/com/google/tinkuser/*.java_expected`
+
+NOTE: We run the following tests manually, not as part of our continuous
+integration tests.
+
+
+```bash
+errorprone_version="2.18.0"
+
+## STEP 0: Switch to a directory so the remainder of the script can be run
+## without modifying the working directory.
+code_path=$(pwd)
+cd $(mktemp -d)
+
+## STEP 1: We get the 3 jar files:
+## tink-{version}-jar, error_prone_refaster-{version}.jar, and error_prone_core-{version}.jar
+## (For good measure, we verify the 3 sha256sums.)
+mkdir jars
+pushd jars
+maven_base="repo1.maven.org/maven2/com/google"
+
+# We download Tink from Head because Tink 1.9 does not have all key templates
+# This URL is found by going to https://oss.sonatype.org/ and browsing.
+tink_jar="tink-HEAD-20230426.095746-3687.jar"
+tink_url="https://oss.sonatype.org/service/local/repositories/snapshots/content/com/google/crypto/tink/tink/HEAD-SNAPSHOT/tink-HEAD-20230426.095746-3687.jar"
+tink_sha256="d88bbb07f02d3c55f9edc2a3450a1d6c8ff59c2cbe00d2d8b5628bdc67c0638f"
+
+refaster_jar="error_prone_refaster-${errorprone_version}.jar"
+refaster_sha256="0cde0a3db5c2f748fae4633ccd8c66a9ba9c5a0f7a380c9104b99372fd0c4959"
+errorprone_jar="error_prone_core-${errorprone_version}-with-dependencies.jar"
+errorprone_sha256="2b3f2d21e7754bece946cf8f7b0e2b2f805c46f58b4839eb302c3d2498a3a55e"
+
+wget "${tink_url}"
+echo "${tink_sha256} ${tink_jar}" | sha256sum -c
+
+wget "https://${maven_base}/errorprone/error_prone_refaster/${errorprone_version}/${refaster_jar}"
+echo "${refaster_sha256} ${refaster_jar}" | sha256sum -c
+
+wget "https://${maven_base}/errorprone/error_prone_core/${errorprone_version}/${errorprone_jar}"
+echo "${errorprone_sha256} ${errorprone_jar}" | sha256sum -c
+
+popd
+
+## STEP 2: Use the current file in tink1to2 to create a "tinkrule.refaster":
+## First we delete the results of previous copy operations.
+rm -rf refaster/ ; cp -r "${code_path}" .
+
+javac -cp "jars/${tink_jar}:jars/${refaster_jar}" \
+  "-Xplugin:RefasterRuleCompiler --out ${PWD}/tinkrule.refaster" \
+  refaster/java/com/google/tink1_templates/AllChanges.java
+
+## STEP 3: Use error prone to create a patch:
+javac -cp "jars/${tink_jar}:jars/${errorprone_jar}" \
+  -XDcompilePolicy=byfile  \
+  -processorpath "jars/${errorprone_jar}" \
+  "-Xplugin:ErrorProne -XepPatchChecks:refaster:${PWD}/tinkrule.refaster -XepPatchLocation:${PWD}" \
+  refaster/java/com/google/tinkuser/TinkUser.java
+
+patch refaster/java/com/google/tinkuser/TinkUser.java error-prone.patch
+
+diff refaster/java/com/google/tinkuser/TinkUser.java \
+  refaster/java/com/google/tinkuser/TinkUser.java_expected
+```
diff --git a/java_src/tools/refaster/java/com/google/tink1_templates/AllChanges.java b/java_src/tools/refaster/java/com/google/tink1_templates/AllChanges.java
new file mode 100644
index 0000000..0e23cc9
--- /dev/null
+++ b/java_src/tools/refaster/java/com/google/tink1_templates/AllChanges.java
@@ -0,0 +1,343 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.tink1to2;
+
+import com.google.crypto.tink.BinaryKeysetReader;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KeysetReader;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
+import com.google.crypto.tink.mac.MacKeyTemplates;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import com.google.crypto.tink.streamingaead.PredefinedStreamingAeadParameters;
+import com.google.crypto.tink.streamingaead.StreamingAeadKeyTemplates;
+import com.google.errorprone.refaster.annotation.AfterTemplate;
+import com.google.errorprone.refaster.annotation.BeforeTemplate;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+// We keep all changes in one file due to https://github.com/google/error-prone/issues/552
+final class AllChanges {
+  class CleanupKeysetHandleReaderNoSecret {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.readNoSecret(b);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return TinkProtoKeysetFormat.parseKeysetWithoutSecret(b);
+    }
+  }
+
+  /**
+   * If users first create a binary keyset reader from a byte[], then call readNoSecret, we can
+   * simply call the new function directly without any parsing.
+   */
+  class CleanupKeysetHandleReadNoSecretReaderWithBinaryReader {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] bytes) throws GeneralSecurityException, IOException {
+      return KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(bytes));
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] bytes) throws GeneralSecurityException, IOException {
+      return TinkProtoKeysetFormat.parseKeysetWithoutSecret(bytes);
+    }
+  }
+  /** For any other reader, we can always just call read. */
+  class CleanupKeysetHandleReadNoSecretReader {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(KeysetReader reader)
+        throws GeneralSecurityException, IOException {
+      return KeysetHandle.readNoSecret(reader);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(KeysetReader reader)
+        throws GeneralSecurityException, IOException {
+      return TinkProtoKeysetFormat.parseKeysetWithoutSecret(reader.read().toByteArray());
+    }
+  }
+
+  class HMAC_SHA256_128BITTAG {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    }
+  }
+
+  class HMAC_SHA256_256BITTAG {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_256BITTAG);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    }
+  }
+
+  class HMAC_SHA512_256BITTAG {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA512_256BITTAG);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA512_256BITTAG);
+    }
+  }
+
+  class HMAC_SHA512_512BITTAG {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA512_512BITTAG);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA512_512BITTAG);
+    }
+  }
+
+  class AES_CMAC {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(MacKeyTemplates.AES_CMAC);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedMacParameters.AES_CMAC);
+    }
+  }
+
+  class AES128_GCM {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.AES128_GCM);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    }
+  }
+
+  class AES256_GCM {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.AES256_GCM);
+    }
+  }
+
+  class AES128_EAX {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.AES128_EAX);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.AES128_EAX);
+    }
+  }
+
+  class AES256_EAX {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.AES256_EAX);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.AES256_EAX);
+    }
+  }
+
+  class AES128_CTR_HMAC_SHA256 {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
+    }
+  }
+
+  class AES256_CTR_HMAC_SHA256 {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.AES256_CTR_HMAC_SHA256);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.AES256_CTR_HMAC_SHA256);
+    }
+  }
+
+  class CHACHA20_POLY1305 {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.CHACHA20_POLY1305);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.CHACHA20_POLY1305);
+    }
+  }
+
+  class XCHACHA20_POLY1305 {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(AeadKeyTemplates.XCHACHA20_POLY1305);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedAeadParameters.XCHACHA20_POLY1305);
+    }
+  }
+
+  class AES256_SIV {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(DeterministicAeadKeyTemplates.AES256_SIV);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedDeterministicAeadParameters.AES256_SIV);
+    }
+  }
+
+  class AES128_CTR_HMAC_SHA256_4KB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_4KB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_4KB);
+    }
+  }
+
+  class AES128_CTR_HMAC_SHA256_1MB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_1MB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_1MB);
+    }
+  }
+
+  class AES256_CTR_HMAC_SHA256_4KB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_CTR_HMAC_SHA256_4KB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_4KB);
+    }
+  }
+
+  class AES256_CTR_HMAC_SHA256_1MB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_CTR_HMAC_SHA256_1MB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_1MB);
+    }
+  }
+
+  class AES128_GCM_HKDF_4KB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_GCM_HKDF_4KB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_GCM_HKDF_4KB);
+    }
+  }
+
+  class AES128_GCM_HKDF_1MB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_GCM_HKDF_1MB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_GCM_HKDF_1MB);
+    }
+  }
+
+  class AES256_GCM_HKDF_4KB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_GCM_HKDF_4KB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_GCM_HKDF_4KB);
+    }
+  }
+
+  class AES256_GCM_HKDF_1MB {
+    @BeforeTemplate
+    public KeysetHandle beforeTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_GCM_HKDF_1MB);
+    }
+
+    @AfterTemplate
+    public KeysetHandle afterTemplate(byte[] b) throws GeneralSecurityException {
+      return KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_GCM_HKDF_1MB);
+    }
+  }
+}
diff --git a/java_src/tools/refaster/java/com/google/tinkuser/TinkUser.java b/java_src/tools/refaster/java/com/google/tinkuser/TinkUser.java
new file mode 100644
index 0000000..67b2078
--- /dev/null
+++ b/java_src/tools/refaster/java/com/google/tinkuser/TinkUser.java
@@ -0,0 +1,75 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.tinkuser;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.BinaryKeysetReader;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KeysetReader;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.mac.MacKeyTemplates;
+import com.google.crypto.tink.streamingaead.StreamingAeadKeyTemplates;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/** Example user code */
+public final class TinkUser {
+  public Aead useReadNoSecret(byte[] b) throws GeneralSecurityException {
+    return KeysetHandle.readNoSecret(b).getPrimitive(Aead.class);
+  }
+  public Aead useBinaryReader(byte[] b) throws GeneralSecurityException, IOException {
+    return KeysetHandle.readNoSecret(BinaryKeysetReader.withBytes(b)).getPrimitive(Aead.class);
+  }
+  public Aead useAnyReader(KeysetReader r) throws GeneralSecurityException, IOException {
+    return KeysetHandle.readNoSecret(r).getPrimitive(Aead.class);
+  }
+
+  public void macKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_128BITTAG);
+    Object b = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA256_256BITTAG);
+    Object c = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA512_256BITTAG);
+    Object d = KeysetHandle.generateNew(MacKeyTemplates.HMAC_SHA512_512BITTAG);
+    Object e = KeysetHandle.generateNew(MacKeyTemplates.AES_CMAC);
+  }
+
+  public void aeadKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(AeadKeyTemplates.AES128_GCM);
+    Object b = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);
+    Object c = KeysetHandle.generateNew(AeadKeyTemplates.AES128_EAX);
+    Object d = KeysetHandle.generateNew(AeadKeyTemplates.AES256_EAX);
+    Object e = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
+    Object f = KeysetHandle.generateNew(AeadKeyTemplates.AES256_CTR_HMAC_SHA256);
+    Object g = KeysetHandle.generateNew(AeadKeyTemplates.CHACHA20_POLY1305);
+    Object h = KeysetHandle.generateNew(AeadKeyTemplates.XCHACHA20_POLY1305);
+  }
+
+  public void deterministicAeadKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(DeterministicAeadKeyTemplates.AES256_SIV);
+  }
+
+  public void streamingAeadKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_4KB);
+    Object b = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_CTR_HMAC_SHA256_1MB);
+    Object c = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_CTR_HMAC_SHA256_4KB);
+    Object d = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_CTR_HMAC_SHA256_1MB);
+    Object e = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_GCM_HKDF_4KB);
+    Object f = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES128_GCM_HKDF_1MB);
+    Object g = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_GCM_HKDF_4KB);
+    Object h = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_GCM_HKDF_1MB);
+  }
+}
diff --git a/java_src/tools/refaster/java/com/google/tinkuser/TinkUser.java_expected b/java_src/tools/refaster/java/com/google/tinkuser/TinkUser.java_expected
new file mode 100644
index 0000000..df18296
--- /dev/null
+++ b/java_src/tools/refaster/java/com/google/tinkuser/TinkUser.java_expected
@@ -0,0 +1,80 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.tinkuser;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.BinaryKeysetReader;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KeysetReader;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.daead.PredefinedDeterministicAeadParameters;
+import com.google.crypto.tink.mac.MacKeyTemplates;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import com.google.crypto.tink.streamingaead.PredefinedStreamingAeadParameters;
+import com.google.crypto.tink.streamingaead.StreamingAeadKeyTemplates;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/** Example user code */
+public final class TinkUser {
+  public Aead useReadNoSecret(byte[] b) throws GeneralSecurityException {
+    return TinkProtoKeysetFormat.parseKeysetWithoutSecret(b).getPrimitive(Aead.class);
+  }
+  public Aead useBinaryReader(byte[] b) throws GeneralSecurityException, IOException {
+    return TinkProtoKeysetFormat.parseKeysetWithoutSecret(b).getPrimitive(Aead.class);
+  }
+  public Aead useAnyReader(KeysetReader r) throws GeneralSecurityException, IOException {
+    return TinkProtoKeysetFormat.parseKeysetWithoutSecret(r.read().toByteArray()).getPrimitive(Aead.class);
+  }
+
+  public void macKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    Object b = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    Object c = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA512_256BITTAG);
+    Object d = KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA512_512BITTAG);
+    Object e = KeysetHandle.generateNew(PredefinedMacParameters.AES_CMAC);
+  }
+
+  public void aeadKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Object b = KeysetHandle.generateNew(PredefinedAeadParameters.AES256_GCM);
+    Object c = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_EAX);
+    Object d = KeysetHandle.generateNew(PredefinedAeadParameters.AES256_EAX);
+    Object e = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_CTR_HMAC_SHA256);
+    Object f = KeysetHandle.generateNew(PredefinedAeadParameters.AES256_CTR_HMAC_SHA256);
+    Object g = KeysetHandle.generateNew(PredefinedAeadParameters.CHACHA20_POLY1305);
+    Object h = KeysetHandle.generateNew(PredefinedAeadParameters.XCHACHA20_POLY1305);
+  }
+
+  public void deterministicAeadKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(PredefinedDeterministicAeadParameters.AES256_SIV);
+  }
+
+  public void streamingAeadKeyTemplateUser() throws Exception {
+    Object a = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_4KB);
+    Object b = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_CTR_HMAC_SHA256_1MB);
+    Object c = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_4KB);
+    Object d = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_CTR_HMAC_SHA256_1MB);
+    Object e = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_GCM_HKDF_4KB);
+    Object f = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES128_GCM_HKDF_1MB);
+    Object g = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_GCM_HKDF_4KB);
+    Object h = KeysetHandle.generateNew(PredefinedStreamingAeadParameters.AES256_GCM_HKDF_1MB);
+  }
+}
diff --git a/javascript/.bazelignore b/javascript/.bazelignore
deleted file mode 100644
index 3c3629e..0000000
--- a/javascript/.bazelignore
+++ /dev/null
@@ -1 +0,0 @@
-node_modules
diff --git a/javascript/.bazelversion b/javascript/.bazelversion
deleted file mode 100644
index ac14c3d..0000000
--- a/javascript/.bazelversion
+++ /dev/null
@@ -1 +0,0 @@
-5.1.1
diff --git a/javascript/BUILD.bazel b/javascript/BUILD.bazel
deleted file mode 100644
index f415e13..0000000
--- a/javascript/BUILD.bazel
+++ /dev/null
@@ -1,62 +0,0 @@
-load("@npm//@bazel/concatjs:index.bzl", "karma_web_test_suite")
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-load("@npm//@angular/bazel:index.bzl", "ng_package")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "javascript",
-    srcs = [
-        "index.ts",
-        "keyset_handle.ts",
-    ],
-    module_name = "tink-crypto",
-    deps = [
-        "//aead",
-        "//aead/subtle",
-        "//binary",
-        "//binary/insecure",
-        "//hybrid",
-        "//internal",
-        "//mac",
-        "//mac/subtle",
-        "//signature",
-        "//signature/subtle",
-        "//testing",
-    ],
-)
-
-karma_web_test_suite(
-    name = "unit_tests",
-    browsers = [
-        "@io_bazel_rules_webtesting//browsers:chromium-local",
-        "@io_bazel_rules_webtesting//browsers:firefox-local",
-    ],
-    tags = [
-        "native",
-        "no_rbe",
-    ],
-    deps = [
-        "//aead:aead_tests",
-        "//hybrid:hybrid_tests",
-        "//internal:internal_tests",
-        "//signature:signature_tests",
-        "//subtle:subtle_tests",
-    ],
-)
-
-exports_files(
-    ["tsconfig.json"],
-    visibility = ["//:__subpackages__"],
-)
-
-ng_package(
-    name = "tink-crypto",
-    package_name = "tink-crypto",
-    srcs = [
-        "package.json",
-        "README.md",
-    ],
-    entry_point = ":index.ts",
-    deps = [":javascript"],
-)
diff --git a/javascript/JAVASCRIPT-HACKING.md b/javascript/JAVASCRIPT-HACKING.md
deleted file mode 100644
index 2fea373..0000000
--- a/javascript/JAVASCRIPT-HACKING.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Hacking Tink for TypeScript/JavaScript
-
-## Building Tink
-
-*   Install [Bazel](https://docs.bazel.build/versions/master/install.html).
-    Bazel is the exclusive build system for Tink for TypeScript/JavaScript
-    (although you don't need it to merely depend on the `tink-crypto` package
-    from npm, only to build from source).
-
-*   Check out source code and run `bazel build`. It will download dependencies
-    and do all the other npm-related stuff automatically.
-
-## Updating Dependencies
-
-*   Dependency versions are specified in `package.json`.
-*   After changing dependencies in `package.json`, run `yarn` (or, if you don't
-    have the Yarn CLI installed globally, `npx yarn`) to update `yarn.lock`.
-
-## Protocol Buffers
-
-We're currently using a pretty ad-hoc method of depending on protos from
-TypeScript code; it involves feeding the output from one build toolchain into a
-different one. The implementation is in `internal/ts_library_from_closure.bzl`.
-Note that it's also necessary to add any new protos to `internal/BUILD.bazel`.
-
-## Code Splitting
-
-There's no support for code splitting right now; `index.ts` is the sole entry
-point and everything has to be directly or indirectly exported from there.
diff --git a/javascript/README.md b/javascript/README.md
deleted file mode 100644
index 74035b9..0000000
--- a/javascript/README.md
+++ /dev/null
@@ -1,169 +0,0 @@
-# Tink
-
-*A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.*
-
-https://developers.google.com/tink
-
-**`Ubuntu`**                        | **`macOS`**
------------------------------------ | ---------------------------------
-[![Kokoro Ubuntu][ubuntu_badge]](#) | [![Kokoro macOS][macos_badge]](#)
-
-[ubuntu_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-ubuntu.png
-[macos_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-macos.png
-
-## Index
-
-1.  [Introduction](#introduction)
-2.  [Current status](#current-status)
-3.  [Getting started](#getting-started)
-4.  [Learn more](#learn-more)
-5.  [Contact and mailing list](#contact-and-mailing-list)
-6.  [Maintainers](#maintainers)
-
-## Introduction
-
-Using crypto in your application [shouldn't have to][devs_are_users_too_slides]
-feel like juggling chainsaws in the dark. Tink is a crypto library written by a
-group of cryptographers and security engineers at Google. It was born out of our
-extensive experience working with Google's product teams, [fixing weaknesses in
-implementations](https://github.com/google/wycheproof), and providing simple
-APIs that can be used safely without needing a crypto background.
-
-Tink provides secure APIs that are easy to use correctly and hard(er) to misuse.
-It reduces common crypto pitfalls with user-centered design, careful
-implementation and code reviews, and extensive testing. At Google, Tink is one
-of the standard crypto libraries, and has been deployed in hundreds of products
-and systems.
-
-To get a quick overview of Tink design please take a look at
-[slides][tink_talk_slides] from [a talk about Tink][tink_talk_recording]
-presented at [Real World Crypto 2019](https://rwc.iacr.org/2019/).
-
-[devs_are_users_too_slides]: https://www.usenix.org/sites/default/files/conference/protected-files/hotsec15_slides_green.pdf
-[tink_talk_slides]: docs/Tink-a_cryptographic_library--RealWorldCrypto2019.pdf
-[tink_talk_recording]: https://www.youtube.com/watch?v=pqev9r3rUJs&t=9665
-
-## Current status
-
-[Java/Android](docs/JAVA-HOWTO.md), [C++](docs/CPP-HOWTO.md),
-[Obj-C](docs/OBJC-HOWTO.md), [Go](docs/GOLANG-HOWTO.md), and
-[Python](docs/PYTHON-HOWTO.md) are field tested and ready for production. The
-latest version is [1.7.0](https://github.com/google/tink/releases/tag/v1.7.0),
-released on 2022-08-09.
-
-Javascript/Typescript is in an alpha state and should only be used for testing.
-
-## Getting started
-
-Documentation for the project is located at https://developers.google.com/tink.
-Currently, it details a variety of common usage scenarios and covers the Java
-and Python implementations. The site will be populated with more content over
-time.
-
-Alternatively, you can look at all of the [`examples`] which demonstrate
-performing simple tasks using Tink in a variety of languages.
-
-[`examples`]: https://github.com/google/tink/tree/master/examples
-
-*   Python
-
-```sh
-pip3 install tink
-```
-
-*   Golang
-
-```sh
-go get github.com/google/tink/go/...
-```
-
-*   Java
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>tink</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-*   Android
-
-```
-dependencies {
-  implementation 'com.google.crypto.tink:tink-android:1.7.0'
-}
-```
-
-*   Objective-C/iOS
-
-```sh
-cd /path/to/your/Xcode project/
-pod init
-pod 'Tink', '1.7.0'
-pod install
-```
-
-## Learn more
-
-*   [Java HOW-TO](docs/JAVA-HOWTO.md)
-*   [C++ HOW-TO](docs/CPP-HOWTO.md)
-*   [Obj-C HOW-TO](docs/OBJC-HOWTO.md)
-*   [Go HOW-TO](docs/GOLANG-HOWTO.md)
-*   [Python HOW-TO](docs/PYTHON-HOWTO.md)
-*   [Security and Usability Design Goals](docs/SECURITY-USABILITY.md)
-*   [Supported Crypto Primitives](docs/PRIMITIVES.md)
-*   [Key Management](docs/KEY-MANAGEMENT.md)
-*   [Managing keys with Tinkey](docs/TINKEY.md)
-*   [Known Issues](docs/KNOWN-ISSUES.md)
-
-## Community-driven ports
-
-Out of the box Tink supports a wide range of languages, but it still doesn't
-support every language. Fortunately, some users like Tink so much that they've
-ported it to their favorite languages! Below you can find notable ports.
-
-**WARNING** While we usually review these ports, until further notice, we do not
-maintain them and have no plan to support them in the foreseeable future.
-
-*   [Clojure](https://github.com/perkss/tinklj)
-
-## Contact and mailing list
-
-If you want to contribute, please read [CONTRIBUTING](docs/CONTRIBUTING.md)
-and send us pull requests. You can also report bugs or file feature requests.
-
-If you'd like to talk to the developers or get notified about major product
-updates, you may want to subscribe to our
-[mailing list](https://groups.google.com/forum/#!forum/tink-users).
-
-## Maintainers
-
-Tink is maintained by (A-Z):
-
--   Moreno Ambrosin
--   Taymon Beal
--   Daniel Bleichenbacher
--   William Conner
--   Thai Duong
--   Thomas Holenstein
--   Stefan Kölbl
--   Charles Lee
--   Cindy Lin
--   Fernando Lobato Meeser
--   Atul Luykx
--   Rafael Misoczki
--   Sophie Schmieg
--   Laurent Simon
--   Elizaveta Tretiakova
--   Jürg Wullschleger
-
-Alumni:
-
--   Haris Andrianakis
--   Tanuj Dhir
--   Quan Nguyen
--   Bartosz Przydatek
--   Enzo Puig
--   Veronika Slívová
--   Paula Vidas
diff --git a/javascript/WORKSPACE b/javascript/WORKSPACE
deleted file mode 100644
index 7ce2931..0000000
--- a/javascript/WORKSPACE
+++ /dev/null
@@ -1,42 +0,0 @@
-workspace(
-    name = "tink_javascript",
-    managed_directories = {"@npm": ["node_modules"]},
-)
-
-load("@tink_javascript//:tink_javascript_deps.bzl", "tink_javascript_deps")
-
-# Loads the Tink javascript dependencies.
-tink_javascript_deps()
-
-load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
-
-protobuf_deps()
-
-load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
-
-yarn_install(
-    name = "npm",
-    package_json = "//:package.json",
-    yarn_lock = "//:yarn.lock",
-)
-
-load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories")
-
-web_test_repositories()
-
-load("@io_bazel_rules_webtesting//web/versioned:browsers-0.3.3.bzl", "browser_repositories")
-
-browser_repositories(
-    chromium = True,
-    firefox = True,
-)
-
-load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains")
-
-rules_closure_dependencies()
-
-rules_closure_toolchains()
-
-load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
-
-bazel_skylib_workspace()
diff --git a/javascript/aead/BUILD.bazel b/javascript/aead/BUILD.bazel
deleted file mode 100644
index 39c8b7a..0000000
--- a/javascript/aead/BUILD.bazel
+++ /dev/null
@@ -1,53 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "aead",
-    srcs = [
-        "aead.ts",
-        "aead_config.ts",
-        "aead_key_templates.ts",
-        "aead_wrapper.ts",
-        "aes_ctr_hmac.ts",
-        "aes_ctr_hmac_aead_key_manager.ts",
-        "aes_ctr_hmac_aead_key_templates.ts",
-        "aes_gcm.ts",
-        "aes_gcm_key_manager.ts",
-        "aes_gcm_key_templates.ts",
-        "index.ts",
-        "wrapper.ts",
-    ],
-    module_name = "tink-crypto/aead",
-    deps = [
-        "//aead/internal",
-        "//exception",
-        "//internal",
-        "//internal:proto",
-        "//subtle",
-    ],
-)
-
-ts_library(
-    name = "aead_tests",
-    testonly = True,
-    srcs = [
-        "aead_config_test.ts",
-        "aead_key_templates_test.ts",
-        "aead_wrapper_test.ts",
-        "aes_ctr_hmac_aead_key_manager_test.ts",
-        "aes_ctr_hmac_aead_key_templates_test.ts",
-        "aes_gcm_key_manager_test.ts",
-        "aes_gcm_key_templates_test.ts",
-    ],
-    deps = [
-        ":aead",
-        "//aead/internal",
-        "//exception",
-        "//internal",
-        "//internal:proto",
-        "//subtle",
-        "//testing/internal",
-        "@npm//@types/jasmine",
-    ],
-)
diff --git a/javascript/aead/aead.ts b/javascript/aead/aead.ts
deleted file mode 100644
index e1810f3..0000000
--- a/javascript/aead/aead.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {Aead} from './internal/aead';
diff --git a/javascript/aead/aead_config.ts b/javascript/aead/aead_config.ts
deleted file mode 100644
index 312ece9..0000000
--- a/javascript/aead/aead_config.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadWrapper} from './aead_wrapper';
-import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
-import {AesGcmKeyManager} from './aes_gcm_key_manager';
-
-/**
- * Static methods and constants for registering with the Registry all instances
- * of Aead key types supported in a particular release of Tink.
- *
- * To register all Aead key types from the current Tink release one can do:
- *
- * AeadConfig.register();
- *
- * For more information on creation and usage of Aead instances see AeadFactory.
- *
- * @final
- */
-export class AeadConfig {
-  static PRIMITIVE_NAME: string = 'Aead';
-  static AES_CTR_HMAC_AEAD_TYPE_URL: string;
-  static AES_GCM_TYPE_URL: string;
-
-  /**
-   * Registers key managers for all Aead key types from the current Tink
-   * release.
-   */
-  static register() {
-    // TODO MacConfig.register() should be here.
-    AesGcmKeyManager.register();
-    AesCtrHmacAeadKeyManager.register();
-    AeadWrapper.register();
-  }
-}
-AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL = AesCtrHmacAeadKeyManager.KEY_TYPE;
-AeadConfig.AES_GCM_TYPE_URL = AesGcmKeyManager.KEY_TYPE;
diff --git a/javascript/aead/aead_config_test.ts b/javascript/aead/aead_config_test.ts
deleted file mode 100644
index 6c37248..0000000
--- a/javascript/aead/aead_config_test.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {KeysetHandle} from '../internal/keyset_handle';
-import {PbKeyData, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Random from '../subtle/random';
-
-import {AeadConfig} from './aead_config';
-import {AeadKeyTemplates} from './aead_key_templates';
-import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
-import {AesGcmKeyManager} from './aes_gcm_key_manager';
-import {Aead} from './internal/aead';
-
-describe('aead config test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('constants', function() {
-    expect(AeadConfig.PRIMITIVE_NAME).toBe(PRIMITIVE_NAME);
-
-    expect(AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL)
-        .toBe(AES_CTR_HMAC_AEAD_KEY_TYPE);
-    expect(AeadConfig.AES_GCM_TYPE_URL).toBe(AES_GCM_KEY_TYPE);
-  });
-
-  it('register, corresponding key managers were registered', function() {
-    AeadConfig.register();
-
-    // Test that the corresponding key managers were registered.
-    const aesCtrHmacKeyManager =
-        Registry.getKeyManager(AES_CTR_HMAC_AEAD_KEY_TYPE);
-    expect(aesCtrHmacKeyManager instanceof AesCtrHmacAeadKeyManager).toBe(true);
-
-    const aesGcmKeyManager = Registry.getKeyManager(AES_GCM_KEY_TYPE);
-    expect(aesGcmKeyManager instanceof AesGcmKeyManager).toBe(true);
-
-    // TODO add tests for other key types here, whenever they are available in
-    // Tink.
-  });
-
-  it('register, predefined templates should work', async function() {
-    AeadConfig.register();
-    let templates = [
-      AeadKeyTemplates.aes128Gcm(), AeadKeyTemplates.aes256Gcm(),
-      AeadKeyTemplates.aes128CtrHmacSha256(),
-      AeadKeyTemplates.aes256CtrHmacSha256()
-    ];
-    for (const template of templates) {
-      const keyData = await Registry.newKeyData(template);
-      const keysetHandle = createKeysetHandleFromKeyData(keyData);
-
-      const aead = await keysetHandle.getPrimitive<Aead>(Aead);
-      const plaintext = Random.randBytes(10);
-      const aad = Random.randBytes(8);
-      const ciphertext = await aead.encrypt(plaintext, aad);
-      const decryptedCiphertext = await aead.decrypt(ciphertext, aad);
-
-      expect(decryptedCiphertext).toEqual(plaintext);
-    }
-  });
-});
-
-// Constants used in tests.
-const PRIMITIVE_NAME = 'Aead';
-const AES_CTR_HMAC_AEAD_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey';
-const AES_GCM_KEY_TYPE = 'type.googleapis.com/google.crypto.tink.AesGcmKey';
-
-/**
- * Creates a keyset containing only the key given by keyData and returns it
- * wrapped in a KeysetHandle.
- */
-function createKeysetHandleFromKeyData(keyData: PbKeyData): KeysetHandle {
-  const keyId = 1;
-  const key = new PbKeysetKey()
-                  .setKeyData(keyData)
-                  .setStatus(PbKeyStatusType.ENABLED)
-                  .setKeyId(keyId)
-                  .setOutputPrefixType(PbOutputPrefixType.TINK);
-
-  const keyset = new PbKeyset();
-  keyset.addKey(key);
-  keyset.setPrimaryKeyId(keyId);
-  return new KeysetHandle(keyset);
-}
diff --git a/javascript/aead/aead_key_templates.ts b/javascript/aead/aead_key_templates.ts
deleted file mode 100644
index 4f27b38..0000000
--- a/javascript/aead/aead_key_templates.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbKeyTemplate} from '../internal/proto';
-
-import {AesCtrHmacAeadKeyTemplates} from './aes_ctr_hmac_aead_key_templates';
-import {AesGcmKeyTemplates} from './aes_gcm_key_templates';
-
-/**
- * Pre-generated KeyTemplates for Aead keys.
- *
- * One can use these templates to generate new Keyset with
- * KeysetHandle.generateNew method. To generate a new keyset that contains a
- * single AesCtrHmacAeadKey, one can do:
- *
- * AeadConfig.Register();
- * KeysetHandle handle =
- *     KeysetHandle.generateNew(AeadKeyTemplates.aes128CtrHmacSha256());
- *
- * @final
- */
-export class AeadKeyTemplates {
-  /**
-   * Returns a KeyTemplate that generates new instances of AesCtrHmacAeadKey
-   * with the following parameters:
-   *    AES key size: 16 bytes
-   *    AES IV size: 16 bytes
-   *    HMAC key size: 32 bytes
-   *    HMAC tag size: 16 bytes
-   *    HMAC hash function: SHA256
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes128CtrHmacSha256(): PbKeyTemplate {
-    return AesCtrHmacAeadKeyTemplates.aes128CtrHmacSha256();
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of AesCtrHmacAeadKey
-   * with the following parameters:
-   *    AES key size: 32 bytes
-   *    AES IV size: 16 bytes
-   *    HMAC key size: 32 bytes
-   *    HMAC tag size: 32 bytes
-   *    HMAC hash function: SHA256
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes256CtrHmacSha256(): PbKeyTemplate {
-    return AesCtrHmacAeadKeyTemplates.aes256CtrHmacSha256();
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of AesGcmKey
-   * with the following parameters:
-   *    key size: 16 bytes
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes128Gcm(): PbKeyTemplate {
-    return AesGcmKeyTemplates.aes128Gcm();
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of AesGcmKey
-   * with the following parameters:
-   *    key size: 32 bytes
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes256Gcm(): PbKeyTemplate {
-    return AesGcmKeyTemplates.aes256Gcm();
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of AesGcmKey
-   * with the following parameters:
-   *     key size: 32 bytes
-   *     OutputPrefixType: RAW
-   *
-   */
-  static aes256GcmNoPrefix(): PbKeyTemplate {
-    return AesGcmKeyTemplates.aes256GcmNoPrefix();
-  }
-}
diff --git a/javascript/aead/aead_key_templates_test.ts b/javascript/aead/aead_key_templates_test.ts
deleted file mode 100644
index bbd64e2..0000000
--- a/javascript/aead/aead_key_templates_test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbKeyTemplate} from '../internal/proto';
-
-import {AeadKeyTemplates} from './aead_key_templates';
-
-describe('aead key templates test', function() {
-  it('aes128 ctr hmac sha256', function() {
-    const keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-    expect(keyTemplate instanceof PbKeyTemplate).toBe(true);
-  });
-
-  it('aes256 ctr hmac sha256', function() {
-    const keyTemplate = AeadKeyTemplates.aes256CtrHmacSha256();
-    expect(keyTemplate instanceof PbKeyTemplate).toBe(true);
-  });
-
-  it('aes128 gcm', function() {
-    const keyTemplate = AeadKeyTemplates.aes128Gcm();
-    expect(keyTemplate instanceof PbKeyTemplate).toBe(true);
-  });
-
-  it('aes256 gcm', function() {
-    const keyTemplate = AeadKeyTemplates.aes256Gcm();
-    expect(keyTemplate instanceof PbKeyTemplate).toBe(true);
-  });
-
-  it('aes256 gcm no prefix', function() {
-    const keyTemplate = AeadKeyTemplates.aes256GcmNoPrefix();
-    expect(keyTemplate instanceof PbKeyTemplate).toBe(true);
-  });
-});
diff --git a/javascript/aead/aead_wrapper.ts b/javascript/aead/aead_wrapper.ts
deleted file mode 100644
index 191e515..0000000
--- a/javascript/aead/aead_wrapper.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {CryptoFormat} from '../internal/crypto_format';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PrimitiveWrapper} from '../internal/primitive_wrapper';
-import {PbKeyStatusType} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import {Constructor} from '../internal/util';
-
-import {Aead} from './internal/aead';
-
-/**
- * @final
- */
-class WrappedAead extends Aead {
-  // The constructor should be @private, but it is not supported by Closure
-  // (see https://github.com/google/closure-compiler/issues/2761).
-  constructor(private readonly aeadSet: PrimitiveSet.PrimitiveSet<Aead>) {
-    super();
-  }
-
-  static newAead(aeadSet: PrimitiveSet.PrimitiveSet<Aead>): Aead {
-    if (!aeadSet) {
-      throw new SecurityException('Primitive set has to be non-null.');
-    }
-    if (!aeadSet.getPrimary()) {
-      throw new SecurityException('Primary has to be non-null.');
-    }
-    return new WrappedAead(aeadSet);
-  }
-
-  /**
-   */
-  async encrypt(plaintext: Uint8Array, opt_associatedData?: Uint8Array|null):
-      Promise<Uint8Array> {
-    if (!plaintext) {
-      throw new SecurityException('Plaintext has to be non-null.');
-    }
-    const primary = this.aeadSet.getPrimary()
-    if (!primary) {
-      throw new SecurityException('Primary has to be non-null.');
-    }
-    const primitive = primary.getPrimitive();
-    const encryptedText =
-        await primitive.encrypt(plaintext, opt_associatedData);
-    const keyId = primary.getIdentifier();
-    const ciphertext = new Uint8Array(keyId.length + encryptedText.length);
-    ciphertext.set(keyId, 0);
-    ciphertext.set(encryptedText, keyId.length);
-    return ciphertext;
-  }
-
-  /**
-   */
-  async decrypt(ciphertext: Uint8Array, opt_associatedData?: Uint8Array|null):
-      Promise<Uint8Array> {
-    if (!ciphertext) {
-      throw new SecurityException('Ciphertext has to be non-null.');
-    }
-    if (ciphertext.length > CryptoFormat.NON_RAW_PREFIX_SIZE) {
-      const keyId = ciphertext.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE);
-      const entries = await this.aeadSet.getPrimitives(keyId);
-      const rawCiphertext = ciphertext.subarray(
-          CryptoFormat.NON_RAW_PREFIX_SIZE, ciphertext.length);
-      let decryptedText: Uint8Array|undefined;
-      try {
-        decryptedText = await this.tryDecryption(
-            entries, rawCiphertext, opt_associatedData);
-      } catch (e) {
-      }
-      if (decryptedText) {
-        return decryptedText;
-      }
-    }
-    const entries = await this.aeadSet.getRawPrimitives();
-    const decryptedText =
-        await this.tryDecryption(entries, ciphertext, opt_associatedData);
-    return decryptedText;
-  }
-
-  /**
-   * Tries to decrypt the ciphertext using each entry in entriesArray and
-   * returns the ciphertext decrypted by first primitive which succeed. It
-   * throws an exception if no entry succeeds.
-   *
-   *
-   */
-  private async tryDecryption(
-      entriesArray: Array<PrimitiveSet.Entry<Aead>>, ciphertext: Uint8Array,
-      opt_associatedData?: Uint8Array|null): Promise<Uint8Array> {
-    const entriesArrayLength = entriesArray.length;
-    for (let i = 0; i < entriesArrayLength; i++) {
-      if (entriesArray[i].getKeyStatus() != PbKeyStatusType.ENABLED) {
-        continue;
-      }
-      const primitive = entriesArray[i].getPrimitive();
-      let decryptionResult;
-      try {
-        decryptionResult =
-            await primitive.decrypt(ciphertext, opt_associatedData);
-      } catch (e) {
-        continue;
-      }
-      return decryptionResult;
-    }
-    throw new SecurityException('Decryption failed for the given ciphertext.');
-  }
-}
-
-export class AeadWrapper implements PrimitiveWrapper<Aead> {
-  /**
-   */
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<Aead>): Aead {
-    return WrappedAead.newAead(primitiveSet);
-  }
-
-  /**
-   */
-  getPrimitiveType(): Constructor<Aead> {
-    return Aead;
-  }
-
-  static register() {
-    Registry.registerPrimitiveWrapper(new AeadWrapper());
-  }
-}
diff --git a/javascript/aead/aead_wrapper_test.ts b/javascript/aead/aead_wrapper_test.ts
deleted file mode 100644
index 3b44700..0000000
--- a/javascript/aead/aead_wrapper_test.ts
+++ /dev/null
@@ -1,284 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {CryptoFormat} from '../internal/crypto_format';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Bytes from '../subtle/bytes';
-import {assertExists} from '../testing/internal/test_utils';
-
-import {AeadWrapper} from './aead_wrapper';
-import {Aead} from './internal/aead';
-
-describe('aead wrapper test', function() {
-  it('new aead primitive set without primary', async function() {
-    const primitiveSet = createPrimitiveSet(/* opt_withPrimary = */ false);
-    try {
-      new AeadWrapper().wrap(primitiveSet);
-    // This is an intentionally unsafe partial mock
-    // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.primitiveSetWithoutPrimary());
-      return;
-    }
-    fail('Should throw an exception.');
-  });
-
-  it('new aead primitive should work', async function() {
-    const primitiveSet = createPrimitiveSet();
-    const aead = new AeadWrapper().wrap(primitiveSet);
-    expect(aead != null && aead != undefined).toBe(true);
-  });
-
-  it('encrypt', async function() {
-    const primitiveSet = createPrimitiveSet();
-    const aead = new AeadWrapper().wrap(primitiveSet);
-
-    const plaintext = new Uint8Array([0, 1, 2, 3]);
-
-    const ciphertext = await aead.encrypt(plaintext);
-    expect(ciphertext != null).toBe(true);
-
-    // Ciphertext should begin with primary key output prefix.
-    expect(ciphertext.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE))
-        .toEqual(assertExists(primitiveSet.getPrimary()).getIdentifier());
-  });
-
-  it('decrypt bad ciphertext', async function() {
-    const primitiveSet = createPrimitiveSet();
-    const aead = new AeadWrapper().wrap(primitiveSet);
-
-    const ciphertext = new Uint8Array([9, 8, 7, 6, 5, 4, 3]);
-
-    try {
-      await aead.decrypt(ciphertext);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
-      return;
-    }
-    fail('Should throw an exception');
-  });
-
-  it('decrypt with ciphertext encrypted by primary key', async function() {
-    const primitiveSet = createPrimitiveSet();
-    const aead = new AeadWrapper().wrap(primitiveSet);
-
-    const plaintext = new Uint8Array([12, 51, 45, 200, 120, 111]);
-
-    const ciphertext = await aead.encrypt(plaintext);
-    const decryptResult = await aead.decrypt(ciphertext);
-
-    expect(decryptResult).toEqual(plaintext);
-  });
-
-  it('decrypt ciphertext encrypted by non primary key', async function() {
-    const primitiveSet = createPrimitiveSet();
-    const aead = new AeadWrapper().wrap(primitiveSet);
-
-    // Encrypt the plaintext with primary.
-    const plaintext = new Uint8Array([0xAA, 0xBB, 0xAB, 0xBA, 0xFF]);
-    const ciphertext = await aead.encrypt(plaintext);
-
-    // Add a new primary to primitive set and make new AeadSetWrapper with the
-    // updated primitive set.
-    const keyId = 0xFFFFFFFF;
-    const key =
-        createKey(keyId, PbOutputPrefixType.LEGACY, /* enabled = */ true);
-    const entry =
-        primitiveSet.addPrimitive(new DummyAead(new Uint8Array([0xFF])), key);
-    primitiveSet.setPrimary(entry);
-    const aead2 = new AeadWrapper().wrap(primitiveSet);
-
-    // Check that the ciphertext can be decrypted by the setWrapper with new
-    // primary and that the decryption corresponds to the plaintext.
-    const decryptResult = await aead2.decrypt(ciphertext);
-
-    expect(decryptResult).toEqual(plaintext);
-  });
-
-  it('decrypt ciphertext raw primitive', async function() {
-    const primitiveSet = createPrimitiveSet();
-    // Create a RAW primitive and add it to primitiveSet.
-    const keyId = 0xFFFFFFFF;
-    const rawKey =
-        createKey(keyId, PbOutputPrefixType.RAW, /* enabled = */ true);
-    const rawKeyAead = new DummyAead(new Uint8Array([0xFF]));
-    primitiveSet.addPrimitive(rawKeyAead, rawKey);
-
-    // Encrypt the plaintext by aead corresponding to the rawKey.
-    const plaintext = new Uint8Array([0x11, 0x15, 0xAA, 0x54]);
-    const ciphertext = await rawKeyAead.encrypt(plaintext);
-
-    // Create aead which should be able to decrypt the ciphertext.
-    const aead = new AeadWrapper().wrap(primitiveSet);
-
-    // Try to decrypt the ciphertext by aead and check that the result
-    // corresponds to the plaintext.
-    const decryptResult = await aead.decrypt(ciphertext);
-    expect(decryptResult).toEqual(plaintext);
-  });
-
-  it('decrypt ciphertext disabled primitive', async function() {
-    const primitiveSet = createPrimitiveSet();
-
-    // Create a primitive with disabled key and add it to primitiveSet.
-    const keyId = 0xFFFFFFFF;
-    const key = createKey(keyId, PbOutputPrefixType.RAW, /* enabled = */ false);
-    const disabledKeyAead = new DummyAead(new Uint8Array([0xFF]));
-    primitiveSet.addPrimitive(disabledKeyAead, key);
-
-    // Encrypt the plaintext by a primitive with disabled key.
-    const plaintext = new Uint8Array([0, 1, 2, 3]);
-    const ciphertext = await disabledKeyAead.encrypt(plaintext);
-
-    // Create aead containing the primitive with disabled key.
-    const aead = new AeadWrapper().wrap(primitiveSet);
-
-    // Check that the ciphertext cannot be decrypted as disabled keys cannot be
-    // used to neither encryption nor decryption.
-    try {
-      await aead.decrypt(ciphertext);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('encrypt decrypt,  associated data should be passed', async function() {
-    const primitiveSet = createPrimitiveSet();
-    const aead = new AeadWrapper().wrap(primitiveSet);
-    const plaintext = new Uint8Array([0, 1, 2, 3, 4, 5, 6]);
-    const aad = new Uint8Array([8, 9, 10, 11, 12]);
-
-    // Encrypt the plaintext with aad. The ciphertext should end with aad if
-    // it was passed correctly.
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    const ciphertextAad =
-        ciphertext.slice(ciphertext.length - aad.length, ciphertext.length);
-    expect(ciphertextAad).toEqual(aad);
-
-    // Decrypt the ciphertext with aad. It is possible only if aad was passed
-    // correctly.
-    const decryptedCiphertext = await aead.decrypt(ciphertext, aad);
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-});
-
-/**
- * Class holding texts for each type of exception.
- * @final
- */
-class ExceptionText {
-  static nullPrimitiveSet(): string {
-    return 'SecurityException: Primitive set has to be non-null.';
-  }
-
-  static primitiveSetWithoutPrimary(): string {
-    return 'SecurityException: Primary has to be non-null.';
-  }
-
-  static cannotBeDecrypted(): string {
-    return 'SecurityException: Decryption failed for the given ciphertext.';
-  }
-}
-
-/** Function for creating keys for testing purposes. */
-function createKey(
-    keyId: number, outputPrefix: PbOutputPrefixType,
-    enabled: boolean): PbKeysetKey {
-  const key = new PbKeysetKey();
-
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-
-  key.setOutputPrefixType(outputPrefix);
-  key.setKeyId(keyId);
-
-  return key;
-}
-
-/**
- * Creates a primitive set with 'numberOfPrimitives' primitives. The keys
- * corresponding to the primitives have ids from the set
- * [1, ..., numberOfPrimitives] and the primitive corresponding to key with id
- * 'numberOfPrimitives' is set to be primary whenever opt_withPrimary is set to
- * true (where true is the default value).
- */
-function createPrimitiveSet(opt_withPrimary: boolean = true):
-    PrimitiveSet.PrimitiveSet<Aead> {
-  const numberOfPrimitives = 5;
-
-  const primitiveSet = new PrimitiveSet.PrimitiveSet<DummyAead>(DummyAead);
-  for (let i = 1; i < numberOfPrimitives; i++) {
-    let outputPrefix: PbOutputPrefixType;
-    switch (i % 3) {
-      case 0:
-        outputPrefix = PbOutputPrefixType.TINK;
-        break;
-      case 1:
-        outputPrefix = PbOutputPrefixType.LEGACY;
-        break;
-      default:
-        outputPrefix = PbOutputPrefixType.RAW;
-    }
-    const key = createKey(i, outputPrefix, /* enabled = */ i % 4 < 2);
-    primitiveSet.addPrimitive(new DummyAead(new Uint8Array([i])), key);
-  }
-
-  const key = createKey(
-      numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true);
-  const aead = new DummyAead(new Uint8Array([numberOfPrimitives]));
-  const entry = primitiveSet.addPrimitive(aead, key);
-  if (opt_withPrimary) {
-    primitiveSet.setPrimary(entry);
-  }
-
-  return primitiveSet;
-}
-
-/** @final */
-class DummyAead extends Aead {
-  constructor(private readonly primitiveIdentifier: Uint8Array) {
-    super();
-  }
-
-  /** @override*/
-  async encrypt(plaintext: Uint8Array, opt_associatedData?: Uint8Array) {
-    const result = Bytes.concat(plaintext, this.primitiveIdentifier);
-    if (opt_associatedData) {
-      return Bytes.concat(result, opt_associatedData);
-    }
-    return result;
-  }
-
-  /** @override*/
-  async decrypt(ciphertext: Uint8Array, opt_associatedData?: Uint8Array) {
-    if (opt_associatedData) {
-      const aad = ciphertext.subarray(
-          ciphertext.length - opt_associatedData.length, ciphertext.length);
-
-      if ([...aad].toString() != [...opt_associatedData].toString()) {
-        throw new SecurityException(ExceptionText.cannotBeDecrypted());
-      }
-      ciphertext = ciphertext.subarray(0, ciphertext.length - aad.length);
-    }
-
-    const primitiveIdentifier = ciphertext.subarray(
-        ciphertext.length - this.primitiveIdentifier.length, ciphertext.length);
-    if ([...primitiveIdentifier].toString() !=
-        [...this.primitiveIdentifier].toString()) {
-      throw new SecurityException(ExceptionText.cannotBeDecrypted());
-    }
-
-    return ciphertext.subarray(
-        0, ciphertext.length - this.primitiveIdentifier.length);
-  }
-}
diff --git a/javascript/aead/aes_ctr_hmac.ts b/javascript/aead/aes_ctr_hmac.ts
deleted file mode 100644
index 1e425e7..0000000
--- a/javascript/aead/aes_ctr_hmac.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
-import {AesCtrHmacAeadKeyTemplates} from './aes_ctr_hmac_aead_key_templates';
-
-export function register() {
-  Registry.registerKeyManager(new AesCtrHmacAeadKeyManager());
-}
-
-export const aes128CtrHmacSha256KeyTemplate =
-    AesCtrHmacAeadKeyTemplates.aes128CtrHmacSha256;
-export const aes256CtrHmacSha256KeyTemplate =
-    AesCtrHmacAeadKeyTemplates.aes256CtrHmacSha256;
diff --git a/javascript/aead/aes_ctr_hmac_aead_key_manager.ts b/javascript/aead/aes_ctr_hmac_aead_key_manager.ts
deleted file mode 100644
index 059183d..0000000
--- a/javascript/aead/aes_ctr_hmac_aead_key_manager.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as KeyManager from '../internal/key_manager';
-import {PbAesCtrHmacAeadKey, PbAesCtrHmacAeadKeyFormat, PbAesCtrKey, PbAesCtrKeyFormat, PbAesCtrParams, PbHashType, PbHmacKey, PbHmacKeyFormat, PbHmacParams, PbKeyData, PbMessage} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import {Constructor} from '../internal/util';
-import {aesCtrHmacFromRawKeys} from '../subtle/encrypt_then_authenticate';
-import * as Random from '../subtle/random';
-import * as Validators from '../subtle/validators';
-
-import {Aead} from './internal/aead';
-
-/**
- * @final
- */
-class AesCtrHmacAeadKeyFactory implements KeyManager.KeyFactory {
-  private static readonly VERSION: number = 0;
-  private static readonly MIN_KEY_SIZE: number = 16;
-  private static readonly MIN_IV_SIZE: number = 12;
-  private static readonly MAX_IV_SIZE: number = 16;
-  private static readonly MIN_TAG_SIZE: number = 10;
-  private static readonly MAX_TAG_SIZE = new Map([
-    [PbHashType.SHA1, 20], [PbHashType.SHA256, 32], [PbHashType.SHA512, 64]
-  ]);
-
-  /**
-   */
-  newKey(keyFormat: PbMessage|Uint8Array) {
-    let keyFormatProto: PbAesCtrHmacAeadKeyFormat;
-    if (keyFormat instanceof Uint8Array) {
-      try {
-        keyFormatProto = PbAesCtrHmacAeadKeyFormat.deserializeBinary(keyFormat);
-      } catch (e) {
-        throw new SecurityException(
-            'Could not parse the given Uint8Array as a serialized proto of ' +
-            AesCtrHmacAeadKeyManager.KEY_TYPE);
-      }
-      if (!keyFormatProto || !keyFormatProto.getAesCtrKeyFormat() ||
-          !keyFormatProto.getHmacKeyFormat()) {
-        throw new SecurityException(
-            'Could not parse the given Uint8Array as a serialized proto of ' +
-            AesCtrHmacAeadKeyManager.KEY_TYPE);
-      }
-    } else if (keyFormat instanceof PbAesCtrHmacAeadKeyFormat) {
-      keyFormatProto = keyFormat;
-    } else {
-      throw new SecurityException('Expected AesCtrHmacAeadKeyFormat-proto');
-    }
-
-    const {aesCtrParams, aesCtrKeySize} =
-        this.validateAesCtrKeyFormat(keyFormatProto.getAesCtrKeyFormat());
-    const aesCtrKey = (new PbAesCtrKey())
-                          .setVersion(AesCtrHmacAeadKeyFactory.VERSION)
-                          .setParams(aesCtrParams)
-                          .setKeyValue(Random.randBytes(aesCtrKeySize));
-    const {hmacParams, hmacKeySize} =
-        this.validateHmacKeyFormat(keyFormatProto.getHmacKeyFormat());
-    const hmacKey = (new PbHmacKey())
-                        .setVersion(AesCtrHmacAeadKeyFactory.VERSION)
-                        .setParams(hmacParams)
-                        .setKeyValue(Random.randBytes(hmacKeySize));
-    const aesCtrHmacAeadKey =
-        (new PbAesCtrHmacAeadKey()).setAesCtrKey(aesCtrKey).setHmacKey(hmacKey);
-    return aesCtrHmacAeadKey;
-  }
-
-  /**
-   */
-  newKeyData(serializedKeyFormat: Uint8Array) {
-    const key = (this.newKey(serializedKeyFormat));
-    const keyData =
-        (new PbKeyData())
-            .setTypeUrl(AesCtrHmacAeadKeyManager.KEY_TYPE)
-            .setValue(key.serializeBinary())
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
-    return keyData;
-  }
-
-  // helper functions
-  /**
-   * Checks the parameters and size of a given keyFormat.
-   *
-   */
-  validateAesCtrKeyFormat(keyFormat: undefined|null|PbAesCtrKeyFormat):
-      {aesCtrParams: PbAesCtrParams, aesCtrKeySize: number, ivSize: number} {
-    if (!keyFormat) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: key format undefined');
-    }
-    const aesCtrKeySize = keyFormat.getKeySize();
-    Validators.validateAesKeySize(aesCtrKeySize);
-    const aesCtrParams = keyFormat.getParams();
-    if (!aesCtrParams) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: params undefined');
-    }
-    const ivSize = aesCtrParams.getIvSize();
-    if (ivSize < AesCtrHmacAeadKeyFactory.MIN_IV_SIZE ||
-        ivSize > AesCtrHmacAeadKeyFactory.MAX_IV_SIZE) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: IV size is out of range: ' +
-          ivSize);
-    }
-    return {aesCtrParams, aesCtrKeySize, ivSize};
-  }
-
-  /**
-   * Checks the parameters and size of a given keyFormat.
-   *
-   */
-  validateHmacKeyFormat(keyFormat: undefined|null|PbHmacKeyFormat): {
-    hmacParams: PbHmacParams,
-    hmacKeySize: number,
-    hashType: string,
-    tagSize: number,
-  } {
-    if (!keyFormat) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: key format undefined');
-    }
-    const hmacKeySize = keyFormat.getKeySize();
-    if (hmacKeySize < AesCtrHmacAeadKeyFactory.MIN_KEY_SIZE) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: HMAC key is too small: ' +
-          keyFormat.getKeySize());
-    }
-    const hmacParams = keyFormat.getParams();
-    if (!hmacParams) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: params undefined');
-    }
-    const tagSize = hmacParams.getTagSize();
-    if (tagSize < AesCtrHmacAeadKeyFactory.MIN_TAG_SIZE) {
-      throw new SecurityException(
-          'Invalid HMAC params: tag size ' + tagSize + ' is too small.');
-    }
-    if (!AesCtrHmacAeadKeyFactory.MAX_TAG_SIZE.has(hmacParams.getHash())) {
-      throw new SecurityException('Unknown hash type.');
-    } else if (
-        tagSize >
-        AesCtrHmacAeadKeyFactory.MAX_TAG_SIZE.get(hmacParams.getHash())!) {
-      throw new SecurityException(
-          'Invalid HMAC params: tag size ' + tagSize + ' is out of range.');
-    }
-    let hashType: string;
-    switch (hmacParams.getHash()) {
-      case PbHashType.SHA1:
-        hashType = 'SHA-1';
-        break;
-      case PbHashType.SHA256:
-        hashType = 'SHA-256';
-        break;
-      case PbHashType.SHA512:
-        hashType = 'SHA-512';
-        break;
-      default:
-        hashType = 'UNKNOWN HASH';
-    }
-    return {hmacParams, hmacKeySize, hashType, tagSize};
-  }
-}
-
-/**
- * @final
- */
-export class AesCtrHmacAeadKeyManager implements KeyManager.KeyManager<Aead> {
-  private static readonly SUPPORTED_PRIMITIVE = Aead;
-  static KEY_TYPE: string =
-      'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey';
-  private static readonly VERSION: number = 0;
-  private readonly keyFactory = new AesCtrHmacAeadKeyFactory();
-
-  /**
-   */
-  async getPrimitive(
-      primitiveType: Constructor<Aead>, key: PbKeyData|PbMessage) {
-    if (primitiveType != this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    let deserializedKey: PbAesCtrHmacAeadKey;
-    if (key instanceof PbKeyData) {
-      if (!this.doesSupport(key.getTypeUrl())) {
-        throw new SecurityException(
-            'Key type ' + key.getTypeUrl() +
-            ' is not supported. This key manager supports ' +
-            this.getKeyType() + '.');
-      }
-      try {
-        deserializedKey =
-            PbAesCtrHmacAeadKey.deserializeBinary(key.getValue_asU8());
-      } catch (e) {
-        throw new SecurityException(
-            'Could not parse the key in key data as a serialized proto of ' +
-            AesCtrHmacAeadKeyManager.KEY_TYPE);
-      }
-      if (deserializedKey === null || deserializedKey === undefined) {
-        throw new SecurityException(
-            'Could not parse the key in key data as a serialized proto of ' +
-            AesCtrHmacAeadKeyManager.KEY_TYPE);
-      }
-    } else if (key instanceof PbAesCtrHmacAeadKey) {
-      deserializedKey = key;
-    } else {
-      throw new SecurityException(
-          'Given key type is not supported. ' +
-          'This key manager supports ' + this.getKeyType() + '.');
-    }
-
-    const {aesCtrKeyValue, ivSize} =
-        this.validateAesCtrKey(deserializedKey.getAesCtrKey());
-    const {hmacKeyValue, hashType, tagSize} =
-        this.validateHmacKey(deserializedKey.getHmacKey());
-    return await aesCtrHmacFromRawKeys(
-        aesCtrKeyValue, ivSize, hashType, hmacKeyValue, tagSize);
-  }
-
-  /**
-   */
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  /**
-   */
-  getKeyType() {
-    return AesCtrHmacAeadKeyManager.KEY_TYPE;
-  }
-
-  /**
-   */
-  getPrimitiveType() {
-    return AesCtrHmacAeadKeyManager.SUPPORTED_PRIMITIVE;
-  }
-
-  /**
-   */
-  getVersion() {
-    return AesCtrHmacAeadKeyManager.VERSION;
-  }
-
-  /**
-   */
-  getKeyFactory() {
-    return this.keyFactory;
-  }
-
-  // helper functions
-  /**
-   * Checks the parameters and size of a given AES-CTR key.
-   *
-   */
-  private validateAesCtrKey(key: undefined|null|PbAesCtrKey):
-      {aesCtrKeyValue: Uint8Array, ivSize: number} {
-    if (!key) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: key undefined');
-    }
-    Validators.validateVersion(key.getVersion(), this.getVersion());
-    const keyFormat = (new PbAesCtrKeyFormat())
-                          .setParams(key.getParams())
-                          .setKeySize(key.getKeyValue_asU8().length);
-    const {ivSize} = this.keyFactory.validateAesCtrKeyFormat(keyFormat);
-    return {aesCtrKeyValue: key.getKeyValue_asU8(), ivSize};
-  }
-
-  /**
-   * Checks the parameters and size of a given HMAC key.
-   *
-   */
-  private validateHmacKey(key: undefined|null|PbHmacKey):
-      {hmacKeyValue: Uint8Array, hashType: string, tagSize: number} {
-    if (!key) {
-      throw new SecurityException(
-          'Invalid AES CTR HMAC key format: key undefined');
-    }
-    Validators.validateVersion(key.getVersion(), this.getVersion());
-    const keyFormat = (new PbHmacKeyFormat())
-                          .setParams(key.getParams())
-                          .setKeySize(key.getKeyValue_asU8().length);
-    const {hashType, tagSize} =
-        this.keyFactory.validateHmacKeyFormat(keyFormat);
-    return {hmacKeyValue: key.getKeyValue_asU8(), hashType, tagSize};
-  }
-
-  static register() {
-    Registry.registerKeyManager(new AesCtrHmacAeadKeyManager());
-  }
-}
diff --git a/javascript/aead/aes_ctr_hmac_aead_key_manager_test.ts b/javascript/aead/aes_ctr_hmac_aead_key_manager_test.ts
deleted file mode 100644
index a5cc40f..0000000
--- a/javascript/aead/aes_ctr_hmac_aead_key_manager_test.ts
+++ /dev/null
@@ -1,564 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbAesCtrHmacAeadKey, PbAesCtrHmacAeadKeyFormat, PbAesCtrKey, PbAesCtrKeyFormat, PbAesCtrParams, PbHashType, PbHmacKey, PbHmacKeyFormat, PbHmacParams, PbKeyData} from '../internal/proto';
-import * as Random from '../subtle/random';
-import {assertExists} from '../testing/internal/test_utils';
-
-import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
-import {Aead} from './internal/aead';
-
-const KEY_TYPE = 'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey';
-const VERSION = 0;
-
-/////////////////////////////////////////////////////////////////////////////
-// Helper functions for tests
-
-/** creates new AesCtrHmacAeadKeyFormat with allowed parameters */
-function createTestKeyFormat(): PbAesCtrHmacAeadKeyFormat {
-  const KEY_SIZE = 16;
-  const IV_SIZE = 12;
-  const TAG_SIZE = 16;
-
-
-  const keyFormat = new PbAesCtrHmacAeadKeyFormat();
-  const aesCtrKeyFormat = new PbAesCtrKeyFormat();
-  aesCtrKeyFormat.setKeySize(KEY_SIZE);
-  const aesCtrParams = new PbAesCtrParams();
-  aesCtrParams.setIvSize(IV_SIZE);
-  aesCtrKeyFormat.setParams(aesCtrParams);
-  keyFormat.setAesCtrKeyFormat(aesCtrKeyFormat);
-
-  // set HMAC key
-  const hmacKeyFormat = new PbHmacKeyFormat();
-  hmacKeyFormat.setKeySize(KEY_SIZE);
-  const hmacParams = new PbHmacParams();
-  hmacParams.setHash(PbHashType.SHA1);
-  hmacParams.setTagSize(TAG_SIZE);
-  hmacKeyFormat.setParams(hmacParams);
-  keyFormat.setHmacKeyFormat(hmacKeyFormat);
-
-  return keyFormat;
-}
-
-/** creates new AesCtrHmacAeadKey with allowed parameters */
-function createTestKey(): PbAesCtrHmacAeadKey {
-  const KEY_SIZE = 16;
-  const IV_SIZE = 12;
-  const TAG_SIZE = 16;
-
-
-  const key = new PbAesCtrHmacAeadKey();
-  key.setVersion(0);
-  const aesCtrKey = new PbAesCtrKey();
-  aesCtrKey.setVersion(0);
-  const aesCtrParams = new PbAesCtrParams();
-  aesCtrParams.setIvSize(IV_SIZE);
-  aesCtrKey.setParams(aesCtrParams);
-  aesCtrKey.setKeyValue(Random.randBytes(KEY_SIZE));
-  key.setAesCtrKey(aesCtrKey);
-
-  // set HMAC key
-  const hmacKey = new PbHmacKey();
-  hmacKey.setVersion(0);
-  const hmacParams = new PbHmacParams();
-  hmacParams.setHash(PbHashType.SHA1);
-  hmacParams.setTagSize(TAG_SIZE);
-  hmacKey.setParams(hmacParams);
-  hmacKey.setKeyValue(Random.randBytes(KEY_SIZE));
-  key.setHmacKey(hmacKey);
-
-  return key;
-}
-
-/** creates new PbKeyData with allowed parameters */
-function createTestKeyData(): PbKeyData {
-  const keyData = new PbKeyData()
-                      .setTypeUrl(KEY_TYPE)
-                      .setValue(createTestKey().serializeBinary())
-                      .setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
-  return keyData;
-}
-
-describe('aes ctr hmac aead key manager test', function() {
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for newKey method
-
-  // newKey method -- key formats
-  it('new key bad key format', async function() {
-    const keyFormat = new PbAesCtrKeyFormat();
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: Expected AesCtrHmacAeadKeyFormat-proto');
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('new key bad serialized key', async function() {
-    // this is not a serialized key format
-    const serializedKeyFormat = new Uint8Array(4);
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    try {
-      manager.getKeyFactory().newKey(serializedKeyFormat);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Could not parse the given Uint8Array as a serialized' +
-              ' proto of ' + KEY_TYPE);
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  // newKey method -- bad parametrs of AES CTR KEY format
-  it('new key not supported aes ctr key size', async function() {
-    const keySize: number = 11;
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-    keyFormat.getAesCtrKeyFormat()?.setKeySize(keySize);
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: unsupported AES key size: ' +
-              keySize);
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-  it('new key iv size out of range', async function() {
-    const ivSizeOutOfRange: number[] = [10, 18];
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-
-    const ivSizeOutOfRangeLength = ivSizeOutOfRange.length;
-    for (let i = 0; i < ivSizeOutOfRangeLength; i++) {
-      keyFormat.getAesCtrKeyFormat()?.getParams()?.setIvSize(
-          ivSizeOutOfRange[i]);
-      try {
-        manager.getKeyFactory().newKey(keyFormat);
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(
-                'SecurityException: Invalid AES CTR HMAC key format: IV size is ' +
-                'out of range: ' + ivSizeOutOfRange[i]);
-        continue;
-      }
-      fail('An exception should be thrown.');
-    }
-  });
-
-  // newKey method -- bad parametrs of HMAC KEY format
-  it('new key small hmac key size', async function() {
-    const keySize: number = 11;
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-    keyFormat.getHmacKeyFormat()?.setKeySize(keySize);
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Invalid AES CTR HMAC key format: HMAC key is' +
-              ' too small: ' + keySize);
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('new key hash type unsupported', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-    keyFormat.getHmacKeyFormat()?.getParams()?.setHash(PbHashType.UNKNOWN_HASH);
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-    } catch (e: any) {
-      expect(e.toString()).toBe('SecurityException: Unknown hash type.');
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('new key small tag size', async function() {
-    const SMALL_TAG_SIZE = 8;
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-    keyFormat.getHmacKeyFormat()?.getParams()?.setTagSize(SMALL_TAG_SIZE);
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Invalid HMAC params: tag size ' +
-              SMALL_TAG_SIZE + ' is too small.');
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('new key big tag size for hash type', async function() {
-    const tagSizes = [
-      {'hashType': PbHashType.SHA1, 'tagSize': 22},
-      {'hashType': PbHashType.SHA256, 'tagSize': 34},
-      {'hashType': PbHashType.SHA512, 'tagSize': 66},
-    ];
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-
-    const tagSizesLength = tagSizes.length;
-    for (let i = 0; i < tagSizesLength; i++) {
-      keyFormat.getHmacKeyFormat()?.getParams()?.setHash(
-          tagSizes[i]['hashType']);
-      keyFormat.getHmacKeyFormat()?.getParams()?.setTagSize(
-          tagSizes[i]['tagSize']);
-      try {
-        manager.getKeyFactory().newKey(keyFormat);
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(
-                'SecurityException: Invalid HMAC params: tag size ' +
-                tagSizes[i]['tagSize'] + ' is out of range.');
-        continue;
-      }
-      fail('An exception should be thrown.');
-    }
-  });
-
-  it('new key via format proto', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-
-    const key = manager.getKeyFactory().newKey(keyFormat);
-
-    // testing AES CTR key
-    expect(key.getAesCtrKey()?.getKeyValue_asU8().length)
-        .toBe(keyFormat.getAesCtrKeyFormat()?.getKeySize());
-    expect(key.getAesCtrKey()?.getVersion()).toBe(0);
-    expect(key.getAesCtrKey()?.getParams()?.getIvSize())
-        .toBe(keyFormat.getAesCtrKeyFormat()?.getParams()?.getIvSize());
-
-    // testing HMAC key
-    expect(key.getHmacKey()?.getKeyValue_asU8()?.length)
-        .toBe(keyFormat.getHmacKeyFormat()?.getKeySize());
-    expect(key.getHmacKey()?.getVersion()).toBe(0);
-    expect(key.getHmacKey()?.getParams()?.getHash())
-        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getHash());
-    expect(key.getHmacKey()?.getParams()?.getTagSize())
-        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getTagSize());
-  });
-
-  it('new key via serialized format proto', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-    const serializedKeyFormat = keyFormat.serializeBinary();
-
-    const key = manager.getKeyFactory().newKey(serializedKeyFormat);
-
-    // testing AES CTR key
-    expect(key.getAesCtrKey()?.getKeyValue_asU8().length)
-        .toBe(keyFormat.getAesCtrKeyFormat()?.getKeySize());
-    expect(key.getAesCtrKey()?.getVersion()).toBe(0);
-    expect(key.getAesCtrKey()?.getParams()?.getIvSize())
-        .toBe(keyFormat.getAesCtrKeyFormat()?.getParams()?.getIvSize());
-
-
-    // testing HMAC key
-    expect(key.getHmacKey()?.getKeyValue_asU8()?.length)
-        .toBe(keyFormat.getHmacKeyFormat()?.getKeySize());
-    expect(key.getHmacKey()?.getVersion()).toBe(0);
-    expect(key.getHmacKey()?.getParams()?.getHash())
-        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getHash());
-    expect(key.getHmacKey()?.getParams()?.getTagSize())
-        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getTagSize());
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for NewKeyData method
-
-  it('new key data bad serialized key', async function() {
-    const serializedKeyFormats = [new Uint8Array(1), new Uint8Array(0)];
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-
-    const serializedKeyFormatsLength = serializedKeyFormats.length;
-    for (let i = 0; i < serializedKeyFormatsLength; i++) {
-      try {
-        aeadKeyManager.getKeyFactory().newKeyData(serializedKeyFormats[i]);
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(
-                'SecurityException: Could not parse the given Uint8Array as a ' +
-                'serialized proto of ' + KEY_TYPE);
-        continue;
-      }
-      fail(
-          'An exception should be thrown for the string: ' +
-          serializedKeyFormats[i]);
-    }
-  });
-
-  it('new key data from valid key', async function() {
-    const keyFormat = createTestKeyFormat();
-    const serializedKeyFormat = keyFormat.serializeBinary();
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const keyData = manager.getKeyFactory().newKeyData(serializedKeyFormat);
-
-    expect(keyData.getTypeUrl()).toBe(KEY_TYPE);
-    expect(keyData.getKeyMaterialType())
-        .toBe(PbKeyData.KeyMaterialType.SYMMETRIC);
-
-    const key = PbAesCtrHmacAeadKey.deserializeBinary(keyData.getValue_asU8());
-
-    expect(key.getAesCtrKey()?.getKeyValue_asU8().length)
-        .toBe(keyFormat.getAesCtrKeyFormat()?.getKeySize());
-    expect(key.getHmacKey()?.getKeyValue_asU8()?.length)
-        .toBe(keyFormat.getHmacKeyFormat()?.getKeySize());
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getPrimitive method
-
-  it('get primitive unsupported key data type', async function() {
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const keyData = createTestKeyData().setTypeUrl('bad type url');
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, keyData);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Key type ' + keyData.getTypeUrl() +
-              ' is not supported. This key manager supports ' + KEY_TYPE + '.');
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive unsupported key type', async function() {
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key = new PbAesCtrKey();
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Given key type is not supported. ' +
-              'This key manager supports ' + KEY_TYPE + '.');
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive bad version', async function() {
-    const version = 1;
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    key.getAesCtrKey()?.setVersion(version);
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Version is out of bound, must be between 0 ' +
-              'and ' + VERSION + '.');
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive short aes ctr key', async function() {
-    const keySize = 5;
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    key.getAesCtrKey()?.setKeyValue(new Uint8Array(keySize));
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: unsupported AES key size: ' +
-              keySize);
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive aes ctr key small iv size', async function() {
-    const ivSizeOutOfRange: number[] = [9, 19];
-    const manager = new AesCtrHmacAeadKeyManager();
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    const ivSizeOutOfRangeLength = ivSizeOutOfRange.length;
-    for (let i = 0; i < ivSizeOutOfRangeLength; i++) {
-      key.getAesCtrKey()?.getParams()?.setIvSize(ivSizeOutOfRange[i]);
-      try {
-        await manager.getPrimitive(Aead, key);
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(
-                'SecurityException: Invalid AES CTR HMAC key format: IV size is ' +
-                'out of range: ' + ivSizeOutOfRange[i]);
-        continue;
-      }
-      fail('An exception should be thrown.');
-    }
-  });
-
-  it('get primitive short hmac key', async function() {
-    const keySize = 5;
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    key.getHmacKey()?.setKeyValue(new Uint8Array(keySize));
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Invalid AES CTR HMAC key format: HMAC key is' +
-              ' too small: ' + keySize);
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive hmac key unsupported hash type', async function() {
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    key.getHmacKey()?.getParams()?.setHash(PbHashType.UNKNOWN_HASH);
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, key);
-    } catch (e: any) {
-      expect(e.toString()).toBe('SecurityException: Unknown hash type.');
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive hmac key small tag size', async function() {
-    const SMALL_TAG_SIZE = 9;
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    key.getHmacKey()?.getParams()?.setTagSize(SMALL_TAG_SIZE);
-
-    try {
-      await aeadKeyManager.getPrimitive(Aead, key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: Invalid HMAC params: tag size ' +
-              SMALL_TAG_SIZE + ' is too small.');
-      return;
-    }
-    fail('An exception should be thrown');
-  });
-
-  it('get primitive hmac big tag size', async function() {
-    const tagSizes = [
-      {'hashType': PbHashType.SHA1, 'tagSize': 22},
-      {'hashType': PbHashType.SHA256, 'tagSize': 34},
-      {'hashType': PbHashType.SHA512, 'tagSize': 66},
-    ];
-    const manager = new AesCtrHmacAeadKeyManager();
-
-    const key: PbAesCtrHmacAeadKey = createTestKey();
-
-    const tagSizesLength = tagSizes.length;
-    for (let i = 0; i < tagSizesLength; i++) {
-      const params = assertExists(key.getHmacKey()?.getParams());
-      params.setHash(tagSizes[i]['hashType']);
-      params.setTagSize(tagSizes[i]['tagSize']);
-      try {
-        await manager.getPrimitive(Aead, key);
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(
-                'SecurityException: Invalid HMAC params: tag size ' +
-                tagSizes[i]['tagSize'] + ' is out of range.');
-        continue;
-      }
-      fail('An exception should be thrown.');
-    }
-  });
-
-  // tests for getting primitive from valid key/keyData
-  it('get primitive from key', async function() {
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const key = createTestKey();
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-
-    const primitive: Aead = await aeadKeyManager.getPrimitive(Aead, key);
-    const ciphertext = await primitive.encrypt(plaintext, aad);
-    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);
-
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  it('get primitive from key data', async function() {
-    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
-    const keyData = createTestKeyData();
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-
-    const primitive: Aead = await aeadKeyManager.getPrimitive(Aead, keyData);
-    const ciphertext = await primitive.encrypt(plaintext, aad);
-    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);
-
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getVersion, getKeyType and doesSupport methods
-
-  it('get version should be zero', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    expect(manager.getVersion()).toBe(0);
-  });
-
-  it('get key type should be aes ctr hmac aead key', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    expect(manager.getKeyType()).toBe(KEY_TYPE);
-  });
-
-  it('does support should support aes ctr hmac aead key', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    expect(manager.doesSupport(KEY_TYPE)).toBe(true);
-  });
-
-  it('get primitive type should be aead', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    expect(manager.getPrimitiveType()).toBe(Aead);
-  });
-});
diff --git a/javascript/aead/aes_ctr_hmac_aead_key_templates.ts b/javascript/aead/aes_ctr_hmac_aead_key_templates.ts
deleted file mode 100644
index 9aea70f..0000000
--- a/javascript/aead/aes_ctr_hmac_aead_key_templates.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbAesCtrHmacAeadKeyFormat, PbAesCtrKeyFormat, PbAesCtrParams, PbHashType, PbHmacKeyFormat, PbHmacParams, PbKeyTemplate, PbOutputPrefixType} from '../internal/proto';
-
-import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
-
-/**
- * Pre-generated KeyTemplates for AES CTR HMAC AEAD keys.
- *
- * @final
- */
-export class AesCtrHmacAeadKeyTemplates {
-  /**
-   * Returns a KeyTemplate that generates new instances of AesCtrHmacAeadKey
-   * with the following parameters:
-   *    AES key size: 16 bytes
-   *    AES IV size: 16 bytes
-   *    HMAC key size: 32 bytes
-   *    HMAC tag size: 16 bytes
-   *    HMAC hash function: SHA256
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes128CtrHmacSha256(): PbKeyTemplate {
-    return AesCtrHmacAeadKeyTemplates.newAesCtrHmacSha256KeyTemplate(
-        16,
-        /* aesKeySize = */
-        16,
-        /* ivSize = */
-        32,
-        /* hmacKeySize = */
-        16);
-  }
-
-  /* tagSize = */
-  /**
-   * Returns a KeyTemplate that generates new instances of AesCtrHmacAeadKey
-   * with the following parameters:
-   *    AES key size: 32 bytes
-   *    AES IV size: 16 bytes
-   *    HMAC key size: 32 bytes
-   *    HMAC tag size: 32 bytes
-   *    HMAC hash function: SHA256
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes256CtrHmacSha256(): PbKeyTemplate {
-    return AesCtrHmacAeadKeyTemplates.newAesCtrHmacSha256KeyTemplate(
-        32,
-        /* aesKeySize = */
-        16,
-        /* ivSize = */
-        32,
-        /* hmacKeySize = */
-        32);
-  }
-
-  /* tagSize = */
-  private static newAesCtrHmacSha256KeyTemplate(
-      aesKeySize: number, ivSize: number, hmacKeySize: number,
-      tagSize: number): PbKeyTemplate {
-    // Define AES CTR key format.
-    const aesCtrKeyFormat = (new PbAesCtrKeyFormat())
-                                .setKeySize(aesKeySize)
-                                .setParams(new PbAesCtrParams());
-    aesCtrKeyFormat.getParams()!.setIvSize(ivSize);
-
-    // Define HMAC key format.
-    const hmacKeyFormat = (new PbHmacKeyFormat())
-                              .setKeySize(hmacKeySize)
-                              .setParams(new PbHmacParams());
-    hmacKeyFormat.getParams()!.setTagSize(tagSize);
-    hmacKeyFormat.getParams()!.setHash(PbHashType.SHA256);
-
-    // Define AES CTR HMAC AEAD key format.
-    const keyFormat = (new PbAesCtrHmacAeadKeyFormat())
-                          .setAesCtrKeyFormat(aesCtrKeyFormat)
-                          .setHmacKeyFormat(hmacKeyFormat);
-
-    // Define key template.
-    const keyTemplate = (new PbKeyTemplate())
-                            .setTypeUrl(AesCtrHmacAeadKeyManager.KEY_TYPE)
-                            .setOutputPrefixType(PbOutputPrefixType.TINK)
-                            .setValue(keyFormat.serializeBinary());
-    return keyTemplate;
-  }
-}
diff --git a/javascript/aead/aes_ctr_hmac_aead_key_templates_test.ts b/javascript/aead/aes_ctr_hmac_aead_key_templates_test.ts
deleted file mode 100644
index b6d61d5..0000000
--- a/javascript/aead/aes_ctr_hmac_aead_key_templates_test.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbAesCtrHmacAeadKeyFormat, PbHashType, PbOutputPrefixType} from '../internal/proto';
-
-import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
-import {AesCtrHmacAeadKeyTemplates} from './aes_ctr_hmac_aead_key_templates';
-
-describe('aes ctr hmac aead key templates test', function() {
-  it('aes128 ctr hmac sha256', function() {
-    // Expects function to create key with following parameters.
-    const expectedAesKeySize = 16;
-    const expectedIvSize = 16;
-    const expectedHmacKeySize = 32;
-    const expectedTagSize = 16;
-    const expectedHashFunction = PbHashType.SHA256;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-
-    // Expected type URL is the one supported by AesCtrHmacAeadKeyManager.
-    const manager = new AesCtrHmacAeadKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = AesCtrHmacAeadKeyTemplates.aes128CtrHmacSha256();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test values in key format.
-    const keyFormat = PbAesCtrHmacAeadKeyFormat.deserializeBinary(
-        keyTemplate.getValue_asU8());
-
-    // Test AesCtrKeyFormat.
-    const aesCtrKeyFormat = keyFormat.getAesCtrKeyFormat();
-    expect(aesCtrKeyFormat!.getKeySize()).toBe(expectedAesKeySize);
-    expect(aesCtrKeyFormat!.getParams()!.getIvSize()).toBe(expectedIvSize);
-
-    // Test HmacKeyFormat.
-    const hmacKeyFormat = keyFormat.getHmacKeyFormat();
-    expect(hmacKeyFormat!.getKeySize()).toBe(expectedHmacKeySize);
-    expect(hmacKeyFormat!.getParams()!.getTagSize()).toBe(expectedTagSize);
-    expect(hmacKeyFormat!.getParams()!.getHash()).toBe(expectedHashFunction);
-
-    // Test that the template works with AesCtrHmacAeadKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-
-  it('aes256 ctr hmac sha256', function() {
-    // Expects function to create key with following parameters.
-    const expectedAesKeySize = 32;
-    const expectedIvSize = 16;
-    const expectedHmacKeySize = 32;
-    const expectedTagSize = 32;
-    const expectedHashFunction = PbHashType.SHA256;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-
-    // Expected type URL is the one supported by AesCtrHmacAeadKeyManager.
-    const manager = new AesCtrHmacAeadKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = AesCtrHmacAeadKeyTemplates.aes256CtrHmacSha256();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test values in key format.
-    const keyFormat = PbAesCtrHmacAeadKeyFormat.deserializeBinary(
-        keyTemplate.getValue_asU8());
-
-    // Test AesCtrKeyFormat.
-    const aesCtrKeyFormat = keyFormat.getAesCtrKeyFormat();
-    expect(aesCtrKeyFormat!.getKeySize()).toBe(expectedAesKeySize);
-    expect(aesCtrKeyFormat!.getParams()!.getIvSize()).toBe(expectedIvSize);
-
-    // Test HmacKeyFormat.
-    const hmacKeyFormat = keyFormat.getHmacKeyFormat();
-    expect(hmacKeyFormat!.getKeySize()).toBe(expectedHmacKeySize);
-    expect(hmacKeyFormat!.getParams()!.getTagSize()).toBe(expectedTagSize);
-    expect(hmacKeyFormat!.getParams()!.getHash()).toBe(expectedHashFunction);
-
-    // Test that the template works with AesCtrHmacAeadKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-});
diff --git a/javascript/aead/aes_gcm.ts b/javascript/aead/aes_gcm.ts
deleted file mode 100644
index 1f02e2e..0000000
--- a/javascript/aead/aes_gcm.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {AesGcmKeyManager} from './aes_gcm_key_manager';
-import {AesGcmKeyTemplates} from './aes_gcm_key_templates';
-
-export function register() {
-  Registry.registerKeyManager(new AesGcmKeyManager());
-}
-
-export const aes128GcmKeyTemplate = AesGcmKeyTemplates.aes128Gcm;
-export const aes256GcmKeyTemplate = AesGcmKeyTemplates.aes256Gcm;
-export const aes256GcmNoPrefixKeyTemplate =
-    AesGcmKeyTemplates.aes256GcmNoPrefix;
diff --git a/javascript/aead/aes_gcm_key_manager.ts b/javascript/aead/aes_gcm_key_manager.ts
deleted file mode 100644
index af67825..0000000
--- a/javascript/aead/aes_gcm_key_manager.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as KeyManager from '../internal/key_manager';
-import {PbAesGcmKey, PbAesGcmKeyFormat, PbKeyData, PbMessage} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import {Constructor} from '../internal/util';
-import * as aesGcm from '../subtle/aes_gcm';
-import * as Random from '../subtle/random';
-import * as Validators from '../subtle/validators';
-
-import {Aead} from './internal/aead';
-
-const VERSION = 0;
-
-/**
- * @final
- */
-class AesGcmKeyFactory implements KeyManager.KeyFactory {
-  newKey(keyFormat: PbMessage|Uint8Array) {
-    const keyFormatProto = AesGcmKeyFactory.getKeyFormatProto(keyFormat);
-    AesGcmKeyFactory.validateKeyFormat(keyFormatProto);
-    const key = (new PbAesGcmKey())
-                    .setKeyValue(Random.randBytes(keyFormatProto.getKeySize()))
-                    .setVersion(VERSION);
-    return key;
-  }
-
-  newKeyData(serializedKeyFormat: Uint8Array) {
-    const key = (this.newKey(serializedKeyFormat));
-    const keyData =
-        (new PbKeyData())
-            .setTypeUrl(AesGcmKeyManager.KEY_TYPE)
-            .setValue(key.serializeBinary())
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
-    return keyData;
-  }
-
-  private static validateKeyFormat(keyFormat: PbAesGcmKeyFormat) {
-    Validators.validateAesKeySize(keyFormat.getKeySize());
-  }
-
-  /**
-   * The input keyFormat is either deserialized (in case that the input is
-   * Uint8Array) or checked to be an AesGcmKeyFormat-proto (otherwise).
-   *
-   */
-  private static getKeyFormatProto(keyFormat: PbMessage|
-                                   Uint8Array): PbAesGcmKeyFormat {
-    if (keyFormat instanceof Uint8Array) {
-      return AesGcmKeyFactory.deserializeKeyFormat(keyFormat);
-    } else if (keyFormat instanceof PbAesGcmKeyFormat) {
-      return keyFormat;
-    } else {
-      throw new SecurityException('Expected AesGcmKeyFormat-proto');
-    }
-  }
-
-  private static deserializeKeyFormat(keyFormat: Uint8Array):
-      PbAesGcmKeyFormat {
-    let keyFormatProto: PbAesGcmKeyFormat;
-    try {
-      keyFormatProto = PbAesGcmKeyFormat.deserializeBinary(keyFormat);
-    } catch (e) {
-      throw new SecurityException(
-          'Could not parse the input as a serialized proto of ' +
-          AesGcmKeyManager.KEY_TYPE + ' key format.');
-    }
-    if (!keyFormatProto.getKeySize()) {
-      throw new SecurityException(
-          'Could not parse the input as a serialized proto of ' +
-          AesGcmKeyManager.KEY_TYPE + ' key format.');
-    }
-    return keyFormatProto;
-  }
-}
-
-/**
- * @final
- */
-export class AesGcmKeyManager implements KeyManager.KeyManager<Aead> {
-  private static readonly SUPPORTED_PRIMITIVE = Aead;
-  static KEY_TYPE: string = 'type.googleapis.com/google.crypto.tink.AesGcmKey';
-  private readonly keyFactory: AesGcmKeyFactory;
-
-  /** Visible for testing. */
-  constructor() {
-    this.keyFactory = new AesGcmKeyFactory();
-  }
-
-  async getPrimitive(
-      primitiveType: Constructor<Aead>, key: PbKeyData|PbMessage) {
-    if (primitiveType != this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    const keyProto = AesGcmKeyManager.getKeyProto(key);
-    AesGcmKeyManager.validateKey(keyProto);
-    return await aesGcm.fromRawKey(keyProto.getKeyValue_asU8());
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return AesGcmKeyManager.KEY_TYPE;
-  }
-
-  getPrimitiveType() {
-    return AesGcmKeyManager.SUPPORTED_PRIMITIVE;
-  }
-
-  getVersion() {
-    return VERSION;
-  }
-
-  getKeyFactory() {
-    return this.keyFactory;
-  }
-
-  private static validateKey(key: PbAesGcmKey) {
-    Validators.validateAesKeySize(key.getKeyValue_asU8().length);
-    Validators.validateVersion(key.getVersion(), VERSION);
-  }
-
-  /**
-   * The input key is either deserialized (in case that the input is
-   * KeyData-proto) or checked to be an AesGcmKey-proto (otherwise).
-   *
-   */
-  private static getKeyProto(keyMaterial: PbMessage|PbKeyData): PbAesGcmKey {
-    if (keyMaterial instanceof PbKeyData) {
-      return AesGcmKeyManager.getKeyProtoFromKeyData(keyMaterial);
-    } else if (keyMaterial instanceof PbAesGcmKey) {
-      return keyMaterial;
-    } else {
-      throw new SecurityException(
-          'Key type is not supported. ' +
-          'This key manager supports ' + AesGcmKeyManager.KEY_TYPE + '.');
-    }
-  }
-
-  /**
-   * It validates the key type and returns a deserialized AesGcmKey-proto.
-   *
-   */
-  private static getKeyProtoFromKeyData(keyData: PbKeyData): PbAesGcmKey {
-    if (keyData.getTypeUrl() != AesGcmKeyManager.KEY_TYPE) {
-      throw new SecurityException(
-          'Key type ' + keyData.getTypeUrl() +
-          ' is not supported. This key manager supports ' +
-          AesGcmKeyManager.KEY_TYPE + '.');
-    }
-    let deserializedKey: PbAesGcmKey;
-    try {
-      deserializedKey = PbAesGcmKey.deserializeBinary(keyData.getValue_asU8());
-    } catch (e) {
-      throw new SecurityException(
-          'Could not parse the input as a ' +
-          'serialized proto of ' + AesGcmKeyManager.KEY_TYPE + ' key.');
-    }
-    return deserializedKey;
-  }
-
-  static register() {
-    Registry.registerKeyManager(new AesGcmKeyManager());
-  }
-}
diff --git a/javascript/aead/aes_gcm_key_manager_test.ts b/javascript/aead/aes_gcm_key_manager_test.ts
deleted file mode 100644
index 6e8f157..0000000
--- a/javascript/aead/aes_gcm_key_manager_test.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbAesCtrKey, PbAesCtrKeyFormat, PbAesGcmKey, PbAesGcmKeyFormat, PbKeyData} from '../internal/proto';
-import * as Random from '../subtle/random';
-
-import {AesGcmKeyManager} from './aes_gcm_key_manager';
-import {Aead} from './internal/aead';
-
-const KEY_TYPE = 'type.googleapis.com/google.crypto.tink.AesGcmKey';
-const VERSION = 0;
-const PRIMITIVE = Aead;
-
-describe('aes gcm key manager test', function() {
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for newKey method
-
-  // newKey method -- key formats
-  it('new key, invalid key format', function() {
-    const keyFormat = new PbAesCtrKeyFormat();
-    const manager = new AesGcmKeyManager();
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidKeyFormat());
-    }
-  });
-
-  it('new key, invalid serialized key format', function() {
-    const keyFormat = new Uint8Array(0);
-    const manager = new AesGcmKeyManager();
-
-    try {
-      manager.getKeyFactory().newKey(keyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
-    }
-  });
-
-  it('new key, unsupported key sizes', function() {
-    const manager = new AesGcmKeyManager();
-
-    for (let keySize = 0; keySize < 40; keySize++) {
-      if (keySize === 16 || keySize === 32) {
-        // Keys of size 16 and 32 bytes are supported.
-        continue;
-      }
-      const keyFormat = createTestKeyFormat(keySize);
-
-      try {
-        manager.getKeyFactory().newKey(keyFormat);
-        fail('An exception should be thrown.');
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.unsupportedKeySize(keySize));
-      }
-    }
-  });
-
-  it('new key, via format proto', function() {
-    const manager = new AesGcmKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-
-    const key = manager.getKeyFactory().newKey(keyFormat);
-
-    expect(key.getKeyValue_asU8().length).toBe(keyFormat.getKeySize());
-  });
-
-  it('new key, via serialized format proto', function() {
-    const manager = new AesGcmKeyManager();
-
-    const keyFormat = createTestKeyFormat();
-    const serializedKeyFormat = keyFormat.serializeBinary();
-
-    const key = manager.getKeyFactory().newKey(serializedKeyFormat);
-
-    expect(key.getKeyValue_asU8().length).toBe(keyFormat.getKeySize());
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for NewKeyData method
-
-  it('new key data, should work', function() {
-    const keyFormat = createTestKeyFormat();
-    const serializedKeyFormat = keyFormat.serializeBinary();
-    const manager = new AesGcmKeyManager();
-
-    const keyData = manager.getKeyFactory().newKeyData(serializedKeyFormat);
-
-    expect(keyData.getTypeUrl()).toBe(KEY_TYPE);
-    expect(keyData.getKeyMaterialType())
-        .toBe(PbKeyData.KeyMaterialType.SYMMETRIC);
-
-    const key = PbAesGcmKey.deserializeBinary(keyData.getValue_asU8());
-
-    expect(key.getKeyValue_asU8().length).toBe(keyFormat.getKeySize());
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getPrimitive method
-
-  it('get primitive, unsupported key data type', async function() {
-    const manager = new AesGcmKeyManager();
-    const keyData = createTestKeyData().setTypeUrl('bad_type_url');
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, keyData);
-      fail('An exception should be thrown');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyType(keyData.getTypeUrl()));
-    }
-  });
-
-  it('get primitive, unsupported key type', async function() {
-    const manager = new AesGcmKeyManager();
-    const key = new PbAesCtrKey();
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyType());
-    }
-  });
-
-  it('get primitive, bad version', async function() {
-    const version = 1;
-    const manager = new AesGcmKeyManager();
-    const key = createTestKey().setVersion(version);
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.versionOutOfBounds());
-    }
-  });
-
-  it('get primitive, unsupported key sizes', async function() {
-    const manager = new AesGcmKeyManager();
-
-    for (let keySize = 0; keySize < 40; keySize++) {
-      if (keySize === 16 || keySize === 32) {
-        // Keys of sizes 16 and 32 bytes are supported.
-        continue;
-      }
-
-      const key: PbAesGcmKey = createTestKey(keySize);
-      try {
-        await manager.getPrimitive(PRIMITIVE, key);
-        fail('An exception should be thrown');
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.unsupportedKeySize(keySize));
-      }
-    }
-  });
-
-  it('get primitive, bad serialization', async function() {
-    const manager = new AesGcmKeyManager();
-    const keyData = createTestKeyData().setValue(new Uint8Array([0]));
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, keyData);
-      fail('An exception should be thrown');
-    } catch (e: any) {
-      let message = e.toString();
-      if (message === ExceptionText.unsupportedKeySize(0)) {
-        message = ExceptionText.invalidSerializedKey();
-      }
-      expect(message).toBe(ExceptionText.invalidSerializedKey());
-    }
-  });
-
-  // Tests for getting primitive from valid key/keyData.
-  it('get primitive, from key', async function() {
-    const manager = new AesGcmKeyManager();
-    const key = createTestKey();
-
-    // Get the primitive from key manager.
-    const primitive: Aead = await manager.getPrimitive(PRIMITIVE, key);
-
-    // Test the returned primitive.
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await primitive.encrypt(plaintext, aad);
-    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);
-
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  it('get primitive, from key data', async function() {
-    const manager = new AesGcmKeyManager();
-    const keyData = createTestKeyData();
-
-    // Get primitive.
-    const primitive: Aead = await manager.getPrimitive(PRIMITIVE, keyData);
-
-    // Test the returned primitive.
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await primitive.encrypt(plaintext, aad);
-    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);
-
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getVersion, getKeyType and doesSupport methods
-
-  it('get version, should be zero', function() {
-    const manager = new AesGcmKeyManager();
-    expect(manager.getVersion()).toBe(0);
-  });
-
-  it('get key type, should be aes gcm key type', function() {
-    const manager = new AesGcmKeyManager();
-    expect(manager.getKeyType()).toBe(KEY_TYPE);
-  });
-
-  it('does support, should support aes gcm key type', function() {
-    const manager = new AesGcmKeyManager();
-    expect(manager.doesSupport(KEY_TYPE)).toBe(true);
-  });
-
-  it('get primitive type, should be aead', function() {
-    const manager = new AesGcmKeyManager();
-    expect(manager.getPrimitiveType()).toBe(PRIMITIVE);
-  });
-});
-
-/////////////////////////////////////////////////////////////////////////////
-// Helper functions for tests
-
-class ExceptionText {
-  static unsupportedPrimitive(): string {
-    return 'SecurityException: Requested primitive type which is not supported ' +
-        'by this key manager.';
-  }
-
-  static unsupportedKeySize(keySize: number): string {
-    return 'InvalidArgumentsException: unsupported AES key size: ' + keySize;
-  }
-
-  static versionOutOfBounds(): string {
-    return 'SecurityException: Version is out of bound, must be between 0 and ' +
-        VERSION + '.';
-  }
-
-  static unsupportedKeyType(opt_unsupportedKeyType?: string): string {
-    const prefix = 'SecurityException: Key type';
-    const suffix =
-        'is not supported. This key manager supports ' + KEY_TYPE + '.';
-
-    if (opt_unsupportedKeyType) {
-      return prefix + ' ' + opt_unsupportedKeyType + ' ' + suffix;
-    } else {
-      return prefix + ' ' + suffix;
-    }
-  }
-
-  static invalidSerializedKey(): string {
-    return 'SecurityException: Could not parse the input as a serialized proto of ' +
-        KEY_TYPE + ' key.';
-  }
-
-  static invalidSerializedKeyFormat() {
-    return 'SecurityException: Could not parse the input as a serialized proto of ' +
-        KEY_TYPE + ' key format.';
-  }
-
-  static invalidKeyFormat(): string {
-    return 'SecurityException: Expected AesGcmKeyFormat-proto';
-  }
-}
-
-function createTestKeyFormat(opt_keySize: number = 16): PbAesGcmKeyFormat {
-  const keyFormat = new PbAesGcmKeyFormat().setKeySize(opt_keySize);
-  return keyFormat;
-}
-
-function createTestKey(opt_keySize: number = 16): PbAesGcmKey {
-  const key = new PbAesGcmKey().setVersion(0).setKeyValue(
-      Random.randBytes(opt_keySize));
-
-  return key;
-}
-
-function createTestKeyData(opt_keySize?: number): PbKeyData {
-  const keyData = new PbKeyData()
-                      .setTypeUrl(KEY_TYPE)
-                      .setValue(createTestKey(opt_keySize).serializeBinary())
-                      .setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
-
-  return keyData;
-}
diff --git a/javascript/aead/aes_gcm_key_templates.ts b/javascript/aead/aes_gcm_key_templates.ts
deleted file mode 100644
index db132d1..0000000
--- a/javascript/aead/aes_gcm_key_templates.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbAesGcmKeyFormat, PbKeyTemplate, PbOutputPrefixType} from '../internal/proto';
-
-import {AesGcmKeyManager} from './aes_gcm_key_manager';
-
-/**
- * Pre-generated KeyTemplates for AES GCM keys.
- *
- * @final
- */
-export class AesGcmKeyTemplates {
-  /**
-   * Returns a KeyTemplate that generates new instances of AesGcmKey
-   * with the following parameters:
-   *    key size: 16 bytes
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes128Gcm(): PbKeyTemplate {
-    return AesGcmKeyTemplates.newAesGcmKeyTemplate(
-        /* keySize = */
-        16,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of AesGcmKey
-   * with the following parameters:
-   *    key size: 32 bytes
-   *    OutputPrefixType: TINK
-   *
-   */
-  static aes256Gcm(): PbKeyTemplate {
-    return AesGcmKeyTemplates.newAesGcmKeyTemplate(
-        /* keySize = */
-        32,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of AesGcmKey
-   * with the following parameters:
-   *     key size: 32 bytes
-   *     OutputPrefixType: RAW
-   *
-   */
-  static aes256GcmNoPrefix(): PbKeyTemplate {
-    return AesGcmKeyTemplates.newAesGcmKeyTemplate(
-        /* keySize = */
-        32,
-        /* outputPrefixType = */
-        PbOutputPrefixType.RAW);
-  }
-
-  private static newAesGcmKeyTemplate(
-      keySize: number, outputPrefixType: PbOutputPrefixType): PbKeyTemplate {
-    // Define AES GCM key format.
-    const keyFormat = (new PbAesGcmKeyFormat()).setKeySize(keySize);
-
-    // Define key template.
-    const keyTemplate = (new PbKeyTemplate())
-                            .setTypeUrl(AesGcmKeyManager.KEY_TYPE)
-                            .setOutputPrefixType(outputPrefixType)
-                            .setValue(keyFormat.serializeBinary());
-    return keyTemplate;
-  }
-}
diff --git a/javascript/aead/aes_gcm_key_templates_test.ts b/javascript/aead/aes_gcm_key_templates_test.ts
deleted file mode 100644
index 7e8fe38..0000000
--- a/javascript/aead/aes_gcm_key_templates_test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbAesGcmKeyFormat, PbOutputPrefixType} from '../internal/proto';
-
-import {AesGcmKeyManager} from './aes_gcm_key_manager';
-import {AesGcmKeyTemplates} from './aes_gcm_key_templates';
-
-describe('aes gcm key templates test', function() {
-  it('aes128 gcm', function() {
-    // The created key should have the following parameters.
-    const expectedKeySize = 16;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-    // Expected type URL is the one supported by AesGcmKeyManager.
-    const manager = new AesGcmKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = AesGcmKeyTemplates.aes128Gcm();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test key size value in key format.
-    const keyFormat =
-        PbAesGcmKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    expect(keyFormat.getKeySize()).toBe(expectedKeySize);
-
-    // Test that the template works with AesCtrHmacAeadKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-
-  it('aes256 gcm', function() {
-    // The created key should have the following parameters.
-    const expectedKeySize = 32;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-    // Expected type URL is the one supported by AesGcmKeyManager.
-    const manager = new AesGcmKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = AesGcmKeyTemplates.aes256Gcm();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test key size value in key format.
-    const keyFormat =
-        PbAesGcmKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    expect(keyFormat.getKeySize()).toBe(expectedKeySize);
-
-    // Test that the template works with AesCtrHmacAeadKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-
-  it('aes256 gcm no prefix', function() {
-    // The created key should have the following parameters.
-    const expectedKeySize = 32;
-    const expectedOutputPrefix = PbOutputPrefixType.RAW;
-    // Expected type URL is the one supported by AesGcmKeyManager.
-    const manager = new AesGcmKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = AesGcmKeyTemplates.aes256GcmNoPrefix();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test key size value in key format.
-    const keyFormat =
-        PbAesGcmKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    expect(keyFormat.getKeySize()).toBe(expectedKeySize);
-
-    // Test that the template works with AesCtrHmacAeadKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-});
diff --git a/javascript/aead/index.ts b/javascript/aead/index.ts
deleted file mode 100644
index df53ab7..0000000
--- a/javascript/aead/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as aesCtrHmac from './aes_ctr_hmac';
-import * as aesGcm from './aes_gcm';
-import * as wrapper from './wrapper';
-
-export * from './aead';
-export * from './aes_ctr_hmac';
-export {aes128GcmKeyTemplate, aes256GcmKeyTemplate, aes256GcmNoPrefixKeyTemplate} from './aes_gcm';
-
-export function register() {
-  aesCtrHmac.register();
-  aesGcm.register();
-  wrapper.register();
-}
diff --git a/javascript/aead/internal/BUILD.bazel b/javascript/aead/internal/BUILD.bazel
deleted file mode 100644
index 5be5d07..0000000
--- a/javascript/aead/internal/BUILD.bazel
+++ /dev/null
@@ -1,8 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "internal",
-    srcs = ["aead.ts"],
-)
diff --git a/javascript/aead/internal/aead.ts b/javascript/aead/internal/aead.ts
deleted file mode 100644
index 75754a7..0000000
--- a/javascript/aead/internal/aead.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for Authenticated Encryption with Associated Data (AEAD).
- *
- * Security guarantees: Implementations of this interface are secure against
- * adaptive chosen ciphertext attacks. Encryption with associated data ensures
- * authenticity (who the sender is) and integrity (the data has not been
- * tampered with) of that data, but not its secrecy.
- *
- * @see https://tools.ietf.org/html/rfc5116
- */
-export abstract class Aead {
-  /**
-   * Encrypts `plaintext` with `opt_associatedData` as associated authenticated
-   * data. The resulting ciphertext allows for checking authenticity and
-   * integrity of associated data, but does not guarantee its secrecy.
-   *
-   * @param plaintext the plaintext to be encrypted. It must be
-   *     non-null, but can also be an empty (zero-length) byte array.
-   * @param opt_associatedData  optional associated data to be
-   *     authenticated, but not encrypted. A null value is equivalent to an
-   *     empty (zero-length) byte array. For successful decryption the same
-   *     associated data must be provided along with the ciphertext.
-   * @return resulting ciphertext
-   *
-   */
-  abstract encrypt(plaintext: Uint8Array, opt_associatedData?: Uint8Array|null):
-      Promise<Uint8Array>;
-
-  /**
-   * Decrypts ciphertext with associated authenticated data.
-   * The decryption verifies the authenticity and integrity of the associated
-   * data, but there are no guarantees wrt. secrecy of that data.
-   *
-   * @param ciphertext the ciphertext to be decrypted, must be
-   *     non-null.
-   * @param opt_associatedData  optional associated data to be
-   *     authenticated. A null value is equivalent to an empty (zero-length)
-   *     byte array. For successful decryption the same associated data must be
-   *     provided along with the ciphertext.
-   * @return resulting plaintext
-   */
-  abstract decrypt(
-      ciphertext: Uint8Array,
-      opt_associatedData?: Uint8Array|null): Promise<Uint8Array>;
-}
diff --git a/javascript/aead/subtle/BUILD.bazel b/javascript/aead/subtle/BUILD.bazel
deleted file mode 100644
index 64d9df2..0000000
--- a/javascript/aead/subtle/BUILD.bazel
+++ /dev/null
@@ -1,15 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "subtle",
-    srcs = [
-        "aes_ctr_hmac.ts",
-        "aes_gcm.ts",
-        "encrypt_then_authenticate.ts",
-        "index.ts",
-    ],
-    module_name = "tink-crypto/aead/subtle",
-    deps = ["//subtle"],
-)
diff --git a/javascript/aead/subtle/aes_ctr_hmac.ts b/javascript/aead/subtle/aes_ctr_hmac.ts
deleted file mode 100644
index c96cbeb..0000000
--- a/javascript/aead/subtle/aes_ctr_hmac.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {aesCtrHmacFromRawKeys as fromRawKeys} from '../../subtle/encrypt_then_authenticate';
diff --git a/javascript/aead/subtle/aes_gcm.ts b/javascript/aead/subtle/aes_gcm.ts
deleted file mode 100644
index a57e7c4..0000000
--- a/javascript/aead/subtle/aes_gcm.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {AesGcm, fromRawKey} from '../../subtle/aes_gcm';
diff --git a/javascript/aead/subtle/encrypt_then_authenticate.ts b/javascript/aead/subtle/encrypt_then_authenticate.ts
deleted file mode 100644
index ee05284..0000000
--- a/javascript/aead/subtle/encrypt_then_authenticate.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {EncryptThenAuthenticate} from '../../subtle/encrypt_then_authenticate';
diff --git a/javascript/aead/subtle/index.ts b/javascript/aead/subtle/index.ts
deleted file mode 100644
index c35615f..0000000
--- a/javascript/aead/subtle/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {fromRawKeys as aesCtrHmac} from './aes_ctr_hmac';
-export {AesGcm, fromRawKey as aesGcmFromRawKey} from './aes_gcm';
-export * from './encrypt_then_authenticate';
diff --git a/javascript/aead/wrapper.ts b/javascript/aead/wrapper.ts
deleted file mode 100644
index da30cb1..0000000
--- a/javascript/aead/wrapper.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {AeadWrapper} from './aead_wrapper';
-
-export function register() {
-  Registry.registerPrimitiveWrapper(new AeadWrapper());
-}
diff --git a/javascript/binary/BUILD.bazel b/javascript/binary/BUILD.bazel
deleted file mode 100644
index f70f841..0000000
--- a/javascript/binary/BUILD.bazel
+++ /dev/null
@@ -1,12 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "binary",
-    srcs = ["index.ts"],
-    module_name = "tink-crypto/binary",
-    deps = [
-        "//internal",
-    ],
-)
diff --git a/javascript/binary/index.ts b/javascript/binary/index.ts
deleted file mode 100644
index 93c7e89..0000000
--- a/javascript/binary/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {BinaryKeysetReader} from '../internal/binary_keyset_reader';
-import {KeysetHandle, readNoSecret} from '../internal/keyset_handle';
-
-export function deserializeNoSecretKeyset(
-    serializedKeyset: Uint8Array): KeysetHandle {
-  return readNoSecret(BinaryKeysetReader.withUint8Array(serializedKeyset));
-}
diff --git a/javascript/binary/insecure/BUILD.bazel b/javascript/binary/insecure/BUILD.bazel
deleted file mode 100644
index ba38a33..0000000
--- a/javascript/binary/insecure/BUILD.bazel
+++ /dev/null
@@ -1,10 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "insecure",
-    srcs = ["index.ts"],
-    module_name = "tink-crypto/binary/insecure",
-    deps = ["//internal"],
-)
diff --git a/javascript/binary/insecure/index.ts b/javascript/binary/insecure/index.ts
deleted file mode 100644
index 9c8e231..0000000
--- a/javascript/binary/insecure/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {CleartextKeysetHandle} from '../../internal/cleartext_keyset_handle';
-
-export const deserializeKeyset = CleartextKeysetHandle.deserializeFromBinary;
-export const serializeKeyset = CleartextKeysetHandle.serializeToBinary;
diff --git a/javascript/exception/BUILD.bazel b/javascript/exception/BUILD.bazel
deleted file mode 100644
index 13dec44..0000000
--- a/javascript/exception/BUILD.bazel
+++ /dev/null
@@ -1,11 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "exception",
-    srcs = [
-        "invalid_arguments_exception.ts",
-        "security_exception.ts",
-    ],
-)
diff --git a/javascript/exception/invalid_arguments_exception.ts b/javascript/exception/invalid_arguments_exception.ts
deleted file mode 100644
index 83edc8e..0000000
--- a/javascript/exception/invalid_arguments_exception.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Exception used when a function receives an invalid argument.
- */
-export class InvalidArgumentsException extends Error {
-  constructor(message?: string) {
-    super(message);
-    Object.setPrototypeOf(this, InvalidArgumentsException.prototype);
-  }
-}
-InvalidArgumentsException.prototype.name = 'InvalidArgumentsException';
diff --git a/javascript/exception/security_exception.ts b/javascript/exception/security_exception.ts
deleted file mode 100644
index e4c7c5c..0000000
--- a/javascript/exception/security_exception.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * The base class for all security exceptions.
- */
-export class SecurityException extends Error {
-  constructor(message?: string) {
-    super(message);
-    Object.setPrototypeOf(this, SecurityException.prototype);
-  }
-}
-SecurityException.prototype.name = 'SecurityException';
diff --git a/javascript/hybrid/BUILD.bazel b/javascript/hybrid/BUILD.bazel
deleted file mode 100644
index 9fd02cd..0000000
--- a/javascript/hybrid/BUILD.bazel
+++ /dev/null
@@ -1,63 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "hybrid",
-    srcs = [
-        "decrypt.ts",
-        "decrypt_wrapper.ts",
-        "ecies_aead_hkdf_for_decrypting.ts",
-        "ecies_aead_hkdf_for_encrypting.ts",
-        "ecies_aead_hkdf_private_key_manager.ts",
-        "ecies_aead_hkdf_public_key_manager.ts",
-        "ecies_aead_hkdf_util.ts",
-        "ecies_aead_hkdf_validators.ts",
-        "ecies_with_aes_ctr_hmac.ts",
-        "ecies_with_aes_gcm.ts",
-        "encrypt.ts",
-        "encrypt_wrapper.ts",
-        "hybrid_config.ts",
-        "hybrid_decrypt_wrapper.ts",
-        "hybrid_encrypt_wrapper.ts",
-        "hybrid_key_templates.ts",
-        "index.ts",
-        "registry_ecies_aead_hkdf_dem_helper.ts",
-    ],
-    module_name = "tink-crypto/hybrid",
-    deps = [
-        "//aead",
-        "//exception",
-        "//hybrid/internal",
-        "//internal",
-        "//internal:proto",
-        "//subtle",
-    ],
-)
-
-ts_library(
-    name = "hybrid_tests",
-    testonly = True,
-    srcs = [
-        "ecies_aead_hkdf_private_key_manager_test.ts",
-        "ecies_aead_hkdf_public_key_manager_test.ts",
-        "ecies_aead_hkdf_util_test.ts",
-        "ecies_aead_hkdf_validators_test.ts",
-        "hybrid_config_test.ts",
-        "hybrid_decrypt_wrapper_test.ts",
-        "hybrid_encrypt_wrapper_test.ts",
-        "hybrid_key_templates_test.ts",
-        "registry_ecies_aead_hkdf_dem_helper_test.ts",
-    ],
-    deps = [
-        ":hybrid",
-        "//aead",
-        "//exception",
-        "//hybrid/internal",
-        "//internal",
-        "//internal:proto",
-        "//subtle",
-        "//testing/internal",
-        "@npm//@types/jasmine",
-    ],
-)
diff --git a/javascript/hybrid/decrypt.ts b/javascript/hybrid/decrypt.ts
deleted file mode 100644
index bc23f27..0000000
--- a/javascript/hybrid/decrypt.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {HybridDecrypt} from './internal/hybrid_decrypt';
diff --git a/javascript/hybrid/decrypt_wrapper.ts b/javascript/hybrid/decrypt_wrapper.ts
deleted file mode 100644
index 54c670c..0000000
--- a/javascript/hybrid/decrypt_wrapper.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {HybridDecryptWrapper} from './hybrid_decrypt_wrapper';
-
-export function register() {
-  Registry.registerPrimitiveWrapper(new HybridDecryptWrapper());
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_for_decrypting.ts b/javascript/hybrid/ecies_aead_hkdf_for_decrypting.ts
deleted file mode 100644
index 30e6517..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_for_decrypting.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {EciesAeadHkdfPrivateKeyManager} from './ecies_aead_hkdf_private_key_manager';
-
-export function register() {
-  Registry.registerKeyManager(new EciesAeadHkdfPrivateKeyManager());
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_for_encrypting.ts b/javascript/hybrid/ecies_aead_hkdf_for_encrypting.ts
deleted file mode 100644
index b9d97f2..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_for_encrypting.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {EciesAeadHkdfPublicKeyManager} from './ecies_aead_hkdf_public_key_manager';
-
-export function register() {
-  Registry.registerKeyManager(new EciesAeadHkdfPublicKeyManager());
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_private_key_manager.ts b/javascript/hybrid/ecies_aead_hkdf_private_key_manager.ts
deleted file mode 100644
index 8c0063e..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_private_key_manager.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as KeyManager from '../internal/key_manager';
-import {PbEciesAeadHkdfKeyFormat, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbKeyData, PbMessage} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as Bytes from '../subtle/bytes';
-import * as eciesAeadHkdfHybridDecrypt from '../subtle/ecies_aead_hkdf_hybrid_decrypt';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-
-import {EciesAeadHkdfPublicKeyManager} from './ecies_aead_hkdf_public_key_manager';
-import * as EciesAeadHkdfUtil from './ecies_aead_hkdf_util';
-import * as EciesAeadHkdfValidators from './ecies_aead_hkdf_validators';
-import {HybridDecrypt} from './internal/hybrid_decrypt';
-import {RegistryEciesAeadHkdfDemHelper} from './registry_ecies_aead_hkdf_dem_helper';
-
-const VERSION = 0;
-
-/**
- * @final
- */
-class EciesAeadHkdfPrivateKeyFactory implements KeyManager.PrivateKeyFactory {
-  /**
-   */
-  async newKey(keyFormat: PbMessage|
-               Uint8Array): Promise<PbEciesAeadHkdfPrivateKey> {
-    if (!keyFormat) {
-      throw new SecurityException('Key format has to be non-null.');
-    }
-    const keyFormatProto =
-        EciesAeadHkdfPrivateKeyFactory.getKeyFormatProto(keyFormat);
-    EciesAeadHkdfValidators.validateKeyFormat(keyFormatProto);
-    const params = keyFormatProto.getParams();
-    if (!params) {
-      throw new SecurityException('Params not set');
-    }
-    const kemParams = params.getKemParams();
-    if (!kemParams) {
-      throw new SecurityException('KEM params not set');
-    }
-    const curveTypeProto = kemParams.getCurveType();
-    const curveTypeSubtle = Util.curveTypeProtoToSubtle(curveTypeProto);
-    const curveName = EllipticCurves.curveToString(curveTypeSubtle);
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
-    const jsonPublicKey =
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const jsonPrivateKey =
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    return EciesAeadHkdfPrivateKeyFactory.jsonToProtoKey(
-        jsonPrivateKey, jsonPublicKey, params);
-  }
-
-  /**
-   */
-  async newKeyData(serializedKeyFormat: PbMessage|
-                   Uint8Array): Promise<PbKeyData> {
-    const key = await this.newKey(serializedKeyFormat);
-    const keyData =
-        (new PbKeyData())
-            .setTypeUrl(EciesAeadHkdfPrivateKeyManager.KEY_TYPE)
-            .setValue(key.serializeBinary())
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PRIVATE);
-    return keyData;
-  }
-
-  getPublicKeyData(serializedPrivateKey: Uint8Array) {
-    const privateKey = deserializePrivateKey(serializedPrivateKey);
-    const publicKey = privateKey.getPublicKey();
-    if (!publicKey) {
-      throw new SecurityException('Public key not set');
-    }
-    const publicKeyData =
-        (new PbKeyData())
-            .setValue(publicKey.serializeBinary())
-            .setTypeUrl(EciesAeadHkdfPublicKeyManager.KEY_TYPE)
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC);
-    return publicKeyData;
-  }
-
-  /**
-   * Creates a private key proto corresponding to given JSON key pair and with
-   * the given params.
-   *
-   */
-  private static jsonToProtoKey(
-      jsonPrivateKey: JsonWebKey, jsonPublicKey: JsonWebKey,
-      params: PbEciesAeadHkdfParams): PbEciesAeadHkdfPrivateKey {
-    const {x, y} = jsonPublicKey;
-    if (x === undefined) {
-      throw new SecurityException('x must be set');
-    }
-    if (y === undefined) {
-      throw new SecurityException('y must be set');
-    }
-    const publicKeyProto =
-        (new PbEciesAeadHkdfPublicKey())
-            .setVersion(EciesAeadHkdfPublicKeyManager.VERSION)
-            .setParams(params)
-            .setX(Bytes.fromBase64(x, true))
-            .setY(Bytes.fromBase64(y, true));
-    const {d} = jsonPrivateKey;
-    if (d === undefined) {
-      throw new SecurityException('d must be set');
-    }
-    const privateKeyProto = (new PbEciesAeadHkdfPrivateKey())
-                                .setVersion(VERSION)
-                                .setPublicKey(publicKeyProto)
-                                .setKeyValue(Bytes.fromBase64(d, true));
-    return privateKeyProto;
-  }
-
-  /**
-   * The input keyFormat is either deserialized (in case that the input is
-   * Uint8Array) or checked to be an EciesAeadHkdfKeyFormat-proto (otherwise).
-   *
-   */
-  private static getKeyFormatProto(keyFormat: PbMessage|
-                                   Uint8Array): PbEciesAeadHkdfKeyFormat {
-    if (keyFormat instanceof Uint8Array) {
-      return EciesAeadHkdfPrivateKeyFactory.deserializeKeyFormat(keyFormat);
-    } else if (keyFormat instanceof PbEciesAeadHkdfKeyFormat) {
-      return keyFormat;
-    } else {
-      throw new SecurityException(
-          'Expected ' + EciesAeadHkdfPrivateKeyManager.KEY_TYPE +
-          ' key format proto.');
-    }
-  }
-
-  private static deserializeKeyFormat(keyFormat: Uint8Array):
-      PbEciesAeadHkdfKeyFormat {
-    let keyFormatProto: PbEciesAeadHkdfKeyFormat;
-    try {
-      keyFormatProto = PbEciesAeadHkdfKeyFormat.deserializeBinary(keyFormat);
-    } catch (e) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' +
-          EciesAeadHkdfPrivateKeyManager.KEY_TYPE + ' key format proto.');
-    }
-    if (!keyFormatProto.getParams()) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' +
-          EciesAeadHkdfPrivateKeyManager.KEY_TYPE + ' key format proto.');
-    }
-    return keyFormatProto;
-  }
-}
-
-/**
- * @final
- */
-export class EciesAeadHkdfPrivateKeyManager implements
-    KeyManager.KeyManager<HybridDecrypt> {
-  private static readonly SUPPORTED_PRIMITIVE = HybridDecrypt;
-  static KEY_TYPE: string =
-      'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey';
-  keyFactory = new EciesAeadHkdfPrivateKeyFactory();
-
-  async getPrimitive(
-      primitiveType: Util.Constructor<HybridDecrypt>,
-      key: PbKeyData|PbMessage) {
-    if (primitiveType !== this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    const keyProto = EciesAeadHkdfPrivateKeyManager.getKeyProto(key);
-    EciesAeadHkdfValidators.validatePrivateKey(
-        keyProto, VERSION, EciesAeadHkdfPublicKeyManager.VERSION);
-    const recepientPrivateKey =
-        EciesAeadHkdfUtil.getJsonWebKeyFromProto(keyProto);
-    const publicKey = keyProto.getPublicKey();
-    if (!publicKey) {
-      throw new SecurityException('Public key not set');
-    }
-    const params = publicKey.getParams();
-    if (!params) {
-      throw new SecurityException('Params not set');
-    }
-    const demParams = params.getDemParams();
-    if (!demParams) {
-      throw new SecurityException('DEM params not set');
-    }
-    const keyTemplate = (demParams.getAeadDem());
-    if (!keyTemplate) {
-      throw new SecurityException('Key template not set');
-    }
-    const demHelper = new RegistryEciesAeadHkdfDemHelper(keyTemplate);
-    const pointFormat =
-        Util.pointFormatProtoToSubtle(params.getEcPointFormat());
-    const kemParams = params.getKemParams();
-    if (!kemParams) {
-      throw new SecurityException('KEM params not set');
-    }
-    const hkdfHash = Util.hashTypeProtoToString(kemParams.getHkdfHashType());
-    const hkdfSalt = kemParams.getHkdfSalt_asU8();
-    return eciesAeadHkdfHybridDecrypt.fromJsonWebKey(
-        recepientPrivateKey, hkdfHash, pointFormat, demHelper, hkdfSalt);
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return EciesAeadHkdfPrivateKeyManager.KEY_TYPE;
-  }
-
-  getPrimitiveType() {
-    return EciesAeadHkdfPrivateKeyManager.SUPPORTED_PRIMITIVE;
-  }
-
-  getVersion() {
-    return VERSION;
-  }
-
-  getKeyFactory() {
-    return this.keyFactory;
-  }
-
-  private static getKeyProto(keyMaterial: PbKeyData|
-                             PbMessage): PbEciesAeadHkdfPrivateKey {
-    if (keyMaterial instanceof PbKeyData) {
-      return EciesAeadHkdfPrivateKeyManager.getKeyProtoFromKeyData(keyMaterial);
-    }
-    if (keyMaterial instanceof PbEciesAeadHkdfPrivateKey) {
-      return keyMaterial;
-    }
-    throw new SecurityException(
-        'Key type is not supported. This key ' +
-        'manager supports ' + EciesAeadHkdfPrivateKeyManager.KEY_TYPE + '.');
-  }
-
-  private static getKeyProtoFromKeyData(keyData: PbKeyData):
-      PbEciesAeadHkdfPrivateKey {
-    if (keyData.getTypeUrl() !== EciesAeadHkdfPrivateKeyManager.KEY_TYPE) {
-      throw new SecurityException(
-          'Key type ' + keyData.getTypeUrl() +
-          ' is not supported. This key manager supports ' +
-          EciesAeadHkdfPrivateKeyManager.KEY_TYPE + '.');
-    }
-    return deserializePrivateKey(keyData.getValue_asU8());
-  }
-}
-
-function deserializePrivateKey(serializedPrivateKey: Uint8Array):
-    PbEciesAeadHkdfPrivateKey {
-  let key: PbEciesAeadHkdfPrivateKey;
-  try {
-    key = PbEciesAeadHkdfPrivateKey.deserializeBinary(serializedPrivateKey);
-  } catch (e) {
-    throw new SecurityException(
-        'Input cannot be parsed as ' + EciesAeadHkdfPrivateKeyManager.KEY_TYPE +
-        ' key-proto.');
-  }
-  if (!key.getPublicKey() || !key.getKeyValue_asU8()) {
-    throw new SecurityException(
-        'Input cannot be parsed as ' + EciesAeadHkdfPrivateKeyManager.KEY_TYPE +
-        ' key-proto.');
-  }
-  return key;
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_private_key_manager_test.ts b/javascript/hybrid/ecies_aead_hkdf_private_key_manager_test.ts
deleted file mode 100644
index f63e17c..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_private_key_manager_test.ts
+++ /dev/null
@@ -1,506 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {PbAesCtrKeyFormat, PbEciesAeadDemParams, PbEciesAeadHkdfKeyFormat, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyData, PbKeyTemplate, PbPointFormat} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Random from '../subtle/random';
-import {assertExists, assertInstanceof, assertMessageEquals} from '../testing/internal/test_utils';
-
-import {EciesAeadHkdfPrivateKeyManager} from './ecies_aead_hkdf_private_key_manager';
-import {EciesAeadHkdfPublicKeyManager} from './ecies_aead_hkdf_public_key_manager';
-import {HybridDecrypt} from './internal/hybrid_decrypt';
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-
-const PRIVATE_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey';
-const PRIVATE_KEY_MATERIAL_TYPE = PbKeyData.KeyMaterialType.ASYMMETRIC_PRIVATE;
-const VERSION = 0;
-const PRIVATE_KEY_MANAGER_PRIMITIVE = HybridDecrypt;
-
-const PUBLIC_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey';
-const PUBLIC_KEY_MATERIAL_TYPE = PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC;
-const PUBLIC_KEY_MANAGER_PRIMITIVE = HybridEncrypt;
-
-describe('ecies aead hkdf private key manager test', function() {
-  beforeEach(function() {
-    AeadConfig.register();
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('new key, empty key format', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(new Uint8Array(0));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
-    }
-  });
-
-  it('new key, invalid serialized key format', async function() {
-    const invalidSerializedKeyFormat = new Uint8Array(0);
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(invalidSerializedKeyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
-    }
-  });
-
-  it('new key, unsupported key format proto', async function() {
-    const unsupportedKeyFormatProto = new PbAesCtrKeyFormat();
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(unsupportedKeyFormatProto);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyFormat());
-    }
-  });
-
-  it('new key, invalid format, missing params', async function() {
-    const invalidFormat = new PbEciesAeadHkdfKeyFormat();
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidKeyFormatMissingParams());
-    }
-  });
-
-  it('new key, invalid format, invalid params', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    // unknown point format
-    const invalidFormat = createKeyFormat();
-    invalidFormat.getParams()?.setEcPointFormat(PbPointFormat.UNKNOWN_FORMAT);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownPointFormat());
-    }
-    invalidFormat.getParams()?.setEcPointFormat(PbPointFormat.UNCOMPRESSED);
-
-    // missing KEM params
-    invalidFormat.getParams()?.setKemParams(null);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKemParams());
-    }
-    invalidFormat.getParams()?.setKemParams(createKemParams());
-
-    // unsupported AEAD template
-    const templateTypeUrl = 'UNSUPPORTED_KEY_TYPE_URL';
-    invalidFormat.getParams()?.getDemParams()?.getAeadDem()?.setTypeUrl(
-        templateTypeUrl);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyTemplate(templateTypeUrl));
-    }
-  });
-
-  it('new key, via key format', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    for (const keyFormat of keyFormats) {
-      const key = await manager.getKeyFactory().newKey(keyFormat);
-
-      expect(key.getPublicKey()?.getParams()).toEqual(keyFormat.getParams());
-      // The keys are tested more in tests for getPrimitive method below, where
-      // the primitive based on the created key is tested.
-    }
-  });
-
-  it('new key data, invalid serialized key format', async function() {
-    const serializedKeyFormats = [new Uint8Array(1), new Uint8Array(0)];
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    const serializedKeyFormatsLength = serializedKeyFormats.length;
-    for (let i = 0; i < serializedKeyFormatsLength; i++) {
-      try {
-        await manager.getKeyFactory().newKeyData(serializedKeyFormats[i]);
-        fail(
-            'An exception should be thrown for the string: ' +
-            serializedKeyFormats[i]);
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
-        continue;
-      }
-    }
-  });
-
-  it('new key data, from valid key format', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    for (const keyFormat of keyFormats) {
-      const serializedKeyFormat = keyFormat.serializeBinary();
-      const keyData =
-          await manager.getKeyFactory().newKeyData(serializedKeyFormat);
-      expect(keyData.getTypeUrl()).toBe(PRIVATE_KEY_TYPE);
-      expect(keyData.getKeyMaterialType()).toBe(PRIVATE_KEY_MATERIAL_TYPE);
-
-      const key =
-          PbEciesAeadHkdfPrivateKey.deserializeBinary(keyData.getValue_asU8());
-      assertMessageEquals(
-          key.getPublicKey()?.getParams()!, keyFormat.getParams()!);
-      // The keys are tested more in tests for getPrimitive method below, where
-      // the primitive based on the created key is tested.
-    }
-  });
-
-  it('get public key data, invalid private key serialization', function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-
-    const privateKey = new Uint8Array([0, 1]);  // not a serialized private key
-    try {
-      const factory = manager.getKeyFactory();
-      factory.getPublicKeyData(privateKey);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidSerializedKey());
-    }
-  });
-
-  it('get public key data, should work', async function() {
-    const keyFormat = createKeyFormat();
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const privateKey = await manager.getKeyFactory().newKey(keyFormat);
-    const factory = manager.getKeyFactory();
-    const publicKeyData =
-        factory.getPublicKeyData(privateKey.serializeBinary());
-
-    expect(publicKeyData.getTypeUrl()).toBe(PUBLIC_KEY_TYPE);
-    expect(publicKeyData.getKeyMaterialType()).toBe(PUBLIC_KEY_MATERIAL_TYPE);
-    const publicKey = PbEciesAeadHkdfPublicKey.deserializeBinary(
-        publicKeyData.getValue_asU8());
-    expect(publicKey.getVersion())
-        .toEqual(assertExists(privateKey.getPublicKey()).getVersion());
-    assertMessageEquals(
-        publicKey.getParams()!,
-        assertExists(privateKey.getPublicKey()).getParams()!);
-    expect(publicKey.getX_asU8())
-        .toEqual(assertExists(privateKey.getPublicKey()).getX_asU8());
-    expect(publicKey.getY_asU8())
-        .toEqual(assertExists(privateKey.getPublicKey()).getY_asU8());
-  });
-
-  it('get primitive, unsupported key data type', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const keyFormat = createKeyFormat();
-    const keyData =
-        (await manager.getKeyFactory().newKeyData(keyFormat.serializeBinary()))
-            .setTypeUrl('unsupported_key_type_url');
-
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, keyData);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyType(keyData.getTypeUrl()));
-    }
-  });
-
-  it('get primitive, unsupported key type', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const key = new PbEciesAeadHkdfPublicKey();
-
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyType());
-    }
-  });
-
-  it('get primitive, high version', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const version = manager.getVersion() + 1;
-    const keyFormat = createKeyFormat();
-    const key = assertInstanceof(
-                    await manager.getKeyFactory().newKey(keyFormat),
-                    PbEciesAeadHkdfPrivateKey)
-                    .setVersion(version);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.versionOutOfBounds());
-    }
-  });
-
-  it('get primitive, invalid params', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const keyFormat = createKeyFormat();
-    const key = assertInstanceof(
-        await manager.getKeyFactory().newKey(keyFormat),
-        PbEciesAeadHkdfPrivateKey);
-
-    // missing KEM params
-    key.getPublicKey()?.getParams()?.setKemParams(null);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKemParams());
-    }
-    key.getPublicKey()?.getParams()?.setKemParams(createKemParams());
-
-    // unsupported AEAD key template type URL
-    const templateTypeUrl = 'UNSUPPORTED_KEY_TYPE_URL';
-    key.getPublicKey()?.getParams()?.getDemParams()?.getAeadDem()?.setTypeUrl(
-        templateTypeUrl);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyTemplate(templateTypeUrl));
-    }
-  });
-
-  it('get primitive, invalid serialized key', async function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const keyFormat = createKeyFormat();
-    const keyData =
-        await manager.getKeyFactory().newKeyData(keyFormat.serializeBinary());
-
-
-    for (let i = 0; i < 2; ++i) {
-      // Set the value of keyData to something which is not a serialization of a
-      // proper key.
-      keyData.setValue(new Uint8Array(i));
-      try {
-        await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, keyData);
-        fail('An exception should be thrown ' + i.toString());
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerializedKey());
-      }
-    }
-  });
-
-  it('get primitive, from key', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const privateKeyManager = new EciesAeadHkdfPrivateKeyManager();
-    const publicKeyManager = new EciesAeadHkdfPublicKeyManager();
-
-    for (const keyFormat of keyFormats) {
-      const key = assertInstanceof(
-          await privateKeyManager.getKeyFactory().newKey(keyFormat),
-          PbEciesAeadHkdfPrivateKey);
-      const hybridEncrypt: HybridEncrypt =
-          assertExists(await publicKeyManager.getPrimitive(
-              PUBLIC_KEY_MANAGER_PRIMITIVE, assertExists(key.getPublicKey())));
-      const hybridDecrypt: HybridDecrypt =
-          assertExists(await privateKeyManager.getPrimitive(
-              PRIVATE_KEY_MANAGER_PRIMITIVE, key));
-
-      const plaintext = Random.randBytes(10);
-      const ciphertext = await hybridEncrypt.encrypt(plaintext);
-      const decryptedCiphertext = await hybridDecrypt.decrypt(ciphertext);
-
-      expect(decryptedCiphertext).toEqual(plaintext);
-    }
-  });
-
-  it('get primitive, from key data', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const privateKeyManager = new EciesAeadHkdfPrivateKeyManager();
-    const publicKeyManager = new EciesAeadHkdfPublicKeyManager();
-
-    for (const keyFormat of keyFormats) {
-      const serializedKeyFormat = keyFormat.serializeBinary();
-      const keyData = await privateKeyManager.getKeyFactory().newKeyData(
-          serializedKeyFormat);
-      const factory = privateKeyManager.getKeyFactory();
-      const publicKeyData = factory.getPublicKeyData(keyData.getValue_asU8());
-      const hybridEncrypt: HybridEncrypt =
-          assertExists(await publicKeyManager.getPrimitive(
-              PUBLIC_KEY_MANAGER_PRIMITIVE, publicKeyData));
-      const hybridDecrypt: HybridDecrypt =
-          assertExists(await privateKeyManager.getPrimitive(
-              PRIVATE_KEY_MANAGER_PRIMITIVE, keyData));
-
-      const plaintext = Random.randBytes(10);
-      const ciphertext = await hybridEncrypt.encrypt(plaintext);
-      const decryptedCiphertext = await hybridDecrypt.decrypt(ciphertext);
-
-      expect(decryptedCiphertext).toEqual(plaintext);
-    }
-  });
-
-  it('does support', function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    expect(manager.doesSupport(PRIVATE_KEY_TYPE)).toBe(true);
-  });
-
-  it('get key type', function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    expect(manager.getKeyType()).toBe(PRIVATE_KEY_TYPE);
-  });
-
-  it('get primitive type', function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    expect(manager.getPrimitiveType()).toBe(PRIVATE_KEY_MANAGER_PRIMITIVE);
-  });
-
-  it('get version', function() {
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    expect(manager.getVersion()).toBe(VERSION);
-  });
-});
-
-// Helper classes and functions
-class ExceptionText {
-  static nullKeyFormat(): string {
-    return 'SecurityException: Key format has to be non-null.';
-  }
-
-  static invalidSerializedKeyFormat(): string {
-    return 'SecurityException: Input cannot be parsed as ' + PRIVATE_KEY_TYPE +
-        ' key format proto.';
-  }
-
-  static unsupportedPrimitive(): string {
-    return 'SecurityException: Requested primitive type which is not supported by ' +
-        'this key manager.';
-  }
-
-  static unsupportedKeyFormat(): string {
-    return 'SecurityException: Expected ' + PRIVATE_KEY_TYPE +
-        ' key format proto.';
-  }
-
-  static unsupportedKeyType(opt_requestedKeyType?: string): string {
-    const prefix = 'SecurityException: Key type';
-    const suffix =
-        'is not supported. This key manager supports ' + PRIVATE_KEY_TYPE + '.';
-    if (opt_requestedKeyType) {
-      return prefix + ' ' + opt_requestedKeyType + ' ' + suffix;
-    } else {
-      return prefix + ' ' + suffix;
-    }
-  }
-
-  static versionOutOfBounds(): string {
-    return 'SecurityException: Version is out of bound, must be between 0 and ' +
-        VERSION + '.';
-  }
-
-  static invalidKeyFormatMissingParams(): string {
-    return 'SecurityException: Invalid key format - missing key params.';
-  }
-
-  static unknownPointFormat(): string {
-    return 'SecurityException: Invalid key params - unknown EC point format.';
-  }
-
-  static missingKemParams(): string {
-    return 'SecurityException: Invalid params - missing KEM params.';
-  }
-
-  static unsupportedKeyTemplate(templateTypeUrl: string): string {
-    return 'SecurityException: Invalid DEM params - ' + templateTypeUrl +
-        ' template is not supported by ECIES AEAD HKDF.';
-  }
-
-  static invalidSerializedKey(): string {
-    return 'SecurityException: Input cannot be parsed as ' + PRIVATE_KEY_TYPE +
-        ' key-proto.';
-  }
-}
-
-function createKemParams(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType: PbHashType = PbHashType.SHA256): PbEciesHkdfKemParams {
-  const kemParams = new PbEciesHkdfKemParams()
-                        .setCurveType(opt_curveType)
-                        .setHkdfHashType(opt_hashType);
-
-  return kemParams;
-}
-
-function createDemParams(opt_keyTemplate?: PbKeyTemplate):
-    PbEciesAeadDemParams {
-  if (!opt_keyTemplate) {
-    opt_keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-  }
-
-  const demParams = new PbEciesAeadDemParams().setAeadDem(opt_keyTemplate);
-
-  return demParams;
-}
-
-function createKeyParams(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat: PbPointFormat =
-        PbPointFormat.UNCOMPRESSED): PbEciesAeadHkdfParams {
-  const params = new PbEciesAeadHkdfParams()
-                     .setKemParams(createKemParams(opt_curveType, opt_hashType))
-                     .setDemParams(createDemParams(opt_keyTemplate))
-                     .setEcPointFormat(opt_pointFormat);
-
-  return params;
-}
-
-function createKeyFormat(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat?: PbPointFormat): PbEciesAeadHkdfKeyFormat {
-  const keyFormat = new PbEciesAeadHkdfKeyFormat().setParams(createKeyParams(
-      opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat));
-  return keyFormat;
-}
-
-/**
- * Create set of key formats with all possible predefined/supported parameters.
- */
-function createTestSetOfKeyFormats(): PbEciesAeadHkdfKeyFormat[] {
-  const curveTypes = [
-    PbEllipticCurveType.NIST_P256, PbEllipticCurveType.NIST_P384,
-    PbEllipticCurveType.NIST_P521
-  ];
-  const hashTypes = [PbHashType.SHA1, PbHashType.SHA256, PbHashType.SHA512];
-  const keyTemplates = [
-    AeadKeyTemplates.aes128CtrHmacSha256(),
-    AeadKeyTemplates.aes256CtrHmacSha256()
-  ];
-  const pointFormats = [PbPointFormat.UNCOMPRESSED];
-  const keyFormats: PbEciesAeadHkdfKeyFormat[] = [];
-  for (const curve of curveTypes) {
-    for (const hkdfHash of hashTypes) {
-      for (const keyTemplate of keyTemplates) {
-        for (const pointFormat of pointFormats) {
-          const keyFormat =
-              createKeyFormat(curve, hkdfHash, keyTemplate, pointFormat);
-          keyFormats.push(keyFormat);
-        }
-      }
-    }
-  }
-  return keyFormats;
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_public_key_manager.ts b/javascript/hybrid/ecies_aead_hkdf_public_key_manager.ts
deleted file mode 100644
index dca7dd1..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_public_key_manager.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as KeyManager from '../internal/key_manager';
-import {PbEciesAeadHkdfParams, PbEciesAeadHkdfPublicKey, PbKeyData, PbKeyTemplate, PbMessage} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as eciesAeadHkdfHybridEncrypt from '../subtle/ecies_aead_hkdf_hybrid_encrypt';
-
-import * as EciesAeadHkdfUtil from './ecies_aead_hkdf_util';
-import * as EciesAeadHkdfValidators from './ecies_aead_hkdf_validators';
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-import {RegistryEciesAeadHkdfDemHelper} from './registry_ecies_aead_hkdf_dem_helper';
-
-/**
- * @final
- */
-class EciesAeadHkdfPublicKeyFactory implements KeyManager.KeyFactory {
-  newKey(keyFormat: PbMessage|Uint8Array): never {
-    throw new SecurityException(
-        'This operation is not supported for public keys. ' +
-        'Use EciesAeadHkdfPrivateKeyManager to generate new keys.');
-  }
-
-  newKeyData(serializedKeyFormat: Uint8Array): never {
-    throw new SecurityException(
-        'This operation is not supported for public keys. ' +
-        'Use EciesAeadHkdfPrivateKeyManager to generate new keys.');
-  }
-}
-
-/**
- * @final
- */
-export class EciesAeadHkdfPublicKeyManager implements
-    KeyManager.KeyManager<HybridEncrypt> {
-  static KEY_TYPE: string =
-      'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey';
-  private static readonly SUPPORTED_PRIMITIVE = HybridEncrypt;
-  static VERSION: number = 0;
-  keyFactory = new EciesAeadHkdfPublicKeyFactory();
-
-  async getPrimitive(
-      primitiveType: Util.Constructor<HybridEncrypt>,
-      key: PbKeyData|PbMessage) {
-    if (primitiveType !== this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    const keyProto = EciesAeadHkdfPublicKeyManager.getKeyProto(key);
-    EciesAeadHkdfValidators.validatePublicKey(keyProto, this.getVersion());
-    const recepientPublicKey =
-        EciesAeadHkdfUtil.getJsonWebKeyFromProto(keyProto);
-    const params = (keyProto.getParams() as PbEciesAeadHkdfParams);
-    const demParams = params.getDemParams();
-    if (!demParams) {
-      throw new SecurityException('DEM params not set');
-    }
-    const keyTemplate = (demParams.getAeadDem() as PbKeyTemplate);
-    const demHelper = new RegistryEciesAeadHkdfDemHelper(keyTemplate);
-    const pointFormat =
-        Util.pointFormatProtoToSubtle(params.getEcPointFormat());
-    const kemParams = params.getKemParams();
-    if (!kemParams) {
-      throw new SecurityException('KEM params not set');
-    }
-    const hkdfHash = Util.hashTypeProtoToString(kemParams.getHkdfHashType());
-    const hkdfSalt = kemParams.getHkdfSalt_asU8();
-    return eciesAeadHkdfHybridEncrypt.fromJsonWebKey(
-        recepientPublicKey, hkdfHash, pointFormat, demHelper, hkdfSalt);
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return EciesAeadHkdfPublicKeyManager.KEY_TYPE;
-  }
-
-  getPrimitiveType() {
-    return EciesAeadHkdfPublicKeyManager.SUPPORTED_PRIMITIVE;
-  }
-
-  getVersion() {
-    return EciesAeadHkdfPublicKeyManager.VERSION;
-  }
-
-  getKeyFactory() {
-    return this.keyFactory;
-  }
-
-  private static getKeyProto(keyMaterial: PbKeyData|
-                             PbMessage): PbEciesAeadHkdfPublicKey {
-    if (keyMaterial instanceof PbKeyData) {
-      return EciesAeadHkdfPublicKeyManager.getKeyProtoFromKeyData(keyMaterial);
-    }
-    if (keyMaterial instanceof PbEciesAeadHkdfPublicKey) {
-      return keyMaterial;
-    }
-    throw new SecurityException(
-        'Key type is not supported. This key manager supports ' +
-        EciesAeadHkdfPublicKeyManager.KEY_TYPE + '.');
-  }
-
-  private static getKeyProtoFromKeyData(keyData: PbKeyData):
-      PbEciesAeadHkdfPublicKey {
-    if (keyData.getTypeUrl() !== EciesAeadHkdfPublicKeyManager.KEY_TYPE) {
-      throw new SecurityException(
-          'Key type ' + keyData.getTypeUrl() + ' is not supported. This key ' +
-          'manager supports ' + EciesAeadHkdfPublicKeyManager.KEY_TYPE + '.');
-    }
-    let key: PbEciesAeadHkdfPublicKey;
-    try {
-      key = PbEciesAeadHkdfPublicKey.deserializeBinary(keyData.getValue_asU8());
-    } catch (e) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' +
-          EciesAeadHkdfPublicKeyManager.KEY_TYPE + ' key-proto.');
-    }
-    if (!key.getParams() || !key.getX_asU8() || !key.getY_asU8()) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' +
-          EciesAeadHkdfPublicKeyManager.KEY_TYPE + ' key-proto.');
-    }
-    return key;
-  }
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_public_key_manager_test.ts b/javascript/hybrid/ecies_aead_hkdf_public_key_manager_test.ts
deleted file mode 100644
index ab067f3..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_public_key_manager_test.ts
+++ /dev/null
@@ -1,421 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {PbAesCtrKey, PbEciesAeadDemParams, PbEciesAeadHkdfParams, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyData, PbKeyTemplate, PbPointFormat} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Util from '../internal/util';
-import * as Bytes from '../subtle/bytes';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-import * as Random from '../subtle/random';
-import {assertExists} from '../testing/internal/test_utils';
-
-import {EciesAeadHkdfPublicKeyManager} from './ecies_aead_hkdf_public_key_manager';
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-
-const KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey';
-const VERSION = 0;
-const PRIMITIVE = HybridEncrypt;
-
-describe('ecies aead hkdf public key manager test', function() {
-  beforeEach(function() {
-    AeadConfig.register();
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('new key', function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-
-    try {
-      manager.getKeyFactory().newKey(new Uint8Array(0));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.notSupported());
-    }
-  });
-
-  it('new key data', function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-
-    try {
-      manager.getKeyFactory().newKeyData(new Uint8Array(0));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.notSupported());
-    }
-  });
-
-  it('get primitive, unsupported key data type', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const keyData =
-        (await createKeyData()).setTypeUrl('unsupported_key_type_url');
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, keyData);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyType(keyData.getTypeUrl()));
-    }
-  });
-
-  it('get primitive, unsupported key type', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const key = new PbAesCtrKey();
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyType());
-    }
-  });
-
-  it('get primitive, high version', async function() {
-    const version = 1;
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const key = (await createKey()).setVersion(version);
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.versionOutOfBounds());
-    }
-  });
-
-  it('get primitive, missing params', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const key = (await createKey()).setParams(null);
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingParams());
-    }
-  });
-
-  it('get primitive, invalid params', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const key = await createKey();
-
-    // unknown point format
-    key.getParams()?.setEcPointFormat(PbPointFormat.UNKNOWN_FORMAT);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownPointFormat());
-    }
-    key.getParams()?.setEcPointFormat(PbPointFormat.UNCOMPRESSED);
-
-    // missing KEM params
-    key.getParams()?.setKemParams(null);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKemParams());
-    }
-    key.getParams()?.setKemParams(createKemParams());
-
-    // unsupported AEAD key template
-    const typeUrl = 'UNSUPPORTED_KEY_TYPE_URL';
-    key.getParams()?.getDemParams()?.getAeadDem()?.setTypeUrl(typeUrl);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyTemplate(typeUrl));
-    }
-  });
-
-  it('get primitive, invalid key', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const key = (await createKey()).setX('');
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingXY());
-    }
-    key.getParams()?.setEcPointFormat(PbPointFormat.UNCOMPRESSED);
-
-    // missing KEM params
-    key.getParams()?.setKemParams(null);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKemParams());
-    }
-    key.getParams()?.setKemParams(createKemParams());
-
-    // unsupported AEAD key template
-    const typeUrl = 'UNSUPPORTED_KEY_TYPE_URL';
-    key.getParams()?.getDemParams()?.getAeadDem()?.setTypeUrl(typeUrl);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyTemplate(typeUrl));
-    }
-  });
-
-  it('get primitive, invalid serialized key', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const keyData = await createKeyData();
-
-    for (let i = 0; i < 2; ++i) {
-      // Set the value of keyData to something which is not a serialization of a
-      // proper key.
-      keyData.setValue(new Uint8Array(i));
-      try {
-        await manager.getPrimitive(PRIMITIVE, keyData);
-        fail('An exception should be thrown ' + i.toString());
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerializedKey());
-      }
-    }
-  });
-
-  // tests for getting primitive from valid key/keyData
-  it('get primitive, from key', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const keys = await createTestSetOfKeys();
-    for (const key of keys) {
-      const primitive: HybridEncrypt =
-          assertExists(await manager.getPrimitive(PRIMITIVE, key));
-
-      const plaintext = Random.randBytes(10);
-      const ciphertext = await primitive.encrypt(plaintext);
-
-      expect(ciphertext).not.toEqual(plaintext);
-    }
-  });
-
-  it('get primitive, from key data', async function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-    const keyDatas = await createTestSetOfKeyDatas();
-
-    for (const key of keyDatas) {
-      const primitive: HybridEncrypt =
-          assertExists(await manager.getPrimitive(PRIMITIVE, key));
-
-      const plaintext = Random.randBytes(10);
-      const ciphertext = await primitive.encrypt(plaintext);
-
-      expect(ciphertext).not.toEqual(plaintext);
-    }
-  });
-
-  it('does support', function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-
-    expect(manager.doesSupport(KEY_TYPE)).toBe(true);
-  });
-
-  it('get key type', function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-
-    expect(manager.getKeyType()).toBe(KEY_TYPE);
-  });
-
-  it('get primitive type', function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-
-    expect(manager.getPrimitiveType()).toBe(PRIMITIVE);
-  });
-
-  it('get version', function() {
-    const manager = new EciesAeadHkdfPublicKeyManager();
-
-    expect(manager.getVersion()).toBe(VERSION);
-  });
-});
-
-// Helper classes and functions
-class ExceptionText {
-  static notSupported(): string {
-    return 'SecurityException: This operation is not supported for public keys. ' +
-        'Use EciesAeadHkdfPrivateKeyManager to generate new keys.';
-  }
-
-  static unsupportedPrimitive(): string {
-    return 'SecurityException: Requested primitive type which is not supported by ' +
-        'this key manager.';
-  }
-
-  static unsupportedKeyType(opt_requestedKeyType?: string): string {
-    const prefix = 'SecurityException: Key type';
-    const suffix =
-        'is not supported. This key manager supports ' + KEY_TYPE + '.';
-    if (opt_requestedKeyType) {
-      return prefix + ' ' + opt_requestedKeyType + ' ' + suffix;
-    } else {
-      return prefix + ' ' + suffix;
-    }
-  }
-
-  static versionOutOfBounds(): string {
-    return 'SecurityException: Version is out of bound, must be between 0 and ' +
-        VERSION + '.';
-  }
-
-  static missingParams(): string {
-    return 'SecurityException: Invalid public key - missing key params.';
-  }
-
-  static unknownPointFormat(): string {
-    return 'SecurityException: Invalid key params - unknown EC point format.';
-  }
-
-  static missingKemParams(): string {
-    return 'SecurityException: Invalid params - missing KEM params.';
-  }
-
-  static unsupportedKeyTemplate(templateTypeUrl: string): string {
-    return 'SecurityException: Invalid DEM params - ' + templateTypeUrl +
-        ' template is not supported by ECIES AEAD HKDF.';
-  }
-
-  static missingXY(): string {
-    return 'SecurityException: Invalid public key - missing value of X or Y.';
-  }
-
-  static invalidSerializedKey(): string {
-    return 'SecurityException: Input cannot be parsed as ' + KEY_TYPE +
-        ' key-proto.';
-  }
-}
-
-function createKemParams(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType: PbHashType = PbHashType.SHA256): PbEciesHkdfKemParams {
-  const kemParams = new PbEciesHkdfKemParams()
-                        .setCurveType(opt_curveType)
-                        .setHkdfHashType(opt_hashType);
-
-  return kemParams;
-}
-
-function createDemParams(opt_keyTemplate?: PbKeyTemplate):
-    PbEciesAeadDemParams {
-  if (!opt_keyTemplate) {
-    opt_keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-  }
-
-  const demParams = new PbEciesAeadDemParams().setAeadDem(opt_keyTemplate);
-
-  return demParams;
-}
-
-function createKeyParams(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat: PbPointFormat =
-        PbPointFormat.UNCOMPRESSED): PbEciesAeadHkdfParams {
-  const params = new PbEciesAeadHkdfParams()
-                     .setKemParams(createKemParams(opt_curveType, opt_hashType))
-                     .setDemParams(createDemParams(opt_keyTemplate))
-                     .setEcPointFormat(opt_pointFormat);
-
-  return params;
-}
-
-async function createKey(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType?: PbHashType, opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPublicKey> {
-  const curveSubtleType = Util.curveTypeProtoToSubtle(opt_curveType);
-  const curveName = EllipticCurves.curveToString(curveSubtleType);
-
-  const key =
-      new PbEciesAeadHkdfPublicKey().setVersion(0).setParams(createKeyParams(
-          opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat));
-
-
-  const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
-  const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-  key.setX(
-      Bytes.fromBase64(assertExists(publicKey['x']), /* opt_webSafe = */ true));
-  key.setY(
-      Bytes.fromBase64(assertExists(publicKey['y']), /* opt_webSafe = */ true));
-
-  return key;
-}
-
-function createKeyDataFromKey(key: PbEciesAeadHkdfPublicKey): PbKeyData {
-  const keyData =
-      new PbKeyData()
-          .setTypeUrl(KEY_TYPE)
-          .setValue(key.serializeBinary())
-          .setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC);
-
-  return keyData;
-}
-
-async function createKeyData(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat?: PbPointFormat): Promise<PbKeyData> {
-  const key = await createKey(
-      opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat);
-  return createKeyDataFromKey(key);
-}
-
-/** Create set of keys with all possible predefined/supported parameters. */
-async function createTestSetOfKeys(): Promise<PbEciesAeadHkdfPublicKey[]> {
-  const curveTypes = [
-    PbEllipticCurveType.NIST_P256, PbEllipticCurveType.NIST_P384,
-    PbEllipticCurveType.NIST_P521
-  ];
-  const hashTypes = [PbHashType.SHA1, PbHashType.SHA256, PbHashType.SHA512];
-  const keyTemplates =
-      [AeadKeyTemplates.aes128CtrHmacSha256(), AeadKeyTemplates.aes256Gcm()];
-  const pointFormats = [PbPointFormat.UNCOMPRESSED];
-
-  const keys: PbEciesAeadHkdfPublicKey[] = [];
-  for (const curve of curveTypes) {
-    for (const hkdfHash of hashTypes) {
-      for (const keyTemplate of keyTemplates) {
-        for (const pointFormat of pointFormats) {
-          const key =
-              await createKey(curve, hkdfHash, keyTemplate, pointFormat);
-          keys.push(key);
-        }
-      }
-    }
-  }
-  return keys;
-}
-
-/**
- * Create set of keyData protos with keys of all possible predefined/supported
- * parameters.
- */
-async function createTestSetOfKeyDatas(): Promise<PbKeyData[]> {
-  const keys = await createTestSetOfKeys();
-
-  const keyDatas: PbKeyData[] = [];
-  for (const key of keys) {
-    const keyData = await createKeyDataFromKey(key);
-    keyDatas.push(keyData);
-  }
-
-  return keyDatas;
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_util.ts b/javascript/hybrid/ecies_aead_hkdf_util.ts
deleted file mode 100644
index 2558422..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_util.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-
-// This file contains only functions which are useful for implementation of
-// private and public ECIES AEAD HKDF key manager.
-
-/**
- * WARNING: This method assumes that the given key proto is valid.
- *
- */
-export function getJsonWebKeyFromProto(key: PbEciesAeadHkdfPrivateKey|
-                                       PbEciesAeadHkdfPublicKey): JsonWebKey {
-  let publicKey: PbEciesAeadHkdfPublicKey;
-  let d: Uint8Array|null = null;
-  if (key instanceof PbEciesAeadHkdfPrivateKey) {
-    publicKey = (key.getPublicKey() as PbEciesAeadHkdfPublicKey);
-  } else {
-    publicKey = key;
-  }
-  const params = publicKey.getParams();
-  if (!params) {
-    throw new SecurityException('Params not set');
-  }
-  const kemParams = params.getKemParams();
-  if (!kemParams) {
-    throw new SecurityException('KEM params not set');
-  }
-  const curveType = Util.curveTypeProtoToSubtle(kemParams.getCurveType());
-  const expectedLength = EllipticCurves.fieldSizeInBytes(curveType);
-  const x = Util.bigEndianNumberToCorrectLength(
-      publicKey.getX_asU8(), expectedLength);
-  const y = Util.bigEndianNumberToCorrectLength(
-      publicKey.getY_asU8(), expectedLength);
-  if (key instanceof PbEciesAeadHkdfPrivateKey) {
-    d = Util.bigEndianNumberToCorrectLength(
-        key.getKeyValue_asU8(), expectedLength);
-  }
-  return EllipticCurves.getJsonWebKey(curveType, x, y, d);
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_util_test.ts b/javascript/hybrid/ecies_aead_hkdf_util_test.ts
deleted file mode 100644
index 2dd809b..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_util_test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {PbEciesAeadDemParams, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyTemplate, PbPointFormat} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as Bytes from '../subtle/bytes';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-import {assertExists} from '../testing/internal/test_utils';
-
-import * as EciesAeadHkdfUtil from './ecies_aead_hkdf_util';
-
-describe('ecies aead hkdf util test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('get json web key from proto, public key', async function() {
-    for (const curve
-             of [PbEllipticCurveType.NIST_P256,
-                 PbEllipticCurveType.NIST_P384,
-                 PbEllipticCurveType.NIST_P521,
-    ]) {
-      const key = await createKey(curve);
-      const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(
-          assertExists(key.getPublicKey()));
-
-      // Test the returned jwk.
-      const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
-      const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
-      expect(jwk?.['kty']).toBe('EC');
-      expect(jwk?.['crv']).toBe(curveTypeString);
-      expect(Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
-          .toEqual(assertExists(key.getPublicKey()).getX_asU8());
-      expect(Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
-          .toEqual(assertExists(key.getPublicKey()).getY_asU8());
-      expect(jwk?.['d']).toEqual(undefined);
-      expect(jwk?.['ext']).toBe(true);
-    }
-  });
-
-  it('get json web key from proto, public key, with leading zeros',
-     async function() {
-       for (const curve
-                of [PbEllipticCurveType.NIST_P256,
-                    PbEllipticCurveType.NIST_P384,
-                    PbEllipticCurveType.NIST_P521,
-       ]) {
-         const key = await createKey(curve);
-
-         // Add leading zeros to x and y value of key.
-         const x = assertExists(key.getPublicKey()).getX_asU8();
-         const y = assertExists(key.getPublicKey()).getY_asU8();
-         key.getPublicKey()?.setX(
-             Bytes.concat(new Uint8Array([0, 0, 0, 0, 0]), x));
-         key.getPublicKey()?.setY(Bytes.concat(new Uint8Array([0, 0, 0]), y));
-         const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(
-             assertExists(key.getPublicKey()));
-
-         // Test the returned jwk.
-         const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
-         const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
-         expect(jwk?.['kty']).toBe('EC');
-         expect(jwk?.['crv']).toBe(curveTypeString);
-         expect(
-             Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
-             .toEqual(x);
-         expect(
-             Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
-             .toEqual(y);
-         expect(jwk?.['d']).toEqual(undefined);
-         expect(jwk?.['ext']).toBe(true);
-       }
-     });
-
-  it('get json web key from proto, public key, leading nonzero',
-     async function() {
-       const curve = PbEllipticCurveType.NIST_P256;
-       const key = await createKey(curve);
-       const publicKey = assertExists(key.getPublicKey());
-       const x = publicKey.getX_asU8();
-       publicKey.setX(Bytes.concat(new Uint8Array([1, 0]), x));
-       try {
-         EciesAeadHkdfUtil.getJsonWebKeyFromProto(publicKey);
-         fail('An exception should be thrown.');
-       } catch (e: any) {
-         expect(e.toString())
-             .toBe(
-                 'SecurityException: Number needs more bytes to be represented.');
-       }
-     });
-
-  it('get json web key from proto, private key', async function() {
-    for (const curve
-             of [PbEllipticCurveType.NIST_P256,
-                 PbEllipticCurveType.NIST_P384,
-                 PbEllipticCurveType.NIST_P521,
-    ]) {
-      const key = await createKey(curve);
-      const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(key);
-
-      // Test the returned jwk.
-      const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
-      const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
-      const publicKey = assertExists(key.getPublicKey());
-      expect(jwk?.['kty']).toBe('EC');
-      expect(jwk?.['crv']).toBe(curveTypeString);
-      expect(Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
-          .toEqual(publicKey.getX_asU8());
-      expect(Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
-          .toEqual(publicKey.getY_asU8());
-      expect(Bytes.fromBase64(assertExists(jwk['d']), /* opt_webSafe = */ true))
-          .toEqual(key.getKeyValue_asU8());
-      expect(jwk?.['ext']).toBe(true);
-    }
-  });
-
-  it('get json web key from proto, private key, leading zeros',
-     async function() {
-       for (const curve
-                of [PbEllipticCurveType.NIST_P256,
-                    PbEllipticCurveType.NIST_P384,
-                    PbEllipticCurveType.NIST_P521,
-       ]) {
-         const key = await createKey(curve);
-         const d = key.getKeyValue_asU8();
-         key.setKeyValue(Bytes.concat(new Uint8Array([0, 0, 0]), d));
-         const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(key);
-
-         // Test the returned jwk.
-         const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
-         const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
-
-         const publicKey = assertExists(key.getPublicKey());
-         expect(jwk?.['kty']).toBe('EC');
-         expect(jwk?.['crv']).toBe(curveTypeString);
-         expect(
-             Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
-             .toEqual(publicKey.getX_asU8());
-         expect(
-             Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
-             .toEqual(publicKey.getY_asU8());
-         expect(
-             Bytes.fromBase64(assertExists(jwk['d']), /* opt_webSafe = */ true))
-             .toEqual(d);
-         expect(jwk['ext']).toBe(true);
-       }
-     });
-});
-
-function createKemParams(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType: PbHashType = PbHashType.SHA256): PbEciesHkdfKemParams {
-  const kemParams = new PbEciesHkdfKemParams()
-                        .setCurveType(opt_curveType)
-                        .setHkdfHashType(opt_hashType);
-
-  return kemParams;
-}
-
-function createDemParams(opt_keyTemplate?: PbKeyTemplate):
-    PbEciesAeadDemParams {
-  if (!opt_keyTemplate) {
-    opt_keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-  }
-
-  const demParams = new PbEciesAeadDemParams().setAeadDem(opt_keyTemplate);
-
-  return demParams;
-}
-
-function createKeyParams(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat: PbPointFormat =
-        PbPointFormat.UNCOMPRESSED): PbEciesAeadHkdfParams {
-  const params = new PbEciesAeadHkdfParams()
-                     .setKemParams(createKemParams(opt_curveType, opt_hashType))
-                     .setDemParams(createDemParams(opt_keyTemplate))
-                     .setEcPointFormat(opt_pointFormat);
-
-  return params;
-}
-
-async function createKey(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType?: PbHashType, opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPrivateKey> {
-  const curveTypeSubtle = Util.curveTypeProtoToSubtle((opt_curveType));
-  const curveName = EllipticCurves.curveToString(curveTypeSubtle);
-
-  const publicKeyProto =
-      new PbEciesAeadHkdfPublicKey().setVersion(0).setParams(createKeyParams(
-          opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat));
-
-
-  const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
-  const publicKeyJson =
-      await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-  publicKeyProto.setX(Bytes.fromBase64(
-      assertExists(publicKeyJson['x']), /* opt_webSafe = */ true));
-  publicKeyProto.setY(Bytes.fromBase64(
-      assertExists(publicKeyJson['y']), /* opt_webSafe = */ true));
-
-  const privateKeyProto = new PbEciesAeadHkdfPrivateKey();
-  const privateKeyJson =
-      await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-  privateKeyProto.setKeyValue(Bytes.fromBase64(
-      assertExists(privateKeyJson['d']), /* opt_webSafe = */ true));
-  privateKeyProto.setVersion(0);
-  privateKeyProto.setPublicKey(publicKeyProto);
-
-  return privateKeyProto;
-}
diff --git a/javascript/hybrid/ecies_aead_hkdf_validators.ts b/javascript/hybrid/ecies_aead_hkdf_validators.ts
deleted file mode 100644
index 2c2abbd..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_validators.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import {SecurityException} from '../exception/security_exception';
-import {PbEciesAeadDemParams, PbEciesAeadHkdfKeyFormat, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbPointFormat} from '../internal/proto';
-import * as Validators from '../subtle/validators';
-
-function validateKemParams(kemParams: PbEciesHkdfKemParams) {
-  const curve = kemParams.getCurveType();
-  if (curve !== PbEllipticCurveType.NIST_P256 &&
-      curve !== PbEllipticCurveType.NIST_P384 &&
-      curve !== PbEllipticCurveType.NIST_P521) {
-    throw new SecurityException('Invalid KEM params - unknown curve type.');
-  }
-  const hashType = kemParams.getHkdfHashType();
-  if (hashType !== PbHashType.SHA1 && hashType !== PbHashType.SHA256 &&
-      hashType !== PbHashType.SHA384 && hashType !== PbHashType.SHA512) {
-    throw new SecurityException('Invalid KEM params - unknown hash type.');
-  }
-}
-
-function validateDemParams(demParams: PbEciesAeadDemParams) {
-  if (!demParams.getAeadDem()) {
-    throw new SecurityException(
-        'Invalid DEM params - missing AEAD key template.');
-  }
-
-  // It is checked also here due to methods for creating new keys. We do not
-  // allow creating new keys from formats which contains key templates of
-  // not supported key types.
-  const aeadKeyType = demParams.getAeadDem()!.getTypeUrl();
-  if (aeadKeyType != AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL &&
-      aeadKeyType != AeadConfig.AES_GCM_TYPE_URL) {
-    throw new SecurityException(
-        'Invalid DEM params - ' + aeadKeyType +
-        ' template is not supported by ECIES AEAD HKDF.');
-  }
-}
-
-export function validateParams(params: PbEciesAeadHkdfParams) {
-  const kemParams = params.getKemParams();
-  if (!kemParams) {
-    throw new SecurityException('Invalid params - missing KEM params.');
-  }
-  validateKemParams(kemParams);
-  const demParams = params.getDemParams();
-  if (!demParams) {
-    throw new SecurityException('Invalid params - missing DEM params.');
-  }
-  validateDemParams(demParams);
-  const pointFormat = params.getEcPointFormat();
-  if (pointFormat !== PbPointFormat.UNCOMPRESSED &&
-      pointFormat !== PbPointFormat.COMPRESSED &&
-      pointFormat !== PbPointFormat.DO_NOT_USE_CRUNCHY_UNCOMPRESSED) {
-    throw new SecurityException(
-        'Invalid key params - unknown EC point format.');
-  }
-}
-
-export function validateKeyFormat(keyFormat: PbEciesAeadHkdfKeyFormat) {
-  const params = keyFormat.getParams();
-  if (!params) {
-    throw new SecurityException('Invalid key format - missing key params.');
-  }
-  validateParams(params);
-}
-
-export function validatePublicKey(
-    key: PbEciesAeadHkdfPublicKey, publicKeyManagerVersion: number) {
-  Validators.validateVersion(key.getVersion(), publicKeyManagerVersion);
-  const params = key.getParams();
-  if (!params) {
-    throw new SecurityException('Invalid public key - missing key params.');
-  }
-  validateParams(params);
-  if (!key.getX_asU8().length || !key.getY_asU8().length) {
-    throw new SecurityException(
-        'Invalid public key - missing value of X or Y.');
-  }
-}
-
-// TODO Should we add more checks here?
-export function validatePrivateKey(
-    key: PbEciesAeadHkdfPrivateKey, privateKeyManagerVersion: number,
-    publicKeyManagerVersion: number) {
-  Validators.validateVersion(key.getVersion(), privateKeyManagerVersion);
-  if (!key.getKeyValue_asU8()) {
-    throw new SecurityException(
-        'Invalid private key - missing private key value.');
-  }
-  const publicKey = key.getPublicKey();
-  if (!publicKey) {
-    throw new SecurityException(
-        'Invalid private key - missing public key information.');
-  }
-  validatePublicKey(publicKey, publicKeyManagerVersion);
-}
-
-// TODO Should we add more checks here?
diff --git a/javascript/hybrid/ecies_aead_hkdf_validators_test.ts b/javascript/hybrid/ecies_aead_hkdf_validators_test.ts
deleted file mode 100644
index e93ba07..0000000
--- a/javascript/hybrid/ecies_aead_hkdf_validators_test.ts
+++ /dev/null
@@ -1,430 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {PbEciesAeadDemParams, PbEciesAeadHkdfKeyFormat, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyTemplate, PbPointFormat} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as Bytes from '../subtle/bytes';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-import {assertExists} from '../testing/internal/test_utils';
-
-import * as EciesAeadHkdfValidators from './ecies_aead_hkdf_validators';
-
-
-describe('ecies aead hkdf validators test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('validate params, missing kem params', function() {
-    const invalidParams = createParams().setKemParams(null);
-
-    try {
-      EciesAeadHkdfValidators.validateParams(invalidParams);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKemParams());
-    }
-  });
-
-  it('validate params, invalid kem params, unknown hash type', function() {
-    const invalidParams = createParams();
-    invalidParams.getKemParams()?.setHkdfHashType(PbHashType.UNKNOWN_HASH);
-
-    try {
-      EciesAeadHkdfValidators.validateParams(invalidParams);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownHashType());
-    }
-  });
-
-  it('validate params, invalid kem params, unknown curve type', function() {
-    const invalidParams = createParams();
-    invalidParams.getKemParams()?.setCurveType(
-        PbEllipticCurveType.UNKNOWN_CURVE);
-
-    try {
-      EciesAeadHkdfValidators.validateParams(invalidParams);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownCurveType());
-    }
-  });
-
-  it('validate params, missing dem params', function() {
-    const invalidParams = createParams().setDemParams(null);
-
-    try {
-      EciesAeadHkdfValidators.validateParams(invalidParams);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingDemParams());
-    }
-  });
-
-  it('validate params, invalid dem params, missing aead template', function() {
-    const invalidParams = createParams();
-    invalidParams.getDemParams()?.setAeadDem(null);
-
-    try {
-      EciesAeadHkdfValidators.validateParams(invalidParams);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingAeadTemplate());
-    }
-  });
-
-  it('validate params, invalid dem params, unsupported aead template',
-     function() {
-       const unsupportedTypeUrl = 'UNSUPPORTED_KEY_TYPE_URL';
-       const invalidParams = createParams();
-       invalidParams.getDemParams()?.getAeadDem()?.setTypeUrl(
-           unsupportedTypeUrl);
-
-       try {
-         EciesAeadHkdfValidators.validateParams(invalidParams);
-         fail('An exception should be thrown.');
-       } catch (e: any) {
-         expect(e.toString())
-             .toBe(ExceptionText.unsupportedKeyTemplate(unsupportedTypeUrl));
-       }
-     });
-
-  it('validate params, unknown point format', function() {
-    const invalidParams =
-        createParams().setEcPointFormat(PbPointFormat.UNKNOWN_FORMAT);
-
-    try {
-      EciesAeadHkdfValidators.validateParams(invalidParams);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownPointFormat());
-    }
-  });
-  it('validate params, different valid values', function() {
-    for (const curve
-             of [PbEllipticCurveType.NIST_P256,
-                 PbEllipticCurveType.NIST_P384,
-                 PbEllipticCurveType.NIST_P521,
-    ]) {
-      for (const hashType
-               of [PbHashType.SHA1,
-                   PbHashType.SHA384,
-                   PbHashType.SHA256,
-                   PbHashType.SHA512,
-      ]) {
-        for (const keyTemplate
-                 of [AeadKeyTemplates.aes128CtrHmacSha256(),
-                     AeadKeyTemplates.aes128Gcm(),
-        ]) {
-          for (const pointFormat
-                   of [PbPointFormat.UNCOMPRESSED,
-                       PbPointFormat.COMPRESSED,
-                       PbPointFormat.DO_NOT_USE_CRUNCHY_UNCOMPRESSED,
-          ]) {
-            const params =
-                createParams(curve, hashType, keyTemplate, pointFormat);
-            EciesAeadHkdfValidators.validateParams(params);
-          }
-        }
-      }
-    }
-  });
-
-  it('validate key format, missing params', function() {
-    const invalidKeyFormat = new PbEciesAeadHkdfKeyFormat();
-
-    try {
-      EciesAeadHkdfValidators.validateKeyFormat(invalidKeyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingFormatParams());
-    }
-  });
-
-  it('validate key format, invalid params', function() {
-    const invalidKeyFormat =
-        new PbEciesAeadHkdfKeyFormat().setParams(createParams());
-
-    // Check that also params were checked.
-    // Test missing DEM params.
-    invalidKeyFormat.getParams()?.setDemParams(null);
-    try {
-      EciesAeadHkdfValidators.validateKeyFormat(invalidKeyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingDemParams());
-    }
-    invalidKeyFormat.getParams()?.setDemParams(createDemParams());
-
-    // Test UNKNOWN_HASH in KEM params.
-    invalidKeyFormat.getParams()?.getKemParams()?.setHkdfHashType(
-        PbHashType.UNKNOWN_HASH);
-    try {
-      EciesAeadHkdfValidators.validateKeyFormat(invalidKeyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownHashType());
-    }
-  });
-
-  it('validate public key, missing params', function() {
-    const invalidPublicKey = new PbEciesAeadHkdfPublicKey();
-
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKeyParams());
-    }
-  });
-
-  it('validate public key, missing values x y', function() {
-    const invalidPublicKey =
-        new PbEciesAeadHkdfPublicKey().setParams(createParams());
-
-    // Both X and Y are set to empty.
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingXY());
-    }
-
-    // The key with only Y set to empty is also invalid.
-    invalidPublicKey.setX(new Uint8Array(10));
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingXY());
-    }
-
-    // The key with only X set to empty is also invalid.
-    invalidPublicKey.setY(new Uint8Array(10));
-    invalidPublicKey.setX(new Uint8Array(0));
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingXY());
-    }
-  });
-
-  it('validate public key, invalid params', async function() {
-    const invalidPublicKey = await createPublicKey();
-
-    // Check that also params were checked.
-    // Test missing DEM params.
-    invalidPublicKey.getParams()?.setDemParams(null);
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingDemParams());
-    }
-    invalidPublicKey.getParams()?.setDemParams(createDemParams());
-
-    // Test UNKNOWN_HASH in KEM params.
-    invalidPublicKey.getParams()?.getKemParams()?.setHkdfHashType(
-        PbHashType.UNKNOWN_HASH);
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownHashType());
-    }
-  });
-
-  it('validate public key, version out of bounds', async function() {
-    const managerVersion = 0;
-    const invalidPublicKey = (await createPublicKey()).setVersion(1);
-    try {
-      EciesAeadHkdfValidators.validatePublicKey(
-          invalidPublicKey, managerVersion);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.versionOutOfBounds(managerVersion));
-    }
-  });
-
-  it('validate private key, missing public key', async function() {
-    const invalidPrivateKey = (await createPrivateKey()).setPublicKey(null);
-    try {
-      EciesAeadHkdfValidators.validatePrivateKey(invalidPrivateKey, 0, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingPublicKey());
-    }
-  });
-
-  it('validate private key, invalid public key', async function() {
-    const invalidPrivateKey = await createPrivateKey();
-    invalidPrivateKey.getPublicKey()?.setParams(null);
-    try {
-      EciesAeadHkdfValidators.validatePrivateKey(invalidPrivateKey, 0, 0);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingKeyParams());
-    }
-  });
-
-  it('validate private key, should work', async function() {
-    const privateKey = await createPrivateKey();
-    EciesAeadHkdfValidators.validatePrivateKey(privateKey, 0, 0);
-  });
-
-  it('validate private key, version out of bounds', async function() {
-    const managerVersion = 0;
-    const invalidPrivateKey = (await createPrivateKey()).setVersion(1);
-    try {
-      EciesAeadHkdfValidators.validatePrivateKey(
-          invalidPrivateKey, managerVersion, managerVersion);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.versionOutOfBounds(managerVersion));
-    }
-  });
-});
-
-// Helper classes and functions
-class ExceptionText {
-  static missingFormatParams(): string {
-    return 'SecurityException: Invalid key format - missing key params.';
-  }
-
-  static missingKeyParams(): string {
-    return 'SecurityException: Invalid public key - missing key params.';
-  }
-
-  static unknownPointFormat(): string {
-    return 'SecurityException: Invalid key params - unknown EC point format.';
-  }
-
-  static missingKemParams(): string {
-    return 'SecurityException: Invalid params - missing KEM params.';
-  }
-
-  static unknownHashType(): string {
-    return 'SecurityException: Invalid KEM params - unknown hash type.';
-  }
-
-  static unknownCurveType(): string {
-    return 'SecurityException: Invalid KEM params - unknown curve type.';
-  }
-
-  static missingDemParams(): string {
-    return 'SecurityException: Invalid params - missing DEM params.';
-  }
-
-  static missingAeadTemplate(): string {
-    return 'SecurityException: Invalid DEM params - missing AEAD key template.';
-  }
-
-  static unsupportedKeyTemplate(templateTypeUrl: string): string {
-    return 'SecurityException: Invalid DEM params - ' + templateTypeUrl +
-        ' template is not supported by ECIES AEAD HKDF.';
-  }
-
-  static missingXY(): string {
-    return 'SecurityException: Invalid public key - missing value of X or Y.';
-  }
-
-  static missingPublicKey(): string {
-    return 'SecurityException: Invalid private key - missing public key information.';
-  }
-
-  static missingPrivateKeyValue(): string {
-    return 'SecurityException: Invalid private key - missing private key value.';
-  }
-
-  static versionOutOfBounds(version: number): string {
-    return 'SecurityException: Version is out of bound, must be between 0 and ' +
-        version + '.';
-  }
-}
-
-function createKemParams(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType: PbHashType = PbHashType.SHA256): PbEciesHkdfKemParams {
-  const kemParams = new PbEciesHkdfKemParams()
-                        .setCurveType(opt_curveType)
-                        .setHkdfHashType(opt_hashType);
-
-  return kemParams;
-}
-
-function createDemParams(opt_keyTemplate?: PbKeyTemplate):
-    PbEciesAeadDemParams {
-  if (!opt_keyTemplate) {
-    opt_keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-  }
-
-  const demParams = new PbEciesAeadDemParams().setAeadDem(opt_keyTemplate);
-
-  return demParams;
-}
-
-function createParams(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat: PbPointFormat =
-        PbPointFormat.UNCOMPRESSED): PbEciesAeadHkdfParams {
-  const params = new PbEciesAeadHkdfParams()
-                     .setKemParams(createKemParams(opt_curveType, opt_hashType))
-                     .setDemParams(createDemParams(opt_keyTemplate))
-                     .setEcPointFormat(opt_pointFormat);
-
-  return params;
-}
-
-async function createPrivateKey(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType?: PbHashType, opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPrivateKey> {
-  const curveSubtleType = Util.curveTypeProtoToSubtle(opt_curveType);
-  const curveName = EllipticCurves.curveToString(curveSubtleType);
-
-  const publicKeyProto =
-      new PbEciesAeadHkdfPublicKey().setVersion(0).setParams(createParams(
-          opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat));
-  const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
-  const jsonPublicKey =
-      await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-  publicKeyProto.setX(Bytes.fromBase64(
-      assertExists(jsonPublicKey['x']), /* opt_webSafe = */ true));
-  publicKeyProto.setY(Bytes.fromBase64(
-      assertExists(jsonPublicKey['y']), /* opt_webSafe = */ true));
-
-
-  const privateKeyProto =
-      new PbEciesAeadHkdfPrivateKey().setVersion(0).setPublicKey(
-          publicKeyProto);
-  const jsonPrivateKey =
-      await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-  privateKeyProto.setKeyValue(Bytes.fromBase64(
-      assertExists(jsonPrivateKey['d']), /* opt_webSafe = */ true));
-
-  return privateKeyProto;
-}
-
-async function createPublicKey(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_keyTemplate?: PbKeyTemplate,
-    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPublicKey> {
-  const key = await createPrivateKey(
-      opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat);
-  return assertExists(key.getPublicKey());
-}
diff --git a/javascript/hybrid/ecies_with_aes_ctr_hmac.ts b/javascript/hybrid/ecies_with_aes_ctr_hmac.ts
deleted file mode 100644
index 5156285..0000000
--- a/javascript/hybrid/ecies_with_aes_ctr_hmac.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {HybridKeyTemplates} from './hybrid_key_templates';
-
-export const eciesP256HkdfHmacSha256Aes128CtrHmacSha256KeyTemplate =
-    HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128CtrHmacSha256;
diff --git a/javascript/hybrid/ecies_with_aes_gcm.ts b/javascript/hybrid/ecies_with_aes_gcm.ts
deleted file mode 100644
index 74afbde..0000000
--- a/javascript/hybrid/ecies_with_aes_gcm.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {HybridKeyTemplates} from './hybrid_key_templates';
-
-export const eciesP256HkdfHmacSha256Aes128GcmKeyTemplate =
-    HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm;
diff --git a/javascript/hybrid/encrypt.ts b/javascript/hybrid/encrypt.ts
deleted file mode 100644
index 42525a3..0000000
--- a/javascript/hybrid/encrypt.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {HybridEncrypt} from './internal/hybrid_encrypt';
diff --git a/javascript/hybrid/encrypt_wrapper.ts b/javascript/hybrid/encrypt_wrapper.ts
deleted file mode 100644
index 1691baf..0000000
--- a/javascript/hybrid/encrypt_wrapper.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {HybridEncryptWrapper} from './hybrid_encrypt_wrapper';
-
-export function register() {
-  Registry.registerPrimitiveWrapper(new HybridEncryptWrapper());
-}
diff --git a/javascript/hybrid/hybrid_config.ts b/javascript/hybrid/hybrid_config.ts
deleted file mode 100644
index f034996..0000000
--- a/javascript/hybrid/hybrid_config.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import * as Registry from '../internal/registry';
-
-import {EciesAeadHkdfPrivateKeyManager} from './ecies_aead_hkdf_private_key_manager';
-import {EciesAeadHkdfPublicKeyManager} from './ecies_aead_hkdf_public_key_manager';
-import {HybridDecryptWrapper} from './hybrid_decrypt_wrapper';
-import {HybridEncryptWrapper} from './hybrid_encrypt_wrapper';
-
-// Static methods and constants for registering with the Registry all instances
-// of key types for hybrid encryption and decryption supported in a particular
-// release of Tink.
-// To register all key types from the current Tink release one can do:
-// HybridConfig.register();
-// For more information on creation and usage of hybrid encryption instances
-// see HybridEncryptFactory (for encryption) and HybridDecryptFactory (for
-// decryption).
-
-/**
- * Registers key managers for all HybridEncrypt and HybridDecrypt key types
- * from the current Tink release.
- */
-export function register() {
-  AeadConfig.register();
-  Registry.registerKeyManager(new EciesAeadHkdfPrivateKeyManager());
-  Registry.registerKeyManager(new EciesAeadHkdfPublicKeyManager());
-  Registry.registerPrimitiveWrapper(new HybridEncryptWrapper());
-  Registry.registerPrimitiveWrapper(new HybridDecryptWrapper());
-}
-
-export const ENCRYPT_PRIMITIVE_NAME: string = 'HybridEncrypt';
-
-export const ECIES_AEAD_HKDF_PUBLIC_KEY_TYPE: string =
-    EciesAeadHkdfPublicKeyManager.KEY_TYPE;
-
-export const DECRYPT_PRIMITIVE_NAME: string = 'HybridDecrypt';
-
-export const ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE: string =
-    EciesAeadHkdfPrivateKeyManager.KEY_TYPE;
diff --git a/javascript/hybrid/hybrid_config_test.ts b/javascript/hybrid/hybrid_config_test.ts
deleted file mode 100644
index 82e8885..0000000
--- a/javascript/hybrid/hybrid_config_test.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {KeysetHandle} from '../internal/keyset_handle';
-import {PbKeyData, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Random from '../subtle/random';
-
-import {EciesAeadHkdfPrivateKeyManager} from './ecies_aead_hkdf_private_key_manager';
-import {EciesAeadHkdfPublicKeyManager} from './ecies_aead_hkdf_public_key_manager';
-import * as HybridConfig from './hybrid_config';
-import {HybridKeyTemplates} from './hybrid_key_templates';
-import {HybridDecrypt} from './internal/hybrid_decrypt';
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-
-describe('hybrid config test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('constants', function() {
-    expect(HybridConfig.ENCRYPT_PRIMITIVE_NAME).toBe(ENCRYPT_PRIMITIVE_NAME);
-    expect(HybridConfig.DECRYPT_PRIMITIVE_NAME).toBe(DECRYPT_PRIMITIVE_NAME);
-
-    expect(HybridConfig.ECIES_AEAD_HKDF_PUBLIC_KEY_TYPE)
-        .toBe(ECIES_AEAD_HKDF_PUBLIC_KEY_TYPE);
-    expect(HybridConfig.ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE)
-        .toBe(ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE);
-  });
-
-  it('register, correct key managers were registered', function() {
-    HybridConfig.register();
-
-    // Test that the corresponding key managers were registered.
-    const publicKeyManager =
-        Registry.getKeyManager(ECIES_AEAD_HKDF_PUBLIC_KEY_TYPE);
-    expect(publicKeyManager instanceof EciesAeadHkdfPublicKeyManager)
-        .toBe(true);
-
-    const privateKeyManager =
-        Registry.getKeyManager(ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE);
-    expect(privateKeyManager instanceof EciesAeadHkdfPrivateKeyManager)
-        .toBe(true);
-  });
-
-  // Check that everything was registered correctly and thus new keys may be
-  // generated using the predefined key templates and then they may be used for
-  // encryption and decryption.
-  it('register, predefined templates should work', async function() {
-    HybridConfig.register();
-    let templates = [
-      HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm(),
-      HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128CtrHmacSha256()
-    ];
-    for (const template of templates) {
-      const privateKeyData = await Registry.newKeyData(template);
-      const privateKeysetHandle = createKeysetHandleFromKeyData(privateKeyData);
-      const hybridDecrypt =
-          await privateKeysetHandle.getPrimitive<HybridDecrypt>(HybridDecrypt);
-
-      const publicKeyData = Registry.getPublicKeyData(
-          privateKeyData.getTypeUrl(), privateKeyData.getValue_asU8());
-      const publicKeysetHandle = createKeysetHandleFromKeyData(publicKeyData);
-      const hybridEncrypt =
-          await publicKeysetHandle.getPrimitive<HybridEncrypt>(HybridEncrypt);
-
-      const plaintext = new Uint8Array(Random.randBytes(10));
-      const contextInfo = new Uint8Array(Random.randBytes(8));
-      const ciphertext = await hybridEncrypt.encrypt(plaintext, contextInfo);
-      const decryptedCiphertext =
-          await hybridDecrypt.decrypt(ciphertext, contextInfo);
-
-      expect(decryptedCiphertext).toEqual(plaintext);
-    }
-  });
-});
-
-// Constants used in tests.
-const ENCRYPT_PRIMITIVE_NAME = 'HybridEncrypt';
-const DECRYPT_PRIMITIVE_NAME = 'HybridDecrypt';
-const ECIES_AEAD_HKDF_PUBLIC_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey';
-const ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey';
-
-/**
- * Creates a keyset containing only the key given by keyData and returns it
- * wrapped in a KeysetHandle.
- */
-function createKeysetHandleFromKeyData(keyData: PbKeyData): KeysetHandle {
-  const keyId = 1;
-  const key = new PbKeysetKey()
-                  .setKeyData(keyData)
-                  .setStatus(PbKeyStatusType.ENABLED)
-                  .setKeyId(keyId)
-                  .setOutputPrefixType(PbOutputPrefixType.TINK);
-
-  const keyset = new PbKeyset();
-  keyset.addKey(key);
-  keyset.setPrimaryKeyId(keyId);
-  return new KeysetHandle(keyset);
-}
diff --git a/javascript/hybrid/hybrid_decrypt_wrapper.ts b/javascript/hybrid/hybrid_decrypt_wrapper.ts
deleted file mode 100644
index caf9264..0000000
--- a/javascript/hybrid/hybrid_decrypt_wrapper.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {CryptoFormat} from '../internal/crypto_format';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PrimitiveWrapper} from '../internal/primitive_wrapper';
-import {PbKeyStatusType} from '../internal/proto';
-
-import {HybridDecrypt} from './internal/hybrid_decrypt';
-
-/**
- * @final
- */
-class WrappedHybridDecrypt extends HybridDecrypt {
-  // The constructor should be @private, but it is not supported by Closure
-  // (see https://github.com/google/closure-compiler/issues/2761).
-  constructor(private readonly hybridDecryptPrimitiveSet:
-                  PrimitiveSet.PrimitiveSet<HybridDecrypt>) {
-    super();
-  }
-
-  static newHybridDecrypt(hybridDecryptPrimitiveSet:
-                              PrimitiveSet.PrimitiveSet<HybridDecrypt>):
-      HybridDecrypt {
-    if (!hybridDecryptPrimitiveSet) {
-      throw new SecurityException('Primitive set has to be non-null.');
-    }
-    return new WrappedHybridDecrypt(hybridDecryptPrimitiveSet);
-  }
-
-  async decrypt(ciphertext: Uint8Array, opt_contextInfo?: Uint8Array) {
-    if (!ciphertext) {
-      throw new SecurityException('Ciphertext has to be non-null.');
-    }
-    if (ciphertext.length > CryptoFormat.NON_RAW_PREFIX_SIZE) {
-      const keyId = ciphertext.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE);
-      const primitives =
-          await this.hybridDecryptPrimitiveSet.getPrimitives(keyId);
-      const rawCiphertext = ciphertext.subarray(
-          CryptoFormat.NON_RAW_PREFIX_SIZE, ciphertext.length);
-      let decryptedText: Uint8Array|undefined;
-      try {
-        decryptedText = await this.tryDecryption(
-            primitives, rawCiphertext, opt_contextInfo);
-      } catch (e) {
-      }
-      if (decryptedText) {
-        return decryptedText;
-      }
-    }
-    const primitives = await this.hybridDecryptPrimitiveSet.getRawPrimitives();
-    return this.tryDecryption(primitives, ciphertext, opt_contextInfo);
-  }
-
-  /**
-   * Tries to decrypt the ciphertext using each entry in primitives and
-   * returns the ciphertext decrypted by first primitive which succeed. It
-   * throws an exception if no entry succeeds.
-   *
-   *
-   */
-  private async tryDecryption(
-      primitives: Array<PrimitiveSet.Entry<HybridDecrypt>>,
-      ciphertext: Uint8Array,
-      opt_contextInfo?: Uint8Array|null): Promise<Uint8Array> {
-    const primitivesLength = primitives.length;
-    for (let i = 0; i < primitivesLength; i++) {
-      if (primitives[i].getKeyStatus() != PbKeyStatusType.ENABLED) {
-        continue;
-      }
-      const primitive = primitives[i].getPrimitive();
-      let decryptionResult;
-      try {
-        decryptionResult = await primitive.decrypt(ciphertext, opt_contextInfo);
-      } catch (e) {
-        continue;
-      }
-      return decryptionResult;
-    }
-    throw new SecurityException('Decryption failed for the given ciphertext.');
-  }
-}
-
-export class HybridDecryptWrapper implements PrimitiveWrapper<HybridDecrypt> {
-  /**
-   */
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<HybridDecrypt>) {
-    return WrappedHybridDecrypt.newHybridDecrypt(primitiveSet);
-  }
-
-  /**
-   */
-  getPrimitiveType() {
-    return HybridDecrypt;
-  }
-}
diff --git a/javascript/hybrid/hybrid_decrypt_wrapper_test.ts b/javascript/hybrid/hybrid_decrypt_wrapper_test.ts
deleted file mode 100644
index 89ded18..0000000
--- a/javascript/hybrid/hybrid_decrypt_wrapper_test.ts
+++ /dev/null
@@ -1,303 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Bytes from '../subtle/bytes';
-import * as Random from '../subtle/random';
-
-import {HybridDecryptWrapper} from './hybrid_decrypt_wrapper';
-import {HybridEncryptWrapper} from './hybrid_encrypt_wrapper';
-import {HybridDecrypt} from './internal/hybrid_decrypt';
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-
-describe('hybrid decrypt wrapper test', function() {
-  it('decrypt, invalid ciphertext', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
-    const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
-    // Ciphertext which cannot be decrypted by any primitive in the primitive
-    // set.
-    const ciphertext = new Uint8Array([9, 8, 7, 6, 5, 4, 3]);
-
-    try {
-      await hybridDecrypt.decrypt(ciphertext);
-      fail('Should throw an exception');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
-    }
-  });
-
-  it('decrypt, should work', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const plaintext = Random.randBytes(10);
-    // As keys are just dummy keys which do not contain key data, the same key
-    // is used for both encrypt and decrypt.
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.TINK,
-        /** enabled = */ true);
-    const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
-
-    // Get the ciphertext.
-    const encryptPrimitiveSet = primitiveSets['encryptPrimitiveSet'];
-    const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
-    const entry = encryptPrimitiveSet.addPrimitive(encryptPrimitive, key);
-    // Has to be set to primary as then it is used in encryption.
-    encryptPrimitiveSet.setPrimary(entry);
-    const hybridEncrypt = new HybridEncryptWrapper().wrap(encryptPrimitiveSet);
-    const ciphertext = await hybridEncrypt.encrypt(plaintext);
-
-    // Create a primitive set containing the primitive which can be used for
-    // encryption. Add also few more primitives with the same key as the
-    // primitive set should decrypt the ciphertext whenever there is at least
-    // one primitive which does not fail to decrypt the ciphertext.
-    const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
-    const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
-    decryptPrimitiveSet.addPrimitive(
-        new DummyHybridDecrypt(Random.randBytes(5)), key);
-    decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
-    decryptPrimitiveSet.addPrimitive(
-        new DummyHybridDecrypt(Random.randBytes(5)), key);
-
-    // Decrypt the ciphertext.
-    const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
-    const decryptedCiphertext = await hybridDecrypt.decrypt(ciphertext);
-
-    // Test that the result is the original plaintext.
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  it('decrypt, ciphertext encrypted by raw primitive', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const plaintext = Random.randBytes(10);
-    // As keys are just dummy keys which do not contain key data, the same key
-    // is used for both encrypt and decrypt.
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
-        /** enabled = */ true);
-    const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
-
-    // Get the ciphertext.
-    const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
-    const ciphertext = await encryptPrimitive.encrypt(plaintext);
-
-    // Decrypt the ciphertext.
-    const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
-    const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
-    decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
-    const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
-    const decryptedCiphertext = await hybridDecrypt.decrypt(ciphertext);
-
-    // Test that the result is the original plaintext.
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  it('decrypt, with context info', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const plaintext = Random.randBytes(10);
-    const contextInfo = Random.randBytes(10);
-    // As keys are just dummy keys which do not contain key data, the same key
-    // is used for both encrypt and decrypt.
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
-        /** enabled = */ true);
-    const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
-
-    // Get the ciphertext.
-    const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
-    const ciphertext = await encryptPrimitive.encrypt(plaintext, contextInfo);
-
-    // Get primitive for decryption.
-    const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
-    const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
-    decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
-    const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
-
-    // Check that contextInfo was passed correctly (decryption without
-    // contextInfo argument should not work, but with contextInfo it should work
-    // properly).
-    try {
-      await hybridDecrypt.decrypt(ciphertext);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
-    }
-    const decryptedCiphertext =
-        await hybridDecrypt.decrypt(ciphertext, contextInfo);
-
-    // Test that the result is the original plaintext.
-    expect(decryptedCiphertext).toEqual(plaintext);
-  });
-
-  it('decrypt, with disabled primitive', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const plaintext = Random.randBytes(10);
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
-        /** enabled = */ false);
-    const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
-
-    const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
-    const ciphertext = await encryptPrimitive.encrypt(plaintext);
-
-    const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
-    const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
-    decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
-    const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
-
-    try {
-      await hybridDecrypt.decrypt(ciphertext);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
-    }
-  });
-
-  it('decrypt, with empty ciphertext', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
-    const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
-
-    try {
-      await hybridDecrypt.decrypt(new Uint8Array(0));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
-    }
-  });
-});
-
-/** @final */
-class ExceptionText {
-  static nullPrimitiveSet(): string {
-    return 'SecurityException: Primitive set has to be non-null.';
-  }
-
-  static cannotBeDecrypted(): string {
-    return 'SecurityException: Decryption failed for the given ciphertext.';
-  }
-
-  static nullCiphertext(): string {
-    return 'SecurityException: Ciphertext has to be non-null.';
-  }
-}
-
-/** Function for creating keys for testing purposes. */
-function createDummyKeysetKey(
-    keyId: number, outputPrefix: PbOutputPrefixType,
-    enabled: boolean): PbKeysetKey {
-  const key = new PbKeysetKey();
-
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-
-  key.setOutputPrefixType(outputPrefix);
-  key.setKeyId(keyId);
-
-  return key;
-}
-
-/**
- * Creates a primitive sets for HybridEncrypt and HybridDecrypt with
- * 'numberOfPrimitives' primitives. The keys corresponding to the primitives
- * have ids from the set [1, ..., numberOfPrimitives] and the primitive
- * corresponding to key with id 'numberOfPrimitives' is set to be primary
- * whenever opt_withPrimary is set to true (where true is the default value).
- */
-function createDummyPrimitiveSets(opt_withPrimary: boolean = true): {
-  encryptPrimitiveSet: PrimitiveSet.PrimitiveSet<DummyHybridEncrypt>,
-  decryptPrimitiveSet: PrimitiveSet.PrimitiveSet<DummyHybridDecrypt>
-} {
-  const numberOfPrimitives = 5;
-
-  const encryptPrimitiveSet =
-      new PrimitiveSet.PrimitiveSet<DummyHybridEncrypt>(DummyHybridEncrypt);
-  const decryptPrimitiveSet =
-      new PrimitiveSet.PrimitiveSet<DummyHybridDecrypt>(DummyHybridDecrypt);
-  for (let i = 1; i < numberOfPrimitives; i++) {
-    let outputPrefix: PbOutputPrefixType;
-    switch (i % 3) {
-      case 0:
-        outputPrefix = PbOutputPrefixType.TINK;
-        break;
-      case 1:
-        outputPrefix = PbOutputPrefixType.LEGACY;
-        break;
-      default:
-        outputPrefix = PbOutputPrefixType.RAW;
-    }
-    const key =
-        createDummyKeysetKey(i, outputPrefix, /* enabled = */ i % 4 < 2);
-    const ciphertextSuffix = new Uint8Array([0, 0, i]);
-    const hybridEncrypt = new DummyHybridEncrypt(ciphertextSuffix);
-    encryptPrimitiveSet.addPrimitive(hybridEncrypt, key);
-    const hybridDecrypt = new DummyHybridDecrypt(ciphertextSuffix);
-    decryptPrimitiveSet.addPrimitive(hybridDecrypt, key);
-  }
-
-  const key = createDummyKeysetKey(
-      numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true);
-  const ciphertextSuffix = new Uint8Array([0, 0, numberOfPrimitives]);
-  const hybridEncrypt = new DummyHybridEncrypt(ciphertextSuffix);
-  const encryptEntry = encryptPrimitiveSet.addPrimitive(hybridEncrypt, key);
-  const hybridDecrypt = new DummyHybridDecrypt(ciphertextSuffix);
-  const decryptEntry = decryptPrimitiveSet.addPrimitive(hybridDecrypt, key);
-  if (opt_withPrimary) {
-    encryptPrimitiveSet.setPrimary(encryptEntry);
-    decryptPrimitiveSet.setPrimary(decryptEntry);
-  }
-
-  return {
-    'encryptPrimitiveSet': encryptPrimitiveSet,
-    'decryptPrimitiveSet': decryptPrimitiveSet
-  };
-}
-
-/** @final */
-class DummyHybridEncrypt extends HybridEncrypt {
-  constructor(private readonly ciphertextSuffix: Uint8Array) {
-    super();
-  }
-
-  async encrypt(plaintext: Uint8Array, opt_contextInfo?: Uint8Array) {
-    const ciphertext = Bytes.concat(plaintext, this.ciphertextSuffix);
-    if (opt_contextInfo) {
-      return Bytes.concat(ciphertext, opt_contextInfo);
-    }
-    return ciphertext;
-  }
-}
-
-/** @final */
-class DummyHybridDecrypt extends HybridDecrypt {
-  constructor(private readonly ciphertextSuffix: Uint8Array) {
-    super();
-  }
-
-  async decrypt(ciphertext: Uint8Array, opt_contextInfo?: Uint8Array) {
-    if (opt_contextInfo) {
-      const infoLength = opt_contextInfo.length;
-      const contextInfo =
-          ciphertext.slice(ciphertext.length - infoLength, ciphertext.length);
-      if ([...contextInfo].toString() !== [...opt_contextInfo].toString()) {
-        throw new SecurityException('Context info does not match.');
-      }
-      ciphertext = ciphertext.slice(0, ciphertext.length - infoLength);
-    }
-    const plaintext =
-        ciphertext.slice(0, ciphertext.length - this.ciphertextSuffix.length);
-    const suffix = ciphertext.slice(
-        ciphertext.length - this.ciphertextSuffix.length, ciphertext.length);
-    if ([...suffix].toString() === [...this.ciphertextSuffix].toString()) {
-      return plaintext;
-    }
-    throw new SecurityException(ExceptionText.cannotBeDecrypted());
-  }
-}
diff --git a/javascript/hybrid/hybrid_encrypt_wrapper.ts b/javascript/hybrid/hybrid_encrypt_wrapper.ts
deleted file mode 100644
index 6ea4bbc..0000000
--- a/javascript/hybrid/hybrid_encrypt_wrapper.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PrimitiveWrapper} from '../internal/primitive_wrapper';
-import * as Bytes from '../subtle/bytes';
-
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-
-/**
- * @final
- */
-class WrappedHybridEncrypt extends HybridEncrypt {
-  // The constructor should be @private, but it is not supported by Closure
-  // (see https://github.com/google/closure-compiler/issues/2761).
-  constructor(private readonly hybridEncryptPrimitiveSet:
-                  PrimitiveSet.PrimitiveSet<HybridEncrypt>) {
-    super();
-  }
-
-  static newHybridEncrypt(hybridEncryptPrimitiveSet:
-                              PrimitiveSet.PrimitiveSet<HybridEncrypt>):
-      HybridEncrypt {
-    if (!hybridEncryptPrimitiveSet) {
-      throw new SecurityException('Primitive set has to be non-null.');
-    }
-    if (!hybridEncryptPrimitiveSet.getPrimary()) {
-      throw new SecurityException('Primary has to be non-null.');
-    }
-    return new WrappedHybridEncrypt(hybridEncryptPrimitiveSet);
-  }
-
-  async encrypt(plaintext: Uint8Array, opt_contextInfo?: Uint8Array) {
-    if (!plaintext) {
-      throw new SecurityException('Plaintext has to be non-null.');
-    }
-    const primary = this.hybridEncryptPrimitiveSet.getPrimary();
-    if (!primary) {
-      throw new SecurityException('Primary not set.');
-    }
-    const primitive = primary.getPrimitive();
-    const ciphertext = await primitive.encrypt(plaintext, opt_contextInfo);
-    const keyId = primary.getIdentifier();
-    return Bytes.concat(keyId, ciphertext);
-  }
-}
-
-export class HybridEncryptWrapper implements PrimitiveWrapper<HybridEncrypt> {
-  /**
-   */
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<HybridEncrypt>) {
-    return WrappedHybridEncrypt.newHybridEncrypt(primitiveSet);
-  }
-
-  /**
-   */
-  getPrimitiveType() {
-    return HybridEncrypt;
-  }
-}
diff --git a/javascript/hybrid/hybrid_encrypt_wrapper_test.ts b/javascript/hybrid/hybrid_encrypt_wrapper_test.ts
deleted file mode 100644
index 6bee06c..0000000
--- a/javascript/hybrid/hybrid_encrypt_wrapper_test.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {CryptoFormat} from '../internal/crypto_format';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Random from '../subtle/random';
-import {assertExists} from '../testing/internal/test_utils';
-
-import {HybridEncryptWrapper} from './hybrid_encrypt_wrapper';
-import {HybridEncrypt} from './internal/hybrid_encrypt';
-
-describe('hybrid encrypt wrapper test', function() {
-  it('new hybrid encrypt, primitive set without primary', function() {
-    const primitiveSet = createDummyPrimitiveSet(/* opt_withPrimary = */ false);
-    try {
-      new HybridEncryptWrapper().wrap(primitiveSet);
-      fail('Should throw an exception.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.primitiveSetWithoutPrimary());
-    }
-  });
-
-  it('new hybrid encrypt, should work', function() {
-    const primitiveSet = createDummyPrimitiveSet();
-    const hybridEncrypt = new HybridEncryptWrapper().wrap(primitiveSet);
-    expect(hybridEncrypt != null && hybridEncrypt != undefined).toBe(true);
-  });
-
-  it('encrypt, should work', async function() {
-    const primitiveSet = createDummyPrimitiveSet();
-    const hybridEncrypt = new HybridEncryptWrapper().wrap(primitiveSet);
-
-    const plaintext = Random.randBytes(10);
-
-    const ciphertext = await hybridEncrypt.encrypt(plaintext);
-    expect(ciphertext != null).toBe(true);
-
-    // Ciphertext should begin with primary key output prefix.
-    expect(ciphertext.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE))
-        .toEqual(assertExists(primitiveSet.getPrimary()).getIdentifier());
-  });
-});
-
-/**
- * Class holding texts for each type of exception.
- * @final
- */
-class ExceptionText {
-  static nullPrimitiveSet(): string {
-    return 'SecurityException: Primitive set has to be non-null.';
-  }
-
-  static primitiveSetWithoutPrimary(): string {
-    return 'SecurityException: Primary has to be non-null.';
-  }
-
-  static nullPlaintext(): string {
-    return 'SecurityException: Plaintext has to be non-null.';
-  }
-}
-
-/** Function for creating keys for testing purposes. */
-function createDummyKeysetKey(
-    keyId: number, outputPrefix: PbOutputPrefixType,
-    enabled: boolean): PbKeysetKey {
-  const key = new PbKeysetKey();
-
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-
-  key.setOutputPrefixType(outputPrefix);
-  key.setKeyId(keyId);
-
-  return key;
-}
-
-/**
- * Creates a primitive set with 'numberOfPrimitives' primitives. The keys
- * corresponding to the primitives have ids from the set
- * [1, ..., numberOfPrimitives] and the primitive corresponding to key with id
- * 'numberOfPrimitives' is set to be primary whenever opt_withPrimary is set to
- * true (where true is the default value).
- */
-function createDummyPrimitiveSet(opt_withPrimary: boolean = true):
-    PrimitiveSet.PrimitiveSet<DummyHybridEncrypt> {
-  const numberOfPrimitives = 5;
-  const primitiveSet =
-      new PrimitiveSet.PrimitiveSet<HybridEncrypt>(HybridEncrypt);
-  for (let i = 1; i < numberOfPrimitives; i++) {
-    let outputPrefix: PbOutputPrefixType;
-    switch (i % 3) {
-      case 0:
-        outputPrefix = PbOutputPrefixType.TINK;
-        break;
-      case 1:
-        outputPrefix = PbOutputPrefixType.LEGACY;
-        break;
-      default:
-        outputPrefix = PbOutputPrefixType.RAW;
-    }
-    const key =
-        createDummyKeysetKey(i, outputPrefix, /* enabled = */ i % 4 < 2);
-    const hybridEncrypt = new DummyHybridEncrypt();
-    primitiveSet.addPrimitive(hybridEncrypt, key);
-  }
-
-  const key = createDummyKeysetKey(
-      numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true);
-  const hybridEncrypt = new DummyHybridEncrypt();
-  const entry = primitiveSet.addPrimitive(hybridEncrypt, key);
-  if (opt_withPrimary) {
-    primitiveSet.setPrimary(entry);
-  }
-
-  return primitiveSet;
-}
-
-/** @final */
-class DummyHybridEncrypt extends HybridEncrypt {
-  async encrypt(plaintext: Uint8Array, opt_contextInfo: Uint8Array) {
-    return plaintext;
-  }
-}
diff --git a/javascript/hybrid/hybrid_key_templates.ts b/javascript/hybrid/hybrid_key_templates.ts
deleted file mode 100644
index 1380f5d..0000000
--- a/javascript/hybrid/hybrid_key_templates.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {PbEciesAeadDemParams, PbEciesAeadHkdfKeyFormat, PbEciesAeadHkdfParams, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyTemplate, PbOutputPrefixType, PbPointFormat} from '../internal/proto';
-
-import * as HybridConfig from './hybrid_config';
-
-/**
- * Pre-generated KeyTemplates for keys for hybrid encryption.
- *
- * One can use these templates to generate new Keyset with
- * KeysetHandle.generateNew method. To generate a new keyset that contains a
- * single EciesAeadHkdfKey, one can do:
- *
- * HybridConfig.Register();
- * KeysetHandle handle = KeysetHandle.generateNew(
- *     HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
- *
- * @final
- */
-export class HybridKeyTemplates {
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EciesAeadHkdfPrivateKey with the following parameters:
-   *
-   *   KEM: ECDH over NIST P-256
-   *   DEM: AES128-GCM
-   *   KDF: HKDF-HMAC-SHA256 with an empty salt
-   *   OutputPrefixType: TINK
-   *
-   */
-  static eciesP256HkdfHmacSha256Aes128Gcm(): PbKeyTemplate {
-    return createEciesAeadHkdfKeyTemplate_(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P256,
-        /* hkdfHash = */
-        PbHashType.SHA256,
-        /* pointFormat = */
-        PbPointFormat.UNCOMPRESSED,
-        /* demKeyTemplate = */
-        AeadKeyTemplates.aes128Gcm(),
-        /* hkdfSalt = */
-        new Uint8Array(0));
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EciesAeadHkdfPrivateKey with the following parameters:
-   *
-   *   KEM: ECDH over NIST P-256
-   *   DEM: AES128-CTR-HMAC-SHA256 with
-   *        - AES key size: 16 bytes
-   *        - AES CTR IV size: 16 bytes
-   *        - HMAC key size: 32 bytes
-   *        - HMAC tag size: 16 bytes
-   *   KDF: HKDF-HMAC-SHA256 with an empty salt
-   *   OutputPrefixType: TINK
-   *
-   */
-  static eciesP256HkdfHmacSha256Aes128CtrHmacSha256(): PbKeyTemplate {
-    return createEciesAeadHkdfKeyTemplate_(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P256,
-        /* hkdfHash = */
-        PbHashType.SHA256,
-        /* pointFormat = */
-        PbPointFormat.UNCOMPRESSED,
-        /* demKeyTemplate = */
-        AeadKeyTemplates.aes128CtrHmacSha256(),
-        /* hkdfSalt = */
-        new Uint8Array(0));
-  }
-}
-
-function createEciesAeadHkdfKeyTemplate_(
-    curveType: PbEllipticCurveType, hkdfHash: PbHashType,
-    pointFormat: PbPointFormat, demKeyTemplate: PbKeyTemplate,
-    hkdfSalt: Uint8Array): PbKeyTemplate {
-  // key format
-  const keyFormat =
-      (new PbEciesAeadHkdfKeyFormat())
-          .setParams(createEciesAeadHkdfParams_(
-              curveType, hkdfHash, pointFormat, demKeyTemplate, hkdfSalt));
-
-  // key template
-  const keyTemplate =
-      (new PbKeyTemplate())
-          .setTypeUrl(HybridConfig.ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE)
-          .setValue(keyFormat.serializeBinary())
-          .setOutputPrefixType(PbOutputPrefixType.TINK);
-  return keyTemplate;
-}
-
-function createEciesAeadHkdfParams_(
-    curveType: PbEllipticCurveType, hkdfHash: PbHashType,
-    pointFormat: PbPointFormat, demKeyTemplate: PbKeyTemplate,
-    hkdfSalt: Uint8Array): PbEciesAeadHkdfParams {
-  // KEM params
-  const kemParams = (new PbEciesHkdfKemParams())
-                        .setCurveType(curveType)
-                        .setHkdfHashType(hkdfHash)
-                        .setHkdfSalt(hkdfSalt);
-
-  // DEM params
-  const demParams = (new PbEciesAeadDemParams()).setAeadDem(demKeyTemplate);
-
-  // params
-  const params = (new PbEciesAeadHkdfParams())
-                     .setKemParams(kemParams)
-                     .setDemParams(demParams)
-                     .setEcPointFormat(pointFormat);
-  return params;
-}
diff --git a/javascript/hybrid/hybrid_key_templates_test.ts b/javascript/hybrid/hybrid_key_templates_test.ts
deleted file mode 100644
index da23635..0000000
--- a/javascript/hybrid/hybrid_key_templates_test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {PbEciesAeadHkdfKeyFormat, PbEllipticCurveType, PbHashType, PbOutputPrefixType, PbPointFormat} from '../internal/proto';
-import {assertMessageEquals} from '../testing/internal/test_utils';
-
-import {EciesAeadHkdfPrivateKeyManager} from './ecies_aead_hkdf_private_key_manager';
-import {HybridKeyTemplates} from './hybrid_key_templates';
-
-describe('hybrid key templates test', function() {
-  it('ecies p256 hkdf hmac sha256 aes128 gcm', function() {
-    // Expects function to create a key with following parameters.
-    const expectedCurve = PbEllipticCurveType.NIST_P256;
-    const expectedHkdfHashFunction = PbHashType.SHA256;
-    const expectedAeadTemplate = AeadKeyTemplates.aes128Gcm();
-    const expectedPointFormat = PbPointFormat.UNCOMPRESSED;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-
-    // Expected type URL is the one supported by EciesAeadHkdfPrivateKeyManager.
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test values in key format.
-    const keyFormat =
-        PbEciesAeadHkdfKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    const params = keyFormat.getParams();
-    expect(params!.getEcPointFormat()).toBe(expectedPointFormat);
-
-    // Test KEM params.
-    const kemParams = params!.getKemParams();
-    expect(kemParams!.getCurveType()).toBe(expectedCurve);
-    expect(kemParams!.getHkdfHashType()).toBe(expectedHkdfHashFunction);
-
-    // Test DEM params.
-    const demParams = params!.getDemParams();
-    assertMessageEquals(demParams!.getAeadDem()!, expectedAeadTemplate);
-
-    // Test that the template works with EciesAeadHkdfPrivateKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-
-  it('ecies p256 hkdf hmac sha256 aes128 ctr hmac sha256', function() {
-    // Expects function to create a key with following parameters.
-    const expectedCurve = PbEllipticCurveType.NIST_P256;
-    const expectedHkdfHashFunction = PbHashType.SHA256;
-    const expectedAeadTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-    const expectedPointFormat = PbPointFormat.UNCOMPRESSED;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-
-    // Expected type URL is the one supported by EciesAeadHkdfPrivateKeyManager.
-    const manager = new EciesAeadHkdfPrivateKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate =
-        HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128CtrHmacSha256();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test values in key format.
-    const keyFormat =
-        PbEciesAeadHkdfKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    const params = keyFormat.getParams();
-    expect(params!.getEcPointFormat()).toBe(expectedPointFormat);
-
-    // Test KEM params.
-    const kemParams = params!.getKemParams();
-    expect(kemParams!.getCurveType()).toBe(expectedCurve);
-    expect(kemParams!.getHkdfHashType()).toBe(expectedHkdfHashFunction);
-
-    // Test DEM params.
-    const demParams = params!.getDemParams();
-    assertMessageEquals(demParams!.getAeadDem()!, expectedAeadTemplate);
-
-    // Test that the template works with EciesAeadHkdfPrivateKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-});
diff --git a/javascript/hybrid/index.ts b/javascript/hybrid/index.ts
deleted file mode 100644
index 949cf29..0000000
--- a/javascript/hybrid/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as aesCtrHmac from '../aead/aes_ctr_hmac';
-import * as aesGcm from '../aead/aes_gcm';
-import * as decryptWrapper from './decrypt_wrapper';
-import * as eciesAeadHkdfForDecrypting from './ecies_aead_hkdf_for_decrypting';
-import * as eciesAeadHkdfForEncrypting from './ecies_aead_hkdf_for_encrypting';
-import * as encryptWrapper from './encrypt_wrapper';
-
-export * from './ecies_with_aes_ctr_hmac';
-export * from './ecies_with_aes_gcm';
-export * from './decrypt';
-export * from './encrypt';
-
-export function register() {
-  aesCtrHmac.register();
-  aesGcm.register();
-  decryptWrapper.register();
-  eciesAeadHkdfForDecrypting.register();
-  eciesAeadHkdfForEncrypting.register();
-  encryptWrapper.register();
-}
diff --git a/javascript/hybrid/internal/BUILD.bazel b/javascript/hybrid/internal/BUILD.bazel
deleted file mode 100644
index 28abb35..0000000
--- a/javascript/hybrid/internal/BUILD.bazel
+++ /dev/null
@@ -1,11 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "internal",
-    srcs = [
-        "hybrid_decrypt.ts",
-        "hybrid_encrypt.ts",
-    ],
-)
diff --git a/javascript/hybrid/internal/hybrid_decrypt.ts b/javascript/hybrid/internal/hybrid_decrypt.ts
deleted file mode 100644
index 30c8679..0000000
--- a/javascript/hybrid/internal/hybrid_decrypt.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for hybrid encryption.
- *
- * Hybrid Encryption combines the efficiency of symmetric encryption with the
- * convenience of public-key encryption: to encrypt a message a fresh symmetric
- * key is generated and used to encrypt the actual plaintext data, while the
- * recipient’s public key is used to encrypt the symmetric key only, and the
- * final ciphertext consists of the symmetric ciphertext and the encrypted
- * symmetric key.
- *
- * WARNING: Hybrid Encryption does not provide authenticity, that is the
- * recipient of an encrypted message does not know the identity of the sender.
- * Similar to general public-key encryption schemes the security goal of Hybrid
- * Encryption is to provide privacy only. In other words, Hybrid Encryption is
- * secure if and only if the recipient can accept anonymous messages or can rely
- * on other mechanisms to authenticate the sender.
- *
- * Security guarantees: The functionality of Hybrid Encryption is represented as
- * a pair of primitives (interfaces): `HybridEncrypt` for encryption of data,
- * and `HybridDecrypt` for decryption. Implementations of these interfaces are
- * secure against adaptive chosen ciphertext attacks.
- *
- * In addition to `plaintext` the encryption takes an extra, optional parameter
- * `opt_contextInfo`, which usually is public data implicit from the context,
- * but should be bound to the resulting ciphertext, i.e. the ciphertext allows
- * for checking the integrity of `opt_contextInfo` (but there are no guarantees
- * wrt. the secrecy or authenticity of `opt_contextInfo`).
- *
- * `opt_contextInfo` can be empty or null, but to ensure the correct
- * decryption of a ciphertext the same value must be provided for the decryption
- * operation as was used during encryption (cf. `HybridEncrypt`}).
- *
- * A concrete instantiation of this interface can implement the binding of
- * contextInfo to the ciphertext in various ways, for example:
- *     * use `opt_contextInfo` as "associated data"-input for the employed
- *     AEAD symmetric encryption (cf. https://tools.ietf.org/html/rfc5116).
- *     * use `opt_contextInfo` as "CtxInfo"-input for HKDF (if the
- * implementation uses HKDF as key derivation function, cf.
- *      https://tools.ietf.org/html/rfc5869).
- *
- */
-export abstract class HybridDecrypt {
-  /**
-   * Decryption operation: decrypts ciphertext verifying the integrity of
-   * `opt_contextInfo`.
-   *
-   * @param ciphertext the ciphertext to be decrypted, must be
-   *     non-null.
-   * @param opt_contextInfo optional the context info to be
-   *     authenticated. For successful decryption it must be the same as used
-   *     during encryption. It can be null, which is equivalent to an empty
-   *     (zero-length) byte array.
-   * @return resulting plaintext
-   */
-  abstract decrypt(ciphertext: Uint8Array, opt_contextInfo?: Uint8Array|null):
-      Promise<Uint8Array>;
-}
diff --git a/javascript/hybrid/internal/hybrid_encrypt.ts b/javascript/hybrid/internal/hybrid_encrypt.ts
deleted file mode 100644
index 1d38723..0000000
--- a/javascript/hybrid/internal/hybrid_encrypt.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for hybrid encryption.
- *
- * Hybrid Encryption combines the efficiency of symmetric encryption with the
- * convenience of public-key encryption: to encrypt a message a fresh symmetric
- * key is generated and used to encrypt the actual plaintext data, while the
- * recipient’s public key is used to encrypt the symmetric key only, and the
- * final ciphertext consists of the symmetric ciphertext and the encrypted
- * symmetric key.
- *
- * WARNING: Hybrid Encryption does not provide authenticity, that is the
- * recipient of an encrypted message does not know the identity of the sender.
- * Similar to general public-key encryption schemes the security goal of Hybrid
- * Encryption is to provide privacy only. In other words, Hybrid Encryption is
- * secure if and only if the recipient can accept anonymous messages or can rely
- * on other mechanisms to authenticate the sender.
- *
- * Security guarantees: The functionality of Hybrid Encryption is represented as
- * a pair of primitives (interfaces): `HybridEncrypt` for encryption of data,
- * and `HybridDecrypt` for decryption. Implementations of these interfaces are
- * secure against adaptive chosen ciphertext attacks.
- *
- * In addition to `plaintext` the encryption takes an extra, optional parameter
- * `opt_contextInfo`, which usually is public data implicit from the context,
- * but should be bound to the resulting ciphertext, i.e. the ciphertext allows
- * for checking the integrity of `opt_contextInfo` (but there are no guarantees
- * wrt. the secrecy or authenticity of `opt_contextInfo`).
- *
- * `opt_contextInfo` can be empty or null, but to ensure the correct
- * decryption of a ciphertext the same value must be provided for the decryption
- * operation as was used during encryption (cf. `HybridEncrypt`}).
- *
- * A concrete instantiation of this interface can implement the binding of
- * contextInfo to the ciphertext in various ways, for example:
- *     * use `opt_contextInfo` as "associated data"-input for the employed
- *     AEAD symmetric encryption (cf. https://tools.ietf.org/html/rfc5116).
- *     * use `opt_contextInfo` as "CtxInfo"-input for HKDF (if the
- * implementation uses HKDF as key derivation function, cf.
- *      https://tools.ietf.org/html/rfc5869).
- *
- */
-export abstract class HybridEncrypt {
-  /**
-   * Encryption operation: encrypts `plaintext` binding `opt_contextInfo` to the
-   * resulting ciphertext.
-   *
-   * @param plaintext the plaintext to be encrypted, must be
-   *     non-null.
-   * @param opt_contextInfo optional context info to be
-   *     authenticated. It can be null, which is equivalent to an empty
-   *     (zero-length) byte array.
-   * @return resulting ciphertext
-   */
-  abstract encrypt(plaintext: Uint8Array, opt_contextInfo?: Uint8Array|null):
-      Promise<Uint8Array>;
-}
diff --git a/javascript/hybrid/registry_ecies_aead_hkdf_dem_helper.ts b/javascript/hybrid/registry_ecies_aead_hkdf_dem_helper.ts
deleted file mode 100644
index a7093a0..0000000
--- a/javascript/hybrid/registry_ecies_aead_hkdf_dem_helper.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead';
-import {AeadConfig} from '../aead/aead_config';
-import {SecurityException} from '../exception/security_exception';
-import {PbAesCtrHmacAeadKey, PbAesCtrHmacAeadKeyFormat, PbAesGcmKey, PbAesGcmKeyFormat, PbKeyTemplate} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import {EciesAeadHkdfDemHelper} from '../subtle/ecies_aead_hkdf_dem_helper';
-
-/**
- * @final
- */
-export class RegistryEciesAeadHkdfDemHelper implements EciesAeadHkdfDemHelper {
-  private readonly key: PbAesCtrHmacAeadKey|PbAesGcmKey;
-  private readonly demKeyTypeUrl: string;
-  private readonly demKeySize: number;
-  private readonly aesCtrKeySize?: number;
-
-  constructor(keyTemplate: PbKeyTemplate) {
-    let demKeySize: number;
-    let aesCtrKeySize: number|undefined;
-    let keyFormat: PbAesCtrHmacAeadKeyFormat|PbAesGcmKeyFormat;
-    const keyTypeUrl = keyTemplate.getTypeUrl();
-    switch (keyTypeUrl) {
-      case AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL:
-        keyFormat =
-            RegistryEciesAeadHkdfDemHelper.getAesCtrHmacKeyFormat(keyTemplate);
-        const aesCtrKeyFormat = keyFormat.getAesCtrKeyFormat();
-        if (!aesCtrKeyFormat) {
-          throw new SecurityException('AES-CTR key format not set');
-        }
-        aesCtrKeySize = aesCtrKeyFormat.getKeySize();
-        const hmacKeyFormat = keyFormat.getHmacKeyFormat();
-        if (!hmacKeyFormat) {
-          throw new SecurityException('HMAC key format not set');
-        }
-        const hmacKeySize = hmacKeyFormat.getKeySize();
-        demKeySize = aesCtrKeySize + hmacKeySize;
-        break;
-      case AeadConfig.AES_GCM_TYPE_URL:
-        keyFormat =
-            RegistryEciesAeadHkdfDemHelper.getAesGcmKeyFormat(keyTemplate);
-        demKeySize = keyFormat.getKeySize();
-        break;
-      default:
-        throw new SecurityException(
-            'Key type URL ' + keyTypeUrl + ' is not supported.');
-    }
-    const keyFactory = Registry.getKeyManager(keyTypeUrl).getKeyFactory();
-    this.key =
-        (keyFactory.newKey(keyFormat) as PbAesCtrHmacAeadKey | PbAesGcmKey);
-    this.demKeyTypeUrl = keyTypeUrl;
-    this.demKeySize = demKeySize;
-    this.aesCtrKeySize = aesCtrKeySize;
-  }
-
-  /**
-   */
-  getDemKeySizeInBytes() {
-    return this.demKeySize;
-  }
-
-  /**
-   */
-  async getAead(demKey: Uint8Array): Promise<Aead> {
-    if (demKey.length !== this.demKeySize) {
-      throw new SecurityException(
-          `Key is not of the correct length, expected length: ${
-              this.demKeySize}, but got key of length: ${demKey.length}.`);
-    }
-    let key: PbAesCtrHmacAeadKey|PbAesGcmKey;
-    if (this.demKeyTypeUrl === AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL) {
-      key = this.replaceAesCtrHmacKeyValue(demKey);
-    } else {
-      key = this.replaceAesGcmKeyValue(demKey);
-    }
-    return Registry.getPrimitive<Aead>(Aead, key, this.demKeyTypeUrl);
-  }
-
-  private static getAesGcmKeyFormat(keyTemplate: PbKeyTemplate):
-      PbAesGcmKeyFormat {
-    let keyFormat: PbAesGcmKeyFormat;
-    try {
-      keyFormat =
-          PbAesGcmKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    } catch (e) {
-      throw new SecurityException(
-          'Could not parse the given Uint8Array as a serialized proto of ' +
-          AeadConfig.AES_GCM_TYPE_URL + '.');
-    }
-    if (!keyFormat.getKeySize()) {
-      throw new SecurityException(
-          'Could not parse the given Uint8Array as a serialized proto of ' +
-          AeadConfig.AES_GCM_TYPE_URL + '.');
-    }
-    return keyFormat;
-  }
-
-  private static getAesCtrHmacKeyFormat(keyTemplate: PbKeyTemplate):
-      PbAesCtrHmacAeadKeyFormat {
-    let keyFormat: PbAesCtrHmacAeadKeyFormat;
-    try {
-      keyFormat = PbAesCtrHmacAeadKeyFormat.deserializeBinary(
-          keyTemplate.getValue_asU8());
-    } catch (e) {
-      throw new SecurityException(
-          'Could not parse the given Uint8Array ' +
-          'as a serialized proto of ' + AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL +
-          '.');
-    }
-    if (!keyFormat.getAesCtrKeyFormat() || !keyFormat.getHmacKeyFormat()) {
-      throw new SecurityException(
-          'Could not parse the given Uint8Array as a serialized proto of ' +
-          AeadConfig.AES_CTR_HMAC_AEAD_TYPE_URL + '.');
-    }
-    return keyFormat;
-  }
-
-  private replaceAesGcmKeyValue(symmetricKey: Uint8Array): PbAesGcmKey {
-    if (!(this.key instanceof PbAesGcmKey)) {
-      throw new SecurityException('Key is not an AES-CTR key');
-    }
-    const key = this.key.setKeyValue(symmetricKey);
-    return key;
-  }
-
-  private replaceAesCtrHmacKeyValue(symmetricKey: Uint8Array):
-      PbAesCtrHmacAeadKey {
-    const key = (this.key as PbAesCtrHmacAeadKey);
-    const aesCtrKey = key.getAesCtrKey();
-    if (!aesCtrKey) {
-      throw new SecurityException('AES-CTR key not set');
-    }
-    const aesCtrKeyValue = symmetricKey.slice(0, this.aesCtrKeySize);
-    aesCtrKey.setKeyValue(aesCtrKeyValue);
-    const hmacKey = key.getHmacKey();
-    if (!hmacKey) {
-      throw new SecurityException('HMAC key not set');
-    }
-    const hmacKeyValue =
-        symmetricKey.slice(this.aesCtrKeySize, this.demKeySize);
-    hmacKey.setKeyValue(hmacKeyValue);
-    return key;
-  }
-}
diff --git a/javascript/hybrid/registry_ecies_aead_hkdf_dem_helper_test.ts b/javascript/hybrid/registry_ecies_aead_hkdf_dem_helper_test.ts
deleted file mode 100644
index 60fe859..0000000
--- a/javascript/hybrid/registry_ecies_aead_hkdf_dem_helper_test.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import * as Registry from '../internal/registry';
-import * as Random from '../subtle/random';
-
-import {RegistryEciesAeadHkdfDemHelper} from './registry_ecies_aead_hkdf_dem_helper';
-
-describe('registry ecies aead hkdf dem helper test', function() {
-  beforeEach(function() {
-    AeadConfig.register();
-  });
-
-  afterEach(function() {
-    Registry.reset();
-  });
-
-  //////////////////////////////////////////////////////////////////////////////
-  // Tests for constructor
-  //////////////////////////////////////////////////////////////////////////////
-
-  it('constructor, unsupported key type', function() {
-    const template = AeadKeyTemplates.aes128CtrHmacSha256().setTypeUrl(
-        'some_unsupported_type_url');
-
-    try {
-      new RegistryEciesAeadHkdfDemHelper(template);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedTypeUrl(template.getTypeUrl()));
-    }
-  });
-
-  it('constructor, invalid key formats', function() {
-    // Some valid AES-GCM and AES-CTR-HMAC key templates.
-    const templates =
-        [AeadKeyTemplates.aes128CtrHmacSha256(), AeadKeyTemplates.aes128Gcm()];
-    const invalidKeyFormats = [new Uint8Array(0), new Uint8Array([0, 1, 2])];
-
-    // Test that if the value is changed to invalid key format than the DEM
-    // helper throws an exception.
-    for (const template of templates) {
-      for (const invalidKeyFormat of invalidKeyFormats) {
-        template.setValue(invalidKeyFormat);
-
-        try {
-          new RegistryEciesAeadHkdfDemHelper(template);
-          fail('An exception should be thrown.');
-        } catch (e: any) {
-          expect(e.toString())
-              .toBe(ExceptionText.invalidKeyFormat(template.getTypeUrl()));
-        }
-      }
-    }
-  });
-
-  it('constructor, aes128 ctr hmac sha256 key template', function() {
-    const template = AeadKeyTemplates.aes128CtrHmacSha256();
-    const helper = new RegistryEciesAeadHkdfDemHelper(template);
-
-    // Expected size is a sum of AES CTR key length and HMAC key length.
-    const expectedSize = 16 + 32;
-    expect(helper.getDemKeySizeInBytes()).toBe(expectedSize);
-  });
-
-  it('constructor, aes256 ctr hmac sha256 key template', function() {
-    const template = AeadKeyTemplates.aes256CtrHmacSha256();
-    const helper = new RegistryEciesAeadHkdfDemHelper(template);
-
-    // Expected size is a sum of AES CTR key length and HMAC key length.
-    const expectedSize = 32 + 32;
-    expect(helper.getDemKeySizeInBytes()).toBe(expectedSize);
-  });
-
-  it('constructor, aes128 gcm', function() {
-    const template = AeadKeyTemplates.aes128Gcm();
-    const helper = new RegistryEciesAeadHkdfDemHelper(template);
-
-    // Expected size is equal to the size of key.
-    const expectedSize = 16;
-    expect(helper.getDemKeySizeInBytes()).toBe(expectedSize);
-  });
-
-  it('constructor, aes256 gcm', function() {
-    const template = AeadKeyTemplates.aes256Gcm();
-    const helper = new RegistryEciesAeadHkdfDemHelper(template);
-
-    // Expected size is equal to the size of key.
-    const expectedSize = 32;
-    expect(helper.getDemKeySizeInBytes()).toBe(expectedSize);
-  });
-
-  //////////////////////////////////////////////////////////////////////////////
-  // Tests for getAead method
-  //////////////////////////////////////////////////////////////////////////////
-
-  it('get aead, invalid key length', async function() {
-    const template = AeadKeyTemplates.aes128CtrHmacSha256();
-    // Expected size is a sum of AES CTR key length and HMAC key length.
-    const expectedKeyLength = 16 + 32;
-    const helper = new RegistryEciesAeadHkdfDemHelper(template);
-    const keyLength = 2;
-
-    try {
-      await helper.getAead(new Uint8Array(keyLength));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.invalidKeyLength(expectedKeyLength, keyLength));
-    }
-  });
-
-  it('get aead, different templates', async function() {
-    const templates = [
-      AeadKeyTemplates.aes128CtrHmacSha256(), AeadKeyTemplates.aes128Gcm(),
-      AeadKeyTemplates.aes256CtrHmacSha256(), AeadKeyTemplates.aes256Gcm()
-    ];
-
-    for (const template of templates) {
-      const helper = new RegistryEciesAeadHkdfDemHelper(template);
-      // Compute some demKey of size corresponding to the template.
-      // The result of getDemKeySizeInBytes is the expected one for the given
-      // templates as it was tested for these templates in previous tests.
-      const demKey = Random.randBytes(helper.getDemKeySizeInBytes());
-
-      // Get Aead from helper.
-      const aead = await helper.getAead(demKey);
-      expect(aead != null).toBe(true);
-
-      // Test the Aead instance.
-      const plaintext = Random.randBytes(10);
-      const aad = Random.randBytes(10);
-      const ciphertext = await aead.encrypt(plaintext, aad);
-      const decryptedCiphertext = await aead.decrypt(ciphertext, aad);
-
-      expect(decryptedCiphertext).toEqual(plaintext);
-    }
-  });
-});
-
-// Helper classes and functions
-class ExceptionText {
-  static unsupportedTypeUrl(typeUrl: string): string {
-    return 'SecurityException: Key type URL ' + typeUrl + ' is not supported.';
-  }
-
-  static invalidKeyFormat(keyType: string): string {
-    return 'SecurityException: Could not parse the given Uint8Array as ' +
-        'a serialized proto of ' + keyType + '.';
-  }
-
-  static invalidKeyLength(expectedKeyLength: number, actualKeyLength: number):
-      string {
-    return 'SecurityException: Key is not of the correct length, expected length: ' +
-        expectedKeyLength + ', but got key of length: ' + actualKeyLength + '.';
-  }
-}
diff --git a/javascript/index.ts b/javascript/index.ts
deleted file mode 100644
index d236066..0000000
--- a/javascript/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/**
- * @fileoverview The entry point for the library. All public APIs must be
- * directly or indirectly exported from here.
- */
-
-export * as aead from './aead';
-export * as aeadSubtle from './aead/subtle';
-export * as binary from './binary';
-export * as binaryInsecure from './binary/insecure';
-export * as hybrid from './hybrid';
-export {generateNew as generateNewKeysetHandle, KeysetHandle} from './keyset_handle';
-export * as mac from './mac';
-export * as macSubtle from './mac/subtle';
-export * as signature from './signature';
-export * as signatureSubtle from './signature/subtle';
-export * as testing from './testing';
diff --git a/javascript/internal/BUILD.bazel b/javascript/internal/BUILD.bazel
deleted file mode 100644
index 8ff8965..0000000
--- a/javascript/internal/BUILD.bazel
+++ /dev/null
@@ -1,172 +0,0 @@
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_proto_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-load(":ts_library_from_closure.bzl", "ts_library_from_closure")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library_from_closure(
-    name = "proto",
-    namespace_aliases = {
-        "PbMessage": "jspb.Message",
-        "PbAesCmacKey": "proto.google.crypto.tink.AesCmacKey",
-        "PbAesCmacKeyFormat": "proto.google.crypto.tink.AesCmacKeyFormat",
-        "PbAesCmacParams": "proto.google.crypto.tink.AesCmacParams",
-        "PbAesCtrHmacAeadKey": "proto.google.crypto.tink.AesCtrHmacAeadKey",
-        "PbAesCtrHmacAeadKeyFormat": "proto.google.crypto.tink.AesCtrHmacAeadKeyFormat",
-        "PbAesCtrKey": "proto.google.crypto.tink.AesCtrKey",
-        "PbAesCtrKeyFormat": "proto.google.crypto.tink.AesCtrKeyFormat",
-        "PbAesCtrParams": "proto.google.crypto.tink.AesCtrParams",
-        "PbAesGcmKey": "proto.google.crypto.tink.AesGcmKey",
-        "PbAesGcmKeyFormat": "proto.google.crypto.tink.AesGcmKeyFormat",
-        "PbAesGcmSivKey": "proto.google.crypto.tink.AesGcmSivKey",
-        "PbAesGcmSivKeyFormat": "proto.google.crypto.tink.AesGcmSivKeyFormat",
-        "PbEcdsaKeyFormat": "proto.google.crypto.tink.EcdsaKeyFormat",
-        "PbEcdsaParams": "proto.google.crypto.tink.EcdsaParams",
-        "PbEcdsaPrivateKey": "proto.google.crypto.tink.EcdsaPrivateKey",
-        "PbEcdsaPublicKey": "proto.google.crypto.tink.EcdsaPublicKey",
-        "PbEcdsaSignatureEncoding": "proto.google.crypto.tink.EcdsaSignatureEncoding",
-        "PbEciesAeadDemParams": "proto.google.crypto.tink.EciesAeadDemParams",
-        "PbEciesAeadHkdfKeyFormat": "proto.google.crypto.tink.EciesAeadHkdfKeyFormat",
-        "PbEciesAeadHkdfParams": "proto.google.crypto.tink.EciesAeadHkdfParams",
-        "PbEciesAeadHkdfPrivateKey": "proto.google.crypto.tink.EciesAeadHkdfPrivateKey",
-        "PbEciesAeadHkdfPublicKey": "proto.google.crypto.tink.EciesAeadHkdfPublicKey",
-        "PbEciesHkdfKemParams": "proto.google.crypto.tink.EciesHkdfKemParams",
-        "PbPointFormat": "proto.google.crypto.tink.EcPointFormat",
-        "PbEllipticCurveType": "proto.google.crypto.tink.EllipticCurveType",
-        "PbEncryptedKeyset": "proto.google.crypto.tink.EncryptedKeyset",
-        "PbHashType": "proto.google.crypto.tink.HashType",
-        "PbHmacKey": "proto.google.crypto.tink.HmacKey",
-        "PbHmacKeyFormat": "proto.google.crypto.tink.HmacKeyFormat",
-        "PbHmacParams": "proto.google.crypto.tink.HmacParams",
-        "PbKeyData": "proto.google.crypto.tink.KeyData",
-        "PbKeyMaterialType": "proto.google.crypto.tink.KeyData.KeyMaterialType",
-        "PbKeyset": "proto.google.crypto.tink.Keyset",
-        "PbKeysetKey": "proto.google.crypto.tink.Keyset.Key",
-        "PbKeysetInfo": "proto.google.crypto.tink.KeysetInfo",
-        "PbKeyStatusType": "proto.google.crypto.tink.KeyStatusType",
-        "PbKeyTemplate": "proto.google.crypto.tink.KeyTemplate",
-        "PbOutputPrefixType": "proto.google.crypto.tink.OutputPrefixType",
-        "PbXChaCha20Poly1305Key": "proto.google.crypto.tink.XChaCha20Poly1305Key",
-        "PbXChaCha20Poly1305KeyFormat": "proto.google.crypto.tink.XChaCha20Poly1305KeyFormat",
-    },
-    deps = [
-        ":aes_cmac_closure_proto",
-        ":aes_ctr_closure_proto",
-        ":aes_ctr_hmac_aead_closure_proto",
-        ":aes_gcm_closure_proto",
-        ":aes_gcm_siv_closure_proto",
-        ":common_closure_proto",
-        ":ecdsa_closure_proto",
-        ":ecies_aead_hkdf_closure_proto",
-        ":hmac_closure_proto",
-        ":tink_closure_proto",
-        ":xchacha20_poly1305_closure_proto",
-        "@io_bazel_rules_closure//closure/protobuf:jspb",
-    ],
-)
-
-ts_library(
-    name = "internal",
-    srcs = [
-        "binary_keyset_reader.ts",
-        "binary_keyset_writer.ts",
-        "cleartext_keyset_handle.ts",
-        "crypto_format.ts",
-        "key_manager.ts",
-        "keyset_handle.ts",
-        "keyset_reader.ts",
-        "keyset_writer.ts",
-        "primitive_set.ts",
-        "primitive_wrapper.ts",
-        "registry.ts",
-        "util.ts",
-    ],
-    deps = [
-        ":proto",
-        "//aead/internal",
-        "//exception",
-        "//subtle",
-    ],
-)
-
-ts_library(
-    name = "internal_tests",
-    testonly = True,
-    srcs = [
-        "binary_keyset_reader_test.ts",
-        "binary_keyset_writer_test.ts",
-        "cleartext_keyset_handle_test.ts",
-        "crypto_format_test.ts",
-        "keyset_handle_test.ts",
-        "primitive_set_test.ts",
-        "proto_test.ts",
-        "registry_test.ts",
-        "util_test.ts",
-    ],
-    deps = [
-        ":internal",
-        ":proto",
-        "//aead",
-        "//exception",
-        "//hybrid",
-        "//mac",
-        "//subtle",
-        "//testing/internal",
-        "@npm//@types/jasmine",
-    ],
-)
-
-closure_proto_library(
-    name = "aes_cmac_closure_proto",
-    deps = ["//proto:aes_cmac_proto"],
-)
-
-closure_proto_library(
-    name = "aes_ctr_hmac_aead_closure_proto",
-    deps = ["//proto:aes_ctr_hmac_aead_proto"],
-)
-
-closure_proto_library(
-    name = "aes_ctr_closure_proto",
-    deps = ["//proto:aes_ctr_proto"],
-)
-
-closure_proto_library(
-    name = "aes_gcm_closure_proto",
-    deps = ["//proto:aes_gcm_proto"],
-)
-
-closure_proto_library(
-    name = "aes_gcm_siv_closure_proto",
-    deps = ["//proto:aes_gcm_siv_proto"],
-)
-
-closure_proto_library(
-    name = "common_closure_proto",
-    deps = ["//proto:common_proto"],
-)
-
-closure_proto_library(
-    name = "ecdsa_closure_proto",
-    deps = ["//proto:ecdsa_proto"],
-)
-
-closure_proto_library(
-    name = "ecies_aead_hkdf_closure_proto",
-    deps = ["//proto:ecies_aead_hkdf_proto"],
-)
-
-closure_proto_library(
-    name = "hmac_closure_proto",
-    deps = ["//proto:hmac_proto"],
-)
-
-closure_proto_library(
-    name = "tink_closure_proto",
-    deps = ["//proto:tink_proto"],
-)
-
-closure_proto_library(
-    name = "xchacha20_poly1305_closure_proto",
-    deps = ["//proto:xchacha20_poly1305_proto"],
-)
diff --git a/javascript/internal/binary_keyset_reader.ts b/javascript/internal/binary_keyset_reader.ts
deleted file mode 100644
index d00a408..0000000
--- a/javascript/internal/binary_keyset_reader.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-
-import {KeysetReader} from './keyset_reader';
-import {PbEncryptedKeyset, PbKeyset} from './proto';
-
-/**
- * BinaryKeysetReader knows how to read a keyset or an encrypted keyset
- * serialized to binary format.
- *
- * @final
- */
-export class BinaryKeysetReader implements KeysetReader {
-  constructor(private readonly serializedKeyset: Uint8Array) {}
-
-  static withUint8Array(serializedKeyset: Uint8Array): BinaryKeysetReader {
-    if (!serializedKeyset) {
-      throw new SecurityException('Serialized keyset has to be non-null.');
-    }
-    return new BinaryKeysetReader(serializedKeyset);
-  }
-
-  read() {
-    let keyset: PbKeyset;
-    try {
-      keyset = PbKeyset.deserializeBinary(this.serializedKeyset);
-    } catch (e) {
-      throw new SecurityException(
-          'Could not parse the given serialized proto as a keyset proto.');
-    }
-    if (keyset.getKeyList().length === 0) {
-      throw new SecurityException(
-          'Could not parse the given serialized proto as a keyset proto.');
-    }
-    return keyset;
-  }
-
-  readEncrypted(): PbEncryptedKeyset {
-    throw new SecurityException('Not implemented yet.');
-  }
-}
diff --git a/javascript/internal/binary_keyset_reader_test.ts b/javascript/internal/binary_keyset_reader_test.ts
deleted file mode 100644
index 3d0af97..0000000
--- a/javascript/internal/binary_keyset_reader_test.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Random from '../subtle/random';
-import {assertMessageEquals} from '../testing/internal/test_utils';
-
-import {BinaryKeysetReader} from './binary_keyset_reader';
-import {PbKeyData, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from './proto';
-
-describe('binary keyset reader test', function() {
-  it('read, invalid serialized keyset proto', function() {
-    for (let i = 0; i < 2; i++) {
-      // The Uint8Array is not a serialized keyset.
-      const reader = BinaryKeysetReader.withUint8Array(new Uint8Array(i));
-
-      try {
-        reader.read();
-        fail('An exception should be thrown.');
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerialization());
-      }
-    }
-  });
-
-  it('read', function() {
-    // Create keyset proto and serialize it.
-    const keyset = new PbKeyset();
-    // The for cycle starts from 1 as setting any proto value to 0 sets it to
-    // null and after serialization and deserialization null is changed to
-    // undefined and the assertion at the end fails (unless you compare the
-    // keyset and newly created keyset value by value).
-    for (let i = 1; i < 20; i++) {
-      let outputPrefix;
-      switch (i % 3) {
-        case 0:
-          outputPrefix = PbOutputPrefixType.TINK;
-          break;
-        case 1:
-          outputPrefix = PbOutputPrefixType.RAW;
-          break;
-        default:
-          outputPrefix = PbOutputPrefixType.LEGACY;
-      }
-      keyset.addKey(createDummyKeysetKey(
-          /* keyId = */ i, outputPrefix, /* enabled = */ i % 4 < 3));
-    }
-    keyset.setPrimaryKeyId(1);
-
-    const serializedKeyset = keyset.serializeBinary();
-
-    // Read the keyset proto serialization.
-    const reader = BinaryKeysetReader.withUint8Array(serializedKeyset);
-    const keysetFromReader = reader.read();
-
-    // Test that it returns the same object as was created.
-    assertMessageEquals(keysetFromReader, keyset);
-  });
-
-  it('read encrypted, not implemented yet', function() {
-    const reader = BinaryKeysetReader.withUint8Array(new Uint8Array(10));
-
-    try {
-      reader.readEncrypted();
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.notImplemented());
-    }
-  });
-});
-
-////////////////////////////////////////////////////////////////////////////////
-// helper functions and classes for tests
-////////////////////////////////////////////////////////////////////////////////
-
-/**
- * Class which holds texts for each type of exception.
- * @final
- */
-class ExceptionText {
-  static notImplemented(): string {
-    return 'SecurityException: Not implemented yet.';
-  }
-
-  static nullKeyset(): string {
-    return 'SecurityException: Serialized keyset has to be non-null.';
-  }
-
-  static invalidSerialization(): string {
-    return 'SecurityException: Could not parse the given serialized proto as ' +
-        'a keyset proto.';
-  }
-}
-
-/** Function for creating keys for testing purposes. */
-function createDummyKeysetKey(
-    keyId: number, outputPrefix: PbOutputPrefixType,
-    enabled: boolean): PbKeysetKey {
-  const key = new PbKeysetKey();
-
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-  key.setOutputPrefixType(outputPrefix);
-  key.setKeyId(keyId);
-
-  // Set some key data.
-  key.setKeyData(new PbKeyData());
-  key.getKeyData()?.setTypeUrl('SOME_KEY_TYPE_URL_' + keyId.toString());
-  key.getKeyData()?.setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
-  key.getKeyData()?.setValue(Random.randBytes(10));
-
-  return key;
-}
diff --git a/javascript/internal/binary_keyset_writer.ts b/javascript/internal/binary_keyset_writer.ts
deleted file mode 100644
index 64c315d..0000000
--- a/javascript/internal/binary_keyset_writer.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {KeysetWriter} from './keyset_writer';
-import {PbEncryptedKeyset, PbKeyset} from './proto';
-
-/**
- * KeysetWriter knows how to write a keyset or an encrypted keyset.
- *
- * @final
- */
-export class BinaryKeysetWriter implements KeysetWriter {
-  encodeBinary(keyset: PbKeyset|PbEncryptedKeyset): Uint8Array {
-    // keep serializeBinary calls monomorphic
-    if (keyset instanceof PbKeyset) {
-      return keyset.serializeBinary();
-    }
-    if (keyset instanceof PbEncryptedKeyset) {
-      return keyset.serializeBinary();
-    }
-    throw new SecurityException('unexpected type for keyset.');
-  }
-}
diff --git a/javascript/internal/binary_keyset_writer_test.ts b/javascript/internal/binary_keyset_writer_test.ts
deleted file mode 100644
index ba908f3..0000000
--- a/javascript/internal/binary_keyset_writer_test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {assertMessageEquals, createKeyset} from '../testing/internal/test_utils';
-
-import {BinaryKeysetReader} from './binary_keyset_reader';
-import {BinaryKeysetWriter} from './binary_keyset_writer';
-
-describe('binary keyset writer test', function() {
-  it('get serialized key set', function() {
-    const dummyKeyset = createKeyset();
-
-    // Write the keyset.
-    const writer = new BinaryKeysetWriter();
-    const serializedKeyset = writer.encodeBinary(dummyKeyset);
-
-    // Read the keyset proto serialization.
-    const reader = BinaryKeysetReader.withUint8Array(serializedKeyset);
-    const keysetFromReader = reader.read();
-
-    // Test that it returns the same object as was created.
-    assertMessageEquals(keysetFromReader, dummyKeyset);
-  });
-});
diff --git a/javascript/internal/cleartext_keyset_handle.ts b/javascript/internal/cleartext_keyset_handle.ts
deleted file mode 100644
index 6aa8c6a..0000000
--- a/javascript/internal/cleartext_keyset_handle.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {BinaryKeysetReader} from './binary_keyset_reader';
-import {BinaryKeysetWriter} from './binary_keyset_writer';
-import {KeysetHandle} from './keyset_handle';
-
-const binaryKeysetWriter = new BinaryKeysetWriter();
-
-/**
- * Static methods for reading or writing cleartext keysets.
- *
- * @final
- */
-export class CleartextKeysetHandle {
-  /**
-   * Serializes a KeysetHandle to binary.
-   *
-   */
-  static serializeToBinary(keysetHandle: KeysetHandle): Uint8Array {
-    return binaryKeysetWriter.encodeBinary(keysetHandle.getKeyset());
-  }
-
-  /**
-   * Creates a KeysetHandle from a binary representation of a keyset.
-   *
-   */
-  static deserializeFromBinary(keysetBinary: Uint8Array): KeysetHandle {
-    const reader = BinaryKeysetReader.withUint8Array(keysetBinary);
-    const keysetFromReader = reader.read();
-    return new KeysetHandle(keysetFromReader);
-  }
-}
diff --git a/javascript/internal/cleartext_keyset_handle_test.ts b/javascript/internal/cleartext_keyset_handle_test.ts
deleted file mode 100644
index 73e8fd3..0000000
--- a/javascript/internal/cleartext_keyset_handle_test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {createKeyset} from '../testing/internal/test_utils';
-
-import {CleartextKeysetHandle} from './cleartext_keyset_handle';
-import {KeysetHandle} from './keyset_handle';
-
-describe('cleartext keyset handle test', function() {
-  it('deserialize from binary', function() {
-    const keyset1 = createKeyset();
-    const keysetHandle =
-        CleartextKeysetHandle.deserializeFromBinary(keyset1.serializeBinary());
-    const keyset2 = keysetHandle.getKeyset();
-    expect(keyset2.getPrimaryKeyId()).toBe(keyset1.getPrimaryKeyId());
-    expect(keyset2.getKeyList()).toEqual(keyset2.getKeyList());
-  });
-
-  it('serialize to binary', function() {
-    const keyset = createKeyset();
-    const keysetHandle = new KeysetHandle(keyset);
-
-    const keysetBinary = CleartextKeysetHandle.serializeToBinary(keysetHandle);
-    expect(keyset.serializeBinary()).toEqual(keysetBinary);
-  });
-});
diff --git a/javascript/internal/crypto_format.ts b/javascript/internal/crypto_format.ts
deleted file mode 100644
index c590cc2..0000000
--- a/javascript/internal/crypto_format.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-import {SecurityException} from '../exception/security_exception';
-
-import {PbKeysetKey, PbOutputPrefixType} from './proto';
-
-/**
- * Constants and methods that deal with the format of the outputs handled by
- * Tink.
- *
- * @static
- * @final
- */
-export class CryptoFormat {
-  /**
-   * Generates the prefix for the outputs handled by the given 'key'.
-   * Throws an exception if the prefix type of 'key' is invalid.
-   *
-   *
-   */
-  static getOutputPrefix(key: PbKeysetKey): Uint8Array {
-    switch (key.getOutputPrefixType()) {
-      case PbOutputPrefixType.LEGACY:
-
-      // fall through
-      case PbOutputPrefixType.CRUNCHY:
-        return CryptoFormat.makeOutputPrefix(
-            key.getKeyId(), CryptoFormat.LEGACY_START_BYTE);
-      case PbOutputPrefixType.TINK:
-        return CryptoFormat.makeOutputPrefix(
-            key.getKeyId(), CryptoFormat.TINK_START_BYTE);
-      case PbOutputPrefixType.RAW:
-        return CryptoFormat.RAW_PREFIX;
-      default:
-        throw new SecurityException('Unsupported key prefix type.');
-    }
-  }
-
-  /**
-   * Makes output prefix which consits of 4 bytes of key id in Big Endian
-   * representation followed by 1 byte of key type identifier.
-   *
-   * @static
-   *
-   */
-  private static makeOutputPrefix(keyId: number, keyTypeIdentifier: number):
-      Uint8Array {
-    let res = [keyTypeIdentifier];
-    res = res.concat(CryptoFormat.numberAsBigEndian(keyId));
-    return new Uint8Array(res);
-  }
-
-  /**
-   * Returns the given number as Uint8Array in Big Endian format.
-   *
-   * Given number has to be a non-negative integer smaller than 2^32.
-   *
-   * @static
-   *
-   */
-  private static numberAsBigEndian(n: number): number[] {
-    if (!Number.isInteger(n) || n < 0 || n >= 2 ** 32) {
-      throw new InvalidArgumentsException(
-          'Number has to be unsigned 32-bit integer.');
-    }
-    const numberOfBytes = 4;
-    const res = new Array(numberOfBytes);
-    for (let i = 0; i < numberOfBytes; i++) {
-      res[i] = 255 & n >> 8 * (numberOfBytes - i - 1);
-    }
-    return res;
-  }
-
-  /**
-   * Prefix size of Tink and Legacy key types.
-   */
-  static readonly NON_RAW_PREFIX_SIZE = 5;
-
-  /**
-   * Prefix size of Legacy key types.
-   */
-  static readonly LEGACY_PREFIX_SIZE = CryptoFormat.NON_RAW_PREFIX_SIZE;
-
-  /**
-   * Legacy starts with 0 and is followed by 4-byte key id.
-   */
-  static readonly LEGACY_START_BYTE = 0;
-
-  /**
-   * Prefix size of Tink key types.
-   */
-  static readonly TINK_PREFIX_SIZE = CryptoFormat.NON_RAW_PREFIX_SIZE;
-
-  /**
-   * Tink starts with 1 and is followed by 4-byte key id.
-   */
-  static readonly TINK_START_BYTE = 1;
-
-  /**
-   * Raw prefix should have length 0.
-   */
-  static readonly RAW_PREFIX_SIZE = 0;
-
-  /**
-   * Raw prefix is empty Uint8Array.
-   */
-  static readonly RAW_PREFIX = new Uint8Array(0);
-}
diff --git a/javascript/internal/crypto_format_test.ts b/javascript/internal/crypto_format_test.ts
deleted file mode 100644
index f93d75a..0000000
--- a/javascript/internal/crypto_format_test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {CryptoFormat} from './crypto_format';
-import {PbKeysetKey as PbKeysetKey, PbOutputPrefixType} from './proto';
-
-describe('crypto format test', function() {
-  it('constants', async function() {
-    expect(CryptoFormat.RAW_PREFIX_SIZE).toBe(0);
-    expect(CryptoFormat.NON_RAW_PREFIX_SIZE).toBe(5);
-    expect(CryptoFormat.LEGACY_PREFIX_SIZE).toBe(5);
-    expect(CryptoFormat.TINK_PREFIX_SIZE).toBe(5);
-
-    expect(CryptoFormat.LEGACY_START_BYTE).toBe(0x00);
-    expect(CryptoFormat.TINK_START_BYTE).toBe(0x01);
-  });
-
-  it('get output prefix unknown prefix type', async function() {
-    let key = new PbKeysetKey()
-                  .setOutputPrefixType(PbOutputPrefixType.UNKNOWN_PREFIX)
-                  .setKeyId(2864434397);
-
-    try {
-      CryptoFormat.getOutputPrefix(key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: Unsupported key prefix type.');
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('get output prefix invalid key id', async function() {
-    // Key id has to be an unsigned 32-bit integer.
-    const invalidKeyIds = [0.2, -10, 2**32];
-    let key = new PbKeysetKey().setOutputPrefixType(PbOutputPrefixType.TINK);
-
-    const invalidKeyIdsLength = invalidKeyIds.length;
-    for (let i = 0; i < invalidKeyIdsLength; i++) {
-      key.setKeyId(invalidKeyIds[i]);
-      try {
-        CryptoFormat.getOutputPrefix(key);
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(
-                'InvalidArgumentsException: Number has to be unsigned 32-bit integer.');
-        continue;
-      }
-      fail('An exception should be thrown for i: ' + i + '.');
-    }
-  });
-
-  it('get output prefix tink', async function() {
-    const key = new PbKeysetKey()
-                    .setOutputPrefixType(PbOutputPrefixType.TINK)
-                    .setKeyId(2864434397);
-    const expectedResult =
-        new Uint8Array([CryptoFormat.TINK_START_BYTE, 0xAA, 0xBB, 0xCC, 0xDD]);
-
-    const actualResult = CryptoFormat.getOutputPrefix(key);
-    expect(actualResult).toEqual(expectedResult);
-  });
-
-  it('get output prefix legacy', async function() {
-    const key = new PbKeysetKey()
-                    .setOutputPrefixType(PbOutputPrefixType.LEGACY)
-                    .setKeyId(16909060);
-    const expectedResult = new Uint8Array(
-        [CryptoFormat.LEGACY_START_BYTE, 0x01, 0x02, 0x03, 0x04]);
-
-    const actualResult = CryptoFormat.getOutputPrefix(key);
-    expect(actualResult).toEqual(expectedResult);
-  });
-
-  it('get output prefix raw', async function() {
-    const key = new PbKeysetKey()
-                    .setOutputPrefixType(PbOutputPrefixType.RAW)
-                    .setKeyId(370491921);
-    const expectedResult = new Uint8Array(0);
-
-    const actualResult = CryptoFormat.getOutputPrefix(key);
-    expect(actualResult).toEqual(expectedResult);
-  });
-});
diff --git a/javascript/internal/key_manager.ts b/javascript/internal/key_manager.ts
deleted file mode 100644
index 59b2f61..0000000
--- a/javascript/internal/key_manager.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbKeyData, PbMessage} from './proto';
-import {Constructor} from './util';
-
-/**
- * An auxiliary container for methods that generate new keys.
- * Those methods are separate from KeyManager as their functionality is
- * independent of the primitive of the corresponding KeyManager.
- *
- */
-export interface KeyFactory {
-  /**
-   * Generates a new random key according to 'keyFormat'.
-   *
-   * @param keyFormat is either a KeyFormat
-   *     proto or a serialized KeyFormat proto
-   * @return the new generated key
-   */
-  newKey(keyFormat: PbMessage|Uint8Array): PbMessage|Promise<PbMessage>;
-
-  /**
-   * Generates a new random key based on the "serialized_key_format" and returns
-   * it as a KeyData proto.
-   *
-   */
-  newKeyData(serializedKeyFormat: Uint8Array): PbKeyData|Promise<PbKeyData>;
-
-  /**
-   * Returns a public key data extracted from the given serialized private key.
-   *
-   */
-  getPublicKeyData?: (serializedPrivateKey: Uint8Array) => PbKeyData;
-}
-
-export interface PrivateKeyFactory extends KeyFactory {
-  getPublicKeyData(serializedPrivateKey: Uint8Array): PbKeyData;
-}
-
-/**
- * A KeyManager "understands" keys of a specific key type: it can generate keys
- * of the supported type and create primitives for supported keys.
- * A key type is identified by the global name of the protocol buffer that holds
- * the corresponding key material, and is given by typeUrl-field of
- * KeyData-protocol buffer.
- *
- * The template parameter P denotes the primitive corresponding to the keys
- * handled by this manager.
- */
-export interface KeyManager<P> {
-  /**
-   * Constructs an instance of primitive P for a given key.
-   *
-   * @param key is either a KeyData proto or a supported
-   *     key proto
-   */
-  getPrimitive(primitiveType: Constructor<P>, key: PbKeyData|PbMessage):
-      Promise<P>;
-
-  /**
-   * Returns true if this KeyManager supports keyType.
-   *
-   */
-  doesSupport(keyType: string): boolean;
-
-  /**
-   * Returns the URL which identifies the keys managed by this KeyManager.
-   *
-   */
-  getKeyType(): string;
-
-  /**
-   * Returns the type of primitive which can be generated by this KeyManager.
-   *
-   * This function is specific for javascript to allow verifying that
-   * the primitive returned by getPrimitive function implements certain
-   * primitive interface (e.g. that the primitive is AEAD).
-   *
-   */
-  getPrimitiveType(): Constructor<P>;
-
-  /**
-   * Returns the version of this KeyManager.
-   *
-   */
-  getVersion(): number;
-
-  /**
-   * Returns a factory that generates keys of the key type handled by this
-   * manager.
-   *
-   */
-  getKeyFactory(): KeyFactory;
-}
diff --git a/javascript/internal/keyset_handle.ts b/javascript/internal/keyset_handle.ts
deleted file mode 100644
index 015ac83..0000000
--- a/javascript/internal/keyset_handle.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead/internal/aead';
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-import {SecurityException} from '../exception/security_exception';
-import * as Random from '../subtle/random';
-
-import * as KeyManager from './key_manager';
-import {KeysetReader} from './keyset_reader';
-import {KeysetWriter} from './keyset_writer';
-import * as PrimitiveSet from './primitive_set';
-import {PbKeyData, PbKeyMaterialType, PbKeyset, PbKeysetKey, PbKeyStatusType, PbKeyTemplate} from './proto';
-import * as Registry from './registry';
-import * as Util from './util';
-
-/**
- * Keyset handle provide abstracted access to Keysets, to limit the exposure of
- * actual protocol buffers that hold sensitive key material.
- *
- * @final
- */
-export class KeysetHandle {
-  private readonly keyset_: PbKeyset;
-
-  constructor(keyset: PbKeyset) {
-    Util.validateKeyset(keyset);
-    this.keyset_ = keyset;
-  }
-
-  /**
-   * Returns a primitive that uses key material from this keyset handle. If
-   * opt_customKeyManager is defined then the provided key manager is used to
-   * instantiate primitives. Otherwise key manager from Registry is used.
-   */
-  async getPrimitive<P>(
-      primitiveType: Util.Constructor<P>,
-      opt_customKeyManager?: KeyManager.KeyManager<P>|null): Promise<P> {
-    if (!primitiveType) {
-      throw new InvalidArgumentsException('primitive type must be non-null');
-    }
-    const primitiveSet =
-        await this.getPrimitiveSet(primitiveType, opt_customKeyManager);
-    return Registry.wrap(primitiveSet);
-  }
-
-  /**
-   * Creates a set of primitives corresponding to the keys with status Enabled
-   * in the given keysetHandle, assuming all the correspoding key managers are
-   * present (keys with status different from Enabled are skipped). If provided
-   * uses customKeyManager instead of registered key managers for keys supported
-   * by the customKeyManager.
-   *
-   * Visible for testing.
-   */
-  async getPrimitiveSet<P>(
-      primitiveType: Util.Constructor<P>,
-      opt_customKeyManager?: KeyManager.KeyManager<P>|
-      null): Promise<PrimitiveSet.PrimitiveSet<P>> {
-    const primitiveSet = new PrimitiveSet.PrimitiveSet<P>(primitiveType);
-    const keys = this.keyset_.getKeyList();
-    const keysLength = keys.length;
-    for (let i = 0; i < keysLength; i++) {
-      const key = keys[i];
-      if (key.getStatus() === PbKeyStatusType.ENABLED) {
-        const keyData = key.getKeyData();
-        if (!keyData) {
-          throw new SecurityException('Key data has to be non null.');
-        }
-        let primitive;
-        if (opt_customKeyManager &&
-            opt_customKeyManager.getKeyType() === keyData.getTypeUrl()) {
-          primitive =
-              await opt_customKeyManager.getPrimitive(primitiveType, keyData);
-        } else {
-          primitive = await Registry.getPrimitive<P>(primitiveType, keyData);
-        }
-        const entry = primitiveSet.addPrimitive(primitive, key);
-        if (key.getKeyId() === this.keyset_.getPrimaryKeyId()) {
-          primitiveSet.setPrimary(entry);
-        }
-      }
-    }
-    return primitiveSet;
-  }
-
-  /**
-   * Encrypts the underlying keyset with the provided masterKeyAead wnd writes
-   * the resulting encryptedKeyset to the given writer which must be non-null.
-   *
-   *
-   */
-  async write(writer: KeysetWriter, masterKeyAead: Aead) {
-    // TODO implement
-    throw new SecurityException('KeysetHandle -- write: Not implemented yet.');
-  }
-
-  /**
-   * Writes this keyset using `writer` if and only if the keyset doesn't contain
-   * any secret key material.
-   *
-   * This can be used to persist public keysets or envelope encryption keysets.
-   * Use `CleartextKeysetHandle` to persist keysets containing secret key
-   * material.
-   */
-  writeNoSecret(writer: KeysetWriter): Uint8Array {
-    assertNoSecretKeyMaterial(this.keyset_);
-    return writer.encodeBinary(this.keyset_);
-  }
-
-  /**
-   * Returns the keyset held by this KeysetHandle.
-   *
-   */
-  getKeyset(): PbKeyset {
-    return this.keyset_;
-  }
-
-  /**
-   * If the managed keyset contains private keys, returns a `KeysetHandle` of
-   * the public keys.
-   */
-  getPublicKeysetHandle(): KeysetHandle {
-    const publicKeyset = new PbKeyset();
-    for (const key of this.keyset_.getKeyList()) {
-      publicKeyset.addKey(key.clone().setKeyData(createPublicKeyData(
-          nonNull('Key data', key.getKeyData()))));
-    }
-    publicKeyset.setPrimaryKeyId(this.keyset_.getPrimaryKeyId());
-    return new KeysetHandle(publicKeyset);
-  }
-}
-
-function nonNull<T>(desc: string, value: T|null|undefined): T {
-  if (value == null) {
-    throw new SecurityException(`${desc} has to be non null.`);
-  }
-  return value;
-}
-
-function createPublicKeyData(privateKeyData: PbKeyData): PbKeyData {
-  if (privateKeyData.getKeyMaterialType() !==
-      PbKeyData.KeyMaterialType.ASYMMETRIC_PRIVATE) {
-    throw new SecurityException('The keyset contains a non-private key');
-  }
-  return Registry.getPublicKeyData(
-      privateKeyData.getTypeUrl(), privateKeyData.getValue_asU8());
-}
-
-/**
- * Validates that `keyset` doesn't contain any secret key material.
- *
- * @throws SecurityException if `keyset` contains secret key material.
- */
-function assertNoSecretKeyMaterial(keyset: PbKeyset) {
-  for (const key of keyset.getKeyList()) {
-    const keyData = nonNull('Key data', key.getKeyData());
-    if (isSecretKeyMaterialType(keyData.getKeyMaterialType())) {
-      throw new SecurityException('Keyset contains secret key material.');
-    }
-  }
-}
-
-/** Returns true if the key material type is secret. */
-function isSecretKeyMaterialType(type: PbKeyMaterialType) {
-  return type === PbKeyMaterialType.UNKNOWN_KEYMATERIAL ||
-      type === PbKeyMaterialType.SYMMETRIC ||
-      type === PbKeyMaterialType.ASYMMETRIC_PRIVATE;
-}
-
-/**
- * Creates a KeysetHandle from an encrypted keyset obtained via reader, using
- * masterKeyAead to decrypt the keyset.
- *
- *
- */
-export async function read(
-    reader: KeysetReader, masterKeyAead: Aead): Promise<KeysetHandle> {
-  // TODO implement
-  throw new SecurityException('KeysetHandle -- read: Not implemented yet.');
-}
-
-/**
- * Returns a new KeysetHandle that contains a single new key generated
- * according to keyTemplate.
- *
- *
- */
-export async function generateNew(keyTemplate: PbKeyTemplate):
-    Promise<KeysetHandle> {
-  // TODO(thaidn): move this to a key manager.
-  const keyset = await generateNewKeyset_(keyTemplate);
-  return new KeysetHandle(keyset);
-}
-
-/**
- * Generates a new Keyset that contains a single new key generated
- * according to keyTemplate.
- *
- */
-async function generateNewKeyset_(keyTemplate: PbKeyTemplate):
-    Promise<PbKeyset> {
-  const key = (new PbKeysetKey())
-                  .setStatus(PbKeyStatusType.ENABLED)
-                  .setOutputPrefixType(keyTemplate.getOutputPrefixType());
-  const keyId = generateNewKeyId_();
-  key.setKeyId(keyId);
-  const keyData = await Registry.newKeyData(keyTemplate);
-  key.setKeyData(keyData);
-  const keyset = new PbKeyset();
-  keyset.addKey(key);
-  keyset.setPrimaryKeyId(keyId);
-  return keyset;
-}
-
-/**
- * Generates a new random key ID.
- *
- * @return The key ID.
- */
-function generateNewKeyId_(): number {
-  const bytes = Random.randBytes(4);
-  let value = 0;
-  for (let i = 0; i < bytes.length; i++) {
-    value += (bytes[i] & 255) << i * 8;
-  }
-
-  // Make sure the key ID is a positive integer smaller than 2^32.
-  return Math.abs(value) % 2 ** 32;
-}
-
-/**
- * Creates a KeysetHandle from a keyset, obtained via reader, which
- * must contain no secret key material.
- *
- * This can be used to load public keysets or envelope encryption keysets.
- * Users that need to load cleartext keysets can use CleartextKeysetHandle.
- *
- */
-export function readNoSecret(reader: KeysetReader): KeysetHandle {
-  if (reader === null) {
-    throw new SecurityException('Reader has to be non-null.');
-  }
-  const keyset = reader.read();
-  assertNoSecretKeyMaterial(keyset);
-  return new KeysetHandle(keyset);
-}
diff --git a/javascript/internal/keyset_handle_test.ts b/javascript/internal/keyset_handle_test.ts
deleted file mode 100644
index 08e3f3c..0000000
--- a/javascript/internal/keyset_handle_test.ts
+++ /dev/null
@@ -1,793 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {SecurityException} from '../exception/security_exception';
-import {HybridDecrypt, HybridEncrypt} from '../hybrid';
-import * as HybridConfig from '../hybrid/hybrid_config';
-import {HybridKeyTemplates} from '../hybrid/hybrid_key_templates';
-import {Mac} from '../mac';
-import * as Bytes from '../subtle/bytes';
-import * as Random from '../subtle/random';
-import {assertExists, assertMessageEquals, createKeyset} from '../testing/internal/test_utils';
-
-import {BinaryKeysetReader} from './binary_keyset_reader';
-import {BinaryKeysetWriter} from './binary_keyset_writer';
-import {CleartextKeysetHandle} from './cleartext_keyset_handle';
-import {KeyFactory, KeyManager} from './key_manager';
-import {generateNew, KeysetHandle, read, readNoSecret} from './keyset_handle';
-import {PbKeyData, PbKeyMaterialType, PbKeyset, PbKeysetKey, PbKeyStatusType, PbMessage, PbOutputPrefixType} from './proto';
-import * as Registry from './registry';
-import {Constructor} from './util';
-
-describe('KeysetHandle', () => {
-  beforeEach(() => {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-
-    HybridConfig.register();
-  });
-
-  afterEach(() => {
-    Registry.reset();
-
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  describe('constructor', () => {
-    it('throws with empty list of keys', async () => {
-      const keyset = new PbKeyset().setKeyList([]);
-      expect(() => new KeysetHandle(keyset))
-          .toThrowError(
-              SecurityException,
-              'Keyset should be non null and must contain at least one key.');
-    });
-
-    it('does not throw for valid keyset protos', async () => {
-      const keyset = createKeyset();
-      expect(() => new KeysetHandle(keyset)).not.toThrow();
-    });
-  });
-
-  describe('getKeyset', () => {
-    it('returns the underlying keyset proto', async () => {
-      const keyset = createKeyset();
-      const keysetHandle = new KeysetHandle(keyset);
-
-      const result = keysetHandle.getKeyset();
-      expect(result).toEqual(keyset);
-    });
-  });
-
-  describe('read', () => {
-    it('is not yet implemented', async () => {
-      const keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-      const keysetHandle = await generateNew(keyTemplate);
-      const serializedKeyset =
-          CleartextKeysetHandle.serializeToBinary(keysetHandle);
-      const keysetReader = new BinaryKeysetReader(serializedKeyset);
-      const aead = await keysetHandle.getPrimitive<Aead>(Aead);
-
-      await expectAsync(read(keysetReader, aead))
-          .toBeRejectedWithError(
-              SecurityException, 'KeysetHandle -- read: Not implemented yet.');
-    });
-  });
-
-  describe('generateNew', () => {
-    it('generates new keyset handles given a key template', async () => {
-      const keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
-      const keysetHandle = await generateNew(keyTemplate);
-      const keyset = keysetHandle.getKeyset();
-      expect(1).toBe(keyset.getKeyList().length);
-
-      const key = keyset.getKeyList()[0];
-      expect(keyset.getPrimaryKeyId()).toBe(key.getKeyId());
-      expect(keyTemplate.getOutputPrefixType()).toBe(key.getOutputPrefixType());
-      expect(PbKeyStatusType.ENABLED).toBe(key.getStatus());
-
-      const keyData = assertExists(key.getKeyData());
-      expect(keyTemplate.getTypeUrl()).toBe(keyData.getTypeUrl());
-
-      const aead = await keysetHandle.getPrimitive(Aead);
-      const plaintext = Random.randBytes(20);
-      const ciphertext = await aead.encrypt(plaintext);
-      expect(await aead.decrypt(ciphertext)).toEqual(plaintext);
-    });
-  });
-
-  describe('write', () => {
-    it('is not yet implemented', async () => {
-      const keyset = createKeysetAndInitializeRegistry(Aead);
-      const keysetHandle = new KeysetHandle(keyset);
-      const keysetWriter = new BinaryKeysetWriter();
-      const aead = await keysetHandle.getPrimitive<Aead>(Aead);
-
-      await expectAsync(keysetHandle.write(keysetWriter, aead))
-          .toBeRejectedWithError(
-              SecurityException, 'KeysetHandle -- write: Not implemented yet.');
-    });
-  });
-
-  describe('getPrimitive', () => {
-    it('aead', async () => {
-      const keyset = createKeysetAndInitializeRegistry(Aead);
-      const keysetHandle = new KeysetHandle(keyset);
-
-      const aead = await keysetHandle.getPrimitive<Aead>(Aead);
-
-      // Test the aead primitive returned by getPrimitive method.
-      const plaintext = new Uint8Array([1, 2, 3, 4, 5, 6]);
-      const ciphertext = await aead.encrypt(plaintext);
-      const decryptedText = await aead.decrypt(ciphertext);
-
-      expect(decryptedText).toEqual(plaintext);
-    });
-
-    it('hybrid encrypt', async () => {
-      const keyset = createKeysetAndInitializeRegistry(HybridEncrypt);
-      const keysetHandle = new KeysetHandle(keyset);
-
-      // Test the HybridEncrypt primitive returned by getPrimitive method.
-      const hybridEncrypt =
-          await keysetHandle.getPrimitive<HybridEncrypt>(HybridEncrypt);
-      const plaintext = Random.randBytes(10);
-      const ciphertext = await hybridEncrypt.encrypt(plaintext);
-
-      // DummyHybridEncrypt just appends a ciphertext suffix to the plaintext.
-      // Since the primary key id is 1, the ciphertext prefix should also be 1.
-      expect(ciphertext)
-          .toEqual(Bytes.concat(
-              new Uint8Array([
-                0, 0, 0, 0, 1
-              ]) /* prefix which is 1-byte version + 4-byte primary key id*/,
-              plaintext,
-              new Uint8Array([1]) /* suffix which is 1-byte primary key id */));
-    });
-
-    it('hybrid decrypt', async () => {
-      const decryptKeysetHandle =
-          new KeysetHandle(createKeysetAndInitializeRegistry(HybridDecrypt));
-      const hybridDecrypt =
-          await decryptKeysetHandle.getPrimitive<HybridDecrypt>(HybridDecrypt);
-
-      const encryptKeysetHandle =
-          new KeysetHandle(createKeysetAndInitializeRegistry(HybridEncrypt));
-      const hybridEncrypt =
-          await encryptKeysetHandle.getPrimitive<HybridEncrypt>(HybridEncrypt);
-
-      const plaintext = Random.randBytes(10);
-      const ciphertext = await hybridEncrypt.encrypt(plaintext);
-      const decrypted = await hybridDecrypt.decrypt(ciphertext);
-
-      expect(decrypted).toEqual(plaintext);
-    });
-
-    it('aead, custom key manager', async () => {
-      const keyset = new PbKeyset();
-
-      // Add a new key with a new key type associated to custom key manager
-      // to the keyset.
-      const keyTypeUrl = 'new_custom_aead_key_type';
-      const keyId = 0xFFFFFFFF;
-      const key =
-          createKey({keyId, outputPrefix: PbOutputPrefixType.TINK, keyTypeUrl});
-      keyset.addKey(key);
-      keyset.setPrimaryKeyId(keyId);
-      const keysetHandle = new KeysetHandle(keyset);
-
-      // Create a custom key manager.
-      const customKeyManager = new DummyKeyManager(
-          keyTypeUrl, new DummyAead(Random.randBytes(10)), Aead);
-
-      // Encrypt with the primitive returned by customKeyManager.
-      const aead =
-          await keysetHandle.getPrimitive<Aead>(Aead, customKeyManager);
-      const plaintext = Random.randBytes(20);
-      const ciphertext = await aead.encrypt(plaintext);
-
-      // Register another key manager with the custom key type.
-      const managerInRegistry = new DummyKeyManager(
-          keyTypeUrl, new DummyAead(Random.randBytes(10)), Aead);
-      Registry.registerKeyManager(managerInRegistry);
-
-      // Check that the primitive returned by getPrimitive cannot decrypt the
-      // ciphertext. This is because managerInRegistry is different from
-      // customKeyManager.
-      const aeadFromRegistry = await keysetHandle.getPrimitive<Aead>(Aead);
-
-      await expectAsync(aeadFromRegistry.decrypt(ciphertext))
-          .toBeRejectedWithError(
-              SecurityException, 'Decryption failed for the given ciphertext.');
-
-      // Check that the primitive returned by getPrimitive with customKeyManager
-      // decrypts correctly.
-      const aeadFromCustomKeyManager =
-          await keysetHandle.getPrimitive<Aead>(Aead, customKeyManager);
-      const decryptedText = await aeadFromCustomKeyManager.decrypt(ciphertext);
-      expect(decryptedText).toEqual(plaintext);
-    });
-
-    it('hybrid encrypt, custom key manager', async () => {
-      const keyset = new PbKeyset();
-
-      // Add a new key with a new key type associated to custom key manager
-      // to the keyset.
-      const keyTypeUrl = 'new_custom_hybrid_encrypt_key_type';
-      const keyId = 0xFFFFFFFF;
-      const key =
-          createKey({keyId, outputPrefix: PbOutputPrefixType.TINK, keyTypeUrl});
-      keyset.addKey(key);
-      keyset.setPrimaryKeyId(keyId);
-      const keysetHandle = new KeysetHandle(keyset);
-
-      // Create a custom key manager.
-      const customKeyManager = new DummyKeyManager(
-          keyTypeUrl, new DummyHybridEncrypt(Random.randBytes(10)),
-          HybridEncrypt);
-
-      // Encrypt with the primitive returned by customKeyManager.
-      const customHybridEncrypt =
-          await keysetHandle.getPrimitive<HybridEncrypt>(
-              HybridEncrypt, customKeyManager);
-      const plaintext = Random.randBytes(20);
-      const ciphertext = await customHybridEncrypt.encrypt(plaintext);
-
-      // Register another key manager with the custom key type.
-      const managerInRegistry = new DummyKeyManager(
-          keyTypeUrl, new DummyHybridEncrypt(Random.randBytes(10)),
-          HybridEncrypt);
-      Registry.registerKeyManager(managerInRegistry);
-
-      // Check that the primitive returned by getPrimitive is not the same as
-      // customHybridEncrypt. This is because managerInRegistry is different
-      // from customKeyManager.
-      const hybridFromRegistry =
-          await keysetHandle.getPrimitive<HybridEncrypt>(HybridEncrypt);
-      const ciphertext2 = await hybridFromRegistry.encrypt(plaintext);
-      expect(ciphertext2).not.toEqual(ciphertext);
-
-      // Check that the primitive returned by getPrimitive with customKeyManager
-      // is the same as customHybridEncrypt.
-      const hybridEncryptFromCustomKeyManager =
-          await keysetHandle.getPrimitive<HybridEncrypt>(
-              HybridEncrypt, customKeyManager);
-      const ciphertext3 =
-          await hybridEncryptFromCustomKeyManager.encrypt(plaintext);
-      expect(ciphertext3).toEqual(ciphertext);
-    });
-
-    it('hybrid decrypt, custom key manager', async () => {
-      // Both private and public keys have the same key id.
-      const keyId = 0xFFFFFFFF;
-
-      // Create a public keyset.
-
-      const publicKeyset = new PbKeyset();
-      // Add a new key with a new key type associated to custom key manager
-      // to the keyset.
-      const publicKeyTypeUrl = 'new_custom_hybrid_encrypt_key_type';
-      const publicKey = createKey({
-        keyId,
-        outputPrefix: PbOutputPrefixType.TINK,
-        keyTypeUrl: publicKeyTypeUrl
-      });
-      publicKeyset.addKey(publicKey);
-      publicKeyset.setPrimaryKeyId(keyId);
-      const publicKeysetHandle = new KeysetHandle(publicKeyset);
-
-      // Create a corresponding private keyset.
-
-      const privateKeyset = new PbKeyset();
-      // Add a new key with a new key type associated to custom key manager
-      // to the keyset.
-      const privateKeyTypeUrl = 'new_custom_hybrid_decrypt_key_type';
-      const privateKey = createKey({
-        keyId,
-        outputPrefix: PbOutputPrefixType.TINK,
-        keyTypeUrl: privateKeyTypeUrl
-      });
-      privateKeyset.addKey(privateKey);
-      privateKeyset.setPrimaryKeyId(keyId);
-      const privateKeysetHandle = new KeysetHandle(privateKeyset);
-
-      // DummyHybridEncrypt (and DummyHybridDecrypt) just appends (and removes)
-      // a suffix to the plaintext. Create a random suffix that allows to
-      // determine which HybridDecrypt object is valid.
-      const ciphertextSuffix = Random.randBytes(10);
-
-      // Register a public key manager that uses the legit ciphertext suffix.
-      const publicKeyManagerInRegistry = new DummyKeyManager(
-          publicKeyTypeUrl, new DummyHybridEncrypt(ciphertextSuffix),
-          HybridEncrypt);
-      Registry.registerKeyManager(publicKeyManagerInRegistry);
-
-      // Encrypt with the primitive returned by getPrimitive.
-      const hybridEncrypt =
-          await publicKeysetHandle.getPrimitive<HybridEncrypt>(HybridEncrypt);
-      const plaintext = Random.randBytes(20);
-      const ciphertext = await hybridEncrypt.encrypt(plaintext);
-
-      // Register a private key manager that uses a random ciphertext suffix.
-      const keyManagerWithRandomSuffix = new DummyKeyManager(
-          privateKeyTypeUrl, new DummyHybridDecrypt(Random.randBytes(10)),
-          HybridDecrypt);
-      Registry.registerKeyManager(keyManagerWithRandomSuffix);
-
-      // Check that the primitive returned by getPrimitive cannot decrypt. This
-      // is because the ciphertext suffix is different.
-      const hybridDecryptFromRegistry =
-          await privateKeysetHandle.getPrimitive<HybridDecrypt>(HybridDecrypt);
-      await expectAsync(hybridDecryptFromRegistry.decrypt(ciphertext))
-          .toBeRejectedWithError(
-              SecurityException, 'Decryption failed for the given ciphertext.');
-
-      // Create a custom private key manager with the correct ciphertext suffix.
-      const customHybridDecryptKeyManager = new DummyKeyManager(
-          privateKeyTypeUrl, new DummyHybridDecrypt(ciphertextSuffix),
-          HybridDecrypt);
-
-      // Check that the primitive returned by getPrimitive with
-      // customHybridDecryptKeyManager can decrypt.
-      const customHybridDecrypt =
-          await privateKeysetHandle.getPrimitive<HybridDecrypt>(
-              HybridDecrypt, customHybridDecryptKeyManager);
-      const decrypted = await customHybridDecrypt.decrypt(ciphertext);
-      expect(decrypted).toEqual(plaintext);
-    });
-
-    it('keyset contains key corresponding to different primitive', async () => {
-      const keyset = createKeysetAndInitializeRegistry(Aead);
-
-      // Add new key with new key type url to the keyset and register a key
-      // manager providing Mac primitives with this key.
-      const macKeyTypeUrl = 'mac_key_type_1';
-      const macKeyId = 0xFFFFFFFF;
-      const macKey = createKey({
-        keyId: macKeyId,
-        outputPrefix: PbOutputPrefixType.TINK,
-        keyTypeUrl: macKeyTypeUrl
-      });
-      keyset.addKey(macKey);
-      const primitive = new DummyMac(new Uint8Array([0xFF]));
-      Registry.registerKeyManager(
-          new DummyKeyManager(macKeyTypeUrl, primitive, Mac));
-
-      const keysetHandle = new KeysetHandle(keyset);
-
-      await expectAsync(keysetHandle.getPrimitive<Aead>(Aead))
-          .toBeRejectedWithError(
-              SecurityException,
-              'Requested primitive type which is not supported by ' +
-                  'this key manager.');
-    });
-  });
-
-  describe('getPrimitiveSet', () => {
-    it('primary key is the enabled key with given id', async () => {
-      const keyId = 1;
-      const primaryUrl = 'key_type_url_for_primary_key';
-      const disabledUrl = 'key_type_url_for_disabled_key';
-
-      const keyset = new PbKeyset();
-      keyset.addKey(createKey({
-        keyId,
-        outputPrefix: PbOutputPrefixType.TINK,
-        keyTypeUrl: disabledUrl,
-        enabled: false
-      }));
-      keyset.addKey(createKey({
-        keyId,
-        outputPrefix: PbOutputPrefixType.LEGACY,
-        keyTypeUrl: disabledUrl,
-        enabled: false
-      }));
-      keyset.addKey(createKey({
-        keyId,
-        outputPrefix: PbOutputPrefixType.RAW,
-        keyTypeUrl: disabledUrl,
-        enabled: false
-      }));
-      keyset.addKey(createKey({
-        keyId,
-        outputPrefix: PbOutputPrefixType.TINK,
-        keyTypeUrl: primaryUrl,
-        enabled: true
-      }));
-      keyset.setPrimaryKeyId(keyId);
-
-      const keysetHandle = new KeysetHandle(keyset);
-
-      const primitive = new DummyAead(new Uint8Array(Random.randBytes(10)));
-      Registry.registerKeyManager(
-          new DummyKeyManager(primaryUrl, primitive, Aead));
-      Registry.registerKeyManager(new DummyKeyManager(
-          disabledUrl, new DummyAead(new Uint8Array(Random.randBytes(10))),
-          Aead));
-
-      const primitiveSet = await keysetHandle.getPrimitiveSet(Aead);
-      const primary = assertExists(primitiveSet.getPrimary());
-      expect(primary.getPrimitive()).toBe(primitive);
-    });
-
-    it('disabled keys should be ignored', async () => {
-      const enabledRawKeysCount = 10;
-      const enabledUrl = 'enabled_key_type_url';
-      const disabledUrl = 'disabled_key_type_url';
-
-      // Create keyset with both enabled and disabled RAW keys.
-      const keyset = new PbKeyset();
-      // Add RAW keys with different ids from [1, ENABLED_RAW_KEYS_COUNT].
-      for (let i = 0; i < enabledRawKeysCount; i++) {
-        keyset.addKey(createKey({
-          keyId: 1 + i,
-          outputPrefix: PbOutputPrefixType.RAW,
-          keyTypeUrl: enabledUrl,
-          enabled: true
-        }));
-        keyset.addKey(createKey({
-          keyId: 1 + i,
-          outputPrefix: PbOutputPrefixType.RAW,
-          keyTypeUrl: disabledUrl,
-          enabled: false
-        }));
-      }
-      keyset.setPrimaryKeyId(1);
-      const keysetHandle = new KeysetHandle(keyset);
-
-      // Register KeyManager (the key manager for enabled keys should be
-      // enough).
-      const primitive = new DummyAead(new Uint8Array(Random.randBytes(10)));
-      Registry.registerKeyManager(
-          new DummyKeyManager(enabledUrl, primitive, Aead));
-
-      // Get primitives and get all raw primitives.
-      const primitiveSet = await keysetHandle.getPrimitiveSet(Aead);
-      const rawPrimitives = primitiveSet.getRawPrimitives();
-
-      // Should return all enabled RAW primitives and nothing else (disabled
-      // primitives should not be added into primitive set).
-      expect(rawPrimitives.length).toBe(enabledRawKeysCount);
-
-      // Test that it returns the correct RAW primitives by using getPrimitive.
-      for (let i = 0; i < enabledRawKeysCount; ++i) {
-        expect(rawPrimitives[i].getPrimitive()).toBe(primitive);
-      }
-    });
-
-    it('with custom key manager', async () => {
-      // Create keyset handle.
-      const keyTypeUrl = 'some_key_type_url';
-      const keyId = 1;
-      const key =
-          createKey({keyId, outputPrefix: PbOutputPrefixType.TINK, keyTypeUrl});
-
-      const keyset = new PbKeyset();
-      keyset.addKey(key);
-      keyset.setPrimaryKeyId(keyId);
-
-      const keysetHandle = new KeysetHandle(keyset);
-
-      // Register key manager for the given keyType.
-      const primitive = new DummyAead(new Uint8Array(Random.randBytes(10)));
-      Registry.registerKeyManager(
-          new DummyKeyManager(keyTypeUrl, primitive, Aead));
-
-      // Use getPrimitives with custom key manager for the keyType.
-      const customPrimitive =
-          new DummyAead(new Uint8Array(Random.randBytes(10)));
-      const customKeyManager =
-          new DummyKeyManager(keyTypeUrl, customPrimitive, Aead);
-      const primitiveSet =
-          await keysetHandle.getPrimitiveSet(Aead, customKeyManager);
-
-      // Primary should be the entry corresponding to the keyTypeUrl and thus
-      // getPrimitive should return customPrimitive.
-      const primary = assertExists(primitiveSet.getPrimary());
-      expect(primary.getPrimitive()).toBe(customPrimitive);
-    });
-  });
-
-  describe('readNoSecret', () => {
-    it('throws for keysets containing secret key material', () => {
-      const secretKeyMaterialTypes = [
-        PbKeyMaterialType.SYMMETRIC, PbKeyMaterialType.ASYMMETRIC_PRIVATE,
-        PbKeyMaterialType.UNKNOWN_KEYMATERIAL
-      ];
-      for (const secretKeyMaterialType of secretKeyMaterialTypes) {
-        // Create a public keyset.
-        const keyset = new PbKeyset();
-        for (let i = 0; i < 3; i++) {
-          const key = createKey({
-            keyId: i + 1,
-            outputPrefix: PbOutputPrefixType.TINK,
-            keyTypeUrl: 'someType',
-            enabled: (i % 4) < 2,
-            keyMaterialType: PbKeyMaterialType.ASYMMETRIC_PUBLIC,
-          });
-          keyset.addKey(key);
-        }
-        keyset.setPrimaryKeyId(1);
-        const key = createKey({
-          keyId: 0xFFFFFFFF,
-          outputPrefix: PbOutputPrefixType.RAW,
-          keyTypeUrl: 'someType',
-          enabled: true,
-          keyMaterialType: secretKeyMaterialType,
-        });
-        keyset.addKey(key);
-        const reader =
-            BinaryKeysetReader.withUint8Array(keyset.serializeBinary());
-        expect(() => readNoSecret(reader))
-            .toThrowError(
-                SecurityException, 'Keyset contains secret key material.');
-      }
-    });
-
-    it('returns non-secret keysets', () => {
-      // Create a public keyset.
-      const keyset = new PbKeyset();
-      for (let i = 0; i < 3; i++) {
-        const key = createKey({
-          keyId: i + 1,
-          outputPrefix: PbOutputPrefixType.TINK,
-          keyTypeUrl: 'someType',
-          enabled: (i % 4) < 2,
-          keyMaterialType: PbKeyMaterialType.ASYMMETRIC_PUBLIC,
-        });
-        keyset.addKey(key);
-      }
-      keyset.setPrimaryKeyId(1);
-
-      const reader =
-          BinaryKeysetReader.withUint8Array(keyset.serializeBinary());
-      const keysetHandle = readNoSecret(reader);
-
-      assertMessageEquals(keysetHandle.getKeyset(), keyset);
-    });
-  });
-
-  describe('getPublicKeysetHandle', () => {
-    it('can get a public keyset from a private keyset', async () => {
-      const privateHandle = await generateNew(
-          HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
-      expect(() => privateHandle.getPublicKeysetHandle())
-          .not.toThrowError(
-              SecurityException, 'The keyset contains a non-private key');
-    });
-
-    it('can not get a public keyset from another public keyset', async () => {
-      const privateHandle = await generateNew(
-          HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
-      const publicHandle = privateHandle.getPublicKeysetHandle();
-      expect(() => publicHandle.getPublicKeysetHandle())
-          .toThrowError(
-              SecurityException, 'The keyset contains a non-private key');
-    });
-  });
-
-  describe('writeNoSecret', () => {
-    it('throws if the keyset contains secret keys', async () => {
-      const privateHandle = await generateNew(
-          HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
-      expect(() => privateHandle.writeNoSecret(new BinaryKeysetWriter()))
-          .toThrowError(SecurityException);
-    });
-
-    it('writes bytes if the keyset contains no secret keys', async () => {
-      const privateHandle = await generateNew(
-          HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
-      const publicHandle = privateHandle.getPublicKeysetHandle();
-      expect(() => publicHandle.writeNoSecret(new BinaryKeysetWriter()))
-          .not.toThrow();
-    });
-
-    it('can import the keyset using readNoSecret', async () => {
-      const privateHandle = await generateNew(
-          HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
-      const publicHandle = privateHandle.getPublicKeysetHandle();
-      const keysetBytes = publicHandle.writeNoSecret(new BinaryKeysetWriter());
-
-      const importedHandle = readNoSecret(new BinaryKeysetReader(keysetBytes));
-      assertMessageEquals(publicHandle.getKeyset(), importedHandle.getKeyset());
-    });
-  });
-});
-
-/** Function for creating keys for testing purposes. */
-function createKey({
-  keyId,
-  outputPrefix,
-  keyTypeUrl,
-  enabled = true,
-  keyMaterialType = PbKeyMaterialType.SYMMETRIC
-}: {
-  keyId: number,
-  outputPrefix: PbOutputPrefixType,
-  keyTypeUrl: string,
-  enabled?: boolean,
-  keyMaterialType?: PbKeyMaterialType,
-}): PbKeysetKey {
-  return new PbKeysetKey()
-      .setStatus(enabled ? PbKeyStatusType.ENABLED : PbKeyStatusType.DISABLED)
-      .setOutputPrefixType(outputPrefix)
-      .setKeyId(keyId)
-      .setKeyData(new PbKeyData()
-                      .setTypeUrl(keyTypeUrl)
-                      .setValue(new Uint8Array([1]))
-                      .setKeyMaterialType(keyMaterialType));
-}
-
-/**
- * Function for creating keysets for testing purposes.
- * Primary has id 1.
- *
- * The function also register DummyKeyManager providing primitives for each
- * keyType added to the Keyset.
- */
-function createKeysetAndInitializeRegistry(
-    primitiveType: Constructor<unknown>, numberOfKeys = 15): PbKeyset {
-  const numberOfKeyTypes = 5;
-  const keyTypePrefix = 'key_type_';
-
-  for (let i = 0; i < numberOfKeyTypes; i++) {
-    const typeUrl = keyTypePrefix + i.toString();
-    let primitive;
-    switch (primitiveType) {
-      case HybridDecrypt:
-        primitive = new DummyHybridDecrypt(new Uint8Array([i]));
-        break;
-      case HybridEncrypt:
-        primitive = new DummyHybridEncrypt(new Uint8Array([i]));
-        break;
-      default:
-        primitive = new DummyAead(new Uint8Array([i]));
-        break;
-    }
-    Registry.registerKeyManager(
-        new DummyKeyManager(typeUrl, primitive, primitiveType));
-  }
-
-  const keyset = new PbKeyset();
-
-  for (let i = 1; i < numberOfKeys; i++) {
-    const keyTypeUrl = keyTypePrefix + (i % numberOfKeyTypes).toString();
-    let outputPrefix: PbOutputPrefixType;
-    switch (i % 3) {
-      case 0:
-        outputPrefix = PbOutputPrefixType.TINK;
-        break;
-      case 1:
-        outputPrefix = PbOutputPrefixType.LEGACY;
-        break;
-      default:
-        outputPrefix = PbOutputPrefixType.RAW;
-    }
-    // There are no primitives added to PrimitiveSet for disabled keys, thus
-    // they are quite rarely added into the Keyset.
-    keyset.addKey(
-        createKey({keyId: i, outputPrefix, keyTypeUrl, enabled: i % 7 < 6}));
-  }
-
-  keyset.setPrimaryKeyId(1);
-  return keyset;
-}
-
-class DummyAead extends Aead {
-  constructor(private readonly ciphertextSuffix: Uint8Array) {
-    super();
-  }
-
-  // Encrypt method just append the primitive identifier to plaintext.
-  async encrypt(plaintext: Uint8Array, associatedData?: Uint8Array) {
-    const result =
-        new Uint8Array(plaintext.length + this.ciphertextSuffix.length);
-    result.set(plaintext, 0);
-    result.set(this.ciphertextSuffix, plaintext.length);
-    return result;
-  }
-
-  // Decrypt method throws an exception whenever ciphertext does not end with
-  // ciphertext suffix, otherwise it returns the first part (without
-  // ciphertext suffix).
-  async decrypt(ciphertext: Uint8Array, associatedData?: Uint8Array) {
-    const plaintext = ciphertext.subarray(
-        0, ciphertext.length - this.ciphertextSuffix.length);
-    const ciphertextSuffix = ciphertext.subarray(
-        ciphertext.length - this.ciphertextSuffix.length, ciphertext.length);
-
-    if ([...ciphertextSuffix].toString() !==
-        [...this.ciphertextSuffix].toString()) {
-      throw new SecurityException('Ciphertext decryption failed.');
-    }
-
-    return plaintext;
-  }
-}
-
-class DummyMac extends Mac {
-  constructor(private readonly tag: Uint8Array) {
-    super();
-  }
-
-  /**
-   * Just appends the tag to the data.
-   */
-  async computeMac(data: Uint8Array) {
-    return this.tag;
-  }
-
-  /**
-   * Returns whether data ends with tag.
-   */
-  async verifyMac(tag: Uint8Array, data: Uint8Array) {
-    return [...tag].toString() === [...this.tag].toString();
-  }
-}
-
-class DummyHybridEncrypt extends HybridEncrypt {
-  constructor(private readonly ciphertextSuffix: Uint8Array) {
-    super();
-  }
-  // Async is used here just because real primitives returns Promise.
-  async encrypt(plaintext: Uint8Array, associatedData?: Uint8Array) {
-    return Bytes.concat(plaintext, this.ciphertextSuffix);
-  }
-}
-
-class DummyHybridDecrypt extends HybridDecrypt {
-  constructor(private readonly ciphertextSuffix: Uint8Array) {
-    super();
-  }
-
-  async decrypt(ciphertext: Uint8Array, associatedData?: Uint8Array) {
-    const cipherLen = ciphertext.length;
-    const suffixLen = this.ciphertextSuffix.length;
-    const plaintext = ciphertext.subarray(0, cipherLen - suffixLen);
-    const suffix = ciphertext.subarray(cipherLen - suffixLen, cipherLen);
-    if (!Bytes.isEqual(this.ciphertextSuffix, suffix)) {
-      throw new SecurityException('Ciphertext decryption failed.');
-    }
-    return plaintext;
-  }
-}
-
-class DummyKeyManager<T> implements KeyManager<T> {
-  constructor(
-      private readonly keyType: string, private readonly primitive: T,
-      private readonly primitiveType: Constructor<T>) {}
-
-  async getPrimitive(primitiveType: Constructor<T>, key: PbKeyData|PbMessage) {
-    if (primitiveType !== this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    return this.primitive;
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return this.keyType;
-  }
-
-  getPrimitiveType() {
-    return this.primitiveType;
-  }
-
-  getVersion(): number {
-    throw new SecurityException('Not implemented, function is not needed.');
-  }
-
-  getKeyFactory(): KeyFactory {
-    throw new SecurityException('Not implemented, function is not needed.');
-  }
-}
diff --git a/javascript/internal/keyset_reader.ts b/javascript/internal/keyset_reader.ts
deleted file mode 100644
index 077f2c7..0000000
--- a/javascript/internal/keyset_reader.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbEncryptedKeyset, PbKeyset} from './proto';
-
-/**
- * KeysetReader knows how to read a keyset or an encrypted keyset from some
- * source.
- *
- */
-export interface KeysetReader {
-  /**
-   * Reads and returns a (cleartext) Keyset object from the underlying source.
-   *
-   */
-  read(): PbKeyset;
-
-  /**
-   * Reads and returns an EncryptedKeyset from the underlying source.
-   *
-   */
-  readEncrypted(): PbEncryptedKeyset;
-}
diff --git a/javascript/internal/keyset_writer.ts b/javascript/internal/keyset_writer.ts
deleted file mode 100644
index c8a9ddc..0000000
--- a/javascript/internal/keyset_writer.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbEncryptedKeyset, PbKeyset} from './proto';
-
-/**
- * KeysetWriter knows how to write a keyset or an encrypted keyset to some
- * storage system.
- *
- */
-export interface KeysetWriter {
-  encodeBinary(keyset: PbKeyset|PbEncryptedKeyset): Uint8Array;
-}
diff --git a/javascript/internal/primitive_set.ts b/javascript/internal/primitive_set.ts
deleted file mode 100644
index f242232..0000000
--- a/javascript/internal/primitive_set.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-
-import {CryptoFormat} from './crypto_format';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from './proto';
-import {Constructor} from './util';
-
-/**
- * Auxiliary class for PrimitiveSet
- * Entry-objects hold individual instances of primitives in the set.
- *
- * @template P
- * @final
- */
-export class Entry<P> {
-  constructor(
-      private readonly primitive: P, private readonly identifier: Uint8Array,
-      private readonly keyStatus: PbKeyStatusType,
-      private readonly outputPrefixType: PbOutputPrefixType) {}
-
-  getPrimitive(): P {
-    return this.primitive;
-  }
-
-  getIdentifier(): Uint8Array {
-    return this.identifier;
-  }
-
-  getKeyStatus(): PbKeyStatusType {
-    return this.keyStatus;
-  }
-
-  getOutputPrefixType(): PbOutputPrefixType {
-    return this.outputPrefixType;
-  }
-}
-
-/**
- * A container class for a set of primitives (i.e. implementations of
- * cryptographic primitives offered by Tink). It provides also additional
- * properties for the primitives it holds. In particular, one of the primitives
- * in the set can be distinguished as "the primary" one.
- *
- * PrimitiveSet is an auxiliary class used for supporting key rotation:
- * primitives in a set correspond to keys in a keyset. Users will usually work
- * with primitive instances which essentially wrap primitive sets. For example
- * an instance of an Aead-primitive for a given keyset holds a set of
- * Aead-primitives corresponding to the keys in the keyset, and uses the set
- * members to do the actual crypto operations: to encrypt data the primary
- * Aead-primitive from the set is used, and upon decryption the ciphertext's
- * prefix determines the identifier of the primitive from the set.
- *
- * PrimitiveSet is a public class to allow its use in implementations of custom
- * primitives.
- *
- * @final
- */
-export class PrimitiveSet<P> {
-  private primary: Entry<P>|null = null;
-
-  // Keys have to be stored as strings as two Uint8Arrays holding the same
-  // digits are still different objects.
-  private readonly identifierToPrimitivesMap: Map<string, Array<Entry<P>>>;
-
-  constructor(private readonly primitiveType: Constructor<P>) {
-    this.identifierToPrimitivesMap = new Map();
-  }
-
-  /**
-   * Returns the type of primitives contained in this set.
-   *
-   */
-  getPrimitiveType(): Constructor<P> {
-    return this.primitiveType;
-  }
-
-  /**
-   * Creates an entry in the primitive table and returns it.
-   *
-   *
-   */
-  addPrimitive(primitive: P, key: PbKeysetKey): Entry<P> {
-    if (!primitive) {
-      throw new SecurityException('Primitive has to be non null.');
-    }
-    if (!key) {
-      throw new SecurityException('Key has to be non null.');
-    }
-    const identifier = CryptoFormat.getOutputPrefix(key);
-    const entry = new Entry(
-        primitive, identifier, key.getStatus(), key.getOutputPrefixType());
-    this.addPrimitiveToMap(entry);
-    return entry;
-  }
-
-  /**
-   * Returns the entry with the primary primitive.
-   *
-   */
-  getPrimary(): Entry<P>|null {
-    return this.primary;
-  }
-
-  /**
-   * Sets given Entry as the primary one.
-   *
-   */
-  setPrimary(primitive: Entry<P>) {
-    if (!primitive) {
-      throw new SecurityException('Primary cannot be set to null.');
-    }
-    if (primitive.getKeyStatus() != PbKeyStatusType.ENABLED) {
-      throw new SecurityException('Primary has to be enabled.');
-    }
-
-    // There has to be exactly one key enabled with this identifier.
-    const entries = this.getPrimitives(primitive.getIdentifier());
-    let entryFound = false;
-    const entriesLength = entries.length;
-    for (let i = 0; i < entriesLength; i++) {
-      if (entries[i].getKeyStatus() === PbKeyStatusType.ENABLED) {
-        entryFound = true;
-        break;
-      }
-    }
-    if (!entryFound) {
-      throw new SecurityException(
-          'Primary cannot be set to an entry which is ' +
-          'not held by this primitive set.');
-    }
-    this.primary = primitive;
-  }
-
-  /**
-   * Returns all primitives using RAW prefix.
-   *
-   */
-  getRawPrimitives(): Array<Entry<P>> {
-    return this.getPrimitives(CryptoFormat.RAW_PREFIX);
-  }
-
-  /**
-   * Returns the entries with primitive identified with identifier.
-   *
-   *
-   */
-  getPrimitives(identifier: Uint8Array): Array<Entry<P>> {
-    const result = this.getPrimitivesFromMap(identifier);
-    if (!result) {
-      return [];
-    } else {
-      return result;
-    }
-  }
-
-  /**
-   * Returns a set of primitives which corresponds to the given identifier.
-   *
-   *
-   */
-  private getPrimitivesFromMap(identifier: Uint8Array|
-                               string): Array<Entry<P>>|undefined {
-    if (identifier instanceof Uint8Array) {
-      identifier = [...identifier].toString();
-    }
-    return this.identifierToPrimitivesMap.get(identifier);
-  }
-
-  /**
-   * Add primitive to map.
-   *
-   */
-  private addPrimitiveToMap(entry: Entry<P>) {
-    const identifier = entry.getIdentifier();
-    const id = [...identifier].toString();
-    const existing = this.getPrimitivesFromMap(id);
-    if (!existing) {
-      this.identifierToPrimitivesMap.set(id, [entry]);
-    } else {
-      existing.push(entry);
-      this.identifierToPrimitivesMap.set(id, existing);
-    }
-  }
-}
diff --git a/javascript/internal/primitive_set_test.ts b/javascript/internal/primitive_set_test.ts
deleted file mode 100644
index 6e1382f..0000000
--- a/javascript/internal/primitive_set_test.ts
+++ /dev/null
@@ -1,350 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead';
-import {SecurityException} from '../exception/security_exception';
-
-import {CryptoFormat} from './crypto_format';
-import * as PrimitiveSet from './primitive_set';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from './proto';
-
-describe('primitive set test', function() {
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for addPrimitive method
-  it('add primitive unknown crypto format', function() {
-    const primitive = new DummyAead1();
-    const key =
-        createKey().setOutputPrefixType(PbOutputPrefixType.UNKNOWN_PREFIX);
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-
-    try {
-      primitiveSet.addPrimitive(primitive, key);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownPrefixType());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-  it('add primitive multiple times should work', function() {
-    const key = createKey();
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-
-    for (let i = 0; i < 4; i++) {
-      let primitive;
-      if (i % 2 === 0) {
-        primitive = new DummyAead1();
-      } else {
-        primitive = new DummyAead2();
-      }
-      const result = primitiveSet.addPrimitive(primitive, key);
-
-      expect(result.getPrimitive()).toEqual(primitive);
-      expect(result.getKeyStatus()).toBe(key.getStatus());
-      expect(result.getOutputPrefixType()).toBe(key.getOutputPrefixType());
-      expect(result.getIdentifier()).toEqual(CryptoFormat.getOutputPrefix(key));
-    }
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getPrimitives method
-  it('get primitives which were not added', function() {
-    // Fill in the structure with some primitives.
-    const numberOfAddedPrimitives = 12;
-    const primitiveSet = initPrimitiveSet(numberOfAddedPrimitives);
-
-    const key = createKey(/* opt_keyId = */ numberOfAddedPrimitives + 1);
-    const identifier = CryptoFormat.getOutputPrefix(key);
-    const result = primitiveSet.getPrimitives(identifier);
-
-    expect(result).toEqual([]);
-  });
-
-  it('get primitives different identifiers', function() {
-    // Fill in the structure with some primitives.
-    const n = 100;
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-
-    const added = [];
-    for (let id = 0; id < n; id++) {
-      const legacyKeyType = ((id % 2) < 1);
-      const enabledKey = ((id % 4) < 2);
-      const key = createKey(id, legacyKeyType, enabledKey);
-
-      let primitive;
-      if  ((id % 8) < 4) {
-        primitive = new DummyAead1();
-      } else {
-        primitive = new DummyAead2();
-      }
-
-      const res = primitiveSet.addPrimitive(primitive, key);
-      added.push({key, entry: res});
-    }
-
-    // Test that getPrimitives return correct value for them.
-    const addedLength = added.length;
-    for (let i = 0; i < addedLength; i++) {
-      const identifier = CryptoFormat.getOutputPrefix(added[i].key);
-      // Should return a set containing only one primitive as each added
-      // primitive has different identifier.
-      const expectedResult = [added[i].entry];
-      const result = primitiveSet.getPrimitives(identifier);
-
-      expect(result).toEqual(expectedResult);
-    }
-  });
-
-  it('get primitives same identifiers', function() {
-    // Fill in the structure with some primitives.
-    const numberOfAddedPrimitives = 50;
-    const primitiveSet = initPrimitiveSet(numberOfAddedPrimitives);
-
-    // Add a group of primitives with same identifier.
-    const n = 12;
-    const keyId = 0xABCDEF98;
-    const legacyKeyType = false;
-
-    const expectedResult = [];
-    for (let i = 0; i < n; i++) {
-      const enabledKey = ((i % 2) < 1);
-      const key = createKey(keyId, legacyKeyType, enabledKey);
-
-      let primitive;
-      if  ((i % 4) < 2) {
-        primitive = new DummyAead1();
-      } else {
-        primitive = new DummyAead2();
-      }
-
-      const res = primitiveSet.addPrimitive(primitive, key);
-      expectedResult.push(res);
-    }
-
-    const identifier =
-        CryptoFormat.getOutputPrefix(createKey(keyId, legacyKeyType));
-    const result = primitiveSet.getPrimitives(identifier);
-
-    expect(result).toEqual(expectedResult);
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getRawPrimitives method
-  it('get raw primitives', function() {
-    const numberOfAddedPrimitives = 20;
-    const primitiveSet = initPrimitiveSet(numberOfAddedPrimitives);
-
-    // No RAW primitives were added.
-    const expectedResult: Array<PrimitiveSet.Entry<Aead>> = [];
-    let result = primitiveSet.getRawPrimitives();
-    expect(result).toEqual(expectedResult);
-
-    // Add RAW primitives and check the result again after each adding.
-    const key = createKey().setOutputPrefixType(PbOutputPrefixType.RAW);
-
-    let addResult = primitiveSet.addPrimitive(new DummyAead1(), key);
-    expectedResult.push(addResult);
-    result = primitiveSet.getRawPrimitives();
-    expect(result).toEqual(expectedResult);
-
-    key.setStatus(PbKeyStatusType.DISABLED);
-    addResult = primitiveSet.addPrimitive(new DummyAead2(), key);
-    expectedResult.push(addResult);
-    result = primitiveSet.getRawPrimitives();
-    expect(result).toEqual(expectedResult);
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for setPrimary and getPrimary methods
-  it('set primary to nonholded entry', function() {
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-    const entry = new PrimitiveSet.Entry(
-        new DummyAead1(), new Uint8Array(10), PbKeyStatusType.ENABLED,
-        PbOutputPrefixType.TINK);
-
-    try {
-      primitiveSet.setPrimary(entry);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.setPrimaryToMissingEntry());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('set primary to entry with disabled key status', function() {
-    const key = createKey(/* opt_keyId = */ 0x12345678,
-        /* opt_legacy = */ false, /* opt_enabled = */ false);
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-
-    const primary = primitiveSet.addPrimitive(new DummyAead1(), key);
-
-    try {
-      primitiveSet.setPrimary(primary);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.setPrimaryToDisabled());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('set and get primary', function() {
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-    expect(primitiveSet.getPrimary()).toBe(null);
-
-    const key1 = createKey(/* opt_keyId = */ 0xBBBCCC);
-
-    const result = primitiveSet.addPrimitive(new DummyAead1(), key1);
-    // Check that primary remains unset, set it to newly added and verify that
-    // it was set.
-    expect(primitiveSet.getPrimary()).toBe(null);
-    primitiveSet.setPrimary(result);
-    expect(primitiveSet.getPrimary()).toEqual(result);
-
-    const key2 = createKey(/* opt_keyId = */ 0xAAABBB);
-    // Add new primitive, check that it does not change primary.
-    const result2 = primitiveSet.addPrimitive(new DummyAead2(), key2);
-    expect(primitiveSet.getPrimary()).toEqual(result);
-
-    // Change the primary and verify the change.
-    primitiveSet.setPrimary(result2);
-    expect(primitiveSet.getPrimary()).toEqual(result2);
-  });
-
-  it('set primary, raw primitives', function() {
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-    for (let i = 0; i < 3; i++) {
-      const key = createKey(i).setOutputPrefixType(PbOutputPrefixType.RAW);
-      primitiveSet.addPrimitive(new DummyAead1(), key);
-    }
-    const primaryKey =
-        createKey(255).setOutputPrefixType(PbOutputPrefixType.RAW);
-    const primaryEntry =
-        primitiveSet.addPrimitive(new DummyAead1(), primaryKey);
-    primitiveSet.setPrimary(primaryEntry);
-  });
-
-  it('get primary type', function() {
-    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
-    expect(primitiveSet.getPrimitiveType()).toEqual(Aead);
-  });
-});
-
-// Helper classes and functions used for testing purposes.
-
-class ExceptionText {
-  static unknownPrefixType(): string {
-    return 'SecurityException: Unsupported key prefix type.';
-  }
-
-  static addingNullPrimitive(): string {
-    return 'SecurityException: Primitive has to be non null.';
-  }
-
-  static addingNullKey(): string {
-    return 'SecurityException: Key has to be non null.';
-  }
-
-  static setPrimaryToNull(): string {
-    return 'SecurityException: Primary cannot be set to null.';
-  }
-
-  static setPrimaryToMissingEntry(): string {
-    return 'SecurityException: Primary cannot be set to an entry which is ' +
-        'not held by this primitive set.';
-  }
-
-  static setPrimaryToDisabled(): string {
-    return 'SecurityException: Primary has to be enabled.';
-  }
-}
-
-/** @final */
-class DummyAead1 extends Aead {
-  encrypt(plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
-    throw new SecurityException(
-        'Not implemented, intentended just for testing.');
-  }
-
-  decrypt(ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
-    throw new SecurityException(
-        'Not implemented, intentended just for testing.');
-  }
-}
-
-/** @final */
-class DummyAead2 extends Aead {
-  encrypt(plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
-    throw new SecurityException(
-        'Not implemented, intentended just for testing.');
-  }
-
-  decrypt(ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
-    throw new SecurityException(
-        'Not implemented, intentended just for testing.');
-  }
-}
-
-/**
- * Function for creating primitive sets for testing purposes.
- * Returns a primitive set containing n values with keyIds from {0, .. , n-1}.
- * There are different combinations of
- *    primitive types ({DummyAead1, DummyAead2}),
- *    key status (enabled, disabled),
- *    and key types (legacy, tink).
- */
-function initPrimitiveSet(n: number): PrimitiveSet.PrimitiveSet<Aead> {
-  const primitiveSet = new PrimitiveSet.PrimitiveSet<Aead>(Aead);
-
-  // Set primary.
-  const primaryKey = createKey(/* opt_id = */ 0, /* opt_legacy = */ false,
-      /* opt_enabled = */ true);
-  const primary = primitiveSet.addPrimitive(new DummyAead1(), primaryKey);
-  primitiveSet.setPrimary(primary);
-
-  // Add n-1 other keys to primitive set.
-  for (let id = 1; id < n; id++) {
-    const legacyKeyType = ((id % 2) < 1);
-    const enabledKey = ((id % 4) < 2);
-    const key = createKey(id, legacyKeyType, enabledKey);
-
-    let primitive;
-    if ((id % 8) < 4) {
-      primitive = new DummyAead1();
-    } else {
-      primitive = new DummyAead2();
-    }
-
-    primitiveSet.addPrimitive(primitive, key);
-  }
-
-  return primitiveSet;
-}
-
-/**
- * Function for creating keys for testing purposes.
- * If doesn't set otherwise it generates a key with id 0x12345678, which is
- * ENABLED and with prefix type TINK.
- */
-function createKey(
-    opt_keyId: number = 0x12345678, opt_legacy: boolean = false,
-    opt_enabled: boolean = true): PbKeysetKey {
-  const key = new PbKeysetKey();
-
-  if (opt_enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-
-  if (opt_legacy) {
-    key.setOutputPrefixType(PbOutputPrefixType.LEGACY);
-  } else {
-    key.setOutputPrefixType(PbOutputPrefixType.TINK);
-  }
-
-  key.setKeyId(opt_keyId);
-
-  return key;
-}
diff --git a/javascript/internal/primitive_wrapper.ts b/javascript/internal/primitive_wrapper.ts
deleted file mode 100644
index b7f3eeb..0000000
--- a/javascript/internal/primitive_wrapper.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as PrimitiveSet from './primitive_set';
-import {Constructor} from './util';
-
-/**
- * Basic interface for wrapping a primitive.
- *
- * A PrimitiveSet can be wrapped by a single primitive in order to fulfil a
- * cryptographic task. This is done by the PrimitiveWrapper. Whenever a new
- * primitive type is added to Tink, the user should define a new
- * PrimitiveWrapper and register it with the Registry.
- */
-export interface PrimitiveWrapper<P> {
-  /**
-   * Wraps a PrimitiveSet and returns a single instance.
-   *
-   */
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<P>): P;
-
-  /**
-   * Returns the type of the managed primitive. Used for internal management.
-   *
-   */
-  getPrimitiveType(): Constructor<P>;
-}
diff --git a/javascript/internal/proto_test.ts b/javascript/internal/proto_test.ts
deleted file mode 100644
index c2a6dfc..0000000
--- a/javascript/internal/proto_test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbKeyset} from './proto';
-
-describe('proto test', function() {
-  it('field', function() {
-    const keyset = new PbKeyset().setPrimaryKeyId(1);
-    expect(keyset.getPrimaryKeyId()).toBe(1);
-  });
-});
diff --git a/javascript/internal/registry.ts b/javascript/internal/registry.ts
deleted file mode 100644
index 4074ac5..0000000
--- a/javascript/internal/registry.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/**
- * @fileoverview Registry for KeyManagers.
- *
- * Registry maps supported key types to corresponding KeyManager objects (i.e.
- * the KeyManagers which may instantiate the primitive corresponding to the
- * given key or generate new key of the given type). Keeping KeyManagers for all
- * primitives in a single Registry (rather than having a separate keyManager per
- * primitive) enables modular construction of compound primitives from "simple"
- * ones (e.g. AES-CTR-HMAC AEAD encryption from IND-CPA encryption and MAC).
- *
- * Regular users will not usually work with Registry directly, but via primitive
- * factories, which query Registry for the specific KeyManagers in the
- * background.
- */
-
-import {SecurityException} from '../exception/security_exception';
-
-import * as KeyManager from './key_manager';
-import * as PrimitiveSet from './primitive_set';
-import {PrimitiveWrapper} from './primitive_wrapper';
-import {PbKeyData, PbKeyTemplate, PbMessage} from './proto';
-import {Constructor, isInstanceOf} from './util';
-
-// key managers maps
-const typeToManagerMap_ = new Map<string, KeyManager.KeyManager<unknown>>();
-
-const typeToNewKeyAllowedMap_ = new Map<string, boolean>();
-
-// primitive wrappers map
-const primitiveTypeToWrapper_ = new Map<unknown, PrimitiveWrapper<unknown>>();
-
-/**
- * Register the given manager for the given key type. Manager must be
- * non-nullptr. New keys are allowed if not specified.
- */
-export function registerKeyManager(
-    manager: KeyManager.KeyManager<unknown>, opt_newKeyAllowed?: boolean) {
-  if (opt_newKeyAllowed === undefined) {
-    opt_newKeyAllowed = true;
-  }
-  if (!manager) {
-    throw new SecurityException('Key manager cannot be null.');
-  }
-  const typeUrl = manager.getKeyType();
-  if (typeToManagerMap_.has(typeUrl)) {
-    // Cannot overwrite the existing key manager by a new one.
-    if (!(typeToManagerMap_.get(typeUrl) instanceof manager.constructor)) {
-      throw new SecurityException(
-          'Key manager for key type ' + typeUrl +
-          ' has already been registered and cannot be overwritten.');
-    }
-
-    // It is forbidden to change new_key_allowed from false to true.
-    if (!typeToNewKeyAllowedMap_.get(typeUrl) && opt_newKeyAllowed) {
-      throw new SecurityException(
-          'Key manager for key type ' + typeUrl +
-          ' has already been registered with forbidden new key operation.');
-    }
-    typeToNewKeyAllowedMap_.set(typeUrl, opt_newKeyAllowed);
-  }
-  typeToManagerMap_.set(typeUrl, manager);
-  typeToNewKeyAllowedMap_.set(typeUrl, opt_newKeyAllowed);
-}
-
-/**
- * Returns a key manager for the given key type or throws an exception if no
- * such manager found.
- *
- * @param typeUrl -- key type
- *
- */
-export function getKeyManager(typeUrl: string): KeyManager.KeyManager<unknown> {
-  const res = typeToManagerMap_.get(typeUrl);
-  if (!res) {
-    throw new SecurityException(
-        'Key manager for key type ' + typeUrl + ' has not been registered.');
-  }
-  return res;
-}
-
-/**
- * It finds KeyManager according to key type (which is either given by
- * PbKeyData or given by opt_typeUrl), than calls the corresponding
- * manager's getPrimitive method.
- *
- * Either key is of type PbKeyData or opt_typeUrl must be provided.
- *
- * @param key -- key is either a proto of some key
- *     or key data.
- * @param opt_typeUrl -- key type
- * @this {typeof Registry}
- *
- */
-export async function getPrimitive<P>(
-    primitiveType: Constructor<P>, key: PbKeyData|PbMessage,
-    opt_typeUrl?: string|null): Promise<P> {
-  if (key instanceof PbKeyData) {
-    if (opt_typeUrl && key.getTypeUrl() != opt_typeUrl) {
-      throw new SecurityException(
-          'Key type is ' + opt_typeUrl + ', but it is expected to be ' +
-          key.getTypeUrl() + ' or undefined.');
-    }
-    opt_typeUrl = key.getTypeUrl();
-  }
-  if (!opt_typeUrl) {
-    throw new SecurityException('Key type has to be specified.');
-  }
-  const manager = getKeyManager(opt_typeUrl);
-  const primitive = await manager.getPrimitive(primitiveType, key);
-  if (!isInstanceOf(primitive, primitiveType)) {
-    throw new TypeError('Unexpected type');
-  }
-  return primitive;
-}
-
-/**
- * Generates a new PbKeyData for the specified keyTemplate. It finds a
- * KeyManager given by keyTemplate.typeUrl and calls the newKeyData method of
- * that manager.
- *
- *
- *
- */
-export async function newKeyData(keyTemplate: PbKeyTemplate):
-    Promise<PbKeyData> {
-  const manager = getKeyManagerWithNewKeyAllowedCheck_(keyTemplate);
-  return manager.getKeyFactory().newKeyData(keyTemplate.getValue_asU8());
-}
-
-/**
- * Generates a new key for the specified keyTemplate using the
- * KeyManager determined by typeUrl field of the keyTemplate.
- *
- *
- *
- * @return returns a key proto
- */
-export async function newKey(keyTemplate: PbKeyTemplate): Promise<PbMessage> {
-  const manager = getKeyManagerWithNewKeyAllowedCheck_(keyTemplate);
-  return manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-}
-
-/**
- * Convenience method for extracting the public key data from the private key
- * given by serializedPrivateKey.
- * It looks up a KeyManager identified by typeUrl, which must hold
- * PrivateKeyFactory, and calls getPublicKeyData method of that factory.
- *
- */
-export function getPublicKeyData(
-    typeUrl: string, serializedPrivateKey: Uint8Array): PbKeyData {
-  const manager = getKeyManager(typeUrl);
-
-  // This solution might cause some problems in the future due to Closure
-  // compiler optimizations, which may map factory.getPublicKeyData to
-  // concrete function.
-  const factory = manager.getKeyFactory();
-  if (!factory.getPublicKeyData) {
-    throw new SecurityException(
-        'Key manager for key type ' + typeUrl +
-        ' does not have a private key factory.');
-  }
-  return factory.getPublicKeyData(serializedPrivateKey);
-}
-
-/**
- * Resets the registry.
- * After reset the registry is empty, i.e. it contains no key managers.
- *
- * This method is only for testing.
- */
-export function reset() {
-  typeToManagerMap_.clear();
-  typeToNewKeyAllowedMap_.clear();
-  primitiveTypeToWrapper_.clear();
-}
-
-/**
- * It finds a KeyManager given by keyTemplate.typeUrl and returns it if it
- * allows creating new keys.
- *
- *
- */
-function getKeyManagerWithNewKeyAllowedCheck_(keyTemplate: PbKeyTemplate):
-    KeyManager.KeyManager<unknown> {
-  const keyType = keyTemplate.getTypeUrl();
-  const manager = getKeyManager(keyType);
-  if (!typeToNewKeyAllowedMap_.get(keyType)) {
-    throw new SecurityException(
-        'New key operation is forbidden for ' +
-        'key type: ' + keyType + '.');
-  }
-  return manager;
-}
-
-/**
- * Tries to register a primitive wrapper.
- */
-export function registerPrimitiveWrapper<P>(wrapper: PrimitiveWrapper<P>) {
-  if (!wrapper) {
-    throw new SecurityException('primitive wrapper cannot be null');
-  }
-  const primitiveType = wrapper.getPrimitiveType();
-  if (!primitiveType) {
-    throw new SecurityException('primitive wrapper cannot be undefined');
-  }
-  if (primitiveTypeToWrapper_.has(primitiveType)) {
-    // Cannot overwrite the existing key manager by a new one.
-    if (!(primitiveTypeToWrapper_.get(primitiveType) instanceof
-          wrapper.constructor)) {
-      throw new SecurityException(
-          'primitive wrapper for type ' + primitiveType +
-          ' has already been registered and cannot be overwritten');
-    }
-  }
-  primitiveTypeToWrapper_.set(primitiveType, wrapper);
-}
-
-/**
- * Wraps a PrimitiveSet and returns a single instance.
- */
-export function wrap<P>(primitiveSet: PrimitiveSet.PrimitiveSet<P>): P {
-  if (!primitiveSet) {
-    throw new SecurityException('primitive set cannot be null.');
-  }
-  const primitiveType = primitiveSet.getPrimitiveType();
-  const wrapper = primitiveTypeToWrapper_.get(primitiveType);
-  if (!wrapper) {
-    throw new SecurityException(
-        'no primitive wrapper found for type ' + primitiveType);
-  }
-  const primitive = wrapper.wrap(primitiveSet);
-  if (!isInstanceOf(primitive, primitiveType)) {
-    throw new TypeError('Unexpected type');
-  }
-  return primitive;
-}
diff --git a/javascript/internal/registry_test.ts b/javascript/internal/registry_test.ts
deleted file mode 100644
index fc889ca..0000000
--- a/javascript/internal/registry_test.ts
+++ /dev/null
@@ -1,792 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead';
-import {AeadConfig} from '../aead/aead_config';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {AesCtrHmacAeadKeyManager} from '../aead/aes_ctr_hmac_aead_key_manager';
-import {SecurityException} from '../exception/security_exception';
-import * as HybridConfig from '../hybrid/hybrid_config';
-import {HybridKeyTemplates} from '../hybrid/hybrid_key_templates';
-import {Mac} from '../mac';
-import {EncryptThenAuthenticate} from '../subtle/encrypt_then_authenticate';
-import {assertExists, assertInstanceof} from '../testing/internal/test_utils';
-
-import * as KeyManager from './key_manager';
-import * as PrimitiveSet from './primitive_set';
-import {PrimitiveWrapper} from './primitive_wrapper';
-import {PbAesCtrHmacAeadKey, PbAesCtrHmacAeadKeyFormat, PbAesCtrKey, PbAesCtrKeyFormat, PbAesCtrParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbHashType, PbHmacKeyFormat, PbHmacParams, PbKeyData, PbKeyTemplate, PbMessage} from './proto';
-import * as Registry from './registry';
-import {Constructor} from './util';
-
-////////////////////////////////////////////////////////////////////////////////
-// tests
-////////////////////////////////////////////////////////////////////////////////
-
-describe('registry test', function() {
-  afterEach(function() {
-    Registry.reset();
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for registerPrimitiveWrapper method
-  it('register primitive wrapper, overwriting with same class', function() {
-    Registry.registerPrimitiveWrapper(new DummyPrimitiveWrapper1(
-        new DummyPrimitive1Impl1(), DummyPrimitive1));
-    Registry.registerPrimitiveWrapper(new DummyPrimitiveWrapper1(
-        new DummyPrimitive1Impl2(), DummyPrimitive1));
-  });
-
-  it('register primitive wrapper, overwriting with different class',
-     function() {
-       class DummyPrimitiveWrapper1Alternative implements
-           PrimitiveWrapper<DummyPrimitive1> {
-         wrap(): DummyPrimitive1 {
-           throw new Error();
-         }
-
-         getPrimitiveType() {
-           return DummyPrimitive1;
-         }
-       }
-       Registry.registerPrimitiveWrapper(new DummyPrimitiveWrapper1(
-           new DummyPrimitive1Impl1(), DummyPrimitive1));
-       try {
-         Registry.registerPrimitiveWrapper(
-             new DummyPrimitiveWrapper1Alternative());
-         fail('An exception should be thrown.');
-       } catch (e: any) {
-         expect(e.toString())
-             .toBe(
-                 'SecurityException: primitive wrapper for type ' +
-                 DummyPrimitive1 +
-                 ' has already been registered and cannot be overwritten');
-       }
-     });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for wrap method
-  it('wrap, should work', function() {
-    const p1 = new DummyPrimitive1Impl1();
-    const p2 = new DummyPrimitive2Impl();
-    Registry.registerPrimitiveWrapper(
-        new DummyPrimitiveWrapper1(p1, DummyPrimitive1));
-    Registry.registerPrimitiveWrapper(
-        new DummyPrimitiveWrapper2(p2, DummyPrimitive2));
-
-    expect(Registry.wrap(new PrimitiveSet.PrimitiveSet(DummyPrimitive1)))
-        .toBe(p1);
-    expect(Registry.wrap(new PrimitiveSet.PrimitiveSet(DummyPrimitive2)))
-        .toBe(p2);
-  });
-
-  it('wrap, not registered primitive type', function() {
-    expect(() => {
-      Registry.wrap(new PrimitiveSet.PrimitiveSet(DummyPrimitive1));
-    }).toThrowError('no primitive wrapper found for type ' + DummyPrimitive1);
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for registerKeyManager  method
-  it('register key manager, overwriting attempt', function() {
-    const keyType = 'someKeyType';
-
-    try {
-      Registry.registerKeyManager(new DummyKeyManager1(keyType));
-      Registry.registerKeyManager(new DummyKeyManager2(keyType));
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.keyManagerOverwrittingAttempt(keyType));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  // Testing newKeyAllowed behavior -- should hold the most restrictive setting.
-  it('register key manager, more restrictive new key allowed',
-     async function() {
-       const keyType = 'someTypeUrl';
-       const keyManager1 = new DummyKeyManager1(keyType);
-       const keyTemplate = new PbKeyTemplate().setTypeUrl(keyType);
-
-       // Register the key manager with new_key_allowed and test that it is
-       // possible to create a new key data.
-       Registry.registerKeyManager(keyManager1);
-       await Registry.newKeyData(keyTemplate);
-
-       // Restrict the key manager and test that new key data cannot be created.
-       Registry.registerKeyManager(keyManager1, false);
-       try {
-         await Registry.newKeyData(keyTemplate);
-       } catch (e: any) {
-         expect(e.toString()).toBe(ExceptionText.newKeyForbidden(keyType));
-         return;
-       }
-       fail('An exception should be thrown.');
-     });
-
-  it('register key manager, less restrictive new key allowed',
-     async function() {
-       const keyType = 'someTypeUrl';
-       const keyManager1 = new DummyKeyManager1(keyType);
-       const keyTemplate = new PbKeyTemplate().setTypeUrl(keyType);
-
-       Registry.registerKeyManager(keyManager1, false);
-
-       // Re-registering key manager with less restrictive setting should not be
-       // possible and the restriction has to be still true (i.e. new key data
-       // cannot be created).
-       try {
-         Registry.registerKeyManager(keyManager1);
-         fail('An exception should be thrown.');
-       } catch (e: any) {
-         expect(e.toString())
-             .toBe(ExceptionText.prohibitedChangeToLessRestricted(
-                 keyManager1.getKeyType()));
-       }
-       try {
-         await Registry.newKeyData(keyTemplate);
-       } catch (e: any) {
-         expect(e.toString()).toBe(ExceptionText.newKeyForbidden(keyType));
-         return;
-       }
-       fail('An exception should be thrown.');
-     });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getKeyManager method
-  it('get key manager, should work', function() {
-    const numberOfKeyManagers = 10;
-    const keyManagers1 = [];
-    const keyManagers2 = [];
-    for (let i = 0; i < numberOfKeyManagers; i++) {
-      keyManagers1.push(new DummyKeyManager1('someKeyType' + i.toString()));
-      keyManagers2.push(new DummyKeyManager2('otherKeyType' + i.toString()));
-
-      Registry.registerKeyManager(keyManagers1[i]);
-      Registry.registerKeyManager(keyManagers2[i]);
-    }
-
-    let result;
-    for (let i = 0; i < numberOfKeyManagers; i++) {
-      result = Registry.getKeyManager(keyManagers1[i].getKeyType());
-      expect(result).toEqual(keyManagers1[i]);
-
-      result = Registry.getKeyManager(keyManagers2[i].getKeyType());
-      expect(result).toEqual(keyManagers2[i]);
-    }
-  });
-
-  it('get key manager, not registered key type', function() {
-    const keyType = 'some_key_type';
-
-    try {
-      Registry.getKeyManager(keyType);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.notRegisteredKeyType(keyType));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for newKeyData method
-  it('new key data, no manager for given key type', async function() {
-    const keyManager1 = new DummyKeyManager1('someKeyType');
-    const differentKeyType = 'otherKeyType';
-    const keyTemplate = new PbKeyTemplate().setTypeUrl(differentKeyType);
-
-    Registry.registerKeyManager(keyManager1);
-    try {
-      await Registry.newKeyData(keyTemplate);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.notRegisteredKeyType(differentKeyType));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('new key data, new key disallowed', async function() {
-    const keyManager1 = new DummyKeyManager1('someKeyType');
-    const keyTemplate =
-        new PbKeyTemplate().setTypeUrl(keyManager1.getKeyType());
-
-    Registry.registerKeyManager(keyManager1, false);
-    try {
-      await Registry.newKeyData(keyTemplate);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.newKeyForbidden(keyManager1.getKeyType()));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('new key data, new key allowed', async function() {
-    const keyTypes: string[] = [];
-    for (let i = 0; i < 10; i++) {
-      keyTypes.push('someKeyType' + i.toString());
-    }
-
-    const keyTypesLength = keyTypes.length;
-    for (let i = 0; i < keyTypesLength; i++) {
-      Registry.registerKeyManager(new DummyKeyManager1(keyTypes[i]), true);
-    }
-
-    for (let i = 0; i < keyTypesLength; i++) {
-      const keyTemplate = new PbKeyTemplate().setTypeUrl(keyTypes[i]);
-      const result = await Registry.newKeyData(keyTemplate);
-      expect(result.getTypeUrl()).toBe(keyTypes[i]);
-    }
-  });
-
-  it('new key data, new key is allowed automatically', async function() {
-    const keyTypes: string[] = [];
-    for (let i = 0; i < 10; i++) {
-      keyTypes.push('someKeyType' + i.toString());
-    }
-
-    const keyTypesLength = keyTypes.length;
-    for (let i = 0; i < keyTypesLength; i++) {
-      Registry.registerKeyManager(new DummyKeyManager1(keyTypes[i]));
-    }
-
-    for (let i = 0; i < keyTypesLength; i++) {
-      const keyTemplate = new PbKeyTemplate().setTypeUrl(keyTypes[i]);
-      const result = await Registry.newKeyData(keyTemplate);
-      expect(result.getTypeUrl()).toBe(keyTypes[i]);
-    }
-  });
-
-  it('new key data, with aes ctr hmac aead key', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    Registry.registerKeyManager(manager);
-    const keyTemplate = createAesCtrHmacAeadTestKeyTemplate();
-    const keyData = await Registry.newKeyData(keyTemplate);
-
-    // Checks that correct AES CTR HMAC AEAD key was returned.
-    const keyFormat = PbAesCtrHmacAeadKeyFormat.deserializeBinary(
-        keyTemplate.getValue_asU8());
-    const key = PbAesCtrHmacAeadKey.deserializeBinary(keyData.getValue_asU8());
-    // Check AES CTR key.
-    expect(keyFormat.getAesCtrKeyFormat()?.getKeySize())
-        .toBe(key.getAesCtrKey()?.getKeyValue_asU8().length);
-    expect(keyFormat.getAesCtrKeyFormat()?.getParams())
-        .toEqual(key.getAesCtrKey()?.getParams());
-    // Check HMAC key.
-    expect(keyFormat.getHmacKeyFormat()?.getKeySize())
-        .toBe(key.getHmacKey()?.getKeyValue_asU8().length);
-    expect(keyFormat.getHmacKeyFormat()?.getParams())
-        .toEqual(key.getHmacKey()?.getParams());
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for newKey method
-  it('new key, no manager for given key type', async function() {
-    const notRegisteredKeyType = 'not_registered_key_type';
-    const keyTemplate = new PbKeyTemplate().setTypeUrl(notRegisteredKeyType);
-
-    try {
-      await Registry.newKey(keyTemplate);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.notRegisteredKeyType(notRegisteredKeyType));
-    }
-  });
-
-  it('new key, new key disallowed', async function() {
-    const keyManager = new DummyKeyManagerForNewKeyTests('someKeyType');
-    const keyTemplate = new PbKeyTemplate().setTypeUrl(keyManager.getKeyType());
-    Registry.registerKeyManager(keyManager, /* opt_newKeyAllowed = */ false);
-
-    try {
-      await Registry.newKey(keyTemplate);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.newKeyForbidden(keyManager.getKeyType()));
-    }
-  });
-
-  it('new key, should work', async function() {
-    const keyTypes: string[] = [];
-    const newKeyMethodResult: Uint8Array[] = [];
-    const keyTypesLength = 10;
-
-    // Add some keys to Registry.
-    for (let i = 0; i < keyTypesLength; i++) {
-      keyTypes.push('someKeyType' + i.toString());
-      newKeyMethodResult.push(new Uint8Array([i + 1]));
-
-      Registry.registerKeyManager(
-          new DummyKeyManagerForNewKeyTests(keyTypes[i], newKeyMethodResult[i]),
-          /* newKeyAllowed = */ true);
-    }
-
-    // For every keyType verify that it calls new key method of the
-    // corresponding KeyManager (KeyFactory).
-    for (let i = 0; i < keyTypesLength; i++) {
-      const keyTemplate = new PbKeyTemplate().setTypeUrl(keyTypes[i]);
-
-      const key =
-          assertInstanceof(await Registry.newKey(keyTemplate), PbAesCtrKey);
-
-      // The new key method of DummyKeyFactory returns an AesCtrKey which
-      // KeyValue is set to corresponding value in newKeyMethodResult.
-      expect(key.getKeyValue_asU8()).toBe(newKeyMethodResult[i]);
-    }
-  });
-  it('new key, with aes ctr hmac aead key', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    Registry.registerKeyManager(manager);
-    const keyTemplate = AeadKeyTemplates.aes256CtrHmacSha256();
-
-    const key = assertInstanceof(
-        await Registry.newKey(keyTemplate), PbAesCtrHmacAeadKey);
-
-    // Checks that correct AES CTR HMAC AEAD key was returned.
-    const keyFormat = PbAesCtrHmacAeadKeyFormat.deserializeBinary(
-        keyTemplate.getValue_asU8());
-    // Check AES CTR key.
-    expect(keyFormat.getAesCtrKeyFormat()?.getKeySize())
-        .toBe(key.getAesCtrKey()?.getKeyValue_asU8().length);
-    expect(keyFormat.getAesCtrKeyFormat()?.getParams())
-        .toEqual(key.getAesCtrKey()?.getParams());
-    // Check HMAC key.
-    expect(keyFormat.getHmacKeyFormat()?.getKeySize())
-        .toBe(key.getHmacKey()?.getKeyValue_asU8().length);
-    expect(keyFormat.getHmacKeyFormat()?.getParams())
-        .toEqual(key.getHmacKey()?.getParams());
-  });
-
-  /////////////////////////////////////////////////////////////////////////////
-  // tests for getPrimitive method
-  it('get primitive, different key types', async function() {
-    const keyDataType = 'key_data_key_type_url';
-    const anotherType = 'another_key_type_url';
-    const keyData = new PbKeyData().setTypeUrl(keyDataType);
-
-    try {
-      await Registry.getPrimitive(Aead, keyData, anotherType);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.keyTypesAreNotMatching(keyDataType, anotherType));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('get primitive, without defining key type', async function() {
-    // Get primitive from key proto without key type.
-    try {
-      await Registry.getPrimitive(Aead, new PbHmacParams());
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.keyTypeNotDefined());
-    }
-  });
-
-  it('get primitive, missing key manager', async function() {
-    const keyDataType = 'key_data_key_type_url';
-    const keyData = new PbKeyData().setTypeUrl(keyDataType);
-
-    try {
-      await Registry.getPrimitive(Aead, keyData);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.notRegisteredKeyType(keyDataType));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('get primitive, from aes ctr hmac aead key data', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    Registry.registerKeyManager(manager);
-    const keyTemplate = createAesCtrHmacAeadTestKeyTemplate();
-    const keyData = await Registry.newKeyData(keyTemplate);
-
-    const primitive =
-        await Registry.getPrimitive(manager.getPrimitiveType(), keyData);
-    expect(primitive instanceof EncryptThenAuthenticate).toBe(true);
-  });
-
-  it('get primitive, from aes ctr hmac aead key', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    Registry.registerKeyManager(manager);
-    const keyTemplate = createAesCtrHmacAeadTestKeyTemplate();
-    const keyData = await Registry.newKeyData(keyTemplate);
-    const key = PbAesCtrHmacAeadKey.deserializeBinary(keyData.getValue_asU8());
-
-    const primitive = await Registry.getPrimitive(
-        manager.getPrimitiveType(), key, keyData.getTypeUrl());
-    expect(primitive instanceof EncryptThenAuthenticate).toBe(true);
-  });
-
-  it('get primitive, mac from aes ctr hmac aead key', async function() {
-    const manager = new AesCtrHmacAeadKeyManager();
-    Registry.registerKeyManager(manager);
-    const keyTemplate = createAesCtrHmacAeadTestKeyTemplate();
-    const keyData = await Registry.newKeyData(keyTemplate);
-    const key = PbAesCtrHmacAeadKey.deserializeBinary(keyData.getValue_asU8());
-
-    try {
-      await Registry.getPrimitive(Mac, key, keyData.getTypeUrl());
-    } catch (e: any) {
-      expect(e.toString().includes(ExceptionText.getPrimitiveBadPrimitive()))
-          .toBe(true);
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  describe('get public key data', function() {
-    it('not private key factory', function() {
-      AeadConfig.register();
-      const notPrivateTypeUrl = AeadConfig.AES_GCM_TYPE_URL;
-      try {
-        Registry.getPublicKeyData(notPrivateTypeUrl, new Uint8Array(8));
-        fail('An exception should be thrown.');
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe(ExceptionText.notPrivateKeyFactory(notPrivateTypeUrl));
-      }
-    });
-
-    it('invalid private key proto serialization', function() {
-      HybridConfig.register();
-      const typeUrl = HybridConfig.ECIES_AEAD_HKDF_PRIVATE_KEY_TYPE;
-      try {
-        Registry.getPublicKeyData(typeUrl, new Uint8Array(10));
-        fail('An exception should be thrown.');
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.couldNotParse(typeUrl));
-      }
-    });
-
-    it('should work', async function() {
-      HybridConfig.register();
-      const privateKeyData = await Registry.newKeyData(
-          HybridKeyTemplates.eciesP256HkdfHmacSha256Aes128Gcm());
-      const privateKey = PbEciesAeadHkdfPrivateKey.deserializeBinary(
-          privateKeyData.getValue_asU8());
-
-      const publicKeyData = Registry.getPublicKeyData(
-          privateKeyData.getTypeUrl(), privateKeyData.getValue_asU8());
-      expect(HybridConfig.ECIES_AEAD_HKDF_PUBLIC_KEY_TYPE)
-          .toBe(publicKeyData.getTypeUrl());
-      expect(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC)
-          .toBe(publicKeyData.getKeyMaterialType());
-
-      const expectedPublicKey = assertExists(privateKey.getPublicKey());
-      const publicKey = PbEciesAeadHkdfPublicKey.deserializeBinary(
-          publicKeyData.getValue_asU8());
-      expect(publicKey).toEqual(expectedPublicKey);
-    });
-  });
-});
-
-////////////////////////////////////////////////////////////////////////////////
-// helper functions and classes for tests
-////////////////////////////////////////////////////////////////////////////////
-
-/**
- * Class which holds texts for each type of exception.
- * @final
- */
-class ExceptionText {
-  static notImplemented(): string {
-    return 'SecurityException: Not implemented yet.';
-  }
-
-  static newKeyForbidden(keyType: string): string {
-    return 'SecurityException: New key operation is forbidden for key type: ' +
-        keyType + '.';
-  }
-
-  static notRegisteredKeyType(keyType: string): string {
-    return 'SecurityException: Key manager for key type ' + keyType +
-        ' has not been registered.';
-  }
-
-  static nullKeyManager(): string {
-    return 'SecurityException: Key manager cannot be null.';
-  }
-
-  static undefinedKeyType(): string {
-    return 'SecurityException: Key type has to be defined.';
-  }
-
-  static keyManagerOverwrittingAttempt(keyType: string): string {
-    return 'SecurityException: Key manager for key type ' + keyType +
-        ' has already been registered and cannot be overwritten.';
-  }
-
-  static notSupportedKey(givenKeyType: string): string {
-    return 'SecurityException: The provided key manager does not support ' +
-        'key type ' + givenKeyType + '.';
-  }
-
-  static prohibitedChangeToLessRestricted(keyType: string): string {
-    return 'SecurityException: Key manager for key type ' + keyType +
-        ' has already been registered with forbidden new key operation.';
-  }
-
-  static keyTypesAreNotMatching(
-      keyTypeFromKeyData: string, keyTypeParam: string): string {
-    return 'SecurityException: Key type is ' + keyTypeParam +
-        ', but it is expected to be ' + keyTypeFromKeyData + ' or undefined.';
-  }
-
-  static keyTypeNotDefined(): string {
-    return 'SecurityException: Key type has to be specified.';
-  }
-
-  static nullKeysetHandle(): string {
-    return 'SecurityException: Keyset handle has to be non-null.';
-  }
-
-  static getPrimitiveBadPrimitive(): string {
-    return 'Requested primitive type which is not supported by this ' +
-        'key manager.';
-  }
-
-  static notPrivateKeyFactory(typeUrl: string): string {
-    return 'SecurityException: Key manager for key type ' + typeUrl +
-        ' does not have a private key factory.';
-  }
-
-  static couldNotParse(typeUrl: string): string {
-    return 'SecurityException: Input cannot be parsed as ' + typeUrl +
-        ' key-proto.';
-  }
-}
-
-/** Creates AES CTR HMAC AEAD key format which can be used in tests */
-function createAesCtrHmacAeadTestKeyTemplate(): PbKeyTemplate {
-  const KEY_SIZE = 16;
-  const IV_SIZE = 12;
-  const TAG_SIZE = 16;
-
-  const keyFormat = new PbAesCtrHmacAeadKeyFormat().setAesCtrKeyFormat(
-      new PbAesCtrKeyFormat());
-  keyFormat.getAesCtrKeyFormat()?.setKeySize(KEY_SIZE);
-  keyFormat.getAesCtrKeyFormat()?.setParams(new PbAesCtrParams());
-  keyFormat.getAesCtrKeyFormat()?.getParams()?.setIvSize(IV_SIZE);
-
-  // set HMAC key
-  keyFormat.setHmacKeyFormat(new PbHmacKeyFormat());
-  keyFormat.getHmacKeyFormat()?.setKeySize(KEY_SIZE);
-  keyFormat.getHmacKeyFormat()?.setParams(new PbHmacParams());
-  keyFormat.getHmacKeyFormat()?.getParams()?.setHash(PbHashType.SHA1);
-  keyFormat.getHmacKeyFormat()?.getParams()?.setTagSize(TAG_SIZE);
-
-  let keyTemplate =
-      new PbKeyTemplate()
-          .setTypeUrl(
-              'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey')
-          .setValue(keyFormat.serializeBinary());
-  return keyTemplate;
-}
-
-// Key factory and key manager classes used in tests
-
-/** @final */
-class DummyKeyFactory implements KeyManager.KeyFactory {
-  constructor(
-      private readonly keyType: string,
-      private readonly newKeyMethodResult = new Uint8Array(10)) {}
-
-  /**
-   */
-  newKey(keyFormat: PbMessage|Uint8Array) {
-    const key = new PbAesCtrKey().setKeyValue(this.newKeyMethodResult);
-    return key;
-  }
-
-  /**
-   */
-  newKeyData(serializedKeyFormat: Uint8Array) {
-    const keyData =
-        new PbKeyData()
-            .setTypeUrl(this.keyType)
-            .setValue(this.newKeyMethodResult)
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.UNKNOWN_KEYMATERIAL);
-
-    return keyData;
-  }
-}
-
-// Primitive abstract types for testing purposes.
-abstract class DummyPrimitive1 {
-  abstract operation1(): number;
-}
-abstract class DummyPrimitive2 {
-  abstract operation2(): string;
-}
-
-// Primitive implementations for testing purposes.
-class DummyPrimitive1Impl1 extends DummyPrimitive1 {
-  operation1() {
-    return 1;
-  }
-}
-class DummyPrimitive1Impl2 extends DummyPrimitive1 {
-  operation1() {
-    return 2;
-  }
-}
-class DummyPrimitive2Impl extends DummyPrimitive2 {
-  operation2() {
-    return 'dummy';
-  }
-}
-
-const DEFAULT_PRIMITIVE_TYPE = Aead;
-
-/** @final */
-class DummyKeyManager1 implements KeyManager.KeyManager<DummyPrimitive1> {
-  private readonly KEY_FACTORY: KeyManager.KeyFactory;
-
-  constructor(
-      private readonly keyType: string,
-      private readonly primitive: DummyPrimitive1 = new DummyPrimitive1Impl1(),
-      private readonly primitiveType = DummyPrimitive1) {
-    this.KEY_FACTORY = new DummyKeyFactory(keyType);
-  }
-
-  async getPrimitive(
-      primitiveType: Constructor<DummyKeyManager1>, key: PbKeyData|PbMessage) {
-    return this.primitive;
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return this.keyType;
-  }
-
-  getPrimitiveType(): Constructor<DummyPrimitive1> {
-    return this.primitiveType;
-  }
-
-  getVersion(): number {
-    throw new SecurityException('Not implemented, only for testing purposes.');
-  }
-
-  getKeyFactory() {
-    return this.KEY_FACTORY;
-  }
-}
-
-/** @final */
-class DummyKeyManager2 implements KeyManager.KeyManager<DummyPrimitive2> {
-  private readonly KEY_FACTORY: KeyManager.KeyFactory;
-
-  constructor(
-      private readonly keyType: string,
-      private readonly primitive: DummyPrimitive2 = new DummyPrimitive2Impl(),
-      private readonly primitiveType = DummyPrimitive2) {
-    this.KEY_FACTORY = new DummyKeyFactory(keyType);
-  }
-
-  async getPrimitive(
-      primitiveType: Constructor<DummyKeyManager2>, key: PbKeyData|PbMessage) {
-    return this.primitive;
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return this.keyType;
-  }
-
-  getPrimitiveType() {
-    return this.primitiveType;
-  }
-
-  getVersion(): number {
-    throw new SecurityException('Not implemented, only for testing purposes.');
-  }
-
-  getKeyFactory() {
-    return this.KEY_FACTORY;
-  }
-}
-
-/** @final */
-class DummyKeyManagerForNewKeyTests implements KeyManager.KeyManager<string> {
-  private readonly KEY_FACTORY: KeyManager.KeyFactory;
-
-  constructor(
-      private readonly keyType: string, opt_newKeyMethodResult?: Uint8Array) {
-    this.KEY_FACTORY = new DummyKeyFactory(keyType, opt_newKeyMethodResult);
-  }
-
-  async getPrimitive(
-      primitiveType: Constructor<DummyKeyManagerForNewKeyTests>,
-      key: PbKeyData|PbMessage): Promise<string> {
-    throw new SecurityException('Not implemented, function is not needed.');
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return this.keyType;
-  }
-
-  getPrimitiveType(): never {
-    throw new SecurityException('Not implemented, function is not needed.');
-  }
-
-  getVersion(): never {
-    throw new SecurityException('Not implemented, function is not needed.');
-  }
-
-  getKeyFactory() {
-    return this.KEY_FACTORY;
-  }
-}
-
-// PrimitiveWrapper classes for testing purposes
-
-/** @final */
-class DummyPrimitiveWrapper1 implements PrimitiveWrapper<DummyPrimitive1> {
-  constructor(
-      private readonly primitive: DummyPrimitive1,
-      private readonly primitiveType: Constructor<DummyPrimitive1>) {}
-
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<DummyPrimitive1>) {
-    return this.primitive;
-  }
-
-  getPrimitiveType() {
-    return this.primitiveType;
-  }
-}
-
-/** @final */
-class DummyPrimitiveWrapper2 implements PrimitiveWrapper<DummyPrimitive2> {
-  constructor(
-      private readonly primitive: DummyPrimitive2,
-      private readonly primitiveType: Constructor<DummyPrimitive2>) {}
-
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<DummyPrimitive2>) {
-    return this.primitive;
-  }
-
-  getPrimitiveType() {
-    return this.primitiveType;
-  }
-}
diff --git a/javascript/internal/ts_library_from_closure.bzl b/javascript/internal/ts_library_from_closure.bzl
deleted file mode 100644
index d621c67..0000000
--- a/javascript/internal/ts_library_from_closure.bzl
+++ /dev/null
@@ -1,95 +0,0 @@
-"""Hacky way of allowing rules_nodejs targets to depend on rules_closure targets.
-
-We don't have a way of producing TypeScript code directly from .proto schemas.
-Nor do we have a way of managing their dependencies using ts_library. So what
-we do is use closure_rules to generate code for the protos and to package up
-all that code and its dependencies into a single .js file, add the required
-ES exports and suppressions to that file, and feed it into the TypeScript
-compiler.
-"""
-
-load("@bazel_skylib//rules:write_file.bzl", "write_file")
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-def ts_library_from_closure(name, namespace_aliases, deps):
-    """Returns a ts_library that wraps the given Closure libraries.
-
-    Args:
-      name: The name to give to the ts_library and its ES module.
-      namespace_aliases: A dictionary whose keys are the names that this
-        TypeScript ES module should export, and the values are the Closure
-        namespaces whose values should be exported under those names.
-      deps: The Closure libraries providing the above namespaces.
-    """
-    write_file(
-        name = name + "_entry_point_js",
-        out = name + "_entry_point.js",
-        content = ["goog.module('epm${name}');".format(name = name)] + [
-            "const epr${alias} = goog.require('{namespace}');".format(
-                alias = alias,
-                namespace = namespace,
-            )
-            for alias, namespace in namespace_aliases.items()
-        ] + [
-            "{alias} = epr${alias};".format(alias = alias)
-            for alias in namespace_aliases
-        ],
-    )
-    write_file(
-        name = name + "_externs_js",
-        out = name + "_externs.js",
-        content = ["/** @externs */"] + [
-            "let {alias};".format(alias = alias)
-            for alias in namespace_aliases
-        ],
-    )
-    closure_js_library(
-        name = name + "_lib",
-        srcs = [
-            name + "_entry_point.js",
-            name + "_externs.js",
-        ],
-        deps = deps,
-    )
-    closure_js_binary(
-        name = name + "_bin",
-        deps = [name + "_lib"],
-        # ECMASCRIPT_NEXT is required since the subtle elliptic curve point
-        # compression functions depend on BigInt, which was introduced in ES2020.
-        # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
-        language = "ECMASCRIPT_NEXT",
-        entry_points = ["epm${name}".format(name = name)],
-        # Advanced optimizations remove dead code, which is a problem here
-        # because we have no way to straightforwardly tell Closure Compiler
-        # that the publicly-exposed properties of classes that are part of our
-        # public API aren't dead code.
-        compilation_level = "SIMPLE",
-        # For easier debugging. Terser will remove this whitespace from
-        # production builds.
-        formatting = "PRETTY_PRINT",
-    )
-    write_file(
-        name = name + "_preamble_js",
-        out = name + "_preamble.js",
-        content = ["// @ts-nocheck"] + [
-            "export let {alias};".format(alias = alias)
-            for alias in namespace_aliases
-        ] + [
-            "export type {alias} = any;".format(alias = alias)
-            for alias in namespace_aliases
-        ],
-    )
-    native.genrule(
-        name = name + "_ts",
-        srcs = [
-            name + "_preamble.js",
-            name + "_bin.js",
-        ],
-        outs = [name + ".ts"],
-        cmd = "cat $(SRCS) >$@",
-    )
-    ts_library(
-        name = name,
-        srcs = [name + ".ts"],
-    )
diff --git a/javascript/internal/util.ts b/javascript/internal/util.ts
deleted file mode 100644
index 1c293f7..0000000
--- a/javascript/internal/util.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as Bytes from '../subtle/bytes';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-
-import {PbEllipticCurveType, PbHashType, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType, PbPointFormat} from './proto';
-
-/**
- * A type representing the constructor function for a given class. Unlike
- * TypeScript's built-in `new` types, this works with abstract classes. It is
- * used to describe the relationship between a primitive type object and its
- * instances.
- */
-export type Constructor<T> = Function&{prototype: T};
-
-/** Like the `instanceof` operator, but works with `Constructor`. */
-export function isInstanceOf<T>(
-    value: unknown, ctor: Constructor<T>): value is T {
-  return value instanceof ctor;
-}
-
-/**
- * Validates the given key and throws SecurityException if it is invalid.
- *
- */
-export function validateKey(key: PbKeysetKey) {
-  if (!key) {
-    throw new SecurityException('Key should be non null.');
-  }
-  if (!key.getKeyData()) {
-    throw new SecurityException(
-        'Key data are missing for key ' + key.getKeyId() + '.');
-  }
-  if (key.getOutputPrefixType() === PbOutputPrefixType.UNKNOWN_PREFIX) {
-    throw new SecurityException(
-        'Key ' + key.getKeyId() + ' has unknown output prefix type.');
-  }
-  if (key.getStatus() === PbKeyStatusType.UNKNOWN_STATUS) {
-    throw new SecurityException(
-        'Key ' + key.getKeyId() + ' has unknown status.');
-  }
-}
-
-/**
- * Validates the given keyset and throws SecurityException if it is invalid.
- *
- */
-export function validateKeyset(keyset: PbKeyset) {
-  if (!keyset || !keyset.getKeyList() || keyset.getKeyList().length < 1) {
-    throw new SecurityException(
-        'Keyset should be non null and must contain at least one key.');
-  }
-  let hasPrimary = false;
-  const numberOfKeys = keyset.getKeyList().length;
-  for (let i = 0; i < numberOfKeys; i++) {
-    const key = keyset.getKeyList()[i];
-    validateKey(key);
-    if (keyset.getPrimaryKeyId() === key.getKeyId() &&
-        key.getStatus() === PbKeyStatusType.ENABLED) {
-      if (hasPrimary) {
-        throw new SecurityException('Primary key has to be unique.');
-      }
-      hasPrimary = true;
-    }
-  }
-  if (!hasPrimary) {
-    throw new SecurityException(
-        'Primary key has to be in the keyset and ' +
-        'has to be enabled.');
-  }
-}
-
-// Functions which are useful for implementation of
-// private and public EC keys.
-
-/**
- * Either prolong or shrinks the array representing number in BigEndian encoding
- * to have the specified size. As webcrypto API assumes that x, y and d values
- * has exactly the supposed number of bytes, whereas corresponding x, y and
- * keyValue values in proto might either have some leading zeros or the leading
- * zeros might be missing.
- *
- */
-export function bigEndianNumberToCorrectLength(
-    bigEndianNumber: Uint8Array, sizeInBytes: number): Uint8Array {
-  const numberLen = bigEndianNumber.length;
-  if (numberLen < sizeInBytes) {
-    const zeros = new Uint8Array(sizeInBytes - numberLen);
-    return Bytes.concat(zeros, bigEndianNumber);
-  }
-  if (numberLen > sizeInBytes) {
-    for (let i = 0; i < numberLen - sizeInBytes; i++) {
-      if (bigEndianNumber[i] != 0) {
-        throw new SecurityException(
-            'Number needs more bytes to be represented.');
-      }
-    }
-    return bigEndianNumber.slice(numberLen - sizeInBytes, numberLen);
-  }
-  return bigEndianNumber;
-}
-
-export function curveTypeProtoToSubtle(curveTypeProto: PbEllipticCurveType):
-    EllipticCurves.CurveType {
-  switch (curveTypeProto) {
-    case PbEllipticCurveType.NIST_P256:
-      return EllipticCurves.CurveType.P256;
-    case PbEllipticCurveType.NIST_P384:
-      return EllipticCurves.CurveType.P384;
-    case PbEllipticCurveType.NIST_P521:
-      return EllipticCurves.CurveType.P521;
-    default:
-      throw new SecurityException('Unknown curve type.');
-  }
-}
-
-export function hashTypeProtoToString(hashTypeProto: PbHashType): string {
-  switch (hashTypeProto) {
-    case PbHashType.SHA1:
-      return 'SHA-1';
-    case PbHashType.SHA256:
-      return 'SHA-256';
-    case PbHashType.SHA512:
-      return 'SHA-512';
-    default:
-      throw new SecurityException('Unknown hash type.');
-  }
-}
-
-export function pointFormatProtoToSubtle(pointFormatProto: PbPointFormat):
-    EllipticCurves.PointFormatType {
-  switch (pointFormatProto) {
-    case PbPointFormat.UNCOMPRESSED:
-      return EllipticCurves.PointFormatType.UNCOMPRESSED;
-    case PbPointFormat.COMPRESSED:
-      return EllipticCurves.PointFormatType.COMPRESSED;
-    case PbPointFormat.DO_NOT_USE_CRUNCHY_UNCOMPRESSED:
-      return EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED;
-    default:
-      throw new SecurityException('Unknown point format.');
-  }
-}
diff --git a/javascript/internal/util_test.ts b/javascript/internal/util_test.ts
deleted file mode 100644
index 033de5c..0000000
--- a/javascript/internal/util_test.ts
+++ /dev/null
@@ -1,229 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as EllipticCurves from '../subtle/elliptic_curves';
-
-import {PbEllipticCurveType, PbHashType, PbKeyData, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType, PbPointFormat} from './proto';
-import * as Util from './util';
-
-////////////////////////////////////////////////////////////////////////////////
-// tests
-////////////////////////////////////////////////////////////////////////////////
-
-describe('util test', function() {
-  // tests for validateKey method
-  it('validate key missing key data', async function() {
-    const key = createKey().setKeyData(null);
-
-    try {
-      Util.validateKey(key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.InvalidKeyMissingKeyData(key.getKeyId()));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate key unknown prefix', async function() {
-    const key =
-        createKey().setOutputPrefixType(PbOutputPrefixType.UNKNOWN_PREFIX);
-
-    try {
-      Util.validateKey(key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.InvalidKeyUnknownPrefix(key.getKeyId()));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate key unknown status', async function() {
-    const key = createKey().setStatus(PbKeyStatusType.UNKNOWN_STATUS);
-
-    try {
-      Util.validateKey(key);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.InvalidKeyUnknownStatus(key.getKeyId()));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate key valid keys', async function() {
-    Util.validateKey(createKey());
-    Util.validateKey(
-        createKey(/* opt_keyId = */ 0xAABBCCDD, /* opt_enabled = */ true));
-    Util.validateKey(
-        createKey(/* opt_keyId = */ 0xABCDABCD, /* opt_enabled = */ false));
-  });
-
-  // tests for validateKeyset method
-  it('validate keyset without keys', async function() {
-    const keyset = new PbKeyset();
-
-    try {
-      Util.validateKeyset(keyset);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.InvalidKeysetMissingKeys());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate keyset disabled primary', async function() {
-    const keyset = createKeyset();
-    keyset.addKey(
-        createKey(/* opt_id = */ 0xFFFFFFFF, /* opt_enabled = */ false));
-    keyset.setPrimaryKeyId(0xFFFFFFFF);
-
-    try {
-      Util.validateKeyset(keyset);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.InvalidKeysetDisabledPrimary());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate keyset multiple primaries', async function() {
-    const keyset = createKeyset();
-    const key =
-        createKey(/* opt_id = */ 0xFFFFFFFF, /* opt_enabled = */ true);
-    keyset.addKey(key);
-    keyset.addKey(key);
-    keyset.setPrimaryKeyId(0xFFFFFFFF);
-
-    try {
-      Util.validateKeyset(keyset);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.InvalidKeysetMultiplePrimaries());
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate keyset with invalid key', async function() {
-    const keyset = createKeyset();
-    const key =
-        createKey(4294967295, true).setStatus(PbKeyStatusType.UNKNOWN_STATUS);
-    keyset.addKey(key);
-
-    try {
-      Util.validateKeyset(keyset);
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.InvalidKeyUnknownStatus(key.getKeyId()));
-      return;
-    }
-    fail('An exception should be thrown.');
-  });
-
-  it('validate keyset with valid keyset', async function() {
-    const keyset = createKeyset();
-
-    Util.validateKeyset(keyset);
-  });
-
-  // tests for protoToSubtle methods
-  it('curve type proto to subtle', function() {
-    expect(Util.curveTypeProtoToSubtle(PbEllipticCurveType.NIST_P256))
-        .toBe(EllipticCurves.CurveType.P256);
-    expect(Util.curveTypeProtoToSubtle(PbEllipticCurveType.NIST_P384))
-        .toBe(EllipticCurves.CurveType.P384);
-    expect(Util.curveTypeProtoToSubtle(PbEllipticCurveType.NIST_P521))
-        .toBe(EllipticCurves.CurveType.P521);
-  });
-
-  it('point format proto to subtle', function() {
-    expect(Util.pointFormatProtoToSubtle(PbPointFormat.UNCOMPRESSED))
-        .toBe(EllipticCurves.PointFormatType.UNCOMPRESSED);
-    expect(Util.pointFormatProtoToSubtle(PbPointFormat.COMPRESSED))
-        .toBe(EllipticCurves.PointFormatType.COMPRESSED);
-    expect(Util.pointFormatProtoToSubtle(
-               PbPointFormat.DO_NOT_USE_CRUNCHY_UNCOMPRESSED))
-        .toBe(EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED);
-  });
-
-  it('hash type proto to string', function() {
-    expect(Util.hashTypeProtoToString(PbHashType.SHA1)).toBe('SHA-1');
-    expect(Util.hashTypeProtoToString(PbHashType.SHA256)).toBe('SHA-256');
-    expect(Util.hashTypeProtoToString(PbHashType.SHA512)).toBe('SHA-512');
-  });
-});
-
-
-/**
- * Class which holds texts for each type of exception.
- * @final
- */
-class ExceptionText {
-  // Exceptions for invalid keys.
-  static InvalidKeyMissingKeyData(keyId: number): string {
-    return 'SecurityException: Key data are missing for key ' + keyId + '.';
-  }
-  static InvalidKeyUnknownPrefix(keyId: number): string {
-    return 'SecurityException: Key ' + keyId +
-        ' has unknown output prefix type.';
-  }
-  static InvalidKeyUnknownStatus(keyId: number): string {
-    return 'SecurityException: Key ' + keyId + ' has unknown status.';
-  }
-
-  // Exceptions for invalid keysets.
-  static InvalidKeysetMissingKeys(): string {
-    return 'SecurityException: Keyset should be non null and ' +
-        'must contain at least one key.';
-  }
-  static InvalidKeysetDisabledPrimary(): string {
-    return 'SecurityException: Primary key has to be in the keyset and ' +
-        'has to be enabled.';
-  }
-  static InvalidKeysetMultiplePrimaries(): string {
-    return 'SecurityException: Primary key has to be unique.';
-  }
-}
-
-/** Returns a valid PbKeysetKey. */
-function createKey(
-    opt_id: number = 0x12345678, opt_enabled: boolean = true,
-    opt_publicKey: boolean = false): PbKeysetKey {
-  const keyData =
-      new PbKeyData().setTypeUrl('someTypeUrl').setValue(new Uint8Array(10));
-  if (opt_publicKey) {
-    keyData.setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC);
-  } else {
-    keyData.setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
-  }
-
-  const key = new PbKeysetKey().setKeyData(keyData);
-  if (opt_enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-  key.setKeyId(opt_id);
-  key.setOutputPrefixType(PbOutputPrefixType.TINK);
-
-  return key;
-}
-
-/** Returns a valid PbKeyset which primary has id equal to 1. */
-function createKeyset(): PbKeyset {
-  const numberOfKeys = 20;
-
-  const keyset = new PbKeyset();
-  for (let i = 0; i < numberOfKeys; i++) {
-    // Key id is never set to 0 as primaryKeyId = 0 if it is unset.
-    const key = createKey(i + 1, /* opt_enabled = */ (i % 2) < 1, (i % 4) < 2);
-    keyset.addKey(key);
-  }
-
-  keyset.setPrimaryKeyId(1);
-  return keyset;
-}
diff --git a/javascript/keyset_handle.ts b/javascript/keyset_handle.ts
deleted file mode 100644
index f25ee4e..0000000
--- a/javascript/keyset_handle.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {generateNew, KeysetHandle} from './internal/keyset_handle';
diff --git a/javascript/mac/BUILD.bazel b/javascript/mac/BUILD.bazel
deleted file mode 100644
index ee95448..0000000
--- a/javascript/mac/BUILD.bazel
+++ /dev/null
@@ -1,13 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "mac",
-    srcs = [
-        "index.ts",
-        "mac.ts",
-    ],
-    module_name = "tink-crypto/mac",
-    deps = ["//mac/internal"],
-)
diff --git a/javascript/mac/index.ts b/javascript/mac/index.ts
deleted file mode 100644
index f3cd1b4..0000000
--- a/javascript/mac/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export * from './mac';
diff --git a/javascript/mac/internal/BUILD.bazel b/javascript/mac/internal/BUILD.bazel
deleted file mode 100644
index bdfe0d8..0000000
--- a/javascript/mac/internal/BUILD.bazel
+++ /dev/null
@@ -1,8 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "internal",
-    srcs = ["mac.ts"],
-)
diff --git a/javascript/mac/internal/mac.ts b/javascript/mac/internal/mac.ts
deleted file mode 100644
index 31e422d..0000000
--- a/javascript/mac/internal/mac.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for Message Authentication Codes (MAC).
- *
- * Security guarantees: Message Authentication Codes provide symmetric message
- * authentication. Instances implementing this interface are secure against
- * existential forgery under chosen plaintext attack, and can be deterministic
- * or randomized. This interface should be used for authentication only, and not
- * for other purposes like generation of pseudorandom bytes.
- *
- */
-export abstract class Mac {
-  /**
-   * Computes message authentication code (MAC) for `data`.
-   *
-   * @param data the data to compute MAC
-   * @return the MAC tag
-   */
-  abstract computeMac(data: Uint8Array): Promise<Uint8Array>;
-
-  /**
-   * Verifies whether `tag` is a correct authentication code for `data`.
-   *
-   * @param tag  the MAC tag
-   * @param data the data to compute MAC
-   */
-  abstract verifyMac(tag: Uint8Array, data: Uint8Array): Promise<boolean>;
-}
diff --git a/javascript/mac/mac.ts b/javascript/mac/mac.ts
deleted file mode 100644
index e4d7165..0000000
--- a/javascript/mac/mac.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {Mac} from './internal/mac';
diff --git a/javascript/mac/subtle/BUILD.bazel b/javascript/mac/subtle/BUILD.bazel
deleted file mode 100644
index 56c4a62..0000000
--- a/javascript/mac/subtle/BUILD.bazel
+++ /dev/null
@@ -1,13 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "subtle",
-    srcs = [
-        "hmac.ts",
-        "index.ts",
-    ],
-    module_name = "tink-crypto/mac/subtle",
-    deps = ["//subtle"],
-)
diff --git a/javascript/mac/subtle/hmac.ts b/javascript/mac/subtle/hmac.ts
deleted file mode 100644
index ed30a9d..0000000
--- a/javascript/mac/subtle/hmac.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {fromRawKey, Hmac} from '../../subtle/hmac';
diff --git a/javascript/mac/subtle/index.ts b/javascript/mac/subtle/index.ts
deleted file mode 100644
index 74b357d..0000000
--- a/javascript/mac/subtle/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {fromRawKey as hmacFromRawKey, Hmac} from './hmac';
diff --git a/javascript/package.json b/javascript/package.json
deleted file mode 100644
index e24547d..0000000
--- a/javascript/package.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
-  "name": "tink-crypto",
-  "version": "0.1.0",
-  "description": "A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.",
-  "keywords": [
-    "crypto",
-    "cryptography",
-    "security",
-    "encryption",
-    "AEAD",
-    "MAC",
-    "signature",
-    "AES-CTR-HMAC",
-    "AES-GCM",
-    "HMAC",
-    "ECDSA",
-    "ECIES",
-    "HKDF"
-  ],
-  "homepage": "https://github.com/google/tink#readme",
-  "bugs": "https://github.com/google/tink/issues",
-  "license": "Apache-2.0",
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/google/tink.git",
-    "directory": "javascript"
-  },
-  "dependencies": {
-    "tslib": "^2.0.1"
-  },
-  "devDependencies": {
-    "@angular/bazel": "12.2.11",
-    "@bazel/concatjs": "4.4.0",
-    "@bazel/typescript": "4.4.0",
-    "@types/jasmine": "3.10.1",
-    "jasmine-core": "3.10.1",
-    "karma": "6.3.16",
-    "karma-chrome-launcher": "3.1.0",
-    "karma-firefox-launcher": "2.1.1",
-    "karma-jasmine": "4.0.1",
-    "karma-json-result-reporter": "1.0.0",
-    "karma-requirejs": "1.1.0",
-    "karma-sourcemap-loader": "0.3.8",
-    "requirejs": "2.3.6",
-    "rollup": "2.58.3",
-    "rollup-plugin-commonjs": "10.1.0",
-    "rollup-plugin-node-resolve": "5.2.0",
-    "rollup-plugin-sourcemaps": "0.6.3",
-    "terser": "5.14.2",
-    "typescript": "4.4.4"
-  },
-  "sideEffects": false,
-  "publishConfig": {
-    "registry": "https://wombat-dressing-room.appspot.com"
-  }
-}
diff --git a/javascript/proto/BUILD.bazel b/javascript/proto/BUILD.bazel
deleted file mode 100644
index 70a8e53..0000000
--- a/javascript/proto/BUILD.bazel
+++ /dev/null
@@ -1,410 +0,0 @@
-licenses(["notice"])
-
-# -----------------------------------------------
-# common
-# -----------------------------------------------
-proto_library(
-    name = "common_proto",
-    srcs = [
-        "common.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# tink
-# -----------------------------------------------
-proto_library(
-    name = "tink_proto",
-    srcs = [
-        "tink.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# config
-# -----------------------------------------------
-proto_library(
-    name = "config_proto",
-    srcs = [
-        "config.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# aes-siv
-# -----------------------------------------------
-proto_library(
-    name = "aes_siv_proto",
-    srcs = [
-        "aes_siv.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# rsa_ssa_pkcs1
-# -----------------------------------------------
-proto_library(
-    name = "rsa_ssa_pkcs1_proto",
-    srcs = [
-        "rsa_ssa_pkcs1.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":common_proto",
-    ],
-)
-
-# -----------------------------------------------
-# cached_dek_aead
-# -----------------------------------------------
-proto_library(
-    name = "cached_dek_aead_proto",
-    srcs = [
-        "cached_dek_aead.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# cached_dek_envelope
-# -----------------------------------------------
-proto_library(
-    name = "cached_dek_envelope_proto",
-    srcs = [
-        "cached_dek_envelope.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [":tink_proto"],
-)
-
-# -----------------------------------------------
-# rsa_ssa_pss
-# -----------------------------------------------
-proto_library(
-    name = "rsa_ssa_pss_proto",
-    srcs = [
-        "rsa_ssa_pss.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":common_proto",
-    ],
-)
-
-# -----------------------------------------------
-# ecdsa
-# -----------------------------------------------
-proto_library(
-    name = "ecdsa_proto",
-    srcs = [
-        "ecdsa.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":common_proto",
-    ],
-)
-
-# -----------------------------------------------
-# jwt_rsa_ssa_pkcs1
-# -----------------------------------------------
-proto_library(
-    name = "jwt_rsa_ssa_pkcs1_proto",
-    srcs = [
-        "jwt_rsa_ssa_pkcs1.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# jwt_rsa_ssa_pss
-# -----------------------------------------------
-proto_library(
-    name = "jwt_rsa_ssa_pss_proto",
-    srcs = [
-        "jwt_rsa_ssa_pss.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# jwt_ecdsa
-# -----------------------------------------------
-proto_library(
-    name = "jwt_ecdsa_proto",
-    srcs = [
-        "jwt_ecdsa.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# ed25519
-# -----------------------------------------------
-proto_library(
-    name = "ed25519_proto",
-    srcs = [
-        "ed25519.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# aes_cmac
-# -----------------------------------------------
-proto_library(
-    name = "aes_cmac_proto",
-    srcs = [
-        "aes_cmac.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# hmac
-# -----------------------------------------------
-proto_library(
-    name = "hmac_proto",
-    srcs = [
-        "hmac.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [":common_proto"],
-)
-
-# -----------------------------------------------
-# JWT hmac
-# -----------------------------------------------
-proto_library(
-    name = "jwt_hmac_proto",
-    srcs = [
-        "jwt_hmac.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# aes_ctr
-# -----------------------------------------------
-proto_library(
-    name = "aes_ctr_proto",
-    srcs = [
-        "aes_ctr.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# -----------------------------------------------
-# aes_ctr_hmac_aead
-# -----------------------------------------------
-proto_library(
-    name = "aes_ctr_hmac_aead_proto",
-    srcs = [
-        "aes_ctr_hmac_aead.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":aes_ctr_proto",
-        ":hmac_proto",
-    ],
-)
-
-# -----------------------------------------------
-# aes_gcm
-# -----------------------------------------------
-proto_library(
-    name = "aes_gcm_proto",
-    srcs = [
-        "aes_gcm.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# aes_gcm_siv
-# -----------------------------------------------
-proto_library(
-    name = "aes_gcm_siv_proto",
-    srcs = [
-        "aes_gcm_siv.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# aes_ctr_hmac_streaming
-# -----------------------------------------------
-proto_library(
-    name = "aes_ctr_hmac_streaming_proto",
-    srcs = ["aes_ctr_hmac_streaming.proto"],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":common_proto",
-        ":hmac_proto",
-    ],
-)
-
-# -----------------------------------------------
-# aes_gcm_hkdf_streaming
-# -----------------------------------------------
-proto_library(
-    name = "aes_gcm_hkdf_streaming_proto",
-    srcs = ["aes_gcm_hkdf_streaming.proto"],
-    visibility = ["//visibility:public"],
-    deps = [":common_proto"],
-)
-
-# -----------------------------------------------
-# aes_eax
-# -----------------------------------------------
-proto_library(
-    name = "aes_eax_proto",
-    srcs = [
-        "aes_eax.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# chacha20_poly1305
-# -----------------------------------------------
-proto_library(
-    name = "chacha20_poly1305_proto",
-    srcs = [
-        "chacha20_poly1305.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# kms_aead
-# -----------------------------------------------
-proto_library(
-    name = "kms_aead_proto",
-    srcs = [
-        "kms_aead.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# kms_envelope
-# -----------------------------------------------
-proto_library(
-    name = "kms_envelope_proto",
-    srcs = [
-        "kms_envelope.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [":tink_proto"],
-)
-
-# -----------------------------------------------
-# ecies_aead_hkdf
-# -----------------------------------------------
-proto_library(
-    name = "ecies_aead_hkdf_proto",
-    srcs = [
-        "ecies_aead_hkdf.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":common_proto",
-        ":tink_proto",
-    ],
-)
-
-# -----------------------------------------------
-# XChacha20 with Poly1305
-# -----------------------------------------------
-proto_library(
-    name = "xchacha20_poly1305_proto",
-    srcs = [
-        "xchacha20_poly1305.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# Hkdf prf
-# -----------------------------------------------
-proto_library(
-    name = "hkdf_prf_proto",
-    srcs = [
-        "hkdf_prf.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [":common_proto"],
-)
-
-# -----------------------------------------------
-# aes_cmac_prf
-# -----------------------------------------------
-proto_library(
-    name = "aes_cmac_prf_proto",
-    srcs = [
-        "aes_cmac_prf.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# -----------------------------------------------
-# hmac_prf
-# -----------------------------------------------
-proto_library(
-    name = "hmac_prf_proto",
-    srcs = [
-        "hmac_prf.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [":common_proto"],
-)
-
-# -----------------------------------------------
-# hpke
-# -----------------------------------------------
-proto_library(
-    name = "hpke_proto",
-    srcs = [
-        "hpke.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# ----------------------------------------------------------------------------
-# prf_based_deriver
-# ----------------------------------------------------------------------------
-proto_library(
-    name = "prf_based_deriver_proto",
-    srcs = ["prf_based_deriver.proto"],
-    visibility = ["//visibility:public"],
-    deps = [":tink_proto"],
-)
-
-# -----------------------------------------------
-# empty
-# -----------------------------------------------
-proto_library(
-    name = "empty_proto",
-    srcs = [
-        "empty.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
-
-# ----------------------------------------------------------------------------
-# test_proto
-# ----------------------------------------------------------------------------
-proto_library(
-    name = "test_proto_proto",
-    testonly = True,
-    srcs = [
-        "test_proto.proto",
-    ],
-    visibility = ["//visibility:public"],
-)
diff --git a/javascript/proto/CMakeLists.txt b/javascript/proto/CMakeLists.txt
deleted file mode 100644
index cb29001..0000000
--- a/javascript/proto/CMakeLists.txt
+++ /dev/null
@@ -1,205 +0,0 @@
-tink_module(proto)
-
-tink_cc_proto(
-  NAME common_cc_proto
-  SRCS common.proto
-)
-
-tink_cc_proto(
-  NAME tink_cc_proto
-  SRCS tink.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME config_cc_proto
-  SRCS config.proto
-)
-
-tink_cc_proto(
-  NAME aes_siv_cc_proto
-  SRCS aes_siv.proto
-)
-
-tink_cc_proto(
-  NAME rsa_ssa_pkcs1_cc_proto
-  SRCS rsa_ssa_pkcs1.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME rsa_ssa_pss_cc_proto
-  SRCS rsa_ssa_pss.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME cached_dek_aead_cc_proto
-  SRCS cached_dek_aead.proto
-)
-
-tink_cc_proto(
-  NAME cached_dek_envelope_cc_proto
-  SRCS cached_dek_envelope.proto
-  DEPS tink::proto::tink_cc_proto
-)
-
-tink_cc_proto(
-  NAME ecdsa_cc_proto
-  SRCS ecdsa.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME jwt_rsa_ssa_pkcs1_cc_proto
-  SRCS jwt_rsa_ssa_pkcs1.proto
-)
-
-tink_cc_proto(
-  NAME jwt_rsa_ssa_pss_cc_proto
-  SRCS jwt_rsa_ssa_pss.proto
-)
-
-tink_cc_proto(
-  NAME jwt_ecdsa_cc_proto
-  SRCS jwt_ecdsa.proto
-)
-
-tink_cc_proto(
-  NAME ed25519_cc_proto
-  SRCS ed25519.proto
-)
-
-tink_cc_proto(
-  NAME aes_cmac_cc_proto
-  SRCS aes_cmac.proto
-)
-
-tink_cc_proto(
-  NAME hmac_cc_proto
-  SRCS hmac.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME aes_ctr_cc_proto
-  SRCS aes_ctr.proto
-)
-
-tink_cc_proto(
-  NAME aes_ctr_hmac_aead_cc_proto
-  SRCS aes_ctr_hmac_aead.proto
-  DEPS
-    tink::proto::aes_ctr_cc_proto
-    tink::proto::hmac_cc_proto
-)
-
-tink_cc_proto(
-  NAME aes_gcm_cc_proto
-  SRCS aes_gcm.proto
-)
-
-tink_cc_proto(
-  NAME aes_gcm_siv_cc_proto
-  SRCS aes_gcm_siv.proto
-)
-
-tink_cc_proto(
-  NAME aes_ctr_hmac_streaming_cc_proto
-  SRCS aes_ctr_hmac_streaming.proto
-  DEPS
-    tink::proto::common_cc_proto
-    tink::proto::hmac_cc_proto
-)
-
-tink_cc_proto(
-  NAME aes_gcm_hkdf_streaming_cc_proto
-  SRCS aes_gcm_hkdf_streaming.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME aes_eax_cc_proto
-  SRCS aes_eax.proto
-)
-
-tink_cc_proto(
-  NAME chacha20_poly1305_cc_proto
-  SRCS chacha20_poly1305.proto
-)
-
-tink_cc_proto(
-  NAME kms_aead_cc_proto
-  SRCS kms_aead.proto
-)
-
-tink_cc_proto(
-  NAME kms_envelope_cc_proto
-  SRCS kms_envelope.proto
-  DEPS tink::proto::tink_cc_proto
-)
-
-tink_cc_proto(
-  NAME ecies_aead_hkdf_cc_proto
-  SRCS ecies_aead_hkdf.proto
-  DEPS
-    tink::proto::common_cc_proto
-    tink::proto::tink_cc_proto
-)
-
-tink_cc_proto(
-  NAME xchacha20_poly1305_cc_proto
-  SRCS xchacha20_poly1305.proto
-)
-
-tink_cc_proto(
-  NAME hkdf_prf_cc_proto
-  SRCS hkdf_prf.proto
-  DEPS
-    tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME aes_cmac_prf_cc_proto
-  SRCS aes_cmac_prf.proto
-)
-
-tink_cc_proto(
-  NAME hmac_prf_cc_proto
-  SRCS hmac_prf.proto
-  DEPS tink::proto::common_cc_proto
-)
-
-tink_cc_proto(
-  NAME hpke_cc_proto
-  SRCS hpke.proto
-)
-
-tink_cc_proto(
-  NAME prf_based_deriver_cc_proto
-  SRCS prf_based_deriver.proto
-  DEPS
-    tink::proto::tink_cc_proto
-)
-
-tink_cc_proto(
-  NAME jwt_hmac_cc_proto
-  SRCS jwt_hmac.proto
-)
-
-tink_cc_proto(
-  NAME empty_cc_proto
-  SRCS empty.proto
-)
-
-tink_cc_proto(
-  NAME test_proto_cc_proto
-  SRCS test_proto.proto
-)
-
-tink_target_group(
-  NAME public_protos_cc_proto
-  DEPS
-    tink::proto::config_cc_proto
-    tink::proto::tink_cc_proto
-)
diff --git a/javascript/proto/aes_cmac.proto b/javascript/proto/aes_cmac.proto
deleted file mode 100644
index b214834..0000000
--- a/javascript/proto/aes_cmac.proto
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_go_proto";
-
-message AesCmacParams {
-  uint32 tag_size = 1;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesCmacKey
-message AesCmacKey {
-  uint32 version = 1;
-  bytes key_value = 2;
-  AesCmacParams params = 3;
-}
-
-message AesCmacKeyFormat {
-  uint32 key_size = 1;
-  AesCmacParams params = 2;
-}
diff --git a/javascript/proto/aes_cmac_prf.proto b/javascript/proto/aes_cmac_prf.proto
deleted file mode 100644
index 58e5f67..0000000
--- a/javascript/proto/aes_cmac_prf.proto
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_prf_go_proto";
-
-// key_type: type.googleapis.com/google.crypto.tink.AesCmacPrfKey
-message AesCmacPrfKey {
-  uint32 version = 1;
-  bytes key_value = 2;
-}
-
-message AesCmacPrfKeyFormat {
-  uint32 version = 2;
-  uint32 key_size = 1;
-}
diff --git a/javascript/proto/aes_ctr.proto b/javascript/proto/aes_ctr.proto
deleted file mode 100644
index ecdb256..0000000
--- a/javascript/proto/aes_ctr.proto
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_go_proto";
-
-message AesCtrParams {
-  uint32 iv_size = 1;
-}
-
-message AesCtrKeyFormat {
-  AesCtrParams params = 1;
-  uint32 key_size = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesCtrKey
-message AesCtrKey {
-  uint32 version = 1;
-  AesCtrParams params = 2;
-  bytes key_value = 3;
-}
diff --git a/javascript/proto/aes_ctr_hmac_aead.proto b/javascript/proto/aes_ctr_hmac_aead.proto
deleted file mode 100644
index dcf541d..0000000
--- a/javascript/proto/aes_ctr_hmac_aead.proto
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/aes_ctr.proto";
-import "proto/hmac.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_aead_go_proto";
-
-message AesCtrHmacAeadKeyFormat {
-  AesCtrKeyFormat aes_ctr_key_format = 1;
-  HmacKeyFormat hmac_key_format = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey
-message AesCtrHmacAeadKey {
-  uint32 version = 1;
-  AesCtrKey aes_ctr_key = 2;
-  HmacKey hmac_key = 3;
-}
diff --git a/javascript/proto/aes_ctr_hmac_streaming.proto b/javascript/proto/aes_ctr_hmac_streaming.proto
deleted file mode 100644
index 064b630..0000000
--- a/javascript/proto/aes_ctr_hmac_streaming.proto
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-import "proto/hmac.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_streaming_go_proto";
-
-message AesCtrHmacStreamingParams {
-  uint32 ciphertext_segment_size = 1;
-  uint32 derived_key_size = 2;  // size of AES-CTR keys derived for each segment
-  HashType hkdf_hash_type = 3;  // hash function for key derivation via HKDF
-  HmacParams hmac_params = 4;   // params for authentication tags
-}
-
-message AesCtrHmacStreamingKeyFormat {
-  uint32 version = 3;
-  AesCtrHmacStreamingParams params = 1;
-  uint32 key_size = 2;  // size of the main key (aka. "ikm", input key material)
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey
-message AesCtrHmacStreamingKey {
-  uint32 version = 1;
-  AesCtrHmacStreamingParams params = 2;
-  bytes key_value = 3;  // the main key, aka. "ikm", input key material
-}
diff --git a/javascript/proto/aes_eax.proto b/javascript/proto/aes_eax.proto
deleted file mode 100644
index c673306..0000000
--- a/javascript/proto/aes_eax.proto
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_eax_go_proto";
-
-// only allowing tag size in bytes = 16
-message AesEaxParams {
-  // possible value is 12 or 16 bytes.
-  uint32 iv_size = 1;
-}
-
-message AesEaxKeyFormat {
-  AesEaxParams params = 1;
-  uint32 key_size = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesEaxKey
-message AesEaxKey {
-  uint32 version = 1;
-  AesEaxParams params = 2;
-  bytes key_value = 3;
-}
diff --git a/javascript/proto/aes_gcm.proto b/javascript/proto/aes_gcm.proto
deleted file mode 100644
index fba7a89..0000000
--- a/javascript/proto/aes_gcm.proto
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_go_proto";
-option objc_class_prefix = "TINKPB";
-
-message AesGcmKeyFormat {
-  uint32 key_size = 2;
-  uint32 version = 3;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesGcmKey
-//
-// A AesGcmKey is an AEAD key. Mathematically, it represents the functions
-// Encrypt and Decrypt which we define in the following.
-//
-// First, Tink computes a "output prefix" OP by considering the
-// "OutputPrefixType" message in Keyset.Key and the ID of the key using the
-// Tink function "AEAD-OutputPrefix": (AesGcmKeys must always be stored in a
-// keyset).
-//
-// AEAD-OutputPrefix(output_prefix_type, id):
-//     if output_prefix_type == RAW:
-//       return "";
-//     if output_prefix_type == TINK:
-//       return 0x01 + BigEndian(id)
-//     if output_prefix_type == CRUNCHY:
-//       return 0x00 + BigEndian(id)
-//
-// Then, the function defined by this is defined as:
-// [GCM], Section 5.2.1:
-//  * "Encrypt" maps a plaintext P and associated data A to a ciphertext given
-//    by the concatenation OP || IV || C || T. In addition to [GCM], Tink
-//    has the following restriction: IV is a uniformly random initialization
-//    vector of length 12 bytes and T is restricted to 16 bytes.
-//
-//  * If OP matches the result of AEAD-OutputPrefix, then "Decrypt" maps the
-//    input OP || IV || C || T and A to the the output P in the manner as
-//    described in [GCM], Section 5.2.2. If OP does not match, then "Decrypt"
-//    returns an error.
-// [GCM]: NIST Special Publication 800-38D: Recommendation for Block Cipher
-// Modes of Operation: Galois/Counter Mode (GCM) and GMAC.
-// http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf.
-
-message AesGcmKey {
-  uint32 version = 1;
-  bytes key_value = 3;
-}
diff --git a/javascript/proto/aes_gcm_hkdf_streaming.proto b/javascript/proto/aes_gcm_hkdf_streaming.proto
deleted file mode 100644
index 61fb479..0000000
--- a/javascript/proto/aes_gcm_hkdf_streaming.proto
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for streaming encryption using AES-GCM
-// with HKDF as key derivation function.
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_hkdf_streaming_go_proto";
-
-message AesGcmHkdfStreamingParams {
-  uint32 ciphertext_segment_size = 1;
-  uint32 derived_key_size = 2;  // size of AES-GCM keys derived for each segment
-  HashType hkdf_hash_type = 3;
-}
-
-message AesGcmHkdfStreamingKeyFormat {
-  uint32 version = 3;
-  AesGcmHkdfStreamingParams params = 1;
-  uint32 key_size = 2;  // size of the main key (aka. "ikm", input key material)
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey
-message AesGcmHkdfStreamingKey {
-  uint32 version = 1;
-  AesGcmHkdfStreamingParams params = 2;
-  bytes key_value = 3;
-}
diff --git a/javascript/proto/aes_gcm_siv.proto b/javascript/proto/aes_gcm_siv.proto
deleted file mode 100644
index df9fada..0000000
--- a/javascript/proto/aes_gcm_siv.proto
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2019 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_siv_go_proto";
-
-// The only allowed IV size is 12 bytes and tag size is 16 bytes.
-// Thus, accept no params.
-message AesGcmSivKeyFormat {
-  uint32 key_size = 2;
-  uint32 version = 1;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesGcmSivKey
-message AesGcmSivKey {
-  uint32 version = 1;
-  bytes key_value = 3;
-}
diff --git a/javascript/proto/aes_siv.proto b/javascript/proto/aes_siv.proto
deleted file mode 100644
index 0023027..0000000
--- a/javascript/proto/aes_siv.proto
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_siv_go_proto";
-
-message AesSivKeyFormat {
-  // Only valid value is: 64.
-  uint32 key_size = 1;
-  uint32 version = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.AesSivKey
-message AesSivKey {
-  uint32 version = 1;
-  // First half is AES-CTR key, second is AES-SIV.
-  bytes key_value = 2;
-}
diff --git a/javascript/proto/cached_dek_aead.proto b/javascript/proto/cached_dek_aead.proto
deleted file mode 100644
index 10bcde5..0000000
--- a/javascript/proto/cached_dek_aead.proto
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/cached_dek_aead_go_proto";
-
-message CachedDekAeadKeyFormat {
-  // Required.
-  // The location of a KMS key.
-  // With Google Cloud KMS, valid values have this format:
-  // gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.
-  // With AWS KMS, valid values have this format:
-  // aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>
-  string key_uri = 1;
-}
-
-// There is no actual key material in the key.
-message CachedDekAeadKey {
-  uint32 version = 1;
-  // The key format also contains the params.
-  CachedDekAeadKeyFormat params = 2;
-}
diff --git a/javascript/proto/cached_dek_envelope.proto b/javascript/proto/cached_dek_envelope.proto
deleted file mode 100644
index 1b096ad..0000000
--- a/javascript/proto/cached_dek_envelope.proto
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/tink.proto";
-
-option java_multiple_files = true;
-option java_package = "com.google.crypto.tink.proto";
-option go_package = "github.com/google/tink/proto/cached_dek_envelope_go_proto";
-
-message CachedDekEnvelopeAeadKeyFormat {
-  // Required.
-  // The location of the KEK in a remote KMS.
-  // With Google Cloud KMS, valid values have this format:
-  // gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.
-  // With AWS KMS, valid values have this format:
-  // aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>
-  string kek_uri = 1;
-  // Key template of the Data Encryption Key, e.g., AesCtrHmacAeadKeyFormat.
-  // Required.
-  KeyTemplate dek_template = 2;
-}
-message CachedDekEnvelopeAeadKey {
-  uint32 version = 1;
-  // The key format also contains the params.
-  CachedDekEnvelopeAeadKeyFormat params = 2;
-}
diff --git a/javascript/proto/chacha20_poly1305.proto b/javascript/proto/chacha20_poly1305.proto
deleted file mode 100644
index 2cd6ead..0000000
--- a/javascript/proto/chacha20_poly1305.proto
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/chacha20_poly1305_go_proto";
-
-message ChaCha20Poly1305KeyFormat {}
-
-// key_type: type.googleapis.com/google.crypto.tink.ChaCha20Poly1305.
-// This key type actually implements ChaCha20Poly1305 as described
-// at https://tools.ietf.org/html/rfc7539#section-2.8.
-message ChaCha20Poly1305Key {
-  uint32 version = 1;
-  bytes key_value = 2;
-}
diff --git a/javascript/proto/common.proto b/javascript/proto/common.proto
deleted file mode 100644
index eaff8d3..0000000
--- a/javascript/proto/common.proto
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for common cryptographic enum types.
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/common_go_proto";
-
-enum EllipticCurveType {
-  UNKNOWN_CURVE = 0;
-  NIST_P256 = 2;
-  NIST_P384 = 3;
-  NIST_P521 = 4;
-  CURVE25519 = 5;
-}
-
-enum EcPointFormat {
-  UNKNOWN_FORMAT = 0;
-  UNCOMPRESSED = 1;
-  COMPRESSED = 2;
-  // Like UNCOMPRESSED but without the \x04 prefix. Crunchy uses this format.
-  // DO NOT USE unless you are a Crunchy user moving to Tink.
-  DO_NOT_USE_CRUNCHY_UNCOMPRESSED = 3;
-}
-
-enum HashType {
-  UNKNOWN_HASH = 0;
-  SHA1 = 1;  // Using SHA1 for digital signature is deprecated but HMAC-SHA1 is
-             // fine.
-  SHA384 = 2;
-  SHA256 = 3;
-  SHA512 = 4;
-  SHA224 = 5;
-}
diff --git a/javascript/proto/config.proto b/javascript/proto/config.proto
deleted file mode 100644
index ebbd742..0000000
--- a/javascript/proto/config.proto
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for configuring Tink runtime environment.
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/config_go_proto";
-
-// An entry that describes a key type to be used with Tink library,
-// specifying the corresponding primitive, key manager, and deprecation status.
-// All fields are required.
-message KeyTypeEntry {
-  // KeyTypeEntry is no longer supported.
-  option deprecated = true;
-
-  string primitive_name = 1;  // E.g. “Aead”, “Mac”, ... (case-insensitive)
-  string type_url = 2;        // Name of the key type.
-  uint32 key_manager_version = 3;  // Minimum required version of key manager.
-  bool new_key_allowed = 4;        // Can the key manager create new keys?
-  string catalogue_name = 5;       // Catalogue to be queried for key manager,
-                              // e.g. "Tink", "Custom", ... (case-insensitive)
-}
-
-// A complete configuration of Tink library: a list of key types
-// to be available via the Registry after initialization.
-// All fields are required.
-message RegistryConfig {
-  // RegistryConfig is no longer supported.
-  option deprecated = true;
-
-  string config_name = 1;
-  repeated KeyTypeEntry entry = 2;
-}
diff --git a/javascript/proto/ecdsa.proto b/javascript/proto/ecdsa.proto
deleted file mode 100644
index 6ba3970..0000000
--- a/javascript/proto/ecdsa.proto
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for Elliptic Curve Digital Signature Algorithm (ECDSA).
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecdsa_go_proto";
-
-enum EcdsaSignatureEncoding {
-  UNKNOWN_ENCODING = 0;
-  // The signature's format is r || s, where r and s are zero-padded and have
-  // the same size in bytes as the order of the curve. For example, for NIST
-  // P-256 curve, r and s are zero-padded to 32 bytes.
-  IEEE_P1363 = 1;
-  // The signature is encoded using ASN.1
-  // (https://tools.ietf.org/html/rfc5480#appendix-A):
-  // ECDSA-Sig-Value :: = SEQUENCE {
-  //  r INTEGER,
-  //  s INTEGER
-  // }
-  DER = 2;
-}
-
-// Protos for Ecdsa.
-message EcdsaParams {
-  // Required.
-  HashType hash_type = 1;
-  // Required.
-  EllipticCurveType curve = 2;
-  // Required.
-  EcdsaSignatureEncoding encoding = 3;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.EcdsaPublicKey
-message EcdsaPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  EcdsaParams params = 2;
-  // Affine coordinates of the public key in bigendian representation. The
-  // public key is a point (x, y) on the curve defined by params.curve. For
-  // ECDH, it is crucial to verify whether the public key point (x, y) is on the
-  // private's key curve. For ECDSA, such verification is a defense in depth.
-  // Required.
-  bytes x = 3;
-  // Required.
-  bytes y = 4;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.EcdsaPrivateKey
-message EcdsaPrivateKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  EcdsaPublicKey public_key = 2;
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes key_value = 3;
-}
-
-message EcdsaKeyFormat {
-  // Required.
-  EcdsaParams params = 2;
-}
diff --git a/javascript/proto/ecies_aead_hkdf.proto b/javascript/proto/ecies_aead_hkdf.proto
deleted file mode 100644
index 9470991..0000000
--- a/javascript/proto/ecies_aead_hkdf.proto
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for Elliptic Curve Digital Signature Algorithm (ECDSA).
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-import "proto/tink.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecies_aead_hkdf_go_proto";
-
-// Protos for keys for ECIES with HKDF and AEAD encryption.
-//
-// These definitions follow loosely ECIES ISO 18033-2 standard
-// (Elliptic Curve Integrated Encryption Scheme, see
-// http://www.shoup.net/iso/std6.pdf), with but with some differences:
-//  * use of HKDF key derivation function (instead of KDF1 and KDF2) enabling
-//  the use
-//    of optional parameters to the key derivation function, which strenghten
-//    the overall security and allow for binding the key material to
-//    application-specific information (cf. RFC 5869,
-//    https://tools.ietf.org/html/rfc5869)
-//  * use of modern AEAD schemes rather than "manual composition" of symmetric
-//  encryption
-//    with message authentication codes (as in DEM1, DEM2, and DEM3 schemes of
-//    ISO 18033-2)
-//
-// ECIES-keys represent HybridEncryption resp. HybridDecryption primitives.
-
-// Parameters of KEM (Key Encapsulation Mechanism)
-message EciesHkdfKemParams {
-  // Required.
-  EllipticCurveType curve_type = 1;
-
-  // Required.
-  HashType hkdf_hash_type = 2;
-
-  // Optional.
-  bytes hkdf_salt = 11;
-}
-
-// Parameters of AEAD DEM (Data Encapsulation Mechanism).
-message EciesAeadDemParams {
-  // Required.
-  // Contains an Aead or DeterministicAead key format (e.g:
-  // AesCtrHmacAeadKeyFormat, AesGcmKeyFormat or AesSivKeyFormat).
-  KeyTemplate aead_dem = 2;
-}
-
-message EciesAeadHkdfParams {
-  // Key Encapsulation Mechanism.
-  // Required.
-  EciesHkdfKemParams kem_params = 1;
-
-  // Data Encapsulation Mechanism.
-  // Required.
-  EciesAeadDemParams dem_params = 2;
-
-  // EC point format.
-  // Required.
-  EcPointFormat ec_point_format = 3;
-}
-
-// EciesAeadHkdfPublicKey represents HybridEncryption primitive.
-// key_type: type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey
-message EciesAeadHkdfPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  EciesAeadHkdfParams params = 2;
-
-  // Affine coordinates of the public key in bigendian representation.
-  // The public key is a point (x, y) on the curve defined by
-  // params.kem_params.curve. Required.
-  bytes x = 3;
-  // Required.
-  bytes y = 4;
-}
-
-// EciesKdfAeadPrivateKey represents HybridDecryption primitive.
-// key_type: type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey
-message EciesAeadHkdfPrivateKey {
-  // Required.
-  uint32 version = 1;
-
-  // Required.
-  EciesAeadHkdfPublicKey public_key = 2;
-
-  // Required.
-  bytes key_value = 3;  // Big integer in bigendian representation.
-}
-
-message EciesAeadHkdfKeyFormat {
-  // Required.
-  EciesAeadHkdfParams params = 1;
-}
diff --git a/javascript/proto/ed25519.proto b/javascript/proto/ed25519.proto
deleted file mode 100644
index 669f33a..0000000
--- a/javascript/proto/ed25519.proto
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for Ed25519 Digital Signature Algorithm.
-// See https://ed25519.cr.yp.to/ed25519-20110926.pdf and
-// https://tools.ietf.org/html/rfc8032.
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ed25519_go_proto";
-
-message Ed25519KeyFormat {
-    uint32 version = 1;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.Ed25519PublicKey
-message Ed25519PublicKey {
-  // Required.
-  uint32 version = 1;
-  // The public key is 32 bytes, encoded according to
-  // https://tools.ietf.org/html/rfc8032#section-5.1.2.
-  // Required.
-  bytes key_value = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.Ed25519PrivateKey
-message Ed25519PrivateKey {
-  // Required.
-  uint32 version = 1;
-  // The private key is 32 bytes of cryptographically secure random data.
-  // See https://tools.ietf.org/html/rfc8032#section-5.1.5.
-  // Required.
-  bytes key_value = 2;
-  // The corresponding public key.
-  Ed25519PublicKey public_key = 3;
-}
diff --git a/javascript/proto/empty.proto b/javascript/proto/empty.proto
deleted file mode 100644
index 33831a9..0000000
--- a/javascript/proto/empty.proto
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/empty_go_proto";
-
-message Empty {}
diff --git a/javascript/proto/experimental/pqcrypto/BUILD.bazel b/javascript/proto/experimental/pqcrypto/BUILD.bazel
deleted file mode 100644
index b5023df..0000000
--- a/javascript/proto/experimental/pqcrypto/BUILD.bazel
+++ /dev/null
@@ -1,15 +0,0 @@
-# package containing useful functions for hybrid encryption PQC primitives
-
-licenses(["notice"])
-
-proto_library(
-    name = "cecpq2_aead_hkdf_proto",
-    srcs = [
-        "cecpq2_aead_hkdf.proto",
-    ],
-    visibility = ["//visibility:public"],
-    deps = [
-        "//proto:common_proto",
-        "//proto:tink_proto",
-    ],
-)
diff --git a/javascript/proto/experimental/pqcrypto/cecpq2_aead_hkdf.proto b/javascript/proto/experimental/pqcrypto/cecpq2_aead_hkdf.proto
deleted file mode 100644
index 3dcbed8..0000000
--- a/javascript/proto/experimental/pqcrypto/cecpq2_aead_hkdf.proto
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-import "proto/tink.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/cecpq2_aead_hkdf_go_proto";
-
-// Protos for keys for CECPQ2 with HKDF and AEAD encryption.
-//
-// These definitions follow loosely ECIES ISO 18033-2 standard
-// (Elliptic Curve Integrated Encryption Scheme, see
-// http://www.shoup.net/iso/std6.pdf), with but with some differences:
-//  * use of CECPQ2 as the KEM instead of ECC-only KEM
-//  * use of HKDF key derivation function (instead of KDF1 and KDF2) enabling
-//  the use
-//    of optional parameters to the key derivation function, which strenghten
-//    the overall security and allow for binding the key material to
-//    application-specific information (cf. RFC 5869,
-//    https://tools.ietf.org/html/rfc5869)
-//  * use of modern AEAD schemes rather than "manual composition" of symmetric
-//  encryption
-//    with message authentication codes (as in DEM1, DEM2, and DEM3 schemes of
-//    ISO 18033-2)
-//
-// CECPQ2-keys represent HybridEncryption resp. HybridDecryption primitives.
-
-// Parameters of KEM (Key Encapsulation Mechanism)
-message Cecpq2HkdfKemParams {
-  // Required.
-  EllipticCurveType curve_type = 1;
-
-  // Required.
-  EcPointFormat ec_point_format = 2;
-
-  // Required.
-  HashType hkdf_hash_type = 3;
-
-  // Optional.
-  bytes hkdf_salt = 11;
-}
-
-// Parameters of AEAD DEM (Data Encapsulation Mechanism).
-message Cecpq2AeadDemParams {
-  // Contains e.g. AesCtrHmacAeadKeyFormat or AesGcmKeyFormat.
-  // Required.
-  KeyTemplate aead_dem = 2;
-}
-
-message Cecpq2AeadHkdfParams {
-  // Key Encapsulation Mechanism.
-  // Required.
-  Cecpq2HkdfKemParams kem_params = 1;
-
-  // Data Encapsulation Mechanism.
-  // Required.
-  Cecpq2AeadDemParams dem_params = 2;
-}
-
-// Cecpq2AeadHkdfPublicKey represents HybridEncryption primitive.
-// key_type: type.googleapis.com/google.crypto.tink.Cecpq2AeadHkdfPublicKey
-message Cecpq2AeadHkdfPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  Cecpq2AeadHkdfParams params = 2;
-
-  // X25519 public key: Affine coordinates of the public key in bigendian
-  // representation. The public key is a point (x, y) on the Curve25519.
-  // Required.
-  bytes x25519_public_key_x = 3;
-  // Required.
-  bytes x25519_public_key_y = 4;
-
-  // HRSS public key:
-  // Required.
-  bytes hrss_public_key_marshalled = 5;
-}
-
-// Cecpq2KdfAeadPrivateKey represents HybridDecryption primitive.
-// key_type: type.googleapis.com/google.crypto.tink.Cecpq2AeadHkdfPrivateKey
-message Cecpq2AeadHkdfPrivateKey {
-  // Required.
-  uint32 version = 1;
-
-  // Required.
-  Cecpq2AeadHkdfPublicKey public_key = 2;
-
-  // X25519 private key:
-  // Required.
-  bytes x25519_private_key = 3;  // Big integer in bigendian representation.
-
-  // HRSS private key seed:
-  // Required.
-  bytes hrss_private_key_seed = 4;
-}
-
-message Cecpq2AeadHkdfKeyFormat {
-  // Required.
-  Cecpq2AeadHkdfParams params = 1;
-}
diff --git a/javascript/proto/experimental/pqcrypto/dilithium.proto b/javascript/proto/experimental/pqcrypto/dilithium.proto
deleted file mode 100644
index fa4c05b..0000000
--- a/javascript/proto/experimental/pqcrypto/dilithium.proto
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-syntax = "proto3";
-
-package google.crypto.tink;
-
-enum DilithiumSeedExpansion {
-  SEED_EXPANSION_UNKNOWN = 0;
-  SEED_EXPANSION_SHAKE = 1;
-  SEED_EXPANSION_AES = 2;
-}
-
-// Protos for Dilithium Digital Signature Algorithm.
-// See https://pq-crystals.org/dilithium/data/dilithium-specification-round3.pdf
-message DilithiumParams {
-  // Required
-  int32 key_size = 1;
-  // Required.
-  DilithiumSeedExpansion seed_expansion = 2;
-}
-
-message DilithiumKeyFormat {
-  uint32 version = 1;
-  // Required.
-  DilithiumParams params = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.DilithiumPublicKey
-message DilithiumPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  bytes key_value = 2;
-  // Required
-  DilithiumParams params = 3;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.DilithiumPrivateKey
-message DilithiumPrivateKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  bytes key_value = 2;
-  // The corresponding public key.
-  DilithiumPublicKey public_key = 3;
-}
diff --git a/javascript/proto/experimental/pqcrypto/falcon.proto b/javascript/proto/experimental/pqcrypto/falcon.proto
deleted file mode 100644
index 13103bc..0000000
--- a/javascript/proto/experimental/pqcrypto/falcon.proto
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-syntax = "proto3";
-
-package google.crypto.tink;
-
-// Protos for FalconDigital Signature Algorithm.
-// See https://falcon-sign.info/ for more details.
-message FalconKeyFormat {
-  uint32 version = 1;
-  // Required.
-  int32 key_size = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.FalconPublicKey
-message FalconPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  bytes key_value = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.FalconPrivateKey
-message FalconPrivateKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  bytes key_value = 2;
-  // The corresponding public key.
-  FalconPublicKey public_key = 3;
-}
diff --git a/javascript/proto/experimental/pqcrypto/sphincs.proto b/javascript/proto/experimental/pqcrypto/sphincs.proto
deleted file mode 100644
index 381451c..0000000
--- a/javascript/proto/experimental/pqcrypto/sphincs.proto
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-syntax = "proto3";
-
-package google.crypto.tink;
-
-enum SphincsHashType {
-  HASH_TYPE_UNSPECIFIED = 0;
-  HARAKA = 1;
-  SHA256 = 2;
-  SHAKE256 = 3;
-}
-
-enum SphincsVariant {
-  VARIANT_UNSPECIFIED = 0;
-  ROBUST = 1;
-  SIMPLE = 2;
-}
-
-enum SphincsSignatureType {
-  SIG_TYPE_UNSPECIFIED = 0;
-  FAST_SIGNING = 1;
-  SMALL_SIGNATURE = 2;
-}
-
-// Protos for Sphincs Digital Signature Algorithm.
-message SphincsParams {
-  // Required
-  int32 key_size = 1;
-  // Required.
-  SphincsHashType hash_type = 2;
-  // Required.
-  SphincsVariant variant = 3;
-  // Required.
-  SphincsSignatureType sig_length_type = 4;
-}
-
-message SphincsKeyFormat {
-  uint32 version = 1;
-  // Required.
-  SphincsParams params = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.SphincsPublicKey
-message SphincsPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  bytes key_value = 2;
-  // Required
-  SphincsParams params = 3;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.SphincsPrivateKey
-message SphincsPrivateKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  bytes key_value = 2;
-  // The corresponding public key.
-  SphincsPublicKey public_key = 3;
-}
diff --git a/javascript/proto/hkdf_prf.proto b/javascript/proto/hkdf_prf.proto
deleted file mode 100644
index 3d3cbe9..0000000
--- a/javascript/proto/hkdf_prf.proto
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hkdf_prf_proto";
-
-message HkdfPrfParams {
-  HashType hash = 1;
-  // Salt, optional in RFC 5869. Using "" is equivalent to zeros of length up to
-  // the block length of the HMac.
-  bytes salt = 2;
-}
-
-message HkdfPrfKey {
-  uint32 version = 1;
-  HkdfPrfParams params = 2;
-  bytes key_value = 3;
-}
-
-message HkdfPrfKeyFormat {
-  HkdfPrfParams params = 1;
-  uint32 key_size = 2;
-  uint32 version = 3;
-}
diff --git a/javascript/proto/hmac.proto b/javascript/proto/hmac.proto
deleted file mode 100644
index 2733e51..0000000
--- a/javascript/proto/hmac.proto
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_go_proto";
-
-message HmacParams {
-  HashType hash = 1;  // HashType is an enum.
-  uint32 tag_size = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.HmacKey
-message HmacKey {
-  uint32 version = 1;
-  HmacParams params = 2;
-  bytes key_value = 3;
-}
-
-message HmacKeyFormat {
-  HmacParams params = 1;
-  uint32 key_size = 2;
-  uint32 version = 3;
-}
diff --git a/javascript/proto/hmac_prf.proto b/javascript/proto/hmac_prf.proto
deleted file mode 100644
index 7b3c52d..0000000
--- a/javascript/proto/hmac_prf.proto
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_prf_go_proto";
-
-message HmacPrfParams {
-  HashType hash = 1;  // HashType is an enum.
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.HmacPrfKey
-message HmacPrfKey {
-  uint32 version = 1;
-  HmacPrfParams params = 2;
-  bytes key_value = 3;
-}
-
-message HmacPrfKeyFormat {
-  HmacPrfParams params = 1;
-  uint32 key_size = 2;
-  uint32 version = 3;
-}
diff --git a/javascript/proto/hpke.proto b/javascript/proto/hpke.proto
deleted file mode 100644
index 847864a..0000000
--- a/javascript/proto/hpke.proto
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hpke_proto";
-
-enum HpkeKem {
-  KEM_UNKNOWN = 0;
-  DHKEM_X25519_HKDF_SHA256 = 1;
-  DHKEM_P256_HKDF_SHA256 = 2;
-  DHKEM_P384_HKDF_SHA384 = 3;
-  DHKEM_P521_HKDF_SHA512 = 4;
-}
-
-enum HpkeKdf {
-  KDF_UNKNOWN = 0;
-  HKDF_SHA256 = 1;
-  HKDF_SHA384 = 2;
-  HKDF_SHA512 = 3;
-}
-
-enum HpkeAead {
-  AEAD_UNKNOWN = 0;
-  AES_128_GCM = 1;
-  AES_256_GCM = 2;
-  CHACHA20_POLY1305 = 3;
-}
-
-message HpkeParams {
-  HpkeKem kem = 1;
-  HpkeKdf kdf = 2;
-  HpkeAead aead = 3;
-}
-
-message HpkePublicKey {
-  uint32 version = 1;
-  HpkeParams params = 2;
-  // KEM-encoding of public key (i.e., SerializePublicKey() ) as described in
-  // https://www.rfc-editor.org/rfc/rfc9180.html#name-cryptographic-dependencies.
-  bytes public_key = 3;
-}
-
-message HpkePrivateKey {
-  uint32 version = 1;
-  HpkePublicKey public_key = 2;
-  // KEM-encoding of private key (i.e., SerializePrivateKey() ) as described in
-  // https://www.rfc-editor.org/rfc/rfc9180.html#name-cryptographic-dependencies.
-  bytes private_key = 3;
-}
-
-message HpkeKeyFormat {
-  HpkeParams params = 1;
-}
diff --git a/javascript/proto/jwt_ecdsa.proto b/javascript/proto/jwt_ecdsa.proto
deleted file mode 100644
index 4c80fe1..0000000
--- a/javascript/proto/jwt_ecdsa.proto
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_ecdsa_go_proto";
-
-// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.4
-enum JwtEcdsaAlgorithm {
-  ES_UNKNOWN = 0;
-  ES256 = 1;  // ECDSA using P-256 and SHA-256
-  ES384 = 2;  // ECDSA using P-384 and SHA-384
-  ES512 = 3;  // ECDSA using P-521 and SHA-512
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey
-message JwtEcdsaPublicKey {
-  uint32 version = 1;
-  JwtEcdsaAlgorithm algorithm = 2;
-  // Affine coordinates of the public key in big-endian representation. The
-  // public key is a point (x, y) on the curve defined by algorithm.
-  bytes x = 3;
-  bytes y = 4;
-
-  // Optional, custom kid header value to be used with "RAW" keys.
-  // "TINK" keys with this value set will be rejected.
-  message CustomKid {
-    string value = 1;
-  }
-  CustomKid custom_kid = 5;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey
-message JwtEcdsaPrivateKey {
-  uint32 version = 1;
-  JwtEcdsaPublicKey public_key = 2;
-  // Unsigned big integer in bigendian representation.
-  bytes key_value = 3;
-}
-
-message JwtEcdsaKeyFormat {
-  uint32 version = 1;
-  JwtEcdsaAlgorithm algorithm = 2;
-}
diff --git a/javascript/proto/jwt_hmac.proto b/javascript/proto/jwt_hmac.proto
deleted file mode 100644
index e54a51d..0000000
--- a/javascript/proto/jwt_hmac.proto
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_hmac_go_proto";
-
-// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
-enum JwtHmacAlgorithm {
-  HS_UNKNOWN = 0;
-  HS256 = 1;  // HMAC using SHA-256
-  HS384 = 2;  // HMAC using SHA-384
-  HS512 = 3;  // HMAC using SHA-512
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.JwtHmacKey
-message JwtHmacKey {
-  uint32 version = 1;
-  JwtHmacAlgorithm algorithm = 2;
-  bytes key_value = 3;
-
-  // Optional, custom kid header value to be used with "RAW" keys.
-  // "TINK" keys with this value set will be rejected.
-  message CustomKid {
-    string value = 1;
-  }
-  CustomKid custom_kid = 4;
-}
-
-message JwtHmacKeyFormat {
-  uint32 version = 1;
-  JwtHmacAlgorithm algorithm = 2;
-  uint32 key_size = 3;
-}
diff --git a/javascript/proto/jwt_rsa_ssa_pkcs1.proto b/javascript/proto/jwt_rsa_ssa_pkcs1.proto
deleted file mode 100644
index adf31c8..0000000
--- a/javascript/proto/jwt_rsa_ssa_pkcs1.proto
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
-
-// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.3
-enum JwtRsaSsaPkcs1Algorithm {
-  RS_UNKNOWN = 0;
-  RS256 = 1;  // RSASSA-PKCS1-v1_5 using SHA-256
-  RS384 = 2;  // RSASSA-PKCS1-v1_5 using SHA-384
-  RS512 = 3;  // RSASSA-PKCS1-v1_5 using SHA-512
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey
-message JwtRsaSsaPkcs1PublicKey {
-  uint32 version = 1;
-  JwtRsaSsaPkcs1Algorithm algorithm = 2;
-  // Modulus.
-  // Unsigned big integer in big-endian representation.
-  bytes n = 3;
-  // Public exponent.
-  // Unsigned big integer in big-endian representation.
-  bytes e = 4;
-
-  // Optional, custom kid header value to be used with "RAW" keys.
-  // "TINK" keys with this value set will be rejected.
-  message CustomKid {
-    string value = 1;
-  }
-  CustomKid custom_kid = 5;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey
-message JwtRsaSsaPkcs1PrivateKey {
-  uint32 version = 1;
-  JwtRsaSsaPkcs1PublicKey public_key = 2;
-  // Private exponent.
-  // Unsigned big integer in big-endian representation.
-  bytes d = 3;
-
-  // The following parameters are used to optimize RSA signature computation.
-  // The prime factor p of n.
-  // Unsigned big integer in big-endian representation.
-  bytes p = 4;
-  // The prime factor q of n.
-  // Unsigned big integer in big-endian representation.
-  bytes q = 5;
-  // d mod (p - 1).
-  // Unsigned big integer in big-endian representation.
-  bytes dp = 6;
-  // d mod (q - 1).
-  // Unsigned big integer in big-endian representation.
-  bytes dq = 7;
-  // Chinese Remainder Theorem coefficient q^(-1) mod p.
-  // Unsigned big integer in big-endian representation.
-  bytes crt = 8;
-}
-
-message JwtRsaSsaPkcs1KeyFormat {
-  uint32 version = 1;
-  JwtRsaSsaPkcs1Algorithm algorithm = 2;
-  uint32 modulus_size_in_bits = 3;
-  bytes public_exponent = 4;
-}
diff --git a/javascript/proto/jwt_rsa_ssa_pss.proto b/javascript/proto/jwt_rsa_ssa_pss.proto
deleted file mode 100644
index 4312645..0000000
--- a/javascript/proto/jwt_rsa_ssa_pss.proto
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_rsa_ssa_pss_go_proto";
-
-// See https://datatracker.ietf.org/doc/html/rfc7518#section-3.5
-enum JwtRsaSsaPssAlgorithm {
-  PS_UNKNOWN = 0;
-  PS256 = 1;  // RSASSA-PSS using SHA-256 and MGF1 with SHA-256
-  PS384 = 2;  // RSASSA-PSS using SHA-384 and MGF1 with SHA-384
-  PS512 = 3;  // RSASSA-PSS using SHA-512 and MGF1 with SHA-512
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey
-message JwtRsaSsaPssPublicKey {
-  uint32 version = 1;
-  JwtRsaSsaPssAlgorithm algorithm = 2;
-  // Modulus.
-  // Unsigned big integer in big-endian representation.
-  bytes n = 3;
-  // Public exponent.
-  // Unsigned big integer in big-endian representation.
-  bytes e = 4;
-
-  // Optional, custom kid header value to be used with "RAW" keys.
-  // "TINK" keys with this value set will be rejected.
-  message CustomKid {
-    string value = 1;
-  }
-  CustomKid custom_kid = 5;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey
-message JwtRsaSsaPssPrivateKey {
-  uint32 version = 1;
-  JwtRsaSsaPssPublicKey public_key = 2;
-  // Private exponent.
-  // Unsigned big integer in big-endian representation.
-  bytes d = 3;
-
-  // The following parameters are used to optimize RSA signature computation.
-  // The prime factor p of n.
-  // Unsigned big integer in big-endian representation.
-  bytes p = 4;
-  // The prime factor q of n.
-  // Unsigned big integer in big-endian representation.
-  bytes q = 5;
-  // d mod (p - 1).
-  // Unsigned big integer in big-endian representation.
-  bytes dp = 6;
-  // d mod (q - 1).
-  // Unsigned big integer in big-endian representation.
-  bytes dq = 7;
-  // Chinese Remainder Theorem coefficient q^(-1) mod p.
-  // Unsigned big integer in big-endian representation.
-  bytes crt = 8;
-}
-
-message JwtRsaSsaPssKeyFormat {
-  uint32 version = 1;
-  JwtRsaSsaPssAlgorithm algorithm = 2;
-  uint32 modulus_size_in_bits = 3;
-  bytes public_exponent = 4;
-}
diff --git a/javascript/proto/kms_aead.proto b/javascript/proto/kms_aead.proto
deleted file mode 100644
index e818788..0000000
--- a/javascript/proto/kms_aead.proto
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_aead_go_proto";
-
-message KmsAeadKeyFormat {
-  // Required.
-  // The location of a KMS key.
-  // With Google Cloud KMS, valid values have this format:
-  // gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.
-  // With AWS KMS, valid values have this format:
-  // aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>
-  string key_uri = 1;
-}
-
-// There is no actual key material in the key.
-message KmsAeadKey {
-  uint32 version = 1;
-  // The key format also contains the params.
-  KmsAeadKeyFormat params = 2;
-}
diff --git a/javascript/proto/kms_envelope.proto b/javascript/proto/kms_envelope.proto
deleted file mode 100644
index fa806e6..0000000
--- a/javascript/proto/kms_envelope.proto
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/tink.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_envelope_go_proto";
-
-message KmsEnvelopeAeadKeyFormat {
-  // Required.
-  // The location of the KEK in a remote KMS.
-  // With Google Cloud KMS, valid values have this format:
-  // gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.
-  // With AWS KMS, valid values have this format:
-  // aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>
-  string kek_uri = 1;
-  // Key template of the Data Encryption Key, e.g., AesCtrHmacAeadKeyFormat.
-  // Required.
-  KeyTemplate dek_template = 2;
-}
-
-// There is no actual key material in the key.
-message KmsEnvelopeAeadKey {
-  uint32 version = 1;
-  // The key format also contains the params.
-  KmsEnvelopeAeadKeyFormat params = 2;
-}
diff --git a/javascript/proto/portable_tink_filter_lite.asciipb b/javascript/proto/portable_tink_filter_lite.asciipb
deleted file mode 100644
index a0f3934..0000000
--- a/javascript/proto/portable_tink_filter_lite.asciipb
+++ /dev/null
@@ -1,27 +0,0 @@
-optimize_mode: LITE_RUNTIME
-allowed_file: "third_party/tink/proto/aes_cmac.proto"
-allowed_file: "third_party/tink/proto/aes_cmac_prf.proto"
-allowed_file: "third_party/tink/proto/aes_ctr.proto"
-allowed_file: "third_party/tink/proto/aes_ctr_hmac_aead.proto"
-allowed_file: "third_party/tink/proto/aes_ctr_hmac_streaming.proto"
-allowed_file: "third_party/tink/proto/aes_eax.proto"
-allowed_file: "third_party/tink/proto/aes_gcm.proto"
-allowed_file: "third_party/tink/proto/aes_gcm_siv.proto"
-allowed_file: "third_party/tink/proto/aes_gcm_hkdf_streaming.proto"
-allowed_file: "third_party/tink/proto/aes_siv.proto"
-allowed_file: "third_party/tink/proto/chacha20_poly1305.proto"
-allowed_file: "third_party/tink/proto/common.proto"
-allowed_file: "third_party/tink/proto/config.proto"
-allowed_file: "third_party/tink/proto/ecdsa.proto"
-allowed_file: "third_party/tink/proto/ecies_aead_hkdf.proto"
-allowed_file: "third_party/tink/proto/ed25519.proto"
-allowed_file: "third_party/tink/proto/empty.proto"
-allowed_file: "third_party/tink/proto/hkdf_prf.proto"
-allowed_file: "third_party/tink/proto/hmac.proto"
-allowed_file: "third_party/tink/proto/hmac_prf.proto"
-allowed_file: "third_party/tink/proto/kms_aead.proto"
-allowed_file: "third_party/tink/proto/kms_envelope.proto"
-allowed_file: "third_party/tink/proto/rsa_ssa_pkcs1.proto"
-allowed_file: "third_party/tink/proto/rsa_ssa_pss.proto"
-allowed_file: "third_party/tink/proto/tink.proto"
-allowed_file: "third_party/tink/proto/xchacha20_poly1305.proto"
diff --git a/javascript/proto/prf_based_deriver.proto b/javascript/proto/prf_based_deriver.proto
deleted file mode 100644
index 06dd334..0000000
--- a/javascript/proto/prf_based_deriver.proto
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/tink.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/prf_based_deriver_go_proto";
-
-message PrfBasedDeriverParams {
-  KeyTemplate derived_key_template = 1;
-}
-
-message PrfBasedDeriverKeyFormat {
-  KeyTemplate prf_key_template = 1;
-  PrfBasedDeriverParams params = 2;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.PrfBasedDeriverKey
-message PrfBasedDeriverKey {
-  uint32 version = 1;
-  KeyData prf_key = 2;
-  PrfBasedDeriverParams params = 3;
-}
diff --git a/javascript/proto/rsa_ssa_pkcs1.proto b/javascript/proto/rsa_ssa_pkcs1.proto
deleted file mode 100644
index 961189d..0000000
--- a/javascript/proto/rsa_ssa_pkcs1.proto
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for RSA SSA (Signature Schemes with Appendix) using PKCS1-v1_5
-// encoding (https://tools.ietf.org/html/rfc8017#section-8.2).
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
-
-message RsaSsaPkcs1Params {
-  // Hash function used in computing hash of the signing message
-  // (see https://tools.ietf.org/html/rfc8017#section-9.2).
-  // Required.
-  HashType hash_type = 1;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PublicKey
-message RsaSsaPkcs1PublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  RsaSsaPkcs1Params params = 2;
-  // Modulus.
-  // Unsigned big integer in bigendian representation.
-  bytes n = 3;
-  // Public exponent.
-  // Unsigned big integer in bigendian representation.
-  bytes e = 4;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey
-message RsaSsaPkcs1PrivateKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  RsaSsaPkcs1PublicKey public_key = 2;
-  // Private exponent.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes d = 3;
-
-  // The following parameters are used to optimize RSA signature computation.
-  // The prime factor p of n.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes p = 4;
-  // The prime factor q of n.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes q = 5;
-  // d mod (p - 1).
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes dp = 6;
-  // d mod (q - 1).
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes dq = 7;
-  // Chinese Remainder Theorem coefficient q^(-1) mod p.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes crt = 8;
-}
-
-message RsaSsaPkcs1KeyFormat {
-  // Required.
-  RsaSsaPkcs1Params params = 1;
-  // Required.
-  uint32 modulus_size_in_bits = 2;
-  // Required.
-  bytes public_exponent = 3;
-}
diff --git a/javascript/proto/rsa_ssa_pss.proto b/javascript/proto/rsa_ssa_pss.proto
deleted file mode 100644
index 8e7903f..0000000
--- a/javascript/proto/rsa_ssa_pss.proto
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for RSA SSA (Signature Schemes with Appendix) using PSS
-// (Probabilistic Signature Scheme ) encoding
-// (https://tools.ietf.org/html/rfc8017#section-8.1).
-syntax = "proto3";
-
-package google.crypto.tink;
-
-import "proto/common.proto";
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pss_go_proto";
-
-message RsaSsaPssParams {
-  // Hash function used in computing hash of the signing message
-  // (see https://tools.ietf.org/html/rfc8017#section-9.1.1).
-  // Required.
-  HashType sig_hash = 1;
-  // Hash function used in MGF1 (a mask generation function based on a
-  // hash function) (see https://tools.ietf.org/html/rfc8017#appendix-B.2.1).
-  // Required.
-  HashType mgf1_hash = 2;
-  // Salt length (see https://tools.ietf.org/html/rfc8017#section-9.1.1)
-  // Required.
-  int32 salt_length = 3;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPssPublicKey
-message RsaSsaPssPublicKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  RsaSsaPssParams params = 2;
-  // Modulus.
-  // Unsigned big integer in bigendian representation.
-  bytes n = 3;
-  // Public exponent.
-  // Unsigned big integer in bigendian representation.
-  bytes e = 4;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey
-message RsaSsaPssPrivateKey {
-  // Required.
-  uint32 version = 1;
-  // Required.
-  RsaSsaPssPublicKey public_key = 2;
-  // Private exponent.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes d = 3;
-
-  // The following parameters are used to optimize RSA signature computation.
-  // The prime factor p of n.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes p = 4;
-  // The prime factor q of n.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes q = 5;
-  // d mod (p - 1).
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes dp = 6;
-  // d mod (q - 1).
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes dq = 7;
-  // Chinese Remainder Theorem coefficient q^(-1) mod p.
-  // Unsigned big integer in bigendian representation.
-  // Required.
-  bytes crt = 8;
-}
-
-message RsaSsaPssKeyFormat {
-  // Required.
-  RsaSsaPssParams params = 1;
-  // Required.
-  uint32 modulus_size_in_bits = 2;
-  // Required.
-  bytes public_exponent = 3;
-}
diff --git a/javascript/proto/test_proto.proto b/javascript/proto/test_proto.proto
deleted file mode 100644
index b82a9e7..0000000
--- a/javascript/proto/test_proto.proto
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-
-message TestProto {
-  uint64 num = 1;
-  bytes str = 2;  // Placeholder for ctype.
-}
-
-message NestedTestProto {
-  TestProto a = 1;
-  TestProto b = 2;
-  uint64 num = 3;
-  bytes str = 4;  // Placeholder for ctype.
-}
-
-message TestProtoWithoutCtype {
-  bytes str = 1;
-}
diff --git a/javascript/proto/tink.proto b/javascript/proto/tink.proto
deleted file mode 100644
index 1787581..0000000
--- a/javascript/proto/tink.proto
+++ /dev/null
@@ -1,194 +0,0 @@
-// Copyright 2017 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-// Definitions for Cloud Crypto SDK (Tink) library.
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/tink_go_proto";
-option objc_class_prefix = "TINKPB";
-
-// Each instantiation of a Tink primitive is identified by type_url,
-// which is a global URL pointing to a *Key-proto that holds key material
-// and other parameters of the instantiation.  For standard Tink key types
-// the value of type_url follows the structure of type_url-field of
-// google.protobuf.Any-protos, and is given as:
-//
-//   type.googleapis.com/packagename.messagename
-//
-// For example, for an HMAC key defined in proto google.cloud.tink.HmacKey
-// the value of type_url is:
-//
-//   type.googleapis.com/google.cloud.tink.HmacKey
-//
-// For each type_url, in addition to the *Key proto, there exist two
-// related structures:
-//   1. *Params: parameters of an instantiation of the primitive,
-//      needed when a key is being used.
-//   2. *KeyFormat: parameters needed to generate a new key; these
-//      include the corresponding Params, since when a factory generates
-//      a key based on KeyFormat, it must add Params to the resulting
-//      key proto with the actual key material.
-// The actual *KeyFormat proto is wrapped in a KeyTemplate message.
-// By convention, the name of the *KeyFormat-proto must be equal
-// to the name of the *Key-proto from type_url-field suffixed with "Format".
-
-message KeyTemplate {
-  // Required. The type_url of the key type in format
-  // type.googleapis.com/packagename.messagename -- see above for details.
-  // This is typically the protobuf type URL of the *Key proto. In particular,
-  // this is different of the protobuf type URL of the *KeyFormat proto.
-  string type_url = 1;
-
-  // Required. The serialized *KeyFormat proto.
-  bytes value = 2;
-
-  // Required. The type of prefix used when computing some primitives to
-  // identify the ciphertext/signature, etc.
-  OutputPrefixType output_prefix_type = 3;
-}
-
-enum KeyStatusType {
-  UNKNOWN_STATUS = 0;
-  ENABLED = 1;    // Can be used for crypto operations.
-  DISABLED = 2;   // Cannot be used, but exists and can become ENABLED.
-  DESTROYED = 3;  // Key data does not exist in this Keyset any more.
-}
-
-// Tink produces and accepts ciphertexts or signatures that consist
-// of a prefix and a payload. The payload and its format is determined
-// entirely by the primitive, but the prefix has to be one of the following
-// 4 types:
-//   - Legacy: prefix is 5 bytes, starts with \x00 and followed by a 4-byte
-//             key id that is computed from the key material. In addition to
-//             that, signature schemes and MACs will add a \x00 byte to the
-//             end of the data being signed / MACed when operating on keys
-//             with this OutputPrefixType.
-//   - Crunchy: prefix is 5 bytes, starts with \x00 and followed by a 4-byte
-//             key id that is generated randomly.
-//   - Tink  : prefix is 5 bytes, starts with \x01 and followed by 4-byte
-//             key id that is generated randomly.
-//   - Raw   : prefix is 0 byte, i.e., empty.
-enum OutputPrefixType {
-  UNKNOWN_PREFIX = 0;
-  TINK = 1;
-  LEGACY = 2;
-  RAW = 3;
-  CRUNCHY = 4;
-}
-
-// Each *Key proto by convention contains a version field, which
-// identifies the version of the key.
-//   message SomeInstantiationKey {
-//     uint32 version = 1;
-//     ...
-//   }
-// If a key type does not mention anything else, only version 0 is currently
-// specified. An implementation must only accept keys with versions it knows,
-// and must reject all keys with unknown version.
-
-// For public key primitives, the public and private keys are distinct entities
-// and represent distinct primitives.  However, by convention, the private key
-// of a public-key primitive contains the corresponding public key proto.
-
-// The actual *Key-proto is wrapped in a KeyData message, which in addition
-// to this serialized proto contains also type_url identifying the
-// definition of *Key-proto (as in KeyFormat-message), and some extra metadata
-// about the type key material.
-message KeyData {
-  // Required.
-  string type_url = 1;  // In format type.googleapis.com/packagename.messagename
-  // Required.
-  // Contains specific serialized *Key proto
-  bytes value = 2;  // Placeholder for ctype.
-  enum KeyMaterialType {
-    UNKNOWN_KEYMATERIAL = 0;
-    SYMMETRIC = 1;
-    ASYMMETRIC_PRIVATE = 2;
-    ASYMMETRIC_PUBLIC = 3;
-    REMOTE = 4;  // points to a remote key, i.e., in a KMS.
-  }
-  // Required.
-  KeyMaterialType key_material_type = 3;
-}
-
-// A Tink user works usually not with single keys, but with keysets,
-// to enable key rotation.  The keys in a keyset can belong to different
-// implementations/key types, but must all implement the same primitive.
-// Any given keyset (and any given key) can be used for one primitive only.
-message Keyset {
-  message Key {
-    // Contains the actual, instantiation specific key proto.
-    // By convention, each key proto contains a version field.
-    KeyData key_data = 1;
-
-    KeyStatusType status = 2;
-
-    // Identifies a key within a keyset, is a part of metadata
-    // of a ciphertext/signature.
-    uint32 key_id = 3;
-
-    // Determines the prefix of the ciphertexts/signatures produced by this key.
-    // This value is copied verbatim from the key template.
-    OutputPrefixType output_prefix_type = 4;
-  }
-
-  // Identifies key used to generate new crypto data (encrypt, sign).
-  // Required.
-  uint32 primary_key_id = 1;
-
-  // Actual keys in the Keyset.
-  // Required.
-  repeated Key key = 2;
-}
-
-// Represents a "safe" Keyset that doesn't contain any actual key material,
-// thus can be used for logging or monitoring. Most fields are copied from
-// Keyset.
-message KeysetInfo {
-  message KeyInfo {
-    // the type url of this key,
-    // e.g., type.googleapis.com/google.crypto.tink.HmacKey.
-    string type_url = 1;
-
-    // See Keyset.Key.status.
-    KeyStatusType status = 2;
-
-    // See Keyset.Key.key_id.
-    uint32 key_id = 3;
-
-    // See Keyset.Key.output_prefix_type.
-    OutputPrefixType output_prefix_type = 4;
-  }
-
-  // See Keyset.primary_key_id.
-  uint32 primary_key_id = 1;
-
-  // KeyInfos in the KeysetInfo.
-  // Each KeyInfo is corresponding to a Key in the corresponding Keyset.
-  repeated KeyInfo key_info = 2;
-}
-
-// Represents a keyset that is encrypted with a master key.
-message EncryptedKeyset {
-  // Required.
-  bytes encrypted_keyset = 2;
-  // Optional.
-  KeysetInfo keyset_info = 3;
-}
diff --git a/javascript/proto/xchacha20_poly1305.proto b/javascript/proto/xchacha20_poly1305.proto
deleted file mode 100644
index cc52624..0000000
--- a/javascript/proto/xchacha20_poly1305.proto
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2018 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-syntax = "proto3";
-
-package google.crypto.tink;
-
-option java_package = "com.google.crypto.tink.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/xchacha20_poly1305_go_proto";
-
-message XChaCha20Poly1305KeyFormat {
-  uint32 version = 1;
-}
-
-// key_type: type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key
-message XChaCha20Poly1305Key {
-  uint32 version = 1;
-  bytes key_value = 3;
-}
diff --git a/javascript/signature/BUILD.bazel b/javascript/signature/BUILD.bazel
deleted file mode 100644
index ba7874a..0000000
--- a/javascript/signature/BUILD.bazel
+++ /dev/null
@@ -1,53 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "signature",
-    srcs = [
-        "ecdsa_for_signing.ts",
-        "ecdsa_for_verifying.ts",
-        "ecdsa_private_key_manager.ts",
-        "ecdsa_public_key_manager.ts",
-        "ecdsa_util.ts",
-        "index.ts",
-        "public_key_sign_wrapper.ts",
-        "public_key_verify_wrapper.ts",
-        "sign.ts",
-        "sign_wrapper.ts",
-        "signature_config.ts",
-        "signature_key_templates.ts",
-        "verify.ts",
-        "verify_wrapper.ts",
-    ],
-    module_name = "tink-crypto/signature",
-    deps = [
-        "//exception",
-        "//internal",
-        "//internal:proto",
-        "//signature/internal",
-        "//subtle",
-    ],
-)
-
-ts_library(
-    name = "signature_tests",
-    testonly = True,
-    srcs = [
-        "ecdsa_private_key_manager_test.ts",
-        "ecdsa_public_key_manager_test.ts",
-        "public_key_sign_wrapper_test.ts",
-        "public_key_verify_wrapper_test.ts",
-        "signature_config_test.ts",
-        "signature_key_templates_test.ts",
-    ],
-    deps = [
-        ":signature",
-        "//internal",
-        "//internal:proto",
-        "//signature/internal",
-        "//subtle",
-        "//testing/internal",
-        "@npm//@types/jasmine",
-    ],
-)
diff --git a/javascript/signature/ecdsa_for_signing.ts b/javascript/signature/ecdsa_for_signing.ts
deleted file mode 100644
index c02b2be..0000000
--- a/javascript/signature/ecdsa_for_signing.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {EcdsaPrivateKeyManager} from './ecdsa_private_key_manager';
-import {SignatureKeyTemplates} from './signature_key_templates';
-
-export function register() {
-  Registry.registerKeyManager(new EcdsaPrivateKeyManager());
-}
-
-export const ecdsaP256KeyTemplate = SignatureKeyTemplates.ecdsaP256;
-export const ecdsaP384KeyTemplate = SignatureKeyTemplates.ecdsaP384;
-export const ecdsaP521KeyTemplate = SignatureKeyTemplates.ecdsaP521;
-export const ecdsaP256IeeeEncodingKeyTemplate =
-    SignatureKeyTemplates.ecdsaP256IeeeEncoding;
-export const ecdsaP384IeeeEncodingKeyTemplate =
-    SignatureKeyTemplates.ecdsaP384IeeeEncoding;
-export const ecdsaP521IeeeEncodingKeyTemplate =
-    SignatureKeyTemplates.ecdsaP521IeeeEncoding;
diff --git a/javascript/signature/ecdsa_for_verifying.ts b/javascript/signature/ecdsa_for_verifying.ts
deleted file mode 100644
index ac6efcd..0000000
--- a/javascript/signature/ecdsa_for_verifying.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {EcdsaPublicKeyManager} from './ecdsa_public_key_manager';
-
-export function register() {
-  Registry.registerKeyManager(new EcdsaPublicKeyManager());
-}
diff --git a/javascript/signature/ecdsa_private_key_manager.ts b/javascript/signature/ecdsa_private_key_manager.ts
deleted file mode 100644
index aaff99e..0000000
--- a/javascript/signature/ecdsa_private_key_manager.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as KeyManager from '../internal/key_manager';
-import {PbEcdsaKeyFormat, PbEcdsaParams, PbEcdsaPrivateKey, PbEcdsaPublicKey, PbKeyData, PbMessage} from '../internal/proto';
-import * as Util from '../internal/util';
-import {Constructor} from '../internal/util';
-import * as Bytes from '../subtle/bytes';
-import * as ecdsaSign from '../subtle/ecdsa_sign';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-
-import {EcdsaPublicKeyManager} from './ecdsa_public_key_manager';
-import * as EcdsaUtil from './ecdsa_util';
-import {PublicKeySign} from './internal/public_key_sign';
-
-const VERSION = 0;
-
-/**
- * @final
- */
-class EcdsaPrivateKeyFactory implements KeyManager.PrivateKeyFactory {
-  /**
-   */
-  async newKey(keyFormat: PbMessage|Uint8Array): Promise<PbEcdsaPrivateKey> {
-    if (!keyFormat) {
-      throw new SecurityException('Key format has to be non-null.');
-    }
-    const keyFormatProto = EcdsaPrivateKeyFactory.getKeyFormatProto(keyFormat);
-    EcdsaUtil.validateKeyFormat(keyFormatProto);
-    const params = keyFormatProto.getParams();
-    if (!params) {
-      throw new SecurityException('Params not set');
-    }
-    const curveTypeProto = params.getCurve();
-    const curveTypeSubtle = Util.curveTypeProtoToSubtle(curveTypeProto);
-    const curveName = EllipticCurves.curveToString(curveTypeSubtle);
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', curveName);
-    const jsonPublicKey =
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const jsonPrivateKey =
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    return EcdsaPrivateKeyFactory.jsonToProtoKey(
-        jsonPrivateKey, jsonPublicKey, params);
-  }
-
-  /**
-   */
-  async newKeyData(serializedKeyFormat: Uint8Array): Promise<PbKeyData> {
-    const key = await this.newKey(serializedKeyFormat);
-    const keyData =
-        (new PbKeyData())
-            .setTypeUrl(EcdsaPrivateKeyManager.KEY_TYPE)
-            .setValue(key.serializeBinary())
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PRIVATE);
-    return keyData;
-  }
-
-  getPublicKeyData(serializedPrivateKey: Uint8Array) {
-    const privateKey = deserializePrivateKey(serializedPrivateKey);
-    const publicKey = privateKey.getPublicKey();
-    if (!publicKey) {
-      throw new SecurityException('Public key not set');
-    }
-    const publicKeyData =
-        (new PbKeyData())
-            .setValue(publicKey.serializeBinary())
-            .setTypeUrl(EcdsaPublicKeyManager.KEY_TYPE)
-            .setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC);
-    return publicKeyData;
-  }
-
-  /**
-   * Creates a private key proto corresponding to given JSON key pair and with
-   * the given params.
-   *
-   */
-  private static jsonToProtoKey(
-      jsonPrivateKey: JsonWebKey, jsonPublicKey: JsonWebKey,
-      params: PbEcdsaParams): PbEcdsaPrivateKey {
-    const {x, y} = jsonPublicKey;
-    if (x === undefined) {
-      throw new SecurityException('x must be set');
-    }
-    if (y === undefined) {
-      throw new SecurityException('y must be set');
-    }
-    const publicKeyProto = (new PbEcdsaPublicKey())
-                               .setVersion(EcdsaPublicKeyManager.VERSION)
-                               .setParams(params)
-                               .setX(Bytes.fromBase64(x, true))
-                               .setY(Bytes.fromBase64(y, true));
-    const {d} = jsonPrivateKey;
-    if (d === undefined) {
-      throw new SecurityException('d must be set');
-    }
-    const privateKeyProto = (new PbEcdsaPrivateKey())
-                                .setVersion(VERSION)
-                                .setPublicKey(publicKeyProto)
-                                .setKeyValue(Bytes.fromBase64(d, true));
-    return privateKeyProto;
-  }
-
-  /**
-   * The input keyFormat is either deserialized (in case that the input is
-   * Uint8Array) or checked to be an EcdsaKeyFormat-proto (otherwise).
-   *
-   */
-  private static getKeyFormatProto(keyFormat: PbMessage|
-                                   Uint8Array): PbEcdsaKeyFormat {
-    if (keyFormat instanceof Uint8Array) {
-      return EcdsaPrivateKeyFactory.deserializeKeyFormat(keyFormat);
-    } else if (keyFormat instanceof PbEcdsaKeyFormat) {
-      return keyFormat;
-    } else {
-      throw new SecurityException(
-          'Expected ' + EcdsaPrivateKeyManager.KEY_TYPE + ' key format proto.');
-    }
-  }
-
-  private static deserializeKeyFormat(keyFormat: Uint8Array): PbEcdsaKeyFormat {
-    let keyFormatProto: PbEcdsaKeyFormat;
-    try {
-      keyFormatProto = PbEcdsaKeyFormat.deserializeBinary(keyFormat);
-    } catch (e) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' + EcdsaPrivateKeyManager.KEY_TYPE +
-          ' key format proto.');
-    }
-    if (!keyFormatProto.getParams()) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' + EcdsaPrivateKeyManager.KEY_TYPE +
-          ' key format proto.');
-    }
-    return keyFormatProto;
-  }
-}
-
-/**
- * @final
- */
-export class EcdsaPrivateKeyManager implements
-    KeyManager.KeyManager<PublicKeySign> {
-  private static readonly SUPPORTED_PRIMITIVE = PublicKeySign;
-  static KEY_TYPE: string =
-      'type.googleapis.com/google.crypto.tink.EcdsaPrivateKey';
-  keyFactory = new EcdsaPrivateKeyFactory();
-
-  async getPrimitive(
-      primitiveType: Constructor<PublicKeySign>, key: PbKeyData|PbMessage) {
-    if (primitiveType !== this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    const keyProto = EcdsaPrivateKeyManager.getKeyProto(key);
-    EcdsaUtil.validatePrivateKey(
-        keyProto, VERSION, EcdsaPublicKeyManager.VERSION);
-    const recepientPrivateKey = EcdsaUtil.getJsonWebKeyFromProto(keyProto);
-    const publicKey = keyProto.getPublicKey();
-    if (!publicKey) {
-      throw new SecurityException('Public key not set');
-    }
-    const params = publicKey.getParams();
-    if (!params) {
-      throw new SecurityException('Params not set');
-    }
-    const hash = Util.hashTypeProtoToString(params.getHashType());
-    const encoding = EcdsaUtil.encodingTypeProtoToEnum(params.getEncoding());
-    return ecdsaSign.fromJsonWebKey(recepientPrivateKey, hash, encoding);
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return EcdsaPrivateKeyManager.KEY_TYPE;
-  }
-
-  getPrimitiveType() {
-    return EcdsaPrivateKeyManager.SUPPORTED_PRIMITIVE;
-  }
-
-  getVersion() {
-    return VERSION;
-  }
-
-  getKeyFactory() {
-    return this.keyFactory;
-  }
-
-  private static getKeyProto(keyMaterial: PbKeyData|
-                             PbMessage): PbEcdsaPrivateKey {
-    if (keyMaterial instanceof PbKeyData) {
-      return EcdsaPrivateKeyManager.getKeyProtoFromKeyData(keyMaterial);
-    }
-    if (keyMaterial instanceof PbEcdsaPrivateKey) {
-      return keyMaterial;
-    }
-    throw new SecurityException(
-        'Key type is not supported. This key ' +
-        'manager supports ' + EcdsaPrivateKeyManager.KEY_TYPE + '.');
-  }
-
-  private static getKeyProtoFromKeyData(keyData: PbKeyData): PbEcdsaPrivateKey {
-    if (keyData.getTypeUrl() !== EcdsaPrivateKeyManager.KEY_TYPE) {
-      throw new SecurityException(
-          'Key type ' + keyData.getTypeUrl() +
-          ' is not supported. This key manager supports ' +
-          EcdsaPrivateKeyManager.KEY_TYPE + '.');
-    }
-    return deserializePrivateKey(keyData.getValue_asU8());
-  }
-}
-
-function deserializePrivateKey(serializedPrivateKey: Uint8Array):
-    PbEcdsaPrivateKey {
-  let key: PbEcdsaPrivateKey;
-  try {
-    key = PbEcdsaPrivateKey.deserializeBinary(serializedPrivateKey);
-  } catch (e) {
-    throw new SecurityException(
-        'Input cannot be parsed as ' + EcdsaPrivateKeyManager.KEY_TYPE +
-        ' key-proto.');
-  }
-  if (!key.getPublicKey() || !key.getKeyValue_asU8()) {
-    throw new SecurityException(
-        'Input cannot be parsed as ' + EcdsaPrivateKeyManager.KEY_TYPE +
-        ' key-proto.');
-  }
-  return key;
-}
diff --git a/javascript/signature/ecdsa_private_key_manager_test.ts b/javascript/signature/ecdsa_private_key_manager_test.ts
deleted file mode 100644
index e459398..0000000
--- a/javascript/signature/ecdsa_private_key_manager_test.ts
+++ /dev/null
@@ -1,507 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbEcdsaKeyFormat, PbEcdsaParams, PbEcdsaPrivateKey, PbEcdsaPublicKey, PbEcdsaSignatureEncoding, PbEllipticCurveType, PbHashType, PbKeyData} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Random from '../subtle/random';
-import {assertExists, assertInstanceof} from '../testing/internal/test_utils';
-
-import {EcdsaPrivateKeyManager} from './ecdsa_private_key_manager';
-import {EcdsaPublicKeyManager} from './ecdsa_public_key_manager';
-import {PublicKeySign} from './internal/public_key_sign';
-import {PublicKeyVerify} from './internal/public_key_verify';
-
-const PRIVATE_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EcdsaPrivateKey';
-const PRIVATE_KEY_MATERIAL_TYPE = PbKeyData.KeyMaterialType.ASYMMETRIC_PRIVATE;
-const VERSION = 0;
-const PRIVATE_KEY_MANAGER_PRIMITIVE = PublicKeySign;
-
-const PUBLIC_KEY_TYPE = 'type.googleapis.com/google.crypto.tink.EcdsaPublicKey';
-const PUBLIC_KEY_MATERIAL_TYPE = PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC;
-const PUBLIC_KEY_MANAGER_PRIMITIVE = PublicKeyVerify;
-
-describe('ecdsa private key manager test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('new key, invalid serialized key format', async function() {
-    const invalidSerializedKeyFormat = new Uint8Array(0);
-    const manager = new EcdsaPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(invalidSerializedKeyFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
-    }
-  });
-
-  it('new key, unsupported key format proto', async function() {
-    const unsupportedKeyFormatProto = new PbEcdsaParams();
-    const manager = new EcdsaPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(unsupportedKeyFormatProto);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyFormat());
-    }
-  });
-
-  it('new key, invalid format, missing params', async function() {
-    const invalidFormat = new PbEcdsaKeyFormat();
-    const manager = new EcdsaPrivateKeyManager();
-
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidKeyFormatMissingParams());
-    }
-  });
-
-  it('new key, invalid format, invalid params', async function() {
-    const manager = new EcdsaPrivateKeyManager();
-
-    // Unknown encoding.
-    const invalidFormat = createKeyFormat();
-    invalidFormat.getParams()?.setEncoding(
-        PbEcdsaSignatureEncoding.UNKNOWN_ENCODING);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownEncoding());
-    }
-    invalidFormat.getParams()?.setEncoding(PbEcdsaSignatureEncoding.DER);
-
-    // Unknown hash.
-    invalidFormat.getParams()?.setHashType(PbHashType.UNKNOWN_HASH);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownHash());
-    }
-    invalidFormat.getParams()?.setHashType(PbHashType.SHA256);
-
-    // Unknown curve.
-    invalidFormat.getParams()?.setCurve(PbEllipticCurveType.UNKNOWN_CURVE);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownCurve());
-    }
-
-    // Bad hash + curve combinations.
-    invalidFormat.getParams()?.setCurve(PbEllipticCurveType.NIST_P384);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-384 or SHA-512 (because curve is P-384) but got SHA-256');
-    }
-    invalidFormat.getParams()?.setCurve(PbEllipticCurveType.NIST_P521);
-    try {
-      await manager.getKeyFactory().newKey(invalidFormat);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-512 (because curve is P-521) but got SHA-256');
-    }
-  });
-
-  it('new key, via key format', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const manager = new EcdsaPrivateKeyManager();
-    for (const keyFormat of keyFormats) {
-      const key = await manager.getKeyFactory().newKey(keyFormat);
-
-      expect(key.getPublicKey()?.getParams()).toEqual(keyFormat.getParams());
-      // The keys are tested more in tests for getPrimitive method below, where
-      // the primitive based on the created key is tested.
-    }
-  });
-
-  it('new key data, invalid serialized key format', async function() {
-    const serializedKeyFormats = [new Uint8Array(1), new Uint8Array(0)];
-    const manager = new EcdsaPrivateKeyManager();
-
-    const serializedKeyFormatsLength = serializedKeyFormats.length;
-    for (let i = 0; i < serializedKeyFormatsLength; i++) {
-      try {
-        await manager.getKeyFactory().newKeyData(serializedKeyFormats[i]);
-        fail(
-            'An exception should be thrown for the string: ' +
-            serializedKeyFormats[i]);
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
-        continue;
-      }
-    }
-  });
-
-  it('new key data, from valid key format', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const manager = new EcdsaPrivateKeyManager();
-    for (const keyFormat of keyFormats) {
-      const serializedKeyFormat = keyFormat.serializeBinary();
-      const keyData =
-          await manager.getKeyFactory().newKeyData(serializedKeyFormat);
-      expect(keyData.getTypeUrl()).toBe(PRIVATE_KEY_TYPE);
-      expect(keyData.getKeyMaterialType()).toBe(PRIVATE_KEY_MATERIAL_TYPE);
-
-      const key = PbEcdsaPrivateKey.deserializeBinary(keyData.getValue_asU8());
-      expect(key.getPublicKey()?.getParams()).toEqual(keyFormat.getParams());
-      // The keys are tested more in tests for getPrimitive method below, where
-      // the primitive based on the created key is tested.
-    }
-  });
-
-  it('get public key data, invalid private key serialization', function() {
-    const manager = new EcdsaPrivateKeyManager();
-
-    const privateKey = new Uint8Array([0, 1]);  // not a serialized private key
-    try {
-      const factory = manager.getKeyFactory();
-      factory.getPublicKeyData(privateKey);
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.invalidSerializedKey());
-    }
-  });
-
-  it('get public key data, should work', async function() {
-    const keyFormat = createKeyFormat();
-    const manager = new EcdsaPrivateKeyManager();
-    const privateKey = await manager.getKeyFactory().newKey(keyFormat);
-    const factory = (manager.getKeyFactory());
-    const publicKeyData =
-        factory.getPublicKeyData(privateKey.serializeBinary());
-
-    expect(publicKeyData.getTypeUrl()).toBe(PUBLIC_KEY_TYPE);
-    expect(publicKeyData.getKeyMaterialType()).toBe(PUBLIC_KEY_MATERIAL_TYPE);
-    const publicKey =
-        PbEcdsaPublicKey.deserializeBinary(publicKeyData.getValue_asU8());
-    expect(publicKey.getVersion())
-        .toEqual(privateKey.getPublicKey()!.getVersion());
-    expect(publicKey.getParams())
-        .toEqual(privateKey.getPublicKey()!.getParams());
-    expect(publicKey.getX_asU8())
-        .toEqual(privateKey.getPublicKey()!.getX_asU8());
-    expect(publicKey.getY_asU8())
-        .toEqual(privateKey.getPublicKey()!.getY_asU8());
-  });
-
-  it('get primitive, unsupported key data type', async function() {
-    const manager = new EcdsaPrivateKeyManager();
-    const keyFormat = createKeyFormat();
-    const keyData =
-        (await manager.getKeyFactory().newKeyData(keyFormat.serializeBinary()))
-            .setTypeUrl('unsupported_key_type_url');
-
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, keyData);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyType(keyData.getTypeUrl()));
-    }
-  });
-
-  it('get primitive, unsupported key type', async function() {
-    const manager = new EcdsaPrivateKeyManager();
-    const key = new PbEcdsaPublicKey();
-
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyType());
-    }
-  });
-
-  it('get primitive, high version', async function() {
-    const manager = new EcdsaPrivateKeyManager();
-    const version = manager.getVersion() + 1;
-    const keyFormat = createKeyFormat();
-    const key =
-        assertInstanceof(
-            await manager.getKeyFactory().newKey(keyFormat), PbEcdsaPrivateKey)
-            .setVersion(version);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.versionOutOfBounds());
-    }
-  });
-
-  it('get primitive, invalid params', async function() {
-    const manager = new EcdsaPrivateKeyManager();
-    const keyFormat = createKeyFormat();
-    const key = assertInstanceof(
-        await manager.getKeyFactory().newKey(keyFormat), PbEcdsaPrivateKey);
-
-    // Unknown encoding.
-    key.getPublicKey()?.getParams()?.setEncoding(
-        PbEcdsaSignatureEncoding.UNKNOWN_ENCODING);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownEncoding());
-    }
-    key.getPublicKey()?.getParams()?.setEncoding(PbEcdsaSignatureEncoding.DER);
-
-    // Unknown hash.
-    key.getPublicKey()?.getParams()?.setHashType(PbHashType.UNKNOWN_HASH);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownHash());
-    }
-    key.getPublicKey()?.getParams()?.setHashType(PbHashType.SHA256);
-
-    // Unknown curve.
-    key.getPublicKey()?.getParams()?.setCurve(
-        PbEllipticCurveType.UNKNOWN_CURVE);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownCurve());
-    }
-
-    // Bad hash + curve combinations.
-    key.getPublicKey()?.getParams()?.setCurve(PbEllipticCurveType.NIST_P384);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-384 or SHA-512 (because curve is P-384) but got SHA-256');
-    }
-    key.getPublicKey()?.getParams()?.setCurve(PbEllipticCurveType.NIST_P521);
-    try {
-      await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-512 (because curve is P-521) but got SHA-256');
-    }
-  });
-
-  it('get primitive, invalid serialized key', async function() {
-    const manager = new EcdsaPrivateKeyManager();
-    const keyFormat = createKeyFormat();
-    const keyData =
-        await manager.getKeyFactory().newKeyData(keyFormat.serializeBinary());
-
-
-    for (let i = 0; i < 2; ++i) {
-      // Set the value of keyData to something which is not a serialization of a
-      // proper key.
-      keyData.setValue(new Uint8Array(i));
-      try {
-        await manager.getPrimitive(PRIVATE_KEY_MANAGER_PRIMITIVE, keyData);
-        fail('An exception should be thrown ' + i.toString());
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerializedKey());
-      }
-    }
-  });
-
-  it('get primitive, from key', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const privateKeyManager = new EcdsaPrivateKeyManager();
-    const publicKeyManager = new EcdsaPublicKeyManager();
-    for (const keyFormat of keyFormats) {
-      const key = assertInstanceof(
-          await privateKeyManager.getKeyFactory().newKey(keyFormat),
-          PbEcdsaPrivateKey);
-      const publicKeyVerify: PublicKeyVerify =
-          assertExists(await publicKeyManager.getPrimitive(
-              PUBLIC_KEY_MANAGER_PRIMITIVE, assertExists(key.getPublicKey())));
-      const publicKeySign: PublicKeySign =
-          assertExists(await privateKeyManager.getPrimitive(
-              PRIVATE_KEY_MANAGER_PRIMITIVE, key));
-
-      const data = Random.randBytes(10);
-      const signature = await publicKeySign.sign(data);
-      const isValid = await publicKeyVerify.verify(signature, data);
-
-      expect(isValid).toBe(true);
-    }
-  });
-
-  it('get primitive, from key data', async function() {
-    const keyFormats = createTestSetOfKeyFormats();
-    const privateKeyManager = new EcdsaPrivateKeyManager();
-    const publicKeyManager = new EcdsaPublicKeyManager();
-
-    for (const keyFormat of keyFormats) {
-      const serializedKeyFormat = keyFormat.serializeBinary();
-      const keyData = await privateKeyManager.getKeyFactory().newKeyData(
-          serializedKeyFormat);
-      const factory = privateKeyManager.getKeyFactory();
-      const publicKeyData = factory.getPublicKeyData(keyData.getValue_asU8());
-
-      const publicKeyVerify: PublicKeyVerify =
-          assertExists(await publicKeyManager.getPrimitive(
-              PUBLIC_KEY_MANAGER_PRIMITIVE, publicKeyData));
-      const publicKeySign: PublicKeySign =
-          assertExists(await privateKeyManager.getPrimitive(
-              PRIVATE_KEY_MANAGER_PRIMITIVE, keyData));
-
-      const data = Random.randBytes(10);
-      const signature = await publicKeySign.sign(data);
-      const isValid = await publicKeyVerify.verify(signature, data);
-
-      expect(isValid).toBe(true);
-    }
-  });
-
-  it('does support', function() {
-    const manager = new EcdsaPrivateKeyManager();
-    expect(manager.doesSupport(PRIVATE_KEY_TYPE)).toBe(true);
-  });
-
-  it('get key type', function() {
-    const manager = new EcdsaPrivateKeyManager();
-    expect(manager.getKeyType()).toBe(PRIVATE_KEY_TYPE);
-  });
-
-  it('get primitive type', function() {
-    const manager = new EcdsaPrivateKeyManager();
-    expect(manager.getPrimitiveType()).toBe(PRIVATE_KEY_MANAGER_PRIMITIVE);
-  });
-
-  it('get version', function() {
-    const manager = new EcdsaPrivateKeyManager();
-    expect(manager.getVersion()).toBe(VERSION);
-  });
-});
-
-// Helper classes and functions
-class ExceptionText {
-  static nullKeyFormat(): string {
-    return 'SecurityException: Key format has to be non-null.';
-  }
-
-  static invalidSerializedKeyFormat(): string {
-    return 'SecurityException: Input cannot be parsed as ' + PRIVATE_KEY_TYPE +
-        ' key format proto.';
-  }
-
-  static unsupportedPrimitive(): string {
-    return 'SecurityException: Requested primitive type which is not supported by ' +
-        'this key manager.';
-  }
-
-  static unsupportedKeyFormat(): string {
-    return 'SecurityException: Expected ' + PRIVATE_KEY_TYPE +
-        ' key format proto.';
-  }
-
-  static unsupportedKeyType(opt_requestedKeyType?: string): string {
-    const prefix = 'SecurityException: Key type';
-    const suffix =
-        'is not supported. This key manager supports ' + PRIVATE_KEY_TYPE + '.';
-    if (opt_requestedKeyType) {
-      return prefix + ' ' + opt_requestedKeyType + ' ' + suffix;
-    } else {
-      return prefix + ' ' + suffix;
-    }
-  }
-
-  static unknownEncoding(): string {
-    return 'SecurityException: Invalid public key - missing signature encoding.';
-  }
-
-  static unknownHash(): string {
-    return 'SecurityException: Unknown hash type.';
-  }
-
-  static unknownCurve(): string {
-    return 'SecurityException: Unknown curve type.';
-  }
-
-  static versionOutOfBounds(): string {
-    return 'SecurityException: Version is out of bound, must be between 0 and ' +
-        VERSION + '.';
-  }
-
-  static invalidKeyFormatMissingParams(): string {
-    return 'SecurityException: Invalid key format - missing params.';
-  }
-
-  static invalidSerializedKey(): string {
-    return 'SecurityException: Input cannot be parsed as ' + PRIVATE_KEY_TYPE +
-        ' key-proto.';
-  }
-}
-
-function createParams(
-    curveType: PbEllipticCurveType, hashType: PbHashType,
-    encoding: PbEcdsaSignatureEncoding): PbEcdsaParams {
-  const params =
-      new PbEcdsaParams().setCurve(curveType).setHashType(hashType).setEncoding(
-          encoding);
-
-  return params;
-}
-
-function createKeyFormat(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType: PbHashType = PbHashType.SHA256,
-    opt_encodingType: PbEcdsaSignatureEncoding =
-        PbEcdsaSignatureEncoding.DER): PbEcdsaKeyFormat {
-  const keyFormat = new PbEcdsaKeyFormat().setParams(
-      createParams(opt_curveType, opt_hashType, opt_encodingType));
-  return keyFormat;
-}
-
-/**
- * Create set of key formats with all possible predefined/supported parameters.
- */
-function createTestSetOfKeyFormats(): PbEcdsaKeyFormat[] {
-  const keyFormats: PbEcdsaKeyFormat[] = [];
-  keyFormats.push(createKeyFormat(
-      PbEllipticCurveType.NIST_P256, PbHashType.SHA256,
-      PbEcdsaSignatureEncoding.DER));
-  keyFormats.push(createKeyFormat(
-      PbEllipticCurveType.NIST_P256, PbHashType.SHA256,
-      PbEcdsaSignatureEncoding.IEEE_P1363));
-  keyFormats.push(createKeyFormat(
-      PbEllipticCurveType.NIST_P384, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.DER));
-  keyFormats.push(createKeyFormat(
-      PbEllipticCurveType.NIST_P384, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.IEEE_P1363));
-  keyFormats.push(createKeyFormat(
-      PbEllipticCurveType.NIST_P521, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.DER));
-  keyFormats.push(createKeyFormat(
-      PbEllipticCurveType.NIST_P521, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.IEEE_P1363));
-  return keyFormats;
-}
diff --git a/javascript/signature/ecdsa_public_key_manager.ts b/javascript/signature/ecdsa_public_key_manager.ts
deleted file mode 100644
index 46d75e2..0000000
--- a/javascript/signature/ecdsa_public_key_manager.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as KeyManager from '../internal/key_manager';
-import {PbEcdsaParams, PbEcdsaPublicKey, PbKeyData, PbMessage} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as ecdsaVerify from '../subtle/ecdsa_verify';
-
-import * as EcdsaUtil from './ecdsa_util';
-import {PublicKeyVerify} from './internal/public_key_verify';
-
-/**
- * @final
- */
-class EcdsaPublicKeyFactory implements KeyManager.KeyFactory {
-  newKey(keyFormat: PbMessage|Uint8Array): never {
-    throw new SecurityException(
-        'This operation is not supported for public keys. ' +
-        'Use EcdsaPrivateKeyManager to generate new keys.');
-  }
-
-  newKeyData(serializedKeyFormat: Uint8Array): never {
-    throw new SecurityException(
-        'This operation is not supported for public keys. ' +
-        'Use EcdsaPrivateKeyManager to generate new keys.');
-  }
-}
-
-/**
- * @final
- */
-export class EcdsaPublicKeyManager implements
-    KeyManager.KeyManager<PublicKeyVerify> {
-  static KEY_TYPE: string =
-      'type.googleapis.com/google.crypto.tink.EcdsaPublicKey';
-  private static readonly SUPPORTED_PRIMITIVE = PublicKeyVerify;
-  static VERSION: number = 0;
-  keyFactory = new EcdsaPublicKeyFactory();
-
-  async getPrimitive(
-      primitiveType: Util.Constructor<PublicKeyVerify>,
-      key: PbKeyData|PbMessage) {
-    if (primitiveType !== this.getPrimitiveType()) {
-      throw new SecurityException(
-          'Requested primitive type which is not ' +
-          'supported by this key manager.');
-    }
-    const keyProto = EcdsaPublicKeyManager.getKeyProto(key);
-    EcdsaUtil.validatePublicKey(keyProto, this.getVersion());
-    const jwk = EcdsaUtil.getJsonWebKeyFromProto(keyProto);
-    const params = (keyProto.getParams() as PbEcdsaParams);
-    const hash = Util.hashTypeProtoToString(params.getHashType());
-    const encoding = EcdsaUtil.encodingTypeProtoToEnum(params.getEncoding());
-    return ecdsaVerify.fromJsonWebKey(jwk, hash, encoding);
-  }
-
-  doesSupport(keyType: string) {
-    return keyType === this.getKeyType();
-  }
-
-  getKeyType() {
-    return EcdsaPublicKeyManager.KEY_TYPE;
-  }
-
-  getPrimitiveType() {
-    return EcdsaPublicKeyManager.SUPPORTED_PRIMITIVE;
-  }
-
-  getVersion() {
-    return EcdsaPublicKeyManager.VERSION;
-  }
-
-  getKeyFactory() {
-    return this.keyFactory;
-  }
-
-  private static getKeyProto(keyMaterial: PbKeyData|
-                             PbMessage): PbEcdsaPublicKey {
-    if (keyMaterial instanceof PbKeyData) {
-      return EcdsaPublicKeyManager.getKeyProtoFromKeyData(keyMaterial);
-    }
-    if (keyMaterial instanceof PbEcdsaPublicKey) {
-      return keyMaterial;
-    }
-    throw new SecurityException(
-        'Key type is not supported. This key manager supports ' +
-        EcdsaPublicKeyManager.KEY_TYPE + '.');
-  }
-
-  private static getKeyProtoFromKeyData(keyData: PbKeyData): PbEcdsaPublicKey {
-    if (keyData.getTypeUrl() !== EcdsaPublicKeyManager.KEY_TYPE) {
-      throw new SecurityException(
-          'Key type ' + keyData.getTypeUrl() + ' is not supported. This key ' +
-          'manager supports ' + EcdsaPublicKeyManager.KEY_TYPE + '.');
-    }
-    let key: PbEcdsaPublicKey;
-    try {
-      key = PbEcdsaPublicKey.deserializeBinary(keyData.getValue_asU8());
-    } catch (e) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' + EcdsaPublicKeyManager.KEY_TYPE +
-          ' key-proto.');
-    }
-    if (!key.getParams() || !key.getX_asU8() || !key.getY_asU8()) {
-      throw new SecurityException(
-          'Input cannot be parsed as ' + EcdsaPublicKeyManager.KEY_TYPE +
-          ' key-proto.');
-    }
-    return key;
-  }
-}
diff --git a/javascript/signature/ecdsa_public_key_manager_test.ts b/javascript/signature/ecdsa_public_key_manager_test.ts
deleted file mode 100644
index 7f7877f..0000000
--- a/javascript/signature/ecdsa_public_key_manager_test.ts
+++ /dev/null
@@ -1,384 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbEcdsaParams, PbEcdsaPublicKey, PbEcdsaSignatureEncoding, PbEllipticCurveType, PbHashType, PbKeyData} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Util from '../internal/util';
-import * as Bytes from '../subtle/bytes';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-import {assertExists} from '../testing/internal/test_utils';
-
-import {EcdsaPublicKeyManager} from './ecdsa_public_key_manager';
-import {PublicKeyVerify} from './internal/public_key_verify';
-
-const KEY_TYPE = 'type.googleapis.com/google.crypto.tink.EcdsaPublicKey';
-const VERSION = 0;
-const PRIMITIVE = PublicKeyVerify;
-
-describe('ecdsa public key manager test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('new key', function() {
-    const manager = new EcdsaPublicKeyManager();
-
-    try {
-      manager.getKeyFactory().newKey(new Uint8Array(0));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.notSupported());
-    }
-  });
-
-  it('new key data', function() {
-    const manager = new EcdsaPublicKeyManager();
-
-    try {
-      manager.getKeyFactory().newKeyData(new Uint8Array(0));
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.notSupported());
-    }
-  });
-
-  it('get primitive, unsupported key data type', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const keyData =
-        (await createKeyData()).setTypeUrl('unsupported_key_type_url');
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, keyData);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(ExceptionText.unsupportedKeyType(keyData.getTypeUrl()));
-    }
-  });
-
-  it('get primitive, unsupported key type', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const key = new PbEcdsaParams();
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unsupportedKeyType());
-    }
-  });
-
-  it('get primitive, high version', async function() {
-    const version = 1;
-    const manager = new EcdsaPublicKeyManager();
-    const key = (await createKey()).setVersion(version);
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.versionOutOfBounds());
-    }
-  });
-
-  it('get primitive, missing params', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const key = (await createKey()).setParams(null);
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.missingParams());
-    }
-  });
-
-  it('get primitive, invalid params', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const key = await createKey();
-
-    // Unknown encoding.
-    key.getParams()?.setEncoding(PbEcdsaSignatureEncoding.UNKNOWN_ENCODING);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownEncoding());
-    }
-    key.getParams()?.setEncoding(PbEcdsaSignatureEncoding.DER);
-
-    // Unknown hash.
-    key.getParams()?.setHashType(PbHashType.UNKNOWN_HASH);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownHash());
-    }
-    key.getParams()?.setHashType(PbHashType.SHA256);
-
-    // Unknown curve.
-    key.getParams()?.setCurve(PbEllipticCurveType.UNKNOWN_CURVE);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString()).toBe(ExceptionText.unknownCurve());
-    }
-
-    // Bad hash + curve combinations.
-    key.getParams()?.setCurve(PbEllipticCurveType.NIST_P384);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-384 or SHA-512 (because curve is P-384) but got SHA-256');
-    }
-    key.getParams()?.setCurve(PbEllipticCurveType.NIST_P521);
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-512 (because curve is P-521) but got SHA-256');
-    }
-  });
-
-  it('get primitive, invalid key', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const key = await createKey();
-    const x = key.getX_asU8();
-    key.setX(new Uint8Array([0]));
-
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(ExceptionText.webCryptoErrors()).toContain(e.toString());
-    }
-
-    key.setX(x);
-    key.setY(new Uint8Array([0]));
-    try {
-      await manager.getPrimitive(PRIMITIVE, key);
-      fail('An exception should be thrown.');
-    } catch (e: any) {
-      expect(ExceptionText.webCryptoErrors()).toContain(e.toString());
-    }
-  });
-
-  it('get primitive, invalid serialized key', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const keyData = await createKeyData();
-
-    for (let i = 0; i < 2; ++i) {
-      // Set the value of keyData to something which is not a serialization of a
-      // proper key.
-      keyData.setValue(new Uint8Array(i));
-      try {
-        await manager.getPrimitive(PRIMITIVE, keyData);
-        fail('An exception should be thrown ' + i.toString());
-      } catch (e: any) {
-        expect(e.toString()).toBe(ExceptionText.invalidSerializedKey());
-      }
-    }
-  });
-
-  // tests for getting primitive from valid key/keyData
-  it('get primitive, from key', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const keys = await createTestSetOfKeys();
-    for (const key of keys) {
-      await manager.getPrimitive(PRIMITIVE, key);
-    }
-  });
-
-  it('get primitive, from key data', async function() {
-    const manager = new EcdsaPublicKeyManager();
-    const keyDatas = await createTestSetOfKeyDatas();
-    for (const key of keyDatas) {
-      await manager.getPrimitive(PRIMITIVE, key);
-    }
-  });
-
-  it('does support', function() {
-    const manager = new EcdsaPublicKeyManager();
-
-    expect(manager.doesSupport(KEY_TYPE)).toBe(true);
-  });
-
-  it('get key type', function() {
-    const manager = new EcdsaPublicKeyManager();
-
-    expect(manager.getKeyType()).toBe(KEY_TYPE);
-  });
-
-  it('get primitive type', function() {
-    const manager = new EcdsaPublicKeyManager();
-
-    expect(manager.getPrimitiveType()).toBe(PRIMITIVE);
-  });
-
-  it('get version', function() {
-    const manager = new EcdsaPublicKeyManager();
-    expect(manager.getVersion()).toBe(VERSION);
-  });
-});
-
-// Helper classes and functions
-class ExceptionText {
-  static notSupported(): string {
-    return 'SecurityException: This operation is not supported for public keys. ' +
-        'Use EcdsaPrivateKeyManager to generate new keys.';
-  }
-
-  static unsupportedPrimitive(): string {
-    return 'SecurityException: Requested primitive type which is not supported by ' +
-        'this key manager.';
-  }
-
-  static unsupportedKeyType(opt_requestedKeyType?: string): string {
-    const prefix = 'SecurityException: Key type';
-    const suffix =
-        'is not supported. This key manager supports ' + KEY_TYPE + '.';
-    if (opt_requestedKeyType) {
-      return prefix + ' ' + opt_requestedKeyType + ' ' + suffix;
-    } else {
-      return prefix + ' ' + suffix;
-    }
-  }
-
-  static versionOutOfBounds(): string {
-    return 'SecurityException: Version is out of bound, must be between 0 and ' +
-        VERSION + '.';
-  }
-
-  static unknownEncoding(): string {
-    return 'SecurityException: Invalid public key - missing signature encoding.';
-  }
-
-  static unknownHash(): string {
-    return 'SecurityException: Unknown hash type.';
-  }
-
-  static unknownCurve(): string {
-    return 'SecurityException: Unknown curve type.';
-  }
-
-  static missingParams(): string {
-    return 'SecurityException: Invalid public key - missing params.';
-  }
-
-  static missingXY(): string {
-    return 'SecurityException: Invalid public key - missing value of X or Y.';
-  }
-
-  static invalidSerializedKey(): string {
-    return 'SecurityException: Input cannot be parsed as ' + KEY_TYPE +
-        ' key-proto.';
-  }
-
-  static webCryptoErrors(): string[] {
-    return [
-      'DataError',
-      // Firefox
-      'DataError: Data provided to an operation does not meet requirements',
-    ];
-  }
-}
-
-function createParams(
-    curveType: PbEllipticCurveType, hashType: PbHashType,
-    encoding: PbEcdsaSignatureEncoding): PbEcdsaParams {
-  const params = (new PbEcdsaParams())
-                     .setCurve(curveType)
-                     .setHashType(hashType)
-                     .setEncoding(encoding);
-  return params;
-}
-
-async function createKey(
-    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
-    opt_hashType: PbHashType = PbHashType.SHA256,
-    opt_encoding: PbEcdsaSignatureEncoding =
-        PbEcdsaSignatureEncoding.DER): Promise<PbEcdsaPublicKey> {
-  const curveSubtleType = Util.curveTypeProtoToSubtle(opt_curveType);
-  const curveName = EllipticCurves.curveToString(curveSubtleType);
-  const key =
-      (new PbEcdsaPublicKey())
-          .setVersion(0)
-          .setParams(createParams(opt_curveType, opt_hashType, opt_encoding));
-  const keyPair = await EllipticCurves.generateKeyPair('ECDSA', curveName);
-  const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-  key.setX(
-      Bytes.fromBase64(assertExists(publicKey['x']), /* opt_webSafe = */ true));
-  key.setY(
-      Bytes.fromBase64(assertExists(publicKey['y']), /* opt_webSafe = */ true));
-  return key;
-}
-
-function createKeyDataFromKey(key: PbEcdsaPublicKey): PbKeyData {
-  const keyData =
-      new PbKeyData()
-          .setTypeUrl(KEY_TYPE)
-          .setValue(key.serializeBinary())
-          .setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC);
-
-  return keyData;
-}
-
-async function createKeyData(
-    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
-    opt_encoding?: PbEcdsaSignatureEncoding): Promise<PbKeyData> {
-  const key = await createKey(opt_curveType, opt_hashType, opt_encoding);
-  return createKeyDataFromKey(key);
-}
-
-// Create set of keys with all possible predefined/supported parameters.
-async function createTestSetOfKeys(): Promise<PbEcdsaPublicKey[]> {
-  const keys: PbEcdsaPublicKey[] = [];
-  keys.push(await createKey(
-      PbEllipticCurveType.NIST_P256, PbHashType.SHA256,
-      PbEcdsaSignatureEncoding.DER));
-  keys.push(await createKey(
-      PbEllipticCurveType.NIST_P256, PbHashType.SHA256,
-      PbEcdsaSignatureEncoding.IEEE_P1363));
-  keys.push(await createKey(
-      PbEllipticCurveType.NIST_P384, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.DER));
-  keys.push(await createKey(
-      PbEllipticCurveType.NIST_P384, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.IEEE_P1363));
-  keys.push(await createKey(
-      PbEllipticCurveType.NIST_P521, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.DER));
-  keys.push(await createKey(
-      PbEllipticCurveType.NIST_P521, PbHashType.SHA512,
-      PbEcdsaSignatureEncoding.IEEE_P1363));
-  return keys;
-}
-
-// Create set of keyData protos with keys of all possible predefined/supported
-// parameters.
-async function createTestSetOfKeyDatas(): Promise<PbKeyData[]> {
-  const keys = await createTestSetOfKeys();
-  const keyDatas: PbKeyData[] = [];
-  for (const key of keys) {
-    const keyData = await createKeyDataFromKey(key);
-    keyDatas.push(keyData);
-  }
-
-  return keyDatas;
-}
diff --git a/javascript/signature/ecdsa_util.ts b/javascript/signature/ecdsa_util.ts
deleted file mode 100644
index 4d0b243..0000000
--- a/javascript/signature/ecdsa_util.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {PbEcdsaKeyFormat, PbEcdsaParams, PbEcdsaPrivateKey, PbEcdsaPublicKey, PbEcdsaSignatureEncoding as PbEcdsaSignatureEncodingType} from '../internal/proto';
-import * as Util from '../internal/util';
-import * as EllipticCurves from '../subtle/elliptic_curves';
-import * as Validators from '../subtle/validators';
-
-export function validateKeyFormat(keyFormat: PbEcdsaKeyFormat) {
-  const params = keyFormat.getParams();
-  if (!params) {
-    throw new SecurityException('Invalid key format - missing params.');
-  }
-  validateParams(params);
-}
-
-export function validatePrivateKey(
-    key: PbEcdsaPrivateKey, privateKeyManagerVersion: number,
-    publicKeyManagerVersion: number) {
-  Validators.validateVersion(key.getVersion(), privateKeyManagerVersion);
-  if (!key.getKeyValue_asU8()) {
-    throw new SecurityException(
-        'Invalid private key - missing private key value.');
-  }
-  const publicKey = key.getPublicKey();
-  if (!publicKey) {
-    throw new SecurityException(
-        'Invalid private key - missing public key information.');
-  }
-  validatePublicKey(publicKey, publicKeyManagerVersion);
-}
-
-export function validatePublicKey(
-    key: PbEcdsaPublicKey, publicKeyManagerVersion: number) {
-  Validators.validateVersion(key.getVersion(), publicKeyManagerVersion);
-  const params = key.getParams();
-  if (!params) {
-    throw new SecurityException('Invalid public key - missing params.');
-  }
-  validateParams(params);
-  if (!key.getX_asU8().length || !key.getY_asU8().length) {
-    throw new SecurityException(
-        'Invalid public key - missing value of X or Y.');
-  }
-}
-
-export function validateParams(params: PbEcdsaParams) {
-  if (params.getEncoding() === PbEcdsaSignatureEncodingType.UNKNOWN_ENCODING) {
-    throw new SecurityException(
-        'Invalid public key - missing signature encoding.');
-  }
-  const hash = Util.hashTypeProtoToString(params.getHashType());
-  const curve = EllipticCurves.curveToString(
-      Util.curveTypeProtoToSubtle(params.getCurve()));
-  Validators.validateEcdsaParams(curve, hash);
-}
-
-export function encodingTypeProtoToEnum(
-    encodingTypeProto: PbEcdsaSignatureEncodingType):
-    EllipticCurves.EcdsaSignatureEncodingType {
-  switch (encodingTypeProto) {
-    case PbEcdsaSignatureEncodingType.DER:
-      return EllipticCurves.EcdsaSignatureEncodingType.DER;
-    case PbEcdsaSignatureEncodingType.IEEE_P1363:
-      return EllipticCurves.EcdsaSignatureEncodingType.IEEE_P1363;
-    default:
-      throw new SecurityException('Unknown ECDSA signature encoding type.');
-  }
-}
-
-/**
- * WARNING: This method assumes that the given key proto is valid.
- *
- */
-export function getJsonWebKeyFromProto(key: PbEcdsaPrivateKey|
-                                       PbEcdsaPublicKey): JsonWebKey {
-  let publicKey: PbEcdsaPublicKey;
-  let d: Uint8Array|null = null;
-  if (key instanceof PbEcdsaPrivateKey) {
-    publicKey = (key.getPublicKey() as PbEcdsaPublicKey);
-  } else {
-    publicKey = key;
-  }
-  const params = publicKey.getParams();
-  if (!params) {
-    throw new SecurityException('Params not set');
-  }
-  const curveType = Util.curveTypeProtoToSubtle(params.getCurve());
-  const expectedLength = EllipticCurves.fieldSizeInBytes(curveType);
-  const x = Util.bigEndianNumberToCorrectLength(
-      publicKey.getX_asU8(), expectedLength);
-  const y = Util.bigEndianNumberToCorrectLength(
-      publicKey.getY_asU8(), expectedLength);
-  if (key instanceof PbEcdsaPrivateKey) {
-    d = Util.bigEndianNumberToCorrectLength(
-        key.getKeyValue_asU8(), expectedLength);
-  }
-  return EllipticCurves.getJsonWebKey(curveType, x, y, d);
-}
diff --git a/javascript/signature/index.ts b/javascript/signature/index.ts
deleted file mode 100644
index d5ffa96..0000000
--- a/javascript/signature/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as ecdsaForSigning from './ecdsa_for_signing';
-import * as ecdsaForVerifying from './ecdsa_for_verifying';
-import * as signWrapper from './sign_wrapper';
-import * as verifyWrapper from './verify_wrapper';
-
-export * from './sign';
-export * from './verify';
-export {ecdsaP256IeeeEncodingKeyTemplate, ecdsaP256KeyTemplate, ecdsaP384IeeeEncodingKeyTemplate, ecdsaP384KeyTemplate, ecdsaP521IeeeEncodingKeyTemplate, ecdsaP521KeyTemplate} from './ecdsa_for_signing';
-
-export function register() {
-  ecdsaForSigning.register();
-  ecdsaForVerifying.register();
-  signWrapper.register();
-  verifyWrapper.register();
-}
diff --git a/javascript/signature/internal/BUILD.bazel b/javascript/signature/internal/BUILD.bazel
deleted file mode 100644
index 05af749..0000000
--- a/javascript/signature/internal/BUILD.bazel
+++ /dev/null
@@ -1,11 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "internal",
-    srcs = [
-        "public_key_sign.ts",
-        "public_key_verify.ts",
-    ],
-)
diff --git a/javascript/signature/internal/public_key_sign.ts b/javascript/signature/internal/public_key_sign.ts
deleted file mode 100644
index bf3513a..0000000
--- a/javascript/signature/internal/public_key_sign.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2018 Google LLC
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for creating digital signatures.
- *
- * Security guarantees: Implementations of these interfaces are secure
- * against adaptive chosen-message attacks. Signing data ensures the
- * authenticity and the integrity of that data, but not its secrecy.
- *
- */
-export abstract class PublicKeySign {
-  /**
-   * Computes the digital signature of `message`.
-   *
-   * @param message the message to be signed, must be non-null.
-   * @return resulting digital signature
-   */
-  abstract sign(message: Uint8Array): Promise<Uint8Array>;
-}
diff --git a/javascript/signature/internal/public_key_verify.ts b/javascript/signature/internal/public_key_verify.ts
deleted file mode 100644
index b70b66b..0000000
--- a/javascript/signature/internal/public_key_verify.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2018 Google LLC
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for verifying digital signatures.
- *
- * Security guarantees: Implementations of these interfaces are secure
- * against adaptive chosen-message attacks. Signing data ensures the
- * authenticity and the integrity of that data, but not its secrecy.
- *
- */
-export abstract class PublicKeyVerify {
-  /**
-   * Verifies the `signature` of `message`.
-   *
-   * @param signature the signature, must be non-null.
-   * @param message the message, must be non-null.
-   * @return true iff the signature is valid, false
-   *     otherwise.
-   */
-  abstract verify(signature: Uint8Array, message: Uint8Array): Promise<boolean>;
-}
diff --git a/javascript/signature/public_key_sign_wrapper.ts b/javascript/signature/public_key_sign_wrapper.ts
deleted file mode 100644
index a4f19ac..0000000
--- a/javascript/signature/public_key_sign_wrapper.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PrimitiveWrapper} from '../internal/primitive_wrapper';
-import * as Bytes from '../subtle/bytes';
-import * as Validators from '../subtle/validators';
-
-import {PublicKeySign} from './internal/public_key_sign';
-
-/**
- * @final
- */
-class WrappedPublicKeySign extends PublicKeySign {
-  // The constructor should be @private, but it is not supported by Closure
-  // (see https://github.com/google/closure-compiler/issues/2761).
-  constructor(private readonly primitiveSet:
-                  PrimitiveSet.PrimitiveSet<PublicKeySign>) {
-    super();
-  }
-
-  static newPublicKeySign(
-      primitiveSet: PrimitiveSet.PrimitiveSet<PublicKeySign>): PublicKeySign {
-    if (!primitiveSet) {
-      throw new SecurityException('Primitive set has to be non-null.');
-    }
-    if (!primitiveSet.getPrimary()) {
-      throw new SecurityException('Primary has to be non-null.');
-    }
-    return new WrappedPublicKeySign(primitiveSet);
-  }
-
-  async sign(data: Uint8Array) {
-    Validators.requireUint8Array(data);
-    const primary = this.primitiveSet.getPrimary();
-    if (!primary) {
-      throw new SecurityException('Primary not set.');
-    }
-    const primitive = primary.getPrimitive();
-    const signature = await primitive.sign(data);
-    const keyId = primary.getIdentifier();
-    return Bytes.concat(keyId, signature);
-  }
-}
-
-export class PublicKeySignWrapper implements PrimitiveWrapper<PublicKeySign> {
-  /**
-   */
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<PublicKeySign>) {
-    return WrappedPublicKeySign.newPublicKeySign(primitiveSet);
-  }
-
-  /**
-   */
-  getPrimitiveType() {
-    return PublicKeySign;
-  }
-}
diff --git a/javascript/signature/public_key_sign_wrapper_test.ts b/javascript/signature/public_key_sign_wrapper_test.ts
deleted file mode 100644
index 10fe36e..0000000
--- a/javascript/signature/public_key_sign_wrapper_test.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {CryptoFormat} from '../internal/crypto_format';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Random from '../subtle/random';
-import {assertExists} from '../testing/internal/test_utils';
-
-import {PublicKeySign} from './internal/public_key_sign';
-import {PublicKeySignWrapper} from './public_key_sign_wrapper';
-
-describe('public key sign wrapper test', function() {
-  it('new public key sign, primitive set without primary', function() {
-    const primitiveSet = createDummyPrimitiveSet(/* opt_withPrimary = */ false);
-    try {
-      new PublicKeySignWrapper().wrap(primitiveSet);
-      fail('Should throw an exception.');
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: Primary has to be non-null.');
-    }
-  });
-
-  it('new public key sign, should work', function() {
-    const primitiveSet = createDummyPrimitiveSet();
-    const publicKeySign = new PublicKeySignWrapper().wrap(primitiveSet);
-    expect(publicKeySign != null && publicKeySign != undefined).toBe(true);
-  });
-
-  it('sign, should work', async function() {
-    const primitiveSet = createDummyPrimitiveSet();
-    const publicKeySign = new PublicKeySignWrapper().wrap(primitiveSet);
-
-    const data = Random.randBytes(10);
-
-    const signature = await publicKeySign.sign(data);
-    expect(signature != null).toBe(true);
-
-    // Signature should begin with primary key output prefix.
-    expect(signature.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE))
-        .toEqual(assertExists(primitiveSet.getPrimary()).getIdentifier());
-  });
-});
-
-/**
- * Class holding texts for each type of exception.
- * @final
- */
-class ExceptionText {
-  static nullPrimitiveSet(): string {
-    return 'CustomError: Primitive set has to be non-null.';
-  }
-
-  static primitiveSetWithoutPrimary(): string {
-    return 'CustomError: Primary has to be non-null.';
-  }
-
-  static nullPlaintext(): string {
-    return 'CustomError: Plaintext has to be non-null.';
-  }
-}
-
-/** Function for creating keys for testing purposes. */
-function createDummyKeysetKey(
-    keyId: number, outputPrefix: PbOutputPrefixType,
-    enabled: boolean): PbKeysetKey {
-  const key = new PbKeysetKey();
-
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-
-  key.setOutputPrefixType(outputPrefix);
-  key.setKeyId(keyId);
-
-  return key;
-}
-
-/**
- * Creates a primitive set with 'numberOfPrimitives' primitives. The keys
- * corresponding to the primitives have ids from the set
- * [1, ..., numberOfPrimitives] and the primitive corresponding to key with id
- * 'numberOfPrimitives' is set to be primary whenever opt_withPrimary is set to
- * true (where true is the default value).
- */
-function createDummyPrimitiveSet(opt_withPrimary: boolean = true):
-    PrimitiveSet.PrimitiveSet<DummyPublicKeySign> {
-  const numberOfPrimitives = 5;
-  const primitiveSet =
-      new PrimitiveSet.PrimitiveSet<DummyPublicKeySign>(DummyPublicKeySign);
-  for (let i = 1; i < numberOfPrimitives; i++) {
-    let outputPrefix: PbOutputPrefixType;
-    switch (i % 3) {
-      case 0:
-        outputPrefix = PbOutputPrefixType.TINK;
-        break;
-      case 1:
-        outputPrefix = PbOutputPrefixType.LEGACY;
-        break;
-      default:
-        outputPrefix = PbOutputPrefixType.RAW;
-    }
-    const key =
-        createDummyKeysetKey(i, outputPrefix, /* enabled = */ i % 4 < 2);
-    const publicKeySign = new DummyPublicKeySign();
-    primitiveSet.addPrimitive(publicKeySign, key);
-  }
-
-  const key = createDummyKeysetKey(
-      numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true);
-  const publicKeySign = new DummyPublicKeySign();
-  const entry = primitiveSet.addPrimitive(publicKeySign, key);
-  if (opt_withPrimary) {
-    primitiveSet.setPrimary(entry);
-  }
-
-  return primitiveSet;
-}
-
-/** @final */
-class DummyPublicKeySign extends PublicKeySign {
-  async sign(data: Uint8Array) {
-    return data;
-  }
-}
diff --git a/javascript/signature/public_key_verify_wrapper.ts b/javascript/signature/public_key_verify_wrapper.ts
deleted file mode 100644
index b8013ce..0000000
--- a/javascript/signature/public_key_verify_wrapper.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {CryptoFormat} from '../internal/crypto_format';
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PrimitiveWrapper} from '../internal/primitive_wrapper';
-import {PbKeyStatusType} from '../internal/proto';
-import * as Validators from '../subtle/validators';
-
-import {PublicKeyVerify} from './internal/public_key_verify';
-
-/**
- * @final
- */
-class WrappedPublicKeyVerify extends PublicKeyVerify {
-  // The constructor should be @private, but it is not supported by Closure
-  // (see https://github.com/google/closure-compiler/issues/2761).
-  constructor(private readonly primitiveSet:
-                  PrimitiveSet.PrimitiveSet<PublicKeyVerify>) {
-    super();
-  }
-
-  static newPublicKeyVerify(primitiveSet:
-                                PrimitiveSet.PrimitiveSet<PublicKeyVerify>):
-      PublicKeyVerify {
-    if (!primitiveSet) {
-      throw new SecurityException('Primitive set has to be non-null.');
-    }
-    return new WrappedPublicKeyVerify(primitiveSet);
-  }
-
-  async verify(signature: Uint8Array, data: Uint8Array) {
-    Validators.requireUint8Array(signature);
-    Validators.requireUint8Array(data);
-    if (signature.length > CryptoFormat.NON_RAW_PREFIX_SIZE) {
-      const keyId = signature.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE);
-      const primitives = await this.primitiveSet.getPrimitives(keyId);
-      const rawSignature = signature.subarray(
-          CryptoFormat.NON_RAW_PREFIX_SIZE, signature.length);
-      let isValid: boolean = false;
-      try {
-        isValid = await this.tryVerification(primitives, rawSignature, data);
-      } catch (e) {
-        // Ignored.
-      }
-
-      if (isValid) {
-        return isValid;
-      }
-    }
-    const primitives = await this.primitiveSet.getRawPrimitives();
-    return this.tryVerification(primitives, signature, data);
-  }
-
-  /**
-   * Tries to verify the signature using each entry in primitives. It
-   * returns false if no entry succeeds.
-   *
-   *
-   */
-  private async tryVerification(
-      primitives: Array<PrimitiveSet.Entry<PublicKeyVerify>>,
-      signature: Uint8Array, data: Uint8Array): Promise<boolean> {
-    const primitivesLength = primitives.length;
-    for (let i = 0; i < primitivesLength; i++) {
-      if (primitives[i].getKeyStatus() != PbKeyStatusType.ENABLED) {
-        continue;
-      }
-      const primitive = primitives[i].getPrimitive();
-      let isValid: boolean;
-      try {
-        isValid = await primitive.verify(signature, data);
-      } catch (e) {
-        continue;
-      }
-      if (isValid) {
-        return isValid;
-      }
-    }
-    return false;
-  }
-}
-
-export class PublicKeyVerifyWrapper implements
-    PrimitiveWrapper<PublicKeyVerify> {
-  /**
-   */
-  wrap(primitiveSet: PrimitiveSet.PrimitiveSet<PublicKeyVerify>) {
-    return WrappedPublicKeyVerify.newPublicKeyVerify(primitiveSet);
-  }
-
-  /**
-   */
-  getPrimitiveType() {
-    return PublicKeyVerify;
-  }
-}
diff --git a/javascript/signature/public_key_verify_wrapper_test.ts b/javascript/signature/public_key_verify_wrapper_test.ts
deleted file mode 100644
index edd7a69..0000000
--- a/javascript/signature/public_key_verify_wrapper_test.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as PrimitiveSet from '../internal/primitive_set';
-import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Bytes from '../subtle/bytes';
-import * as Random from '../subtle/random';
-
-import {PublicKeySign} from './internal/public_key_sign';
-import {PublicKeyVerify} from './internal/public_key_verify';
-import {PublicKeySignWrapper} from './public_key_sign_wrapper';
-import {PublicKeyVerifyWrapper} from './public_key_verify_wrapper';
-
-describe('public key verify wrapper test', function() {
-  it('verify, with empty signature', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const primitiveSet = primitiveSets['publicPrimitiveSet'];
-    const publicKeyVerify = new PublicKeyVerifyWrapper().wrap(primitiveSet);
-    expect(
-        await publicKeyVerify.verify(new Uint8Array(0), Random.randBytes(10)))
-        .toBe(false);
-  });
-
-  it('verify, should work', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const data = Random.randBytes(10);
-    // As keys are just dummy keys which do not contain key data, the same key
-    // is used for both sign and verify.
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.TINK,
-        /** enabled = */ true);
-    const signatureSuffix = Random.randBytes(10);
-
-    // Get the signature
-    const privatePrimitiveSet = primitiveSets['privatePrimitiveSet'];
-    const signPrimitive = new DummyPublicKeySign(signatureSuffix);
-    const entry = privatePrimitiveSet.addPrimitive(signPrimitive, key);
-    // Has to be set to primary as then it is used in signing.
-    privatePrimitiveSet.setPrimary(entry);
-    const publicKeySign = new PublicKeySignWrapper().wrap(privatePrimitiveSet);
-    const signature = await publicKeySign.sign(data);
-
-    // Create a primitive set containing the primitives which can be used for
-    // verification. Add also few more primitives with the same key as the
-    // primitive set should verify the signature whenever there is at least
-    // one primitive which does not fail to verify the signature.
-    const publicPrimitiveSet = primitiveSets['publicPrimitiveSet'];
-    const verifyPrimitive = new DummyPublicKeyVerify(signatureSuffix);
-    publicPrimitiveSet.addPrimitive(
-        new DummyPublicKeyVerify(Random.randBytes(5)), key);
-    publicPrimitiveSet.addPrimitive(verifyPrimitive, key);
-    publicPrimitiveSet.addPrimitive(
-        new DummyPublicKeyVerify(Random.randBytes(5)), key);
-
-    // Verify the signature.
-    const publicKeyVerify =
-        new PublicKeyVerifyWrapper().wrap(publicPrimitiveSet);
-
-    const isValid = await publicKeyVerify.verify(signature, data);
-    expect(isValid).toBe(true);
-  });
-
-  it('verify, raw primitive', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const data = Random.randBytes(10);
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
-        /** enabled = */ true);
-    const signatureSuffix = Random.randBytes(10);
-
-    // Get the signature.
-    const signPrimitive = new DummyPublicKeySign(signatureSuffix);
-    const signature = await signPrimitive.sign(data);
-
-    // Verify the signature.
-    const primitiveSet = primitiveSets['publicPrimitiveSet'];
-    const verifyPrimitive = new DummyPublicKeyVerify(signatureSuffix);
-    primitiveSet.addPrimitive(verifyPrimitive, key);
-    const publicKeyVerify = new PublicKeyVerifyWrapper().wrap(primitiveSet);
-
-    const isValid = await publicKeyVerify.verify(signature, data);
-    expect(isValid).toBe(true);
-  });
-
-  it('verify, with disabled primitive', async function() {
-    const primitiveSets = createDummyPrimitiveSets();
-    const data = Random.randBytes(10);
-    const key = createDummyKeysetKey(
-        /** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
-        /** enabled = */ false);
-    const signatureSuffix = new Uint8Array([0, 0, 0, 0xFF]);
-
-    const signPrimitive = new DummyPublicKeySign(signatureSuffix);
-    const signature = await signPrimitive.sign(data);
-
-    const primitiveSet = primitiveSets['publicPrimitiveSet'];
-    const verifyPrimitive = new DummyPublicKeyVerify(signatureSuffix);
-    primitiveSet.addPrimitive(verifyPrimitive, key);
-    const publicKeyVerify = new PublicKeyVerifyWrapper().wrap(primitiveSet);
-
-    const isValid = await publicKeyVerify.verify(signature, data);
-    expect(isValid).toBe(false);
-  });
-});
-
-/** Function for creating keys for testing purposes. */
-function createDummyKeysetKey(
-    keyId: number, outputPrefix: PbOutputPrefixType,
-    enabled: boolean): PbKeysetKey {
-  const key = new PbKeysetKey();
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-
-  key.setOutputPrefixType(outputPrefix);
-  key.setKeyId(keyId);
-
-  return key;
-}
-
-/**
- * Creates a primitive sets for PublicKeySign and PublicKeyVerify with
- * 'numberOfPrimitives' primitives. The keys corresponding to the primitives
- * have ids from the set [1, ..., numberOfPrimitives] and the primitive
- * corresponding to key with id 'numberOfPrimitives' is set to be primary
- * whenever opt_withPrimary is set to true (where true is the default value).
- */
-function createDummyPrimitiveSets(opt_withPrimary: boolean = true): {
-  publicPrimitiveSet: PrimitiveSet.PrimitiveSet<DummyPublicKeyVerify>,
-  privatePrimitiveSet: PrimitiveSet.PrimitiveSet<DummyPublicKeySign>,
-} {
-  const numberOfPrimitives = 5;
-
-  const publicPrimitiveSet =
-      new PrimitiveSet.PrimitiveSet<DummyPublicKeyVerify>(DummyPublicKeyVerify);
-  const privatePrimitiveSet =
-      new PrimitiveSet.PrimitiveSet<DummyPublicKeySign>(DummyPublicKeySign);
-  for (let i = 1; i < numberOfPrimitives; i++) {
-    let outputPrefix: PbOutputPrefixType;
-    switch (i % 3) {
-      case 0:
-        outputPrefix = PbOutputPrefixType.TINK;
-        break;
-      case 1:
-        outputPrefix = PbOutputPrefixType.LEGACY;
-        break;
-      default:
-        outputPrefix = PbOutputPrefixType.RAW;
-    }
-    const key =
-        createDummyKeysetKey(i, outputPrefix, /* enabled = */ i % 4 < 2);
-    const signatureSuffix = new Uint8Array([0, 0, i]);
-    const publicKeySign = new DummyPublicKeySign(signatureSuffix);
-    privatePrimitiveSet.addPrimitive(publicKeySign, key);
-    const publicKeyVerify = new DummyPublicKeyVerify(signatureSuffix);
-    publicPrimitiveSet.addPrimitive(publicKeyVerify, key);
-  }
-
-  const key = createDummyKeysetKey(
-      numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true);
-  const signatureSuffix = new Uint8Array([0, 0, numberOfPrimitives]);
-  const publicKeySign = new DummyPublicKeySign(signatureSuffix);
-  const signEntry = privatePrimitiveSet.addPrimitive(publicKeySign, key);
-  const publicKeyVerify = new DummyPublicKeyVerify(signatureSuffix);
-  const verifyEntry = publicPrimitiveSet.addPrimitive(publicKeyVerify, key);
-  if (opt_withPrimary) {
-    publicPrimitiveSet.setPrimary(verifyEntry);
-    privatePrimitiveSet.setPrimary(signEntry);
-  }
-
-  return {
-    'publicPrimitiveSet': publicPrimitiveSet,
-    'privatePrimitiveSet': privatePrimitiveSet
-  };
-}
-
-/** @final */
-class DummyPublicKeySign extends PublicKeySign {
-  constructor(private readonly signatureSuffix: Uint8Array) {
-    super();
-  }
-
-  async sign(data: Uint8Array) {
-    return Bytes.concat(data, this.signatureSuffix);
-  }
-}
-
-/** @final */
-class DummyPublicKeyVerify extends PublicKeyVerify {
-  constructor(private readonly signatureSuffix: Uint8Array) {
-    super();
-  }
-
-  async verify(signature: Uint8Array, data: Uint8Array) {
-    return Bytes.isEqual(Bytes.concat(data, this.signatureSuffix), signature);
-  }
-}
diff --git a/javascript/signature/sign.ts b/javascript/signature/sign.ts
deleted file mode 100644
index dee9c30..0000000
--- a/javascript/signature/sign.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {PublicKeySign} from './internal/public_key_sign';
diff --git a/javascript/signature/sign_wrapper.ts b/javascript/signature/sign_wrapper.ts
deleted file mode 100644
index 9742208..0000000
--- a/javascript/signature/sign_wrapper.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {PublicKeySignWrapper} from './public_key_sign_wrapper';
-
-export function register() {
-  Registry.registerPrimitiveWrapper(new PublicKeySignWrapper());
-}
diff --git a/javascript/signature/signature_config.ts b/javascript/signature/signature_config.ts
deleted file mode 100644
index 7297e0e..0000000
--- a/javascript/signature/signature_config.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-
-import {EcdsaPrivateKeyManager} from './ecdsa_private_key_manager';
-import {EcdsaPublicKeyManager} from './ecdsa_public_key_manager';
-import {PublicKeySignWrapper} from './public_key_sign_wrapper';
-import {PublicKeyVerifyWrapper} from './public_key_verify_wrapper';
-
-// Static methods and constants for registering with the Registry all instances
-// of key types for digital signature supported in a particular release of Tink.
-// To register all key types from the current Tink release one can do:
-// SignatureConfig.register();
-
-/**
- * Registers key managers for all PublicKeyVerify and PublicKeySign key types
- * from the current Tink release.
- */
-export function register() {
-  Registry.registerKeyManager(new EcdsaPrivateKeyManager());
-  Registry.registerKeyManager(new EcdsaPublicKeyManager());
-  Registry.registerPrimitiveWrapper(new PublicKeySignWrapper());
-  Registry.registerPrimitiveWrapper(new PublicKeyVerifyWrapper());
-}
-
-export const VERIFY_PRIMITIVE_NAME: string = 'PublicKeyVerify';
-
-export const ECDSA_PUBLIC_KEY_TYPE: string = EcdsaPublicKeyManager.KEY_TYPE;
-
-export const SIGN_PRIMITIVE_NAME: string = 'PublicKeySign';
-
-export const ECDSA_PRIVATE_KEY_TYPE: string = EcdsaPrivateKeyManager.KEY_TYPE;
diff --git a/javascript/signature/signature_config_test.ts b/javascript/signature/signature_config_test.ts
deleted file mode 100644
index 7db4654..0000000
--- a/javascript/signature/signature_config_test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {KeysetHandle} from '../internal/keyset_handle';
-import {PbKeyData, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
-import * as Registry from '../internal/registry';
-import * as Random from '../subtle/random';
-
-import {EcdsaPrivateKeyManager} from './ecdsa_private_key_manager';
-import {EcdsaPublicKeyManager} from './ecdsa_public_key_manager';
-import {PublicKeySign} from './internal/public_key_sign';
-import {PublicKeyVerify} from './internal/public_key_verify';
-import * as SignatureConfig from './signature_config';
-import {SignatureKeyTemplates} from './signature_key_templates';
-
-describe('signature config test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('constants', function() {
-    expect(SignatureConfig.VERIFY_PRIMITIVE_NAME).toBe(VERIFY_PRIMITIVE_NAME);
-    expect(SignatureConfig.SIGN_PRIMITIVE_NAME).toBe(SIGN_PRIMITIVE_NAME);
-
-    expect(SignatureConfig.ECDSA_PUBLIC_KEY_TYPE).toBe(ECDSA_PUBLIC_KEY_TYPE);
-    expect(SignatureConfig.ECDSA_PRIVATE_KEY_TYPE).toBe(ECDSA_PRIVATE_KEY_TYPE);
-  });
-
-  it('register, correct key managers were registered', function() {
-    SignatureConfig.register();
-
-    // Test that the corresponding key managers were registered.
-    const publicKeyManager = Registry.getKeyManager(ECDSA_PUBLIC_KEY_TYPE);
-    expect(publicKeyManager instanceof EcdsaPublicKeyManager).toBe(true);
-
-    const privateKeyManager = Registry.getKeyManager(ECDSA_PRIVATE_KEY_TYPE);
-    expect(privateKeyManager instanceof EcdsaPrivateKeyManager).toBe(true);
-  });
-
-  // Check that everything was registered correctly and thus new keys may be
-  // generated using the predefined key templates and then they may be used for
-  // encryption and decryption.
-  it('register, predefined templates should work', async function() {
-    SignatureConfig.register();
-    let templates = [
-      SignatureKeyTemplates.ecdsaP256(),
-      SignatureKeyTemplates.ecdsaP256IeeeEncoding(),
-      SignatureKeyTemplates.ecdsaP384(),
-      SignatureKeyTemplates.ecdsaP384IeeeEncoding(),
-      SignatureKeyTemplates.ecdsaP521(),
-      SignatureKeyTemplates.ecdsaP521IeeeEncoding(),
-
-    ];
-    for (const template of templates) {
-      const privateKeyData = await Registry.newKeyData(template);
-      const privateKeysetHandle = createKeysetHandleFromKeyData(privateKeyData);
-      const publicKeySign =
-          await privateKeysetHandle.getPrimitive<PublicKeySign>(PublicKeySign);
-      const publicKeyData = Registry.getPublicKeyData(
-          privateKeyData.getTypeUrl(), privateKeyData.getValue_asU8());
-      const publicKeysetHandle = createKeysetHandleFromKeyData(publicKeyData);
-      const publicKeyVerify =
-          await publicKeysetHandle.getPrimitive<PublicKeyVerify>(
-              PublicKeyVerify);
-      const data = Random.randBytes(10);
-      const signature = await publicKeySign.sign(data);
-      const isValid = await publicKeyVerify.verify(signature, data);
-
-      expect(isValid).toBe(true);
-    }
-  });
-});
-
-// Constants used in tests.
-const VERIFY_PRIMITIVE_NAME = 'PublicKeyVerify';
-const SIGN_PRIMITIVE_NAME = 'PublicKeySign';
-const ECDSA_PUBLIC_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EcdsaPublicKey';
-const ECDSA_PRIVATE_KEY_TYPE =
-    'type.googleapis.com/google.crypto.tink.EcdsaPrivateKey';
-
-/**
- * Creates a keyset containing only the key given by keyData and returns it
- * wrapped in a KeysetHandle.
- */
-function createKeysetHandleFromKeyData(keyData: PbKeyData): KeysetHandle {
-  const keyId = 1;
-  const key = new PbKeysetKey()
-                  .setKeyData(keyData)
-                  .setStatus(PbKeyStatusType.ENABLED)
-                  .setKeyId(keyId)
-                  .setOutputPrefixType(PbOutputPrefixType.TINK);
-  const keyset = new PbKeyset();
-  keyset.addKey(key);
-  keyset.setPrimaryKeyId(keyId);
-  return new KeysetHandle(keyset);
-}
diff --git a/javascript/signature/signature_key_templates.ts b/javascript/signature/signature_key_templates.ts
deleted file mode 100644
index 2019bff..0000000
--- a/javascript/signature/signature_key_templates.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbEcdsaKeyFormat, PbEcdsaParams, PbEcdsaSignatureEncoding, PbEllipticCurveType, PbHashType, PbKeyTemplate, PbOutputPrefixType} from '../internal/proto';
-
-import * as SignatureConfig from './signature_config';
-
-/**
- * Pre-generated KeyTemplates for keys for digital signatures.
- *
- * One can use these templates to generate new Keyset with
- * KeysetHandle.generateNew method. To generate a new keyset that contains a
- * single EcdsaKey, one can do:
- *
- * SignatureConfig.Register();
- * KeysetHandle handle = KeysetHandle.generateNew(
- *     SignatureKeyTemplates.ecdsaP256());
- *
- * @final
- */
-export class SignatureKeyTemplates {
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EcdsaPrivateKey with the following parameters:
-   *  Hash function: SHA256
-   *  Curve: NIST P-256
-   *  Signature encoding: DER (this is the encoding that Java uses)
-   *  OutputPrefixType: TINK
-   *
-   */
-  static ecdsaP256(): PbKeyTemplate {
-    return createEcdsaKeyTemplate(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P256,
-        /* hashType = */
-        PbHashType.SHA256,
-        /* encoding = */
-        PbEcdsaSignatureEncoding.DER,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EcdsaPrivateKey with the following parameters:
-   *  Hash function: SHA512
-   *  Curve: NIST P-384
-   *  Signature encoding: DER (this is the encoding that Java uses)
-   *  OutputPrefixType: TINK
-   *
-   */
-  static ecdsaP384(): PbKeyTemplate {
-    return createEcdsaKeyTemplate(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P384,
-        /* hashType = */
-        PbHashType.SHA512,
-        /* encoding = */
-        PbEcdsaSignatureEncoding.DER,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EcdsaPrivateKey with the following parameters:
-   *  Hash function: SHA512
-   *  Curve: NIST P-521
-   *  Signature encoding: DER (this is the encoding that Java uses).
-   *  OutputPrefixType: TINK
-   *
-   */
-  static ecdsaP521(): PbKeyTemplate {
-    return createEcdsaKeyTemplate(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P521,
-        /* hashType = */
-        PbHashType.SHA512,
-        /* encoding = */
-        PbEcdsaSignatureEncoding.DER,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EcdsaPrivateKey with the following parameters:
-   *  Hash function: SHA256
-   *  Curve: NIST P-256
-   *  Signature encoding: IEEE_P1363 (this is the encoding that WebCrypto uses)
-   *  OutputPrefixType: TINK
-   *
-   */
-  static ecdsaP256IeeeEncoding(): PbKeyTemplate {
-    return createEcdsaKeyTemplate(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P256,
-        /* hashType = */
-        PbHashType.SHA256,
-        /* encoding = */
-        PbEcdsaSignatureEncoding.IEEE_P1363,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EcdsaPrivateKey with the following parameters:
-   *  Hash function: SHA512
-   *  Curve: NIST P-384
-   *  Signature encoding: IEEE_P1363 (this is the encoding that WebCrypto uses)
-   *  OutputPrefixType: TINK
-   *
-   */
-  static ecdsaP384IeeeEncoding(): PbKeyTemplate {
-    return createEcdsaKeyTemplate(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P384,
-        /* hashType = */
-        PbHashType.SHA512,
-        /* encoding = */
-        PbEcdsaSignatureEncoding.IEEE_P1363,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-
-  /**
-   * Returns a KeyTemplate that generates new instances of
-   * EcdsaPrivateKey with the following parameters:
-   *  Hash function: SHA512
-   *  Curve: NIST P-521
-   *  Signature encoding: IEEE_P1363 (this is the encoding that WebCrypto uses)
-   *  OutputPrefixType: TINK
-   *
-   */
-  static ecdsaP521IeeeEncoding(): PbKeyTemplate {
-    return createEcdsaKeyTemplate(
-        /* curveType = */
-        PbEllipticCurveType.NIST_P521,
-        /* hashType = */
-        PbHashType.SHA512,
-        /* encoding = */
-        PbEcdsaSignatureEncoding.IEEE_P1363,
-        /* outputPrefixType = */
-        PbOutputPrefixType.TINK);
-  }
-}
-
-function createEcdsaKeyTemplate(
-    curveType: PbEllipticCurveType, hashType: PbHashType,
-    encoding: PbEcdsaSignatureEncoding,
-    outputPrefixType: PbOutputPrefixType): PbKeyTemplate {
-  // key format
-  const keyFormat = new PbEcdsaKeyFormat();
-  const params = (new PbEcdsaParams())
-                     .setCurve(curveType)
-                     .setHashType(hashType)
-                     .setEncoding(encoding);
-  keyFormat.setParams(params);
-
-  // key template
-  const keyTemplate = (new PbKeyTemplate())
-                          .setTypeUrl(SignatureConfig.ECDSA_PRIVATE_KEY_TYPE)
-                          .setValue(keyFormat.serializeBinary())
-                          .setOutputPrefixType(outputPrefixType);
-  return keyTemplate;
-}
diff --git a/javascript/signature/signature_key_templates_test.ts b/javascript/signature/signature_key_templates_test.ts
deleted file mode 100644
index d4ddc9c..0000000
--- a/javascript/signature/signature_key_templates_test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbEcdsaKeyFormat, PbEcdsaSignatureEncoding, PbEllipticCurveType, PbHashType, PbOutputPrefixType} from '../internal/proto';
-
-import {EcdsaPrivateKeyManager} from './ecdsa_private_key_manager';
-import {SignatureKeyTemplates} from './signature_key_templates';
-
-describe('signature key templates test', function() {
-  it('ecdsa p256', function() {
-    // Expects function to create a key with following parameters.
-    const expectedCurve = PbEllipticCurveType.NIST_P256;
-    const expectedHashFunction = PbHashType.SHA256;
-    const expectedEncoding = PbEcdsaSignatureEncoding.DER;
-    const expectedOutputPrefix = PbOutputPrefixType.TINK;
-
-    // Expected type URL is the one supported by EcdsaPrivateKeyManager.
-    const manager = new EcdsaPrivateKeyManager();
-    const expectedTypeUrl = manager.getKeyType();
-
-    const keyTemplate = SignatureKeyTemplates.ecdsaP256();
-
-    expect(keyTemplate.getTypeUrl()).toBe(expectedTypeUrl);
-    expect(keyTemplate.getOutputPrefixType()).toBe(expectedOutputPrefix);
-
-    // Test values in key format.
-    const keyFormat =
-        PbEcdsaKeyFormat.deserializeBinary(keyTemplate.getValue_asU8());
-    const params = keyFormat.getParams();
-    expect(params!.getEncoding()).toBe(expectedEncoding);
-
-    // Test key params.
-    expect(params!.getCurve()).toBe(expectedCurve);
-    expect(params!.getHashType()).toBe(expectedHashFunction);
-
-    // Test that the template works with EcdsaPrivateKeyManager.
-    manager.getKeyFactory().newKey(keyTemplate.getValue_asU8());
-  });
-});
diff --git a/javascript/signature/subtle/BUILD.bazel b/javascript/signature/subtle/BUILD.bazel
deleted file mode 100644
index f7fd3a4..0000000
--- a/javascript/signature/subtle/BUILD.bazel
+++ /dev/null
@@ -1,12 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "subtle",
-    srcs = ["index.ts"],
-    module_name = "tink-crypto/signature/subtle",
-    deps = [
-        "//subtle",
-    ],
-)
diff --git a/javascript/signature/subtle/index.ts b/javascript/signature/subtle/index.ts
deleted file mode 100644
index c010dcb..0000000
--- a/javascript/signature/subtle/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {EcdsaSign, fromJsonWebKey as ecdsaSignFromJsonWebKey} from '../../subtle/ecdsa_sign';
-export {EcdsaSignatureEncodingType, exportCryptoKey, generateKeyPair, importPrivateKey, importPublicKey} from '../../subtle/elliptic_curves';
diff --git a/javascript/signature/verify.ts b/javascript/signature/verify.ts
deleted file mode 100644
index f7b332e..0000000
--- a/javascript/signature/verify.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export {PublicKeyVerify} from './internal/public_key_verify';
diff --git a/javascript/signature/verify_wrapper.ts b/javascript/signature/verify_wrapper.ts
deleted file mode 100644
index 21ea2d9..0000000
--- a/javascript/signature/verify_wrapper.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-import {PublicKeyVerifyWrapper} from './public_key_verify_wrapper';
-
-export function register() {
-  Registry.registerPrimitiveWrapper(new PublicKeyVerifyWrapper());
-}
diff --git a/javascript/subtle/BUILD.bazel b/javascript/subtle/BUILD.bazel
deleted file mode 100644
index d832c24..0000000
--- a/javascript/subtle/BUILD.bazel
+++ /dev/null
@@ -1,80 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "subtle",
-    srcs = [
-        "aes_ctr.ts",
-        "aes_gcm.ts",
-        "bytes.ts",
-        "ecdsa_sign.ts",
-        "ecdsa_verify.ts",
-        "ecies_aead_hkdf_dem_helper.ts",
-        "ecies_aead_hkdf_hybrid_decrypt.ts",
-        "ecies_aead_hkdf_hybrid_encrypt.ts",
-        "ecies_hkdf_kem_recipient.ts",
-        "ecies_hkdf_kem_sender.ts",
-        "elliptic_curves.ts",
-        "encrypt_then_authenticate.ts",
-        "hkdf.ts",
-        "hmac.ts",
-        "ind_cpa_cipher.ts",
-        "random.ts",
-        "validators.ts",
-    ],
-    deps = [
-        "//aead/internal",
-        "//exception",
-        "//hybrid/internal",
-        "//mac/internal",
-        "//signature/internal",
-    ],
-)
-
-ts_library(
-    name = "subtle_tests",
-    testonly = True,
-    srcs = [
-        "aes_ctr_test.ts",
-        "aes_gcm_test.ts",
-        "bytes_test.ts",
-        "ecdsa_sign_test.ts",
-        "ecdsa_verify_test.ts",
-        "ecies_aead_hkdf_hybrid_decrypt_test.ts",
-        "ecies_aead_hkdf_hybrid_encrypt_test.ts",
-        "ecies_hkdf_kem_recipient_test.ts",
-        "ecies_hkdf_kem_sender_test.ts",
-        "elliptic_curves_test.ts",
-        "encrypt_then_authenticate_test.ts",
-        "hkdf_test.ts",
-        "hmac_test.ts",
-        ":wycheproof_ecdh_test_vectors",
-        ":wycheproof_ecdsa_test_vectors",
-    ],
-    deps = [
-        ":subtle",
-        "//aead",
-        "//hybrid",
-        "//internal",
-        "//signature/internal",
-        "//testing/internal",
-        "@npm//@types/jasmine",
-    ],
-)
-
-genrule(
-    name = "wycheproof_ecdh_test_vectors",
-    testonly = True,
-    srcs = ["@wycheproof//testvectors:ecdh_webcrypto"],
-    outs = [":wycheproof_ecdh_test_vectors.ts"],
-    cmd = 'cat - $< <<<"export const WYCHEPROOF_ECDH_TEST_VECTORS =" >$@',
-)
-
-genrule(
-    name = "wycheproof_ecdsa_test_vectors",
-    testonly = True,
-    srcs = ["@wycheproof//testvectors:ecdsa_webcrypto"],
-    outs = [":wycheproof_ecdsa_test_vectors.ts"],
-    cmd = 'cat - $< <<<"export const WYCHEPROOF_ECDSA_TEST_VECTORS =" >$@',
-)
diff --git a/javascript/subtle/aes_ctr.ts b/javascript/subtle/aes_ctr.ts
deleted file mode 100644
index 324a681..0000000
--- a/javascript/subtle/aes_ctr.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-
-import * as Bytes from './bytes';
-import {IndCpaCipher} from './ind_cpa_cipher';
-import * as Random from './random';
-import * as Validators from './validators';
-
-/**
- * The minimum IV size.
- *
- */
-const MIN_IV_SIZE_IN_BYTES: number = 12;
-
-/**
- * AES block size.
- *
- */
-const AES_BLOCK_SIZE_IN_BYTES: number = 16;
-
-/**
- * Implementation of AES-CTR.
- *
- * @final
- */
-export class AesCtr implements IndCpaCipher {
-  /**
-   * @param ivSize the size of the IV
-   */
-  constructor(
-      private readonly key: CryptoKey, private readonly ivSize: number) {}
-
-  /**
-   */
-  async encrypt(plaintext: Uint8Array): Promise<Uint8Array> {
-    Validators.requireUint8Array(plaintext);
-    const iv = Random.randBytes(this.ivSize);
-    const counter = new Uint8Array(AES_BLOCK_SIZE_IN_BYTES);
-    counter.set(iv);
-    const alg = {'name': 'AES-CTR', 'counter': counter, 'length': 128};
-    const ciphertext =
-        await self.crypto.subtle.encrypt(alg, this.key, plaintext);
-    return Bytes.concat(iv, new Uint8Array(ciphertext));
-  }
-
-  /**
-   */
-  async decrypt(ciphertext: Uint8Array): Promise<Uint8Array> {
-    Validators.requireUint8Array(ciphertext);
-    if (ciphertext.length < this.ivSize) {
-      throw new SecurityException('ciphertext too short');
-    }
-    const counter = new Uint8Array(AES_BLOCK_SIZE_IN_BYTES);
-    counter.set(ciphertext.subarray(0, this.ivSize));
-    const alg = {'name': 'AES-CTR', 'counter': counter, 'length': 128};
-    return new Uint8Array(await self.crypto.subtle.decrypt(
-        alg, this.key, new Uint8Array(ciphertext.subarray(this.ivSize))));
-  }
-}
-
-/**
- * @param ivSize the size of the IV, must be larger than or equal to
- *     {@link MIN_IV_SIZE_IN_BYTES}
- */
-export async function fromRawKey(
-    key: Uint8Array, ivSize: number): Promise<IndCpaCipher> {
-  if (!Number.isInteger(ivSize)) {
-    throw new SecurityException('invalid IV length, must be an integer');
-  }
-  if (ivSize < MIN_IV_SIZE_IN_BYTES || ivSize > AES_BLOCK_SIZE_IN_BYTES) {
-    throw new SecurityException(
-        'invalid IV length, must be at least ' + MIN_IV_SIZE_IN_BYTES +
-        ' and at most ' + AES_BLOCK_SIZE_IN_BYTES);
-  }
-  Validators.requireUint8Array(key);
-  Validators.validateAesKeySize(key.length);
-  const cryptoKey = await self.crypto.subtle.importKey(
-      'raw', key, {'name': 'AES-CTR', 'length': key.length}, false,
-      ['encrypt', 'decrypt']);
-  return new AesCtr(cryptoKey, ivSize);
-}
diff --git a/javascript/subtle/aes_ctr_test.ts b/javascript/subtle/aes_ctr_test.ts
deleted file mode 100644
index 2bfbbb5..0000000
--- a/javascript/subtle/aes_ctr_test.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {fromRawKey as aesCtrFromRawKey} from './aes_ctr';
-import * as Bytes from './bytes';
-import * as Random from './random';
-
-describe('aes ctr test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the timeout.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('basic', async function() {
-    // Set longer time for promiseTimout as the test sometimes takes longer than
-    // 1 second in Firefox.
-    const key = Random.randBytes(16);
-    for (let i = 0; i < 100; i++) {
-      const msg = Random.randBytes(20);
-      const cipher = await aesCtrFromRawKey(key, 16);
-      let ciphertext = await cipher.encrypt(msg);
-      let plaintext = await cipher.decrypt(ciphertext);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-    }
-  });
-
-  it('probabilistic encryption', async function() {
-    const cipher = await aesCtrFromRawKey(Random.randBytes(16), 16);
-    const msg = Random.randBytes(20);
-    const results = new Set();
-    for (let i = 0; i < 100; i++) {
-      const ciphertext = await cipher.encrypt(msg);
-      results.add(Bytes.toHex(ciphertext));
-    }
-    expect(results.size).toBe(100);
-  });
-
-  it('constructor', async function() {
-    try {
-      await aesCtrFromRawKey(Random.randBytes(16), 11);  // IV size too short
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: invalid IV length, must be at least 12 and at most 16');
-    }
-    try {
-      await aesCtrFromRawKey(Random.randBytes(16), 17);  // IV size too long
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: invalid IV length, must be at least 12 and at most 16');
-    }
-    try {
-      await aesCtrFromRawKey(
-          Random.randBytes(24), 12);  // 192-bit keys not supported
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: unsupported AES key size: 24');
-    }
-  });
-
-  it('constructor, invalid iv sizes', async function() {
-    try {
-      await aesCtrFromRawKey(Random.randBytes(16), NaN);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: invalid IV length, must be an integer');
-    }
-
-    try {
-      await aesCtrFromRawKey(Random.randBytes(16), 12.5);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: invalid IV length, must be an integer');
-    }
-
-    try {
-      await aesCtrFromRawKey(Random.randBytes(16), 0);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: invalid IV length, must be at least 12 and at most 16');
-    }
-  });
-
-  it('with test vectors', async function() {
-    // Test data from NIST SP 800-38A pp 55.
-    const NIST_TEST_VECTORS = [
-      {
-        'key': '2b7e151628aed2a6abf7158809cf4f3c',
-        'message':
-            '6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51' +
-            '30c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710',
-        'ciphertext':
-            '874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff' +
-            '5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee',
-        'iv': 'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'
-      },
-    ];
-    for (let i = 0; i < NIST_TEST_VECTORS.length; i++) {
-      const testVector = NIST_TEST_VECTORS[i];
-      const key = Bytes.fromHex(testVector['key']);
-      const iv = Bytes.fromHex(testVector['iv']);
-      const msg = Bytes.fromHex(testVector['message']);
-      const ciphertext = Bytes.fromHex(testVector['ciphertext']);
-      const aesctr = await aesCtrFromRawKey(key, iv.length);
-      const plaintext = await aesctr.decrypt(Bytes.concat(iv, ciphertext));
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-    }
-  });
-});
diff --git a/javascript/subtle/aes_gcm.ts b/javascript/subtle/aes_gcm.ts
deleted file mode 100644
index ad4878a..0000000
--- a/javascript/subtle/aes_gcm.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead/internal/aead';
-import {SecurityException} from '../exception/security_exception';
-
-import * as Bytes from './bytes';
-import * as Random from './random';
-import * as Validators from './validators';
-
-/**
- * The only supported IV size.
- *
- */
-const IV_SIZE_IN_BYTES: number = 12;
-
-/**
- * The only supported tag size.
- *
- */
-const TAG_SIZE_IN_BITS: number = 128;
-
-/**
- * Implementation of AES-GCM.
- *
- * @final
- */
-export class AesGcm extends Aead {
-  constructor(private readonly key: CryptoKey) {
-    super();
-  }
-
-  /**
-   */
-  async encrypt(plaintext: Uint8Array, associatedData?: Uint8Array):
-      Promise<Uint8Array> {
-    Validators.requireUint8Array(plaintext);
-    if (associatedData != null) {
-      Validators.requireUint8Array(associatedData);
-    }
-    const iv = Random.randBytes(IV_SIZE_IN_BYTES);
-    const alg: AesGcmParams = {
-      'name': 'AES-GCM',
-      'iv': iv,
-      'tagLength': TAG_SIZE_IN_BITS
-    };
-    if (associatedData) {
-      alg['additionalData'] = associatedData;
-    }
-    const ciphertext =
-        await self.crypto.subtle.encrypt(alg, this.key, plaintext);
-    return Bytes.concat(iv, new Uint8Array(ciphertext));
-  }
-
-  /**
-   */
-  async decrypt(ciphertext: Uint8Array, associatedData?: Uint8Array):
-      Promise<Uint8Array> {
-    Validators.requireUint8Array(ciphertext);
-    if (ciphertext.length < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BITS / 8) {
-      throw new SecurityException('ciphertext too short');
-    }
-    if (associatedData != null) {
-      Validators.requireUint8Array(associatedData);
-    }
-    const iv = new Uint8Array(IV_SIZE_IN_BYTES);
-    iv.set(ciphertext.subarray(0, IV_SIZE_IN_BYTES));
-    const alg: AesGcmParams = {
-      'name': 'AES-GCM',
-      'iv': iv,
-      'tagLength': TAG_SIZE_IN_BITS
-    };
-    if (associatedData) {
-      alg['additionalData'] = associatedData;
-    }
-    try {
-      return new Uint8Array(await self.crypto.subtle.decrypt(
-          alg, this.key,
-          new Uint8Array(ciphertext.subarray(IV_SIZE_IN_BYTES))));
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      throw new SecurityException(e.toString());
-    }
-  }
-}
-
-export async function fromRawKey(key: Uint8Array): Promise<Aead> {
-  Validators.requireUint8Array(key);
-  Validators.validateAesKeySize(key.length);
-  const webCryptoKey = await self.crypto.subtle.importKey(
-      /* format */
-      'raw', key,
-      /* keyData */
-      {'name': 'AES-GCM', 'length': key.length},
-      /* algo */
-      false,
-      /* extractable*/
-      ['encrypt', 'decrypt']);
-
-  /* usage */
-  return new AesGcm(webCryptoKey);
-}
diff --git a/javascript/subtle/aes_gcm_test.ts b/javascript/subtle/aes_gcm_test.ts
deleted file mode 100644
index d35b42c..0000000
--- a/javascript/subtle/aes_gcm_test.ts
+++ /dev/null
@@ -1,629 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {fromRawKey as aesGcmFromRawKey} from './aes_gcm';
-import * as Bytes from './bytes';
-import * as Random from './random';
-
-/** Asserts that an exception is the result of a Web Crypto error. */
-function assertCryptoError(exception: unknown) {
-  const message = String(exception);
-  expect(message.startsWith('SecurityException: OperationError')).toBe(true);
-}
-
-describe('aes gcm test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the timeout.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('basic', async function() {
-    const aead = await aesGcmFromRawKey(Random.randBytes(16));
-    for (let i = 0; i < 100; i++) {
-      const msg = Random.randBytes(i);
-      let ciphertext = await aead.encrypt(msg);
-      let plaintext = await aead.decrypt(ciphertext);
-      expect(ciphertext.length).toBe(12 /* iv */ + msg.length + 16 /* tag */);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-
-      let aad = null;
-      ciphertext = await aead.encrypt(msg, aad);
-      plaintext = await aead.decrypt(ciphertext, aad);
-      expect(ciphertext.length).toBe(12 /* iv */ + msg.length + 16 /* tag */);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-
-      aad = Random.randBytes(20);
-      ciphertext = await aead.encrypt(msg, aad);
-      plaintext = await aead.decrypt(ciphertext, aad);
-      expect(ciphertext.length).toBe(12 /* iv */ + msg.length + 16 /* tag */);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-    }
-  });
-
-  it('probabilistic encryption', async function() {
-    const aead = await aesGcmFromRawKey(Random.randBytes(16));
-    const msg = Random.randBytes(20);
-    const aad = Random.randBytes(20);
-    const results = new Set();
-    for (let i = 0; i < 100; i++) {
-      const ciphertext = await aead.encrypt(msg, aad);
-      results.add(Bytes.toHex(ciphertext));
-    }
-    expect(results.size).toBe(100);
-  });
-
-  it('bit flip ciphertext', async function() {
-    const aead = await aesGcmFromRawKey(Random.randBytes(16));
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    for (let i = 0; i < ciphertext.length; i++) {
-      for (let j = 0; j < 8; j++) {
-        const c1 = new Uint8Array(ciphertext);
-        c1[i] = (c1[i] ^ (1 << j));
-        try {
-          await aead.decrypt(c1, aad);
-          fail('expected aead.decrypt to fail');
-        } catch (e) {
-          assertCryptoError(e);
-        }
-      }
-    }
-  });
-
-  it('bit flip aad', async function() {
-    const aead = await aesGcmFromRawKey(Random.randBytes(16));
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    for (let i = 0; i < aad.length; i++) {
-      for (let j = 0; j < 8; j++) {
-        const aad1 = new Uint8Array(aad);
-        aad1[i] = (aad1[i] ^ (1 << j));
-        try {
-          await aead.decrypt(ciphertext, aad1);
-          fail('expected aead.decrypt to fail');
-        } catch (e) {
-          assertCryptoError(e);
-        }
-      }
-    }
-  });
-
-  it('truncation', async function() {
-    const aead = await aesGcmFromRawKey(Random.randBytes(16));
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    for (let i = 1; i <= ciphertext.length; i++) {
-      const c1 = new Uint8Array(ciphertext.buffer, 0, ciphertext.length - i);
-      try {
-        await aead.decrypt(c1, aad);
-        fail('expected aead.decrypt to fail');
-        // Preserving old behavior when moving to
-        // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-        // tslint:disable-next-line:no-any
-      } catch (e: any) {
-        if (c1.length < 12 /* iv */ + 16 /* tag */) {
-          expect(e.toString()).toBe('SecurityException: ciphertext too short');
-        } else {
-          assertCryptoError(e);
-        }
-      }
-    }
-  });
-
-  it('with nist test vectors', async function() {
-    // Download from
-    // https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/CAVP-TESTING-BLOCK-CIPHER-MODES.
-    const NIST_TEST_VECTORS =
-        [
-          {
-            'Key':
-                'b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4',
-            'IV': '516c33929df5a3284ff463d7',
-            'PT': '',
-            'AAD': '',
-            'CT': '',
-            'Tag': 'bdc1ac884d332457a1d2664f168c76f0',
-          },
-          {
-            'Key':
-                '78dc4e0aaf52d935c3c01eea57428f00ca1fd475f5da86a49c8dd73d68c8e223',
-            'IV': 'd79cf22d504cc793c3fb6c8a',
-            'PT': '',
-            'AAD': 'b96baa8c1c75a671bfb2d08d06be5f36',
-            'CT': '',
-            'Tag': '3e5d486aa2e30b22e040b85723a06e76',
-          },
-          {
-            'Key':
-                '886cff5f3e6b8d0e1ad0a38fcdb26de97e8acbe79f6bed66959a598fa5047d65',
-            'IV': '3a8efa1cd74bbab5448f9945',
-            'PT': '',
-            'AAD': '519fee519d25c7a304d6c6aa1897ee1eb8c59655',
-            'CT': '',
-            'Tag': 'f6d47505ec96c98a42dc3ae719877b87',
-          },
-          {
-            'Key':
-                'f4069bb739d07d0cafdcbc609ca01597f985c43db63bbaaa0debbb04d384e49c',
-            'IV': 'd25ff30fdc3d464fe173e805',
-            'PT': '',
-            'AAD':
-                '3e1449c4837f0892f9d55127c75c4b25d69be334baf5f19394d2d8bb460cbf2120e14736d0f634aa792feca20e455f11',
-            'CT': '',
-            'Tag': '805ec2931c2181e5bfb74fa0a975f0cf',
-          },
-          {
-            'Key':
-                '03ccb7dbc7b8425465c2c3fc39ed0593929ffd02a45ff583bd89b79c6f646fe9',
-            'IV': 'fd119985533bd5520b301d12',
-            'PT': '',
-            'AAD':
-                '98e68c10bf4b5ae62d434928fc6405147c6301417303ef3a703dcfd2c0c339a4d0a89bd29fe61fecf1066ab06d7a5c31a48ffbfed22f749b17e9bd0dc1c6f8fbd6fd4587184db964d5456132106d782338c3f117ec05229b0899',
-            'CT': '',
-            'Tag': 'cf54e7141349b66f248154427810c87a',
-          },
-          {
-            'Key':
-                '31bdadd96698c204aa9ce1448ea94ae1fb4a9a0b3c9d773b51bb1822666b8f22',
-            'IV': '0d18e06c7c725ac9e362e1ce',
-            'PT': '2db5168e932556f8089a0622981d017d',
-            'AAD': '',
-            'CT': 'fa4362189661d163fcd6a56d8bf0405a',
-            'Tag': 'd636ac1bbedd5cc3ee727dc2ab4a9489',
-          },
-          {
-            'Key':
-                '92e11dcdaa866f5ce790fd24501f92509aacf4cb8b1339d50c9c1240935dd08b',
-            'IV': 'ac93a1a6145299bde902f21a',
-            'PT': '2d71bcfa914e4ac045b2aa60955fad24',
-            'AAD': '1e0889016f67601c8ebea4943bc23ad6',
-            'CT': '8995ae2e6df3dbf96fac7b7137bae67f',
-            'Tag': 'eca5aa77d51d4a0a14d9c51e1da474ab',
-          },
-          {
-            'Key':
-                '83688deb4af8007f9b713b47cfa6c73e35ea7a3aa4ecdb414dded03bf7a0fd3a',
-            'IV': '0b459724904e010a46901cf3',
-            'PT': '33d893a2114ce06fc15d55e454cf90c3',
-            'AAD': '794a14ccd178c8ebfd1379dc704c5e208f9d8424',
-            'CT': 'cc66bee423e3fcd4c0865715e9586696',
-            'Tag': '0fb291bd3dba94a1dfd8b286cfb97ac5',
-          },
-          {
-            'Key':
-                'e4fed339c7b0cd267305d11ab0d5c3273632e8872d35bdc367a1363438239a35',
-            'IV': '0365882cf75432cfd23cbd42',
-            'PT': 'fff39a087de39a03919fbd2f2fa5f513',
-            'AAD':
-                '8a97d2af5d41160ac2ff7dd8ba098e7aa4d618f0f455957d6a6d0801796747ba57c32dfbaaaf15176528fe3a0e4550c9',
-            'CT': '8d9e68f03f7e5f4a0ffaa7650d026d08',
-            'Tag': '3554542c478c0635285a61d1b51f6afa',
-          },
-          {
-            'Key':
-                '80d755e24d129e68a5259ec2cf618e39317074a83c8961d3768ceb2ed8d5c3d7',
-            'IV': '7598c07ba7b16cd12cf50813',
-            'PT': '5e7fd1298c4f15aa0f1c1e47217aa7a9',
-            'AAD':
-                '0e94f4c48fd0c9690c853ad2a5e197c5de262137b69ed0cdfa28d8d12413e4ffff15374e1cccb0423e8ed829a954a335ed705a272ad7f9abd1057c849bb0d54b768e9d79879ec552461cc04adb6ca0040c5dd5bc733d21a93702',
-            'CT': '5762a38cf3f2fdf3645d2f6696a7eead',
-            'Tag': '8a6708e69468915c5367573924fe1ae3',
-          },
-          {
-            'Key':
-                '82c4f12eeec3b2d3d157b0f992d292b237478d2cecc1d5f161389b97f999057a',
-            'IV': '7b40b20f5f397177990ef2d1',
-            'PT': '982a296ee1cd7086afad976945',
-            'AAD': '',
-            'CT': 'ec8e05a0471d6b43a59ca5335f',
-            'Tag': '113ddeafc62373cac2f5951bb9165249',
-          },
-          {
-            'Key':
-                'dad89d9be9bba138cdcf8752c45b579d7e27c3dbb40f53e771dd8cfd500aa2d5',
-            'IV': 'cfb2aec82cfa6c7d89ee72ff',
-            'PT': 'b526ba1050177d05b0f72f8d67',
-            'AAD': '6e43784a91851a77667a02198e28dc32',
-            'CT': '8b29e66e924ecae84f6d8f7d68',
-            'Tag': '1e365805c8f28b2ed8a5cadfd9079158',
-          },
-          {
-            'Key':
-                '69b458f2644af9020463b40ee503cdf083d693815e2659051ae0d039e606a970',
-            'IV': '8d1da8ab5f91ccd09205944b',
-            'PT': 'f3e0e09224256bf21a83a5de8d',
-            'AAD': '036ad5e5494ef817a8af2f5828784a4bfedd1653',
-            'CT': 'c0a62d77e6031bfdc6b13ae217',
-            'Tag': 'a794a9aaee48cd92e47761bf1baff0af',
-          },
-          {
-            'Key':
-                '5f671466378f470ba5f5160e2209f3d95a48b7e560625d5a08654414de23aee2',
-            'IV': '6b3c08a663d04132243dd96c',
-            'PT': 'c428592d9f8a7f107ec4d0df05',
-            'AAD':
-                '12965559c31d538f937bda6eee9c93b0387318dc5d9496fb1c3a0b9b978dbfebff2a5823974ee9d679834dbe59f7ec51',
-            'CT': '1d8d7fe4357080c817303ce19c',
-            'Tag': 'e88d6b566fdc7b4fd62106bd2eb806ec',
-          },
-          {
-            'Key':
-                'ff9506b4d46ba54128876fadfcc673a4c927c618ea7d95cfcaa508cbc8f7fc66',
-            'IV': '3742ad2208a0484345eee1be',
-            'PT': '7fd0d6cadc92cad27bb2d7d8c8',
-            'AAD':
-                'f1360a27fdc244be8739d85af6491c762a693aafe668c449515fdeeedb6a90aeee3891bbc8b69adc6a6426cb12fcdebc32c9f58c5259d128b91efa28620a3a9a0168b0ff5e76951cb41647ba4aa1f87fac0d97ac580e42cffc7e',
-            'CT': 'bdb8346b28eb4d7226493611a6',
-            'Tag': '7484d827b767647f44c7f94a39f8175c',
-          },
-          {
-            'Key':
-                '268ed1b5d7c9c7304f9cae5fc437b4cd3aebe2ec65f0d85c3918d3d3b5bba89b',
-            'IV': '9ed9d8180564e0e945f5e5d4',
-            'PT':
-                'fe29a40d8ebf57262bdb87191d01843f4ca4b2de97d88273154a0b7d9e2fdb80',
-            'AAD': '',
-            'CT':
-                '791a4a026f16f3a5ea06274bf02baab469860abde5e645f3dd473a5acddeecfc',
-            'Tag': '05b2b74db0662550435ef1900e136b15',
-          },
-          {
-            'Key':
-                '37ccdba1d929d6436c16bba5b5ff34deec88ed7df3d15d0f4ddf80c0c731ee1f',
-            'IV': '5c1b21c8998ed6299006d3f9',
-            'PT':
-                'ad4260e3cdc76bcc10c7b2c06b80b3be948258e5ef20c508a81f51e96a518388',
-            'AAD': '22ed235946235a85a45bc5fad7140bfa',
-            'CT':
-                '3b335f8b08d33ccdcad228a74700f1007542a4d1e7fc1ebe3f447fe71af29816',
-            'Tag': '1fbf49cc46f458bf6e88f6370975e6d4',
-          },
-          {
-            'Key':
-                '5853c020946b35f2c58ec427152b840420c40029636adcbb027471378cfdde0f',
-            'IV': 'eec313dd07cc1b3e6b068a47',
-            'PT':
-                'ce7458e56aef9061cb0c42ec2315565e6168f5a6249ffd31610b6d17ab64935e',
-            'AAD': '1389b522c24a774181700553f0246bbabdd38d6f',
-            'CT':
-                'eadc3b8766a77ded1a58cb727eca2a9790496c298654cda78febf0da16b6903b',
-            'Tag': '3d49a5b32fde7eafcce90079217ffb57',
-          },
-          {
-            'Key':
-                'dc776f0156c15d032623854b625c61868e5db84b7b6f9fbd3672f12f0025e0f6',
-            'IV': '67130951c4a57f6ae7f13241',
-            'PT':
-                '9378a727a5119595ad631b12a5a6bc8a91756ef09c8d6eaa2b718fe86876da20',
-            'AAD':
-                'fd0920faeb7b212932280a009bac969145e5c316cf3922622c3705c3457c4e9f124b2076994323fbcfb523f8ed16d241',
-            'CT':
-                '6d958c20870d401a3c1f7a0ac092c97774d451c09f7aae992a8841ff0ab9d60d',
-            'Tag': 'b876831b4ecd7242963b040aa45c4114',
-          },
-          {
-            'Key':
-                '26bf255bee60ef0f653769e7034db95b8c791752754e575c761059e9ee8dcf78',
-            'IV': 'cecd97ab07ce57c1612744f5',
-            'PT':
-                '96983917a036650763aca2b4e927d95ffc74339519ed40c4336dba91edfbf9ad',
-            'AAD':
-                'afebbe9f260f8c118e52b84d8880a34622675faef334cdb41be9385b7d059b79c0f8a432d25f8b71e781b177fce4d4c57ac5734543e85d7513f96382ff4b2d4b95b2f1fdbaf9e78bbd1db13a7dd26e8a4ac83a3e8ab42d1d545f',
-            'CT':
-                'e34b1540a769f7913331d66796e00bdc3ee0f258cf244eb7663375cc5ad6c658',
-            'Tag': '3841f02beb7a7fca7e578922d0a2f80c',
-          },
-          {
-            'Key':
-                '1fded32d5999de4a76e0f8082108823aef60417e1896cf4218a2fa90f632ec8a',
-            'IV': '1f3afa4711e9474f32e70462',
-            'PT':
-                '06b2c75853df9aeb17befd33cea81c630b0fc53667ff45199c629c8e15dce41e530aa792f796b8138eeab2e86c7b7bee1d40b0',
-            'AAD': '',
-            'CT':
-                '91fbd061ddc5a7fcc9513fcdfdc9c3a7c5d4d64cedf6a9c24ab8a77c36eefbf1c5dc00bc50121b96456c8cd8b6ff1f8b3e480f',
-            'Tag': '30096d340f3d5c42d82a6f475def23eb',
-          },
-          {
-            'Key':
-                '5fe01c4baf01cbe07796d5aaef6ec1f45193a98a223594ae4f0ef4952e82e330',
-            'IV': 'bd587321566c7f1a5dd8652d',
-            'PT':
-                '881dc6c7a5d4509f3c4bd2daab08f165ddc204489aa8134562a4eac3d0bcad7965847b102733bb63d1e5c598ece0c3e5dadddd',
-            'AAD': '9013617817dda947e135ee6dd3653382',
-            'CT':
-                '16e375b4973b339d3f746c1c5a568bc7526e909ddff1e19c95c94a6ccff210c9a4a40679de5760c396ac0e2ceb1234f9f5fe26',
-            'Tag': 'abd3d26d65a6275f7a4f56b422acab49',
-          },
-          {
-            'Key':
-                '24501ad384e473963d476edcfe08205237acfd49b5b8f33857f8114e863fec7f',
-            'IV': '9ff18563b978ec281b3f2794',
-            'PT':
-                '27f348f9cdc0c5bd5e66b1ccb63ad920ff2219d14e8d631b3872265cf117ee86757accb158bd9abb3868fdc0d0b074b5f01b2c',
-            'AAD': 'adb5ec720ccf9898500028bf34afccbcaca126ef',
-            'CT':
-                'eb7cb754c824e8d96f7c6d9b76c7d26fb874ffbf1d65c6f64a698d839b0b06145dae82057ad55994cf59ad7f67c0fa5e85fab8',
-            'Tag': 'bc95c532fecc594c36d1550286a7a3f0',
-          },
-          {
-            'Key':
-                '463b412911767d57a0b33969e674ffe7845d313b88c6fe312f3d724be68e1fca',
-            'IV': '611ce6f9a6880750de7da6cb',
-            'PT':
-                'e7d1dcf668e2876861940e012fe52a98dacbd78ab63c08842cc9801ea581682ad54af0c34d0d7f6f59e8ee0bf4900e0fd85042',
-            'AAD':
-                '0a682fbc6192e1b47a5e0868787ffdafe5a50cead3575849990cdd2ea9b3597749403efb4a56684f0c6bde352d4aeec5',
-            'CT':
-                '8886e196010cb3849d9c1a182abe1eeab0a5f3ca423c3669a4a8703c0f146e8e956fb122e0d721b869d2b6fcd4216d7d4d3758',
-            'Tag': '2469cecd70fd98fec9264f71df1aee9a',
-          },
-          {
-            'Key':
-                '148579a3cbca86d5520d66c0ec71ca5f7e41ba78e56dc6eebd566fed547fe691',
-            'IV': 'b08a5ea1927499c6ecbfd4e0',
-            'PT':
-                '9d0b15fdf1bd595f91f8b3abc0f7dec927dfd4799935a1795d9ce00c9b879434420fe42c275a7cd7b39d638fb81ca52b49dc41',
-            'AAD':
-                'e4f963f015ffbb99ee3349bbaf7e8e8e6c2a71c230a48f9d59860a29091d2747e01a5ca572347e247d25f56ba7ae8e05cde2be3c97931292c02370208ecd097ef692687fecf2f419d3200162a6480a57dad408a0dfeb492e2c5d',
-            'CT':
-                '2097e372950a5e9383c675e89eea1c314f999159f5611344b298cda45e62843716f215f82ee663919c64002a5c198d7878fd3f',
-            'Tag': 'adbecdb0d5c2224d804d2886ff9a5760',
-          },
-          {
-            'Key': '11754cd72aec309bf52f7687212e8957',
-            'IV': '3c819d9a9bed087615030b65',
-            'PT': '',
-            'AAD': '',
-            'CT': '',
-            'Tag': '250327c674aaf477aef2675748cf6971',
-          },
-          {
-            'Key': '77be63708971c4e240d1cb79e8d77feb',
-            'IV': 'e0e00f19fed7ba0136a797f3',
-            'PT': '',
-            'AAD': '7a43ec1d9c0a5a78a0b16533a6213cab',
-            'CT': '',
-            'Tag': '209fcc8d3675ed938e9c7166709dd946',
-          },
-          {
-            'Key': '2fb45e5b8f993a2bfebc4b15b533e0b4',
-            'IV': '5b05755f984d2b90f94b8027',
-            'PT': '',
-            'AAD': 'e85491b2202caf1d7dce03b97e09331c32473941',
-            'CT': '',
-            'Tag': 'c75b7832b2a2d9bd827412b6ef5769db',
-          },
-          {
-            'Key': '99e3e8793e686e571d8285c564f75e2b',
-            'IV': 'c2dd0ab868da6aa8ad9c0d23',
-            'PT': '',
-            'AAD':
-                'b668e42d4e444ca8b23cfdd95a9fedd5178aa521144890b093733cf5cf22526c5917ee476541809ac6867a8c399309fc',
-            'CT': '',
-            'Tag': '3f4fba100eaf1f34b0baadaae9995d85',
-          },
-          {
-            'Key': '20b5b6b854e187b058a84d57bc1538b6',
-            'IV': '94c1935afc061cbf254b936f',
-            'PT': '',
-            'AAD':
-                'ca418e71dbf810038174eaa3719b3fcb80531c7110ad9192d105eeaafa15b819ac005668752b344ed1b22faf77048baf03dbddb3b47d6b00e95c4f005e0cc9b7627ccafd3f21b3312aa8d91d3fa0893fe5bff7d44ca46f23afe0',
-            'CT': '',
-            'Tag': 'b37286ebaf4a54e0ffc2a1deafc9f6db',
-          },
-          {
-            'Key': '7fddb57453c241d03efbed3ac44e371c',
-            'IV': 'ee283a3fc75575e33efd4887',
-            'PT': 'd5de42b461646c255c87bd2962d3b9a2',
-            'AAD': '',
-            'CT': '2ccda4a5415cb91e135c2a0f78c9b2fd',
-            'Tag': 'b36d1df9b9d5e596f83e8b7f52971cb3',
-          },
-          {
-            'Key': 'c939cc13397c1d37de6ae0e1cb7c423c',
-            'IV': 'b3d8cc017cbb89b39e0f67e2',
-            'PT': 'c3b3c41f113a31b73d9a5cd432103069',
-            'AAD': '24825602bd12a984e0092d3e448eda5f',
-            'CT': '93fe7d9e9bfd10348a5606e5cafa7354',
-            'Tag': '0032a1dc85f1c9786925a2e71d8272dd',
-          },
-          {
-            'Key': 'd4a22488f8dd1d5c6c19a7d6ca17964c',
-            'IV': 'f3d5837f22ac1a0425e0d1d5',
-            'PT': '7b43016a16896497fb457be6d2a54122',
-            'AAD': 'f1c5d424b83f96c6ad8cb28ca0d20e475e023b5a',
-            'CT': 'c2bd67eef5e95cac27e3b06e3031d0a8',
-            'Tag': 'f23eacf9d1cdf8737726c58648826e9c',
-          },
-          {
-            'Key': '89850dd398e1f1e28443a33d40162664',
-            'IV': 'e462c58482fe8264aeeb7231',
-            'PT': '2805cdefb3ef6cc35cd1f169f98da81a',
-            'AAD':
-                'd74e99d1bdaa712864eec422ac507bddbe2b0d4633cd3dff29ce5059b49fe868526c59a2a3a604457bc2afea866e7606',
-            'CT': 'ba80e244b7fc9025cd031d0f63677e06',
-            'Tag': 'd84a8c3eac57d1bb0e890a8f461d1065',
-          },
-          {
-            'Key': 'bd7c5c63b7542b56a00ebe71336a1588',
-            'IV': '87721f23ba9c3c8ea5571abc',
-            'PT': 'de15ddbb1e202161e8a79af6a55ac6f3',
-            'AAD':
-                'a6ec8075a0d3370eb7598918f3b93e48444751624997b899a87fa6a9939f844e008aa8b70e9f4c3b1a19d3286bf543e7127bfecba1ad17a5ec53fccc26faecacc4c75369498eaa7d706aef634d0009279b11e4ba6c993e5e9ed9',
-            'CT': '41eb28c0fee4d762de972361c863bc80',
-            'Tag': '9cb567220d0b252eb97bff46e4b00ff8',
-          },
-          {
-            'Key': 'fe9bb47deb3a61e423c2231841cfd1fb',
-            'IV': '4d328eb776f500a2f7fb47aa',
-            'PT': 'f1cc3818e421876bb6b8bbd6c9',
-            'AAD': '',
-            'CT': 'b88c5c1977b35b517b0aeae967',
-            'Tag': '43fd4727fe5cdb4b5b42818dea7ef8c9',
-          },
-          {
-            'Key': 'dfefde23c6122bf0370ab5890e804b73',
-            'IV': '92d6a8029990670f16de79e2',
-            'PT': '64260a8c287de978e96c7521d0',
-            'AAD': 'a2b16d78251de6c191ce350e5c5ef242',
-            'CT': 'bf78de948a847c173649d4b4d0',
-            'Tag': '9da3829968cdc50794d1c30d41cd4515',
-          },
-          {
-            'Key': 'fe0121f42e599f88ff02a985403e19bb',
-            'IV': '3bb9eb7724cbe1943d43de21',
-            'PT': 'fd331ca8646091c29f21e5f0a1',
-            'AAD': '2662d895035b6519f3510eae0faa3900ad23cfdf',
-            'CT': '59fe29b07b0de8d869efbbd9b4',
-            'Tag': 'd24c3e9c1c73c0af1097e26061c857de',
-          },
-          {
-            'Key': 'cbd3b8dbfcfb11ce345706e6cd73881a',
-            'IV': 'dc62bb68d0ec9a5d759d6741',
-            'PT': '85f83bf598dfd55bc8bfde2a64',
-            'AAD':
-                '0944b661fe6294f3c92abb087ec1b259b032dc4e0c5f28681cbe6e63c2178f474326f35ad3ca80c28e3485e7e5b252c8',
-            'CT': '206f6b3bb032dfecd39f8340b1',
-            'Tag': '425a21b2ea90580c889134032b914bb5',
-          },
-          {
-            'Key': 'e5b1e7a94e9e1fda0873571eec713429',
-            'IV': '5ddde829a81713346af8e5b7',
-            'PT': '850069e5ed768b5dc9ed7ad485',
-            'AAD':
-                'b0ce75da427fba93da6d3455b2b440a877599a6d8d6d2d66ee90b5cf9a33baaa8329a9ffaac290e8e33f2af2548c2a8a181b3d4d9f8fac860cc26b0d26b9cc53bc9f405afa73605ebeb376f2d1d7fcb065bab92f20f295556ade',
-            'CT': 'c211d9079d5562659db01e17d1',
-            'Tag': '884893fb035d3d7237d47c363de62bb3',
-          },
-          {
-            'Key': '9971071059abc009e4f2bd69869db338',
-            'IV': '07a9a95ea3821e9c13c63251',
-            'PT':
-                'f54bc3501fed4f6f6dfb5ea80106df0bd836e6826225b75c0222f6e859b35983',
-            'AAD': '',
-            'CT':
-                '0556c159f84ef36cb1602b4526b12009c775611bffb64dc0d9ca9297cd2c6a01',
-            'Tag': '7870d9117f54811a346970f1de090c41',
-          },
-          {
-            'Key': '298efa1ccf29cf62ae6824bfc19557fc',
-            'IV': '6f58a93fe1d207fae4ed2f6d',
-            'PT':
-                'cc38bccd6bc536ad919b1395f5d63801f99f8068d65ca5ac63872daf16b93901',
-            'AAD': '021fafd238463973ffe80256e5b1c6b1',
-            'CT':
-                'dfce4e9cd291103d7fe4e63351d9e79d3dfd391e3267104658212da96521b7db',
-            'Tag': '542465ef599316f73a7a560509a2d9f2',
-          },
-          {
-            'Key': 'fedc7155192d00b23cdd98750db9ebba',
-            'IV': 'a76b74f55c1a1756a08338b1',
-            'PT':
-                '6831435b8857daf1c513b148820d13b5a72cc490bda79a98a6f520d8763c39d1',
-            'AAD': '2ad206c4176e7e552aa08836886816fafa77e759',
-            'CT':
-                '15823805da89a1923bfc1d6f87784d56bad1128b4dffdbdeefbb2fa562c35e68',
-            'Tag': 'd23dc455ced49887c717e8eabeec2984',
-          },
-          {
-            'Key': '48b7f337cdf9252687ecc760bd8ec184',
-            'IV': '3e894ebb16ce82a53c3e05b2',
-            'PT':
-                'bb2bac67a4709430c39c2eb9acfabc0d456c80d30aa1734e57997d548a8f0603',
-            'AAD':
-                '7d924cfd37b3d046a96eb5e132042405c8731e06509787bbeb41f258275746495e884d69871f77634c584bb007312234',
-            'CT':
-                'd263228b8ce051f67e9baf1ce7df97d10cd5f3bc972362055130c7d13c3ab2e7',
-            'Tag': '71446737ca1fa92e6d026d7d2ed1aa9c',
-          },
-          {
-            'Key': '8fbf7ca12fd525dde91e625873fe51c2',
-            'IV': '200bea517b9790a1cfadaf5e',
-            'PT':
-                '39d3e6277c4b4963840d1642e6faae0a5be2da97f61c4e55bb57ce021903d4c4',
-            'AAD':
-                'a414c07fe2e60bec9ccc409e9e899c6fe60580bb2607c861f7f08523e69cda1b9c3a711d1d9c35091771e4c950b9996d0ad04f2e00d1b3105853542a96e09ffffc2ec80f8cf88728f594f0aeb14f98a688234e8bfbf70327b364',
-            'CT':
-                'fe678ef76f69ac95db553b6dadd5a07a9dc8e151fe6a9fa3a1cd621636b87868',
-            'Tag': '7c860774f88332b9a7ce6bbd0272a727',
-          },
-          {
-            'Key': '594157ec4693202b030f33798b07176d',
-            'IV': '49b12054082660803a1df3df',
-            'PT':
-                '3feef98a976a1bd634f364ac428bb59cd51fb159ec1789946918dbd50ea6c9d594a3a31a5269b0da6936c29d063a5fa2cc8a1c',
-            'AAD': '',
-            'CT':
-                'c1b7a46a335f23d65b8db4008a49796906e225474f4fe7d39e55bf2efd97fd82d4167de082ae30fa01e465a601235d8d68bc69',
-            'Tag': 'ba92d3661ce8b04687e8788d55417dc2',
-          },
-          {
-            'Key': 'b61553bb854895b929751cd0c5f80384',
-            'IV': '8863f999ae64e55d0bbd7457',
-            'PT':
-                '9b1b113217d0c4ea7943cf123c69c6ad2e3c97368c51c9754145d155dde1ee8640c8cafff17a5c9737d26a137eee4bf369096d',
-            'AAD': 'd914b5f2d1b08ce53ea59cb310587245',
-            'CT':
-                'acfab4632b8a25805112f13d85e082bc89dc49bd92164fa8a2dad242c3a1b2f2696f2fdff579025f3f146ea97da3e47dc34b65',
-            'Tag': '5d9b5f4a9868c1c69cbd6fd851f01340',
-          },
-          {
-            'Key': 'fe47fcce5fc32665d2ae399e4eec72ba',
-            'IV': '5adb9609dbaeb58cbd6e7275',
-            'PT':
-                '7c0e88c88899a779228465074797cd4c2e1498d259b54390b85e3eef1c02df60e743f1b840382c4bccaf3bafb4ca8429bea063',
-            'AAD': '88319d6e1d3ffa5f987199166c8a9b56c2aeba5a',
-            'CT':
-                '98f4826f05a265e6dd2be82db241c0fbbbf9ffb1c173aa83964b7cf5393043736365253ddbc5db8778371495da76d269e5db3e',
-            'Tag': '291ef1982e4defedaa2249f898556b47',
-          },
-          {
-            'Key': '3c50622868f450aa0928990c15e1eb36',
-            'IV': '811d5290768d57e7d87bb6c7',
-            'PT':
-                'edd0a8f82833e919740fe2bf9edecf4ac86c72dc89490cef7b6983aaaf99fc856c5cc87d63f98a7c861bf3271fea6da86a15ab',
-            'AAD':
-                'dae2c7e0a3d3fd2bc04eca19b15178a003b5cf84890c28c2a615f20f8adb427f70698c12b2ef87780c1193fbb8cd1674',
-            'CT':
-                'a51425b0608d3b4b46d4ec05ca1ddaf02bdd2089ae0554ecfb2a1c84c63d82dc71ddb9ab1b1f0b49de2ad27c2b5173e7000aa6',
-            'Tag': 'bd9b5efca48008cd973a4f7d2c723844',
-          },
-          {
-            'Key': '2c1f21cf0f6fb3661943155c3e3d8492',
-            'IV': '23cb5ff362e22426984d1907',
-            'PT':
-                '42f758836986954db44bf37c6ef5e4ac0adaf38f27252a1b82d02ea949c8a1a2dbc0d68b5615ba7c1220ff6510e259f06655d8',
-            'AAD':
-                '5d3624879d35e46849953e45a32a624d6a6c536ed9857c613b572b0333e701557a713e3f010ecdf9a6bd6c9e3e44b065208645aff4aabee611b391528514170084ccf587177f4488f33cfb5e979e42b6e1cfc0a60238982a7aec',
-            'CT':
-                '81824f0e0d523db30d3da369fdc0d60894c7a0a20646dd015073ad2732bd989b14a222b6ad57af43e1895df9dca2a5344a62cc',
-            'Tag': '57a3ee28136e94c74838997ae9823f3a',
-          },
-        ];
-    for (let i = 0; i < NIST_TEST_VECTORS.length; i++) {
-      const testVector = NIST_TEST_VECTORS[i];
-      const aead = await aesGcmFromRawKey(Bytes.fromHex(testVector['Key']));
-      const ciphertext = Bytes.fromHex(
-          testVector['IV'] + testVector['CT'] + testVector['Tag']);
-      const aad = Bytes.fromHex(testVector['AAD']);
-      try {
-        const plaintext = await aead.decrypt(ciphertext, aad);
-        expect(testVector['PT']).toBe(Bytes.toHex(plaintext));
-      } catch (e) {
-        fail(e);
-      }
-    }
-  });
-});
diff --git a/javascript/subtle/bytes.ts b/javascript/subtle/bytes.ts
deleted file mode 100644
index 0832551..0000000
--- a/javascript/subtle/bytes.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-
-/**
- * Does near constant time byte array comparison.
- * @param ba1 The first bytearray to check.
- * @param ba2 The second bytearray to check.
- * @return If the array are equal.
- */
-export function isEqual(ba1: Uint8Array, ba2: Uint8Array): boolean {
-  if (ba1.length !== ba2.length) {
-    return false;
-  }
-  let result = 0;
-  for (let i = 0; i < ba1.length; i++) {
-    result |= ba1[i] ^ ba2[i];
-  }
-  return result == 0;
-}
-
-/**
- * Returns a new array that is the result of joining the arguments.
- */
-export function concat(...var_args: Uint8Array[]): Uint8Array {
-  let length = 0;
-  for (let i = 0; i < arguments.length; i++) {
-    length += arguments[i].length;
-  }
-  const result = new Uint8Array(length);
-  let curOffset = 0;
-  for (let i = 0; i < arguments.length; i++) {
-    result.set(arguments[i], curOffset);
-    curOffset += arguments[i].length;
-  }
-  return result;
-}
-
-/**
- * Converts a non-negative integer number to a 64-bit big-endian byte array.
- * @param value The number to convert.
- * @return The number as a big-endian byte array.
- * @throws {InvalidArgumentsException}
- * @static
- */
-export function fromNumber(value: number): Uint8Array {
-  if (Number.isNaN(value) || value % 1 !== 0) {
-    throw new InvalidArgumentsException('cannot convert non-integer value');
-  }
-  if (value < 0) {
-    throw new InvalidArgumentsException('cannot convert negative number');
-  }
-  if (value > Number.MAX_SAFE_INTEGER) {
-    throw new InvalidArgumentsException(
-        'cannot convert number larger than ' + Number.MAX_SAFE_INTEGER);
-  }
-  const twoPower32 = 2 ** 32;
-  let low = value % twoPower32;
-  let high = value / twoPower32;
-  const result = new Uint8Array(8);
-  for (let i = 7; i >= 4; i--) {
-    result[i] = low & 255;
-    low >>>= 8;
-  }
-  for (let i = 3; i >= 0; i--) {
-    result[i] = high & 255;
-    high >>>= 8;
-  }
-  return result;
-}
-
-/**
- * Converts the hex string to a byte array.
- *
- * @param hex the input
- * @return the byte array output
- * @throws {!InvalidArgumentsException}
- * @static
- */
-export function fromHex(hex: string): Uint8Array {
-  if (hex.length % 2 != 0) {
-    throw new InvalidArgumentsException(
-        'Hex string length must be multiple of 2');
-  }
-  const arr = new Uint8Array(hex.length / 2);
-  for (let i = 0; i < hex.length; i += 2) {
-    arr[i / 2] = parseInt(hex.substring(i, i + 2), 16);
-  }
-  return arr;
-}
-
-/**
- * Converts a byte array to hex.
- *
- * @param bytes the byte array input
- * @return hex the output
- * @static
- */
-export function toHex(bytes: Uint8Array): string {
-  let result = '';
-  for (let i = 0; i < bytes.length; i++) {
-    const hexByte = bytes[i].toString(16);
-    result += hexByte.length > 1 ? hexByte : '0' + hexByte;
-  }
-  return result;
-}
-
-/**
- * Converts the Base64 string to a byte array.
- *
- * @param encoded the base64 string
- * @param opt_webSafe True indicates we should use the alternative
- *     alphabet, which does not require escaping for use in URLs.
- * @return the byte array output
- * @static
- */
-export function fromBase64(encoded: string, opt_webSafe?: boolean): Uint8Array {
-  if (opt_webSafe) {
-    const normalBase64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
-    return fromByteString(window.atob(normalBase64));
-  }
-  return fromByteString(window.atob(encoded));
-}
-
-/**
- * Base64 encode a byte array.
- *
- * @param bytes the byte array input
- * @param opt_webSafe True indicates we should use the alternative
- *     alphabet, which does not require escaping for use in URLs.
- * @return base64 output
- * @static
- */
-export function toBase64(bytes: Uint8Array, opt_webSafe?: boolean): string {
-  const encoded = window
-                      .btoa(
-                          /* padding */
-                          toByteString(bytes))
-                      .replace(/=/g, '');
-  if (opt_webSafe) {
-    return encoded.replace(/\+/g, '-').replace(/\//g, '_');
-  }
-  return encoded;
-}
-
-/**
- * Converts a byte string to a byte array. Only support ASCII and Latin-1
- * strings, does not support multi-byte characters.
- *
- * @param str the input
- * @return the byte array output
- * @static
- */
-export function fromByteString(str: string): Uint8Array {
-  const output = [];
-  let p = 0;
-  for (let i = 0; i < str.length; i++) {
-    const c = str.charCodeAt(i);
-    output[p++] = c;
-  }
-  return new Uint8Array(output);
-}
-
-/**
- * Turns a byte array into the string given by the concatenation of the
- * characters to which the numbers correspond. Each byte is corresponding to a
- * character. Does not support multi-byte characters.
- *
- * @param bytes Array of numbers representing
- *     characters.
- * @return Stringification of the array.
- */
-export function toByteString(bytes: Uint8Array): string {
-  let str = '';
-  for (let i = 0; i < bytes.length; i += 1) {
-    str += String.fromCharCode(bytes[i]);
-  }
-  return str;
-}
diff --git a/javascript/subtle/bytes_test.ts b/javascript/subtle/bytes_test.ts
deleted file mode 100644
index c5979cc..0000000
--- a/javascript/subtle/bytes_test.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Bytes from './bytes';
-import * as Random from './random';
-
-describe('bytes test', function() {
-  it('concat', function() {
-    let ba1 = new Uint8Array(0);
-    let ba2 = new Uint8Array(0);
-    let ba3 = new Uint8Array(0);
-    let result = Bytes.concat(ba1, ba2, ba3);
-    expect(result.length).toBe(0);
-
-    ba1 = Random.randBytes(10);
-    result = Bytes.concat(ba1, ba2, ba3);
-    expect(result.length).toBe(ba1.length);
-    expect(Bytes.toHex(result)).toBe(Bytes.toHex(ba1));
-
-    result = Bytes.concat(ba2, ba1, ba3);
-    expect(result.length).toBe(ba1.length);
-    expect(Bytes.toHex(result)).toBe(Bytes.toHex(ba1));
-
-    result = Bytes.concat(ba3, ba2, ba1);
-    expect(result.length).toBe(ba1.length);
-    expect(Bytes.toHex(result)).toBe(Bytes.toHex(ba1));
-
-    ba2 = Random.randBytes(11);
-    result = Bytes.concat(ba1, ba2, ba3);
-    expect(result.length).toBe(ba1.length + ba2.length);
-    expect(Bytes.toHex(result)).toBe(Bytes.toHex(ba1) + Bytes.toHex(ba2));
-
-    result = Bytes.concat(ba1, ba3, ba2);
-    expect(result.length).toBe(ba1.length + ba2.length);
-    expect(Bytes.toHex(result)).toBe(Bytes.toHex(ba1) + Bytes.toHex(ba2));
-
-    result = Bytes.concat(ba3, ba1, ba2);
-    expect(result.length).toBe(ba1.length + ba2.length);
-    expect(Bytes.toHex(result)).toBe(Bytes.toHex(ba1) + Bytes.toHex(ba2));
-
-    ba3 = Random.randBytes(12);
-    result = Bytes.concat(ba1, ba2, ba3);
-    expect(result.length).toBe(ba1.length + ba2.length + ba3.length);
-    expect(Bytes.toHex(result))
-        .toBe(Bytes.toHex(ba1) + Bytes.toHex(ba2) + Bytes.toHex(ba3));
-  });
-
-  it('from number', function() {
-    let number = 0;
-    expect(Array.from(Bytes.fromNumber(number))).toEqual([
-      0, 0, 0, 0, 0, 0, 0, 0
-    ]);
-    number = 1;
-    expect(Array.from(Bytes.fromNumber(number))).toEqual([
-      0, 0, 0, 0, 0, 0, 0, 1
-    ]);
-    number = 4294967296;  // 2^32
-    expect(Array.from(Bytes.fromNumber(number))).toEqual([
-      0, 0, 0, 1, 0, 0, 0, 0
-    ]);
-    number = 4294967297;  // 2^32 + 1
-    expect(Array.from(Bytes.fromNumber(number))).toEqual([
-      0, 0, 0, 1, 0, 0, 0, 1
-    ]);
-    number = Number.MAX_SAFE_INTEGER; // 2^53 - 1
-    expect(Array.from(Bytes.fromNumber(number))).toEqual([
-      0, 31, 255, 255, 255, 255, 255, 255
-    ]);
-
-    expect(function() {
-      Bytes.fromNumber(3.14);
-    }).toThrow();
-    expect(function() {
-      Bytes.fromNumber(-1);
-    }).toThrow();
-    expect(function() {
-      Bytes.fromNumber(Number.MAX_SAFE_INTEGER + 1);
-    }).toThrow();
-  });
-
-  it('to base64, remove all padding', function() {
-    for (let i = 0; i < 10; i++) {
-      const array = new Uint8Array(i);
-      const base64Representation = Bytes.toBase64(array, true);
-      expect(base64Representation[base64Representation.length - 1])
-          .not.toBe('=');
-    }
-  });
-
-  it('to base64, from base64', function() {
-    for (let i = 0; i < 100; i++) {
-      const array = Random.randBytes(i);
-      const base64Representation = Bytes.toBase64(array, true);
-      const arrayRepresentation = Bytes.fromBase64(base64Representation, true);
-      expect(arrayRepresentation).toEqual(array);
-    }
-  });
-
-  it('from byte string', function() {
-    expect(Bytes.fromByteString(''))
-        .withContext('empty string')
-        .toEqual(new Uint8Array(0));
-
-    let arr = new Uint8Array(
-        [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100]);
-    expect(Bytes.fromByteString('Hello, world'))
-        .withContext('ASCII')
-        .toEqual(arr);
-
-    arr = new Uint8Array([83, 99, 104, 246, 110]);
-    expect(Bytes.fromByteString('Sch\u00f6n'))
-        .withContext('Latin')
-        .toEqual(arr);
-  });
-
-  it('to byte string', function() {
-    expect(Bytes.toByteString(new Uint8Array(0)))
-        .withContext('empty string')
-        .toBe('');
-
-    let arr = new Uint8Array(
-        [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100]);
-    expect(Bytes.toByteString(arr)).withContext('ASCII').toBe('Hello, world');
-
-    arr = new Uint8Array([83, 99, 104, 246, 110]);
-    expect(Bytes.toByteString(arr)).withContext('Latin').toBe('Sch\u00f6n');
-  });
-
-  it('to string, from string', function() {
-    for (let i = 0; i < 100; i++) {
-      const array = Random.randBytes(i);
-      const str = Bytes.toByteString(array);
-      const arrayRepresentation = Bytes.fromByteString(str);
-      expect(arrayRepresentation).toEqual(array);
-    }
-  });
-});
diff --git a/javascript/subtle/ecdsa_sign.ts b/javascript/subtle/ecdsa_sign.ts
deleted file mode 100644
index c10b1c6..0000000
--- a/javascript/subtle/ecdsa_sign.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {PublicKeySign} from '../signature/internal/public_key_sign';
-
-import * as EllipticCurves from './elliptic_curves';
-import * as Validators from './validators';
-
-/**
- * Implementation of ECDSA signing.
- *
- * @final
- */
-export class EcdsaSign extends PublicKeySign {
-  private readonly encoding: EllipticCurves.EcdsaSignatureEncodingType;
-
-  /**
-   * @param opt_encoding The
-   *     optional encoding of the signature. If absent, default is IEEE P1363.
-   */
-  constructor(
-      private readonly key: CryptoKey, private readonly hash: string,
-      opt_encoding?: EllipticCurves.EcdsaSignatureEncodingType|null) {
-    super();
-    if (!opt_encoding) {
-      opt_encoding = EllipticCurves.EcdsaSignatureEncodingType.IEEE_P1363;
-    }
-    this.encoding = opt_encoding;
-  }
-
-  /**
-   */
-  async sign(message: Uint8Array): Promise<Uint8Array> {
-    Validators.requireUint8Array(message);
-    const signature = await window.crypto.subtle.sign(
-        {name: 'ECDSA', hash: {name: this.hash}}, this.key, message);
-    if (this.encoding === EllipticCurves.EcdsaSignatureEncodingType.DER) {
-      return EllipticCurves.ecdsaIeee2Der(new Uint8Array(signature));
-    }
-    return new Uint8Array(signature);
-  }
-}
-
-/**
- * @param opt_encoding The
- *     optional encoding of the signature. If absent, default is IEEE P1363.
- */
-export async function fromJsonWebKey(
-    jwk: JsonWebKey, hash: string,
-    opt_encoding?: EllipticCurves.EcdsaSignatureEncodingType|
-    null): Promise<PublicKeySign> {
-  if (!jwk) {
-    throw new SecurityException('private key has to be non-null');
-  }
-  const {crv} = jwk;
-  if (!crv) {
-    throw new SecurityException('curve has to be defined');
-  }
-  Validators.validateEcdsaParams(crv, hash);
-  const cryptoKey = await EllipticCurves.importPrivateKey('ECDSA', jwk);
-  return new EcdsaSign(cryptoKey, hash, opt_encoding);
-}
diff --git a/javascript/subtle/ecdsa_sign_test.ts b/javascript/subtle/ecdsa_sign_test.ts
deleted file mode 100644
index 7629bc8..0000000
--- a/javascript/subtle/ecdsa_sign_test.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {fromJsonWebKey} from './ecdsa_sign';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-
-describe('ecdsa sign test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('sign', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-    for (let i = 0; i < 100; i++) {
-      const data = Random.randBytes(i);
-      const signature = await signer.sign(data);
-      const isValid = await window.crypto.subtle.verify(
-          {
-            name: 'ECDSA',
-            hash: {
-              name: 'SHA-256',
-            },
-          },
-          keyPair.publicKey!, signature, data);
-      expect(isValid).toBe(true);
-    }
-  });
-
-  it('sign with der encoding', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256',
-        EllipticCurves.EcdsaSignatureEncodingType.DER);
-    for (let i = 0; i < 100; i++) {
-      const data = Random.randBytes(i);
-      let signature = await signer.sign(data);
-      // Should fail WebCrypto only accepts IEEE encoding.
-      let isValid = await window.crypto.subtle.verify(
-          {
-            name: 'ECDSA',
-            hash: {
-              name: 'SHA-256',
-            },
-          },
-          keyPair.publicKey!, signature, data);
-      expect(isValid).toBe(false);
-      // Convert the signature to IEEE encoding.
-      signature = EllipticCurves.ecdsaDer2Ieee(signature, 64);
-      isValid = await window.crypto.subtle.verify(
-          {
-            name: 'ECDSA',
-            hash: {
-              name: 'SHA-256',
-            },
-          },
-          keyPair.publicKey!, signature, data);
-      expect(isValid).toBe(true);
-    }
-  });
-
-  it('sign always generate new signatures', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-    const signatures = new Set();
-    for (let i = 0; i < 100; i++) {
-      const data = Random.randBytes(i);
-      const signature = await signer.sign(data);
-      signatures.add(signature);
-    }
-    expect(signatures.size).toBe(100);
-  });
-
-  it('constructor with invalid hash', async function() {
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-      await fromJsonWebKey(
-          await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-1');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-256 (because curve is P-256) but ' +
-              'got SHA-1');
-    }
-
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-384');
-      await fromJsonWebKey(
-          await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-384 or SHA-512 (because curve is P-384) but got SHA-256');
-    }
-
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-521');
-      await fromJsonWebKey(
-          await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-512 (because curve is P-521) but got SHA-256');
-    }
-  });
-
-  it('constructor with invalid curve', async function() {
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-      const jwk = await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-      jwk.crv = 'blah';
-      await fromJsonWebKey(jwk, 'SHA-256');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe('SecurityException: unsupported curve: blah');
-    }
-  });
-});
diff --git a/javascript/subtle/ecdsa_verify.ts b/javascript/subtle/ecdsa_verify.ts
deleted file mode 100644
index 65531d3..0000000
--- a/javascript/subtle/ecdsa_verify.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {PublicKeyVerify} from '../signature/internal/public_key_verify';
-
-import * as EllipticCurves from './elliptic_curves';
-import * as Validators from './validators';
-
-/**
- * Implementation of ECDSA verifying.
- *
- * @final
- */
-export class EcdsaVerify extends PublicKeyVerify {
-  private readonly ieeeSignatureLength: number;
-
-  /**
-   * @param encoding The
-   *     encoding of the signature.
-   */
-  constructor(
-      private readonly key: CryptoKey, private readonly hash: string,
-      private readonly encoding: EllipticCurves.EcdsaSignatureEncodingType) {
-    super();
-    const {namedCurve}: Partial<EcKeyAlgorithm> = key.algorithm;
-    if (!namedCurve) {
-      throw new SecurityException('Curve has to be defined.');
-    }
-    this.ieeeSignatureLength = 2 *
-        EllipticCurves.fieldSizeInBytes(
-            EllipticCurves.curveFromString(namedCurve));
-  }
-
-  /**
-   */
-  async verify(signature: Uint8Array, message: Uint8Array): Promise<boolean> {
-    Validators.requireUint8Array(signature);
-    Validators.requireUint8Array(message);
-    if (this.encoding === EllipticCurves.EcdsaSignatureEncodingType.DER) {
-      signature =
-          EllipticCurves.ecdsaDer2Ieee(signature, this.ieeeSignatureLength);
-    }
-    return window.crypto.subtle.verify(
-        {name: 'ECDSA', hash: {name: this.hash}}, this.key, signature, message);
-  }
-}
-
-/**
- * @param opt_encoding The
- *     optional encoding of the signature. If absent, default is IEEE P1363.
- */
-export async function fromJsonWebKey(
-    jwk: JsonWebKey, hash: string,
-    encoding: EllipticCurves.EcdsaSignatureEncodingType =
-        EllipticCurves.EcdsaSignatureEncodingType.IEEE_P1363):
-    Promise<PublicKeyVerify> {
-  if (!jwk) {
-    throw new SecurityException('public key has to be non-null');
-  }
-  const {crv} = jwk;
-  if (!crv) {
-    throw new SecurityException('curve has to be defined');
-  }
-  Validators.validateEcdsaParams(crv, hash);
-  const cryptoKey = await EllipticCurves.importPublicKey('ECDSA', jwk);
-  return new EcdsaVerify(cryptoKey, hash, encoding);
-}
diff --git a/javascript/subtle/ecdsa_verify_test.ts b/javascript/subtle/ecdsa_verify_test.ts
deleted file mode 100644
index 72e569d..0000000
--- a/javascript/subtle/ecdsa_verify_test.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PublicKeyVerify} from '../signature/internal/public_key_verify';
-
-import * as Bytes from './bytes';
-import * as ecdsaSign from './ecdsa_sign';
-import * as ecdsaVerify from './ecdsa_verify';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-import * as Validators from './validators';
-import {WYCHEPROOF_ECDSA_TEST_VECTORS} from './wycheproof_ecdsa_test_vectors';
-
-describe('ecdsa verify test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('verify', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await ecdsaSign.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-    const verifier = await ecdsaVerify.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
-    for (let i = 0; i < 100; i++) {
-      const data = Random.randBytes(i);
-      const signature = await signer.sign(data);
-      expect(await verifier.verify(signature, data)).toBe(true);
-    }
-  });
-
-  it('verify with der encoding', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await ecdsaSign.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256',
-        EllipticCurves.EcdsaSignatureEncodingType.DER);
-    const verifier = await ecdsaVerify.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
-    const verifierDer = await ecdsaVerify.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256',
-        EllipticCurves.EcdsaSignatureEncodingType.DER);
-    for (let i = 0; i < 100; i++) {
-      const data = Random.randBytes(i);
-      const signature = await signer.sign(data);
-      expect(await verifier.verify(signature, data)).toBe(false);
-      expect(await verifierDer.verify(signature, data)).toBe(true);
-    }
-  });
-
-  it('constructor with invalid hash', async function() {
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-      await ecdsaVerify.fromJsonWebKey(
-          await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-1');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-256 (because curve is P-256) but got SHA-1');
-    }
-
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-384');
-      await ecdsaVerify.fromJsonWebKey(
-          await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-384 or SHA-512 (because curve is P-384) but got SHA-256');
-    }
-
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-521');
-      await ecdsaVerify.fromJsonWebKey(
-          await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'SecurityException: expected SHA-512 (because curve is P-521) but got SHA-256');
-    }
-  });
-
-  it('constructor with invalid curve', async function() {
-    try {
-      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-      const jwk = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-      jwk.crv = 'blah';
-      await ecdsaVerify.fromJsonWebKey(jwk, 'SHA-256');
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe('SecurityException: unsupported curve: blah');
-    }
-  });
-
-  it('verify modified signature', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await ecdsaSign.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-    const verifier = await ecdsaVerify.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
-    const data = Random.randBytes(20);
-    const signature = await signer.sign(data);
-
-    for (let i = 0; i < signature.length; i++) {
-      for (let j = 0; j < 8; j++) {
-        const s1 = new Uint8Array(signature);
-        s1[i] = (s1[i] ^ (1 << j));
-        expect(await verifier.verify(s1, data)).toBe(false);
-      }
-    }
-  });
-
-  it('verify modified data', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
-    const signer = await ecdsaSign.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
-    const verifier = await ecdsaVerify.fromJsonWebKey(
-        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
-    const data = Random.randBytes(20);
-    const signature = await signer.sign(data);
-
-    for (let i = 0; i < data.length; i++) {
-      for (let j = 0; j < 8; j++) {
-        const data1 = new Uint8Array(data);
-        data1[i] = (data1[i] ^ (1 << j));
-        expect(await verifier.verify(signature, data1)).toBe(false);
-      }
-    }
-  });
-
-  it('wycheproof', async function() {
-    for (const testGroup of WYCHEPROOF_ECDSA_TEST_VECTORS['testGroups']) {
-      try {
-        Validators.validateEcdsaParams(
-            testGroup['jwk']['crv'], testGroup['sha']);
-      } catch (e) {
-        // Tink does not support this config.
-        continue;
-      }
-      const verifier =
-          await ecdsaVerify.fromJsonWebKey(testGroup['jwk'], testGroup['sha']);
-      let errors = '';
-      for (const test of testGroup['tests']) {
-        errors += await runWycheproofTest(verifier, test);
-      }
-      if (errors !== '') {
-        fail(errors);
-      }
-    }
-  });
-});
-
-/**
- * Runs the test with test vector given as an input and returns either empty
- * string or a text describing the failure.
- */
-async function runWycheproofTest(
-    verifier: PublicKeyVerify,
-    test: {'tcId': number, 'msg': string, 'sig': string, 'result': string}):
-    Promise<string> {
-  try {
-    const sig = Bytes.fromHex(test['sig']);
-    const msg = Bytes.fromHex(test['msg']);
-    const isValid = await verifier.verify(sig, msg);
-    if (isValid) {
-      if (test['result'] === 'invalid') {
-        return 'invalid signature accepted on test ' + test['tcId'] + '\n';
-      }
-    } else {
-      if (test['result'] === 'valid') {
-        return 'valid signature rejected on test ' + test['tcId'] + '\n';
-      }
-    }
-  } catch (e) {
-    if (test['result'] === 'valid') {
-      return 'valid signature rejected on test ' + test['tcId'] +
-          ': unexpected exception "' + String(e) + '".\n';
-    }
-  }
-  // If the test passes return an empty string.
-  return '';
-}
diff --git a/javascript/subtle/ecies_aead_hkdf_dem_helper.ts b/javascript/subtle/ecies_aead_hkdf_dem_helper.ts
deleted file mode 100644
index 709a60d..0000000
--- a/javascript/subtle/ecies_aead_hkdf_dem_helper.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead/internal/aead';
-
-/**
- * A helper for DEM (data encapsulation mechanism) of ECIES-AEAD-HKDF.
- */
-export interface EciesAeadHkdfDemHelper {
-  /**
-   * @return the size of the DEM key in bytes
-   */
-  getDemKeySizeInBytes(): number;
-
-  /**
-   * Creates a new `Aead` primitive that uses the key material given in
-   * `demKey`, which must be of length `getDemKeySizeInBytes()`.
-   *
-   * @param demKey the DEM key.
-   * @return the newly created `Aead` primitive.
-   */
-  getAead(demKey: Uint8Array): Promise<Aead>;
-}
diff --git a/javascript/subtle/ecies_aead_hkdf_hybrid_decrypt.ts b/javascript/subtle/ecies_aead_hkdf_hybrid_decrypt.ts
deleted file mode 100644
index a353081..0000000
--- a/javascript/subtle/ecies_aead_hkdf_hybrid_decrypt.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead/internal/aead';
-import {SecurityException} from '../exception/security_exception';
-import {HybridDecrypt} from '../hybrid/internal/hybrid_decrypt';
-
-import {EciesAeadHkdfDemHelper} from './ecies_aead_hkdf_dem_helper';
-import {EciesHkdfKemRecipient, fromJsonWebKey as kemRecipientFromJsonWebKey} from './ecies_hkdf_kem_recipient';
-import * as EllipticCurves from './elliptic_curves';
-
-/**
- * Implementation of ECIES AEAD HKDF hybrid decryption.
- *
- * @final
- */
-export class EciesAeadHkdfHybridDecrypt extends HybridDecrypt {
-  private readonly kemRecipient_: EciesHkdfKemRecipient;
-  private readonly hkdfHash_: string;
-  private readonly pointFormat_: EllipticCurves.PointFormatType;
-  private readonly demHelper_: EciesAeadHkdfDemHelper;
-  private readonly headerSize: number;
-  private readonly hkdfSalt: Uint8Array|undefined;
-
-  /**
-   * @param hkdfHash the name of the HMAC algorithm, accepted names
-   *     are: SHA-1, SHA-256 and SHA-512.
-   */
-  constructor(
-      recipientPrivateKey: JsonWebKey, kemRecipient: EciesHkdfKemRecipient,
-      hkdfHash: string, pointFormat: EllipticCurves.PointFormatType,
-      demHelper: EciesAeadHkdfDemHelper, opt_hkdfSalt?: Uint8Array) {
-    super();
-    if (!recipientPrivateKey) {
-      throw new SecurityException('Recipient private key has to be non-null.');
-    }
-    if (!kemRecipient) {
-      throw new SecurityException('KEM recipient has to be non-null.');
-    }
-    if (!hkdfHash) {
-      throw new SecurityException('HKDF hash algorithm has to be non-null.');
-    }
-    if (!pointFormat) {
-      throw new SecurityException('Point format has to be non-null.');
-    }
-    if (!demHelper) {
-      throw new SecurityException('DEM helper has to be non-null.');
-    }
-    const {crv} = recipientPrivateKey;
-    if (!crv) {
-      throw new SecurityException('Curve has to be defined.');
-    }
-    const curveType = EllipticCurves.curveFromString(crv);
-    const headerSize =
-        EllipticCurves.encodingSizeInBytes(curveType, pointFormat);
-    this.kemRecipient_ = kemRecipient;
-    this.hkdfHash_ = hkdfHash;
-    this.pointFormat_ = pointFormat;
-    this.demHelper_ = demHelper;
-    this.headerSize = headerSize;
-    this.hkdfSalt = opt_hkdfSalt;
-  }
-
-  /**
-   * Decrypts ciphertext using opt_contextInfo as info parameter of the
-   * underlying HKDF.
-   *
-   */
-  async decrypt(ciphertext: Uint8Array, associatedData?: Uint8Array) {
-    if (ciphertext.length < this.headerSize) {
-      throw new SecurityException('Ciphertext is too short.');
-    }
-
-    // Split the ciphertext to KEM token and AEAD ciphertext.
-    const kemToken = ciphertext.slice(0, this.headerSize);
-    const ciphertextBody = ciphertext.slice(this.headerSize, ciphertext.length);
-    const aead = await this.getAead(kemToken, associatedData);
-    return aead.decrypt(ciphertextBody);
-  }
-
-  private async getAead(
-      kemToken: Uint8Array, opt_contextInfo?: Uint8Array|null): Promise<Aead> {
-    // Variable hkdfInfo is not optional for decapsulate method. Thus it should
-    // be an empty array in case that it is not defined by the caller of decrypt
-    // method.
-    if (!opt_contextInfo) {
-      opt_contextInfo = new Uint8Array(0);
-    }
-    const symmetricKey = await this.kemRecipient_.decapsulate(
-        kemToken, this.demHelper_.getDemKeySizeInBytes(), this.pointFormat_,
-        this.hkdfHash_, opt_contextInfo, this.hkdfSalt);
-    return this.demHelper_.getAead(symmetricKey);
-  }
-}
-
-/**
- * @param hkdfHash the name of the HMAC algorithm, accepted names
- *     are: SHA-1, SHA-256 and SHA-512.
- */
-export async function fromJsonWebKey(
-    recipientPrivateKey: JsonWebKey, hkdfHash: string,
-    pointFormat: EllipticCurves.PointFormatType,
-    demHelper: EciesAeadHkdfDemHelper,
-    opt_hkdfSalt?: Uint8Array): Promise<HybridDecrypt> {
-  if (!recipientPrivateKey) {
-    throw new SecurityException('Recipient private key has to be non-null.');
-  }
-  if (!hkdfHash) {
-    throw new SecurityException('HKDF hash algorithm has to be non-null.');
-  }
-  if (!pointFormat) {
-    throw new SecurityException('Point format has to be non-null.');
-  }
-  if (!demHelper) {
-    throw new SecurityException('DEM helper has to be non-null.');
-  }
-  if (!recipientPrivateKey) {
-    throw new SecurityException('Recipient private key has to be non-null.');
-  }
-  const kemRecipient = await kemRecipientFromJsonWebKey(recipientPrivateKey);
-  return new EciesAeadHkdfHybridDecrypt(
-      recipientPrivateKey, kemRecipient, hkdfHash, pointFormat, demHelper,
-      opt_hkdfSalt);
-}
diff --git a/javascript/subtle/ecies_aead_hkdf_hybrid_decrypt_test.ts b/javascript/subtle/ecies_aead_hkdf_hybrid_decrypt_test.ts
deleted file mode 100644
index 92c5663..0000000
--- a/javascript/subtle/ecies_aead_hkdf_hybrid_decrypt_test.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {RegistryEciesAeadHkdfDemHelper as DemHelper} from '../hybrid/registry_ecies_aead_hkdf_dem_helper';
-import * as Registry from '../internal/registry';
-
-import {fromJsonWebKey as decrypterFromJsonWebKey} from './ecies_aead_hkdf_hybrid_decrypt';
-import {fromJsonWebKey as encrypterFromJsonWebKey} from './ecies_aead_hkdf_hybrid_encrypt';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-
-describe('ecies aead hkdf hybrid decrypt test', function() {
-  beforeEach(function() {
-    AeadConfig.register();
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('new instance, should work', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const privateKey =
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    const hkdfSalt = new Uint8Array(0);
-    const hkdfHash = 'SHA-256';
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const demHelper = new DemHelper(AeadKeyTemplates.aes128CtrHmacSha256());
-
-    await decrypterFromJsonWebKey(
-        privateKey, hkdfHash, pointFormat, demHelper, hkdfSalt);
-  });
-
-  it('decrypt, short ciphertext, should not work', async function() {
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const demHelper = new DemHelper(AeadKeyTemplates.aes128CtrHmacSha256());
-    const hkdfHash = 'SHA-512';
-    const curve = EllipticCurves.CurveType.P256;
-
-    const curveName = EllipticCurves.curveToString(curve);
-    const curveEncodingSize =
-        EllipticCurves.encodingSizeInBytes(curve, pointFormat);
-
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
-    const privateKey =
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-
-    const hybridEncrypt = await encrypterFromJsonWebKey(
-        publicKey, hkdfHash, pointFormat, demHelper);
-    const hybridDecrypt = await decrypterFromJsonWebKey(
-        privateKey, hkdfHash, pointFormat, demHelper);
-
-    const plaintext = Random.randBytes(10);
-    const ciphertext = await hybridEncrypt.encrypt(plaintext);
-    try {
-      await hybridDecrypt.decrypt(ciphertext.slice(0, curveEncodingSize - 1));
-      fail('Should throw an exception');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe('SecurityException: Ciphertext is too short.');
-    }
-  });
-
-  it('decrypt, different dem helpers from one template, should work',
-     async function() {
-       const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-       const privateKey =
-           await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-       const publicKey =
-           await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-       const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-       const hkdfHash = 'SHA-256';
-       const keyTemplate = AeadKeyTemplates.aes256CtrHmacSha256();
-
-       const demHelperEncrypt = new DemHelper(keyTemplate);
-       const hybridEncrypt = await encrypterFromJsonWebKey(
-           publicKey, hkdfHash, pointFormat, demHelperEncrypt);
-
-       const demHelperDecrypt = new DemHelper(keyTemplate);
-       const hybridDecrypt = await decrypterFromJsonWebKey(
-           privateKey, hkdfHash, pointFormat, demHelperDecrypt);
-
-       const plaintext = Random.randBytes(15);
-
-       const ciphertext = await hybridEncrypt.encrypt(plaintext);
-       const decryptedCipher = await hybridDecrypt.decrypt(ciphertext);
-       expect(decryptedCipher).toEqual(plaintext);
-     });
-
-  it('decrypt, different pamarameters, should work', async function() {
-    const repetitions = 5;
-    const hkdfSalt = new Uint8Array(0);
-
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const hmacAlgorithms = ['SHA-1', 'SHA-256', 'SHA-512'];
-    const demHelper = new DemHelper(AeadKeyTemplates.aes256CtrHmacSha256());
-    const curves = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-
-    // Test the encryption for different HMAC algorithms and different types of
-    // curves.
-    for (const hkdfHash of hmacAlgorithms) {
-      for (const curve of curves) {
-        const curveName = EllipticCurves.curveToString(curve);
-        const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
-        const privateKey =
-            await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-        const publicKey =
-            await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-
-        const hybridEncrypt = await encrypterFromJsonWebKey(
-            publicKey, hkdfHash, pointFormat, demHelper, hkdfSalt);
-        const hybridDecrypt = await decrypterFromJsonWebKey(
-            privateKey, hkdfHash, pointFormat, demHelper, hkdfSalt);
-
-        for (let i = 0; i < repetitions; ++i) {
-          const plaintext = Random.randBytes(15);
-          const contextInfo = Random.randBytes(i);
-          const ciphertext =
-              await hybridEncrypt.encrypt(plaintext, contextInfo);
-          const decryptedCiphertext =
-              await hybridDecrypt.decrypt(ciphertext, contextInfo);
-
-          expect(decryptedCiphertext).toEqual(plaintext);
-        }
-      }
-    }
-  });
-});
diff --git a/javascript/subtle/ecies_aead_hkdf_hybrid_encrypt.ts b/javascript/subtle/ecies_aead_hkdf_hybrid_encrypt.ts
deleted file mode 100644
index e0e8235..0000000
--- a/javascript/subtle/ecies_aead_hkdf_hybrid_encrypt.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-import {HybridEncrypt} from '../hybrid/internal/hybrid_encrypt';
-
-import * as Bytes from './bytes';
-import {EciesAeadHkdfDemHelper} from './ecies_aead_hkdf_dem_helper';
-import * as sender from './ecies_hkdf_kem_sender';
-import * as EllipticCurves from './elliptic_curves';
-
-/**
- * Implementation of ECIES AEAD HKDF hybrid encryption.
- *
- * @final
- */
-export class EciesAeadHkdfHybridEncrypt extends HybridEncrypt {
-  private readonly kemSender_: sender.EciesHkdfKemSender;
-  private readonly hkdfHash_: string;
-  private readonly pointFormat_: EllipticCurves.PointFormatType;
-  private readonly demHelper_: EciesAeadHkdfDemHelper;
-  private readonly hkdfSalt: Uint8Array|undefined;
-
-  /**
-   * @param hkdfHash the name of the HMAC algorithm, accepted names
-   *     are: SHA-1, SHA-256 and SHA-512.
-   */
-  constructor(
-      kemSender: sender.EciesHkdfKemSender, hkdfHash: string,
-      pointFormat: EllipticCurves.PointFormatType,
-      demHelper: EciesAeadHkdfDemHelper, opt_hkdfSalt?: Uint8Array) {
-    super();
-    // TODO(thaidn): do we actually need these null checks?
-    if (!kemSender) {
-      throw new SecurityException('KEM sender has to be non-null.');
-    }
-    if (!hkdfHash) {
-      throw new SecurityException('HMAC algorithm has to be non-null.');
-    }
-    if (!pointFormat) {
-      throw new SecurityException('Point format has to be non-null.');
-    }
-    if (!demHelper) {
-      throw new SecurityException('DEM helper has to be non-null.');
-    }
-    this.kemSender_ = kemSender;
-    this.hkdfHash_ = hkdfHash;
-    this.pointFormat_ = pointFormat;
-    this.demHelper_ = demHelper;
-    this.hkdfSalt = opt_hkdfSalt;
-  }
-
-  /**
-   * Encrypts plaintext using opt_contextInfo as info parameter of the
-   * underlying HKDF.
-   *
-   */
-  async encrypt(
-      plaintext: Uint8Array,
-      associatedData: Uint8Array = new Uint8Array(0)): Promise<Uint8Array> {
-    const keySizeInBytes = this.demHelper_.getDemKeySizeInBytes();
-    const kemKey = await this.kemSender_.encapsulate(
-        keySizeInBytes, this.pointFormat_, this.hkdfHash_, associatedData,
-        this.hkdfSalt);
-    const aead = await this.demHelper_.getAead(kemKey['key']);
-    const ciphertextBody = await aead.encrypt(plaintext);
-    const header = kemKey['token'];
-    return Bytes.concat(header, ciphertextBody);
-  }
-}
-
-/**
- * @param hkdfHash the name of the HMAC algorithm, accepted names
- *     are: SHA-1, SHA-256 and SHA-512.
- */
-export async function fromJsonWebKey(
-    recipientPublicKey: JsonWebKey, hkdfHash: string,
-    pointFormat: EllipticCurves.PointFormatType,
-    demHelper: EciesAeadHkdfDemHelper,
-    opt_hkdfSalt?: Uint8Array): Promise<HybridEncrypt> {
-  if (!recipientPublicKey) {
-    throw new SecurityException('Recipient public key has to be non-null.');
-  }
-  if (!hkdfHash) {
-    throw new SecurityException('HMAC algorithm has to be non-null.');
-  }
-  if (!pointFormat) {
-    throw new SecurityException('Point format has to be non-null.');
-  }
-  if (!demHelper) {
-    throw new SecurityException('DEM helper has to be non-null.');
-  }
-  const kemSender = await sender.fromJsonWebKey(recipientPublicKey);
-  return new EciesAeadHkdfHybridEncrypt(
-      kemSender, hkdfHash, pointFormat, demHelper, opt_hkdfSalt);
-}
diff --git a/javascript/subtle/ecies_aead_hkdf_hybrid_encrypt_test.ts b/javascript/subtle/ecies_aead_hkdf_hybrid_encrypt_test.ts
deleted file mode 100644
index e7f87da..0000000
--- a/javascript/subtle/ecies_aead_hkdf_hybrid_encrypt_test.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {AeadConfig} from '../aead/aead_config';
-import {AeadKeyTemplates} from '../aead/aead_key_templates';
-import {RegistryEciesAeadHkdfDemHelper} from '../hybrid/registry_ecies_aead_hkdf_dem_helper';
-import * as Registry from '../internal/registry';
-
-import {fromJsonWebKey} from './ecies_aead_hkdf_hybrid_encrypt';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-
-describe('ecies aead hkdf hybrid encrypt test', function() {
-  beforeEach(function() {
-    AeadConfig.register();
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    Registry.reset();
-    // Reset the timeout.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('new instance, should work', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const hkdfHash = 'SHA-256';
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const demHelper = new RegistryEciesAeadHkdfDemHelper(
-        AeadKeyTemplates.aes128CtrHmacSha256());
-
-    await fromJsonWebKey(publicKey, hkdfHash, pointFormat, demHelper);
-  });
-
-  it('encrypt, different arguments', async function() {
-    const hkdfSalt = new Uint8Array(0);
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const demHelper = new RegistryEciesAeadHkdfDemHelper(
-        AeadKeyTemplates.aes256CtrHmacSha256());
-    const hmacAlgorithms = ['SHA-1', 'SHA-256', 'SHA-512'];
-
-    // Test the encryption for different HMAC algorithms and different types of
-    // curves.
-    for (const hkdfHash of hmacAlgorithms) {
-      for (const curve
-               of [EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-                   EllipticCurves.CurveType.P521]) {
-        const curveName = EllipticCurves.curveToString(curve);
-        const keyPair =
-            await EllipticCurves.generateKeyPair('ECDH', curveName!);
-        const publicKey =
-            await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-
-        const hybridEncrypt = await fromJsonWebKey(
-            publicKey, hkdfHash, pointFormat, demHelper, hkdfSalt);
-
-        const plaintext = Random.randBytes(15);
-        const ciphertext = await hybridEncrypt.encrypt(plaintext);
-
-        expect(ciphertext).not.toEqual(plaintext);
-      }
-    }
-  });
-});
diff --git a/javascript/subtle/ecies_hkdf_kem_recipient.ts b/javascript/subtle/ecies_hkdf_kem_recipient.ts
deleted file mode 100644
index 2fdbb26..0000000
--- a/javascript/subtle/ecies_hkdf_kem_recipient.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-
-import * as Bytes from './bytes';
-import * as EllipticCurves from './elliptic_curves';
-import * as Hkdf from './hkdf';
-
-/**
- * HKDF-based ECIES-KEM (key encapsulation mechanism) for ECIES recipient.
- */
-export class EciesHkdfKemRecipient {
-  private readonly privateKey_: CryptoKey;
-
-  constructor(privateKey: CryptoKey) {
-    if (!privateKey) {
-      throw new SecurityException('Private key has to be non-null.');
-    }
-
-    // CryptoKey should have the properties type and algorithm.
-    if (privateKey.type !== 'private' || !privateKey.algorithm) {
-      throw new SecurityException('Expected crypto key of type: private.');
-    }
-    this.privateKey_ = privateKey;
-  }
-
-  /**
-   * @param kemToken the public ephemeral point.
-   * @param keySizeInBytes The length of the generated pseudorandom
-   *     string in bytes. The maximal size is 255 * DigestSize, where DigestSize
-   *     is the size of the underlying HMAC.
-   * @param pointFormat The format of the
-   *     public ephemeral point.
-   * @param hkdfHash the name of the hash function. Accepted names are
-   *     SHA-1, SHA-256 and SHA-512.
-   * @param hkdfInfo Context and application specific
-   *     information (can be a zero-length array).
-   * @param opt_hkdfSalt Salt value (a non-secret random
-   *     value). If not provided, it is set to a string of hash length zeros.
-   * @return The KEM key and token.
-   */
-  async decapsulate(
-      kemToken: Uint8Array, keySizeInBytes: number,
-      pointFormat: EllipticCurves.PointFormatType, hkdfHash: string,
-      hkdfInfo: Uint8Array, opt_hkdfSalt?: Uint8Array): Promise<Uint8Array> {
-    const {namedCurve}: Partial<EcKeyAlgorithm> = this.privateKey_.algorithm;
-    if (!namedCurve) {
-      throw new SecurityException('Curve has to be defined.');
-    }
-    const jwk = EllipticCurves.pointDecode(namedCurve, pointFormat, kemToken);
-    const publicKey = await EllipticCurves.importPublicKey('ECDH', jwk);
-    const sharedSecret = await EllipticCurves.computeEcdhSharedSecret(
-        this.privateKey_, publicKey);
-    const hkdfIkm = Bytes.concat(kemToken, sharedSecret);
-    const kemKey = await Hkdf.compute(
-        keySizeInBytes, hkdfHash, hkdfIkm, hkdfInfo, opt_hkdfSalt);
-    return kemKey;
-  }
-}
-
-export async function fromJsonWebKey(jwk: JsonWebKey):
-    Promise<EciesHkdfKemRecipient> {
-  const privateKey = await EllipticCurves.importPrivateKey('ECDH', jwk);
-  return new EciesHkdfKemRecipient(privateKey);
-}
diff --git a/javascript/subtle/ecies_hkdf_kem_recipient_test.ts b/javascript/subtle/ecies_hkdf_kem_recipient_test.ts
deleted file mode 100644
index 4e374c2..0000000
--- a/javascript/subtle/ecies_hkdf_kem_recipient_test.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Bytes from './bytes';
-import {EciesHkdfKemRecipient, fromJsonWebKey as recipientFromJsonWebKey} from './ecies_hkdf_kem_recipient';
-import {fromJsonWebKey as senderFromJsonWebKey} from './ecies_hkdf_kem_sender';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-
-describe('ecies hkdf kem recipient test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('encap decap', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const privateKey = await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    const sender = await senderFromJsonWebKey(publicKey);
-    const recipient = await recipientFromJsonWebKey(privateKey);
-    for (let i = 1; i < 20; i++) {
-      const keySizeInBytes = i;
-      const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-      const hkdfHash = 'SHA-256';
-      const hkdfInfo = Random.randBytes(i);
-      const hkdfSalt = Random.randBytes(i);
-
-      const kemKeyToken = await sender.encapsulate(
-          keySizeInBytes, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-      const key = await recipient.decapsulate(
-          kemKeyToken['token'], keySizeInBytes, pointFormat, hkdfHash, hkdfInfo,
-          hkdfSalt);
-
-      expect(kemKeyToken['key'].length).toBe(keySizeInBytes);
-      expect(Bytes.toHex(kemKeyToken['key'])).toBe(Bytes.toHex(key));
-    }
-  });
-
-  it('decap, non integer key size', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const privateKey =
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    const sender = await senderFromJsonWebKey(publicKey);
-    const recipient = await recipientFromJsonWebKey(privateKey);
-    const keySizeInBytes = 16;
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const hkdfHash = 'SHA-256';
-    const hkdfInfo = Random.randBytes(16);
-    const hkdfSalt = Random.randBytes(16);
-    const kemKeyToken = await sender.encapsulate(
-        keySizeInBytes, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-
-    try {
-      await recipient.decapsulate(
-          kemKeyToken['token'], NaN, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-      fail('An exception should be thrown.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be an integer');
-    }
-
-    try {
-      await recipient.decapsulate(
-          kemKeyToken['token'], 1.8, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-      fail('An exception should be thrown.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be an integer');
-    }
-  });
-
-  it('new instance, invalid parameters', async function() {
-    // Test newInstance with public key instead private key.
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    try {
-      await recipientFromJsonWebKey(publicKey);
-      fail('An exception should be thrown.');
-    } catch (e) {
-    }
-  });
-
-  it('new instance, invalid private key', async function() {
-    for (const testVector of TEST_VECTORS) {
-      const ellipticCurveString = EllipticCurves.curveToString(testVector.crv);
-      const privateJwk = EllipticCurves.pointDecode(
-          ellipticCurveString, testVector.pointFormat,
-          Bytes.fromHex(testVector.privateKeyPoint));
-      privateJwk['d'] = Bytes.toBase64(
-          Bytes.fromHex(testVector.privateKeyValue), /* opt_webSafe = */ true);
-
-      // Change the x value such that the key si no more valid. Recipient should
-      // either throw an exception or ignore the x value and compute the same
-      // output value.
-      const xLength = EllipticCurves.fieldSizeInBytes(testVector.crv);
-      privateJwk['x'] =
-          Bytes.toBase64(new Uint8Array(xLength), /* opt_webSafe = */ true);
-      let output;
-      try {
-        const recipient = await recipientFromJsonWebKey(privateJwk);
-        const hkdfInfo = Bytes.fromHex(testVector.hkdfInfo);
-        const salt = Bytes.fromHex(testVector.salt);
-        output = await recipient.decapsulate(
-            Bytes.fromHex(testVector.token), testVector.outputLength,
-            testVector.pointFormat, testVector.hashType, hkdfInfo, salt);
-      } catch (e) {
-        // Everything works properly if exception was thrown.
-        return;
-      }
-      // If there was no exception, the output should be still correct (x value
-      // should be ignored during the computation).
-      expect(Bytes.toHex(output)).toBe(testVector.expectedOutput);
-    }
-  });
-
-  it('constructor, invalid parameters', async function() {
-    // Test public key instead of private key.
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    try {
-      new EciesHkdfKemRecipient(keyPair.publicKey!);
-      fail('An exception should be thrown.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: Expected crypto key of type: private.');
-    }
-  });
-
-  it('encap decap, different params', async function() {
-    const curveTypes = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-    const hashTypes = ['SHA-1', 'SHA-256', 'SHA-512'];
-    for (const curve of curveTypes) {
-      const curveString = EllipticCurves.curveToString(curve);
-      for (const hashType of hashTypes) {
-        const keyPair =
-            await EllipticCurves.generateKeyPair('ECDH', curveString);
-        const keySizeInBytes = 32;
-        const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-        const hkdfInfo = Random.randBytes(8);
-        const hkdfSalt = Random.randBytes(16);
-
-        const publicKey =
-            await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-        const sender = await senderFromJsonWebKey(publicKey);
-        const kemKeyToken = await sender.encapsulate(
-            keySizeInBytes, pointFormat, hashType, hkdfInfo, hkdfSalt);
-
-        const privateKey =
-            await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-        const recipient = await recipientFromJsonWebKey(privateKey);
-        const key = await recipient.decapsulate(
-            kemKeyToken['token'], keySizeInBytes, pointFormat, hashType,
-            hkdfInfo, hkdfSalt);
-
-        expect(kemKeyToken['key'].length).toBe(keySizeInBytes);
-        expect(Bytes.toHex(kemKeyToken['key'])).toBe(Bytes.toHex(key));
-      }
-    }
-  });
-
-  it('encap decap, modified token', async function() {
-    const curveTypes = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-    const hashTypes = ['SHA-1', 'SHA-256', 'SHA-512'];
-    for (let curve of curveTypes) {
-      const curveString = EllipticCurves.curveToString(curve);
-      for (let hashType of hashTypes) {
-        const keyPair =
-            await EllipticCurves.generateKeyPair('ECDH', curveString);
-        const privateKey =
-            await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-        const recipient = await recipientFromJsonWebKey(privateKey);
-        const keySizeInBytes = 32;
-        const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-        const hkdfInfo = Random.randBytes(8);
-        const hkdfSalt = Random.randBytes(16);
-
-        // Create invalid token (EC point), while preserving the 0x04 prefix
-        // byte.
-        const token = Random.randBytes(
-            EllipticCurves.encodingSizeInBytes(curve, pointFormat));
-        token[0] = 0x04;
-        try {
-          await recipient.decapsulate(
-              token, keySizeInBytes, pointFormat, hashType, hkdfInfo, hkdfSalt);
-          fail('Should throw an exception');
-        } catch (e) {
-        }
-      }
-    }
-  });
-
-  it('decapsulate, test vectors generated by java', async function() {
-    for (const testVector of TEST_VECTORS) {
-      const ellipticCurveString = EllipticCurves.curveToString(testVector.crv);
-      const privateJwk = EllipticCurves.pointDecode(
-          ellipticCurveString, testVector.pointFormat,
-          Bytes.fromHex(testVector.privateKeyPoint));
-      privateJwk['d'] = Bytes.toBase64(
-          Bytes.fromHex(testVector.privateKeyValue), /* opt_webSafe = */ true);
-      const recipient = await recipientFromJsonWebKey(privateJwk);
-      const hkdfInfo = Bytes.fromHex(testVector.hkdfInfo);
-      const salt = Bytes.fromHex(testVector.salt);
-      const output = await recipient.decapsulate(
-          Bytes.fromHex(testVector.token), testVector.outputLength,
-          testVector.pointFormat, testVector.hashType, hkdfInfo, salt);
-      expect(Bytes.toHex(output)).toBe(testVector.expectedOutput);
-    }
-  });
-});
-
-
-class TestVector {
-  constructor(
-      readonly crv: EllipticCurves.CurveType, readonly hashType: string,
-      readonly pointFormat: EllipticCurves.PointFormatType,
-      readonly token: string, readonly privateKeyPoint: string,
-      readonly privateKeyValue: string, readonly salt: string,
-      readonly hkdfInfo: string, readonly outputLength: number,
-      readonly expectedOutput: string) {}
-}
-
-// Test vectors generated by Java version of Tink.
-//
-// Token (i.e. sender public key) and privateKeyPoint values are in UNCOMPRESSED
-// EcPoint encoding (i.e. it has prefix '04' followed by x and y values).
-const TEST_VECTORS: TestVector[] = [
-  new TestVector(
-      EllipticCurves.CurveType.P256, 'SHA-256',
-      EllipticCurves.PointFormatType.UNCOMPRESSED,
-      /* token = */ '04' +
-          '5cdd8e426d11970a610f0e5f9b27f247a421c477b379f2ff3fd3bac50dfff9ff' +
-          '7cada79ab1de9ce4aeaff45fcd2628d1b6d7ecac99d4c26409d4ab8a362c8e7a',
-      /* privateKeyPoint = */ '04' +
-          '4adf0fff84b995bb97af250128a3d779c86ba3cd7e5c0fa2c10895d0b995aaee' +
-          'cdced57616ebb04c808f191c2bf3848c495dcfddcdd1bb73d8ea7a15c642af05',
-      /* privateKeyValue = */
-      'da73e10f7d81483daa63438b982c879706bcf8fef8c7c4d3071c3ef2367714f3',
-      /* salt = */ 'abcdef',
-      /* hkdfInfo = */ 'aaaaaaaaaaaaaaaa',
-      /* outputLength = */ 32,
-      /* expectedOutput = */
-      'aeeee35a14967310798f037e2f126e2e326369115eb9e2d1a34d9c6761f60511'),
-  new TestVector(
-      EllipticCurves.CurveType.P384, 'SHA-1',
-      EllipticCurves.PointFormatType.UNCOMPRESSED,
-      /* token = */ '04' +
-          '75bc8a2e6cf80ce2e0a1cd60ab3d68e4d357b58ff69f0de14b7ec13c58a79750496e07db3f933167148d80730b96f000' +
-          '9389967de410535ca3e103e7ce73dae9525f934589a6cd1fca37e61411985788dcedc71b35ef63b7365e391f6e2a945f',
-      /* privateKeyPoint = */ '04' +
-          '5f81886c4202897355b1da79348d53abd9e9119a7de6f5f10dfe751f7ca9c807035c029bac59499337c4af185fe61728' +
-          'f132bfb234365a9c61e1e56c11acca3bee6621961c7c38eb9dcbd39b332fd35006876dccdb206a7b2d43cf70589c3356',
-      /* privateKeyValue = */
-      '544b5f32731d6277fa71e756f0b2d6840f62e6b744a8b8cdf91f8cf29e6d8562f6237369721f756ab044711e0d42c53c',
-      /* salt = */ 'ababcdcd',
-      /* hkdfInfo = */ '100000000000000001',
-      /* outputLength = */ 32,
-      /* expectedOutput = */
-      '7a25c525eabaa0d994c27f7661a208b5ea25c2a778198237de6e4f235cd64a33'),
-  new TestVector(
-      EllipticCurves.CurveType.P521, 'SHA-512',
-      EllipticCurves.PointFormatType.UNCOMPRESSED,
-      /* token = */ '04' +
-          '0075192f8decddf7a0371b2c859aad738cc5424fa70e74b560070ed8309ae8a6064b06f9aaad8020ac8620e62a6c1196efa44180d325a36a54945743b9382bd49bc1' +
-          '000dfa1e30b228e975998b7afeaaf30235ec505960e58bf3269b69fffcbce9f15fc1441fab2ed97f554ae4bde8b956efb2372c5b330cb1aa0ab81b99e792acd7f5a8',
-      /* privateKeyPoint = */ '04' +
-          '00e57037a96bcbca532ef2f75646d825304ea716bbc9c4bf953455074347158f4818122c76e26a4cf94b39f451b7f5960b9cda43d49999ddc401c1be7f082052b387' +
-          '0147197ba83ec55c8b02e6cbe7b49ce6d6c238edb89561bde6b4574a585c684379d8040888117866823258216344a7268dc696c3a2d192824a1e693609b44661fc2c',
-      /* privateKeyValue = */
-      '001e5410117d22e95c5768b82a786dd66fa8c326b938a3a81fdd6113499437ae9f74e9f876adf085c187c6a147abc13460b8ed3050a6b228005426b61f2b616a79c6',
-      /* salt = */ '00001111',
-      /* hkdfInfo = */ '1234123412341234',
-      /* outputLength = */ 32,
-      /* expectedOutput = */
-      '3f7f64c7aba2cb012c9b5a952385290604b3b5843ec6e6714647a9c9d6ac87be')
-];
diff --git a/javascript/subtle/ecies_hkdf_kem_sender.ts b/javascript/subtle/ecies_hkdf_kem_sender.ts
deleted file mode 100644
index 664c5f0..0000000
--- a/javascript/subtle/ecies_hkdf_kem_sender.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {SecurityException} from '../exception/security_exception';
-
-import * as Bytes from './bytes';
-import * as EllipticCurves from './elliptic_curves';
-import * as Hkdf from './hkdf';
-
-/**
- * HKDF-based ECIES-KEM (key encapsulation mechanism) for ECIES sender.
- */
-export class EciesHkdfKemSender {
-  private readonly publicKey: CryptoKey;
-
-  constructor(recipientPublicKey: CryptoKey) {
-    if (!recipientPublicKey) {
-      throw new SecurityException('Recipient public key has to be non-null.');
-    }
-
-    // CryptoKey should have the properties type and algorithm.
-    if (recipientPublicKey.type !== 'public' || !recipientPublicKey.algorithm) {
-      throw new SecurityException('Expected Crypto key of type: public.');
-    }
-    this.publicKey = recipientPublicKey;
-  }
-
-  /**
-   * @param keySizeInBytes The length of the generated pseudorandom
-   *     string in bytes. The maximal size is 255 * DigestSize, where DigestSize
-   *     is the size of the underlying HMAC.
-   * @param pointFormat The format of the
-   *     public ephemeral point.
-   * @param hkdfHash the name of the hash function. Accepted names are
-   *     SHA-1, SHA-256 and SHA-512.
-   * @param hkdfInfo Context and application specific
-   *     information (can be a zero-length array).
-   * @param opt_hkdfSalt Salt value (a non-secret random
-   *     value). If not provided, it is set to a string of hash length zeros.
-   * @return The KEM key and
-   *     token.
-   */
-  async encapsulate(
-      keySizeInBytes: number, pointFormat: EllipticCurves.PointFormatType,
-      hkdfHash: string, hkdfInfo: Uint8Array, opt_hkdfSalt?: Uint8Array):
-      Promise<{key: Uint8Array, token: Uint8Array}> {
-    const {namedCurve}: Partial<EcKeyAlgorithm> = this.publicKey.algorithm;
-    if (!namedCurve) {
-      throw new SecurityException('Curve has to be defined.');
-    }
-    const ephemeralKeyPair =
-        await EllipticCurves.generateKeyPair('ECDH', namedCurve);
-    const sharedSecret = await EllipticCurves.computeEcdhSharedSecret(
-        ephemeralKeyPair.privateKey!, this.publicKey);
-    const jwk =
-        await EllipticCurves.exportCryptoKey(ephemeralKeyPair.publicKey!);
-    const {crv} = jwk;
-    if (!crv) {
-      throw new SecurityException('Curve has to be defined.');
-    }
-    const kemToken = EllipticCurves.pointEncode(crv, pointFormat, jwk);
-    const hkdfIkm = Bytes.concat(kemToken, sharedSecret);
-    const kemKey = await Hkdf.compute(
-        keySizeInBytes, hkdfHash, hkdfIkm, hkdfInfo, opt_hkdfSalt);
-    return {'key': kemKey, 'token': kemToken};
-  }
-}
-
-export async function fromJsonWebKey(jwk: JsonWebKey):
-    Promise<EciesHkdfKemSender> {
-  const publicKey = await EllipticCurves.importPublicKey('ECDH', jwk);
-  return new EciesHkdfKemSender(publicKey);
-}
diff --git a/javascript/subtle/ecies_hkdf_kem_sender_test.ts b/javascript/subtle/ecies_hkdf_kem_sender_test.ts
deleted file mode 100644
index 9202cba..0000000
--- a/javascript/subtle/ecies_hkdf_kem_sender_test.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Bytes from './bytes';
-import {EciesHkdfKemSender, fromJsonWebKey} from './ecies_hkdf_kem_sender';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-
-describe('ecies hkdf kem sender test', function() {
-  it('encapsulate, always generate random key', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const sender = await fromJsonWebKey(publicKey);
-    const keySizeInBytes = 32;
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const hkdfHash = 'SHA-256';
-    const hkdfInfo = Random.randBytes(32);
-    const hkdfSalt = Random.randBytes(32);
-    const keys = new Set();
-    const tokens = new Set();
-    for (let i = 0; i < 20; i++) {
-      const kemKeyToken = await sender.encapsulate(
-          keySizeInBytes, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-      keys.add(Bytes.toHex(kemKeyToken['key']));
-      tokens.add(Bytes.toHex(kemKeyToken['token']));
-    }
-    expect(keys.size).toBe(20);
-    expect(tokens.size).toBe(20);
-  });
-
-  it('encapsulate, non integer key size', async function() {
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const publicKey = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-    const sender = await fromJsonWebKey(publicKey);
-    const pointFormat = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const hkdfHash = 'SHA-256';
-    const hkdfInfo = Random.randBytes(32);
-    const hkdfSalt = Random.randBytes(32);
-    try {
-      await sender.encapsulate(NaN, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-      fail('An exception should be thrown.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be an integer');
-    }
-    try {
-      await sender.encapsulate(0, pointFormat, hkdfHash, hkdfInfo, hkdfSalt);
-      fail('An exception should be thrown.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be positive');
-    }
-  });
-
-  it('new instance, invalid parameters', async function() {
-    // Test fromJsonWebKey with public key instead private key.
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const privateKey =
-        await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
-    try {
-      await fromJsonWebKey(privateKey);
-      fail('An exception should be thrown.');
-    } catch (e) {
-    }
-  });
-
-  it('new instance, invalid public key', async function() {
-    for (const curve
-             of [EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-                 EllipticCurves.CurveType.P521]) {
-      const crvString = EllipticCurves.curveToString(curve);
-      const keyPair = await EllipticCurves.generateKeyPair('ECDH', crvString);
-      const publicJwk =
-          await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
-      // Change the 'x' value to make the public key invalid. Either getting new
-      // recipient with corrupted public key or trying to encapsulate with this
-      // recipient should fail.
-      const xLength = EllipticCurves.fieldSizeInBytes(curve);
-      publicJwk['x'] =
-          Bytes.toBase64(new Uint8Array(xLength), /* opt_webSafe = */ true);
-      const hkdfInfo = Random.randBytes(10);
-      const salt = Random.randBytes(8);
-      try {
-        const sender = await fromJsonWebKey(publicJwk);
-        await sender.encapsulate(
-            /* keySizeInBytes = */ 32,
-            EllipticCurves.PointFormatType.UNCOMPRESSED,
-            /* hkdfHash = */ 'SHA-256', hkdfInfo, salt);
-        fail('Should throw an exception.');
-      } catch (e) {
-      }
-    }
-  });
-
-  it('constructor, invalid parameters', async function() {
-    // Test constructor with public key instead private key.
-    const keyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    try {
-      new EciesHkdfKemSender(keyPair.privateKey!);
-      fail('An exception should be thrown.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: Expected Crypto key of type: public.');
-    }
-  });
-});
diff --git a/javascript/subtle/elliptic_curves.ts b/javascript/subtle/elliptic_curves.ts
deleted file mode 100644
index 19fe7c1..0000000
--- a/javascript/subtle/elliptic_curves.ts
+++ /dev/null
@@ -1,778 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * @fileoverview Common enums.
- */
-
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-
-import * as Bytes from './bytes';
-
-/**
- * Supported elliptic curves.
- */
-export enum CurveType {
-  P256 = 1,
-  P384,
-  P521
-}
-
-/**
- * Supported point format.
- */
-export enum PointFormatType {
-  UNCOMPRESSED = 1,
-  COMPRESSED,
-
-  // Like UNCOMPRESSED but without the \x04 prefix. Crunchy uses this format.
-  // DO NOT USE unless you are a Crunchy user moving to Tink.
-  DO_NOT_USE_CRUNCHY_UNCOMPRESSED
-}
-
-/**
- * Supported ECDSA signature encoding.
- */
-export enum EcdsaSignatureEncodingType {
-
-  // The DER signature is encoded using ASN.1
-  // (https://tools.ietf.org/html/rfc5480#appendix-A):
-  // ECDSA-Sig-Value :: = SEQUENCE { r INTEGER, s INTEGER }. In particular, the
-  // encoding is:
-  // 0x30 || totalLength || 0x02 || r's length || r || 0x02 || s's length || s.
-  DER = 1,
-
-  // The IEEE_P1363 signature's format is r || s, where r and s are zero-padded
-  // and have the same size in bytes as the order of the curve. For example, for
-  // NIST P-256 curve, r and s are zero-padded to 32 bytes.
-  IEEE_P1363
-}
-
-/**
- * Transform an ECDSA signature in DER encoding to IEEE P1363 encoding.
- *
- * @param der the ECDSA signature in DER encoding
- * @param ieeeLength the length of the ECDSA signature in IEEE
- *     encoding. This is usually 2 * size of the elliptic curve field.
- * @return ECDSA signature in IEEE encoding
- */
-export function ecdsaDer2Ieee(der: Uint8Array, ieeeLength: number): Uint8Array {
-  if (!isValidDerEcdsaSignature(der)) {
-    throw new InvalidArgumentsException('invalid DER signature');
-  }
-  if (!Number.isInteger(ieeeLength) || ieeeLength < 0) {
-    throw new InvalidArgumentsException(
-        'ieeeLength must be a nonnegative integer');
-  }
-  const ieee = new Uint8Array(ieeeLength);
-  const length = der[1] & 255;
-  let offset = 1 +
-      /* 0x30 */
-      1;
-
-  /* totalLength */
-  if (length >= 128) {
-    offset++;
-  }
-
-  // Long form length
-  offset++;
-
-  // 0x02
-  const rLength = der[offset++];
-  let extraZero = 0;
-  if (der[offset] === 0) {
-    extraZero = 1;
-  }
-  const rOffset = ieeeLength / 2 - rLength + extraZero;
-  ieee.set(der.subarray(offset + extraZero, offset + rLength), rOffset);
-  offset += rLength +
-      /* r byte array */
-      1;
-
-  /* 0x02 */
-  const sLength = der[offset++];
-  extraZero = 0;
-  if (der[offset] === 0) {
-    extraZero = 1;
-  }
-  const sOffset = ieeeLength - sLength + extraZero;
-  ieee.set(der.subarray(offset + extraZero, offset + sLength), sOffset);
-  return ieee;
-}
-
-/**
- * Transform an ECDSA signature in IEEE 1363 encoding to DER encoding.
- *
- * @param ieee the ECDSA signature in IEEE encoding
- * @return ECDSA signature in DER encoding
- */
-export function ecdsaIeee2Der(ieee: Uint8Array): Uint8Array {
-  if (ieee.length % 2 != 0 || ieee.length == 0 || ieee.length > 132) {
-    throw new InvalidArgumentsException(
-        'Invalid IEEE P1363 signature encoding. Length: ' + ieee.length);
-  }
-  const r = toUnsignedBigNum(ieee.subarray(0, ieee.length / 2));
-  const s = toUnsignedBigNum(ieee.subarray(ieee.length / 2, ieee.length));
-  let offset = 0;
-  const length = 1 + 1 + r.length + 1 + 1 + s.length;
-  let der;
-  if (length >= 128) {
-    der = new Uint8Array(length + 3);
-    der[offset++] = 48;
-    der[offset++] = 128 + 1;
-    der[offset++] = length;
-  } else {
-    der = new Uint8Array(length + 2);
-    der[offset++] = 48;
-    der[offset++] = length;
-  }
-  der[offset++] = 2;
-  der[offset++] = r.length;
-  der.set(r, offset);
-  offset += r.length;
-  der[offset++] = 2;
-  der[offset++] = s.length;
-  der.set(s, offset);
-  return der;
-}
-
-/**
- * Validate that the ECDSA signature is in DER encoding, based on
- * https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki.
- *
- * @param sig an ECDSA siganture
- */
-export function isValidDerEcdsaSignature(sig: Uint8Array): boolean {
-  // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]
-  // * total-length: 1-byte or 2-byte length descriptor of everything that
-  // follows.
-  // * R-length: 1-byte length descriptor of the R value that follows.
-  // * R: arbitrary-length big-endian encoded R value. It must use the shortest
-  //   possible encoding for a positive integers (which means no null bytes at
-  //   the start, except a single one when the next byte has its highest bit
-  //   set).
-  // * S-length: 1-byte length descriptor of the S value that follows.
-  // * S: arbitrary-length big-endian encoded S value. The same rules apply.
-  /* S */
-  if (sig.length < 1 +
-          /* 0x30 */
-          1 +
-          /* total-length */
-          1 +
-          /* 0x02 */
-          1 +
-          /* R-length */
-          1 +
-          /* R */
-          1 +
-          /* 0x02 */
-          1 +
-          /* S-length */
-          1) {
-    // Signature is too short.
-    return false;
-  }
-
-  // Checking bytes from left to right.
-
-  // byte #1: a signature is of type 0x30 (compound).
-  if (sig[0] != 48) {
-    return false;
-  }
-
-  // byte #2 and maybe #3: the total length of the signature.
-  let totalLen = sig[1] & 255;
-  let totalLenLen = 1;
-
-  // the length of the total length field, could be 2-byte.
-  if (totalLen == 129) {
-    // The signature is >= 128 bytes thus total length field is in long-form
-    // encoding and occupies 2 bytes.
-    totalLenLen = 2;
-
-    // byte #3 is the total length.
-    totalLen = sig[2] & 255;
-    if (totalLen < 128) {
-      // Length in long-form encoding must be >= 128.
-      return false;
-    }
-  } else if (totalLen == 128 || totalLen > 129) {
-    // Impossible values for the second byte.
-    return false;
-  }
-
-
-  // Make sure the length covers the entire sig.
-  if (totalLen != sig.length - 1 - totalLenLen) {
-    return false;
-  }
-
-  // Start checking R.
-  // Check whether the R element is an integer.
-  if (sig[1 + totalLenLen] != 2) {
-    return false;
-  }
-
-  // Extract the length of the R element.
-  const rLen = sig[1 +
-                   /* 0x30 */
-                   totalLenLen + 1] &
-      /* 0x02 */
-      255;
-
-  // Make sure the length of the S element is still inside the signature.
-  if (1 +
-          /* 0x30 */
-          totalLenLen + 1 +
-          /* 0x02 */
-          1 +
-          /* rLen */
-          rLen + 1 >=
-      /* 0x02 */
-      sig.length) {
-    return false;
-  }
-
-  // Zero-length integers are not allowed for R.
-  if (rLen == 0) {
-    return false;
-  }
-
-  // Negative numbers are not allowed for R.
-  if ((sig[3 + totalLenLen] & 255) >= 128) {
-    return false;
-  }
-
-  // Null bytes at the start of R are not allowed, unless R would
-  // otherwise be interpreted as a negative number.
-  if (rLen > 1 && sig[3 + totalLenLen] == 0 &&
-      (sig[4 + totalLenLen] & 255) < 128) {
-    return false;
-  }
-
-  // Start checking S.
-  // Check whether the S element is an integer.
-  if (sig[3 + totalLenLen + rLen] != 2) {
-    return false;
-  }
-
-  // Extract the length of the S element.
-  const sLen = sig[1 +
-                   /* 0x30 */
-                   totalLenLen + 1 +
-                   /* 0x02 */
-                   1 +
-                   /* rLen */
-                   rLen + 1] &
-      /* 0x02 */
-      255;
-
-  // Verify that the length of the signature matches the sum of the length of
-  // the elements.
-  if (1 +
-          /* 0x30 */
-          totalLenLen + 1 +
-          /* 0x02 */
-          1 +
-          /* rLen */
-          rLen + 1 +
-          /* 0x02 */
-          1 +
-          /* sLen */
-          sLen !=
-      sig.length) {
-    return false;
-  }
-
-  // Zero-length integers are not allowed for S.
-  if (sLen == 0) {
-    return false;
-  }
-
-  // Negative numbers are not allowed for S.
-  if ((sig[5 + totalLenLen + rLen] & 255) >= 128) {
-    return false;
-  }
-
-  // Null bytes at the start of S are not allowed, unless S would
-  // otherwise be interpreted as a negative number.
-  if (sLen > 1 && sig[5 + totalLenLen + rLen] == 0 &&
-      (sig[6 + totalLenLen + rLen] & 255) < 128) {
-    return false;
-  }
-  return true;
-}
-
-/**
- * Transform a big integer in big endian to minimal unsigned form which has
- * no extra zero at the beginning except when the highest bit is set.
- *
- */
-function toUnsignedBigNum(bytes: Uint8Array): Uint8Array {
-  // Remove zero prefixes.
-  let start = 0;
-  while (start < bytes.length && bytes[start] == 0) {
-    start++;
-  }
-  if (start == bytes.length) {
-    start = bytes.length - 1;
-  }
-  let extraZero = 0;
-
-  // If the 1st bit is not zero, add 1 zero byte.
-  if ((bytes[start] & 128) == 128) {
-    // Add extra zero.
-    extraZero = 1;
-  }
-  const res = new Uint8Array(bytes.length - start + extraZero);
-  res.set(bytes.subarray(start), extraZero);
-  return res;
-}
-
-export function curveToString(curve: CurveType): string {
-  switch (curve) {
-    case CurveType.P256:
-      return 'P-256';
-    case CurveType.P384:
-      return 'P-384';
-    case CurveType.P521:
-      return 'P-521';
-  }
-}
-
-export function curveFromString(curve: string): CurveType {
-  switch (curve) {
-    case 'P-256':
-      return CurveType.P256;
-    case 'P-384':
-      return CurveType.P384;
-    case 'P-521':
-      return CurveType.P521;
-  }
-  throw new InvalidArgumentsException('unknown curve: ' + curve);
-}
-
-/** Helper method for unit tests. */
-export function formatFromString(format: string): PointFormatType {
-  switch (format) {
-    case 'UNCOMPRESSED':
-      return PointFormatType.UNCOMPRESSED;
-    case 'DO_NOT_USE_CRUNCHY_UNCOMPRESSED':
-      return PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED;
-    case 'COMPRESSED':
-      return PointFormatType.COMPRESSED;
-    default:
-      throw new InvalidArgumentsException('unknown format: ' + format);
-  }
-}
-
-export function pointEncode(
-    curve: string, format: PointFormatType, point: JsonWebKey): Uint8Array {
-  const fieldSize = fieldSizeInBytes(curveFromString(curve));
-  switch (format) {
-    case PointFormatType.UNCOMPRESSED: {
-      const {x, y} = point;
-      if (x === undefined) {
-        throw new InvalidArgumentsException('x must be provided');
-      }
-      if (y === undefined) {
-        throw new InvalidArgumentsException('y must be provided');
-      }
-      const result = new Uint8Array(1 + 2 * fieldSize);
-      result[0] = 4;
-      result.set(
-          /* opt_webSafe = */
-          Bytes.fromBase64(x, true), 1);
-      result.set(
-          /* opt_webSafe = */
-          Bytes.fromBase64(y, true), 1 + fieldSize);
-      return result;
-    }
-    case PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED: {
-      const {x, y} = point;
-      if (x === undefined) {
-        throw new InvalidArgumentsException('x must be provided');
-      }
-      if (y === undefined) {
-        throw new InvalidArgumentsException('y must be provided');
-      }
-      let decodedX = Bytes.fromBase64(x, /* opt_webSafe = */ true);
-      let decodedY = Bytes.fromBase64(y, /* opt_webSafe = */ true);
-      if (decodedX.length > fieldSize) {
-        // x has leading 0's, strip them.
-        decodedX = decodedX.slice(decodedX.length - fieldSize, decodedX.length);
-      }
-      if (decodedY.length > fieldSize) {
-        // y has leading 0's, strip them.
-        decodedY = decodedY.slice(decodedY.length - fieldSize, decodedY.length);
-      }
-      const result = new Uint8Array(2 * fieldSize);
-      result.set(decodedX, 0);
-      result.set(decodedY, fieldSize);
-      return result;
-    }
-    case PointFormatType.COMPRESSED: {
-      const {x, y} = point;
-      if (x === undefined) {
-        throw new InvalidArgumentsException('x must be provided');
-      }
-      if (y === undefined) {
-        throw new InvalidArgumentsException('y must be provided');
-      }
-      let decodedX = Bytes.fromBase64(x, /* opt_webSafe = */ true);
-      let decodedY = Bytes.fromBase64(y, /* opt_webSafe = */ true);
-      if (decodedX.length > fieldSize) {
-        // x has leading 0's, strip them.
-        decodedX = decodedX.slice(decodedX.length - fieldSize, decodedX.length);
-      }
-      if (decodedY.length > fieldSize) {
-        // y has leading 0's, strip them.
-        decodedY = decodedY.slice(decodedY.length - fieldSize, decodedY.length);
-      }
-      const result = new Uint8Array(1 + fieldSize);
-      result.set(decodedX, /* offset = */ 1 + fieldSize - decodedX.length);
-      result[0] = testBit(byteArrayToInteger(decodedY), 0) ? 3 : 2;
-      return result;
-    }
-    default:
-      throw new InvalidArgumentsException('invalid format');
-  }
-}
-
-function getModulus(curve: CurveType): bigint {
-  // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf (Appendix D).
-  switch (curve) {
-    case CurveType.P256:
-      return BigInt(
-          '115792089210356248762697446949407573530086143415290314195533631308' +
-              '867097853951');
-    case CurveType.P384:
-      return BigInt(
-          '394020061963944792122790401001436138050797392704654466679482934042' +
-              '45721771496870329047266088258938001861606973112319');
-    case CurveType.P521:
-      return BigInt(
-          '686479766013060971498190079908139321726943530014330540939446345918' +
-              '55431833976560521225596406614545549772963113914808580371219879' +
-              '99716643812574028291115057151');
-    default:
-      throw new InvalidArgumentsException('invalid curve');
-  }
-}
-
-function getB(curve: CurveType): bigint {
-  // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf (Appendix D).
-  switch (curve) {
-    case CurveType.P256:
-      return BigInt(
-          '0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b');
-    case CurveType.P384:
-      return BigInt(
-          '0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875a' +
-              'c656398d8a2ed19d2a85c8edd3ec2aef');
-    case CurveType.P521:
-      return BigInt(
-          '0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef10' +
-              '9e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b5' +
-              '03f00');
-    default:
-      throw new InvalidArgumentsException('invalid curve');
-  }
-}
-
-/** Converts byte array to bigint. */
-export function byteArrayToInteger(bytes: Uint8Array): bigint {
-  return BigInt('0x' + Bytes.toHex(bytes));
-}
-
-/** Converts bigint to byte array. */
-export function integerToByteArray(i: bigint): Uint8Array {
-  let input = i.toString(16);
-  // If necessary, prepend leading zero to ensure that input length is even.
-  input = input.length % 2 === 0 ? input : '0' + input;
-  return Bytes.fromHex(input);
-}
-
-/** Returns true iff the ith bit (in lsb order) of n is set. */
-function testBit(n: bigint, i: number): boolean {
-  const m = BigInt(1) << BigInt(i);
-  return (n & m) !== BigInt(0);
-}
-
-/**
- * Computes a modular exponent.  Since JavaScript BigInt operations are not
- * constant-time, information about the inputs could leak.  Therefore, THIS
- * METHOD SHOULD ONLY BE USED FOR POINT DECOMPRESSION.
- *
- * @param b base
- * @param exp exponent
- * @param p modulus
- * @return b^exp modulo p
- */
-function modPow(b: bigint, exp: bigint, p: bigint): bigint {
-  if (exp === BigInt(0)) {
-    return BigInt(1);
-  }
-  let result = b;
-  const exponentBitString = exp.toString(2);
-  for (let i = 1; i < exponentBitString.length; ++i) {
-    result = (result * result) % p;
-    if (exponentBitString[i] === '1') {
-      result = (result * b) % p;
-    }
-  }
-  return result;
-}
-
-/**
- * Computes a square root modulo an odd prime.  Since timing and exceptions can
- * leak information about the inputs, THIS METHOD SHOULD ONLY BE USED FOR
- * POINT DECOMPRESSION.
- *
- * @param x square
- * @param p prime modulus
- * @return square root of x modulo p
- */
-function modSqrt(x: bigint, p: bigint): bigint {
-  if (p <= BigInt(0)) {
-    throw new InvalidArgumentsException('p must be positive');
-  }
-  const base = x % p;
-  // The currently supported NIST curves P-256, P-384, and P-521 all satisfy
-  // p % 4 == 3.  However, although currently a no-op, the following check
-  // should be left in place in case other curves are supported in the future.
-  if (testBit(p, 0) && /* istanbul ignore next */ testBit(p, 1)) {
-    // Case p % 4 == 3 (applies to NIST curves P-256, P-384, and P-521)
-    // q = (p + 1) / 4
-    const q = (p + BigInt(1)) >> BigInt(2);
-    const squareRoot = modPow(base, q, p);
-    if ((squareRoot * squareRoot) % p !== base) {
-      throw new InvalidArgumentsException(
-          'could not find a modular square root');
-    }
-    return squareRoot;
-  }
-  // Skipping other elliptic curve types that require Cipolla's algorithm.
-  throw new InvalidArgumentsException('unsupported modulus value');
-}
-
-/**
- * Computes the y-coordinate of a point on an elliptic curve given its
- * x-coordinate.  Since timing and exceptions can leak information about the
- * inputs, THIS METHOD SHOULD ONLY BE USED FOR POINT DECOMPRESSION.
- *
- * @param x x-coordinate
- * @param lsb least significant bit of the y-coordinate
- * @param curve NIST curve P-256, P-384, or P-521
- * @return y-coordinate
- */
-function getY(x: bigint, lsb: boolean, curve: string): bigint {
-  const p = getModulus(curveFromString(curve));
-  const a = p - BigInt(3);
-  const b = getB(curveFromString(curve));
-  const rhs = (((x * x) + a) * x + b) % p;
-  let y = modSqrt(rhs, p);
-  if (lsb !== testBit(y, 0)) {
-    y = (p - y) % p;
-  }
-  return y;
-}
-
-export function pointDecode(
-    curve: string, format: PointFormatType, point: Uint8Array): JsonWebKey {
-  const fieldSize = fieldSizeInBytes(curveFromString(curve));
-  switch (format) {
-    case PointFormatType.UNCOMPRESSED: {
-      if (point.length !== 1 + 2 * fieldSize || point[0] !== 4) {
-        throw new InvalidArgumentsException('invalid point');
-      }
-      const result = ({
-        'kty': 'EC',
-        'crv': curve,
-        'x': Bytes.toBase64(
-            new Uint8Array(point.subarray(1, 1 + fieldSize)),
-            /* websafe */
-            true),
-        'y': Bytes.toBase64(
-            new Uint8Array(point.subarray(1 + fieldSize, point.length)),
-            /* websafe */
-            true),
-        'ext': true
-      } as JsonWebKey);
-      return result;
-    }
-    case PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED: {
-      if (point.length !== 2 * fieldSize) {
-        throw new InvalidArgumentsException('invalid point');
-      }
-      const result = ({
-        'kty': 'EC',
-        'crv': curve,
-        'x': Bytes.toBase64(
-            new Uint8Array(point.subarray(0, fieldSize)), /* websafe */ true),
-        'y': Bytes.toBase64(
-            new Uint8Array(point.subarray(fieldSize, point.length)),
-            /* websafe */ true),
-        'ext': true
-      } as JsonWebKey);
-      return result;
-    }
-    case PointFormatType.COMPRESSED: {
-      if (point.length !== 1 + fieldSize) {
-        throw new InvalidArgumentsException(
-            'compressed point has wrong length');
-      }
-      if (point[0] !== 2 && point[0] !== 3) {
-        throw new InvalidArgumentsException('invalid format');
-      }
-      const lsb = (point[0] === 3); // point[0] must be 2 (false) or 3 (true).
-      const x = byteArrayToInteger(point.subarray(1, point.length));
-      const p = getModulus(curveFromString(curve));
-      if (x < BigInt(0) || x >= p) {
-        throw new InvalidArgumentsException('x is out of range');
-      }
-      const y = getY(x, lsb, curve);
-      const result : JsonWebKey = {
-        'kty': 'EC',
-        'crv': curve,
-        'x': Bytes.toBase64(integerToByteArray(x), /* websafe */ true),
-        'y': Bytes.toBase64(integerToByteArray(y), /* websafe */ true),
-        'ext': true
-      };
-      return result;
-    }
-    default:
-      throw new InvalidArgumentsException('invalid format');
-  }
-}
-
-export function getJsonWebKey(
-    curve: CurveType, x: Uint8Array, y: Uint8Array,
-    d?: Uint8Array|null): JsonWebKey {
-  const key = ({
-    'kty': 'EC',
-    'crv': curveToString(curve),
-    'x': Bytes.toBase64(
-        x,
-        /* websafe */
-        true),
-    'y': Bytes.toBase64(
-        y,
-        /* websafe */
-        true),
-    'ext': true
-  } as JsonWebKey);
-  if (d) {
-    key['d'] = Bytes.toBase64(
-        d,
-        /* websafe */
-        true);
-  }
-  return key;
-}
-
-export function fieldSizeInBytes(curve: CurveType): number {
-  switch (curve) {
-    case CurveType.P256:
-      return 32;
-    case CurveType.P384:
-      return 48;
-    case CurveType.P521:
-      return 66;
-  }
-}
-
-export function encodingSizeInBytes(
-    curve: CurveType, pointFormat: PointFormatType): number {
-  switch (pointFormat) {
-    case PointFormatType.UNCOMPRESSED:
-      return 2 * fieldSizeInBytes(curve) + 1;
-    case PointFormatType.COMPRESSED:
-      return fieldSizeInBytes(curve) + 1;
-    case PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED:
-      return 2 * fieldSizeInBytes(curve);
-  }
-}
-
-export async function computeEcdhSharedSecret(
-    privateKey: CryptoKey, publicKey: CryptoKey): Promise<Uint8Array> {
-  const {namedCurve}: Partial<EcKeyImportParams> = privateKey.algorithm;
-  if (!namedCurve) {
-    throw new InvalidArgumentsException('namedCurve must be provided');
-  }
-  const ecdhParams = {'public': publicKey, ...privateKey.algorithm};
-  const fieldSizeInBits = 8 * fieldSizeInBytes(curveFromString(namedCurve));
-  const sharedSecret = await window.crypto.subtle.deriveBits(
-      ecdhParams, privateKey, fieldSizeInBits);
-  return new Uint8Array(sharedSecret);
-}
-
-export async function generateKeyPair(
-    algorithm: 'ECDH'|'ECDSA', curve: string): Promise<CryptoKeyPair> {
-  if (algorithm != 'ECDH' && algorithm != 'ECDSA') {
-    throw new InvalidArgumentsException(
-        'algorithm must be either ECDH or ECDSA');
-  }
-  const params = {'name': algorithm, 'namedCurve': curve};
-  const ephemeralKeyPair = await window.crypto.subtle.generateKey(
-      params, /* extractable= */ true,
-      algorithm == 'ECDH' ? ['deriveKey', 'deriveBits'] : ['sign', 'verify']);
-  return ephemeralKeyPair as CryptoKeyPair;
-}
-
-export async function exportCryptoKey(cryptoKey: CryptoKey):
-    Promise<JsonWebKey> {
-  const jwk = await window.crypto.subtle.exportKey('jwk', cryptoKey);
-  return (jwk);
-}
-
-export async function importPublicKey(
-    algorithm: string, jwk: JsonWebKey): Promise<CryptoKey> {
-  if (algorithm != 'ECDH' && algorithm != 'ECDSA') {
-    throw new InvalidArgumentsException(
-        'algorithm must be either ECDH or ECDSA');
-  }
-  const {crv} = jwk;
-  if (!crv) {
-    throw new InvalidArgumentsException('crv must be provided');
-  }
-  const publicKey = await window.crypto.subtle.importKey(
-      /* format */
-      'jwk', jwk, {'name': algorithm, 'namedCurve': crv},
-      /* algorithm */
-      true,
-      /* extractable */
-      algorithm == 'ECDH' ? [] : ['verify']);
-
-  /* usage */
-  return publicKey;
-}
-
-export async function importPrivateKey(
-    algorithm: string, jwk: JsonWebKey): Promise<CryptoKey> {
-  if (algorithm != 'ECDH' && algorithm != 'ECDSA') {
-    throw new InvalidArgumentsException(
-        'algorithm must be either ECDH or ECDSA');
-  }
-  const {crv} = jwk;
-  if (!crv) {
-    throw new InvalidArgumentsException('crv must be provided');
-  }
-  const privateKey = await window.crypto.subtle.importKey(
-      /* format */
-      'jwk', jwk,
-      /* key material */
-      {'name': algorithm, 'namedCurve': crv},
-      /* algorithm */
-      true,
-      /* extractable */
-      algorithm == 'ECDH' ? ['deriveKey', 'deriveBits'] : ['sign']);
-
-  /* usage */
-  return privateKey;
-}
diff --git a/javascript/subtle/elliptic_curves_test.ts b/javascript/subtle/elliptic_curves_test.ts
deleted file mode 100644
index 561dc63..0000000
--- a/javascript/subtle/elliptic_curves_test.ts
+++ /dev/null
@@ -1,859 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {assertExists} from '../testing/internal/test_utils';
-
-import * as Bytes from './bytes';
-import * as EllipticCurves from './elliptic_curves';
-import * as Random from './random';
-import {WYCHEPROOF_ECDH_TEST_VECTORS} from './wycheproof_ecdh_test_vectors';
-
-describe('elliptic curves test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('compute ecdh shared secret', async function() {
-    const aliceKeyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const bobKeyPair = await EllipticCurves.generateKeyPair('ECDH', 'P-256');
-    const sharedSecret1 = await EllipticCurves.computeEcdhSharedSecret(
-        aliceKeyPair.privateKey!, bobKeyPair.publicKey!);
-    const sharedSecret2 = await EllipticCurves.computeEcdhSharedSecret(
-        bobKeyPair.privateKey!, aliceKeyPair.publicKey!);
-    expect(Bytes.toHex(sharedSecret2)).toBe(Bytes.toHex(sharedSecret1));
-  });
-
-  it('wycheproof, wycheproof webcrypto', async function() {
-    for (const testGroup of WYCHEPROOF_ECDH_TEST_VECTORS['testGroups']) {
-      let errors = '';
-      for (const test of testGroup['tests']) {
-        errors += await runWycheproofTest(test);
-      }
-      if (errors !== '') {
-        fail(errors);
-      }
-    }
-  });
-
-  // Test that both ECDH public and private key are defined in the result.
-  it('generate key pair e c d h', async function() {
-    const curveTypes = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-    for (const curve of curveTypes) {
-      const curveTypeString = EllipticCurves.curveToString(curve);
-      const keyPair =
-          await EllipticCurves.generateKeyPair('ECDH', curveTypeString);
-      expect(keyPair.privateKey! != null).toBe(true);
-      expect(keyPair.publicKey! != null).toBe(true);
-    }
-  });
-
-  // Test that both ECDSA public and private key are defined in the result.
-  it('generate key pair e c d s a', async function() {
-    const curveTypes = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-    for (const curve of curveTypes) {
-      const curveTypeString = EllipticCurves.curveToString(curve);
-      const keyPair =
-          await EllipticCurves.generateKeyPair('ECDSA', curveTypeString);
-      expect(keyPair.privateKey! != null).toBe(true);
-      expect(keyPair.publicKey! != null).toBe(true);
-    }
-  });
-
-  // Test that when ECDH crypto key is exported and imported it gives the same
-  // key as the original one.
-  it('import export crypto key e c d h', async function() {
-    const curveTypes = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-    for (const curve of curveTypes) {
-      const curveTypeString = EllipticCurves.curveToString(curve);
-      const keyPair =
-          await EllipticCurves.generateKeyPair('ECDH', curveTypeString);
-
-      const publicKey = keyPair.publicKey!;
-      const publicCryptoKey = await EllipticCurves.exportCryptoKey(publicKey);
-      const importedPublicKey =
-          await EllipticCurves.importPublicKey('ECDH', publicCryptoKey);
-      expect(importedPublicKey).toEqual(publicKey);
-
-      const privateKey = keyPair.privateKey!;
-      const privateCryptoKey = await EllipticCurves.exportCryptoKey(privateKey);
-      const importedPrivateKey =
-          await EllipticCurves.importPrivateKey('ECDH', privateCryptoKey);
-      expect(importedPrivateKey).toEqual(privateKey);
-    }
-  });
-
-  // Test that when ECDSA crypto key is exported and imported it gives the same
-  // key as the original one.
-  it('import export crypto key e c d s a', async function() {
-    const curveTypes = [
-      EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-      EllipticCurves.CurveType.P521
-    ];
-    for (const curve of curveTypes) {
-      const curveTypeString = EllipticCurves.curveToString(curve);
-      const keyPair =
-          await EllipticCurves.generateKeyPair('ECDSA', curveTypeString);
-
-      const publicKey = keyPair.publicKey!;
-      const publicCryptoKey = await EllipticCurves.exportCryptoKey(publicKey);
-      const importedPublicKey =
-          await EllipticCurves.importPublicKey('ECDSA', publicCryptoKey);
-      expect(importedPublicKey).toEqual(publicKey);
-
-      const privateKey = keyPair.privateKey!;
-      const privateCryptoKey = await EllipticCurves.exportCryptoKey(privateKey);
-      const importedPrivateKey =
-          await EllipticCurves.importPrivateKey('ECDSA', privateCryptoKey);
-      expect(importedPrivateKey).toEqual(privateKey);
-    }
-  });
-
-  // Test that when JSON ECDH web key is imported and exported it gives the same
-  // key as the original one.
-  it('import export json key e c d h', async function() {
-    for (const testKey of TEST_KEYS) {
-      const jwk: JsonWebKey = ({
-        'kty': 'EC',
-        'crv': testKey.curve,
-        'x': Bytes.toBase64(Bytes.fromHex(testKey.x), true),
-        'y': Bytes.toBase64(Bytes.fromHex(testKey.y), true),
-        'ext': true,
-      });
-
-      let importedKey;
-      if (!testKey.d) {
-        jwk['key_ops'] = [];
-        importedKey = await EllipticCurves.importPublicKey('ECDH', jwk);
-      } else {
-        jwk['key_ops'] = ['deriveKey', 'deriveBits'];
-        jwk['d'] = Bytes.toBase64(Bytes.fromHex(testKey.d), true);
-        importedKey = await EllipticCurves.importPrivateKey('ECDH', jwk);
-      }
-
-      const exportedKey = await EllipticCurves.exportCryptoKey(importedKey);
-      expect(exportedKey).toEqual(jwk);
-    }
-  });
-
-  // Test that when JSON ECDSA web key is imported and exported it gives the
-  // same key as the original one.
-  it('import export json key e c d s a', async function() {
-    for (const testKey of TEST_KEYS) {
-      const jwk: JsonWebKey = ({
-        'kty': 'EC',
-        'crv': testKey.curve,
-        'x': Bytes.toBase64(Bytes.fromHex(testKey.x), true),
-        'y': Bytes.toBase64(Bytes.fromHex(testKey.y), true),
-        'ext': true,
-      });
-
-      let importedKey;
-      if (!testKey.d) {
-        jwk['key_ops'] = ['verify'];
-        importedKey = await EllipticCurves.importPublicKey('ECDSA', jwk);
-      } else {
-        jwk['key_ops'] = ['sign'];
-        jwk['d'] = Bytes.toBase64(Bytes.fromHex(testKey.d), true);
-        importedKey = await EllipticCurves.importPrivateKey('ECDSA', jwk);
-      }
-
-      const exportedKey = await EllipticCurves.exportCryptoKey(importedKey);
-      expect(exportedKey).toEqual(jwk);
-    }
-  });
-
-  it('curve to string', function() {
-    expect(EllipticCurves.curveToString(EllipticCurves.CurveType.P256))
-        .toBe('P-256');
-    expect(EllipticCurves.curveToString(EllipticCurves.CurveType.P384))
-        .toBe('P-384');
-    expect(EllipticCurves.curveToString(EllipticCurves.CurveType.P521))
-        .toBe('P-521');
-  });
-
-  it('curve from string', function() {
-    expect(EllipticCurves.curveFromString('P-256'))
-        .toBe(EllipticCurves.CurveType.P256);
-    expect(EllipticCurves.curveFromString('P-384'))
-        .toBe(EllipticCurves.CurveType.P384);
-    expect(EllipticCurves.curveFromString('P-521'))
-        .toBe(EllipticCurves.CurveType.P521);
-  });
-
-  it('field size in bytes', function() {
-    expect(EllipticCurves.fieldSizeInBytes(EllipticCurves.CurveType.P256))
-        .toBe(256 / 8);
-    expect(EllipticCurves.fieldSizeInBytes(EllipticCurves.CurveType.P384))
-        .toBe(384 / 8);
-    expect(EllipticCurves.fieldSizeInBytes(EllipticCurves.CurveType.P521))
-        .toBe((521 + 7) / 8);
-  });
-
-  it('encoding size in bytes, uncompressed point format type', function() {
-    expect(EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P256,
-               EllipticCurves.PointFormatType.UNCOMPRESSED))
-        .toBe(2 * (256 / 8) + 1);
-    expect(EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P384,
-               EllipticCurves.PointFormatType.UNCOMPRESSED))
-        .toBe(2 * (384 / 8) + 1);
-    expect(EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P521,
-               EllipticCurves.PointFormatType.UNCOMPRESSED))
-        .toBe(2 * ((521 + 7) / 8) + 1);
-  });
-
-  it('encoding size in bytes, compressed point format type', function() {
-    expect(EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P256,
-               EllipticCurves.PointFormatType.COMPRESSED))
-        .toBe((256 / 8) + 1);
-    expect(EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P384,
-               EllipticCurves.PointFormatType.COMPRESSED))
-        .toBe((384 / 8) + 1);
-    expect(EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P521,
-               EllipticCurves.PointFormatType.COMPRESSED))
-        .toBe(((521 + 7) / 8) + 1);
-  });
-
-  it('encoding size in bytes, crunchy uncompressed point format type',
-     function() {
-       expect(
-           EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P256,
-               EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED))
-           .toBe(2 * (256 / 8));
-       expect(
-           EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P384,
-               EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED))
-           .toBe(2 * (384 / 8));
-       expect(
-           EllipticCurves.encodingSizeInBytes(
-               EllipticCurves.CurveType.P521,
-               EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED))
-           .toBe(2 * ((521 + 7) / 8));
-     });
-
-  it('point decode, wrong point size', function() {
-    const point = new Uint8Array(10);
-    const format = EllipticCurves.PointFormatType.UNCOMPRESSED;
-
-    for (const curve
-             of [EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-                 EllipticCurves.CurveType.P521]) {
-      const curveTypeString = EllipticCurves.curveToString(curve);
-
-      // It should throw an exception as the point array is too short.
-      try {
-        EllipticCurves.pointDecode(curveTypeString, format, point);
-        fail('Should throw an exception.');
-        // Preserving old behavior when moving to
-        // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-        // tslint:disable-next-line:no-any
-      } catch (e: any) {
-        expect(e.toString()).toBe('InvalidArgumentsException: invalid point');
-      }
-    }
-  });
-
-  it('point decode, unknown curve', function() {
-    const point = new Uint8Array(10);
-    const format = EllipticCurves.PointFormatType.UNCOMPRESSED;
-    const curve = 'some-unknown-curve';
-
-    try {
-      EllipticCurves.pointDecode(curve, format, point);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString().includes('unknown curve')).toBe(true);
-    }
-  });
-
-  it('point encode, compressed', () => {
-    for (const test of TEST_VECTORS) {
-      const format = EllipticCurves.formatFromString(test.format);
-      if (format !== EllipticCurves.PointFormatType.COMPRESSED) {
-        // TODO(b/214598739): Investigate compatibility of other formats with
-        // Java test vectors.
-        continue;
-      }
-      const point: JsonWebKey = {
-        kty: 'EC',
-        crv: test.curve,
-        x: Bytes.toBase64(
-            EllipticCurves.integerToByteArray(BigInt('0x' + test.x)),
-            /* websafe = */ true),
-        y: Bytes.toBase64(
-            EllipticCurves.integerToByteArray(BigInt('0x' + test.y)),
-            /* websafe = */ true),
-        ext: true,
-      };
-
-      const encodedPoint =
-          EllipticCurves.pointEncode(assertExists(point.crv), format, point);
-
-      expect(Bytes.toHex(encodedPoint)).toEqual(test.encoded);
-    }
-  });
-
-  it('point decode, compressed', () => {
-    for (const test of TEST_VECTORS) {
-      const format = EllipticCurves.formatFromString(test.format);
-      if (format !== EllipticCurves.PointFormatType.COMPRESSED) {
-        // TODO(b/214598739): Investigate compatibility of other formats with
-        // Java test vectors.
-        continue;
-      }
-      const decodedPoint = EllipticCurves.pointDecode(
-          test.curve, format, Bytes.fromHex(test.encoded));
-      // NOTE: Any leading zero inserted by Bytes.toHex() must be removed.
-      const decodedX =
-          Bytes.toHex(Bytes.fromBase64(assertExists(decodedPoint.x), true))
-              .replace(/^0?/, '');
-      const decodedY =
-          Bytes.toHex(Bytes.fromBase64(assertExists(decodedPoint.y), true))
-              .replace(/^0?/, '');
-
-      expect(decodedX).toEqual(test.x);
-      expect(decodedY).toEqual(test.y);
-    }
-  });
-
-  it('point encode decode', () => {
-    for (const test of TEST_VECTORS) {
-      const format = EllipticCurves.formatFromString(test.format);
-      if (format !== EllipticCurves.PointFormatType.COMPRESSED) {
-        // TODO(b/214598739): Investigate compatibility of other formats with
-        // Java test vectors.
-        continue;
-      }
-      const point: JsonWebKey = {
-        kty: 'EC',
-        crv: test.curve,
-        x: Bytes.toBase64(
-            EllipticCurves.integerToByteArray(BigInt('0x' + test.x)),
-            /* websafe = */ true),
-        y: Bytes.toBase64(
-            EllipticCurves.integerToByteArray(BigInt('0x' + test.y)),
-            /* websafe = */ true),
-        ext: true,
-      };
-
-      const encodedPoint =
-          EllipticCurves.pointEncode(assertExists(point.crv), format, point);
-      const decodedPoint = EllipticCurves.pointDecode(
-          assertExists(point.crv), format, encodedPoint);
-
-      expect(decodedPoint).toEqual(point);
-    }
-  });
-
-  it('point encode decode, random points', () => {
-    for (const format of
-             [EllipticCurves.PointFormatType.UNCOMPRESSED,
-              EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED]) {
-      for (const curveType
-               of [EllipticCurves.CurveType.P256, EllipticCurves.CurveType.P384,
-                   EllipticCurves.CurveType.P521]) {
-        const curveTypeString = EllipticCurves.curveToString(curveType);
-        const x = Random.randBytes(EllipticCurves.fieldSizeInBytes(curveType));
-        const y = Random.randBytes(EllipticCurves.fieldSizeInBytes(curveType));
-        const point: JsonWebKey = {
-          kty: 'EC',
-          crv: curveTypeString,
-          x: Bytes.toBase64(x, /* websafe = */ true),
-          y: Bytes.toBase64(y, /* websafe = */ true),
-          ext: true,
-        };
-
-        const encodedPoint =
-            EllipticCurves.pointEncode(assertExists(point.crv), format, point);
-        const decodedPoint =
-            EllipticCurves.pointDecode(curveTypeString, format, encodedPoint);
-
-        expect(decodedPoint).toEqual(point);
-      }
-    }
-  });
-
-  it('ecdsa der2 ieee', function() {
-    for (const test of ECDSA_IEEE_DER_TEST_VECTORS) {
-      expect(EllipticCurves.ecdsaDer2Ieee(test.der, test.ieee.length))
-          .toEqual(test.ieee);
-    }
-  });
-
-  it('ecdsa der2 ieee with invalid signatures', function() {
-    for (const test of INVALID_DER_ECDSA_SIGNATURES) {
-      try {
-        EllipticCurves.ecdsaDer2Ieee(
-            Bytes.fromHex(test), 1 /* ieeeLength, ignored */);
-        // Preserving old behavior when moving to
-        // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-        // tslint:disable-next-line:no-any
-      } catch (e: any) {
-        expect(e.toString())
-            .toBe('InvalidArgumentsException: invalid DER signature');
-      }
-    }
-  });
-
-  it('ecdsa ieee2 der', function() {
-    for (const test of ECDSA_IEEE_DER_TEST_VECTORS) {
-      expect(EllipticCurves.ecdsaIeee2Der(test.ieee)).toEqual(test.der);
-    }
-  });
-
-  it('is valid der ecdsa signature', function() {
-    for (const test of INVALID_DER_ECDSA_SIGNATURES) {
-      expect(EllipticCurves.isValidDerEcdsaSignature(Bytes.fromHex(test)))
-          .toBe(false);
-    }
-  });
-});
-
-/**
- * Runs the test with test vector given as an input and returns either empty
- * string or a text describing the failure.
- */
-async function runWycheproofTest(test: {
-  'tcId': number,
-  'public': JsonWebKey,
-  'private': JsonWebKey,
-  'shared': string,
-  'result': string,
-}): Promise<string> {
-  try {
-    const privateKey =
-        await EllipticCurves.importPrivateKey('ECDH', test['private']);
-    try {
-      const publicKey =
-          await EllipticCurves.importPublicKey('ECDH', test['public']);
-      const sharedSecret =
-          await EllipticCurves.computeEcdhSharedSecret(privateKey, publicKey);
-      if (test['result'] === 'invalid') {
-        return 'Fail on test ' + test['tcId'] + ': No exception thrown.\n';
-      }
-      const sharedSecretHex = Bytes.toHex(sharedSecret);
-      if (sharedSecretHex !== test['shared']) {
-        return 'Fail on test ' + test['tcId'] + ': unexpected result was "' +
-            sharedSecretHex + '".\n';
-      }
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      if (test['result'] === 'valid') {
-        return 'Fail on test ' + test['tcId'] + ': unexpected exception "' +
-            e.toString() + '".\n';
-      }
-    }
-    // Preserving old behavior when moving to
-    // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-    // tslint:disable-next-line:no-any
-  } catch (e: any) {
-    if (test['result'] === 'valid') {
-      if (test['private']['crv'] == "P-256K") {
-        // P-256K doesn't have to be supported. Hence failing to import the
-        // key is OK.
-        return '';
-      }
-      return 'Fail on test ' + test['tcId'] +
-          ': unexpected exception trying to import private key "' +
-          e.toString() + '".\n';
-    }
-  }
-  // If the test passes return an empty string.
-  return '';
-}
-
-class TestKey {
-  constructor(
-      readonly curve: string, readonly x: string, readonly y: string,
-      readonly d?: string) {}
-}
-
-// This set of keys was generated by Java version of Tink.
-// It contains one private and one public key for each curve type supported by
-// Tink.
-const TEST_KEYS: TestKey[] = [
-  new TestKey(
-      /* curve = */ 'P-256',
-      /* x = */
-      '2eab800e5d8e9b15d0f87c55324b477ffc9382d7137599e0203113a4e41b50d0',
-      /* y = */
-      '50bb2c11cfb72f3c380c2f93ea088d6938b91bcf581cd94a73ed0a3f623a6b8b'),
-  new TestKey(
-      /* curve = */ 'P-256',
-      /* x = */
-      '844c085cc4450297b681126356e10da074dea817f69bc2b1f3d6b1fc82593c7d',
-      /* y = */
-      '3cdb41fc89867d2066cc9c4f9ad7e890152bad24de20621abfe608234cbe40f1',
-      /* opt_d = */
-      'f96796cc28b36038817cc5d7db01c52ee0411dd848dc0833e9e26e989e4a64db'),
-  new TestKey(
-      /* curve = */ 'P-384',
-      /* x = */
-      'f3290cc80faa65e8821b0bf835f51e3431a4d78dcebd81b74c53b9b704bd995df93b648d51057a9a96a654fb8332391e',
-      /* y = */
-      '7e52bb9f654781a6894ef5ae77869207fa32ddbcec4a02d27ba1ead5472b3b9f39b09e9bca7d936809c143e99c655401'),
-  new TestKey(
-      /* curve = */ 'P-384',
-      /* x = */
-      'be9df79abedb82fc0e527630955f63f2f74b4984f0a4ac063a089565393ed20ac7a784f4efa434f5b1fa1837c76c8472',
-      /* y = */
-      'cf34ad0d4f3f2cbd546780509ec7073bb26fa0547d09ed10b83bf9b90903037ac956dbd661d02ce3e397e0547356b331',
-      /* opt_d = */
-      '34d86595280a8bdca23ccd60eeac9581016e895c2bc867c26dc2f99f6d0f627ce586ad36d1d2981968d8852dc9276d12'),
-  new TestKey(
-      /* curve = */ 'P-521',
-      /* x = */
-      '012f2211ec7e634919857be3066becf20c438b84ff24501712c91c98f527b44c7b001f8611935cb1179541c2b3cc3a1fc9259d50cd4842a847ea0cafe22cd75fe788',
-      /* y = */
-      '016b5d3f5480122643a26ef9e7c7e36875f53c28167d6afc35777d32ea76127d34287325bf14779f2e4cf3864fcc951ba601cec92b03291e34db2e815d4bd6fc2045'),
-  new TestKey(
-      /* curve = */ 'P-521',
-      /* x = */
-      '01ee3aabecef323cb4581e044be21914b567c426eae18d71720a71a0b236f5324ef9666fe855f5d7986d3e33a9250396f63c780572b3ad9417d69c2a87773ce39194',
-      /* y = */
-      '0036bea90db019304719d269e5335f9790e730e241a1b02cfdab8bdcfd0bcff8bdcb3ddeb9c3a94ecff1ab6abb80b0c1655f871c6089d3a4bf8625cf6bd182897f1b',
-      /* opt_d = */
-      '00b9f9f5d91cbfa9b7f92b041b137ac9822ca4a38f71ce227f624cac6178ca8351fab24bc2cc3f85d7ab72f54a0f9d1bb11a888a79a9c7b1ca267ddc82043585e437')
-];
-
-class EcdsaIeeeDerTestVector {
-  ieee: Uint8Array;
-  der: Uint8Array;
-
-  constructor(ieee: string, der: string) {
-    this.ieee = Bytes.fromHex(ieee);
-    this.der = Bytes.fromHex(der);
-  }
-}
-const ECDSA_IEEE_DER_TEST_VECTORS: EcdsaIeeeDerTestVector[] = [
-  new EcdsaIeeeDerTestVector(  // normal case, short-form length
-      '0102030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10',
-      '302402100102030405060708090a0b0c0d0e0f1002100102030405060708090a0b0c0d0e0f10'),
-  new EcdsaIeeeDerTestVector(  // normal case, long-form length
-      '010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000203010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000203',
-      '30818802420100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000002030242010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000203'),
-  new EcdsaIeeeDerTestVector(  // zero prefix.
-      '0002030405060708090a0b0c0d0e0f100002030405060708090a0b0c0d0e0f10',
-      '3022020f02030405060708090a0b0c0d0e0f10020f02030405060708090a0b0c0d0e0f10'),
-  new EcdsaIeeeDerTestVector(  // highest bit is set.
-      '00ff030405060708090a0b0c0d0e0f1000ff030405060708090a0b0c0d0e0f10',
-      '3024021000ff030405060708090a0b0c0d0e0f10021000ff030405060708090a0b0c0d0e0f10'),
-  new EcdsaIeeeDerTestVector(  // highest bit is set, full length.
-      'ff02030405060708090a0b0c0d0e0f10ff02030405060708090a0b0c0d0e0f10',
-      '3026021100ff02030405060708090a0b0c0d0e0f10021100ff02030405060708090a0b0c0d0e0f10'),
-  new EcdsaIeeeDerTestVector(  // all zeros.
-      '0000000000000000000000000000000000000000000000000000000000000000',
-      '3006020100020100'),
-];
-
-const INVALID_DER_ECDSA_SIGNATURES: string[] = [
-  '2006020101020101',    // 1st byte is not 0x30 (SEQUENCE tag)
-  '3006050101020101',    // 3rd byte is not 0x02 (INTEGER tag)
-  '3006020101050101',    // 6th byte is not 0x02 (INTEGER tag)
-  '308206020101020101',  // long form length is not 0x81
-  '30ff020101020101',    // invalid total length
-  '3006020201020101',    // invalid rLength
-  '3006020101020201',    // invalid sLength
-  '30060201ff020101',    // no extra zero when highest bit of r is set
-  '30060201010201ff',    // no extra zero when highest bit of s is set
-];
-
-// Following test vectors copied from 'testVectors2' variable in
-// /src/test/java/com/google/crypto/tink/subtle/EllipticCurvesTest.java.
-class TestVector {
-  constructor(
-      readonly curve: string, readonly format: string, readonly x: string,
-      readonly y: string, readonly encoded: string) {}
-}
-
-const TEST_VECTORS:
-    TestVector[] =
-        [
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */
-              'b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a',
-              /* y = */
-              '1886ccdca5487a6772f9401888203f90587cc00a730e2b83d5c6f89b3b568df7',
-              /* encoded = */
-              '04b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a1886ccdca5487a6772f9401888203f90587cc00a730e2b83d5c6f89b3b568df7',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'DO_NOT_USE_CRUNCHY_UNCOMPRESSED',
-              /* x = */
-              'b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a',
-              /* y = */
-              '1886ccdca5487a6772f9401888203f90587cc00a730e2b83d5c6f89b3b568df7',
-              /* encoded = */
-              'b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a1886ccdca5487a6772f9401888203f90587cc00a730e2b83d5c6f89b3b568df7',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'COMPRESSED',
-              /* x = */
-              'b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a',
-              /* y = */
-              '1886ccdca5487a6772f9401888203f90587cc00a730e2b83d5c6f89b3b568df7',
-              /* encoded = */
-              '03b0cfc7bc02fc980d858077552947ffb449b10df8949dee4e56fe21e016dcb25a',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              '66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4',
-              /* encoded = */
-              '04000000000000000000000000000000000000000000000000000000000000000066485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'DO_NOT_USE_CRUNCHY_UNCOMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              '66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4',
-              /* encoded = */
-              '000000000000000000000000000000000000000000000000000000000000000066485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'COMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              '66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4',
-              /* encoded = */
-              '020000000000000000000000000000000000000000000000000000000000000000',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */
-              'ffffffff00000001000000000000000000000000fffffffffffffffffffffffc',
-              /* y = */
-              '19719bebf6aea13f25c96dfd7c71f5225d4c8fc09eb5a0ab9f39e9178e55c121',
-              /* encoded = */
-              '04ffffffff00000001000000000000000000000000fffffffffffffffffffffffc19719bebf6aea13f25c96dfd7c71f5225d4c8fc09eb5a0ab9f39e9178e55c121',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'DO_NOT_USE_CRUNCHY_UNCOMPRESSED',
-              /* x = */
-              'ffffffff00000001000000000000000000000000fffffffffffffffffffffffc',
-              /* y = */
-              '19719bebf6aea13f25c96dfd7c71f5225d4c8fc09eb5a0ab9f39e9178e55c121',
-              /* encoded = */
-              'ffffffff00000001000000000000000000000000fffffffffffffffffffffffc19719bebf6aea13f25c96dfd7c71f5225d4c8fc09eb5a0ab9f39e9178e55c121',
-              ),
-          new TestVector(
-              /* curve = */ 'P-256',
-              /* format = */ 'COMPRESSED',
-              /* x = */
-              'ffffffff00000001000000000000000000000000fffffffffffffffffffffffc',
-              /* y = */
-              '19719bebf6aea13f25c96dfd7c71f5225d4c8fc09eb5a0ab9f39e9178e55c121',
-              /* encoded = */
-              '03ffffffff00000001000000000000000000000000fffffffffffffffffffffffc',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */
-              'aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7',
-              /* y = */
-              '3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f',
-              /* encoded = */
-              '04aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'COMPRESSED',
-              /* x = */
-              'aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7',
-              /* y = */
-              '3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f',
-              /* encoded = */
-              '03aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              '3cf99ef04f51a5ea630ba3f9f960dd593a14c9be39fd2bd215d3b4b08aaaf86bbf927f2c46e52ab06fb742b8850e521e',
-              /* encoded = */
-              '040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003cf99ef04f51a5ea630ba3f9f960dd593a14c9be39fd2bd215d3b4b08aaaf86bbf927f2c46e52ab06fb742b8850e521e',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'COMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              '3cf99ef04f51a5ea630ba3f9f960dd593a14c9be39fd2bd215d3b4b08aaaf86bbf927f2c46e52ab06fb742b8850e521e',
-              /* encoded = */
-              '02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */ '2',
-              /* y = */
-              '732152442fb6ee5c3e6ce1d920c059bc623563814d79042b903ce60f1d4487fccd450a86da03f3e6ed525d02017bfdb3',
-              /* encoded = */
-              '04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002732152442fb6ee5c3e6ce1d920c059bc623563814d79042b903ce60f1d4487fccd450a86da03f3e6ed525d02017bfdb3',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'COMPRESSED',
-              /* x = */ '2',
-              /* y = */
-              '732152442fb6ee5c3e6ce1d920c059bc623563814d79042b903ce60f1d4487fccd450a86da03f3e6ed525d02017bfdb3',
-              /* encoded = */
-              '03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */
-              'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc',
-              /* y = */
-              '2de9de09a95b74e6b2c430363e1afb8dff7164987a8cfe0a0d5139250ac02f797f81092a9bdc0e09b574a8f43bf80c17',
-              /* encoded = */
-              '04fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc2de9de09a95b74e6b2c430363e1afb8dff7164987a8cfe0a0d5139250ac02f797f81092a9bdc0e09b574a8f43bf80c17',
-              ),
-          new TestVector(
-              /* curve = */ 'P-384',
-              /* format = */ 'COMPRESSED',
-              /* x = */
-              'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc',
-              /* y = */
-              '2de9de09a95b74e6b2c430363e1afb8dff7164987a8cfe0a0d5139250ac02f797f81092a9bdc0e09b574a8f43bf80c17',
-              /* encoded = */
-              '03fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */
-              'c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66',
-              /* y = */
-              '11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650',
-              /* encoded = */
-              '0400c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'COMPRESSED',
-              /* x = */
-              'c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66',
-              /* y = */
-              '11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650',
-              /* encoded = */
-              '0200c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              'd20ec9fea6b577c10d26ca1bb446f40b299e648b1ad508aad068896fee3f8e614bc63054d5772bf01a65d412e0bcaa8e965d2f5d332d7f39f846d440ae001f4f87',
-              /* encoded = */
-              '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d20ec9fea6b577c10d26ca1bb446f40b299e648b1ad508aad068896fee3f8e614bc63054d5772bf01a65d412e0bcaa8e965d2f5d332d7f39f846d440ae001f4f87',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'COMPRESSED',
-              /* x = */ '0',
-              /* y = */
-              'd20ec9fea6b577c10d26ca1bb446f40b299e648b1ad508aad068896fee3f8e614bc63054d5772bf01a65d412e0bcaa8e965d2f5d332d7f39f846d440ae001f4f87',
-              /* encoded = */
-              '03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */ '1',
-              /* y = */
-              '10e59be93c4f269c0269c79e2afd65d6aeaa9b701eacc194fb3ee03df47849bf550ec636ebee0ddd4a16f1cd9406605af38f584567770e3f272d688c832e843564',
-              /* encoded = */
-              '040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010010e59be93c4f269c0269c79e2afd65d6aeaa9b701eacc194fb3ee03df47849bf550ec636ebee0ddd4a16f1cd9406605af38f584567770e3f272d688c832e843564',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'COMPRESSED',
-              /* x = */ '1',
-              /* y = */
-              '10e59be93c4f269c0269c79e2afd65d6aeaa9b701eacc194fb3ee03df47849bf550ec636ebee0ddd4a16f1cd9406605af38f584567770e3f272d688c832e843564',
-              /* encoded = */
-              '02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */ '2',
-              /* y = */
-              'd9254fdf800496acb33790b103c5ee9fac12832fe546c632225b0f7fce3da4574b1a879b623d722fa8fc34d5fc2a8731aad691a9a8bb8b554c95a051d6aa505acf',
-              /* encoded = */
-              '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200d9254fdf800496acb33790b103c5ee9fac12832fe546c632225b0f7fce3da4574b1a879b623d722fa8fc34d5fc2a8731aad691a9a8bb8b554c95a051d6aa505acf',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'COMPRESSED',
-              /* x = */ '2',
-              /* y = */
-              'd9254fdf800496acb33790b103c5ee9fac12832fe546c632225b0f7fce3da4574b1a879b623d722fa8fc34d5fc2a8731aad691a9a8bb8b554c95a051d6aa505acf',
-              /* encoded = */
-              '03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'UNCOMPRESSED',
-              /* x = */
-              '1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd',
-              /* y = */
-              '10e59be93c4f269c0269c79e2afd65d6aeaa9b701eacc194fb3ee03df47849bf550ec636ebee0ddd4a16f1cd9406605af38f584567770e3f272d688c832e843564',
-              /* encoded = */
-              '0401fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd0010e59be93c4f269c0269c79e2afd65d6aeaa9b701eacc194fb3ee03df47849bf550ec636ebee0ddd4a16f1cd9406605af38f584567770e3f272d688c832e843564',
-              ),
-          new TestVector(
-              /* curve = */ 'P-521',
-              /* format = */ 'COMPRESSED',
-              /* x = */
-              '1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd',
-              /* y = */
-              '10e59be93c4f269c0269c79e2afd65d6aeaa9b701eacc194fb3ee03df47849bf550ec636ebee0ddd4a16f1cd9406605af38f584567770e3f272d688c832e843564',
-              /* encoded = */
-              '0201fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd',
-              )
-        ];
diff --git a/javascript/subtle/encrypt_then_authenticate.ts b/javascript/subtle/encrypt_then_authenticate.ts
deleted file mode 100644
index 03a9a65..0000000
--- a/javascript/subtle/encrypt_then_authenticate.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {Aead} from '../aead/internal/aead';
-import {SecurityException} from '../exception/security_exception';
-import {Mac} from '../mac/internal/mac';
-
-import * as aesCtr from './aes_ctr';
-import * as Bytes from './bytes';
-import * as hmac from './hmac';
-import {IndCpaCipher} from './ind_cpa_cipher';
-import * as Validators from './validators';
-
-/**
- * This primitive performs an encrypt-then-Mac operation on plaintext and
- * additional authenticated data (aad).
- *
- * The Mac is computed over `aad || ciphertext || size of aad`, thus it
- * doesn't violate https://en.wikipedia.org/wiki/Horton_Principle.
- *
- * This implementation is based on
- * http://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05.
- *
- * @final
- */
-export class EncryptThenAuthenticate extends Aead {
-  /**
-   * @param ivSize the IV size in bytes
-   * @param tagSize the MAC tag size in bytes
-   * @throws {InvalidArgumentsException}
-   */
-  constructor(
-      private readonly cipher: IndCpaCipher, private readonly ivSize: number,
-      private readonly mac: Mac, private readonly tagSize: number) {
-    super();
-  }
-
-  /**
-   * The plaintext is encrypted with an {@link IndCpaCipher}, then MAC
-   * is computed over `aad || ciphertext || t` where t is aad's length in bits
-   * represented as 64-bit bigendian unsigned integer. The final ciphertext
-   * format is `ind-cpa ciphertext || mac`.
-   *
-   */
-  async encrypt(plaintext: Uint8Array, associatedData = new Uint8Array(0)):
-      Promise<Uint8Array> {
-    Validators.requireUint8Array(plaintext);
-    const payload = await this.cipher.encrypt(plaintext);
-    Validators.requireUint8Array(associatedData);
-    const aadLength = Bytes.fromNumber(associatedData.length * 8);
-    const mac = await this.mac.computeMac(
-        Bytes.concat(associatedData, payload, aadLength));
-    if (this.tagSize != mac.length) {
-      throw new SecurityException(
-          'invalid tag size, expected ' + this.tagSize + ' but got ' +
-          mac.length);
-    }
-    return Bytes.concat(payload, mac);
-  }
-
-  /**
-   */
-  async decrypt(ciphertext: Uint8Array, associatedData = new Uint8Array(0)):
-      Promise<Uint8Array> {
-    Validators.requireUint8Array(ciphertext);
-    if (ciphertext.length < this.ivSize + this.tagSize) {
-      throw new SecurityException('ciphertext too short');
-    }
-    const payload = new Uint8Array(
-        ciphertext.subarray(0, ciphertext.length - this.tagSize));
-    Validators.requireUint8Array(associatedData);
-    const aadLength = Bytes.fromNumber(associatedData.length * 8);
-    const input = Bytes.concat(associatedData, payload, aadLength);
-    const tag = new Uint8Array(ciphertext.subarray(payload.length));
-    const isValidMac = await this.mac.verifyMac(tag, input);
-    if (!isValidMac) {
-      throw new SecurityException('invalid MAC');
-    }
-    return this.cipher.decrypt(payload);
-  }
-}
-
-/**
- * @param ivSize the size of the IV
- * @param hmacHashAlgo accepted names are SHA-1, SHA-256 and SHA-512
- * @param tagSize the size of the tag
- * @throws {InvalidArgumentsException}
- * @static
- */
-export async function aesCtrHmacFromRawKeys(
-    aesKey: Uint8Array, ivSize: number, hmacHashAlgo: string,
-    hmacKey: Uint8Array, tagSize: number): Promise<EncryptThenAuthenticate> {
-  Validators.requireUint8Array(aesKey);
-  Validators.requireUint8Array(hmacKey);
-  const cipher = await aesCtr.fromRawKey(aesKey, ivSize);
-  const mac = await hmac.fromRawKey(hmacHashAlgo, hmacKey, tagSize);
-  return new EncryptThenAuthenticate(cipher, ivSize, mac, tagSize);
-}
diff --git a/javascript/subtle/encrypt_then_authenticate_test.ts b/javascript/subtle/encrypt_then_authenticate_test.ts
deleted file mode 100644
index 00a76e2..0000000
--- a/javascript/subtle/encrypt_then_authenticate_test.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Bytes from './bytes';
-import {aesCtrHmacFromRawKeys} from './encrypt_then_authenticate';
-import * as Random from './random';
-
-describe('encrypt then authenticate test', function() {
-  beforeEach(function() {
-    // Use a generous promise timeout for running continuously.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
-  });
-
-  afterEach(function() {
-    // Reset the promise timeout to default value.
-    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
-  });
-
-  it('basic', async function() {
-    const aead = await aesCtrHmacFromRawKeys(
-        Random.randBytes(16) /* aesKey */, 12 /* ivSize */, 'SHA-256',
-        Random.randBytes(16) /* hmacKey */, 10 /* tagSize */);
-    for (let i = 0; i < 100; i++) {
-      const msg = Random.randBytes(20);
-      let ciphertext = await aead.encrypt(msg);
-      let plaintext = await aead.decrypt(ciphertext);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-
-      ciphertext = await aead.encrypt(msg);
-      plaintext = await aead.decrypt(ciphertext);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-
-      const aad = Random.randBytes(20);
-      ciphertext = await aead.encrypt(msg, aad);
-      plaintext = await aead.decrypt(ciphertext, aad);
-      expect(Bytes.toHex(plaintext)).toBe(Bytes.toHex(msg));
-    }
-  });
-
-  it('probabilistic encryption', async function() {
-    const aead = await aesCtrHmacFromRawKeys(
-        Random.randBytes(16) /* aesKey */, 12 /* ivSize */, 'SHA-256',
-        Random.randBytes(16) /* hmacKey */, 10 /* tagSize */);
-    const msg = Random.randBytes(20);
-    const aad = Random.randBytes(20);
-    const results = new Set();
-    for (let i = 0; i < 100; i++) {
-      const ciphertext = await aead.encrypt(msg, aad);
-      results.add(Bytes.toHex(ciphertext));
-    }
-    expect(results.size).toBe(100);
-  });
-
-  it('bit flip ciphertext', async function() {
-    const aead = await aesCtrHmacFromRawKeys(
-        Random.randBytes(16) /* aesKey */, 16 /* ivSize */, 'SHA-256',
-        Random.randBytes(16) /* hmacKey */, 16 /* tagSize */);
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    for (let i = 0; i < ciphertext.length; i++) {
-      for (let j = 0; j < 8; j++) {
-        const c1 = new Uint8Array(ciphertext);
-        c1[i] = (c1[i] ^ (1 << j));
-        try {
-          await aead.decrypt(c1, aad);
-          fail('Should throw an exception.');
-          // Preserving old behavior when moving to
-          // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-          // tslint:disable-next-line:no-any
-        } catch (e: any) {
-          expect(e.toString()).toBe('SecurityException: invalid MAC');
-        }
-      }
-    }
-  });
-
-  it('bit flip aad', async function() {
-    const aead = await aesCtrHmacFromRawKeys(
-        Random.randBytes(16) /* aesKey */, 16 /* ivSize */, 'SHA-256',
-        Random.randBytes(16) /* hmacKey */, 16 /* tagSize */);
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    for (let i = 0; i < aad.length; i++) {
-      for (let j = 0; j < 8; j++) {
-        const aad1 = new Uint8Array(aad);
-        aad1[i] = (aad1[i] ^ (1 << j));
-        try {
-          await aead.decrypt(ciphertext, aad1);
-          fail('Should throw an exception.');
-          // Preserving old behavior when moving to
-          // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-          // tslint:disable-next-line:no-any
-        } catch (e: any) {
-          expect(e.toString()).toBe('SecurityException: invalid MAC');
-        }
-      }
-    }
-  });
-
-  it('truncation', async function() {
-    const aead = await aesCtrHmacFromRawKeys(
-        Random.randBytes(16) /* aesKey */, 16 /* ivSize */, 'SHA-256',
-        Random.randBytes(16) /* hmacKey */, 16 /* tagSize */);
-    const plaintext = Random.randBytes(8);
-    const aad = Random.randBytes(8);
-    const ciphertext = await aead.encrypt(plaintext, aad);
-    for (let i = 1; i <= ciphertext.length; i++) {
-      const c1 = new Uint8Array(ciphertext.buffer, 0, ciphertext.length - i);
-      try {
-        await aead.decrypt(c1, aad);
-        fail('Should throw an exception.');
-        // Preserving old behavior when moving to
-        // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-        // tslint:disable-next-line:no-any
-      } catch (e: any) {
-        if (c1.length < 32) {
-          expect(e.toString()).toBe('SecurityException: ciphertext too short');
-        } else {
-          expect(e.toString()).toBe('SecurityException: invalid MAC');
-        }
-      }
-    }
-  });
-
-  it('with rfc test vectors', async function() {
-    // Test data from
-    // https://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05. As we
-    // use CTR while RFC uses CBC mode, it's not possible to compare plaintexts.
-    // However, the test is still valueable to make sure that we correcly
-    // compute HMAC over ciphertext and aad.
-    const RFC_TEST_VECTORS = [
-      {
-        'macKey': '000102030405060708090a0b0c0d0e0f',
-        'encryptionKey': '101112131415161718191a1b1c1d1e1f',
-        'ciphertext': '1af38c2dc2b96ffdd86694092341bc04' +
-            'c80edfa32ddf39d5ef00c0b468834279' +
-            'a2e46a1b8049f792f76bfe54b903a9c9' +
-            'a94ac9b47ad2655c5f10f9aef71427e2' +
-            'fc6f9b3f399a221489f16362c7032336' +
-            '09d45ac69864e3321cf82935ac4096c8' +
-            '6e133314c54019e8ca7980dfa4b9cf1b' +
-            '384c486f3a54c51078158ee5d79de59f' +
-            'bd34d848b3d69550a67646344427ade5' +
-            '4b8851ffb598f7f80074b9473c82e2db' +
-            '652c3fa36b0a7c5b3219fab3a30bc1c4',
-        'aad': '546865207365636f6e64207072696e63' +
-            '69706c65206f66204175677573746520' +
-            '4b6572636b686f666673',
-        'hashAlgoName': 'SHA-256',
-        'ivSize': 16,
-        'tagSize': 16
-      },
-      {
-        'macKey':
-            '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
-        'encryptionKey':
-            '202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f',
-        'ciphertext': '1af38c2dc2b96ffdd86694092341bc04' +
-            '4affaaadb78c31c5da4b1b590d10ffbd' +
-            '3dd8d5d302423526912da037ecbcc7bd' +
-            '822c301dd67c373bccb584ad3e9279c2' +
-            'e6d12a1374b77f077553df829410446b' +
-            '36ebd97066296ae6427ea75c2e0846a1' +
-            '1a09ccf5370dc80bfecbad28c73f09b3' +
-            'a3b75e662a2594410ae496b2e2e6609e' +
-            '31e6e02cc837f053d21f37ff4f51950b' +
-            'be2638d09dd7a4930930806d0703b1f6' +
-            '4dd3b4c088a7f45c216839645b2012bf' +
-            '2e6269a8c56a816dbc1b267761955bc5',
-        'aad':
-            '546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673',
-        'hashAlgoName': 'SHA-512',
-        'ivSize': 16,
-        'tagSize': 32
-      },
-    ];
-    for (let i = 0; i < RFC_TEST_VECTORS.length; i++) {
-      const testVector = RFC_TEST_VECTORS[i];
-      const aead = await aesCtrHmacFromRawKeys(
-          Bytes.fromHex(testVector['encryptionKey']), testVector['ivSize'],
-          testVector['hashAlgoName'], Bytes.fromHex(testVector['macKey']),
-          testVector['tagSize']);
-      const ciphertext = Bytes.fromHex(testVector['ciphertext']);
-      const aad = Bytes.fromHex(testVector['aad']);
-      try {
-        await aead.decrypt(ciphertext, aad);
-      } catch (e) {
-        fail(e);
-      }
-    }
-  });
-});
diff --git a/javascript/subtle/hkdf.ts b/javascript/subtle/hkdf.ts
deleted file mode 100644
index fca9aa5..0000000
--- a/javascript/subtle/hkdf.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * @fileoverview An implementation of HKDF, RFC 5869.
- */
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-
-import {fromRawKey as hmacFromRawKey} from './hmac';
-import * as Validators from './validators';
-
-/**
- * Computes an HKDF.
- *
- * @param size The length of the generated pseudorandom string in
- *     bytes. The maximal size is 255 * DigestSize, where DigestSize is the size
- *     of the underlying HMAC.
- * @param hash the name of the hash function. Accepted names are SHA-1,
- *     SHA-256 and SHA-512
- * @param ikm Input keying material.
- * @param info Context and application specific
- *     information (can be a zero-length array).
- * @param opt_salt Salt value (a non-secret random
- *     value). If not provided, it is set to a string of hash length zeros.
- * @return Output keying material (okm).
- */
-export async function compute(
-    size: number, hash: string, ikm: Uint8Array, info: Uint8Array,
-    opt_salt?: Uint8Array): Promise<Uint8Array> {
-  let digestSize;
-  if (!Number.isInteger(size)) {
-    throw new InvalidArgumentsException('size must be an integer');
-  }
-  if (size <= 0) {
-    throw new InvalidArgumentsException('size must be positive');
-  }
-  switch (hash) {
-    case 'SHA-1':
-      digestSize = 20;
-      if (size > 255 * 20) {
-        throw new InvalidArgumentsException('size too large');
-      }
-      break;
-    case 'SHA-256':
-      digestSize = 32;
-      if (size > 255 * 32) {
-        throw new InvalidArgumentsException('size too large');
-      }
-      break;
-    case 'SHA-512':
-      digestSize = 64;
-      if (size > 255 * 64) {
-        throw new InvalidArgumentsException('size too large');
-      }
-      break;
-    default:
-      throw new InvalidArgumentsException(hash + ' is not supported');
-  }
-  Validators.requireUint8Array(ikm);
-  Validators.requireUint8Array(info);
-  let salt = opt_salt;
-  if (opt_salt == null || salt === undefined || salt.length == 0) {
-    salt = new Uint8Array(digestSize);
-  }
-  Validators.requireUint8Array(salt);
-
-  // Extract.
-  let hmac = await hmacFromRawKey(hash, salt, digestSize);
-  const prk = await hmac.computeMac(
-      // Pseudorandom Key
-      ikm);
-
-  // Expand
-  hmac = await hmacFromRawKey(hash, prk, digestSize);
-  let ctr = 1;
-  let pos = 0;
-  let digest = new Uint8Array(0);
-  const result = new Uint8Array(size);
-  while (true) {
-    const input = new Uint8Array(digest.length + info.length + 1);
-    input.set(digest, 0);
-    input.set(info, digest.length);
-    input[input.length - 1] = ctr;
-    digest = await hmac.computeMac(input);
-    if (pos + digest.length < size) {
-      result.set(digest, pos);
-      pos += digest.length;
-      ctr++;
-    } else {
-      result.set(digest.subarray(0, size - pos), pos);
-      break;
-    }
-  }
-  return result;
-}
diff --git a/javascript/subtle/hkdf_test.ts b/javascript/subtle/hkdf_test.ts
deleted file mode 100644
index be5fa06..0000000
--- a/javascript/subtle/hkdf_test.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Bytes from './bytes';
-import * as Hkdf from './hkdf';
-import * as Random from './random';
-
-describe('hkdf test', function() {
-  it('constructor', async function() {
-    const ikm = Random.randBytes(16);
-    const info = Random.randBytes(16);
-    try {
-      await Hkdf.compute(0, 'SHA-256', ikm, info);  // 0 output size
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be positive');
-    }
-
-    try {
-      await Hkdf.compute(-1, 'SHA-256', ikm, info);  // negative output size
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be positive');
-    }
-
-    try {
-      await Hkdf.compute(
-          /** 255 * digestSize + 1 */ (255 * 20) + 1, 'SHA-1', ikm,
-          info);  // size too large
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe('InvalidArgumentsException: size too large');
-    }
-
-    try {
-      await Hkdf.compute(
-          /** 255 * digestSize + 1 */ (255 * 32) + 1, 'SHA-256', ikm,
-          info);  // size too large
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe('InvalidArgumentsException: size too large');
-    }
-
-    try {
-      await Hkdf.compute(
-          /** 255 * digestSize + 1 */ (255 * 64) + 1, 'SHA-512', ikm,
-          info);  // size too large
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString()).toBe('InvalidArgumentsException: size too large');
-    }
-  });
-
-  it('constructor, non integer output size', async function() {
-    const ikm = Random.randBytes(16);
-    const info = Random.randBytes(16);
-    try {
-      await Hkdf.compute(NaN, 'SHA-256', ikm, info);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be an integer');
-    }
-
-    try {
-      await Hkdf.compute(1.5, 'SHA-256', ikm, info);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: size must be an integer');
-    }
-  });
-
-  it('with test vectors', async function() {
-    // Test cases are specified in Appendix A of RFC 5869.
-    const TEST_VECTORS = [
-      {
-        'hash': 'SHA-256',
-        'output':
-            '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865',
-        'outputSize': 42,
-        'ikm': '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
-        'salt': '000102030405060708090a0b0c',
-        'info': 'f0f1f2f3f4f5f6f7f8f9',
-      },
-      {
-        'hash': 'SHA-256',
-        'output':
-            'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c' +
-            '59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71' +
-            'cc30c58179ec3e87c14c01d5c1f3434f1d87',
-        'outputSize': 82,
-        'ikm':
-            '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
-            '202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f' +
-            '404142434445464748494a4b4c4d4e4f',
-        'salt':
-            '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f' +
-            '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f' +
-            'a0a1a2a3a4a5a6a7a8a9aaabacadaeaf',
-        'info':
-            'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf' +
-            'd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef' +
-            'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff',
-      },
-      // Salt is empty
-      {
-        'hash': 'SHA-256',
-        'output':
-            '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d' +
-            '9d201395faa4b61a96c8',
-        'outputSize': 42,
-        'ikm': '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
-        'salt': '',
-        'info': '',
-      },
-      {
-        'hash': 'SHA-1',
-        'output':
-            '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896',
-        'outputSize': 42,
-        'ikm': '0b0b0b0b0b0b0b0b0b0b0b',
-        'salt': '000102030405060708090a0b0c',
-        'info': 'f0f1f2f3f4f5f6f7f8f9',
-      },
-      {
-        'hash': 'SHA-1',
-        'output':
-            '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe' +
-            '8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e' +
-            '927336d0441f4c4300e2cff0d0900b52d3b4',
-        'outputSize': 82,
-        'ikm':
-            '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' +
-            '202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f' +
-            '404142434445464748494a4b4c4d4e4f',
-        'salt':
-            '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f' +
-            '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f' +
-            'a0a1a2a3a4a5a6a7a8a9aaabacadaeaf',
-        'info':
-            'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf' +
-            'd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef' +
-            'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff',
-      },
-      // Salt is empty
-      {
-        'hash': 'SHA-1',
-        'output':
-            '0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0' +
-            'ea00033de03984d34918',
-        'outputSize': 42,
-        'ikm': '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
-        'salt': '',
-        'info': '',
-      },
-      // Salt is empty
-      {
-        'hash': 'SHA-1',
-        'output':
-            '2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5' +
-            '673a081d70cce7acfc48',
-        'outputSize': 42,
-        'ikm': '0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c',
-        'salt': '',
-        'info': '',
-      },
-    ];
-    for (let i = 0; i < TEST_VECTORS.length; i++) {
-      const testVector = TEST_VECTORS[i];
-      const ikm = Bytes.fromHex(testVector['ikm']);
-      const salt = Bytes.fromHex(testVector['salt']);
-      const info = Bytes.fromHex(testVector['info']);
-      const hkdf = await Hkdf.compute(
-          testVector['outputSize'], testVector['hash'], ikm, info, salt);
-      expect(testVector['output']).toBe(Bytes.toHex(hkdf));
-    }
-  });
-});
diff --git a/javascript/subtle/hmac.ts b/javascript/subtle/hmac.ts
deleted file mode 100644
index 23c77b7..0000000
--- a/javascript/subtle/hmac.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-import {Mac} from '../mac/internal/mac';
-
-import * as Bytes from './bytes';
-import * as Validators from './validators';
-
-/**
- * The minimum tag size.
- *
- */
-const MIN_TAG_SIZE_IN_BYTES: number = 10;
-
-/**
- * Implementation of HMAC.
- *
- * @final
- */
-export class Hmac extends Mac {
-  /**
-   * @param hash accepted names are SHA-1, SHA-256 and SHA-512
-   * @param tagSize the size of the tag
-   */
-  constructor(
-      private readonly hash: string, private readonly key: CryptoKey,
-      private readonly tagSize: number) {
-    super();
-  }
-
-  /**
-   */
-  async computeMac(data: Uint8Array): Promise<Uint8Array> {
-    Validators.requireUint8Array(data);
-    const tag = await self.crypto.subtle.sign(
-        {'name': 'HMAC', 'hash': {'name': this.hash}}, this.key, data);
-    return new Uint8Array(tag.slice(0, this.tagSize));
-  }
-
-  /**
-   */
-  async verifyMac(tag: Uint8Array, data: Uint8Array): Promise<boolean> {
-    Validators.requireUint8Array(tag);
-    Validators.requireUint8Array(data);
-    const computedTag = await this.computeMac(data);
-    return Bytes.isEqual(tag, computedTag);
-  }
-}
-
-/**
- * @param hash accepted names are SHA-1, SHA-256 and SHA-512
- * @param tagSize the size of the tag
- */
-export async function fromRawKey(
-    hash: string, key: Uint8Array, tagSize: number): Promise<Mac> {
-  Validators.requireUint8Array(key);
-  if (!Number.isInteger(tagSize)) {
-    throw new InvalidArgumentsException('invalid tag size, must be an integer');
-  }
-  if (tagSize < MIN_TAG_SIZE_IN_BYTES) {
-    throw new InvalidArgumentsException(
-        'tag too short, must be at least ' + MIN_TAG_SIZE_IN_BYTES + ' bytes');
-  }
-  switch (hash) {
-    case 'SHA-1':
-      if (tagSize > 20) {
-        throw new InvalidArgumentsException(
-            'tag too long, must not be larger than 20 bytes');
-      }
-      break;
-    case 'SHA-256':
-      if (tagSize > 32) {
-        throw new InvalidArgumentsException(
-            'tag too long, must not be larger than 32 bytes');
-      }
-      break;
-    case 'SHA-512':
-      if (tagSize > 64) {
-        throw new InvalidArgumentsException(
-            'tag too long, must not be larger than 64 bytes');
-      }
-      break;
-    default:
-      throw new InvalidArgumentsException(hash + ' is not supported');
-  }
-
-  // TODO(b/115974209): Add check that key.length > 16.
-  const cryptoKey = await self.crypto.subtle.importKey(
-      'raw', key,
-      {'name': 'HMAC', 'hash': {'name': hash}, 'length': key.length * 8}, false,
-      ['sign', 'verify']);
-  return new Hmac(hash, cryptoKey, tagSize);
-}
diff --git a/javascript/subtle/hmac_test.ts b/javascript/subtle/hmac_test.ts
deleted file mode 100644
index f629c42..0000000
--- a/javascript/subtle/hmac_test.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Bytes from './bytes';
-import {fromRawKey as hmacFromRawKey} from './hmac';
-import * as Random from './random';
-
-describe('hmac test', function() {
-  it('basic', async function() {
-    const key = Random.randBytes(16);
-    const msg = Random.randBytes(4);
-    const hmac = await hmacFromRawKey('SHA-1', key, 10);
-    const tag = await hmac.computeMac(msg);
-    expect(tag.length).toBe(10);
-    expect(await hmac.verifyMac(tag, msg)).toBe(true);
-  });
-
-  it('constructor', async function() {
-    try {
-      await hmacFromRawKey(
-          'blah', Random.randBytes(16), 16);  // invalid HMAC algo name
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('InvalidArgumentsException: blah is not supported');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-1', Random.randBytes(15), 16);  // invalid key size
-      // TODO(b/115974209): This case does not throw an exception.
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: key too short, must be at least 16 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-1', Random.randBytes(16), 9);  // tag size too short
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: tag too short, must be at least 10 bytes');
-    }
-    try {
-      await hmacFromRawKey(
-          'SHA-1', Random.randBytes(16), 21);  // tag size too long
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: tag too long, must not be larger than 20 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-256', Random.randBytes(15), 16);  // invalid key size
-      // TODO(b/115974209): This case does not throw an exception.
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: key too short, must be at least 16 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-256', Random.randBytes(16), 9);  // tag size too short
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: tag too short, must be at least 10 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-256', Random.randBytes(16), 33);  // tag size too long
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: tag too long, must not be larger than 32 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-512', Random.randBytes(15), 16);  // invalid key size
-      // TODO(b/115974209): This case does not throw an exception.
-
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe('SecurityException: key too short, must be at least 16 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-512', Random.randBytes(16), 9);  // tag size too short
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: tag too short, must be at least 10 bytes');
-    }
-
-    try {
-      await hmacFromRawKey(
-          'SHA-512', Random.randBytes(16), 65);  // tag size too long
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: tag too long, must not be larger than 64 bytes');
-    }
-  });
-
-  it('constructor, invalid tag sizes', async function() {
-    try {
-      await hmacFromRawKey('SHA-512', Random.randBytes(16), NaN);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: invalid tag size, must be an integer');
-    }
-
-    try {
-      await hmacFromRawKey('SHA-512', Random.randBytes(16), 12.5);
-      fail('Should throw an exception.');
-      // Preserving old behavior when moving to
-      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
-      // tslint:disable-next-line:no-any
-    } catch (e: any) {
-      expect(e.toString())
-          .toBe(
-              'InvalidArgumentsException: invalid tag size, must be an integer');
-    }
-  });
-
-  it('modify', async function() {
-    const key = Random.randBytes(16);
-    const msg = Random.randBytes(8);
-    const hmac = await hmacFromRawKey('SHA-1', key, 20);
-    const tag = await hmac.computeMac(msg);
-
-    // Modify tag.
-    for (let i = 0; i < tag.length; i++) {
-      const tag1 = new Uint8Array(tag);
-      tag1[i] = tag[i] ^ 0xff;
-      expect(await hmac.verifyMac(tag1, msg)).toBe(false);
-    }
-
-    // Modify msg.
-    for (let i = 0; i < msg.length; i++) {
-      const msg1 = new Uint8Array(msg);
-      msg1[i] = msg1[i] ^ 0xff;
-      expect(await hmac.verifyMac(tag, msg1)).toBe(false);
-    }
-  });
-
-  it('with test vectors', async function() {
-    // Test data from
-    // http://csrc.nist.gov/groups/STM/cavp/message-authentication.html#testing.
-    const NIST_TEST_VECTORS = [
-      {
-        'algo': 'SHA-1',
-        'key':
-            '816aa4c3ee066310ac1e6666cf830c375355c3c8ba18cfe1f50a48c988b46272',
-        'message':
-            '220248f5e6d7a49335b3f91374f18bb8b0ff5e8b9a5853f3cfb293855d78301d' +
-            '837a0a2eb9e4f056f06c08361bd07180ee802651e69726c28910d2baef379606' +
-            '815dcbab01d0dc7acb0ba8e65a2928130da0522f2b2b3d05260885cf1c64f14c' +
-            'a3145313c685b0274bf6a1cb38e4f99895c6a8cc72fbe0e52c01766fede78a1a',
-        'tag': '17cb2e9e98b748b5ae0f7078ea5519e5'
-      },
-      {
-        'algo': 'SHA-256',
-        'key':
-            '6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95' +
-            'febbdd61236f33245',
-        'message':
-            '752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef7' +
-            '3f918f675945a9aefe26daea27587e8dc909dd56fd0468805f834039b345f855' +
-            'cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b58e4d47b8fee' +
-            'edc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b',
-        'tag': '05d1243e6465ed9620c9aec1c351a186'
-      },
-      {
-        'algo': 'SHA-512',
-        'key':
-            '726374c4b8df517510db9159b730f93431e0cd468d4f3821eab0edb93abd0fba' +
-            '46ab4f1ef35d54fec3d85fa89ef72ff3d35f22cf5ab69e205c10afcdf4aaf113' +
-            '38dbb12073474fddb556e60b8ee52f91163ba314303ee0c910e64e87fbf30221' +
-            '4edbe3f2',
-        'message':
-            'ac939659dc5f668c9969c0530422e3417a462c8b665e8db25a883a625f7aa59b' +
-            '89c5ad0ece5712ca17442d1798c6dea25d82c5db260cb59c75ae650be56569c1' +
-            'bd2d612cc57e71315917f116bbfa65a0aeb8af7840ee83d3e7101c52cf652d27' +
-            '73531b7a6bdd690b846a741816c860819270522a5b0cdfa1d736c501c583d916',
-
-        'tag':
-            'bd3d2df6f9d284b421a43e5f9cb94bc4ff88a88243f1f0133bad0fb1791f6569'
-      },
-    ];
-    for (let i = 0; i < NIST_TEST_VECTORS.length; i++) {
-      const testVector = NIST_TEST_VECTORS[i];
-      const key = Bytes.fromHex(testVector['key']);
-      const message = Bytes.fromHex(testVector['message']);
-      const tag = Bytes.fromHex(testVector['tag']);
-      const hmac = await hmacFromRawKey(testVector['algo'], key, tag.length);
-      expect(await hmac.verifyMac(tag, message)).toBe(true);
-    }
-  });
-});
diff --git a/javascript/subtle/ind_cpa_cipher.ts b/javascript/subtle/ind_cpa_cipher.ts
deleted file mode 100644
index 79926ec..0000000
--- a/javascript/subtle/ind_cpa_cipher.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * Interface for symmetric key ciphers that are indistinguishable against
- * chosen-plaintext attacks.
- *
- * Security guarantees: implementation of this interface do not provide
- * authentication, thus should not be used directly, but only to construct safer
- * primitives such as {@link tink.Aead}.
- *
- */
-export interface IndCpaCipher {
-  /**
-   * Encrypts `plaintext`.
-   *
-   * @param plaintext the plaintext to be encrypted. It must be
-   *     non-null, but can also be an empty (zero-length) byte array.
-   * @return resulting ciphertext
-   * @throws {SecurityException}
-   */
-  encrypt(plaintext: Uint8Array): Promise<Uint8Array>;
-
-  /**
-   * Decrypts ciphertext with associated authenticated data.
-   *
-   * @param ciphertext the ciphertext to be decrypted, must be
-   *     non-null.
-   * @return resulting plaintext
-   * @throws {SecurityException}
-   */
-  decrypt(ciphertext: Uint8Array): Promise<Uint8Array>;
-}
diff --git a/javascript/subtle/random.ts b/javascript/subtle/random.ts
deleted file mode 100644
index 2a3d30a..0000000
--- a/javascript/subtle/random.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-
-/**
- * @fileoverview Several simple wrappers of crypto.getRandomValues.
- */
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-
-/**
- * Randomly generates `n` bytes.
- *
- * @param n number of bytes to generate
- * @return the random bytes
- * @static
- */
-export function randBytes(n: number): Uint8Array {
-  if (!Number.isInteger(n) || n < 0) {
-    throw new InvalidArgumentsException('n must be a nonnegative integer');
-  }
-  const result = new Uint8Array(n);
-  crypto.getRandomValues(result);
-  return result;
-}
diff --git a/javascript/subtle/validators.ts b/javascript/subtle/validators.ts
deleted file mode 100644
index 73093a8..0000000
--- a/javascript/subtle/validators.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';
-import {SecurityException} from '../exception/security_exception';
-const SUPPORTED_AES_KEY_SIZES: number[] = [16, 32];
-
-/**
- * Validates AES key sizes, at the moment only 128-bit and 256-bit keys are
- * supported.
- *
- * @param n the key size in bytes
- * @throws {!InvalidArgumentsException}
- * @static
- */
-export function validateAesKeySize(n: number) {
-  if (!SUPPORTED_AES_KEY_SIZES.includes(n)) {
-    throw new InvalidArgumentsException('unsupported AES key size: ' + n);
-  }
-}
-
-/**
- * Validates that the input is a non null Uint8Array.
- *
- * @throws {!InvalidArgumentsException}
- * @static
- */
-export function requireUint8Array(input: Uint8Array) {
-  if (input == null || !(input instanceof Uint8Array)) {
-    throw new InvalidArgumentsException('input must be a non null Uint8Array');
-  }
-}
-
-/**
- * Validates version, throws exception if candidate version is negative or
- * bigger than expected.
- *
- * @param candidate - version to be validated
- * @param maxVersion - upper bound on version
- * @throws {!SecurityException}
- * @static
- */
-export function validateVersion(candidate: number, maxVersion: number) {
-  if (candidate < 0 || candidate > maxVersion) {
-    throw new SecurityException(
-        'Version is out of bound, must be ' +
-        'between 0 and ' + maxVersion + '.');
-  }
-}
-
-/**
- * Validates ECDSA parameters.
- *
- * @throws {!SecurityException}
- */
-export function validateEcdsaParams(curve: string, hash: string) {
-  switch (curve) {
-    case 'P-256':
-      if (hash != 'SHA-256') {
-        throw new SecurityException(
-            'expected SHA-256 (because curve is P-256) but got ' + hash);
-      }
-      break;
-    case 'P-384':
-      if (hash != 'SHA-384' && hash != 'SHA-512') {
-        throw new SecurityException(
-            'expected SHA-384 or SHA-512 (because curve is P-384) but got ' +
-            hash);
-      }
-      break;
-    case 'P-521':
-      if (hash != 'SHA-512') {
-        throw new SecurityException(
-            'expected SHA-512 (because curve is P-521) but got ' + hash);
-      }
-      break;
-    default:
-      throw new SecurityException('unsupported curve: ' + curve);
-  }
-}
diff --git a/javascript/testing/BUILD.bazel b/javascript/testing/BUILD.bazel
deleted file mode 100644
index d993237..0000000
--- a/javascript/testing/BUILD.bazel
+++ /dev/null
@@ -1,10 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "testing",
-    srcs = ["index.ts"],
-    module_name = "tink-crypto/testing",
-    deps = ["//internal"],
-)
diff --git a/javascript/testing/index.ts b/javascript/testing/index.ts
deleted file mode 100644
index ac116db..0000000
--- a/javascript/testing/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as Registry from '../internal/registry';
-
-export const resetRegistry = Registry.reset;
diff --git a/javascript/testing/internal/BUILD.bazel b/javascript/testing/internal/BUILD.bazel
deleted file mode 100644
index 8acd75e..0000000
--- a/javascript/testing/internal/BUILD.bazel
+++ /dev/null
@@ -1,13 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-
-package(default_visibility = ["//:__subpackages__"])
-
-ts_library(
-    name = "internal",
-    testonly = True,
-    srcs = ["test_utils.ts"],
-    deps = [
-        "//internal:proto",
-        "@npm//@types/jasmine",
-    ],
-)
diff --git a/javascript/testing/internal/test_utils.ts b/javascript/testing/internal/test_utils.ts
deleted file mode 100644
index cd9f191..0000000
--- a/javascript/testing/internal/test_utils.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {PbKeyData, PbKeyMaterialType, PbKeyset, PbKeysetKey, PbKeyStatusType, PbMessage, PbOutputPrefixType} from '../../internal/proto';
-
-/**
- * Returns its input type-narrowed not to be null or undefined. Throws a failed
- * test assertion if it's null or undefined at runtime.
- */
-export function assertExists<T>(value: T): NonNullable<T> {
-  expect(value).toBeDefined();
-  expect(value).not.toBeNull();
-  return value as NonNullable<T>;
-}
-
-/**
- * Returns its input type-narrowed to a particular type. Throws a failed test
- * assertion if it isn't that type at runtime.
- */
-export function assertInstanceof<T>(
-    value: unknown, type: new (...args: never[]) => T): T;
-// For classes exported via ts_library_from_closure.
-// tslint:disable-next-line:no-any
-export function assertInstanceof<T>(value: unknown, type: any): any;
-export function assertInstanceof<T>(
-    value: unknown, type: new (...args: never[]) => unknown) {
-  expect(value instanceof type)
-      .withContext(`${value} should be an instance of ${type}`)
-      .toBe(true);
-  return value;
-}
-
-/**
- * Creates a key for testing purposes. Generates a new key with id, output
- * prefix type and status given by optional arguments. The default values are
- * the following: id = 0x12345678, output prefix type = TINK, and status =
- * ENABLED.
- *
- *
- */
-export function createKey(
-    keyId: number = 305419896, legacy: boolean = false, enabled: boolean = true,
-    keyMaterialType: PbKeyMaterialType =
-        PbKeyData.KeyMaterialType.SYMMETRIC): PbKeysetKey {
-  const key = new PbKeysetKey();
-  if (enabled) {
-    key.setStatus(PbKeyStatusType.ENABLED);
-  } else {
-    key.setStatus(PbKeyStatusType.DISABLED);
-  }
-  if (legacy) {
-    key.setOutputPrefixType(PbOutputPrefixType.LEGACY);
-  } else {
-    key.setOutputPrefixType(PbOutputPrefixType.TINK);
-  }
-  key.setKeyId(keyId);
-  const keyData = (new PbKeyData())
-                      .setTypeUrl('someTypeUrl')
-                      .setValue(new Uint8Array(10))
-                      .setKeyMaterialType(keyMaterialType);
-  key.setKeyData(keyData);
-  return key;
-}
-
-/**
- * Returns a valid PbKeyset whose primary key has id equal to 1.
- *
- *
- */
-export function createKeyset(
-    keysetSize: number = 20, keyMaterialType?: PbKeyMaterialType): PbKeyset {
-  const keyset = new PbKeyset();
-  for (let i = 0; i < keysetSize; i++) {
-    const key = createKey(
-        /* legacy = */
-        i + 1, i % 2 < 1,
-        /* enabled = */
-        i % 4 < 2, keyMaterialType);
-    keyset.addKey(key);
-  }
-  keyset.setPrimaryKeyId(1);
-  return keyset;
-}
-
-/** Asserts that two protos are equal. */
-export function assertMessageEquals<T extends PbMessage>(m1: T, m2: T) {
-  expect(PbMessage.equals(m1, m2)).toBeTrue();
-}
diff --git a/javascript/tink_javascript_deps.bzl b/javascript/tink_javascript_deps.bzl
deleted file mode 100644
index be04773..0000000
--- a/javascript/tink_javascript_deps.bzl
+++ /dev/null
@@ -1,66 +0,0 @@
-"""Dependencies of TypeScript/JavaScript Tink."""
-
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-def tink_javascript_deps():
-    """Load dependencies of TypeScript/JavaScript Tink."""
-
-    # proto
-    # proto_library, cc_proto_library and java_proto_library rules implicitly depend
-    # on @com_google_protobuf//:proto, @com_google_protobuf//:cc_toolchain and
-    # @com_google_protobuf//:java_toolchain, respectively.
-    # This statement defines the @com_google_protobuf repo.
-    # Release from 2021-06-08
-    if not native.existing_rule("com_google_protobuf"):
-        http_archive(
-            name = "com_google_protobuf",
-            strip_prefix = "protobuf-3.19.3",
-            urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.3.zip"],
-            sha256 = "6b6bf5cd8d0cca442745c4c3c9f527c83ad6ef35a405f64db5215889ac779b42",
-        )
-
-    if not native.existing_rule("build_bazel_rules_nodejs"):
-        # Release from 2021-10-11
-        http_archive(
-            name = "build_bazel_rules_nodejs",
-            urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/4.4.0/rules_nodejs-4.4.0.tar.gz"],
-            sha256 = "c9c5d60d6234d65b06f86abd5edc60cadd1699f739ee49d33a099d2d67eb1ae8",
-        )
-
-    if not native.existing_rule("io_bazel_rules_closure"):
-        # Tag from 2021-06-11
-        http_archive(
-            name = "io_bazel_rules_closure",
-            strip_prefix = "rules_closure-0.12.0",
-            urls = ["https://github.com/bazelbuild/rules_closure/archive/0.12.0.tar.gz"],
-            sha256 = "9498e57368efb82b985db1ed426a767cbf1ba0398fd7aed632fc3908654e1b1e",
-        )
-
-    if not native.existing_rule("io_bazel_rules_webtesting"):
-        # Release from 2021-09-15
-        http_archive(
-            name = "io_bazel_rules_webtesting",
-            urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz"],
-            sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a",
-        )
-
-    # Basic rules we need to add to bazel.
-    if not native.existing_rule("bazel_skylib"):
-        # Release from 2021-09-27
-        http_archive(
-            name = "bazel_skylib",
-            urls = [
-                "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
-            ],
-            sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
-        )
-
-    if not native.existing_rule("wycheproof"):
-        # Commit from 2019-12-17
-        http_archive(
-            name = "wycheproof",
-            strip_prefix = "wycheproof-d8ed1ba95ac4c551db67f410c06131c3bc00a97c",
-            url = "https://github.com/google/wycheproof/archive/d8ed1ba95ac4c551db67f410c06131c3bc00a97c.zip",
-            sha256 = "eb1d558071acf1aa6d677d7f1cabec2328d1cf8381496c17185bd92b52ce7545",
-        )
diff --git a/javascript/tsconfig.json b/javascript/tsconfig.json
deleted file mode 100644
index df213b7..0000000
--- a/javascript/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-// See https://www.typescriptlang.org/tsconfig for documentation of these
-// options.
-{
-  "compilerOptions": {
-    // ES2020 (or later) is required since the subtle elliptic curve point
-    // compression functions depend on BigInt, which was added in ES2020.
-    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
-    "target": "ES2020",
-    "lib": ["ES2020", "DOM"]
-  }
-}
diff --git a/javascript/yarn.lock b/javascript/yarn.lock
deleted file mode 100644
index 62ea421..0000000
--- a/javascript/yarn.lock
+++ /dev/null
@@ -1,1588 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@angular/[email protected]":
-  version "12.2.11"
-  resolved "https://registry.yarnpkg.com/@angular/bazel/-/bazel-12.2.11.tgz#08f00f3fc775ba1248a05c607a9e1050a35a4ad6"
-  integrity sha512-QIhD//TTVudMtJZmK7uE44zEQWLIAU34DtjcXbxfpb07juREF0ni+mjFkfD8a+6jURsvhLrV2XjvjgoVv4O9qA==
-  dependencies:
-    "@microsoft/api-extractor" "7.18.4"
-    shelljs "0.8.4"
-    tsickle "^0.38.0"
-    tslib "^2.2.0"
-
-"@bazel/[email protected]":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@bazel/concatjs/-/concatjs-4.4.0.tgz#4a69345bf27391e6da58e5df58b27a81eaecf55c"
-  integrity sha512-lxA/yHKFN9q7L6lvA6GgZwceU7SjSNrGxh/AV7db+klzHfV9jHpvE/yo2yCmUi/nrOq8TC7SUJnLWqFx1rl24g==
-  dependencies:
-    protobufjs "6.8.8"
-    source-map-support "0.5.9"
-    tsutils "3.21.0"
-
-"@bazel/[email protected]":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-4.4.0.tgz#c421a0d54e3385b5218ca277c3fa8d60659b6f89"
-  integrity sha512-2qmcQ95yCWHZzbCgjJ4SnqgWfkv3dhnNfZudjro/k11txPJfKbWgUK/PCzcqOTiwR9vkzV3QJTGV9jdNbkT3bw==
-  dependencies:
-    "@bazel/worker" "4.4.0"
-    protobufjs "6.8.8"
-    semver "5.6.0"
-    source-map-support "0.5.9"
-    tsutils "3.21.0"
-
-"@bazel/[email protected]":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-4.4.0.tgz#d4f469ece137727244fc7798f6455efe0d653903"
-  integrity sha512-ASehVjUWF13wAFPhycT8/s1VIBg/jsG1D+EjdA8oNpuNacsDSmbJJu5a00dSqjp2GM0jTR3Vmj/SRKgYrvYSuA==
-  dependencies:
-    google-protobuf "^3.6.1"
-
-"@jridgewell/gen-mapping@^0.3.0":
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
-  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
-  dependencies:
-    "@jridgewell/set-array" "^1.0.1"
-    "@jridgewell/sourcemap-codec" "^1.4.10"
-    "@jridgewell/trace-mapping" "^0.3.9"
-
-"@jridgewell/resolve-uri@^3.0.3":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
-  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
-
-"@jridgewell/set-array@^1.0.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
-  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
-
-"@jridgewell/source-map@^0.3.2":
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
-  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
-  dependencies:
-    "@jridgewell/gen-mapping" "^0.3.0"
-    "@jridgewell/trace-mapping" "^0.3.9"
-
-"@jridgewell/sourcemap-codec@^1.4.10":
-  version "1.4.14"
-  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
-  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
-
-"@jridgewell/trace-mapping@^0.3.9":
-  version "0.3.14"
-  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
-  integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
-  dependencies:
-    "@jridgewell/resolve-uri" "^3.0.3"
-    "@jridgewell/sourcemap-codec" "^1.4.10"
-
-"@microsoft/[email protected]":
-  version "7.13.4"
-  resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.13.4.tgz#bff4a52a35da5d9896650041d4f7a769c970da60"
-  integrity sha512-NYaR3hJinh089/Gkee8fvmEFf9zKkoUvNxgkqUlKBCDXH2+Ou4tNDuL8G6zjhKBPicHkp2VcL8l7q9H6txUkjQ==
-  dependencies:
-    "@microsoft/tsdoc" "0.13.2"
-    "@microsoft/tsdoc-config" "~0.15.2"
-    "@rushstack/node-core-library" "3.39.1"
-
-"@microsoft/[email protected]":
-  version "7.18.4"
-  resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.18.4.tgz#2d7641b36d323b4ac710d838a972be7e4f14d32b"
-  integrity sha512-Wx45VuIAu09Pk9Qwzt0I57OX31BaWO2r6+mfSXqYFsJjYTqwUkdFh92G1GKYgvuR9oF/ai7w10wrFpx5WZYbGg==
-  dependencies:
-    "@microsoft/api-extractor-model" "7.13.4"
-    "@microsoft/tsdoc" "0.13.2"
-    "@microsoft/tsdoc-config" "~0.15.2"
-    "@rushstack/node-core-library" "3.39.1"
-    "@rushstack/rig-package" "0.2.13"
-    "@rushstack/ts-command-line" "4.8.1"
-    colors "~1.2.1"
-    lodash "~4.17.15"
-    resolve "~1.17.0"
-    semver "~7.3.0"
-    source-map "~0.6.1"
-    typescript "~4.3.5"
-
-"@microsoft/tsdoc-config@~0.15.2":
-  version "0.15.2"
-  resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz#eb353c93f3b62ab74bdc9ab6f4a82bcf80140f14"
-  integrity sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA==
-  dependencies:
-    "@microsoft/tsdoc" "0.13.2"
-    ajv "~6.12.6"
-    jju "~1.4.0"
-    resolve "~1.19.0"
-
-"@microsoft/[email protected]":
-  version "0.13.2"
-  resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz#3b0efb6d3903bd49edb073696f60e90df08efb26"
-  integrity sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==
-
-"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
-  integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
-
-"@protobufjs/base64@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
-  integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
-
-"@protobufjs/codegen@^2.0.4":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
-  integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
-
-"@protobufjs/eventemitter@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
-  integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
-
-"@protobufjs/fetch@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
-  integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
-  dependencies:
-    "@protobufjs/aspromise" "^1.1.1"
-    "@protobufjs/inquire" "^1.1.0"
-
-"@protobufjs/float@^1.0.2":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
-  integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
-
-"@protobufjs/inquire@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
-  integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
-
-"@protobufjs/path@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
-  integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
-
-"@protobufjs/pool@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
-  integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
-
-"@protobufjs/utf8@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
-  integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
-
-"@rollup/pluginutils@^3.0.9":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
-  integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
-  dependencies:
-    "@types/estree" "0.0.39"
-    estree-walker "^1.0.1"
-    picomatch "^2.2.2"
-
-"@rushstack/[email protected]":
-  version "3.39.1"
-  resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.39.1.tgz#dd1dc270e3035ac4de270f0ca80c25724ce19cc7"
-  integrity sha512-HHgMEHZTXQ3NjpQzWd5+fSt2Eod9yFwj6qBPbaeaNtDNkOL8wbLoxVimQNtcH0Qhn4wxF5u2NTDNFsxf2yd1jw==
-  dependencies:
-    "@types/node" "10.17.13"
-    colors "~1.2.1"
-    fs-extra "~7.0.1"
-    import-lazy "~4.0.0"
-    jju "~1.4.0"
-    resolve "~1.17.0"
-    semver "~7.3.0"
-    timsort "~0.3.0"
-    z-schema "~3.18.3"
-
-"@rushstack/[email protected]":
-  version "0.2.13"
-  resolved "https://registry.yarnpkg.com/@rushstack/rig-package/-/rig-package-0.2.13.tgz#418f0aeb4c9b33bd8bd2547759fc0ae91fd970c7"
-  integrity sha512-qQMAFKvfb2ooaWU9DrGIK9d8QfyHy/HiuITJbWenlKgzcDXQvQgEduk57YF4Y7LLasDJ5ZzLaaXwlfX8qCRe5Q==
-  dependencies:
-    resolve "~1.17.0"
-    strip-json-comments "~3.1.1"
-
-"@rushstack/[email protected]":
-  version "4.8.1"
-  resolved "https://registry.yarnpkg.com/@rushstack/ts-command-line/-/ts-command-line-4.8.1.tgz#c233a0226112338e58e7e4fd219247b4e7cec883"
-  integrity sha512-rmxvYdCNRbyRs+DYAPye3g6lkCkWHleqO40K8UPvUAzFqEuj6+YCVssBiOmrUDCoM5gaegSNT0wFDYhz24DWtw==
-  dependencies:
-    "@types/argparse" "1.0.38"
-    argparse "~1.0.9"
-    colors "~1.2.1"
-    string-argv "~0.3.1"
-
-"@types/[email protected]":
-  version "1.0.38"
-  resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9"
-  integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==
-
-"@types/component-emitter@^1.2.10":
-  version "1.2.11"
-  resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506"
-  integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==
-
-"@types/cookie@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
-  integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
-
-"@types/cors@^2.8.12":
-  version "2.8.12"
-  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
-  integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
-
-"@types/estree@*":
-  version "0.0.50"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
-  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
-
-"@types/[email protected]":
-  version "0.0.39"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
-  integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
-
-"@types/[email protected]":
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.10.1.tgz#9a7ceab2e539503f70a7e6a7028c70171aca2754"
-  integrity sha512-So26woGjM6F9b2julbJlXdcPdyhwteZzEX2EbFmreuJBamPVVdp6w4djywUG9TmcwjiC+ECAe+RSSBgYEOgEqQ==
-
-"@types/long@^4.0.0":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
-  integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
-
-"@types/node@*", "@types/node@>=10.0.0":
-  version "16.11.6"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae"
-  integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==
-
-"@types/[email protected]":
-  version "10.17.13"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c"
-  integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==
-
-"@types/node@^10.1.0":
-  version "10.17.60"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
-  integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
-
-"@types/[email protected]":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
-  integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==
-  dependencies:
-    "@types/node" "*"
-
-accepts@~1.3.4:
-  version "1.3.7"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
-  integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
-  dependencies:
-    mime-types "~2.1.24"
-    negotiator "0.6.2"
-
-acorn@^8.5.0:
-  version "8.7.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
-  integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
-
-ajv@~6.12.6:
-  version "6.12.6"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
-  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
-  dependencies:
-    fast-deep-equal "^3.1.1"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.4.1"
-    uri-js "^4.2.2"
-
-ansi-regex@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
-  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^4.0.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
-  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
-  dependencies:
-    color-convert "^2.0.1"
-
-anymatch@~3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
-  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
-  dependencies:
-    normalize-path "^3.0.0"
-    picomatch "^2.0.4"
-
-argparse@~1.0.9:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
-  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
-  dependencies:
-    sprintf-js "~1.0.2"
-
-atob@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
-  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
-
-balanced-match@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
-  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-arraybuffer@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c"
-  integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==
-
[email protected], base64id@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
-  integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-
-binary-extensions@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
-  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-body-parser@^1.19.0:
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
-  integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
-  dependencies:
-    bytes "3.1.0"
-    content-type "~1.0.4"
-    debug "2.6.9"
-    depd "~1.1.2"
-    http-errors "1.7.2"
-    iconv-lite "0.4.24"
-    on-finished "~2.3.0"
-    qs "6.7.0"
-    raw-body "2.4.0"
-    type-is "~1.6.17"
-
-brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
-  dependencies:
-    balanced-match "^1.0.0"
-    concat-map "0.0.1"
-
-braces@^3.0.2, braces@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
-  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
-  dependencies:
-    fill-range "^7.0.1"
-
-buffer-from@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
-  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-
-builtin-modules@^3.1.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
-  integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==
-
[email protected]:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
-  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
-
-chokidar@^3.5.1:
-  version "3.5.2"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
-  integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
-  dependencies:
-    anymatch "~3.1.2"
-    braces "~3.0.2"
-    glob-parent "~5.1.2"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.6.0"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-cliui@^7.0.2:
-  version "7.0.4"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
-  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
-  dependencies:
-    string-width "^4.2.0"
-    strip-ansi "^6.0.0"
-    wrap-ansi "^7.0.0"
-
-color-convert@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
-  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
-  dependencies:
-    color-name "~1.1.4"
-
-color-name@~1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
-  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
[email protected]:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
-  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-
-colors@~1.2.1:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc"
-  integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==
-
-commander@^2.20.0, commander@^2.7.1:
-  version "2.20.3"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
-  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
-
-component-emitter@~1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
-  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
-
[email protected]:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-connect@^3.7.0:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
-  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
-  dependencies:
-    debug "2.6.9"
-    finalhandler "1.1.2"
-    parseurl "~1.3.3"
-    utils-merge "1.0.1"
-
-content-type@~1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
-  integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-
-cookie@~0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
-  integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
-
-cors@~2.8.5:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
-  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
-custom-event@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
-  integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
-
-date-format@^4.0.5:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.5.tgz#ba385f89782c6cb114cf45dfa4704c6bb29fca51"
-  integrity sha512-zBhRiN/M0gDxUoM2xRtzTjJzSg0XEi1ofYpF84PfXeS3hN2PsGxmc7jw3DNQtFlimRbMmob5FC3G0cJq6jQQpw==
-
[email protected]:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
-  dependencies:
-    ms "2.0.0"
-
-debug@^4.3.3, debug@~4.3.1, debug@~4.3.2:
-  version "4.3.3"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
-  integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
-  dependencies:
-    ms "2.1.2"
-
-decode-uri-component@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
-
-depd@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
-  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
-
-di@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-  integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
-
-dom-serialize@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
-  integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
-  dependencies:
-    custom-event "~1.0.0"
-    ent "~2.2.0"
-    extend "^3.0.0"
-    void-elements "^2.0.0"
-
[email protected]:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-
-emoji-regex@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
-  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-encodeurl@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-
-engine.io-parser@~5.0.0:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.1.tgz#6695fc0f1e6d76ad4a48300ff80db5f6b3654939"
-  integrity sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA==
-  dependencies:
-    base64-arraybuffer "~1.0.1"
-
-engine.io@~6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.0.tgz#2b993fcd73e6b3a6abb52b40b803651cd5747cf0"
-  integrity sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg==
-  dependencies:
-    "@types/cookie" "^0.4.1"
-    "@types/cors" "^2.8.12"
-    "@types/node" ">=10.0.0"
-    accepts "~1.3.4"
-    base64id "2.0.0"
-    cookie "~0.4.1"
-    cors "~2.8.5"
-    debug "~4.3.1"
-    engine.io-parser "~5.0.0"
-    ws "~8.2.3"
-
-ent@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
-  integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
-
-escalade@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
-  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-escape-html@~1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
-
-estree-walker@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
-  integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
-
-estree-walker@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
-  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
-
-eventemitter3@^4.0.0:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
-  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
-extend@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
-  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
-
-fast-deep-equal@^3.1.1:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
-  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-
-fast-json-stable-stringify@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
-  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-
-fill-range@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
-  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
-  dependencies:
-    to-regex-range "^5.0.1"
-
[email protected]:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
-  integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    parseurl "~1.3.3"
-    statuses "~1.5.0"
-    unpipe "~1.0.0"
-
-flatted@^3.2.5:
-  version "3.2.5"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
-  integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
-
-follow-redirects@^1.0.0:
-  version "1.14.8"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
-  integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
-
-fs-extra@^10.0.1:
-  version "10.0.1"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8"
-  integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==
-  dependencies:
-    graceful-fs "^4.2.0"
-    jsonfile "^6.0.1"
-    universalify "^2.0.0"
-
-fs-extra@~7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
-  integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
-  dependencies:
-    graceful-fs "^4.1.2"
-    jsonfile "^4.0.0"
-    universalify "^0.1.0"
-
-fs.realpath@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-fsevents@~2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
-  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
-fun-map@^3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/fun-map/-/fun-map-3.3.1.tgz#6415fde3b93ad58f9ee9566236cff3e3c64b94cb"
-  integrity sha1-ZBX947k61Y+e6VZiNs/z48ZLlMs=
-
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-caller-file@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
-  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-glob-parent@~5.1.2:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
-  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
-  dependencies:
-    is-glob "^4.0.1"
-
-glob@^7.0.0, glob@^7.1.3, glob@^7.1.7:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
-  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-google-protobuf@^3.6.1:
-  version "3.19.0"
-  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.0.tgz#97f474323c92f19fd6737af1bb792e396991e0b8"
-  integrity sha512-qXGAiv3OOlaJXJNeKOBKxbBAwjsxzhx+12ZdKOkZTsqsRkyiQRmr/nBkAkqnuQ8cmA9X5NVXvObQTpHVnXE2DQ==
-
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
-  version "4.2.9"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
-  integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
-
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
-  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
-  dependencies:
-    function-bind "^1.1.1"
-
[email protected]:
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
-  integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.3"
-    setprototypeof "1.1.1"
-    statuses ">= 1.5.0 < 2"
-    toidentifier "1.0.0"
-
-http-proxy@^1.18.1:
-  version "1.18.1"
-  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
-  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
-  dependencies:
-    eventemitter3 "^4.0.0"
-    follow-redirects "^1.0.0"
-    requires-port "^1.0.0"
-
[email protected]:
-  version "0.4.24"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
-  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3"
-
-import-lazy@~4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
-  integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
-
-inflight@^1.0.4:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
-  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
-  dependencies:
-    once "^1.3.0"
-    wrappy "1"
-
-inherits@2:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
-  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
[email protected]:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
-  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
-
-interpret@^1.0.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
-  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
-
-is-binary-path@~2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
-  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
-  dependencies:
-    binary-extensions "^2.0.0"
-
-is-core-module@^2.1.0, is-core-module@^2.2.0:
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
-  integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
-  dependencies:
-    has "^1.0.3"
-
-is-docker@^2.0.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
-  integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
-
-is-extglob@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
-  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
-
-is-fullwidth-code-point@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
-  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1, is-glob@~4.0.1:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
-  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
-  dependencies:
-    is-extglob "^2.1.1"
-
-is-module@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
-  integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
-
-is-number@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
-  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-is-reference@^1.1.2:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
-  integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
-  dependencies:
-    "@types/estree" "*"
-
-is-wsl@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
-  integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
-  dependencies:
-    is-docker "^2.0.0"
-
-isbinaryfile@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf"
-  integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==
-
-isexe@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
-  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-
[email protected], jasmine-core@^3.6.0:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.10.1.tgz#7aa6fa2b834a522315c651a128d940eca553989a"
-  integrity sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==
-
-jju@~1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
-  integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo=
-
-json-schema-traverse@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
-  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-
-jsonfile@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
-  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
-jsonfile@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
-  integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
-  dependencies:
-    universalify "^2.0.0"
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
[email protected]:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
-  integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==
-  dependencies:
-    which "^1.2.1"
-
[email protected]:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-2.1.1.tgz#6457226f8e4f091b664cef79bb5d39bf1e008765"
-  integrity sha512-VzDMgPseXak9DtfyE1O5bB2BwsMy1zzO1kUxVW1rP0yhC4tDNJ0p3JoFdzvrK4QqVzdqUMa9Rx9YzkdFp8hz3Q==
-  dependencies:
-    is-wsl "^2.2.0"
-    which "^2.0.1"
-
[email protected]:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82"
-  integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==
-  dependencies:
-    jasmine-core "^3.6.0"
-
[email protected]:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/karma-json-result-reporter/-/karma-json-result-reporter-1.0.0.tgz#9bfaa17d610470d08556e48ccbaf64d7950c7255"
-  integrity sha1-m/qhfWEEcNCFVuSMy69k15UMclU=
-  dependencies:
-    fun-map "^3.3.1"
-
[email protected]:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/karma-requirejs/-/karma-requirejs-1.1.0.tgz#fddae2cb87d7ebc16fb0222893564d7fee578798"
-  integrity sha1-/driy4fX68FvsCIok1ZNf+5Xh5g=
-
[email protected]:
-  version "0.3.8"
-  resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz#d4bae72fb7a8397328a62b75013d2df937bdcf9c"
-  integrity sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==
-  dependencies:
-    graceful-fs "^4.1.2"
-
[email protected]:
-  version "6.3.16"
-  resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.16.tgz#76d1a705fd1cf864ee5ed85270b572641e0958ef"
-  integrity sha512-nEU50jLvDe5yvXqkEJRf8IuvddUkOY2x5Xc4WXHz6dxINgGDrgD2uqQWeVrJs4hbfNaotn+HQ1LZJ4yOXrL7xQ==
-  dependencies:
-    body-parser "^1.19.0"
-    braces "^3.0.2"
-    chokidar "^3.5.1"
-    colors "1.4.0"
-    connect "^3.7.0"
-    di "^0.0.1"
-    dom-serialize "^2.2.1"
-    glob "^7.1.7"
-    graceful-fs "^4.2.6"
-    http-proxy "^1.18.1"
-    isbinaryfile "^4.0.8"
-    lodash "^4.17.21"
-    log4js "^6.4.1"
-    mime "^2.5.2"
-    minimatch "^3.0.4"
-    mkdirp "^0.5.5"
-    qjobs "^1.2.0"
-    range-parser "^1.2.1"
-    rimraf "^3.0.2"
-    socket.io "^4.2.0"
-    source-map "^0.6.1"
-    tmp "^0.2.1"
-    ua-parser-js "^0.7.30"
-    yargs "^16.1.1"
-
-lodash.get@^4.0.0:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
-
-lodash.isequal@^4.0.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
-  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
-
-lodash@^4.17.21, lodash@~4.17.15:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-log4js@^6.4.1:
-  version "6.4.3"
-  resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.4.3.tgz#8bddd981846873895bcc55c0961560c7214a8ad7"
-  integrity sha512-H/oQKcCVIhQ8zCtUh5aftdp9eRpGyVB1M5sKzAJ0i10q5jS+YXk133vtLgzT1RIoWMbIn7QD1LUto8a1hqh6gA==
-  dependencies:
-    date-format "^4.0.5"
-    debug "^4.3.3"
-    flatted "^3.2.5"
-    rfdc "^1.3.0"
-    streamroller "^3.0.5"
-
-long@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
-  integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
-
-lru-cache@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
-  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
-  dependencies:
-    yallist "^4.0.0"
-
-magic-string@^0.25.2:
-  version "0.25.7"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
-  integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
-  dependencies:
-    sourcemap-codec "^1.4.4"
-
[email protected]:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
-  integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
-
[email protected]:
-  version "1.50.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f"
-  integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==
-
-mime-types@~2.1.24:
-  version "2.1.33"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb"
-  integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==
-  dependencies:
-    mime-db "1.50.0"
-
-mime@^2.5.2:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
-  integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
-
-minimatch@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
-  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimist@^1.2.5:
-  version "1.2.6"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
-  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
-
-mkdirp@^0.5.5:
-  version "0.5.5"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
-  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
-  dependencies:
-    minimist "^1.2.5"
-
[email protected]:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
-
[email protected]:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
-  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
[email protected]:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
-  integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
-  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-object-assign@^4:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
-  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-
-on-finished@~2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
-  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
-  dependencies:
-    ee-first "1.1.1"
-
-once@^1.3.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
-  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
-  dependencies:
-    wrappy "1"
-
-parseurl@~1.3.3:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
-  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
-path-is-absolute@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-path-parse@^1.0.6:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
-  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
-
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
-  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
-
[email protected]:
-  version "6.8.8"
-  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c"
-  integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==
-  dependencies:
-    "@protobufjs/aspromise" "^1.1.2"
-    "@protobufjs/base64" "^1.1.2"
-    "@protobufjs/codegen" "^2.0.4"
-    "@protobufjs/eventemitter" "^1.1.0"
-    "@protobufjs/fetch" "^1.1.0"
-    "@protobufjs/float" "^1.0.2"
-    "@protobufjs/inquire" "^1.1.0"
-    "@protobufjs/path" "^1.1.2"
-    "@protobufjs/pool" "^1.1.0"
-    "@protobufjs/utf8" "^1.1.0"
-    "@types/long" "^4.0.0"
-    "@types/node" "^10.1.0"
-    long "^4.0.0"
-
-punycode@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
-  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-
-qjobs@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
-  integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
-
[email protected]:
-  version "6.7.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
-  integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
-
-range-parser@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
-  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-
[email protected]:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
-  integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
-  dependencies:
-    bytes "3.1.0"
-    http-errors "1.7.2"
-    iconv-lite "0.4.24"
-    unpipe "1.0.0"
-
-readdirp@~3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
-  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
-  dependencies:
-    picomatch "^2.2.1"
-
-rechoir@^0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
-  integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
-  dependencies:
-    resolve "^1.1.6"
-
-require-directory@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
-  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
[email protected]:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
-  integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
-
-requires-port@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
-  integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
-
-resolve@^1.1.6, resolve@^1.11.0, resolve@^1.11.1:
-  version "1.20.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
-  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
-  dependencies:
-    is-core-module "^2.2.0"
-    path-parse "^1.0.6"
-
-resolve@~1.17.0:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
-  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
-  dependencies:
-    path-parse "^1.0.6"
-
-resolve@~1.19.0:
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
-  integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
-  dependencies:
-    is-core-module "^2.1.0"
-    path-parse "^1.0.6"
-
-rfdc@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
-  integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
-
-rimraf@^3.0.0, rimraf@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
-  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
-  dependencies:
-    glob "^7.1.3"
-
[email protected]:
-  version "10.1.0"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb"
-  integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==
-  dependencies:
-    estree-walker "^0.6.1"
-    is-reference "^1.1.2"
-    magic-string "^0.25.2"
-    resolve "^1.11.0"
-    rollup-pluginutils "^2.8.1"
-
[email protected]:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz#730f93d10ed202473b1fb54a5997a7db8c6d8523"
-  integrity sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==
-  dependencies:
-    "@types/resolve" "0.0.8"
-    builtin-modules "^3.1.0"
-    is-module "^1.0.0"
-    resolve "^1.11.1"
-    rollup-pluginutils "^2.8.1"
-
[email protected]:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed"
-  integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==
-  dependencies:
-    "@rollup/pluginutils" "^3.0.9"
-    source-map-resolve "^0.6.0"
-
-rollup-pluginutils@^2.8.1:
-  version "2.8.2"
-  resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
-  integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
-  dependencies:
-    estree-walker "^0.6.1"
-
[email protected]:
-  version "2.58.3"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.58.3.tgz#71a08138d9515fb65043b6a18618b2ed9ac8d239"
-  integrity sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-"safer-buffer@>= 2.1.2 < 3":
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
-  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
[email protected]:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
-  integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
-
-semver@~7.3.0:
-  version "7.3.5"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
-  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
-  dependencies:
-    lru-cache "^6.0.0"
-
[email protected]:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
-  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
-
[email protected]:
-  version "0.8.4"
-  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
-  integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
-  dependencies:
-    glob "^7.0.0"
-    interpret "^1.0.0"
-    rechoir "^0.6.2"
-
-socket.io-adapter@~2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289"
-  integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==
-
-socket.io-parser@~4.0.4:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
-  integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
-  dependencies:
-    "@types/component-emitter" "^1.2.10"
-    component-emitter "~1.3.0"
-    debug "~4.3.1"
-
-socket.io@^4.2.0:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.1.tgz#c0aa14f3f916a8ab713e83a5bd20c16600245763"
-  integrity sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA==
-  dependencies:
-    accepts "~1.3.4"
-    base64id "~2.0.0"
-    debug "~4.3.2"
-    engine.io "~6.0.0"
-    socket.io-adapter "~2.3.2"
-    socket.io-parser "~4.0.4"
-
-source-map-resolve@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2"
-  integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==
-  dependencies:
-    atob "^2.1.2"
-    decode-uri-component "^0.2.0"
-
[email protected]:
-  version "0.5.9"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
-  integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
-  dependencies:
-    buffer-from "^1.0.0"
-    source-map "^0.6.0"
-
-source-map-support@~0.5.20:
-  version "0.5.20"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
-  integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==
-  dependencies:
-    buffer-from "^1.0.0"
-    source-map "^0.6.0"
-
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
-  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
-sourcemap-codec@^1.4.4:
-  version "1.4.8"
-  resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
-  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-
-sprintf-js@~1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
-  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
-
-"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
-  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-
-streamroller@^3.0.5:
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.0.5.tgz#17e348dc2a662f9f325373549ab91d55316051ab"
-  integrity sha512-5uzTEUIi4OB5zy/H30kbUN/zpDNJsFUA+Z47ZL8EfrP93lcZvRLEqdbhdunEPa7CouuAzXXsHpCJ9dg90Umw7g==
-  dependencies:
-    date-format "^4.0.5"
-    debug "^4.3.3"
-    fs-extra "^10.0.1"
-
-string-argv@~0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
-  integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
-
-string-width@^4.1.0, string-width@^4.2.0:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
-strip-json-comments@~3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
-  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
-
[email protected]:
-  version "5.14.2"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10"
-  integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==
-  dependencies:
-    "@jridgewell/source-map" "^0.3.2"
-    acorn "^8.5.0"
-    commander "^2.20.0"
-    source-map-support "~0.5.20"
-
-timsort@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
-  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
-
-tmp@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
-  integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
-  dependencies:
-    rimraf "^3.0.0"
-
-to-regex-range@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
-  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
-  dependencies:
-    is-number "^7.0.0"
-
[email protected]:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
-  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
-
-tsickle@^0.38.0:
-  version "0.38.1"
-  resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.38.1.tgz#30762db759d40c435943093b6972c7f2efb384ef"
-  integrity sha512-4xZfvC6+etRu6ivKCNqMOd1FqcY/m6JY3Y+yr5+Xw+i751ciwrWINi6x/3l1ekcODH9GZhlf0ny2LpzWxnjWYA==
-
-tslib@^1.8.1:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
-  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-tslib@^2.0.1, tslib@^2.2.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
-  integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
-
[email protected]:
-  version "3.21.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
-  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
-  dependencies:
-    tslib "^1.8.1"
-
-type-is@~1.6.17:
-  version "1.6.18"
-  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
-  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
-  dependencies:
-    media-typer "0.3.0"
-    mime-types "~2.1.24"
-
[email protected]:
-  version "4.4.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
-  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
-
-typescript@~4.3.5:
-  version "4.3.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
-  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
-
-ua-parser-js@^0.7.30:
-  version "0.7.30"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.30.tgz#4cf5170e8b55ac553fe8b38df3a82f0669671f0b"
-  integrity sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg==
-
-universalify@^0.1.0:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
-  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
-
-universalify@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
-  integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
-
[email protected], unpipe@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
-
-uri-js@^4.2.2:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
-  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
-  dependencies:
-    punycode "^2.1.0"
-
[email protected]:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-  integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-
-validator@^8.0.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9"
-  integrity sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==
-
-vary@^1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
-  integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
-
-void-elements@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-  integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
-
-which@^1.2.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
-  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
-  dependencies:
-    isexe "^2.0.0"
-
-which@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
-  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
-  dependencies:
-    isexe "^2.0.0"
-
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
-wrappy@1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
-  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-ws@~8.2.3:
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
-  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
-
-y18n@^5.0.5:
-  version "5.0.8"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
-  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
-yargs-parser@^20.2.2:
-  version "20.2.9"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
-  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs@^16.1.1:
-  version "16.2.0"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
-  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
-  dependencies:
-    cliui "^7.0.2"
-    escalade "^3.1.1"
-    get-caller-file "^2.0.5"
-    require-directory "^2.1.1"
-    string-width "^4.2.0"
-    y18n "^5.0.5"
-    yargs-parser "^20.2.2"
-
-z-schema@~3.18.3:
-  version "3.18.4"
-  resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.4.tgz#ea8132b279533ee60be2485a02f7e3e42541a9a2"
-  integrity sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==
-  dependencies:
-    lodash.get "^4.0.0"
-    lodash.isequal "^4.0.0"
-    validator "^8.0.0"
-  optionalDependencies:
-    commander "^2.7.1"
diff --git a/kokoro/gcp_ubuntu_per_language/apps/run_tests.sh b/kokoro/gcp_ubuntu_per_language/apps/run_tests.sh
deleted file mode 100644
index a7519d6..0000000
--- a/kokoro/gcp_ubuntu_per_language/apps/run_tests.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-set -euo pipefail
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-
-./kokoro/testutils/update_android_sdk.sh
-
-cd apps
-use_bazel.sh $(cat .bazelversion)
-time bazel build -- ...
-time bazel test --test_output="errors" -- ...
diff --git a/kokoro/gcp_ubuntu_per_language/cc/bazel/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/bazel/run_tests.sh
index 9e95055..b2b2668 100644
--- a/kokoro/gcp_ubuntu_per_language/cc/bazel/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/cc/bazel/run_tests.sh
@@ -18,6 +18,8 @@
 set -euo pipefail
 cd ${KOKORO_ARTIFACTS_DIR}/git/tink
 
+./kokoro/testutils/upgrade_gcc.sh
+
 cd cc
 use_bazel.sh $(cat .bazelversion)
 bazel build ...
diff --git a/kokoro/gcp_ubuntu_per_language/cc/bazel_absl_status/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/bazel_absl_status/run_tests.sh
deleted file mode 100644
index c4c49ff..0000000
--- a/kokoro/gcp_ubuntu_per_language/cc/bazel_absl_status/run_tests.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-# Copyright 2021 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-set -euo pipefail
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-
-cd cc
-use_bazel.sh $(cat .bazelversion)
-bazel build \
-  --//config:tink_use_absl_status=True \
-  --//config:tink_use_absl_statusor=True \
-  -- ...
-bazel test \
-  --//config:tink_use_absl_status=True \
-  --//config:tink_use_absl_statusor=True \
-  --test_output=errors \
-  -- ...
diff --git a/kokoro/gcp_ubuntu_per_language/cc/bazel_fips/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/bazel_fips/run_tests.sh
index 57f085a..905e421 100644
--- a/kokoro/gcp_ubuntu_per_language/cc/bazel_fips/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/cc/bazel_fips/run_tests.sh
@@ -18,6 +18,8 @@
 set -euo pipefail
 cd ${KOKORO_ARTIFACTS_DIR}/git/tink
 
+./kokoro/testutils/upgrade_gcc.sh
+
 cd cc
 use_bazel.sh $(cat .bazelversion)
 
diff --git a/kokoro/gcp_ubuntu_per_language/cc/cmake/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/cmake/run_tests.sh
index bb3b8b7..8ca4618 100644
--- a/kokoro/gcp_ubuntu_per_language/cc/cmake/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/cc/cmake/run_tests.sh
@@ -20,4 +20,9 @@
   cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
 fi
 
+./kokoro/testutils/upgrade_gcc.sh
+# Sourcing is needed to update the caller environment.
+# Install CMake 3.13 which is the minimum required.
+source ./kokoro/testutils/install_cmake.sh "3.13.5" \
+  "e2fd0080a6f0fc1ec84647acdcd8e0b4019770f48d83509e6a5b0b6ea27e5864"
 ./kokoro/testutils/run_cmake_tests.sh .
diff --git a/kokoro/gcp_ubuntu_per_language/cc/cmake_openssl/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/cmake_openssl/run_tests.sh
index da19b3a..d52de1a 100644
--- a/kokoro/gcp_ubuntu_per_language/cc/cmake_openssl/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/cc/cmake_openssl/run_tests.sh
@@ -21,6 +21,7 @@
 fi
 
 ./kokoro/testutils/update_certs.sh
+./kokoro/testutils/upgrade_gcc.sh
 # Sourcing is needed to update the caller environment.
 source ./kokoro/testutils/install_cmake.sh
 source ./kokoro/testutils/install_openssl.sh
diff --git a/kokoro/gcp_ubuntu_per_language/cc/examples/cmake/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/examples/cmake/run_tests.sh
index d4d8fe6..b9f6ea1 100644
--- a/kokoro/gcp_ubuntu_per_language/cc/examples/cmake/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/cc/examples/cmake/run_tests.sh
@@ -20,7 +20,11 @@
   cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
 fi
 
-export TEST_TMPDIR="$(mktemp -dt examples-cc-cmake.XXXXXX)"
-export TEST_SRCDIR="$(cd ..; pwd)"
-cd cc/examples/helloworld
-./cmake_build_test.sh
+./kokoro/testutils/upgrade_gcc.sh
+# Sourcing is needed to update the caller environment.
+# Install CMake 3.13 which is the minimum required.
+source ./kokoro/testutils/install_cmake.sh "3.13.5" \
+  "e2fd0080a6f0fc1ec84647acdcd8e0b4019770f48d83509e6a5b0b6ea27e5864"
+
+./kokoro/testutils/run_cmake_tests.sh "cc/examples" -DTINK_BUILD_TESTS=OFF
+
diff --git a/kokoro/gcp_ubuntu_per_language/cc/examples/cmake_openssl/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cc/examples/cmake_openssl/run_tests.sh
index 2faf511..c15c8f1 100644
--- a/kokoro/gcp_ubuntu_per_language/cc/examples/cmake_openssl/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/cc/examples/cmake_openssl/run_tests.sh
@@ -21,12 +21,10 @@
 fi
 
 ./kokoro/testutils/update_certs.sh
-# Only installing CMake; OpenSSL is installed by the example script.
+./kokoro/testutils/upgrade_gcc.sh
 source ./kokoro/testutils/install_cmake.sh
+source ./kokoro/testutils/install_openssl.sh
 
-(
-  export TEST_TMPDIR="$(mktemp -dt examples-cc-cmake-openssl.XXXXXX)"
-  export TEST_SRCDIR="$(cd ..; pwd)"
-  cd cc/examples/helloworld
-  ./cmake_build_test.sh --openssl
-)
+./kokoro/testutils/run_cmake_tests.sh "cc/examples" -DTINK_BUILD_TESTS=OFF \
+  -DTINK_USE_SYSTEM_OPENSSL=ON
+
diff --git a/kokoro/gcp_ubuntu_per_language/cross_language/run_tests.sh b/kokoro/gcp_ubuntu_per_language/cross_language/run_tests.sh
deleted file mode 100644
index 5773373..0000000
--- a/kokoro/gcp_ubuntu_per_language/cross_language/run_tests.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-set -euo pipefail
-
-CURRENT_BAZEL_VERSION=""
-
-use_bazel() {
-  local candidate_version="$1"
-  if [[ "${candidate_version}" != "${CURRENT_BAZEL_VERSION}" ]]; then
-    CURRENT_BAZEL_VERSION="${candidate_version}"
-    if [[ -n "${KOKORO_ROOT:-}" ]] ; then
-      use_bazel.sh "${candidate_version}"
-    else
-      bazel --version
-    fi
-  fi
-}
-
-main() {
-  if [[ -n "${KOKORO_ROOT:-}" ]] ; then
-    cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
-    ./kokoro/testutils/copy_credentials.sh "tools/testdata" "all"
-    ./kokoro/testutils/update_android_sdk.sh
-    # Sourcing required to update callers environment.
-    source ./kokoro/testutils/install_python3.sh
-    source ./kokoro/testutils/install_go.sh
-  fi
-  (
-    cd testing/cc
-    use_bazel "$(cat .bazelversion)"
-    time bazel build -- ...
-    time bazel test --test_output=errors -- ...
-  )
-  (
-    cd testing/go
-    use_bazel "$(cat .bazelversion)"
-    time bazel build -- ...
-    time bazel test --test_output=errors -- ...
-  )
-  (
-    cd testing/java_src
-    use_bazel "$(cat .bazelversion)"
-    time bazel build -- ...
-    time bazel build :testing_server_deploy.jar
-    time bazel test --test_output=errors -- ...
-  )
-  (
-    cd testing/python
-    use_bazel "$(cat .bazelversion)"
-    time bazel build -- ...
-    time bazel test --test_output=errors -- ...
-  )
-
-  local TINK_CROSS_LANG_ROOT_PATH="${PWD}/testing"
-  (
-    cd testing/cross_language
-    use_bazel "$(cat .bazelversion)"
-    time bazel test \
-      --test_env TINK_CROSS_LANG_ROOT_PATH="${TINK_CROSS_LANG_ROOT_PATH}" \
-      --test_output=errors \
-      -- ...
-  )
-}
-
-main "$@"
diff --git a/kokoro/gcp_ubuntu_per_language/go/bazel/run_tests.sh b/kokoro/gcp_ubuntu_per_language/go/bazel/run_tests.sh
index 838a783..6165792 100644
--- a/kokoro/gcp_ubuntu_per_language/go/bazel/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/go/bazel/run_tests.sh
@@ -39,4 +39,5 @@
 fi
 readonly MANUAL_TARGETS
 
-./kokoro/testutils/run_bazel_tests.sh go "${MANUAL_TARGETS[@]}"
+./kokoro/testutils/run_bazel_tests.sh \
+  -t --test_arg=--test.v go "${MANUAL_TARGETS[@]}"
diff --git a/kokoro/gcp_ubuntu_per_language/java_src/run_tests.sh b/kokoro/gcp_ubuntu_per_language/java_src/run_tests.sh
index 50f46f4..d19498a 100644
--- a/kokoro/gcp_ubuntu_per_language/java_src/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/java_src/run_tests.sh
@@ -59,14 +59,14 @@
 
 
   # Targets in //src/main/java/com/google/crypto/tink/integration/awskms of
-  # type "java_library"
+  # type "java_library" excluding testonly targets
   local all_aws_kms_libs="$(mktemp)"
-  bazel query "kind(java_library,${integration_dir}/awskms/...)" > "${all_aws_kms_libs}"
+  bazel query "kind(java_library,${integration_dir}/awskms/...) except attr(testonly,1,${integration_dir}/...)" > "${all_aws_kms_libs}"
 
   # Targets in //src/main/java/com/google/crypto/tink/integration/gcpkms of
-  # type "java_library"
+  # type "java_library" excluding testonly targets
   all_gcp_kms_libs="$(mktemp)"
-  bazel query "kind(java_library,${integration_dir}/gcpkms/...)" > "${all_gcp_kms_libs}"
+  bazel query "kind(java_library,${integration_dir}/gcpkms/...) except attr(testonly,1,${integration_dir}/...)" > "${all_gcp_kms_libs}"
   popd
 
   python3 kokoro/testutils/create_main_build_file.py \
@@ -99,20 +99,4 @@
   use_bazel.sh "$(cat java_src/.bazelversion)"
 fi
 
-./kokoro/testutils/copy_credentials.sh "java_src/testdata" "all"
-./kokoro/testutils/update_android_sdk.sh
-
-# Run manual tests which rely on key material injected into the Kokoro
-# environement.
-MANUAL_TARGETS=()
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  MANUAL_TARGETS+=(
-    "//src/test/java/com/google/crypto/tink/integration/gcpkms:KmsAeadKeyManagerWithGcpTest"
-    "//src/test/java/com/google/crypto/tink/integration/gcpkms:KmsEnvelopeAeadKeyManagerWithGcpTest"
-  )
-fi
-readonly MANUAL_TARGETS
-
-./kokoro/testutils/run_bazel_tests.sh java_src "${MANUAL_TARGETS[@]}"
-
 test_build_bazel_file
diff --git a/kokoro/gcp_ubuntu_per_language/javascript/run_tests.sh b/kokoro/gcp_ubuntu_per_language/javascript/run_tests.sh
deleted file mode 100644
index fe30ec1..0000000
--- a/kokoro/gcp_ubuntu_per_language/javascript/run_tests.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-set -euo pipefail
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-
-cd javascript
-use_bazel.sh $(cat .bazelversion)
-bazel build ...
-bazel test --test_output=errors -- ...
diff --git a/kokoro/gcp_ubuntu_per_language/python/bazel/run_tests.sh b/kokoro/gcp_ubuntu_per_language/python/bazel/run_tests.sh
index efa7994..5398f06 100644
--- a/kokoro/gcp_ubuntu_per_language/python/bazel/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/python/bazel/run_tests.sh
@@ -18,25 +18,7 @@
 
 if [[ -n "${KOKORO_ROOT:-}" ]]; then
   cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
+  use_bazel.sh "$(cat python/.bazelversion)"
 fi
 
-./kokoro/testutils/copy_credentials.sh "python/testdata" "all"
-# Sourcing required to update callers environment.
-source ./kokoro/testutils/install_python3.sh
-
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  use_bazel.sh $(cat python/.bazelversion)
-fi
-
-# Run manual tests which rely on key material injected into the Kokoro
-# environement.
-MANUAL_TARGETS=()
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  MANUAL_TARGETS+=(
-    "//tink/integration/awskms:_aws_kms_aead_test"
-    "//tink/integration/gcpkms:_gcp_kms_aead_test"
-  )
-fi
-readonly MANUAL_TARGETS
-
-./kokoro/testutils/run_bazel_tests.sh python "${MANUAL_TARGETS[@]}"
+# These tests are now run in python/kokoro/gcp_ubuntu/bazel/run_tests.sh
diff --git a/kokoro/gcp_ubuntu_per_language/python/pip/run_tests.sh b/kokoro/gcp_ubuntu_per_language/python/pip/run_tests.sh
index 0557a80..abaa904 100644
--- a/kokoro/gcp_ubuntu_per_language/python/pip/run_tests.sh
+++ b/kokoro/gcp_ubuntu_per_language/python/pip/run_tests.sh
@@ -24,10 +24,11 @@
 fi
 
 ./kokoro/testutils/copy_credentials.sh "python/testdata" "all"
+./kokoro/testutils/upgrade_gcc.sh
 # Sourcing required to update callers environment.
 source ./kokoro/testutils/install_python3.sh
 source ./kokoro/testutils/install_protoc.sh
-source ./kokoro/testutils/install_tink_via_pip.sh "${PWD}/python"
+./kokoro/testutils/install_tink_via_pip.sh "${PWD}/python" "${PWD}"
 
 cd python
 
diff --git a/kokoro/gcp_ubuntu_per_language/tools/run_tests.sh b/kokoro/gcp_ubuntu_per_language/tools/run_tests.sh
deleted file mode 100644
index 043fae7..0000000
--- a/kokoro/gcp_ubuntu_per_language/tools/run_tests.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-set -euo pipefail
-
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-
-./kokoro/testutils/copy_credentials.sh "tools/testdata" "all"
-./kokoro/testutils/update_android_sdk.sh
-# Sourcing required to update callers environment.
-source ./kokoro/testutils/install_python3.sh
-source ./kokoro/testutils/install_go.sh
-
-echo "Using go binary from $(which go): $(go version)"
-
-cd tools
-use_bazel.sh $(cat .bazelversion)
-time bazel build -- ...
-time bazel test --test_output=errors -- ...
-
-# Run manual tests which rely on key material injected into the Kokoro
-# environement.
-if [[ -n "${KOKORO_ROOT}" ]]; then
-  declare -a MANUAL_TARGETS
-  MANUAL_TARGETS=(
-    "//testing/cc:aws_kms_aead_test"
-    "//testing/cc:gcp_kms_aead_test"
-    "//testing/cross_language:aead_envelope_test"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:AddKeyCommandTest"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:CreateKeysetCommandTest"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:CreatePublicKeysetCommandTest"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:RotateKeysetCommandTest"
-  )
-  readonly MANUAL_TARGETS
-  time bazel test --test_output=errors -- "${MANUAL_TARGETS[@]}"
-fi
diff --git a/kokoro/macos_external/apps/run_tests.sh b/kokoro/macos_external/apps/run_tests.sh
deleted file mode 100644
index 52010e6..0000000
--- a/kokoro/macos_external/apps/run_tests.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-set -euo pipefail
-
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-
-export XCODE_VERSION=11.3
-export DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
-export ANDROID_HOME="/Users/kbuilder/Library/Android/sdk"
-export COURSIER_OPTS="-Djava.net.preferIPv6Addresses=true"
-
-./kokoro/testutils/update_android_sdk.sh
-
-cd apps
-use_bazel.sh $(cat .bazelversion)
-time bazel build -- ...
-time bazel test --test_output="errors" -- ...
diff --git a/kokoro/macos_external/cc/bazel/run_tests.sh b/kokoro/macos_external/cc/bazel/run_tests.sh
index 220bb25..e17b297 100644
--- a/kokoro/macos_external/cc/bazel/run_tests.sh
+++ b/kokoro/macos_external/cc/bazel/run_tests.sh
@@ -15,11 +15,10 @@
 # limitations under the License.
 ################################################################################
 
-
 set -euo pipefail
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
 
-cd cc
-use_bazel.sh $(cat .bazelversion)
-bazel build -- ...
-bazel test --test_output="errors" -- ...
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]] ; then
+  cd "$(echo "${KOKORO_ARTIFACTS_DIR}"/git*/tink)"
+fi
+
+./kokoro/testutils/run_bazel_tests.sh cc
diff --git a/kokoro/macos_external/cc/examples/cmake/run_tests.sh b/kokoro/macos_external/cc/examples/cmake/run_tests.sh
index 5fe3246..d91e757 100644
--- a/kokoro/macos_external/cc/examples/cmake/run_tests.sh
+++ b/kokoro/macos_external/cc/examples/cmake/run_tests.sh
@@ -20,9 +20,5 @@
   cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
 fi
 
-export TEST_TMPDIR="$(mktemp -dt examples-cc-cmake.XXXXXX)"
-export TEST_SRCDIR="$(cd ..; pwd)"
-(
-  cd cc/examples/helloworld
-  ./cmake_build_test.sh
-)
+./kokoro/testutils/run_cmake_tests.sh "cc/examples" -DTINK_BUILD_TESTS=OFF
+
diff --git a/kokoro/macos_external/go/bazel/run_tests.sh b/kokoro/macos_external/go/bazel/run_tests.sh
index 838a783..4bd5b50 100644
--- a/kokoro/macos_external/go/bazel/run_tests.sh
+++ b/kokoro/macos_external/go/bazel/run_tests.sh
@@ -16,22 +16,26 @@
 
 set -euo pipefail
 
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
-  use_bazel.sh "$(cat go/.bazelversion)"
+IS_KOKORO="false"
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then
+  IS_KOKORO="true"
+fi
+readonly IS_KOKORO
+
+if [[ "${IS_KOKORO}" == "true" ]]; then
+  cd "$(echo "${KOKORO_ARTIFACTS_DIR}"/git*/tink)"
 fi
 
 ./kokoro/testutils/copy_credentials.sh "go/testdata" "all"
 # Sourcing required to update callers environment.
 source ./kokoro/testutils/install_go.sh
-
 echo "Using go binary from $(which go): $(go version)"
-
-./kokoro/testutils/check_go_generated_files_up_to_date.sh go/
+./kokoro/testutils/check_go_generated_files_up_to_date.sh go
 
 MANUAL_TARGETS=()
-# Run manual tests that rely on test data only available via Bazel.
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
+# Run manual tests which rely on key material injected into the Kokoro
+# environement.
+if [[ "${IS_KOKORO}" == "true" ]]; then
   MANUAL_TARGETS+=(
     "//integration/gcpkms:gcpkms_test"
     "//integration/awskms:awskms_test"
@@ -39,4 +43,5 @@
 fi
 readonly MANUAL_TARGETS
 
-./kokoro/testutils/run_bazel_tests.sh go "${MANUAL_TARGETS[@]}"
+./kokoro/testutils/run_bazel_tests.sh \
+  -t --test_arg=--test.v go "${MANUAL_TARGETS[@]}"
diff --git a/kokoro/macos_external/java_src/run_tests.sh b/kokoro/macos_external/java_src/run_tests.sh
index 5a3ffcf..779558f 100644
--- a/kokoro/macos_external/java_src/run_tests.sh
+++ b/kokoro/macos_external/java_src/run_tests.sh
@@ -16,24 +16,15 @@
 
 set -euo pipefail
 
-export XCODE_VERSION=11.3
-export DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
-export ANDROID_HOME="/Users/kbuilder/Library/Android/sdk"
+export XCODE_VERSION="14.1"
+export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
+export ANDROID_HOME="/usr/local/share/android-sdk"
 export COURSIER_OPTS="-Djava.net.preferIPv6Addresses=true"
 
-declare -a TEST_FLAGS
-TEST_FLAGS=(
-  --strategy=TestRunner=standalone
-  --test_output=errors
-  --jvmopt="-Djava.net.preferIPv6Addresses=true"
-)
-readonly TEST_FLAGS
+if [[ -n "${KOKORO_ROOT:-}" ]] ; then
+  cd "$(echo "${KOKORO_ARTIFACTS_DIR}"/git*/tink)"
+  export JAVA_HOME=$(/usr/libexec/java_home -v "1.8.0_292")
+fi
 
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-./kokoro/testutils/copy_credentials.sh "java_src/testdata" "all"
 ./kokoro/testutils/update_android_sdk.sh
-
-cd java_src
-use_bazel.sh $(cat .bazelversion)
-bazel build ...
-bazel test "${TEST_FLAGS[@]}" -- ...
+./kokoro/testutils/run_bazel_tests.sh java_src
diff --git a/kokoro/macos_external/javascript/run_tests.sh b/kokoro/macos_external/javascript/run_tests.sh
deleted file mode 100644
index 8d01912..0000000
--- a/kokoro/macos_external/javascript/run_tests.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-
-set -euo pipefail
-
-main() {
-  if [[ -n "${KOKORO_ROOT:-}" ]]; then
-    cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
-  fi
-
-  cd javascript
-  if [[ -n "${KOKORO_ROOT:-}" ]]; then
-    use_bazel.sh "$(cat .bazelversion)"
-  fi
-
-  local readonly TEST_FLAGS=(
-    --strategy=TestRunner=standalone
-    --test_output=errors
-  )
-
-  # This is needed to handle recent Chrome distributions on macOS which have
-  # paths with spaces. Context:
-  # https://github.com/bazelbuild/bazel/issues/4327#issuecomment-627422865
-  local readonly BAZEL_FLAGS=( --experimental_inprocess_symlink_creation )
-  bazel build "${BAZEL_FLAGS[@]}" -- ...
-  bazel test "${BAZEL_FLAGS[@]}" "${TEST_FLAGS[@]}" -- ...
-}
-
-main "$@"
diff --git a/kokoro/macos_external/objc/run_tests.sh b/kokoro/macos_external/objc/run_tests.sh
index a9a2f95..e76fd85 100644
--- a/kokoro/macos_external/objc/run_tests.sh
+++ b/kokoro/macos_external/objc/run_tests.sh
@@ -13,13 +13,3 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ################################################################################
-
-
-set -euo pipefail
-cd ${KOKORO_ARTIFACTS_DIR}/git/tink
-
-cd objc
-## TODO(b/155060426) Reenable once the tests work.
-# use_bazel.sh $(cat .bazelversion)
-# time bazel build -- ...
-# time bazel test --test_output="errors" -- ...
diff --git a/kokoro/macos_external/python/bazel/run_tests.sh b/kokoro/macos_external/python/bazel/run_tests.sh
index 12912c6..93b0cc3 100644
--- a/kokoro/macos_external/python/bazel/run_tests.sh
+++ b/kokoro/macos_external/python/bazel/run_tests.sh
@@ -16,24 +16,14 @@
 
 set -euo pipefail
 
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
+IS_KOKORO="false"
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then
+  IS_KOKORO="true"
+fi
+readonly IS_KOKORO
+
+if [[ "${IS_KOKORO}" == "true" ]]; then
+  cd "$(echo "${KOKORO_ARTIFACTS_DIR}"/git*/tink)"
 fi
 
-./kokoro/testutils/copy_credentials.sh "python/testdata" "gcp"
-# Install protobuf pip packages.
-pip3 install protobuf --user
-
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  use_bazel.sh $(cat python/.bazelversion)
-fi
-
-# Run manual tests which rely on key material injected into the Kokoro
-# environement.
-MANUAL_TARGETS=()
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  MANUAL_TARGETS+=( "//tink/integration/gcpkms:_gcp_kms_aead_test" )
-fi
-readonly MANUAL_TARGETS
-
-./kokoro/testutils/run_bazel_tests.sh python "${MANUAL_TARGETS[@]}"
+# These tests are now run in python/kokoro/macos_external/bazel/run_tests.sh
diff --git a/kokoro/macos_external/python/pip/run_tests.sh b/kokoro/macos_external/python/pip/run_tests.sh
index 5fee23e..491e661 100644
--- a/kokoro/macos_external/python/pip/run_tests.sh
+++ b/kokoro/macos_external/python/pip/run_tests.sh
@@ -16,15 +16,13 @@
 
 set -euo pipefail
 
-# If we are running on Kokoro cd into the repository.
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
-  cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
-  use_bazel.sh "$(cat python/.bazelversion)"
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then
+  cd "$(echo "${KOKORO_ARTIFACTS_DIR}"/git*/tink)"
 fi
 
 ./kokoro/testutils/copy_credentials.sh "python/testdata" "all"
 source ./kokoro/testutils/install_protoc.sh
-source ./kokoro/testutils/install_tink_via_pip.sh "${PWD}/python"
+./kokoro/testutils/install_tink_via_pip.sh "${PWD}/python" "${PWD}"
 
 # Get root certificates for gRPC
 curl -OLsS https://raw.githubusercontent.com/grpc/grpc/master/etc/roots.pem
diff --git a/kokoro/macos_external/tools/run_tests.sh b/kokoro/macos_external/tools/run_tests.sh
index a9f0fab..63b802d 100644
--- a/kokoro/macos_external/tools/run_tests.sh
+++ b/kokoro/macos_external/tools/run_tests.sh
@@ -14,53 +14,31 @@
 # limitations under the License.
 ################################################################################
 
-
 set -euo pipefail
 
-
-export XCODE_VERSION=11.3
-export DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
-export ANDROID_HOME="/Users/kbuilder/Library/Android/sdk"
+export XCODE_VERSION="14.1"
+export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
+export ANDROID_HOME="/usr/local/share/android-sdk"
 export COURSIER_OPTS="-Djava.net.preferIPv6Addresses=true"
 
-cd "${KOKORO_ARTIFACTS_DIR}/git/tink"
+IS_KOKORO="false"
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then
+  IS_KOKORO="true"
+fi
+readonly IS_KOKORO
+
+if [[ "${IS_KOKORO}" == "true" ]] ; then
+  cd "$(echo "${KOKORO_ARTIFACTS_DIR}"/git*/tink)"
+  export JAVA_HOME=$(/usr/libexec/java_home -v "1.8.0_292")
+fi
+
 ./kokoro/testutils/copy_credentials.sh "tools/testdata" "all"
 ./kokoro/testutils/update_android_sdk.sh
 # Sourcing required to update callers environment.
 source ./kokoro/testutils/install_go.sh
-
 echo "Using go binary from $(which go): $(go version)"
 
 # TODO(b/155225382): Avoid modifying the sytem Python installation.
 pip3 install --user protobuf
 
-cd tools
-use_bazel.sh $(cat .bazelversion)
-
-declare -a TEST_FLAGS
-TEST_FLAGS=(
-  --strategy=TestRunner=standalone
-  --test_output=errors
-  --jvmopt="-Djava.net.preferIPv6Addresses=true"
-)
-readonly TEST_FLAGS
-
-time bazel build -- ...
-time bazel test "${TEST_FLAGS[@]}" -- ...
-
-# Run manual tests which rely on key material injected into the Kokoro
-# environement.
-if [[ -n "${KOKORO_ROOT}" ]]; then
-  declare -a MANUAL_TARGETS
-  MANUAL_TARGETS=(
-    "//testing/cc:aws_kms_aead_test"
-    "//testing/cc:gcp_kms_aead_test"
-    "//testing/cross_language:aead_envelope_test"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:AddKeyCommandTest"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:CreateKeysetCommandTest"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:CreatePublicKeysetCommandTest"
-    "//tinkey/src/test/java/com/google/crypto/tink/tinkey:RotateKeysetCommandTest"
-  )
-  readonly MANUAL_TARGETS
-  time bazel test "${TEST_FLAGS[@]}" -- "${MANUAL_TARGETS[@]}"
-fi
+./kokoro/testutils/run_bazel_tests.sh tools
diff --git a/kokoro/remote.sh b/kokoro/remote.sh
deleted file mode 100755
index 883123c..0000000
--- a/kokoro/remote.sh
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-####################################################################################
-
-# Fail on any error.
-set -e
-
-# Display commands to stderr.
-set -x
-
-# Change to repo root
-cd git*/tink
-
-# Only in Kokoro environments.
-if [[ -n "${KOKORO_ROOT}" ]]; then
-  # TODO(b/73748835): Workaround on Kokoro.
-  rm -f ~/.bazelrc
-
-  use_bazel.sh $(cat .bazelversion)
-  ./kokoro/testutils/update_android_sdk.sh
-fi
-
-echo "Using bazel binary: $(which bazel)"
-bazel version
-
-# Create an invocation ID for the bazel, and write it as an artifact.
-# Kokoro will use that later to post the bazel invocation details.
-INVOCATION_ID=$(uuidgen)
-echo "Invocation ID = ${INVOCATION_ID}"
-ID_OUT="${KOKORO_ARTIFACTS_DIR}/bazel_invocation_ids"
-echo "${INVOCATION_ID}" >> "${ID_OUT}"
-
-# Setup all the RBE args needed by bazel.
-# The kokoro env variables are set in tink/kokoro/presubmit-remote.cfg.
-declare -a RBE_ARGS
-RBE_ARGS=(
-  --invocation_id="${INVOCATION_ID}"
-  --auth_enabled=true
-  --auth_credentials="${KOKORO_BAZEL_AUTH_CREDENTIAL}"
-  --auth_scope=https://www.googleapis.com/auth/cloud-source-tools
-  --bes_backend="${KOKORO_BES_BACKEND_ADDRESS}"
-  --bes_timeout=600s
-  --project_id="${KOKORO_BES_PROJECT_ID}"
-  --remote_cache="${KOKORO_FOUNDRY_BACKEND_ADDRESS}"
-  --remote_executor="${KOKORO_FOUNDRY_BACKEND_ADDRESS}"
-  --test_env=USER=anon
-  --remote_instance_name="${KOKORO_FOUNDRY_PROJECT_ID}/instances/default_instance"
-  --config=remote
-)
-readonly RBE_ARGS
-
-RBE_BAZELRC="${PWD}/tools/remote_build_execution/bazel-rbe.bazelrc"
-echo "RBE_BAZELRC: ${RBE_BAZELRC}"
-
-# TODO(b/141297103): enable Python
-# TODO(b/143102587): enable Javascript
-
-#######################################
-# Builds and runs unit test within the given Tink workspace.
-#
-# Arguments:
-#   workspace directory path relative to the Tink root.
-#######################################
-build_and_run_tests() {
-  local workspace_dir="${1}"
-  (
-    cd "${workspace_dir}"
-
-    time bazel --bazelrc="${RBE_BAZELRC}" \
-      build "${RBE_ARGS[@]}" \
-      --build_tag_filters=-no_rbe \
-      -- ...
-
-    time bazel --bazelrc="${RBE_BAZELRC}" \
-      test "${RBE_ARGS[@]}" \
-      --test_output=errors \
-      --test_tag_filters=-no_rbe \
-      --jvmopt=-Drbe=1 \
-      -- ...
-  )
-}
-
-# C++.
-build_and_run_tests "cc/"
-# Java.
-build_and_run_tests "java_src/"
-# Go.
-build_and_run_tests "go/"
-
-# TODO(b/141297103): Python causes this to fail on remote
-# # Build tools and run cross-language tests.
-# build_and_run_tests "tools/"
-
-# We don't currently run TypeScript/JavaScript tests remotely because they
-# require libx11-xcb-dev in order to bring up browsers.
diff --git a/kokoro/run_tests.sh b/kokoro/run_tests.sh
index 8b81f3f..920dab8 100755
--- a/kokoro/run_tests.sh
+++ b/kokoro/run_tests.sh
@@ -21,93 +21,66 @@
 # Display commands to stderr.
 set -x
 
-
 readonly PLATFORM="$(uname | tr '[:upper:]' '[:lower:]')"
 
-fail_with_debug_output() {
-  ls -l
-  df -h /
-  exit 1
+IS_KOKORO="false"
+if [[ -n "${KOKORO_ROOT}" ]]; then
+  IS_KOKORO="true"
+fi
+readonly IS_KOKORO
+
+use_bazel() {
+  local -r bazel_version="$1"
+  if [[ "${IS_KOKORO}" == "false" ]]; then
+    # Do nothing.
+    return 0
+  fi
+  if ! command -v "bazelisk" &> /dev/null; then
+    use_bazel.sh "${bazel_version}"
+  fi
 }
 
-run_linux_tests() {
-  local workspace_dir="$1"
-  shift 1
-  local manual_targets=("$@")
-
-  # This is needed to handle recent Chrome distributions on macOS which have
-  # paths with spaces.
-  #
-  # Context:
-  # https://github.com/bazelbuild/bazel/issues/4327#issuecomment-627422865
-  local -a BAZEL_FLAGS
-  if [[ "${PLATFORM}" == 'darwin' && "${workspace_dir}" == 'javascript' ]]; then
-    BAZEL_FLAGS+=( --experimental_inprocess_symlink_creation )
-  fi
-  readonly BAZEL_FLAGS
-
-  local -a TEST_FLAGS=( --strategy=TestRunner=standalone --test_output=all )
-  if [[ "${PLATFORM}" == 'darwin' ]]; then
-    TEST_FLAGS+=( --jvmopt="-Djava.net.preferIPv6Addresses=true" )
-  fi
-  readonly TEST_FLAGS
-  (
-    cd "${workspace_dir}"
-    time bazel build "${BAZEL_FLAGS[@]}" -- ... || fail_with_debug_output
-    time bazel test "${BAZEL_FLAGS[@]}" "${TEST_FLAGS[@]}" -- ... \
-      || fail_with_debug_output
-    if (( ${#manual_targets[@]} > 0 )); then
-      time bazel test "${TEST_FLAGS[@]}"  -- "${manual_targets[@]}" \
-        || fail_with_debug_output
-    fi
-  )
+run_cc_tests() {
+  use_bazel "$(cat cc/.bazelversion)"
+  ./kokoro/testutils/run_bazel_tests.sh "cc"
 }
 
-run_all_linux_tests() {
-  # Only run these tests if exeucting a Kokoro GitHub continuous integration
-  # job or if running locally (e.g. as part of release.sh).
-  #
-  # TODO(b/228529710): Use an easier to maintain approach to test parity.
-  if [[ "${KOKORO_JOB_NAME:-}" =~ ^tink/github \
-      || -z "${KOKORO_JOB_NAME+x}" ]]; then
-    run_linux_tests "cc"
+run_go_tests() {
+  use_bazel "$(cat go/.bazelversion)"
+  ./kokoro/testutils/run_bazel_tests.sh -t --test_arg=--test.v "go"
+}
 
-    local -a MANUAL_JAVA_TARGETS
-    if [[ -n "${KOKORO_ROOT}" ]]; then
-      MANUAL_JAVA_TARGETS+=(
-        "//src/test/java/com/google/crypto/tink/integration/gcpkms:KmsAeadKeyManagerWithGcpTest"
-        "//src/test/java/com/google/crypto/tink/integration/gcpkms:KmsEnvelopeAeadKeyManagerWithGcpTest"
-      )
-    fi
-    readonly MANUAL_JAVA_TARGETS
-    run_linux_tests "java_src" "${MANUAL_JAVA_TARGETS[@]}"
+run_py_tests() {
+  use_bazel "$(cat python/.bazelversion)"
+  ./kokoro/testutils/run_bazel_tests.sh "python"
+}
 
-    run_linux_tests "go"
-    run_linux_tests "python"
+run_tools_tests() {
+  use_bazel "$(cat tools/.bazelversion)"
+  ./kokoro/testutils/run_bazel_tests.sh "tools"
+}
 
-    local -a MANUAL_TOOLS_TARGETS
-    if [[ -n "${KOKORO_ROOT}" ]]; then
-      MANUAL_TOOLS_TARGETS+=(
-        "//testing/cc:aws_kms_aead_test"
-        "//testing/cc:gcp_kms_aead_test"
-        "//testing/cross_language:aead_envelope_test"
-        "//tinkey/src/test/java/com/google/crypto/tink/tinkey:AddKeyCommandTest"
-        "//tinkey/src/test/java/com/google/crypto/tink/tinkey:CreateKeysetCommandTest"
-        "//tinkey/src/test/java/com/google/crypto/tink/tinkey:CreatePublicKeysetCommandTest"
-        "//tinkey/src/test/java/com/google/crypto/tink/tinkey:RotateKeysetCommandTest"
-      )
-    fi
-    readonly MANUAL_TOOLS_TARGETS
-    run_linux_tests "tools" "${MANUAL_TOOLS_TARGETS[@]}"
-
-    run_linux_tests "apps"
+run_java_tests() {
+  use_bazel "$(cat java_src/.bazelversion)"
+  local -a MANUAL_JAVA_TARGETS
+  if [[ "${IS_KOKORO}" == "true" ]]; then
+    MANUAL_JAVA_TARGETS+=(
+      "//src/test/java/com/google/crypto/tink/integration/gcpkms:GcpKmsIntegrationTest"
+    )
   fi
+  readonly MANUAL_JAVA_TARGETS
+  ./kokoro/testutils/run_bazel_tests.sh "java_src" "${MANUAL_JAVA_TARGETS[@]}"
+}
 
-  run_linux_tests "javascript"
-  run_linux_tests "cc/examples"
+run_cc_examples_tests() {
+  use_bazel "$(cat cc/examples/.bazelversion)"
+  ./kokoro/testutils/run_bazel_tests.sh "cc/examples"
+}
 
+run_java_examples_tests() {
+  use_bazel "$(cat java_src/examples/.bazelversion)"
   local -a MANUAL_EXAMPLE_JAVA_TARGETS
-  if [[ -n "${KOKORO_ROOT}" ]]; then
+  if [[ "${IS_KOKORO}" == "true" ]]; then
     MANUAL_EXAMPLE_JAVA_TARGETS=(
       "//gcs:gcs_envelope_aead_example_test"
       "//encryptedkeyset:encrypted_keyset_example_test"
@@ -115,13 +88,26 @@
     )
   fi
   readonly MANUAL_EXAMPLE_JAVA_TARGETS
-  run_linux_tests "java_src/examples" "${MANUAL_EXAMPLE_JAVA_TARGETS[@]}"
+  ./kokoro/testutils/run_bazel_tests.sh "java_src/examples" \
+    "${MANUAL_EXAMPLE_JAVA_TARGETS[@]}"
+}
 
+run_py_examples_tests() {
+  use_bazel "$(cat python/examples/.bazelversion)"
   ## Install Tink and its dependencies via pip for the examples/python tests.
-  install_tink_via_pip
+  ./kokoro/testutils/install_tink_via_pip.sh "${PWD}/python" "${PWD}"
+  if [[ "${IS_KOKORO}" == "true" ]]; then
+    local pip_flags=( --require-hashes )
+    if [[ "${PLATFORM}" == "darwin" ]]; then
+      pip_flags+=( --user )
+    fi
+    readonly pip_flags
+    # Install dependencies for the examples/python tests.
+    pip3 install "${pip_flags[@]}" -r python/examples/requirements.txt
+  fi
 
   local -a MANUAL_EXAMPLE_PYTHON_TARGETS
-  if [[ -n "${KOKORO_ROOT}" ]]; then
+  if [[ "${IS_KOKORO}" == "true" ]]; then
     MANUAL_EXAMPLE_PYTHON_TARGETS=(
       "//gcs:gcs_envelope_aead_test_package"
       "//gcs:gcs_envelope_aead_test"
@@ -132,127 +118,49 @@
     )
   fi
   readonly MANUAL_EXAMPLE_PYTHON_TARGETS
-  run_linux_tests "python/examples" "${MANUAL_EXAMPLE_PYTHON_TARGETS[@]}"
+  ./kokoro/testutils/run_bazel_tests.sh "python/examples" \
+    "${MANUAL_EXAMPLE_PYTHON_TARGETS[@]}"
 }
 
-run_macos_tests() {
-  local -a BAZEL_FLAGS=(
-    --compilation_mode=dbg --dynamic_mode=off --cpu=ios_x86_64
-    --ios_cpu=x86_64 --experimental_enable_objc_cc_deps
-    --ios_sdk_version="${IOS_SDK_VERSION}"
-    --xcode_version="${XCODE_VERSION}" --verbose_failures
-    --test_output=all
-  )
-  readonly BAZEL_FLAGS
-
-  (
-    cd objc
-
-    # Build the iOS targets.
-    time bazel build "${BAZEL_FLAGS[@]}" ... || fail_with_debug_output
-
-    # Run the iOS tests.
-    time bazel test "${BAZEL_FLAGS[@]}" :TinkTests || fail_with_debug_output
-  )
-}
-
-install_tink_via_pip() {
-  local -a PIP_FLAGS
-  if [[ "${PLATFORM}" == 'darwin' ]]; then
-    PIP_FLAGS=( --user )
+run_all_tests() {
+  # Only run these tests if exeucting a Kokoro GitHub continuous integration
+  # job or if running locally (e.g. as part of release.sh).
+  #
+  # TODO(b/231610897): Use an easier to maintain approach to test parity.
+  if [[ "${KOKORO_JOB_NAME:-}" =~ ^tink/github \
+        || -z "${KOKORO_JOB_NAME+x}" ]]; then
+    run_cc_tests
+    run_java_tests
+    run_go_tests
+    run_py_tests
+    run_tools_tests
   fi
-  readonly PIP_FLAGS
-
-  # Set path to Tink base folder
-  export TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH="${PWD}"
-
-  # Check if we can build Tink python package.
-  pip3 install "${PIP_FLAGS[@]}" --upgrade pip
-  # TODO(b/219813176): Remove once Kokoro environment is compatible.
-  pip3 install "${PIP_FLAGS[@]}" --upgrade 'setuptools==60.9.0'
-  pip3 install "${PIP_FLAGS[@]}" ./python
-
-  # Install dependencies for the examples/python tests
-  pip3 install "${PIP_FLAGS[@]}" google-cloud-storage
-}
-
-install_temp_protoc() {
-  local protoc_version='3.19.3'
-  local protoc_platform
-  case "${PLATFORM}" in
-    'linux')
-      protoc_platform='linux-x86_64'
-      ;;
-    'darwin')
-      protoc_platform='osx-x86_64'
-      ;;
-    *)
-      echo "Unsupported platform, unable to install protoc."
-      exit 1
-      ;;
-  esac
-  local protoc_zip="protoc-${protoc_version}-${protoc_platform}.zip"
-  local protoc_url="https://github.com/protocolbuffers/protobuf/releases/download/v${protoc_version}/${protoc_zip}"
-  local -r protoc_tmpdir=$(mktemp -dt tink-protoc.XXXXXX)
-  (
-    cd "${protoc_tmpdir}"
-    curl -OLsS "${protoc_url}"
-    unzip ${protoc_zip} bin/protoc
-  )
-  export PATH="${protoc_tmpdir}/bin:${PATH}"
-}
-
-#######################################
-# Test Maven packages.
-# Globals:
-#   PLATFORM
-#   KOKORO_JOB_NAME
-# Arguments:
-#   None
-#######################################
-test_maven_packages() {
-  # Only test in the Ubuntu environment.
-  if [[ "${PLATFORM}" != "linux" ]]; then
-    return
-  fi
-
-  local -a maven_script_flags
-  if [[ "${KOKORO_JOB_NAME:-}" != "tink/github/gcp_ubuntu/continuous" ]]; then
-    # Unless running the GitHub continuous job, deploy and test Maven packages
-    # locally.
-    #
-    # Otherwise, snapshots will be published to the Maven Central repository.
-    maven_script_flags+=( -l )
-  fi
-  readonly maven_script_flags
-
-  ./maven/publish_snapshot.sh "${maven_script_flags[@]}"
-  ./maven/test_snapshot.sh "${maven_script_flags[@]}"
+  run_cc_examples_tests
+  run_java_examples_tests
+  run_py_examples_tests
 }
 
 main() {
   # Initialization for Kokoro environments.
-  if [[ -n "${KOKORO_ROOT}" ]]; then
+  if [[ "${IS_KOKORO}" == "true" ]]; then
     cd "${KOKORO_ARTIFACTS_DIR}"/git*/tink*
-
-    use_bazel.sh $(cat .bazelversion)
-
-    # Install protoc into a temporary directory.
-    install_temp_protoc
+    # Install protoc.
+    source ./kokoro/testutils/install_protoc.sh
 
     if [[ "${PLATFORM}" == 'linux' ]]; then
-      # Install a more recent Python. Sourcing required to update callers
-      # environment.
+      # Sourcing required to update callers environment.
       source ./kokoro/testutils/install_python3.sh
+      ./kokoro/testutils/upgrade_gcc.sh
     fi
 
     if [[ "${PLATFORM}" == 'darwin' ]]; then
       # Default values for iOS SDK and Xcode. Can be overriden by another script.
       : "${IOS_SDK_VERSION:=13.2}"
-      : "${XCODE_VERSION:=11.3}"
+      : "${XCODE_VERSION:=14.1}"
 
-      export DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
-      export ANDROID_HOME="/Users/kbuilder/Library/Android/sdk"
+      export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
+      export JAVA_HOME=$(/usr/libexec/java_home -v "1.8.0_292")
+      export ANDROID_HOME="/usr/local/share/android-sdk"
       export COURSIER_OPTS="-Djava.net.preferIPv6Addresses=true"
 
       # TODO(b/155225382): Avoid modifying the sytem Python installation.
@@ -284,9 +192,6 @@
     exit 4
   fi
 
-  echo "using bazel binary: $(which bazel)"
-  bazel version
-
   echo "using java binary: $(which java)"
   java -version
 
@@ -305,15 +210,7 @@
   echo "using protoc: $(which protoc)"
   protoc --version
 
-  run_all_linux_tests
-
-  if [[ "${PLATFORM}" == 'darwin' ]]; then
-    # TODO(b/155060426): re-enable after ObjC WORKSPACE is added.
-    # run_macos_tests
-    echo "*** ObjC tests not enabled yet."
-  fi
-
-  test_maven_packages
+  run_all_tests
 }
 
 main "$@"
diff --git a/kokoro/testutils/check_go_generated_files_up_to_date.sh b/kokoro/testutils/check_go_generated_files_up_to_date.sh
index da87244..d682ef1 100755
--- a/kokoro/testutils/check_go_generated_files_up_to_date.sh
+++ b/kokoro/testutils/check_go_generated_files_up_to_date.sh
@@ -16,18 +16,44 @@
 
 # This scripts checks that a given Go workspace has its generated Bazel files up
 # to date.
-#
-# NOTEs:
-#   * Bazel and go must be already installed.
-#
-# Usage:
-#   ./kokoro/testutils/check_go_generated_files_up_to_date.sh <go project dir>
 
-check_go_generated_files_up_to_date() {
-  local go_project_dir="$1"
+BAZEL_CMD="bazel"
+# Use Bazelisk (https://github.com/bazelbuild/bazelisk) if available.
+if command -v "bazelisk" &> /dev/null; then
+  BAZEL_CMD="bazelisk"
+  "${BAZEL_CMD}" version
+fi
+
+usage() {
+  echo "Usage: $0 [-h] [-c <compat value (default 1.19)>] <go project dir>"
+  echo "  -c: Value to pass to `-compat`. Default to 1.19."
+  echo "  -h: Help. Print this usage information."
+  exit 1
+}
+
+COMPAT="1.19"
+GO_PROJECT_DIR=
+
+process_args() {
+  # Parse options.
+  while getopts "hc:" opt; do
+    case "${opt}" in
+      c) COMPAT="${OPTARG}" ;;
+      *) usage ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+  readonly GO_PROJECT_DIR="$1"
+  if [[ -z "${GO_PROJECT_DIR}" ]]; then
+    usage
+  fi
+}
+
+main() {
+  process_args "$@"
 
   (
-    cd "${go_project_dir}"
+    cd "${GO_PROJECT_DIR}"
     local -r temp_dir_current_generated_files="$(mktemp -dt \
       current_tink_go_build_files.XXXXXX)"
     local -r go_generated_files=(
@@ -45,19 +71,20 @@
 
     for generated_file_path in "${current_go_generated_files[@]}"; do
       mkdir -p \
-        "$(dirname "${temp_dir_current_generated_files}/${generated_file_path}")"
+        "$(dirname \
+          "${temp_dir_current_generated_files}/${generated_file_path}")"
       cp "${generated_file_path}" \
         "${temp_dir_current_generated_files}/${generated_file_path}"
     done
 
-    # Update build files
-    go mod tidy
-    # Update deps.bzl
-    bazel run //:gazelle-update-repos
-    # Update all BUILD.bazel files
-    bazel run //:gazelle
+    # Update build files.
+    go mod tidy -compat="${COMPAT}"
+    # Update deps.bzl.
+    "${BAZEL_CMD}" run //:gazelle-update-repos
+    # Update all BUILD.bazel files.
+    "${BAZEL_CMD}" run //:gazelle
 
-    # Compare current with new build files
+    # Compare current with new build files.
     local new_go_generated_files=( "${go_generated_files[@]}" )
     while read -r -d $'\0' generated_file; do
       new_go_generated_files+=("${generated_file}")
@@ -67,12 +94,12 @@
     for generated_file_path in "${new_go_generated_files[@]}"; do
       if ! cmp -s "${generated_file_path}" \
           "${temp_dir_current_generated_files}/${generated_file_path}"; then
-        echo "FAIL: ${generated_file_path} needs to be updated. Please follow \
-the instructions on go/tink-workflows#update-go-build."
+        echo "ERROR: ${generated_file_path} needs to be updated. Please follow \
+the instructions on go/tink-workflows#update-go-build." >&2
         exit 1
       fi
     done
   )
 }
 
-check_go_generated_files_up_to_date "$@"
+main "$@"
diff --git a/kokoro/testutils/copy_credentials.sh b/kokoro/testutils/copy_credentials.sh
index 168b27e..37493bc 100755
--- a/kokoro/testutils/copy_credentials.sh
+++ b/kokoro/testutils/copy_credentials.sh
@@ -66,6 +66,10 @@
 #   TINK_TEST_SERVICE_ACCOUNT
 #######################################
 copy_gcp_credentials() {
+  if [[ -z "${TINK_TEST_SERVICE_ACCOUNT}" ]]; then
+    echo "ERROR: TINK_TEST_SERVICE_ACCOUNT is expected to be set" >&2
+    exit 1
+  fi
   cp "${TINK_TEST_SERVICE_ACCOUNT}" "${TESTDATA_DIR}/gcp/credential.json"
 }
 
@@ -77,6 +81,11 @@
 #   AWS_TINK_TEST_SERVICE_ACCOUNT
 #######################################
 copy_aws_credentials() {
+  if [[ -z "${AWS_TINK_TEST_SERVICE_ACCOUNT}" ]]; then
+    echo "ERROR: AWS_TINK_TEST_SERVICE_ACCOUNT is expected to be set" >&2
+    exit 1
+  fi
+
   # Create the different format for the AWS credentials
   local -r aws_key_id="AKIATNYZMJOHVMN7MSYH"
   local -r aws_key="$(cat ${AWS_TINK_TEST_SERVICE_ACCOUNT})"
@@ -101,6 +110,7 @@
 
 main() {
   if [[ -z "${KOKORO_ROOT}" ]]; then
+    echo "Not running on Kokoro, skipping copying credentials."
     exit 0
   fi
 
diff --git a/kokoro/testutils/create_main_build_file.py b/kokoro/testutils/create_main_build_file.py
index 0c2fbf0..6da4efb 100644
--- a/kokoro/testutils/create_main_build_file.py
+++ b/kokoro/testutils/create_main_build_file.py
@@ -28,7 +28,6 @@
 ## This file is created using "create_main_build_file.py".
 
 load("//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-load("//tools:check_deps.bzl", "check_deps")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -80,7 +79,7 @@
         "Automatic-Module-Name: com.google.crypto.tink.integration.awskms",
     ],
     root_packages = [
-        "com.google.crypto.tink",
+        "com.google.crypto.tink.integration.awskms",
     ],
     deps = [
 $awsk_deps_formatted
@@ -94,19 +93,11 @@
         "Automatic-Module-Name: com.google.crypto.tink.integration.gcpkms",
     ],
     root_packages = [
-        "com.google.crypto.tink",
+        "com.google.crypto.tink.integration.gcpkms",
     ],
     deps = [
 $gcpk_deps_formatted
     ],
-)
-
-# Check that tink-android depends on protobuf-lite, not the full version.
-check_deps(
-    name = "tink-android-dep-checks",
-    disallowed_deps = ["@com_google_protobuf//java/core:core"],
-    required_deps = ["@com_google_protobuf//java/lite:lite"],
-    deps = [":tink-android-unshaded"],
 )""")
 
 
diff --git a/kokoro/testutils/install_cmake.sh b/kokoro/testutils/install_cmake.sh
index 0891925..ee423b6 100755
--- a/kokoro/testutils/install_cmake.sh
+++ b/kokoro/testutils/install_cmake.sh
@@ -38,17 +38,20 @@
   local cmake_archive="${cmake_name}.tar.gz"
   local cmake_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}/${cmake_archive}"
   local cmake_tmpdir="$(mktemp -dt tink-cmake.XXXXXX)"
+  local -r cmake_folder="${cmake_tmpdir}/${cmake_name}"
   (
     cd "${cmake_tmpdir}"
     curl -OLsS "${cmake_url}"
     echo "${cmake_sha256} ${cmake_archive}" | sha256sum -c
-
-    tar xzf "${cmake_archive}"
+    mkdir -p "${cmake_folder}"
+    # This is needed because versions <= 3.19.X are named
+    # cmake-<VERSION>-Linux-x86_64.
+    tar xzf "${cmake_archive}" -C "${cmake_folder}" --strip-component=1
   )
   export PATH="${cmake_tmpdir}/${cmake_name}/bin:${PATH}"
 }
 
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then
   # If specifying the version, users must also specify the digest.
   if (( "$#" == 1 )); then
     echo \
diff --git a/kokoro/testutils/install_go.sh b/kokoro/testutils/install_go.sh
index 2d70e87..5c39ffc 100755
--- a/kokoro/testutils/install_go.sh
+++ b/kokoro/testutils/install_go.sh
@@ -26,7 +26,7 @@
 #  source ./kokoro/testutils/install_go.sh
 
 install_temp_go() {
-  local -r go_version="1.17.7"
+  local -r go_version="1.19.9"
 
   local -r platform="$(uname | tr '[:upper:]' '[:lower:]')"
   local go_platform
@@ -58,6 +58,6 @@
   export PATH="${go_tmpdir}/go/bin:${PATH}"
 }
 
-if [[ -n "${KOKORO_ROOT:-}" ]] ; then
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]] ; then
   install_temp_go
 fi
diff --git a/kokoro/testutils/install_openssl.sh b/kokoro/testutils/install_openssl.sh
index 339585d..65b38ec 100755
--- a/kokoro/testutils/install_openssl.sh
+++ b/kokoro/testutils/install_openssl.sh
@@ -30,15 +30,19 @@
 
 readonly DEFAULT_OPENSSL_VERSION="1.1.1l"
 readonly DEFAULT_OPENSSL_SHA256="0b7a3e5e59c34827fe0c3a74b7ec8baef302b98fa80088d7f9153aa16fa76bd1"
+readonly PLATFORM="$(uname | tr '[:upper:]' '[:lower:]')"
 
 install_openssl() {
   local openssl_version="${1:-${DEFAULT_OPENSSL_VERSION}}"
   local openssl_sha256="${2:-${DEFAULT_OPENSSL_SHA256}}"
+
   local openssl_name="openssl-${openssl_version}"
   local openssl_archive="${openssl_name}.tar.gz"
   local openssl_url="https://www.openssl.org/source/${openssl_archive}"
 
-  local openssl_tmpdir="$(mktemp -dt tink-openssl.XXXXXX)"
+  local openssl_tmpdir="$(mktemp -dt tink-openssl-${openssl_version}.XXXXXX)"
+  echo "Building and installing OpensSSL ${openssl_version} to \
+${openssl_tmpdir}..."
   (
     cd "${openssl_tmpdir}"
     curl -OLsS "${openssl_url}"
@@ -47,14 +51,19 @@
     tar xzf "${openssl_archive}"
     cd "${openssl_name}"
     ./config --prefix="${openssl_tmpdir}" --openssldir="${openssl_tmpdir}"
-    make
-    make install
+    if [[ "${PLATFORM}" == "darwin" ]]; then
+      make -j "$(sysctl -n hw.ncpu)" > /dev/null
+    else
+      make -j "$(nproc)" > /dev/null
+    fi
+    make install_sw > /dev/null
   )
+  echo "Done"
   export OPENSSL_ROOT_DIR="${openssl_tmpdir}"
   export PATH="${openssl_tmpdir}/bin:${PATH}"
 }
 
-if [[ -n "${KOKORO_ROOT:-}" ]]; then
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then
   # If specifying the version, users must also specify the digest.
   if (( "$#" == 1 )); then
     echo \
diff --git a/kokoro/testutils/install_protoc.sh b/kokoro/testutils/install_protoc.sh
index e5898ea..59cf82c 100755
--- a/kokoro/testutils/install_protoc.sh
+++ b/kokoro/testutils/install_protoc.sh
@@ -24,7 +24,8 @@
 # Usage:
 #   source ./kokoro/testutils/install_protoc.sh [version]
 
-readonly DEFAULT_PROTOC_VERSION="3.19.3"
+## Per default, use X.21.9.
+readonly DEFAULT_PROTOC_VERSION="21.9"
 
 install_temp_protoc() {
   local protoc_version="${1:-${DEFAULT_PROTOC_VERSION}}"
diff --git a/kokoro/testutils/install_tink_via_pip.sh b/kokoro/testutils/install_tink_via_pip.sh
old mode 100644
new mode 100755
index b964bc6..678932a
--- a/kokoro/testutils/install_tink_via_pip.sh
+++ b/kokoro/testutils/install_tink_via_pip.sh
@@ -18,53 +18,73 @@
 # This scripts installs Tink for Python and its dependencies using Pip.
 # Tink's root folder must be specified.
 #
-# NOTES:
-#   * If not running on Kokoro, this script will do nothing.
-#   * This script MUST be sourced to update the environment of the calling
-#     script.
-#   * The required Bazel version *must* be installed before running this script
-#     with:
-#       use_bazel.sh "$(cat <path to version file>/.bazelversion)"
-#
-# Usage:
-#   source ./kokoro/testutils/install_tink_via_pip.sh <path to tink python root>
+# NOTE: If not running on Kokoro, this script will do nothing.
 
-install_tink_via_pip() {
-  local tink_py_path="${1}"
+set -eo pipefail
+
+usage() {
+  cat <<EOF
+Usage:  $0 <path to tink python root> <tink python deps dir>
+  -h: Help. Print this usage information.
+EOF
+  exit 1
+}
+
+TINK_PY_ROOT_DIR=
+TINK_PY_DEPS_BASE_DIR=
+
+#######################################
+# Process command line arguments.
+#######################################
+process_args() {
+  # Parse options.
+  while getopts "h" opt; do
+    case "${opt}" in
+      *) usage ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+  TINK_PY_ROOT_DIR="$1"
+  if [[ -z "${TINK_PY_ROOT_DIR}" ]]; then
+    echo "ERROR: The root folder of Tink Python must be specified" >&2
+    usage
+  fi
+  TINK_PY_DEPS_BASE_DIR="$2"
+  if [[ -z "${TINK_PY_DEPS_BASE_DIR}" ]]; then
+    echo "ERROR: The folder containing Tink Python's dependencies must be \
+specified" >&2
+    usage
+  fi
+  readonly TINK_PY_ROOT_DIR
+  readonly TINK_PY_DEPS_BASE_DIR
+}
+
+
+main() {
+  process_args "$@"
+  if [[ -z "${KOKORO_ROOT:-}" ]] ; then
+    echo "Not running on Kokoro, skip installing tink-py"
+    return
+  fi
   (
-    cd "${tink_py_path}"
-    readonly local platform="$(uname | tr '[:upper:]' '[:lower:]')"
-    local setuptools_requirements="setuptools"
+    cd "${TINK_PY_ROOT_DIR}"
+    local -r platform="$(uname | tr '[:upper:]' '[:lower:]')"
     local -a pip_flags
     if [[ "${platform}" == 'darwin' ]]; then
       # On MacOS we need to use the --user flag as otherwise pip will complain
       # about permissions.
       pip_flags=( --user )
-      # TODO(b/219813176): Remove once Kokoro environment is compatible.
-      setuptools_requirements="setuptools==60.9.0"
     fi
     readonly pip_flags
 
     # Set base path to the Tink Python's dependencies.
-    export TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH="${PWD}/.."
-
-    # Temporary disable treating unset variables generating errors to avoid old
-    # versions of bash generating errors when expanding empty pip_flags array.
-    set +u
-    # Check if we can build Tink python package.
-    pip3 install "${pip_flags[@]}" --upgrade pip
-    pip3 install "${pip_flags[@]}" --upgrade "${setuptools_requirements}"
+    export TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH="${TINK_PY_DEPS_BASE_DIR}"
+    pip3 install "${pip_flags[@]}" --upgrade pip setuptools
+    # Install Tink Python requirements.
+    pip3 install "${pip_flags[@]}" --require-hashes -r requirements.txt
+    # Install Tink Python
     pip3 install "${pip_flags[@]}" .
-    # Install dependencies for the examples/python tests
-    pip3 install "${pip_flags[@]}" google-cloud-storage
-    set -u
   )
 }
 
-if [[ -n "${KOKORO_ROOT:-}" ]] ; then
-  if (( "$#" < 1 )); then
-    echo "Tink Python folder path must be specified" >&2
-    exit 1
-  fi
-  install_tink_via_pip "$@"
-fi
+main "$@"
diff --git a/kokoro/testutils/replace_http_archive_with_local_repository_test.sh b/kokoro/testutils/replace_http_archive_with_local_repository_test.sh
new file mode 100755
index 0000000..776c998
--- /dev/null
+++ b/kokoro/testutils/replace_http_archive_with_local_repository_test.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+readonly DEFAULT_DIR="$(dirname -- $0)"
+
+readonly DEFAULT_TESTDATA_PREFIX="${DEFAULT_DIR}/testdata/\
+replace_http_archive_with_local_repository_test_"
+
+readonly CLI="${1:-"${DEFAULT_DIR}/\
+replace_http_archive_with_local_repository.py"}"
+
+readonly TEST_UTILS="${2:-${DEFAULT_DIR}/test_utils.sh}"
+
+readonly GENERAL_TEST_EXPECTED="${3:-"${DEFAULT_TESTDATA_PREFIX}\
+general_test_expected.txt"}"
+
+readonly GENERAL_TEST_INPUT="${4:-"${DEFAULT_TESTDATA_PREFIX}\
+general_test_input.txt"}"
+
+readonly HTTP_ARCHIVE_DELETED_EXPECTED="${5:-"${DEFAULT_TESTDATA_PREFIX}\
+http_archive_deleted_expected.txt"}"
+
+readonly HTTP_ARCHIVE_DELETED_INPUT="${6:-"${DEFAULT_TESTDATA_PREFIX}\
+http_archive_deleted_input.txt"}"
+
+readonly HTTP_ARCHIVE_NOT_DELETED_EXPECTED="${7:-"${DEFAULT_TESTDATA_PREFIX}\
+http_archive_not_deleted_expected.txt"}"
+
+readonly HTTP_ARCHIVE_NOT_DELETED_INPUT="${8:-"${DEFAULT_TESTDATA_PREFIX}\
+http_archive_not_deleted_input.txt"}"
+
+# Load the test library.
+source "${TEST_UTILS}"
+
+_copy_test_file() {
+  local -r file_to_copy="$1"
+  local -r destination="$2"
+  cp "${file_to_copy}" "${destination}"
+  chmod 0666 "${destination}"
+}
+
+# Test that http_archive entries are correctly replaced.
+test_ReplaceHttpArchiveWithLocalRepositoryTest_GeneralTest() {
+  ls "${TEST_CASE_TMPDIR}"
+  _copy_test_file "${GENERAL_TEST_INPUT}" "${TEST_CASE_TMPDIR}/input.txt"
+  "${CLI}" -f "${TEST_CASE_TMPDIR}/input.txt" -t "/tmp/git"
+  ASSERT_CMD_SUCCEEDED
+  ASSERT_FILE_EQUALS "${TEST_CASE_TMPDIR}/input.txt" \
+    "${GENERAL_TEST_EXPECTED}"
+}
+
+# Test that loading http_archive isn't deleted because there is at least another
+# http_archive entry in the WORKSPACE file.
+test_ReplaceHttpArchiveWithLocalRepositoryTest_\
+HttpArchiveIsNotDeletedBecauseOtherHttpArchiveIsPresent() {
+  _copy_test_file "${HTTP_ARCHIVE_NOT_DELETED_INPUT}" \
+    "${TEST_CASE_TMPDIR}/input.txt"
+  "${CLI}" -f "${TEST_CASE_TMPDIR}/input.txt" -t "/tmp/git"
+  ASSERT_CMD_SUCCEEDED
+  ASSERT_FILE_EQUALS "${TEST_CASE_TMPDIR}/input.txt" \
+    "${HTTP_ARCHIVE_NOT_DELETED_EXPECTED}"
+}
+
+# Test that loading http_archive is deleted because there are no other uses of
+# http_archive.
+test_ReplaceHttpArchiveWithLocalRepositoryTest_\
+HttpArchiveIsDeletedBecauseOtherHttpArchiveIsComment() {
+  _copy_test_file "${HTTP_ARCHIVE_DELETED_INPUT}" \
+    "${TEST_CASE_TMPDIR}/input.txt"
+  "${CLI}" -f "${TEST_CASE_TMPDIR}/input.txt" -t "/tmp/git"
+  ASSERT_CMD_SUCCEEDED
+  ASSERT_FILE_EQUALS "${TEST_CASE_TMPDIR}/input.txt" \
+    "${HTTP_ARCHIVE_DELETED_EXPECTED}"
+}
+
+main() {
+  run_all_tests "$@"
+}
+
+main "$@"
diff --git a/kokoro/testutils/run_bazel_tests.sh b/kokoro/testutils/run_bazel_tests.sh
index 7c6d7bb..a22d1db 100755
--- a/kokoro/testutils/run_bazel_tests.sh
+++ b/kokoro/testutils/run_bazel_tests.sh
@@ -19,9 +19,6 @@
 # Users must spcify the WORKSPACE directory. Optionally, the user can specify
 # a set of additional manual targets to run.
 #
-# Usage:
-#   ./kokoro/testutils/run_bazel_tests.sh [-mh] <workspace directory> \
-#     [<manual target> <manual target> ...]
 
 # Note: -E extends the trap to shell functions, command substitutions, and
 # commands executed in a subshell environment.
@@ -30,10 +27,12 @@
 trap print_debug_output ERR
 
 usage() {
-  echo "Usage: $0 [-mh] <workspace directory> \\"
-  echo "         [<manual target> <manual target> ...]"
+  echo "Usage: $0 [-mh] [-b <build parameter> ...] [-t <test parameter> ...] \\"
+  echo "         <workspace directory> [<manual target> <manual target> ...]"
   echo "  -m: Runs only the manual targets. If set, manual targets must be"
   echo "      provided."
+  echo "  -b: Comma separated list of flags to pass to `bazel build`."
+  echo "  -t: Comma separated list of flags to pass to `bazel test`."
   echo "  -h: Help. Print this usage information."
   exit 1
 }
@@ -42,6 +41,9 @@
 MANUAL_ONLY="false"
 WORKSPACE_DIR=
 MANUAL_TARGETS=
+BAZEL_CMD="bazel"
+BUILD_FLAGS=()
+TEST_FLAGS=()
 
 #######################################
 # Process command line arguments.
@@ -52,10 +54,11 @@
 #######################################
 process_args() {
   # Parse options.
-  while getopts "mh" opt; do
+  while getopts "mhb:t:" opt; do
     case "${opt}" in
-      d) MANUAL_ONLY="true" ;;
-      h) usage ;;
+      m) MANUAL_ONLY="true" ;;
+      b) BUILD_FLAGS=($(echo "${OPTARG}" | tr ',' '\n')) ;;
+      t) TEST_FLAGS=($(echo "${OPTARG}" | tr ',' '\n')) ;;
       *) usage ;;
     esac
   done
@@ -75,6 +78,13 @@
   if [[ "${MANUAL_ONLY}" == "true" ]] && (( ${#MANUAL_TARGETS[@]} == 0 )); then
     usage
   fi
+
+  # Use Bazelisk (https://github.com/bazelbuild/bazelisk) if available.
+  if command -v "bazelisk" &> /dev/null; then
+    BAZEL_CMD="bazelisk"
+  fi
+  readonly BAZEL_CMD
+  echo "Using: $(which ${BAZEL_CMD})"
 }
 
 #######################################
@@ -88,30 +98,39 @@
 main() {
   process_args "$@"
 
-  local -a test_flags=(
+  TEST_FLAGS+=(
     --strategy=TestRunner=standalone
     --test_output=all
   )
+
+  local -r workspace_dir="$(cd ${WORKSPACE_DIR} && pwd)"
+
   if [[ "${PLATFORM}" == 'darwin' ]]; then
-    test_flags+=( --jvmopt="-Djava.net.preferIPv6Addresses=true" )
+    TEST_FLAGS+=( --jvmopt="-Djava.net.preferIPv6Addresses=true" )
+    if [[ "${workspace_dir}" =~ javascript ]]; then
+      BUILD_FLAGS+=( --experimental_inprocess_symlink_creation )
+      TEST_FLAGS+=( --experimental_inprocess_symlink_creation )
+    fi
   fi
-  readonly test_flags
+  readonly BUILD_FLAGS
+  readonly TEST_FLAGS
   (
-    cd "${WORKSPACE_DIR}"
+    set -x
+    cd "${workspace_dir}"
     if [[ "${MANUAL_ONLY}" == "false" ]]; then
-      time bazel build -- ...
+      time "${BAZEL_CMD}" build "${BUILD_FLAGS[@]}" -- ...
       # Exit code 4 means targets build correctly but no tests were found. See
       # https://bazel.build/docs/scripts#exit-codes.
       bazel_test_return=0
-      time bazel test "${test_flags[@]}" -- ... || bazel_test_return="$?"
+      time "${BAZEL_CMD}" test "${TEST_FLAGS[@]}" -- ... || bazel_test_return="$?"
       if (( $bazel_test_return != 0 && $bazel_test_return != 4 )); then
         return "${bazel_test_return}"
       fi
     fi
     # Run specific manual targets.
     if (( ${#MANUAL_TARGETS[@]} > 0 )); then
-      time bazel build -- "${MANUAL_TARGETS[@]}"
-      time bazel test "${test_flags[@]}"  -- "${MANUAL_TARGETS[@]}"
+      time "${BAZEL_CMD}" build "${BUILD_FLAGS[@]}" -- "${MANUAL_TARGETS[@]}"
+      time "${BAZEL_CMD}" test "${TEST_FLAGS[@]}"  -- "${MANUAL_TARGETS[@]}"
     fi
   )
 }
diff --git a/kokoro/testutils/run_cmake_tests.sh b/kokoro/testutils/run_cmake_tests.sh
index ffc7e41..22e1a7b 100755
--- a/kokoro/testutils/run_cmake_tests.sh
+++ b/kokoro/testutils/run_cmake_tests.sh
@@ -59,17 +59,18 @@
   process_args "$@"
   local -r cmake_parameters=(
     -DTINK_BUILD_TESTS=ON
-    -DCMAKE_CXX_STANDARD=11
+    -DCMAKE_CXX_STANDARD=14
+    -DCMAKE_CXX_STANDARD_REQUIRED=ON
     "${ADDITIONAL_CMAKE_PARAMETERS[@]}"
   )
   # We need an absolute path to the CMake project directory.
-  local -r tink_cmake_project_dir="$(pwd)/$(basename ${CMAKE_PROJECT_DIR})"
+  local -r tink_cmake_project_dir="$(cd "${CMAKE_PROJECT_DIR}" && pwd)"
   local -r cmake_build_dir="$(mktemp -dt cmake-build.XXXXXX)"
   cd "${cmake_build_dir}"
   cmake --version
   cmake "${tink_cmake_project_dir}" "${cmake_parameters[@]}"
-  make -j"$(nproc)" all
-  CTEST_OUTPUT_ON_FAILURE=1 make test
+  cmake --build . --parallel "$(nproc)"
+  CTEST_OUTPUT_ON_FAILURE=1 ctest --parallel "$(nproc)"
 }
 
 main "$@"
diff --git a/kokoro/testutils/test_utils.sh b/kokoro/testutils/test_utils.sh
new file mode 100755
index 0000000..3baffdd
--- /dev/null
+++ b/kokoro/testutils/test_utils.sh
@@ -0,0 +1,173 @@
+#!/bin/bash
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+# Set of utilities to run unit tests for bash scripts.
+#
+# Example usage:
+# From your test script:
+#   source some/path/to/test_utils.sh
+#
+#   # Test functions must be defined as follows:
+#   test_<Test Name>_<Test Case Name>() {
+#     # Do some ground work.
+#     # Run the test script.
+#     ./path/to/script_to_test <input1> <input2> ...
+#     ASSERT_CMD_SUCCEEDED
+#     ASSERT_FILE_EQUALS <file1> <file2>
+#   }
+#
+#   # Finds all the functions starting with `test_`, extracts test name and
+#   # test case, and run them.
+#   run_all_tests "$@"
+#
+
+# This is either set by Bazel or generated.
+: "${TEST_TMPDIR:="$(mktemp -td test.XXXXX)"}"
+readonly TEST_TMPDIR
+
+# Temporary directory for the testcase to use.
+TEST_CASE_TMPDIR=
+
+# Current test name.
+_CURRENT_TEST_SCOPE=
+
+# Current test case.
+_CURRENT_TEST_CASE=
+
+# True if at least one of the test cases terminated with an error.
+_HAS_ERROR="false"
+
+_print_testcase_failed_and_exit() {
+  echo "[   FAILED ] ${_CURRENT_TEST_SCOPE}.${_CURRENT_TEST_CASE}"
+  exit 1
+}
+
+#######################################
+# Starts a new test case.
+#
+# Globals:
+#   _CURRENT_TEST_SCOPE
+#   _CURRENT_TEST_CASE
+#######################################
+_start_test_case() {
+  echo "[ RUN      ] ${_CURRENT_TEST_SCOPE}.${_CURRENT_TEST_CASE}"
+  # Create a tmp dir for the test case.
+  TEST_CASE_TMPDIR="${TEST_TMPDIR}/${_CURRENT_TEST_SCOPE}/${_CURRENT_TEST_CASE}"
+  mkdir -p "${TEST_CASE_TMPDIR}"
+}
+
+#######################################
+# Ends a test case printing a success message.
+#
+# Globals:
+#   _CURRENT_TEST_SCOPE
+#   _CURRENT_TEST_CASE
+#######################################
+_end_test_case_with_success() {
+  test_case="$1"
+  echo "[       OK ] ${_CURRENT_TEST_SCOPE}.${_CURRENT_TEST_CASE}"
+}
+
+#######################################
+# Returns the list of tests defined in the test script.
+#
+# A test case is a function of the form:
+#     test_<Test Name>_<Test Case>
+#
+# This function returns all the functions starting with `test_`.
+#
+# Globals:
+#   None
+# Arguments:
+#   None
+#######################################
+_get_all_tests() {
+  declare -F |
+    while read line; do
+      case "${line}" in "declare -f test_"*)
+          echo "${line#declare -f }"
+        ;;
+      esac
+    done
+}
+
+#######################################
+# Runs a given test function.
+#
+# A test case is a function of the form:
+#     test_<Test Name>_<Test Case>
+#
+# This script extracts test name and test case from the name.
+#
+# Globals:
+#   _CURRENT_TEST_SCOPE
+# Arguments:
+#   None
+#######################################
+_do_run_test() {
+  test_function="$1"
+  IFS=_ read _CURRENT_TEST_SCOPE _CURRENT_TEST_CASE <<< "${test_function#test_}"
+  _start_test_case
+  (
+    # Make sure we exit only when assertions fail.
+    set +e
+    "${test_function}"
+  )
+  local -r result=$?
+  if (( $result == 0 )); then
+    _end_test_case_with_success
+  else
+    _HAS_ERROR="true"
+  fi
+}
+
+#######################################
+# Runs all the test cases defined in the test script file.
+# Globals:
+#   None
+# Arguments:
+#   None
+#
+#######################################
+run_all_tests() {
+  for test in $(_get_all_tests); do
+    _do_run_test "${test}"
+  done
+  # Make sure we return an error code for the failing test
+  if [[ "${_HAS_ERROR}" == "true" ]]; then
+    exit 1
+  fi
+}
+
+ASSERT_CMD_SUCCEEDED() {
+  if (( $? != 0 )); then
+      _print_testcase_failed_and_exit
+  fi
+}
+
+ASSERT_CMD_FAILED() {
+  if (( $? == 0 )); then
+      _print_testcase_failed_and_exit
+  fi
+}
+
+ASSERT_FILE_EQUALS() {
+  input_file="$1"
+  expected_file="$2"
+  if ! diff "${input_file}" "${expected_file}"; then
+    _print_testcase_failed_and_exit
+  fi
+}
diff --git a/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_general_test_expected.txt b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_general_test_expected.txt
new file mode 100644
index 0000000..185b987
--- /dev/null
+++ b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_general_test_expected.txt
@@ -0,0 +1,61 @@
+workspace(name = "some_example_workspace")
+
+local_repository(
+    name = "tink_cc",
+    path = "/tmp/git/tink_cc",
+)
+
+local_repository(
+    name = "tink_cc_awskms",
+    path = "/tmp/git/tink_cc_awskms",
+)
+
+local_repository(
+    name = "tink_cc_gcpkms",
+    path = "/tmp/git/tink_cc_gcpkms",
+)
+
+local_repository(
+    name = "tink_java",
+    path = "/tmp/git/tink_java",
+)
+
+local_repository(
+    name = "tink_java_awskms",
+    path = "/tmp/git/tink_java_awskms",
+)
+
+local_repository(
+    name = "tink_java_gcpkms",
+    path = "/tmp/git/tink_java_gcpkms",
+)
+
+local_repository(
+    name = "tink_py",
+    path = "/tmp/git/tink_py",
+)
+
+local_repository(
+    name = "tink_go",
+    path = "/tmp/git/tink_go",
+)
+
+local_repository(
+    name = "tink_go_gcpkms",
+    path = "/tmp/git/tink_go_gcpkms",
+)
+
+local_repository(
+    name = "com_github_tink_crypto_tink_go",
+    path = "/tmp/git/tink_go",
+)
+
+local_repository(
+    name = "com_github_tink_crypto_tink_go_gcpkms",
+    path = "/tmp/git/tink_go_gcpkms",
+)
+
+local_repository(
+    name = "com_github_tink_crypto_tink_go_awskms",
+    path = "/tmp/git/tink_go_awskms",
+)
diff --git a/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_general_test_input.txt b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_general_test_input.txt
new file mode 100644
index 0000000..88d5ec7
--- /dev/null
+++ b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_general_test_input.txt
@@ -0,0 +1,75 @@
+workspace(name = "some_example_workspace")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+    name = "tink_cc",
+    urls = ["https://github.com/tink-crypto/tink-cc/archive/main.zip"],
+    strip_prefix = "tink-cc-main",
+)
+
+http_archive(
+    name = "tink_cc_awskms",
+    urls = ["https://github.com/tink-crypto/tink-cc-awskms/archive/main.zip"],
+    strip_prefix = "tink-cc-awskms-main",
+)
+
+http_archive(
+    name = "tink_cc_gcpkms",
+    urls = ["https://github.com/tink-crypto/tink-cc-gcpkms/archive/main.zip"],
+    strip_prefix = "tink-cc-gcpkms-main",
+)
+
+http_archive(
+    name = "tink_java",
+    urls = ["https://github.com/tink-crypto/tink-java/archive/main.zip"],
+    strip_prefix = "tink-java-main",
+)
+
+http_archive(
+    name = "tink_java_awskms",
+    urls = ["https://github.com/tink-crypto/tink-java-awskms/archive/main.zip"],
+    strip_prefix = "tink-java-awskms-main",
+)
+
+http_archive(
+    name = "tink_java_gcpkms",
+    urls = ["https://github.com/tink-crypto/tink-java-gcpkms/archive/main.zip"],
+    strip_prefix = "tink-java-gcpkms-main",
+)
+
+http_archive(
+    name = "tink_py",
+    urls = ["https://github.com/tink-crypto/tink-py/archive/main.zip"],
+    strip_prefix = "tink-py-main",
+)
+
+http_archive(
+    name = "tink_go",
+    urls = ["https://github.com/tink-crypto/tink-go/archive/main.zip"],
+    strip_prefix = "tink-go-main",
+)
+
+http_archive(
+    name = "tink_go_gcpkms",
+    urls = ["https://github.com/tink-crypto/tink-go-gcpkms/archive/main.zip"],
+    strip_prefix = "tink-go-gcpkms-main",
+)
+
+http_archive(
+    name = "com_github_tink_crypto_tink_go",
+    urls = ["https://github.com/tink-crypto/tink-go/archive/main.zip"],
+    strip_prefix = "tink-go-main",
+)
+
+http_archive(
+    name = "com_github_tink_crypto_tink_go_gcpkms",
+    urls = ["https://github.com/tink-crypto/tink-go-gcpkms/archive/main.zip"],
+    strip_prefix = "tink-go-gcpkms-main",
+)
+
+http_archive(
+    name = "com_github_tink_crypto_tink_go_awskms",
+    urls = ["https://github.com/tink-crypto/tink-go-awskms/archive/main.zip"],
+    strip_prefix = "tink-go-awskms-main",
+)
diff --git a/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_deleted_expected.txt b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_deleted_expected.txt
new file mode 100644
index 0000000..e17098f
--- /dev/null
+++ b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_deleted_expected.txt
@@ -0,0 +1,15 @@
+workspace(name = "some_example_workspace")
+
+local_repository(
+    name = "tink_cc",
+    path = "/tmp/git/tink_cc",
+)
+
+local_repository(
+    name = "tink_cc_awskms",
+    path = "/tmp/git/tink_cc_awskms",
+)
+
+# This is a comment with http_archive( in it.
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
diff --git a/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_deleted_input.txt b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_deleted_input.txt
new file mode 100644
index 0000000..59529fb
--- /dev/null
+++ b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_deleted_input.txt
@@ -0,0 +1,19 @@
+workspace(name = "some_example_workspace")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+    name = "tink_cc",
+    urls = ["https://github.com/tink-crypto/tink-cc/archive/main.zip"],
+    strip_prefix = "tink-cc-main",
+)
+
+http_archive(
+    name = "tink_cc_awskms",
+    urls = ["https://github.com/tink-crypto/tink-cc-awskms/archive/main.zip"],
+    strip_prefix = "tink-cc-awskms-main",
+)
+
+# This is a comment with http_archive( in it.
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
diff --git a/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_not_deleted_expected.txt b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_not_deleted_expected.txt
new file mode 100644
index 0000000..cb64212
--- /dev/null
+++ b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_not_deleted_expected.txt
@@ -0,0 +1,38 @@
+workspace(name = "some_example_workspace")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+local_repository(
+    name = "tink_cc",
+    path = "/tmp/git/tink_cc",
+)
+
+local_repository(
+    name = "tink_cc_awskms",
+    path = "/tmp/git/tink_cc_awskms",
+)
+
+local_repository(
+    name = "tink_cc_gcpkms",
+    path = "/tmp/git/tink_cc_gcpkms",
+)
+
+local_repository(
+    name = "tink_py",
+    path = "/tmp/git/tink_py",
+)
+
+# -------------------------------------------------------------------------
+# Bazel Gazelle.
+# -------------------------------------------------------------------------
+# Release from 2021-10-11.
+http_archive(
+    name = "bazel_gazelle",
+    sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+    ],
+)
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
diff --git a/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_not_deleted_input.txt b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_not_deleted_input.txt
new file mode 100644
index 0000000..154c4ca
--- /dev/null
+++ b/kokoro/testutils/testdata/replace_http_archive_with_local_repository_test_http_archive_not_deleted_input.txt
@@ -0,0 +1,42 @@
+workspace(name = "some_example_workspace")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+    name = "tink_cc",
+    urls = ["https://github.com/tink-crypto/tink-cc/archive/main.zip"],
+    strip_prefix = "tink-cc-main",
+)
+
+http_archive(
+    name = "tink_cc_awskms",
+    urls = ["https://github.com/tink-crypto/tink-cc-awskms/archive/main.zip"],
+    strip_prefix = "tink-cc-awskms-main",
+)
+
+http_archive(
+    name = "tink_cc_gcpkms",
+    urls = ["https://github.com/tink-crypto/tink-cc-gcpkms/archive/main.zip"],
+    strip_prefix = "tink-cc-gcpkms-main",
+)
+
+http_archive(
+    name = "tink_py",
+    urls = ["https://github.com/tink-crypto/tink-py/archive/main.zip"],
+    strip_prefix = "tink-py-main",
+)
+
+# -------------------------------------------------------------------------
+# Bazel Gazelle.
+# -------------------------------------------------------------------------
+# Release from 2021-10-11.
+http_archive(
+    name = "bazel_gazelle",
+    sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+    ],
+)
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
diff --git a/kokoro/testutils/update_android_sdk.sh b/kokoro/testutils/update_android_sdk.sh
index 78f40f0..eb9ed58 100755
--- a/kokoro/testutils/update_android_sdk.sh
+++ b/kokoro/testutils/update_android_sdk.sh
@@ -33,6 +33,8 @@
 (yes || true) | "${ANDROID_HOME}/tools/bin/sdkmanager" \
   "build-tools;30.0.3" \
   "platform-tools" \
+  "platforms;android-23" \
+  "platforms;android-26" \
   "platforms;android-29" \
   "platforms;android-30" \
   "platforms;android-31" \
diff --git a/kokoro/testutils/upgrade_gcc.sh b/kokoro/testutils/upgrade_gcc.sh
new file mode 100755
index 0000000..edff7ec
--- /dev/null
+++ b/kokoro/testutils/upgrade_gcc.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+# This script upgrades GCC when running on Kokoro.
+#
+# Currently this is only needed for Ubuntu 1604 (gcc-5 => gcc-7).
+#
+# Usage instructions:
+#
+#  ./kokoro/testutils/upgrade_gcc.sh
+set -e
+
+upgrade_gcc() {
+  # Install gcc-7.
+  sudo apt install build-essential software-properties-common -y
+  sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
+  sudo apt update
+  sudo apt install gcc-7 g++-7 -y
+  sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 70 \
+    --slave /usr/bin/g++ g++ /usr/bin/g++-7
+  gcc -v
+}
+
+if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]] ; then
+  upgrade_gcc
+fi
diff --git a/maven/README.md b/maven/README.md
deleted file mode 100644
index e29b876..0000000
--- a/maven/README.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Tink Java on Maven
-
-Tink Java has 4 Maven artifacts, all are in the `com.google.crypto.tink` group.
-
-### `tink`
-
-The core of Tink built for server-side applications. It only depends on
-[`com.google.code.gson:gson`](https://search.maven.org/artifact/com.google.code.gson/gson)
-and
-[`com.google.protobuf:protobuf-java`](https://search.maven.org/artifact/com.google.protobuf/protobuf-java).
-
-### `tink-android`
-
-Similar to `tink` but is built for Android applicationss.
-
-This build includes an embeded copy
-[`com.google.protobuf:protobuf-lite`](https://search.maven.org/artifact/com.google.protobuf/protobuf-javalite),
-which is renamed to be `com.google.crypto.tink.shaded.protobuf`. This is done to
-avoid dependency conflicts with other common dependencies that depend on a
-conflicting version (e.g.  Firebase).
-
-### `tink-awskms`
-
-A plugin for the `tink` artifact that integrates Tink with AWS KMS.
-
-### `tink-gcpkms`
-
-A plugin for the `tink` artifact that integrates Tink with GCP KMS.
-
-## Publishing snapshots
-
-```shell
-./maven/publish_snapshot.sh
-```
-
-This command publishes latest snapshots to Maven, and their Javadocs to
-https://google.github.io/tink.
-
-Snapshots are automatically published for every new commit to the master branch
-of https://github.com/google/tink.
-
-## Testing snapshots
-
-```shell
-./maven/test_snapshot.sh
-```
-
-New snapshots are also automatically tested for every new commit to the master
-branch of https://github.com/google/tink.
-
-## Adding a dependency
-
-This should be considered only when there is no other option, 'cause adding
-dependency makes Tink bloated and less secure. If a dependency got tampered
-with, Tink would also be affected.
-
-This process consists of 3 steps:
-
-1.  To add a dependency for an artifact, you add it to the artifact's `pom.xml`.
-    For example, the `com.google.crypto.tink:tink` artifact uses `tink.pom.xml`.
-    You should always depend on the latest version of the dependency on Maven
-    Central.
-
-2.  Next, you want to publish a new snapshot of the artifact with the new
-    dependency.
-
-3.  Then you want to test the new snapshot.
-
-You want to repeat these steps until there is no error.
-
-If you encounter `Dependency convergence` error, it means that the artifact has
-already, usually indirectly, depended on another version of the new dependency.
-You want to exclude that version.
-
-See the history of the POM files for examples.
diff --git a/maven/apps-paymentmethodtoken.pom.xml b/maven/apps-paymentmethodtoken.pom.xml
deleted file mode 100644
index c905c3a..0000000
--- a/maven/apps-paymentmethodtoken.pom.xml
+++ /dev/null
@@ -1,121 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API for Google Payment Method Token</name>
-  <description>An implementation of Google Payment Method Token (https://developers.google.com/android-pay/integration/payment-token-cryptography), using Tink (https://github.com/google/tink).
-  </description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-paymentmethodtoken</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-    <google-api-client.version>1.22.0</google-api-client.version>
-    <gson.version>2.8.9</gson.version>
-    <joda-time.version>2.9.9</joda-time.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.api-client</groupId>
-      <artifactId>google-api-client</artifactId>
-      <version>${google-api-client.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava-jdk5</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>joda-time</groupId>
-      <artifactId>joda-time</artifactId>
-      <version>${joda-time.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.code.gson</groupId>
-      <artifactId>gson</artifactId>
-      <version>${gson.version}</version>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/maven/apps-rewardedads.pom.xml b/maven/apps-rewardedads.pom.xml
deleted file mode 100644
index eef324e..0000000
--- a/maven/apps-rewardedads.pom.xml
+++ /dev/null
@@ -1,117 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API for Google Mobile Rewarded Video Ads SSV</name>
-  <description>An implementation of the verifier side of Server-Side Verification of Google mobile rewarded video ads, using Tink (https://github.com/google/tink).
-  </description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-rewardedads</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-    <google-api-client.version>1.22.0</google-api-client.version>
-    <gson.version>2.8.9</gson.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.api-client</groupId>
-      <artifactId>google-api-client</artifactId>
-      <version>${google-api-client.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava-jdk5</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.code.gson</groupId>
-      <artifactId>gson</artifactId>
-      <version>${gson.version}</version>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/maven/apps-webpush.pom.xml b/maven/apps-webpush.pom.xml
deleted file mode 100644
index 269dbd8..0000000
--- a/maven/apps-webpush.pom.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API for Message Encryption for Web Push (RFC 8291)</name>
-  <description>An implementation of Message Encryption for Web Push (RFC 8291), using Tink (https://github.com/google/tink).
-  </description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-webpush</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/maven/execute_deploy.sh b/maven/execute_deploy.sh
deleted file mode 100755
index 61953fb..0000000
--- a/maven/execute_deploy.sh
+++ /dev/null
@@ -1,262 +0,0 @@
-#!/bin/bash
-
-# Copyright 2018 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-# Fail on any error.
-set -e
-
-usage() {
-  echo "Usage: $0 [-dlh] <maven goal> <version> <additional maven args...>"
-  echo "  -d: Dry run. Only execute idempotent commands (default: FALSE)."
-  echo "  -l: Local. Deploy locally (default: FALSE)."
-  echo "  -h: Help. Print this usage information."
-  exit 1
-}
-
-# Process flags.
-
-DRY_RUN="false"
-LOCAL="FALSE"
-
-while getopts "dlh" opt; do
-  case "${opt}" in
-    d) DRY_RUN="true" ;;
-    l) LOCAL="true" ;;
-    h) usage ;;
-    *) usage ;;
-  esac
-done
-shift $((OPTIND - 1))
-
-readonly DRY_RUN
-readonly LOCAL
-
-if (( $# < 2 )); then
-  usage
-fi
-
-# Process arguments.
-
-readonly MVN_GOAL="$1"
-readonly VERSION="$2"
-shift 2
-
-# All remaining arguments are used as extra arguments to maven.
-readonly EXTRA_MAVEN_ARGS=("$@")
-
-# URL of the git repository used for javadoc publishing.
-GIT_URL="[email protected]:google/tink.git"
-if [ -n "${KOKORO_ROOT}" ]; then
-  # GITHUB_ACCESS_TOKEN is populated from Keystore via the Kokoro configuration.
-  GIT_URL="https://ise-crypto:${GITHUB_ACCESS_TOKEN}@github.com/google/tink.git"
-fi
-readonly GIT_URL
-
-# Arguments to use for all git invocations.
-declare -a GIT_ARGS=(-c [email protected] -c user.name="Tink Team")
-readonly GIT_ARGS
-
-do_command() {
-  if ! "$@"; then
-    echo "*** Failed executing command. ***"
-    echo "Failed command: $@"
-    exit 1
-  fi
-  return $?
-}
-
-print_command() {
-  printf '%q ' '+' "$@"
-  echo
-}
-
-print_and_do() {
-  print_command "$@"
-  do_command "$@"
-  return $?
-}
-
-do_if_not_dry_run() {
-  # $@ is an array containing a command to be executed and its arguments.
-  print_command "$@"
-  if [[ "${DRY_RUN}" == "true" ]]; then
-    echo "  *** Dry run, command not executed. ***"
-    return 0
-  fi
-  do_command "$@"
-  return $?
-}
-
-echo_output_file() {
-  local workspace_dir="$1"
-  local library="$2"
-
-  (
-    cd "${workspace_dir}"
-    local file="bazel-bin/${library}"
-    if [[ ! -e "${file}" ]]; then
-       file="bazel-genfiles/${library}"
-    fi
-    if [[ ! -e "${file}" ]]; then
-      echo "Could not find Bazel output file for ${library}"
-      exit 1
-    fi
-    echo -n "${file}"
-  )
-}
-
-deploy_library() {
-  local library_name="$1"
-  local workspace_dir="$2"
-  local library="$3"
-  local src_jar="$4"
-  local javadoc="$5"
-  local pom_file="$6"
-
-  (
-    print_and_do cd "${workspace_dir}"
-    print_and_do bazel build "${library}" "${src_jar}" "${javadoc}"
-
-    local library_file="$(echo_output_file "." "${library}")"
-    local src_jar_file="$(echo_output_file "." "${src_jar}")"
-    local javadoc_file="$(echo_output_file "." "${javadoc}")"
-
-    # Update the version
-    do_if_not_dry_run sed -i \
-      's/VERSION_PLACEHOLDER/'"${VERSION}"'/' "${pom_file}"
-
-    do_if_not_dry_run mvn "${MVN_GOAL}" \
-      -Dfile="${library_file}" \
-      -Dsources="${src_jar_file}" \
-      -Djavadoc="${javadoc_file}" \
-      -DpomFile="${pom_file}" \
-      "${EXTRA_MAVEN_ARGS[@]:+${EXTRA_MAVEN_ARGS[@]}}"
-
-    # Reverse the version change
-    do_if_not_dry_run sed -i \
-      's/'"${VERSION}"'/VERSION_PLACEHOLDER/' "${pom_file}"
-  )
-
-  publish_javadoc_to_github_pages \
-    "${library_name}" \
-    "${workspace_dir}" \
-    "${javadoc}"
-}
-
-publish_javadoc_to_github_pages() {
-  if [[ "${LOCAL}" == "true" ]]; then
-    echo "Local deployment, skipping publishing javadoc to GitHub Pages..."
-    return 0
-  fi
-
-  local library_name="$1"
-  local workspace_dir="$2"
-  local javadoc="$3"
-
-  local javadoc_file="$(echo_output_file "${workspace_dir}" "${javadoc}")"
-  javadoc_file="${workspace_dir}/${javadoc_file}"
-  readonly javadoc_file
-
-  print_and_do rm -rf gh-pages
-  print_and_do git "${GIT_ARGS[@]}" clone \
-    --quiet --branch=gh-pages "${GIT_URL}" gh-pages > /dev/null
-  (
-    print_and_do cd gh-pages
-    if [ -d "javadoc/${library_name}/${VERSION}" ]; then
-      print_and_do git "${GIT_ARGS[@]}" rm -rf \
-          "javadoc/${library_name}/${VERSION}"
-    fi
-    print_and_do mkdir -p "javadoc/${library_name}/${VERSION}"
-    print_and_do unzip "../${javadoc_file}" \
-      -d "javadoc/${library_name}/${VERSION}"
-    print_and_do rm -rf "javadoc/${library_name}/${VERSION}/META-INF/"
-    print_and_do git "${GIT_ARGS[@]}" add \
-      -f "javadoc/${library_name}/${VERSION}"
-    if [[ "$(git "${GIT_ARGS[@]}" status --porcelain)" ]]; then
-      # Changes exist.
-      do_if_not_dry_run \
-        git "${GIT_ARGS[@]}" commit \
-        -m "${library_name}-${VERSION} Javadoc auto-pushed to gh-pages"
-
-      do_if_not_dry_run \
-        git "${GIT_ARGS[@]}" push -fq origin gh-pages > /dev/null
-      echo -e "Published Javadoc to gh-pages.\n"
-    else
-      # No changes exist.
-      echo -e "No changes in ${library_name}-${VERSION} Javadoc.\n"
-    fi
-  )
-}
-
-main() {
-  deploy_library \
-    tink \
-    java_src \
-    tink.jar \
-    tink-src.jar \
-    tink-javadoc.jar \
-    "../$(dirname $0)/tink.pom.xml"
-
-  deploy_library \
-    tink-awskms \
-    java_src \
-    tink-awskms.jar \
-    tink-awskms-src.jar \
-    tink-awskms-javadoc.jar \
-    "../$(dirname $0)/tink-awskms.pom.xml"
-
-  deploy_library \
-    tink-gcpkms \
-    java_src \
-    tink-gcpkms.jar \
-    tink-gcpkms-src.jar \
-    tink-gcpkms-javadoc.jar \
-    "../$(dirname $0)/tink-gcpkms.pom.xml"
-
-  deploy_library \
-    tink-android \
-    java_src \
-    tink-android.jar \
-    tink-android-src.jar \
-    tink-android-javadoc.jar \
-    "../$(dirname $0)/tink-android.pom.xml"
-
-  deploy_library \
-    apps-paymentmethodtoken \
-    apps \
-    paymentmethodtoken/maven.jar \
-    paymentmethodtoken/maven-src.jar \
-    paymentmethodtoken/maven-javadoc.jar \
-    "../$(dirname $0)/apps-paymentmethodtoken.pom.xml"
-
-  deploy_library \
-    apps-rewardedads \
-    apps \
-    rewardedads/maven.jar \
-    rewardedads/maven-src.jar \
-    rewardedads/maven-javadoc.jar \
-    "../$(dirname $0)/apps-rewardedads.pom.xml"
-
-  deploy_library \
-    apps-webpush \
-    apps \
-    webpush/maven.jar \
-    webpush/maven-src.jar \
-    webpush/maven-javadoc.jar \
-    "../$(dirname $0)/apps-webpush.pom.xml"
-}
-
-main "$@"
diff --git a/maven/publish_snapshot.sh b/maven/publish_snapshot.sh
deleted file mode 100755
index 1a4e312..0000000
--- a/maven/publish_snapshot.sh
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/bin/bash
-
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-####################################################################################
-
-# Fail on any error.
-set -e
-
-# Display commands to stderr.
-set -x
-
-usage() {
-  echo "Usage: $0 [-dlh]"
-  echo "  -d: Dry run. Only execute idempotent commands (default: FALSE)."
-  echo "  -l: Local. Publish to local Maven repository (default: FALSE)."
-  echo "  -h: Help. Print this usage information."
-  exit 1
-}
-
-# Process flags.
-
-DRY_RUN="false"
-LOCAL="false"
-
-while getopts "dlh" opt; do
-  case "${opt}" in
-    d) DRY_RUN="true" ;;
-    l) LOCAL="true" ;;
-    h) usage ;;
-    *) usage ;;
-  esac
-done
-shift $((OPTIND - 1))
-
-readonly DRY_RUN
-readonly LOCAL
-
-declare -a COMMON_FLAGS
-if [[ "${DRY_RUN}" == "true" ]]; then
-  COMMON_FLAGS+=( -d )
-fi
-readonly COMMON_FLAGS
-
-if [[ "${LOCAL}" == "true" ]]; then
-  echo -e "Publishing local Maven snapshot...\n"
-  bash "$(dirname $0)/execute_deploy.sh" "${COMMON_FLAGS[@]}" -l \
-    "install:install-file" \
-    "HEAD-SNAPSHOT"
-else
-  echo -e "Publishing Maven snapshot...\n"
-  bash "$(dirname $0)/execute_deploy.sh" "${COMMON_FLAGS[@]}" \
-    "deploy:deploy-file" \
-    "HEAD-SNAPSHOT" \
-    "-DrepositoryId=ossrh" \
-    "-Durl=https://oss.sonatype.org/content/repositories/snapshots" \
-    "--settings=../$(dirname $0)/settings.xml"
-fi
-
-echo -e "Finished publishing Maven snapshot."
diff --git a/maven/publish_to_maven_central.sh b/maven/publish_to_maven_central.sh
deleted file mode 100644
index e509db1..0000000
--- a/maven/publish_to_maven_central.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-####################################################################################
-
-set -eu
-
-if [ $# -lt 1 ]; then
-  echo "usage $0 <version-name>"
-  exit 1;
-fi
-
-version_name="$1"
-shift 1
-
-if [[ ! "${version_name}" =~ ^1\. ]]; then
-  echo 'Version name must begin with "1."'
-  exit 2
-fi
-
-if [[ "${version_name}" =~ " " ]]; then
-  echo "Version name must not have any spaces"
-  exit 3
-fi
-
-bash "$(dirname $0)/execute_deploy.sh" \
-  "gpg:sign-and-deploy-file" \
-  "${version_name}" \
-  "-DrepositoryId=ossrh" \
-  "-Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/" \
-  "-X"
diff --git a/maven/settings.xml b/maven/settings.xml
deleted file mode 100644
index d970660..0000000
--- a/maven/settings.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<settings>
-  <servers>
-    <server>
-      <id>ossrh</id>
-      <username>${env.SONATYPE_USERNAME}</username>
-      <password>${env.SONATYPE_PASSWORD}</password>
-    </server>
-  </servers>
-</settings>
diff --git a/maven/test_snapshot.sh b/maven/test_snapshot.sh
deleted file mode 100755
index 1b973a6..0000000
--- a/maven/test_snapshot.sh
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/bin/bash
-
-# Copyright 2017 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-####################################################################################
-
-# Fail on any error.
-set -e
-
-# Display commands to stderr.
-set -x
-
-# Version of Android build-tools required for gradle.
-readonly ANDROID_BUILD_TOOLS_VERSION="28.0.3"
-
-usage() {
-  echo "Usage: $0 [-lh]"
-  echo "  -l: Local. Publish to local Maven repository (default: FALSE)."
-  echo "  -h: Help. Print this usage information."
-  exit 1
-}
-
-# Process flags.
-
-LOCAL="false"
-
-while getopts "lh" opt; do
-  case "${opt}" in
-    l) LOCAL="true" ;;
-    h) usage ;;
-    *) usage ;;
-  esac
-done
-shift $((OPTIND - 1))
-
-readonly LOCAL
-
-#######################################
-# Test snapshot Maven packages using an example Java app.
-# Globals:
-#   LOCAL
-# Arguments:
-#   None
-#######################################
-test_java_snapshot() {
-  local -a mvn_flags
-  if [[ "${LOCAL}" == "true" ]]; then
-    # Use snapshots present in the local repository.
-    mvn_flags+=( --no-snapshot-updates )
-  fi
-  readonly mvn_flags
-
-  local -r test_tmpdir="$(mktemp -d)"
-  mkdir -p "${test_tmpdir}"
-
-  local -r test_util="tools/testing/cross_language/test_util.sh"
-  source "${test_util}" || exit 1
-
-  local -r pom_file="java_src/examples/helloworld/pom.xml"
-
-  mvn "${mvn_flags[@]}" package -f "${pom_file}"
-
-  local -r plaintext="${test_tmpdir}/plaintext.bin"
-  local -r encrypted="${test_tmpdir}/encrypted.bin"
-  local -r decrypted="${test_tmpdir}/decrypted.bin"
-  local -r keyset="${test_tmpdir}/keyset.cfg"
-
-  openssl rand 128 > "${plaintext}"
-  mvn exec:java "${mvn_flags[@]}" -f "${pom_file}" \
-    -Dexec.args="encrypt --keyset ${keyset} --in ${plaintext} --out ${encrypted}"
-  mvn exec:java "${mvn_flags[@]}" -f $pom_file \
-    -Dexec.args="decrypt --keyset ${keyset} --in ${encrypted} --out ${decrypted}"
-
-  assert_files_equal "${plaintext}" "${decrypted}"
-
-  rm -rf "${test_tmpdir}"
-}
-
-#######################################
-# Test snapshot Maven packages using an example Android app.
-# Globals:
-#   LOCAL
-# Arguments:
-#   None
-#######################################
-test_android_snapshot() {
-  local -a gradle_flags
-  if [[ "${LOCAL}" == "true" ]]; then
-    # Use snapshots present in the local repository.
-    gradle_flags+=( -PmavenLocation=local )
-  fi
-  readonly gradle_flags
-
-  # Only in the Kokoro environment.
-  if [[ -n "${KOKORO_ROOT}" ]]; then
-    yes | "${ANDROID_HOME}/tools/bin/sdkmanager" \
-      "build-tools;${ANDROID_BUILD_TOOLS_VERSION}"
-    yes | "${ANDROID_HOME}/tools/bin/sdkmanager" --licenses
-  fi
-
-  ./examples/android/helloworld/gradlew \
-    "${gradle_flags[@]}" \
-    -p ./examples/android/helloworld build
-}
-
-main() {
-  echo -e "Testing new Maven snapshot"
-  test_java_snapshot
-  test_android_snapshot
-  echo -e "New Maven snapshot works"
-}
-
-main "$@"
diff --git a/maven/tink-android.pom.xml b/maven/tink-android.pom.xml
deleted file mode 100644
index dcac49b..0000000
--- a/maven/tink-android.pom.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API for Android</name>
-  <description>Tink is a small cryptographic library that provides a safe, simple, agile and fast way to accomplish some common cryptographic tasks.
-  </description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>tink-android</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-
-    <gson.version>2.8.9</gson.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.code.gson</groupId>
-      <artifactId>gson</artifactId>
-      <version>${gson.version}</version>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/maven/tink-awskms.pom.xml b/maven/tink-awskms.pom.xml
deleted file mode 100644
index d9a419d..0000000
--- a/maven/tink-awskms.pom.xml
+++ /dev/null
@@ -1,224 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API with AWS KMS</name>
-  <description>This is a plugin that integrates Tink with AWS KMS.</description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>tink-awskms</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-
-    <aws-java-sdk-core.version>1.12.182</aws-java-sdk-core.version>
-    <aws-java-sdk-kms.version>1.12.182</aws-java-sdk-kms.version>
-    <commons-logging.version>1.2</commons-logging.version>
-    <google-auto-service.version>1.0.1</google-auto-service.version>
-    <google-guava.version>31.0.1-jre</google-guava.version>
-    <httpclient.version>4.5.13</httpclient.version>
-    <jackson.version>2.13.1</jackson.version>
-    <tink.version>VERSION_PLACEHOLDER</tink.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.amazonaws</groupId>
-      <artifactId>aws-java-sdk-core</artifactId>
-      <version>${aws-java-sdk-core.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.fasterxml.jackson.core</groupId>
-          <artifactId>jackson-databind</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.fasterxml.jackson.dataformat</groupId>
-          <artifactId>jackson-dataformat-cbor</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>org.apache.httpcomponents</groupId>
-          <artifactId>httpclient</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>commons-codec</groupId>
-          <artifactId>commons-codec</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>commons-logging</groupId>
-          <artifactId>commons-logging</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.amazonaws</groupId>
-      <artifactId>aws-java-sdk-kms</artifactId>
-      <version>${aws-java-sdk-kms.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.fasterxml.jackson.core</groupId>
-          <artifactId>jackson-databind</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-core</artifactId>
-      <version>${jackson.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-      <version>${jackson.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.fasterxml.jackson.core</groupId>
-          <artifactId>jackson-core</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.fasterxml.jackson.dataformat</groupId>
-      <artifactId>jackson-dataformat-cbor</artifactId>
-      <version>${jackson.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.fasterxml.jackson.core</groupId>
-          <artifactId>jackson-core</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-      <version>${google-guava.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.auto.service</groupId>
-      <artifactId>auto-service</artifactId>
-      <version>${google-auto-service.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink</artifactId>
-      <version>${tink.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>commons-logging</groupId>
-      <artifactId>commons-logging</artifactId>
-      <version>${commons-logging.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.httpcomponents</groupId>
-      <artifactId>httpclient</artifactId>
-      <version>${httpclient.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>commons-logging</groupId>
-          <artifactId>commons-logging</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>org.apache.httpcomponents</groupId>
-          <artifactId>httpcore</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-  </dependencies>
-</project>
diff --git a/maven/tink-gcpkms.pom.xml b/maven/tink-gcpkms.pom.xml
deleted file mode 100644
index 6841a7c..0000000
--- a/maven/tink-gcpkms.pom.xml
+++ /dev/null
@@ -1,190 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API with Google Cloud KMS</name>
-  <description>This is a plugin that integrates Tink with Google Cloud KMS.</description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>tink-gcpkms</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-
-    <google-api-client.version>1.33.2</google-api-client.version>
-    <google-api-services-cloudkms.version>v1-rev108-1.25.0</google-api-services-cloudkms.version>
-    <google-auto-service.version>1.0.1</google-auto-service.version>
-    <google-guava.version>31.0.1-jre</google-guava.version>
-    <httpclient.version>4.5.13</httpclient.version>
-    <gson.version>2.8.9</gson.version>
-    <google-auth-library-oauth2-http.version>1.5.3</google-auth-library-oauth2-http.version>
-    <tink.version>VERSION_PLACEHOLDER</tink.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.code.gson</groupId>
-      <artifactId>gson</artifactId>
-      <version>${gson.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.api-client</groupId>
-      <artifactId>google-api-client</artifactId>
-      <version>${google-api-client.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava-jdk5</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>org.apache.httpcomponents</groupId>
-          <artifactId>httpclient</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.google.http-client</groupId>
-          <artifactId>google-http-client-gson</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.google.http-client</groupId>
-          <artifactId>google-http-client</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.apis</groupId>
-      <artifactId>google-api-services-cloudkms</artifactId>
-      <version>${google-api-services-cloudkms.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.api-client</groupId>
-          <artifactId>google-api-client</artifactId>
-        </exclusion>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-      <version>${google-guava.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.auto.service</groupId>
-      <artifactId>auto-service</artifactId>
-      <version>${google-auto-service.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.crypto.tink</groupId>
-      <artifactId>tink</artifactId>
-      <version>${tink.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>org.apache.httpcomponents</groupId>
-      <artifactId>httpclient</artifactId>
-      <version>${httpclient.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>com.google.auth</groupId>
-      <artifactId>google-auth-library-oauth2-http</artifactId>
-      <version>${google-auth-library-oauth2-http.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.guava</groupId>
-          <artifactId>guava</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/maven/tink.pom.xml b/maven/tink.pom.xml
deleted file mode 100644
index d60ce65..0000000
--- a/maven/tink.pom.xml
+++ /dev/null
@@ -1,104 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2022 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <name>Tink Cryptography API</name>
-  <description>Tink is a small cryptographic library that provides a safe, simple, agile and fast way to accomplish some common cryptographic tasks.
-  </description>
-
-  <parent>
-    <groupId>org.sonatype.oss</groupId>
-    <artifactId>oss-parent</artifactId>
-    <version>7</version>
-  </parent>
-
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>tink</artifactId>
-  <version>VERSION_PLACEHOLDER</version>
-  <packaging>jar</packaging>
-  <url>http://github.com/google/tink</url>
-
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>
-    </license>
-  </licenses>
-
-  <distributionManagement>
-    <snapshotRepository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-    </snapshotRepository>
-    <repository>
-      <id>ossrh</id>
-      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-    </repository>
-  </distributionManagement>
-
-  <issueManagement>
-    <system>GitHub</system>
-    <url>https://github.com/google/tink/issues</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-      <name>tink-users</name>
-      <subscribe>[email protected]</subscribe>
-      <unsubscribe>[email protected]</unsubscribe>
-      <post>[email protected]</post>
-      <archive>https://groups.google.com/group/tink-users</archive>
-    </mailingList>
-  </mailingLists>
-
-  <developers>
-    <developer>
-      <organization>Google Inc.</organization>
-      <organizationUrl>https://www.google.com</organizationUrl>
-    </developer>
-  </developers>
-
-  <scm>
-    <connection>scm:git:[email protected]:google/tink.git</connection>
-    <developerConnection>scm:git:[email protected]:google/tink.git</developerConnection>
-    <url>https://github.com/google/tink.git</url>
-    <tag>HEAD</tag>
-  </scm>
-
-  <properties>
-    <java.version>1.8</java.version>
-    <gson.version>2.8.9</gson.version>
-    <protobuf.version>3.19.3</protobuf.version>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.protobuf</groupId>
-      <artifactId>protobuf-java</artifactId>
-      <version>${protobuf.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.code.gson</groupId>
-      <artifactId>gson</artifactId>
-      <version>${gson.version}</version>
-    </dependency>
-  </dependencies>
-</project>
diff --git a/objc/.bazelignore b/objc/.bazelignore
new file mode 100644
index 0000000..1e107f5
--- /dev/null
+++ b/objc/.bazelignore
@@ -0,0 +1 @@
+examples
diff --git a/objc/.bazelrc b/objc/.bazelrc
new file mode 100644
index 0000000..93a5e86
--- /dev/null
+++ b/objc/.bazelrc
@@ -0,0 +1,7 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
+# This sets -std=c++14 in the copt of *.mm files. See
+# https://github.com/bazelbuild/bazel/issues/5318.
+build --per_file_copt='.*\.mm\$@-std=c++14'
diff --git a/objc/.bazelversion b/objc/.bazelversion
index ac14c3d..09b254e 100644
--- a/objc/.bazelversion
+++ b/objc/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/objc/BUILD.bazel b/objc/BUILD.bazel
index 1093d01..d253972 100644
--- a/objc/BUILD.bazel
+++ b/objc/BUILD.bazel
@@ -1,11 +1,11 @@
 load("@build_bazel_rules_apple//apple:ios.bzl", "ios_static_framework", "ios_unit_test")
+load("//:minimum_os.bzl", "IOS_MINIMUM_OS")
 load("//:tink_version.bzl", "TINK_VERSION_LABEL")
 load("//:template_rule.bzl", "template_rule")
 
 licenses(["notice"])
 
-package(default_visibility = ["//tools/build_defs:internal_pkg"])
-
+package(default_visibility = ["//:__subpackages__"])
 
 # public libraries
 
@@ -79,8 +79,8 @@
     ":signature_config",
     ":signature_key_template",
     ":version",
-    "//objc/util:errors",
-    "//objc/util:strings",
+    "//Tink/util:errors",
+    "//Tink/util:strings",
 ]
 
 objc_library(
@@ -108,7 +108,7 @@
         "TINKKeysetHandle+Cleartext.h",
     ],
     bundle_name = "Tink",
-    minimum_os_version = "9.0",
+    minimum_os_version = IOS_MINIMUM_OS,
     deps = [
         ":cleartext_keyset_handle",
         ":objc",
@@ -127,10 +127,9 @@
     ],
     visibility = ["//visibility:public"],
     deps = [
-        ":registry_config",
-        "//cc/config:tink_config",
-        "//cc/util:errors",
-        "//objc/util:errors",
+        "//Tink:registry_config",
+        "//Tink/util:errors",
+        "@tink_cc//tink/config:tink_config",
     ],
 )
 
@@ -142,13 +141,13 @@
     ],
     visibility = ["//visibility:public"],
     deps = [
-        ":keyset_reader",
-        ":tink_cc_pb",
-        "//cc:binary_keyset_reader",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink:keyset_reader",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:binary_keyset_reader",
     ],
 )
 
@@ -159,10 +158,11 @@
         "TINKKeysetHandle+Cleartext.h",
     ],
     deps = [
-        ":keyset_handle",
-        ":keyset_reader",
-        "//cc:cleartext_keyset_handle",
-        "//objc/util:errors",
+        "//Tink:keyset_handle",
+        "//Tink:keyset_reader",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:errors",
+        "@tink_cc//tink:cleartext_keyset_handle",
     ],
 )
 
@@ -174,9 +174,8 @@
     ],
     visibility = ["//visibility:public"],
     deps = [
-        ":registry_config",
-        "//cc:config",
-        "//objc/util:errors",
+        "//Tink:registry_config",
+        "//Tink/util:errors",
     ],
 )
 
@@ -189,12 +188,12 @@
     visibility = ["//visibility:public"],
     deps = [
         ":keyset_reader",
-        ":tink_cc_pb",
-        "//cc:json_keyset_reader",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:json_keyset_reader",
     ],
 )
 
@@ -207,10 +206,10 @@
     ],
     visibility = ["//visibility:public"],
     deps = [
-        ":tink_cc_pb",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:errors",
         "@com_google_absl//absl/status",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -230,16 +229,16 @@
         ":aead_internal",
         ":key_template",
         ":keyset_reader",
-        "//cc:binary_keyset_reader",
-        "//cc:binary_keyset_writer",
-        "//cc:cleartext_keyset_handle",
-        "//cc:keyset_handle",
-        "//cc/util:status",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:binary_keyset_reader",
+        "@tink_cc//tink:binary_keyset_writer",
+        "@tink_cc//tink:cleartext_keyset_handle",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -252,7 +251,7 @@
     ],
     visibility = ["//visibility:public"],
     deps = [
-        "//cc:keyset_reader",
+        "@tink_cc//tink:keyset_reader",
     ],
 )
 
@@ -261,12 +260,9 @@
     srcs = ["core/TINKRegistryConfig.mm"],
     hdrs = [
         "TINKRegistryConfig.h",
-        "core/TINKRegistryConfig_Internal.h",
     ],
     visibility = ["//visibility:public"],
-    deps = [
-        ":config_cc_pb",
-    ],
+    deps = [],
 )
 
 template_rule(
@@ -301,10 +297,10 @@
     hdrs = ["aead/TINKAeadInternal.h"],
     deps = [
         ":aead",
-        "//cc:aead",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:aead",
     ],
 )
 
@@ -315,10 +311,9 @@
     visibility = ["//visibility:public"],
     deps = [
         ":registry_config",
-        "//cc/aead:aead_config",
-        "//cc/util:errors",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
+        "@tink_cc//tink/aead:aead_config",
     ],
 )
 
@@ -331,10 +326,9 @@
         ":aead",
         ":aead_internal",
         ":keyset_handle",
-        "//cc:keyset_handle",
-        "//cc/aead:aead_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -345,11 +339,10 @@
     visibility = ["//visibility:public"],
     deps = [
         ":key_template",
-        ":tink_cc_pb",
-        "//cc/aead:aead_key_templates",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
         "@com_google_absl//absl/status",
+        "@tink_cc//tink/aead:aead_key_templates",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -370,10 +363,10 @@
     deps = [
         ":deterministic_aead",
         ":keyset_handle",
-        "//cc:deterministic_aead",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:deterministic_aead",
     ],
 )
 
@@ -384,10 +377,9 @@
     visibility = ["//visibility:public"],
     deps = [
         ":registry_config",
-        "//cc/daead:deterministic_aead_config",
-        "//cc/util:errors",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
+        "@tink_cc//tink/daead:deterministic_aead_config",
     ],
 )
 
@@ -400,10 +392,9 @@
         ":deterministic_aead",
         ":deterministic_aead_internal",
         ":keyset_handle",
-        "//cc:keyset_handle",
-        "//cc/daead:deterministic_aead_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -414,11 +405,10 @@
     visibility = ["//visibility:public"],
     deps = [
         ":key_template",
-        ":tink_cc_pb",
-        "//cc/daead:deterministic_aead_key_templates",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
         "@com_google_absl//absl/status",
+        "@tink_cc//tink/daead:deterministic_aead_key_templates",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -451,10 +441,9 @@
         ":hybrid_decrypt",
         ":hybrid_decrypt_internal",
         ":keyset_handle",
-        "//cc:keyset_handle",
-        "//cc/hybrid:hybrid_decrypt_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -464,10 +453,10 @@
     hdrs = ["hybrid/TINKHybridDecryptInternal.h"],
     deps = [
         ":hybrid_decrypt",
-        "//cc:hybrid_decrypt",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:hybrid_decrypt",
     ],
 )
 
@@ -480,10 +469,9 @@
         ":hybrid_encrypt",
         ":hybrid_encrypt_internal",
         ":keyset_handle",
-        "//cc:keyset_handle",
-        "//cc/hybrid:hybrid_encrypt_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -493,10 +481,10 @@
     hdrs = ["hybrid/TINKHybridEncryptInternal.h"],
     deps = [
         ":hybrid_encrypt",
-        "//cc:hybrid_encrypt",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:hybrid_encrypt",
     ],
 )
 
@@ -507,10 +495,9 @@
     visibility = ["//visibility:public"],
     deps = [
         ":registry_config",
-        "//cc/hybrid:hybrid_config",
-        "//cc/util:errors",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
+        "@tink_cc//tink/hybrid:hybrid_config",
     ],
 )
 
@@ -521,11 +508,11 @@
     visibility = ["//visibility:public"],
     deps = [
         ":key_template",
-        ":tink_cc_pb",
-        "//cc/hybrid:hybrid_key_templates",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:errors",
         "@com_google_absl//absl/status",
+        "@tink_cc//tink/hybrid:hybrid_key_templates",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -548,10 +535,9 @@
     visibility = ["//visibility:public"],
     deps = [
         ":registry_config",
-        "//cc/mac:mac_config",
-        "//cc/util:errors",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
+        "@tink_cc//tink/mac:mac_config",
     ],
 )
 
@@ -561,13 +547,12 @@
     hdrs = ["TINKMacFactory.h"],
     visibility = ["//visibility:public"],
     deps = [
-        ":keyset_handle",
-        ":mac",
-        ":mac_internal",
-        "//cc:keyset_handle",
-        "//cc/mac:mac_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink:keyset_handle",
+        "//Tink:mac",
+        "//Tink:mac_internal",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -576,11 +561,11 @@
     srcs = ["mac/TINKMacInternal.mm"],
     hdrs = ["mac/TINKMacInternal.h"],
     deps = [
-        ":mac",
-        "//cc:mac",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink:mac",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:mac",
     ],
 )
 
@@ -590,29 +575,11 @@
     hdrs = ["TINKMacKeyTemplate.h"],
     visibility = ["//visibility:public"],
     deps = [
-        ":key_template",
-        ":tink_cc_pb",
-        "//cc/mac:mac_key_templates",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink:key_template",
+        "//Tink/util:errors",
         "@com_google_absl//absl/status",
-    ],
-)
-
-# C++ protocol buffers.
-# We need to convert them to cc_library so we can add them as a dependency of objc_library targets.
-
-cc_library(
-    name = "config_cc_pb",
-    deps = [
-        "//proto:config_cc_proto",
-    ],
-)
-
-cc_library(
-    name = "tink_cc_pb",
-    deps = [
-        "//proto:tink_cc_proto",
+        "@tink_cc//tink/mac:mac_key_templates",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -643,9 +610,8 @@
     visibility = ["//visibility:public"],
     deps = [
         ":registry_config",
-        "//cc/signature:signature_config",
-        "//cc/util:errors",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink/signature:signature_config",
     ],
 )
 
@@ -656,11 +622,11 @@
     visibility = ["//visibility:public"],
     deps = [
         ":key_template",
-        ":tink_cc_pb",
-        "//cc/signature:signature_key_templates",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:errors",
         "@com_google_absl//absl/status",
+        "@tink_cc//tink/signature:signature_key_templates",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -670,10 +636,10 @@
     hdrs = ["signature/TINKPublicKeySignInternal.h"],
     deps = [
         ":public_key_sign",
-        "//cc:public_key_sign",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:public_key_sign",
     ],
 )
 
@@ -683,10 +649,10 @@
     hdrs = ["signature/TINKPublicKeyVerifyInternal.h"],
     deps = [
         ":public_key_verify",
-        "//cc:public_key_verify",
-        "//objc/util:errors",
-        "//objc/util:strings",
+        "//Tink/util:errors",
+        "//Tink/util:strings",
         "@com_google_absl//absl/strings",
+        "@tink_cc//tink:public_key_verify",
     ],
 )
 
@@ -699,10 +665,9 @@
         ":keyset_handle",
         ":public_key_sign",
         ":public_key_sign_internal",
-        "//cc:keyset_handle",
-        "//cc/signature:public_key_sign_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -715,10 +680,9 @@
         ":keyset_handle",
         ":public_key_verify",
         ":public_key_verify_internal",
-        "//cc:keyset_handle",
-        "//cc/signature:public_key_verify_factory",
-        "//cc/util:status",
-        "//objc/util:errors",
+        "//Tink/util:errors",
+        "@tink_cc//tink:keyset_handle",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -728,7 +692,7 @@
 
 ios_unit_test(
     name = "TinkTests",
-    minimum_os_version = "9.0",
+    minimum_os_version = IOS_MINIMUM_OS,
     deps = [
         ":UnitTestLib",
     ],
@@ -743,26 +707,33 @@
             "Tests/UnitTests/**/*.mm",
         ],
         exclude = [
-            "Tests/UnitTests/core/TINKJSONKeysetReaderTest.mm",
+            # TODO(b/155060426) Enable the KeysetHandle Tests.
             "Tests/UnitTests/core/TINKKeysetHandleTest.mm",
         ],
     ),
     deps = [
         ":aead_internal",
+        ":key_template",
+        ":keyset_handle",
+        ":public_key_sign_internal",
+        ":public_key_verify_internal",
         ":testonly",
-        "//cc:aead",
-        "//cc:crypto_format",
-        "//cc:keyset_handle",
-        "//cc/aead:aead_config",
-        "//cc/aead:aead_factory",
-        "//cc/util:status",
-        "//cc/util:test_keyset_handle",
-        "//cc/util:test_util",
-        "//objc/util:proto_helpers",
-        "//objc/util:test_helpers",
-        "//proto:all_objc_proto",
-        "@com_google_absl//absl/memory",
+        "//Tink/proto_redirect:aes_gcm_cc_pb_redirect",
+        "//Tink/proto_redirect:aes_siv_cc_pb_redirect",
+        "//Tink/proto_redirect:hmac_cc_pb_redirect",
+        "//Tink/proto_redirect:tink_cc_pb_redirect",
+        "//Tink/util:strings",
         "@com_google_absl//absl/status",
-        "@com_google_protobuf//:protobuf_lite",
+        "@com_google_absl//absl/strings",
+        "@tink_cc//tink:insecure_secret_key_access",
+        "@tink_cc//tink:proto_keyset_format",
+        "@tink_cc//tink/aead:aead_config",
+        "@tink_cc//tink/aead:aes_gcm_key_manager",
+        "@tink_cc//tink/aead:xchacha20_poly1305_key_manager",
+        "@tink_cc//tink/daead:deterministic_aead_config",
+        "@tink_cc//tink/mac:mac_config",
+        "@tink_cc//tink/signature:signature_config",
+        "@tink_cc//tink/util:status",
+        "@tink_cc//tink/util:test_util",
     ],
 )
diff --git a/objc/TINKDeterministicAeadInternal.h b/objc/TINKDeterministicAeadInternal.h
index 07bc944..204e5e3 100644
--- a/objc/TINKDeterministicAeadInternal.h
+++ b/objc/TINKDeterministicAeadInternal.h
@@ -16,7 +16,7 @@
  **************************************************************************
  */
 
-#import "objc/TINKDeterministicAead.h"
+#import "TINKDeterministicAead.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/TINKJSONKeysetReader.h b/objc/TINKJSONKeysetReader.h
index 6eba8ce..88ff8b8 100644
--- a/objc/TINKJSONKeysetReader.h
+++ b/objc/TINKJSONKeysetReader.h
@@ -28,7 +28,7 @@
 
 /**
  * Initializes a TINKJSONKeysetReader using a serialized keyset in proto JSON wire format.
- * The serialized keyset can be cleartext or encrypted.
+ * The serialized keyset can either be cleartext or encrypted.
  */
 - (nullable instancetype)initWithSerializedKeyset:(NSData *)keyset
                                             error:(NSError **)error NS_DESIGNATED_INITIALIZER;
diff --git a/objc/Tests/Host/AppDelegate.m b/objc/Tests/Host/AppDelegate.m
index 8aff424..0e3a458 100644
--- a/objc/Tests/Host/AppDelegate.m
+++ b/objc/Tests/Host/AppDelegate.m
@@ -16,7 +16,7 @@
  **************************************************************************
  */
 
-#import "objc/Tests/Host/AppDelegate.h"
+#import "third_party/tink/objc/Tests/Host/AppDelegate.h"
 
 @interface AppDelegate ()
 
diff --git a/objc/Tests/Host/main.m b/objc/Tests/Host/main.m
index 1f71831..72562ef 100644
--- a/objc/Tests/Host/main.m
+++ b/objc/Tests/Host/main.m
@@ -18,7 +18,7 @@
 
 #import <UIKit/UIKit.h>
 
-#import "objc/Tests/Host/AppDelegate.h"
+#import "third_party/tink/objc/Tests/Host/AppDelegate.h"
 
 int main(int argc, char* argv[]) {
   @autoreleasepool {
diff --git a/objc/Tests/UnitTests/aead/TINKAeadFactoryTest.mm b/objc/Tests/UnitTests/aead/TINKAeadFactoryTest.mm
index 9721d7e..82da98a 100644
--- a/objc/Tests/UnitTests/aead/TINKAeadFactoryTest.mm
+++ b/objc/Tests/UnitTests/aead/TINKAeadFactoryTest.mm
@@ -16,39 +16,46 @@
  **************************************************************************
  */
 
-#import "objc/TINKAeadFactory.h"
+#import "TINKAeadFactory.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKAead.h"
-#import "objc/TINKAeadConfig.h"
-#import "objc/TINKAeadFactory.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKAead.h"
+#import "TINKAeadConfig.h"
+#import "TINKAeadFactory.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
 #include "tink/aead.h"
 #include "tink/aead/aead_config.h"
 #include "tink/aead/aes_gcm_key_manager.h"
 #include "tink/crypto_format.h"
+#include "tink/insecure_secret_key_access.h"
 #include "tink/keyset_handle.h"
+#include "tink/proto_keyset_format.h"
 #include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
 #include "tink/util/test_util.h"
-#include "proto/aes_gcm.pb.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
+#include "third_party/tink/objc/proto_redirect/aes_gcm_cc_pb_redirect.h"
 
-using crypto::tink::AesGcmKeyManager;
-using crypto::tink::KeyFactory;
-using crypto::tink::TestKeysetHandle;
-using crypto::tink::test::AddRawKey;
-using crypto::tink::test::AddTinkKey;
-using google::crypto::tink::AesGcmKey;
-using google::crypto::tink::AesGcmKeyFormat;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::Keyset;
-using google::crypto::tink::KeyStatusType;
+using ::crypto::tink::AesGcmKeyManager;
+using ::crypto::tink::InsecureSecretKeyAccess;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::ParseKeysetFromProtoKeysetFormat;
+using ::crypto::tink::test::AddRawKey;
+using ::crypto::tink::test::AddTinkKey;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::AesGcmKey;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
 
 @interface TINKAeadFactoryTest : XCTestCase
 @end
@@ -62,8 +69,11 @@
   XCTAssertNil(error);
 
   Keyset keyset;
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
+  StatusOr<crypto::tink::KeysetHandle> cc_keyset_handle =
+      ParseKeysetFromProtoKeysetFormat(/*serialized_keyset=*/"", InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(cc_keyset_handle.ok());
+  TINKKeysetHandle *handle = [[TINKKeysetHandle alloc]
+      initWithCCKeysetHandle:std::make_unique<KeysetHandle>(*cc_keyset_handle)];
   XCTAssertNotNil(handle);
 
   error = nil;
@@ -102,8 +112,11 @@
   XCTAssertNotNil(aeadConfig);
   XCTAssertNil(error);
 
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
+  StatusOr<crypto::tink::KeysetHandle> cc_keyset_handle =
+      ParseKeysetFromProtoKeysetFormat(keyset.SerializeAsString(), InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(cc_keyset_handle.ok());
+  TINKKeysetHandle *handle = [[TINKKeysetHandle alloc]
+      initWithCCKeysetHandle:std::make_unique<KeysetHandle>(*cc_keyset_handle)];
   XCTAssertNotNil(handle);
 
   id<TINKAead> aead = [TINKAeadFactory primitiveWithKeysetHandle:handle error:&error];
diff --git a/objc/Tests/UnitTests/aead/TINKAeadKeyTemplateTest.mm b/objc/Tests/UnitTests/aead/TINKAeadKeyTemplateTest.mm
index 2f8abd6..3c85811 100644
--- a/objc/Tests/UnitTests/aead/TINKAeadKeyTemplateTest.mm
+++ b/objc/Tests/UnitTests/aead/TINKAeadKeyTemplateTest.mm
@@ -16,16 +16,18 @@
  **************************************************************************
  */
 
-#import "objc/TINKAeadKeyTemplate.h"
+#import "TINKAeadKeyTemplate.h"
 
 #import <XCTest/XCTest.h>
 
+#include <memory>
+#include <string>
+#include <utility>
+
 #include "tink/aead/xchacha20_poly1305_key_manager.h"
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-
-using google::crypto::tink::XChaCha20Poly1305KeyFormat;
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
 
 @interface TINKAeadKeyTemplatesTest : XCTestCase
 @end
diff --git a/objc/Tests/UnitTests/core/TINKBinaryKeysetReaderTest.mm b/objc/Tests/UnitTests/core/TINKBinaryKeysetReaderTest.mm
index 13d9534..ed3c819 100644
--- a/objc/Tests/UnitTests/core/TINKBinaryKeysetReaderTest.mm
+++ b/objc/Tests/UnitTests/core/TINKBinaryKeysetReaderTest.mm
@@ -16,14 +16,18 @@
  **************************************************************************
  */
 
-#import "objc/TINKBinaryKeysetReader.h"
+#import "TINKBinaryKeysetReader.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "util/TINKStrings.h"
 
 #include "tink/util/test_util.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 static NSData *gBadSerializedKeyset;
 static NSData *gGoodSerializedKeyset;
diff --git a/objc/Tests/UnitTests/core/TINKCleartextKeysetHandleTest.mm b/objc/Tests/UnitTests/core/TINKCleartextKeysetHandleTest.mm
index 32b8dc0..12370a1 100644
--- a/objc/Tests/UnitTests/core/TINKCleartextKeysetHandleTest.mm
+++ b/objc/Tests/UnitTests/core/TINKCleartextKeysetHandleTest.mm
@@ -19,16 +19,29 @@
 #import <Foundation/Foundation.h>
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKBinaryKeysetReader.h"
-#import "objc/TINKKeysetHandle+Cleartext.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKBinaryKeysetReader.h"
+#import "TINKKeysetHandle+Cleartext.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
-#include "tink/util/test_keyset_handle.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/proto_keyset_format.h"
+#include "tink/util/secret_data.h"
+#include "tink/util/statusor.h"
 #include "tink/util/test_util.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
+
+using ::crypto::tink::InsecureSecretKeyAccess;
+using ::crypto::tink::SerializeKeysetToProtoKeysetFormat;
+using ::crypto::tink::util::SecretData;
+using ::crypto::tink::util::SecretDataAsStringView;
+using ::crypto::tink::util::StatusOr;
 
 @interface TINKCleartextKeysetHandleTest : XCTestCase
 @end
@@ -38,10 +51,10 @@
 - (void)testReadValidKeyset {
   google::crypto::tink::Keyset keyset;
   google::crypto::tink::Keyset::Key key;
-  crypto::tink::test::AddTinkKey("some key type", 42, key,
+  crypto::tink::test::AddTinkKey("some_key_type", 42, key,
                                  google::crypto::tink::KeyStatusType::ENABLED,
                                  google::crypto::tink::KeyData::SYMMETRIC, &keyset);
-  crypto::tink::test::AddRawKey("some other key type", 711, key,
+  crypto::tink::test::AddRawKey("some_other_key_type", 711, key,
                                 google::crypto::tink::KeyStatusType::ENABLED,
                                 google::crypto::tink::KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
@@ -59,9 +72,11 @@
       [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
 
   XCTAssertNotNil(handle);
-  XCTAssertTrue(
-      crypto::tink::TestKeysetHandle::GetKeyset(*handle.ccKeysetHandle).SerializeAsString() ==
-      keyset.SerializeAsString());
+  StatusOr<SecretData> serialized =
+      SerializeKeysetToProtoKeysetFormat(*handle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serialized.ok());
+  XCTAssertEqualObjects(TINKStringViewToNSData(SecretDataAsStringView(*serialized)),
+                        TINKStringViewToNSData(keyset.SerializeAsString()));
 
   // Trying to use the same reader again must fail.
   error = nil;
@@ -91,10 +106,10 @@
 - (void)testSerializeKeyset {
   google::crypto::tink::Keyset keyset;
   google::crypto::tink::Keyset::Key key;
-  crypto::tink::test::AddTinkKey("some key type", 42, key,
+  crypto::tink::test::AddTinkKey("some_key_type", 42, key,
                                  google::crypto::tink::KeyStatusType::ENABLED,
                                  google::crypto::tink::KeyData::SYMMETRIC, &keyset);
-  crypto::tink::test::AddRawKey("some other key type", 711, key,
+  crypto::tink::test::AddRawKey("some_other_key_type", 711, key,
                                 google::crypto::tink::KeyStatusType::ENABLED,
                                 google::crypto::tink::KeyData::SYMMETRIC, &keyset);
   keyset.set_primary_key_id(42);
diff --git a/objc/Tests/UnitTests/core/TINKJSONKeysetReaderTest.mm b/objc/Tests/UnitTests/core/TINKJSONKeysetReaderTest.mm
index 1cab383..bee0592 100644
--- a/objc/Tests/UnitTests/core/TINKJSONKeysetReaderTest.mm
+++ b/objc/Tests/UnitTests/core/TINKJSONKeysetReaderTest.mm
@@ -16,21 +16,51 @@
  **************************************************************************
  */
 
-#import "objc/TINKJSONKeysetReader.h"
+#import "TINKJSONKeysetReader.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
 
-#include "google/protobuf/util/json_util.h"
-#include "tink/util/test_util.h"
-#include "proto/tink.pb.h"
+#import "TINKAead.h"
+#import "TINKAeadFactory.h"
+#import "TINKAllConfig.h"
+#import "TINKKeysetHandle+Cleartext.h"
+#import "TINKKeysetHandle.h"
+#import "util/TINKStrings.h"
 
-static NSData *gBadJSONSerializedKeyset;
-static NSData *gGoodJSONSerializedKeyset;
-static NSData *gGoodJSONSerializedEncryptedKeyset;
-static NSData *gGoodSerializedKeyset;
-static NSData *gGoodSerializedEncryptedKeyset;
+#include "absl/strings/escaping.h"
+
+constexpr absl::string_view kSingleKeyAesGcmKeyset = R"json(
+  {
+    "primaryKeyId":1931667682,
+    "key":[{
+      "keyData":{
+        "typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value":"GhD+9l0RANZjzZEZ8PDp7LRW",
+        "keyMaterialType":"SYMMETRIC"},
+      "status":"ENABLED",
+      "keyId":1931667682,
+      "outputPrefixType":"TINK"
+    }]
+  })json";
+
+@interface TINKJSONKeysetReader (FromStringView)
+
+- (instancetype)initWithStringView:(absl::string_view)view error:(NSError **)error;
+
+@end
+
+@implementation TINKJSONKeysetReader (FromStringView)
+
+- (instancetype)initWithStringView:(absl::string_view)view error:(NSError **)error {
+  NSData *serializedKeysetData = [NSData dataWithBytes:view.data() length:view.size()];
+  return [self initWithSerializedKeyset:serializedKeysetData error:error];
+}
+
+@end
 
 @interface TINKJSONKeysetReaderTest : XCTestCase
 @end
@@ -38,73 +68,192 @@
 @implementation TINKJSONKeysetReaderTest
 
 + (void)setUp {
-  google::protobuf::util::JsonPrintOptions json_options;
-  json_options.add_whitespace = true;
-  json_options.always_print_primitive_fields = true;
-
-  google::crypto::tink::Keyset keyset;
-  google::crypto::tink::Keyset::Key key;
-  crypto::tink::test::AddTinkKey("some key type", 42, key,
-                                 google::crypto::tink::KeyStatusType::ENABLED,
-                                 google::crypto::tink::KeyData::SYMMETRIC, &keyset);
-  crypto::tink::test::AddRawKey("some other key type", 711, key,
-                                google::crypto::tink::KeyStatusType::ENABLED,
-                                google::crypto::tink::KeyData::SYMMETRIC, &keyset);
-  keyset.set_primary_key_id(42);
-  std::string ccGoodSerializedKeyset;
-  auto status =
-      google::protobuf::util::MessageToJsonString(keyset, &ccGoodSerializedKeyset, json_options);
-  XCTAssertTrue(status.ok());
-
-  gGoodJSONSerializedKeyset = TINKStringToNSData(ccGoodSerializedKeyset);
-  gBadJSONSerializedKeyset = TINKStringToNSData("some weird string");
-
-  google::crypto::tink::EncryptedKeyset encrypted_keyset;
-  encrypted_keyset.set_encrypted_keyset("some ciphertext with keyset");
-
-  auto keyset_info = encrypted_keyset.mutable_keyset_info();
-  keyset_info->set_primary_key_id(42);
-  auto key_info = keyset_info->add_key_info();
-  key_info->set_type_url("some type_url");
-  key_info->set_key_id(42);
-  std::string ccGoodSerializedEncryptedKeyset;
-  status = google::protobuf::util::MessageToJsonString(
-      encrypted_keyset, &ccGoodSerializedEncryptedKeyset, json_options);
-  XCTAssertTrue(status.ok());
-
-  std::string tmp;
-  encrypted_keyset.SerializeToString(&tmp);
-  gGoodSerializedEncryptedKeyset = TINKStringToNSData(tmp);
-
-  gGoodJSONSerializedEncryptedKeyset = TINKStringToNSData(ccGoodSerializedEncryptedKeyset);
-  keyset.SerializeToString(&tmp);
-  gGoodSerializedKeyset = TINKStringToNSData(tmp);
+  NSError *error = nil;
+  TINKAllConfig *allConfig = [[TINKAllConfig alloc] initWithError:&error];
+  XCTAssertNotNil(allConfig);
+  XCTAssertNil(error);
 }
 
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wnonnull"
-- (void)testReaderCreation {
-  // Serialized keyset is nil.
+- (void)testCreateKeysetHandle {
   NSError *error = nil;
   TINKJSONKeysetReader *reader =
-      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:nil error:&error];
-  XCTAssertNil(reader);
-  XCTAssertNotNil(error);
-
-  // Good serialized keyset.
-  error = nil;
-  reader = [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:gGoodJSONSerializedKeyset
-                                                            error:&error];
+      [[TINKJSONKeysetReader alloc] initWithStringView:kSingleKeyAesGcmKeyset error:&error];
   XCTAssertNotNil(reader);
-  XCTAssertNil(error);
+  XCTAssertNil(error, @"Initialization of TINKJSONKeysetReader failed with %@", error);
 
-  // Bad serialized keyset.
-  error = nil;
-  reader =
-      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:gBadJSONSerializedKeyset error:&error];
-  XCTAssertNotNil(reader);
-  XCTAssertNil(error);
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNotNil(handle);
+  XCTAssertNil(error, @"Initialization of TINKKeysetHandle failed with %@", error);
 }
-#pragma clang diagnostic pop
+
+- (void)testCreateAead {
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader =
+      [[TINKJSONKeysetReader alloc] initWithStringView:kSingleKeyAesGcmKeyset error:&error];
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+
+  id<TINKAead> aead = [TINKAeadFactory primitiveWithKeysetHandle:handle error:&error];
+  XCTAssertNotNil(aead);
+  XCTAssertNil(error, @"Aead creation failed with %@", error);
+
+  NSString *kEmpty = @"";
+  NSData *kEmptyData = [kEmpty dataUsingEncoding:NSUTF8StringEncoding];
+
+  // Test vector for this key: encryption of the empty string with empty aad. Generated with Java.
+  std::string kCiphertext = absl::HexStringToBytes(
+      "017322e8e2c38b8d06b46b40010a6e2a19e572eb3e626ea64238bf9018fa61cbea");
+  NSData *kCiphertextData = [NSData dataWithBytes:kCiphertext.data() length:kCiphertext.size()];
+  NSData *computedPlaintext = [aead decrypt:kCiphertextData
+                         withAdditionalData:kEmptyData
+                                      error:&error];
+  XCTAssertNil(error, @"Decryption failed with %@", error);
+  XCTAssertEqual([computedPlaintext length], 0);
+}
+
+// If the keyset is not even json, we currently fail when initializing the TINKKeysetHandle.
+- (void)testCreateKeysetHandle_brokenKeysetNotJson_fails {
+  constexpr absl::string_view kKeysetNotJson = "This for sure isn't JSON }}}";
+
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader =
+      [[TINKJSONKeysetReader alloc] initWithStringView:kKeysetNotJson error:&error];
+  XCTAssertNotNil(reader);
+  XCTAssertNil(error, @"Initialization of TINKJSONKeysetReader failed with %@", error);
+
+  (void)[[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNotNil(error);
+}
+
+// If the keyset is broken e.g. because the primary doesn't exist, currently we only fail once we
+// create the primitive.
+- (void)testCreateKeysetHandle_brokenKeysetNoPrimary_fails {
+  constexpr absl::string_view kKeysetWithNoPrimaryKey = R"json(
+    {
+      "primaryKeyId":1,
+      "key":[{
+        "keyData":{
+          "typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey",
+          "value":"GhD+9l0RANZjzZEZ8PDp7LRW",
+          "keyMaterialType":"SYMMETRIC"},
+        "status":"ENABLED",
+        "keyId":2,
+        "outputPrefixType":"TINK"
+      }]
+    })json";
+
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader =
+      [[TINKJSONKeysetReader alloc] initWithStringView:kKeysetWithNoPrimaryKey error:&error];
+  XCTAssertNotNil(reader);
+  XCTAssertNil(error, @"Initialization of TINKJSONKeysetReader failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNotNil(handle);
+  XCTAssertNil(error, @"Initialization of TINKKeysetHandle failed with %@", error);
+
+  (void)[TINKAeadFactory primitiveWithKeysetHandle:handle error:&error];
+  XCTAssertNotNil(error);
+}
+
+// If the keyset is broken in that the key serialization is invalid, currently we fail when creating
+// the aead.
+- (void)testCreateKeysetHandle_brokenKeysetInvalidKey_fails {
+  // gA== in base64 decodes to 0x80, a simple invalid serialized proto.
+  constexpr absl::string_view kKeysetWithInvalidKey = R"json(
+  {
+    "primaryKeyId":1931667682,
+    "key":[{
+      "keyData":{
+        "typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value":"gA==",
+        "keyMaterialType":"SYMMETRIC"},
+      "status":"ENABLED",
+      "keyId":1931667682,
+      "outputPrefixType":"TINK"
+    }]
+  })json";
+
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader =
+      [[TINKJSONKeysetReader alloc] initWithStringView:kKeysetWithInvalidKey error:&error];
+  XCTAssertNotNil(reader);
+  XCTAssertNil(error, @"Initialization of TINKJSONKeysetReader failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNotNil(handle);
+  XCTAssertNil(error, @"Initialization of TINKKeysetHandle failed with %@", error);
+
+  (void)[TINKAeadFactory primitiveWithKeysetHandle:handle error:&error];
+  XCTAssertNotNil(error);
+}
+
+// Test that keysets with two keys work (by decrypting a non-primary ciphertext).
+// Created from the above keyset with tinkey add-key --key_template=AES_128_GCM
+- (void)testMultiKeyKeyset {
+  constexpr absl::string_view kKeysetWithTwoKeys = R"json(
+  {
+    "primaryKeyId": 1617278036,
+    "key": [
+      {
+        "keyData": {
+          "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          "value": "GhD+9l0RANZjzZEZ8PDp7LRW",
+          "keyMaterialType": "SYMMETRIC"
+        },
+        "status": "ENABLED",
+        "keyId": 1931667682,
+        "outputPrefixType": "TINK"
+      },
+      {
+        "keyData": {
+          "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          "value": "GhDqOTk9s1gXwupF4k9R9Cps",
+          "keyMaterialType": "SYMMETRIC"
+        },
+        "status": "ENABLED",
+        "keyId": 1617278036,
+        "outputPrefixType": "RAW"
+      }
+    ]
+  })json";
+
+  // Get the AEAD from the kKeysetWithTwoKeys
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader = [[TINKJSONKeysetReader alloc] initWithStringView:kKeysetWithTwoKeys
+                                                                            error:&error];
+  XCTAssertNotNil(reader);
+  XCTAssertNil(error, @"Initialization of TINKJSONKeysetReader failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNotNil(handle);
+  XCTAssertNil(error, @"Initialization of TINKKeysetHandle failed with %@", error);
+
+  id<TINKAead> twoKeysAead =
+      [TINKAeadFactory primitiveWithKeysetHandle:handle error:&error];
+  XCTAssertNil(error);
+
+  // Get the AEAD from the kSingleKeyAesGcmKeyset
+  reader = [[TINKJSONKeysetReader alloc] initWithStringView:kSingleKeyAesGcmKeyset error:&error];
+
+  handle = [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+
+  id<TINKAead> singleKeyAead = [TINKAeadFactory primitiveWithKeysetHandle:handle error:&error];
+
+  // Encrypt with singleKeyAead, then decrypt with twoKeysAead
+  NSData *empty = [[NSData alloc] init];
+
+  NSData *ciphertext = [singleKeyAead encrypt:empty withAdditionalData:empty error:&error];
+  XCTAssertNil(error, @"Encrypt failed with %@", error);
+
+  NSData *decryption = [twoKeysAead decrypt:ciphertext withAdditionalData:empty error:&error];
+  XCTAssertNil(error, @"Encrypt failed with %@", error);
+  XCTAssertEqualObjects(decryption, empty);
+}
 
 @end
diff --git a/objc/Tests/UnitTests/core/TINKKeysetHandleTest.mm b/objc/Tests/UnitTests/core/TINKKeysetHandleTest.mm
index edcd7ae..80d5a6c 100644
--- a/objc/Tests/UnitTests/core/TINKKeysetHandleTest.mm
+++ b/objc/Tests/UnitTests/core/TINKKeysetHandleTest.mm
@@ -16,31 +16,42 @@
  **************************************************************************
  */
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
 
 #import <Security/Security.h>
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKAead.h"
-#import "objc/TINKAeadKeyTemplate.h"
-#import "objc/TINKAllConfig.h"
-#import "objc/TINKBinaryKeysetReader.h"
-#import "objc/TINKConfig.h"
-#import "objc/TINKHybridKeyTemplate.h"
-#import "objc/TINKSignatureKeyTemplate.h"
-#import "objc/aead/TINKAeadInternal.h"
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKAead.h"
+#import "TINKAeadKeyTemplate.h"
+#import "TINKAllConfig.h"
+#import "TINKBinaryKeysetReader.h"
+#import "TINKConfig.h"
+#import "TINKHybridKeyTemplate.h"
+#import "TINKSignatureKeyTemplate.h"
+#import "aead/TINKAeadInternal.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
 #include "tink/binary_keyset_reader.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/proto_keyset_format.h"
+#include "tink/util/secret_data.h"
 #include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
 #include "tink/util/test_util.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
+using ::crypto::tink::InsecureSecretKeyAccess;
+using ::crypto::tink::SerializeKeysetToProtoKeysetFormat;
 using ::crypto::tink::test::AddRawKey;
 using ::crypto::tink::test::AddTinkKey;
+using ::crypto::tink::util::SecretData;
+using ::crypto::tink::util::SecretDataAsStringView;
+using ::crypto::tink::util::StatusOr;
 using ::google::crypto::tink::EncryptedKeyset;
 using ::google::crypto::tink::KeyData;
 using ::google::crypto::tink::Keyset;
@@ -69,10 +80,10 @@
   gKeyset = new Keyset();
   google::crypto::tink::Keyset::Key ccKey;
 
-  crypto::tink::test::AddTinkKey("some key type", 42, ccKey,
+  crypto::tink::test::AddTinkKey("some_key_type", 42, ccKey,
                                  google::crypto::tink::KeyStatusType::ENABLED,
                                  google::crypto::tink::KeyData::SYMMETRIC, gKeyset);
-  crypto::tink::test::AddRawKey("some other key type", 711, ccKey,
+  crypto::tink::test::AddRawKey("some_other_key_type", 711, ccKey,
                                 google::crypto::tink::KeyStatusType::ENABLED,
                                 google::crypto::tink::KeyData::SYMMETRIC, gKeyset);
   gKeyset->set_primary_key_id(42);
@@ -112,13 +123,13 @@
     // Store the keyset.
     [attributes setObject:kGoodKeysetName forKey:(__bridge id)kSecAttrAccount];
     [attributes setObject:gGoodSerializedKeyset forKey:(__bridge id)kSecValueData];
-    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
+    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nullptr);
     XCTAssertTrue(status == errSecSuccess || status == errSecDuplicateItem);
 
     // Store the bad keyset.
     [attributes setObject:kBadKeysetName forKey:(__bridge id)kSecAttrAccount];
     [attributes setObject:gBadSerializedKeyset forKey:(__bridge id)kSecValueData];
-    status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
+    status = SecItemAdd((__bridge CFDictionaryRef)attributes, nullptr);
     XCTAssertTrue(status == errSecSuccess || status == errSecDuplicateItem);
   });
 }
@@ -147,11 +158,11 @@
   TINKKeysetHandle *handle =
       [[TINKKeysetHandle alloc] initWithKeysetReader:reader andKey:aead error:nil];
   XCTAssertNotNil(handle);
-  std::string output;
-  crypto::tink::TestKeysetHandle::GetKeyset(*handle.ccKeysetHandle).SerializeToString(&output);
-
-  XCTAssertTrue([serializedKeysetData isEqualToData:[NSData dataWithBytes:output.data()
-                                                                   length:output.size()]]);
+  StatusOr<SecretData> serialized =
+      SerializeKeysetToProtoKeysetFormat(*handle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serialized.ok());
+  XCTAssertEqualObjects(serializedKeysetData,
+                        TINKStringViewToNSData(SecretDataAsStringView(*serialized)));
 }
 
 - (void)testWrongAead_Binary {
@@ -279,12 +290,12 @@
   XCTAssertNil(error);
 
   // Verify the contents of the keyset.
-  auto ccKeyset = crypto::tink::TestKeysetHandle::GetKeyset(*handle.ccKeysetHandle);
-  std::string serializedCCKeyset;
-  XCTAssertTrue(ccKeyset.SerializeToString(&serializedCCKeyset));
-  XCTAssertTrue(
-      [gGoodSerializedKeyset isEqualToData:[NSData dataWithBytes:serializedCCKeyset.data()
-                                                          length:serializedCCKeyset.length()]]);
+  StatusOr<SecretData> serialized =
+      SerializeKeysetToProtoKeysetFormat(*handle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serialized.ok());
+
+  XCTAssertEqualObjects(gGoodSerializedKeyset,
+                        TINKStringViewToNSData(SecretDataAsStringView(*serialized)));
 }
 
 - (void)testBadKeysetFromKeychain {
@@ -345,15 +356,16 @@
   XCTAssertNil(error);
 
   // Compare the two keysets, verify that they are identical.
-  auto keyset1 = crypto::tink::TestKeysetHandle::GetKeyset(*handle1.ccKeysetHandle);
-  std::string serializedKeyset1;
-  XCTAssertTrue(keyset1.SerializeToString(&serializedKeyset1));
+  StatusOr<SecretData> serializedKeyset1 =
+      SerializeKeysetToProtoKeysetFormat(*handle1.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serializedKeyset1.ok());
 
-  auto keyset2 = crypto::tink::TestKeysetHandle::GetKeyset(*handle2.ccKeysetHandle);
-  std::string serializedKeyset2;
-  XCTAssertTrue(keyset2.SerializeToString(&serializedKeyset2));
+  StatusOr<SecretData> serializedKeyset2 =
+      SerializeKeysetToProtoKeysetFormat(*handle2.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serializedKeyset2.ok());
 
-  XCTAssertTrue(serializedKeyset1 == serializedKeyset2);
+  XCTAssertEqualObjects(TINKStringViewToNSData(SecretDataAsStringView(*serializedKeyset1)),
+                        TINKStringViewToNSData(SecretDataAsStringView(*serializedKeyset2)));
 }
 
 - (void)testDeleteKeysetFromKeychain {
@@ -399,15 +411,28 @@
   XCTAssertNotNil(publicHandle);
   XCTAssertNil(error);
 
-  auto keyset = crypto::tink::TestKeysetHandle::GetKeyset(*handle.ccKeysetHandle);
-  auto public_keyset = crypto::tink::TestKeysetHandle::GetKeyset(*publicHandle.ccKeysetHandle);
-  XCTAssertEqual(keyset.primary_key_id(), public_keyset.primary_key_id());
-  XCTAssertEqual(keyset.key_size(), public_keyset.key_size());
-  XCTAssertEqual(keyset.key(0).status(), public_keyset.key(0).status());
-  XCTAssertEqual(keyset.key(0).key_id(), public_keyset.key(0).key_id());
-  XCTAssertEqual(keyset.key(0).output_prefix_type(), public_keyset.key(0).output_prefix_type());
+  StatusOr<SecretData> serializedKeyset =
+      SerializeKeysetToProtoKeysetFormat(*handle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serializedKeyset.ok());
+
+  StatusOr<SecretData> serializedPublicKeyset = SerializeKeysetToProtoKeysetFormat(
+      *publicHandle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(serializedKeyset.ok());
+
+  Keyset keyset;
+  XCTAssertTrue(
+      keyset.ParseFromString(std::string(SecretDataAsStringView(*serializedKeyset))));
+  Keyset publicKeyset;
+  XCTAssertTrue(
+      publicKeyset.ParseFromString(std::string(SecretDataAsStringView(*serializedPublicKeyset))));
+
+  XCTAssertEqual(keyset.primary_key_id(), publicKeyset.primary_key_id());
+  XCTAssertEqual(keyset.key_size(), publicKeyset.key_size());
+  XCTAssertEqual(keyset.key(0).status(), publicKeyset.key(0).status());
+  XCTAssertEqual(keyset.key(0).key_id(), publicKeyset.key(0).key_id());
+  XCTAssertEqual(keyset.key(0).output_prefix_type(), publicKeyset.key(0).output_prefix_type());
   XCTAssertEqual(google::crypto::tink::KeyData::ASYMMETRIC_PUBLIC,
-                 public_keyset.key(0).key_data().key_material_type());
+                 publicKeyset.key(0).key_data().key_material_type());
 }
 
 - (void)testPublicKeysetHandleWithHandleFailedNotAsymmetric {
@@ -431,11 +456,11 @@
 }
 
 - (void)testReadNoSecret {
-  auto keyset = absl::make_unique<Keyset>();
+  auto keyset = std::make_unique<Keyset>();
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
              keyset.get());
-  AddRawKey("some other key type", 711, key, KeyStatusType::ENABLED, KeyData::REMOTE, keyset.get());
+  AddRawKey("some_other_key_type", 711, key, KeyStatusType::ENABLED, KeyData::REMOTE, keyset.get());
   keyset->set_primary_key_id(42);
   NSData *serializedKeyset = TINKStringToNSData(keyset->SerializeAsString());
   NSError *error = nil;
@@ -444,15 +469,18 @@
 
   XCTAssertNil(error);
   XCTAssertNotNil(handle);
-  XCTAssertTrue(
-      crypto::tink::TestKeysetHandle::GetKeyset(*handle.ccKeysetHandle).SerializeAsString() ==
-      keyset->SerializeAsString());
+  StatusOr<SecretData> ccSerializedKeysetSecretData =
+      SerializeKeysetToProtoKeysetFormat(*handle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(ccSerializedKeysetSecretData.ok());
+  NSData *ccSerializedKeyset =
+      TINKStringViewToNSData(SecretDataAsStringView(*ccSerializedKeysetSecretData));
+  XCTAssertEqualObjects(ccSerializedKeyset, serializedKeyset);
 }
 
 - (void)testReadNoSecretFailForTypeUnknown {
-  auto keyset = absl::make_unique<Keyset>();
+  auto keyset = std::make_unique<Keyset>();
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED, KeyData::UNKNOWN_KEYMATERIAL,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED, KeyData::UNKNOWN_KEYMATERIAL,
              keyset.get());
   keyset->set_primary_key_id(42);
   NSData *serializedKeyset = TINKStringToNSData(keyset->SerializeAsString());
@@ -467,9 +495,9 @@
 }
 
 - (void)testReadNoSecretFailForTypeSymmetric {
-  auto keyset = absl::make_unique<Keyset>();
+  auto keyset = std::make_unique<Keyset>();
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED, KeyData::SYMMETRIC, keyset.get());
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED, KeyData::SYMMETRIC, keyset.get());
   keyset->set_primary_key_id(42);
   NSData *serializedKeyset = TINKStringToNSData(keyset->SerializeAsString());
   NSError *error = nil;
@@ -483,9 +511,9 @@
 }
 
 - (void)testReadNoSecretFailForTypeAssymmetricPrivate {
-  auto keyset = absl::make_unique<Keyset>();
+  auto keyset = std::make_unique<Keyset>();
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PRIVATE,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PRIVATE,
              keyset.get());
   keyset->set_primary_key_id(42);
   NSData *serializedKeyset = TINKStringToNSData(keyset->SerializeAsString());
@@ -500,15 +528,15 @@
 }
 
 - (void)testReadNoSecretFailForHidden {
-  auto keyset = absl::make_unique<Keyset>();
+  auto keyset = std::make_unique<Keyset>();
   Keyset::Key key;
-  AddTinkKey("some key type", 42, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
+  AddTinkKey("some_key_type", 42, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
              keyset.get());
   for (int i = 0; i < 10; ++i) {
     AddTinkKey(absl::StrCat("more key type", i), i, key, KeyStatusType::ENABLED,
                KeyData::ASYMMETRIC_PUBLIC, keyset.get());
   }
-  AddRawKey("some other key type", 10, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PRIVATE,
+  AddRawKey("some_other_key_type", 10, key, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PRIVATE,
             keyset.get());
   for (int i = 0; i < 10; ++i) {
     AddRawKey(absl::StrCat("more key type", i + 100), i + 100, key, KeyStatusType::ENABLED,
@@ -547,8 +575,11 @@
   XCTAssertNotNil(serializedKeysetNoSecret);
   XCTAssertNil(error);
 
-  auto testKeysetHandle = crypto::tink::TestKeysetHandle::GetKeyset(*publicHandle.ccKeysetHandle);
-  NSData *testSerializedKeyset = TINKStringToNSData(testKeysetHandle.SerializeAsString());
+  StatusOr<SecretData> testCCSerializedKeyset = SerializeKeysetToProtoKeysetFormat(
+      *publicHandle.ccKeysetHandle, InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(testCCSerializedKeyset.ok());
+  NSData *testSerializedKeyset =
+      TINKStringViewToNSData(SecretDataAsStringView(*testCCSerializedKeyset));
   XCTAssertEqualObjects(serializedKeysetNoSecret, testSerializedKeyset);
 }
 
diff --git a/objc/Tests/UnitTests/core/TINKVersionTest.m b/objc/Tests/UnitTests/core/TINKVersionTest.m
index 15ae7b7..1d30701 100644
--- a/objc/Tests/UnitTests/core/TINKVersionTest.m
+++ b/objc/Tests/UnitTests/core/TINKVersionTest.m
@@ -16,7 +16,7 @@
  **************************************************************************
  */
 
-#import "objc/TINKVersion.h"
+#import "TINKVersion.h"
 
 #import <XCTest/XCTest.h>
 
diff --git a/objc/Tests/UnitTests/daead/TINKDeterministicAeadFactoryTest.mm b/objc/Tests/UnitTests/daead/TINKDeterministicAeadFactoryTest.mm
index aa9d5a3..23ee790 100644
--- a/objc/Tests/UnitTests/daead/TINKDeterministicAeadFactoryTest.mm
+++ b/objc/Tests/UnitTests/daead/TINKDeterministicAeadFactoryTest.mm
@@ -16,39 +16,46 @@
  **************************************************************************
  */
 
-#import "objc/TINKDeterministicAeadFactory.h"
+#import "TINKDeterministicAeadFactory.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKDeterministicAead.h"
-#import "objc/TINKDeterministicAeadConfig.h"
-#import "objc/TINKDeterministicAeadFactory.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKDeterministicAead.h"
+#import "TINKDeterministicAeadConfig.h"
+#import "TINKDeterministicAeadFactory.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
 #include "tink/crypto_format.h"
 #include "tink/daead/aes_siv_key_manager.h"
 #include "tink/daead/deterministic_aead_config.h"
 #include "tink/deterministic_aead.h"
+#include "tink/insecure_secret_key_access.h"
 #include "tink/keyset_handle.h"
-#include "tink/util/test_keyset_handle.h"
+#include "tink/proto_keyset_format.h"
 #include "tink/util/status.h"
 #include "tink/util/test_util.h"
-#include "proto/aes_siv.pb.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
+#include "third_party/tink/objc/proto_redirect/aes_siv_cc_pb_redirect.h"
 
-using crypto::tink::AesSivKeyManager;
-using crypto::tink::KeyFactory;
-using crypto::tink::TestKeysetHandle;
-using crypto::tink::test::AddRawKey;
-using crypto::tink::test::AddTinkKey;
-using google::crypto::tink::AesSivKey;
-using google::crypto::tink::AesSivKeyFormat;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::Keyset;
-using google::crypto::tink::KeyStatusType;
+using ::crypto::tink::AesSivKeyManager;
+using ::crypto::tink::InsecureSecretKeyAccess;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::ParseKeysetFromProtoKeysetFormat;
+using ::crypto::tink::test::AddRawKey;
+using ::crypto::tink::test::AddTinkKey;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::AesSivKey;
+using ::google::crypto::tink::AesSivKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
 
 @interface TINKDeterministicAeadFactoryTest : XCTestCase
 @end
@@ -63,8 +70,11 @@
   XCTAssertNil(error);
 
   Keyset keyset;
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
+  StatusOr<KeysetHandle> ccKeysetHandle =
+      ParseKeysetFromProtoKeysetFormat(keyset.SerializeAsString(), InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(ccKeysetHandle.ok());
+  TINKKeysetHandle *handle = [[TINKKeysetHandle alloc]
+      initWithCCKeysetHandle:std::make_unique<KeysetHandle>(*ccKeysetHandle)];
   XCTAssertNotNil(handle);
 
   id<TINKDeterministicAead> aead = [TINKDeterministicAeadFactory primitiveWithKeysetHandle:handle
@@ -103,8 +113,11 @@
   XCTAssertNotNil(aeadConfig);
   XCTAssertNil(error);
 
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
+  StatusOr<KeysetHandle> ccKeysetHandle =
+      ParseKeysetFromProtoKeysetFormat(keyset.SerializeAsString(), InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(ccKeysetHandle.ok());
+  TINKKeysetHandle *handle = [[TINKKeysetHandle alloc]
+      initWithCCKeysetHandle:std::make_unique<KeysetHandle>(*ccKeysetHandle)];
   XCTAssertNotNil(handle);
 
   id<TINKDeterministicAead> aead = [TINKDeterministicAeadFactory primitiveWithKeysetHandle:handle
diff --git a/objc/Tests/UnitTests/daead/TINKDeterministicAeadKeyTemplateTest.mm b/objc/Tests/UnitTests/daead/TINKDeterministicAeadKeyTemplateTest.mm
index 098252b..4744c52 100644
--- a/objc/Tests/UnitTests/daead/TINKDeterministicAeadKeyTemplateTest.mm
+++ b/objc/Tests/UnitTests/daead/TINKDeterministicAeadKeyTemplateTest.mm
@@ -16,12 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKDeterministicAeadKeyTemplate.h"
+#import "TINKDeterministicAeadKeyTemplate.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
 #include "proto/common.pb.h"
 #include "proto/tink.pb.h"
 
diff --git a/objc/Tests/UnitTests/hybrid/TINKHybridDecryptFactoryTest.mm b/objc/Tests/UnitTests/hybrid/TINKHybridDecryptFactoryTest.mm
deleted file mode 100644
index bd3279c..0000000
--- a/objc/Tests/UnitTests/hybrid/TINKHybridDecryptFactoryTest.mm
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- **************************************************************************
- */
-
-#import <XCTest/XCTest.h>
-
-#include "absl/status/status.h"
-#include "tink/crypto_format.h"
-#include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
-#include "tink/util/test_util.h"
-#include "proto/ecies_aead_hkdf.pb.h"
-#include "proto/tink.pb.h"
-
-#import "objc/TINKConfig.h"
-#import "objc/TINKHybridConfig.h"
-#import "objc/TINKHybridDecrypt.h"
-#import "objc/TINKHybridDecryptFactory.h"
-#import "objc/TINKHybridEncrypt.h"
-#import "objc/TINKHybridEncryptFactory.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKStrings.h"
-
-using ::crypto::tink::TestKeysetHandle;
-using ::crypto::tink::test::AddTinkKey;
-using ::crypto::tink::test::AddRawKey;
-using ::crypto::tink::test::AddLegacyKey;
-using ::google::crypto::tink::EciesAeadHkdfPrivateKey;
-using ::google::crypto::tink::EcPointFormat;
-using ::google::crypto::tink::EllipticCurveType;
-using ::google::crypto::tink::HashType;
-using ::google::crypto::tink::KeyData;
-using ::google::crypto::tink::Keyset;
-using ::google::crypto::tink::KeyStatusType;
-
-@interface TINKHybridDecryptFactoryTest : XCTestCase
-@end
-
-static EciesAeadHkdfPrivateKey getNewEciesPrivateKey() {
-  return crypto::tink::test::GetEciesAesGcmHkdfTestKey(
-      EllipticCurveType::NIST_P256, EcPointFormat::UNCOMPRESSED, HashType::SHA256, 32);
-}
-
-@implementation TINKHybridDecryptFactoryTest
-
-- (void)testEncryptWith:(Keyset *)publicKeyset andDecryptWith:(Keyset *)privateKeyset {
-  TINKKeysetHandle *privateKeysetHandle = [[TINKKeysetHandle alloc]
-      initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(*privateKeyset)];
-
-  TINKKeysetHandle *publicKeysetHandle = [[TINKKeysetHandle alloc]
-      initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(*publicKeyset)];
-
-  // Get a HybridDecrypt primitive.
-  NSError *error = nil;
-  id<TINKHybridDecrypt> hybridDecrypt =
-      [TINKHybridDecryptFactory primitiveWithKeysetHandle:privateKeysetHandle error:&error];
-  XCTAssertNotNil(hybridDecrypt);
-  XCTAssertNil(error);
-
-  // Get a HybridEncrypt primitive.
-  error = nil;
-  id<TINKHybridEncrypt> primitive =
-      [TINKHybridEncryptFactory primitiveWithKeysetHandle:publicKeysetHandle error:&error];
-  XCTAssertNotNil(primitive);
-  XCTAssertNil(error);
-
-  NSData *const plaintext = [@"some plaintext" dataUsingEncoding:NSUTF8StringEncoding];
-  NSData *const context = [@"some context info" dataUsingEncoding:NSUTF8StringEncoding];
-
-  // Encrypt.
-  error = nil;
-  NSData *ciphertext = [primitive encrypt:plaintext withContextInfo:context error:&error];
-  XCTAssertNil(error);
-  XCTAssertNotNil(ciphertext);
-
-  // Decrypt.
-  error = nil;
-  NSData *result = [hybridDecrypt decrypt:ciphertext withContextInfo:context error:&error];
-  XCTAssertNil(error);
-  XCTAssertNotNil(result);
-  XCTAssertTrue([result isEqualToData:plaintext]);
-}
-
-- (void)testPrimitiveWithEmptyKeyset {
-  NSError *error = nil;
-  TINKHybridConfig *hybridConfig = [[TINKHybridConfig alloc] initWithError:&error];
-  XCTAssertNotNil(hybridConfig);
-  XCTAssertNil(error);
-
-  google::crypto::tink::Keyset keyset;
-  TINKKeysetHandle *keysetHandle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
-  XCTAssertNotNil(keysetHandle);
-
-  id<TINKHybridDecrypt> primitive =
-      [TINKHybridDecryptFactory primitiveWithKeysetHandle:keysetHandle error:&error];
-
-  XCTAssertNil(primitive);
-  XCTAssertNotNil(error);
-  XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-  NSDictionary *userInfo = [error userInfo];
-  NSString *errorString = [userInfo objectForKey:NSLocalizedFailureReasonErrorKey];
-  XCTAssertTrue([errorString containsString:@"at least one key"]);
-}
-
-- (void)testPrimitiveWithKeyset {
-  NSError *error = nil;
-  TINKHybridConfig *hybridConfig = [[TINKHybridConfig alloc] initWithError:&error];
-  XCTAssertNotNil(hybridConfig);
-  XCTAssertNil(error);
-
-  XCTAssertTrue([TINKConfig registerConfig:hybridConfig error:&error]);
-  XCTAssertNil(error);
-
-  uint32_t keyId1 = 1;
-  uint32_t keyId2 = 2;
-  uint32_t keyId3 = 3;
-  EciesAeadHkdfPrivateKey eciesKey1 = getNewEciesPrivateKey();
-  EciesAeadHkdfPrivateKey eciesKey2 = getNewEciesPrivateKey();
-  EciesAeadHkdfPrivateKey eciesKey3 = getNewEciesPrivateKey();
-
-  std::string privateKeyType = "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey";
-  Keyset privateKeyset;
-  AddTinkKey(privateKeyType, keyId1, eciesKey1, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PRIVATE,
-             &privateKeyset);
-  AddRawKey(privateKeyType, keyId2, eciesKey2, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PRIVATE,
-            &privateKeyset);
-  AddLegacyKey(privateKeyType, keyId3, eciesKey3, KeyStatusType::ENABLED,
-               KeyData::ASYMMETRIC_PRIVATE, &privateKeyset);
-
-  std::string publicKeyType = "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey";
-  Keyset publicKeyset;
-  AddTinkKey(publicKeyType, keyId1, eciesKey1.public_key(), KeyStatusType::ENABLED,
-             KeyData::ASYMMETRIC_PUBLIC, &publicKeyset);
-  AddRawKey(publicKeyType, keyId2, eciesKey2.public_key(), KeyStatusType::ENABLED,
-            KeyData::ASYMMETRIC_PUBLIC, &publicKeyset);
-  AddLegacyKey(publicKeyType, keyId3, eciesKey3.public_key(), KeyStatusType::ENABLED,
-               KeyData::ASYMMETRIC_PUBLIC, &publicKeyset);
-
-  privateKeyset.set_primary_key_id(keyId1);
-  publicKeyset.set_primary_key_id(keyId3);
-  [self testEncryptWith:&publicKeyset andDecryptWith:&privateKeyset];
-
-  privateKeyset.set_primary_key_id(keyId2);
-  publicKeyset.set_primary_key_id(keyId3);
-  [self testEncryptWith:&publicKeyset andDecryptWith:&privateKeyset];
-
-  privateKeyset.set_primary_key_id(keyId3);
-  publicKeyset.set_primary_key_id(keyId1);
-  [self testEncryptWith:&publicKeyset andDecryptWith:&privateKeyset];
-}
-
-@end
diff --git a/objc/Tests/UnitTests/hybrid/TINKHybridEncryptFactoryTest.mm b/objc/Tests/UnitTests/hybrid/TINKHybridEncryptFactoryTest.mm
deleted file mode 100644
index e9a4283..0000000
--- a/objc/Tests/UnitTests/hybrid/TINKHybridEncryptFactoryTest.mm
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- **************************************************************************
- */
-
-#import <XCTest/XCTest.h>
-
-#include "absl/status/status.h"
-#include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
-#include "tink/util/test_util.h"
-#include "proto/ecies_aead_hkdf.pb.h"
-#include "proto/tink.pb.h"
-
-#import "objc/TINKConfig.h"
-#import "objc/TINKHybridConfig.h"
-#import "objc/TINKHybridEncrypt.h"
-#import "objc/TINKHybridEncryptFactory.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKStrings.h"
-
-using ::crypto::tink::TestKeysetHandle;
-using ::crypto::tink::test::AddTinkKey;
-using ::crypto::tink::test::AddRawKey;
-using ::crypto::tink::test::AddLegacyKey;
-using ::google::crypto::tink::EciesAeadHkdfPublicKey;
-using ::google::crypto::tink::EcPointFormat;
-using ::google::crypto::tink::EllipticCurveType;
-using ::google::crypto::tink::HashType;
-using ::google::crypto::tink::KeyData;
-using ::google::crypto::tink::Keyset;
-using ::google::crypto::tink::KeyStatusType;
-
-@interface TINKHybridEncryptFactoryTest : XCTestCase
-@end
-
-static EciesAeadHkdfPublicKey getNewEciesPublicKey() {
-  return crypto::tink::test::GetEciesAesGcmHkdfTestKey(
-      EllipticCurveType::NIST_P256, EcPointFormat::UNCOMPRESSED, HashType::SHA256, 32).public_key();
-}
-
-@implementation TINKHybridEncryptFactoryTest
-
-- (void)testPrimitiveWithEmptyKeyset {
-  google::crypto::tink::Keyset keyset;
-  TINKKeysetHandle *keysetHandle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
-
-  NSError *error = nil;
-  id<TINKHybridEncrypt> primitive =
-      [TINKHybridEncryptFactory primitiveWithKeysetHandle:keysetHandle error:&error];
-
-  XCTAssertNil(primitive);
-  XCTAssertNotNil(error);
-  XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-  NSDictionary *userInfo = [error userInfo];
-  NSString *errorString = [userInfo objectForKey:NSLocalizedFailureReasonErrorKey];
-  XCTAssertTrue([errorString containsString:@"at least one key"]);
-}
-
-- (void)testPrimitiveWithKeyset {
-  // Prepare a Keyset.
-  Keyset publicKeyset;
-  std::string publicKeyType = "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey";
-
-  uint32_t keyId1 = 1234543;
-  uint32_t keyId2 = 726329;
-  uint32_t keyId3 = 7213743;
-  EciesAeadHkdfPublicKey eciesKey1 = getNewEciesPublicKey();
-  EciesAeadHkdfPublicKey eciesKey2 = getNewEciesPublicKey();
-  EciesAeadHkdfPublicKey eciesKey3 = getNewEciesPublicKey();
-
-  AddTinkKey(publicKeyType, keyId1, eciesKey1, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
-             &publicKeyset);
-  AddRawKey(publicKeyType, keyId2, eciesKey2, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
-            &publicKeyset);
-  AddLegacyKey(publicKeyType, keyId3, eciesKey3, KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
-               &publicKeyset);
-
-  publicKeyset.set_primary_key_id(keyId3);
-  // Create a KeysetHandle and use it with the factory.
-  TINKKeysetHandle *keysetHandle = [[TINKKeysetHandle alloc]
-      initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(publicKeyset)];
-  XCTAssertNotNil(keysetHandle);
-
-  // Get a HybridEncrypt primitive.
-  NSError *error = nil;
-  id<TINKHybridEncrypt> primitive =
-      [TINKHybridEncryptFactory primitiveWithKeysetHandle:keysetHandle error:&error];
-  XCTAssertNil(error);
-  XCTAssertNotNil(primitive);
-
-  // Test the resulting HybridEncrypt-instance.
-  NSData *plaintext = [@"some plaintext" dataUsingEncoding:NSUTF8StringEncoding];
-  NSData *context = [@"some context info" dataUsingEncoding:NSUTF8StringEncoding];
-
-  error = nil;
-  NSData *result = [primitive encrypt:plaintext withContextInfo:context error:&error];
-  XCTAssertNil(error);
-  XCTAssertNotNil(result);
-}
-
-@end
diff --git a/objc/Tests/UnitTests/hybrid/TINKHybridFactoryTest.mm b/objc/Tests/UnitTests/hybrid/TINKHybridFactoryTest.mm
new file mode 100644
index 0000000..fc95e19
--- /dev/null
+++ b/objc/Tests/UnitTests/hybrid/TINKHybridFactoryTest.mm
@@ -0,0 +1,217 @@
+/**
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ **************************************************************************
+ */
+
+#import <XCTest/XCTest.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/strings/escaping.h"
+
+#import "TINKAllConfig.h"
+#import "TINKHybridDecrypt.h"
+#import "TINKHybridDecryptFactory.h"
+#import "TINKHybridEncrypt.h"
+#import "TINKHybridEncryptFactory.h"
+#import "TINKJSONKeysetReader.h"
+#import "TINKKeysetHandle+Cleartext.h"
+#import "TINKKeysetHandle.h"
+
+// Generated with Tinkey:
+// tinkey create-keyset --key-template ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM --out-format json |\
+//   tinkey add-key --key-template ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM --out-format json |\
+//   tinkey add-key --key-template ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM --out-format json
+// (Plus automatic formatting)
+constexpr absl::string_view kMultiKeyEciesKeyset = R"json(
+{
+  "primaryKeyId": 138849321,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey",
+        "value": "EosBEkQKBAgCEAMSOhI4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAEYARohALH+4dcOwY2vRpBvXiOMzFsoL66yAhHB0TBnjgXC2cWQIiB1nh7uDEjA7KSPYrcRxE2uCOPX1MzQZpbS9FIGLzX2PxohAM9yDmyOqaLoFRgavO0mpFh72Wp/7dolb2vlrnMpRXqS",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 927487497,
+      "outputPrefixType": "TINK"
+    },
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey",
+        "value": "EosBEkQKBAgCEAMSOhI4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAEYARohAJXR8OXxcnr9iW5eY0HfrtvwzctIfs6aVLiYTPAoIRPkIiAgm6A70zykjThWhvnfS0FIQwzGwDnDFiH7Fr+dwrv52hogQcGnSqsab9DiQNUHbO+JyUU6focFdezz/V3YbbVvukw=",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 138849321,
+      "outputPrefixType": "TINK"
+    },
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey",
+        "value": "EooBEkQKBAgCEAMSOhI4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAEYARogRIQXmow3eAQoc9GJGePefa3jSEpml3yMPM6SFDTqz2oiIG3ApN4jHu/r2jarYac815pSvHi8EHz+AoJgSrM4nfXsGiEAiuxp4DsSH/hx4kPD1sAn3uxDuw9itodcyffPqLaELJ8=",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 590961871,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
+)json";
+
+// Obtained with tinkey from the above; plus manual editing to split up the keyset.
+constexpr absl::string_view kKeyThreePublicKeyset = R"json(
+{
+  "primaryKeyId": 590961871,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey",
+        "value": "EkQKBAgCEAMSOhI4CjB0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5jcnlwdG8udGluay5BZXNHY21LZXkSAhAQGAEYARogRIQXmow3eAQoc9GJGePefa3jSEpml3yMPM6SFDTqz2oiIG3ApN4jHu/r2jarYac815pSvHi8EHz+AoJgSrM4nfXs",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 590961871,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
+)json";
+
+
+@interface TINKHybridEncryptDecryptFactoryTest : XCTestCase
+@end
+
+@implementation TINKHybridEncryptDecryptFactoryTest
+
++ (void)setUp {
+  NSError *error = nil;
+  TINKAllConfig *allConfig = [[TINKAllConfig alloc] initWithError:&error];
+  XCTAssertNotNil(allConfig);
+  XCTAssertNil(error);
+}
+
+- (void)testCreateHybridDecrypt {
+  NSData *serializedKeysetData = [NSData dataWithBytes:kMultiKeyEciesKeyset.data()
+                                                length:kMultiKeyEciesKeyset.size()];
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader =
+      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:serializedKeysetData error:&error];
+  XCTAssertNil(error, @"TINKJSONKeysetReader creation failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNil(error, @"TINKKeysetHandle creation failed with %@", error);
+
+  id<TINKHybridDecrypt> hybridDecrypt = [TINKHybridDecryptFactory primitiveWithKeysetHandle:handle
+                                                                                      error:&error];
+  XCTAssertNotNil(hybridDecrypt);
+  XCTAssertNil(error, @"HybridDecrypt creation failed with %@", error);
+}
+
+- (void)testCreateHybridEncrypt {
+  NSData *serializedKeysetData = [NSData dataWithBytes:kKeyThreePublicKeyset.data()
+                                                length:kKeyThreePublicKeyset.size()];
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader =
+      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:serializedKeysetData error:&error];
+  XCTAssertNil(error, @"TINKJSONKeysetReader creation failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNil(error, @"TINKKeysetHandle creation failed with %@", error);
+
+  id<TINKHybridEncrypt> hybridEncrypt = [TINKHybridEncryptFactory primitiveWithKeysetHandle:handle
+                                                                                      error:&error];
+  XCTAssertNotNil(hybridEncrypt);
+  XCTAssertNil(error, @"HybridEncrypt creation failed with %@", error);
+}
+
+- (void)testEncryptThenDecrypt {
+  NSData *serializedPrivateKeyData = [NSData dataWithBytes:kMultiKeyEciesKeyset.data()
+                                                    length:kMultiKeyEciesKeyset.size()];
+  NSError *error = nil;
+  TINKJSONKeysetReader *privateReader =
+      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:serializedPrivateKeyData error:&error];
+  TINKKeysetHandle *privateHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:privateReader
+                                                                    error:&error];
+
+  id<TINKHybridDecrypt> hybridDecrypt =
+      [TINKHybridDecryptFactory primitiveWithKeysetHandle:privateHandle error:&error];
+
+  NSData *serializedPublicKeyData = [NSData dataWithBytes:kKeyThreePublicKeyset.data()
+                                                   length:kKeyThreePublicKeyset.size()];
+  TINKJSONKeysetReader *publicReader =
+      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:serializedPublicKeyData error:&error];
+
+  TINKKeysetHandle *publicHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:publicReader
+                                                                    error:&error];
+
+  id<TINKHybridEncrypt> hybridEncrypt =
+      [TINKHybridEncryptFactory primitiveWithKeysetHandle:publicHandle error:&error];
+
+  NSData* empty = [[NSData alloc] init];
+  NSData* ciphertext = [hybridEncrypt encrypt:empty withContextInfo:empty error:&error];
+  XCTAssertNil(error, @"encrypt failed with %@", error);
+
+  NSData* decryption = [hybridDecrypt decrypt:ciphertext withContextInfo:empty error:&error];
+  XCTAssertNil(error, @"encrypt failed with %@", error);
+
+  XCTAssertEqualObjects(decryption, empty);
+}
+
+// We test that changing the context makes decryption fail.
+- (void)testEncryptModifyContext_DecryptFails {
+  NSData *serializedPrivateKeyData = [NSData dataWithBytes:kMultiKeyEciesKeyset.data()
+                                                    length:kMultiKeyEciesKeyset.size()];
+  NSError *error = nil;
+  TINKJSONKeysetReader *privateReader =
+      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:serializedPrivateKeyData error:&error];
+  TINKKeysetHandle *privateHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:privateReader
+                                                                    error:&error];
+
+  id<TINKHybridDecrypt> hybridDecrypt =
+      [TINKHybridDecryptFactory primitiveWithKeysetHandle:privateHandle error:&error];
+
+  NSData *serializedPublicKeyData = [NSData dataWithBytes:kKeyThreePublicKeyset.data()
+                                                   length:kKeyThreePublicKeyset.size()];
+  TINKJSONKeysetReader *publicReader =
+      [[TINKJSONKeysetReader alloc] initWithSerializedKeyset:serializedPublicKeyData error:&error];
+
+  TINKKeysetHandle *publicHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:publicReader
+                                                                    error:&error];
+
+  id<TINKHybridEncrypt> hybridEncrypt =
+      [TINKHybridEncryptFactory primitiveWithKeysetHandle:publicHandle error:&error];
+
+  NSData* empty = [[NSData alloc] init];
+  NSData* ciphertext = [hybridEncrypt encrypt:empty withContextInfo:empty error:&error];
+  XCTAssertNil(error, @"encrypt failed with %@", error);
+
+  NSData* wrongContext = [NSData dataWithBytes:"hi" length:2];
+  (void)[hybridDecrypt decrypt:ciphertext withContextInfo:wrongContext error:&error];
+  XCTAssertNotNil(error);
+}
+
+@end
diff --git a/objc/Tests/UnitTests/hybrid/TINKHybridKeyTemplateTest.mm b/objc/Tests/UnitTests/hybrid/TINKHybridKeyTemplateTest.mm
index 2fe7f58..b20294f 100644
--- a/objc/Tests/UnitTests/hybrid/TINKHybridKeyTemplateTest.mm
+++ b/objc/Tests/UnitTests/hybrid/TINKHybridKeyTemplateTest.mm
@@ -16,13 +16,17 @@
  **************************************************************************
  */
 
-#import "objc/TINKHybridKeyTemplate.h"
+#import "TINKHybridKeyTemplate.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKAeadKeyTemplate.h"
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKAeadKeyTemplate.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
 #include "proto/common.pb.h"
 #include "proto/tink.pb.h"
 
@@ -45,7 +49,7 @@
   XCTAssertNotNil(error);
   XCTAssertNil(keyTemplate);
   XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-  NSDictionary *userInfo = [error userInfo];
+  NSDictionary<NSErrorUserInfoKey, id> *userInfo = [error userInfo];
   NSString *errorString = [userInfo objectForKey:NSLocalizedFailureReasonErrorKey];
   XCTAssertTrue([errorString containsString:@"Invalid TINKHybridKeyTemplate"]);
 }
diff --git a/objc/Tests/UnitTests/mac/TINKMacFactoryTest.mm b/objc/Tests/UnitTests/mac/TINKMacFactoryTest.mm
index 141f35a..b88ee0d 100644
--- a/objc/Tests/UnitTests/mac/TINKMacFactoryTest.mm
+++ b/objc/Tests/UnitTests/mac/TINKMacFactoryTest.mm
@@ -16,40 +16,46 @@
  **************************************************************************
  */
 
-#import "objc/TINKAeadFactory.h"
+#import "TINKAeadFactory.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/TINKMac.h"
-#import "objc/TINKMacConfig.h"
-#import "objc/TINKMacFactory.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKStrings.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKKeysetHandle.h"
+#import "TINKMac.h"
+#import "TINKMacConfig.h"
+#import "TINKMacFactory.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
 #include "tink/crypto_format.h"
+#include "tink/insecure_secret_key_access.h"
 #include "tink/keyset_handle.h"
 #include "tink/mac.h"
 #include "tink/mac/hmac_key_manager.h"
 #include "tink/mac/mac_config.h"
+#include "tink/proto_keyset_format.h"
 #include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
 #include "tink/util/test_util.h"
-#include "proto/hmac.pb.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
+#include "third_party/tink/objc/proto_redirect/hmac_cc_pb_redirect.h"
 
-using crypto::tink::HmacKeyManager;
-using crypto::tink::KeyFactory;
-using crypto::tink::TestKeysetHandle;
-using crypto::tink::test::AddRawKey;
-using crypto::tink::test::AddTinkKey;
-using google::crypto::tink::HashType;
-using google::crypto::tink::HmacKey;
-using google::crypto::tink::HmacKeyFormat;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::Keyset;
-using google::crypto::tink::KeyStatusType;
+using ::crypto::tink::HmacKeyManager;
+using ::crypto::tink::InsecureSecretKeyAccess;
+using ::crypto::tink::KeysetHandle;
+using ::crypto::tink::test::AddRawKey;
+using ::crypto::tink::test::AddTinkKey;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HmacKey;
+using ::google::crypto::tink::HmacKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
 
 @interface TINKMacFactoryTest : XCTestCase
 @end
@@ -58,8 +64,11 @@
 
 - (void)testEmptyKeyset {
   Keyset keyset;
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
+  StatusOr<KeysetHandle> cc_keyset_handle =
+      ParseKeysetFromProtoKeysetFormat(keyset.SerializeAsString(), InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(cc_keyset_handle.ok());
+  TINKKeysetHandle *handle = [[TINKKeysetHandle alloc]
+      initWithCCKeysetHandle:std::make_unique<KeysetHandle>(*cc_keyset_handle)];
   XCTAssertNotNil(handle);
 
   NSError *error = nil;
@@ -101,8 +110,11 @@
   XCTAssertNotNil(macConfig);
   XCTAssertNil(error);
 
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
+  StatusOr<KeysetHandle> cc_keyset_handle =
+      ParseKeysetFromProtoKeysetFormat(keyset.SerializeAsString(), InsecureSecretKeyAccess::Get());
+  XCTAssertTrue(cc_keyset_handle.ok());
+  TINKKeysetHandle *handle = [[TINKKeysetHandle alloc]
+      initWithCCKeysetHandle:std::make_unique<KeysetHandle>(*cc_keyset_handle)];
   XCTAssertNotNil(handle);
 
   id<TINKMac> mac = [TINKMacFactory primitiveWithKeysetHandle:handle error:&error];
@@ -135,7 +147,7 @@
   XCTAssertTrue([error.localizedFailureReason containsString:@"verification failed"]);
 
   const char *dataBytes = (const char *)data.bytes;
-  XCTAssertTrue(dataBytes != NULL);
+  XCTAssertTrue(dataBytes != nullptr);
 
   // Flip all the bits in data one by one.
   for (NSUInteger byteIndex = 0; byteIndex < data.length; byteIndex++) {
@@ -159,7 +171,7 @@
   }
 
   const char *macBytes = (const char *)computedMac.bytes;
-  XCTAssertTrue(macBytes != NULL);
+  XCTAssertTrue(macBytes != nullptr);
 
   // Flip all the bits in the MAC one by one.
   for (NSUInteger byteIndex = 0; byteIndex < computedMac.length; byteIndex++) {
diff --git a/objc/Tests/UnitTests/mac/TINKMacKeyTemplateTest.mm b/objc/Tests/UnitTests/mac/TINKMacKeyTemplateTest.mm
index becdbac..b35509b 100644
--- a/objc/Tests/UnitTests/mac/TINKMacKeyTemplateTest.mm
+++ b/objc/Tests/UnitTests/mac/TINKMacKeyTemplateTest.mm
@@ -16,12 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKMacKeyTemplate.h"
+#import "TINKMacKeyTemplate.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
 #include "proto/common.pb.h"
 #include "proto/tink.pb.h"
 
@@ -44,7 +48,7 @@
   XCTAssertNotNil(error);
   XCTAssertNil(keyTemplate);
   XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-  NSDictionary *userInfo = [error userInfo];
+  NSDictionary<NSErrorUserInfoKey, id> *userInfo = [error userInfo];
   NSString *errorString = [userInfo objectForKey:NSLocalizedFailureReasonErrorKey];
   XCTAssertTrue([errorString containsString:@"Invalid TINKMacKeyTemplate"]);
 }
diff --git a/objc/Tests/UnitTests/signature/TINKPublicKeySignFactory.mm b/objc/Tests/UnitTests/signature/TINKPublicKeySignFactory.mm
deleted file mode 100644
index 026c1af..0000000
--- a/objc/Tests/UnitTests/signature/TINKPublicKeySignFactory.mm
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * Copyright 2018 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- **************************************************************************
- */
-
-#import "objc/TINKPublicKeySignFactory.h"
-
-#import <XCTest/XCTest.h>
-
-#import "objc/TINKKeysetHandle.h"
-#import "objc/TINKPublicKeySign.h"
-#import "objc/TINKPublicKeySignFactory.h"
-#import "objc/TINKSignatureConfig.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/signature/TINKPublicKeySignInternal.h"
-#import "objc/util/TINKStrings.h"
-
-#include "absl/status/status.h"
-#include "tink/crypto_format.h"
-#include "tink/keyset_handle.h"
-#include "tink/signature/ecdsa_sign_key_manager.h"
-#include "tink/signature/signature_config.h"
-#include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
-#include "tink/util/test_util.h"
-#include "proto/ecdsa.pb.h"
-#include "proto/tink.pb.h"
-
-using crypto::tink::EcdsaSignKeyManager;
-using crypto::tink::KeyFactory;
-using crypto::tink::TestKeysetHandle;
-using crypto::tink::test::AddRawKey;
-using crypto::tink::test::AddTinkKey;
-using google::crypto::tink::EcdsaPrivateKey;
-using google::crypto::tink::EllipticCurveType;
-using google::crypto::tink::EcdsaSignatureEncoding;
-using google::crypto::tink::HashType;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::Keyset;
-using google::crypto::tink::KeyStatusType;
-
-static EcdsaPrivateKey GetNewEcdsaPrivateKey() {
-  return crypto::tink::test::GetEcdsaTestPrivateKey(EllipticCurveType::NIST_P256, HashType::SHA256,
-                                                    EcdsaSignatureEncoding::DER);
-}
-
-@interface TINKPublicKeySignFactoryTest : XCTestCase
-@end
-
-@implementation TINKPublicKeySignFactoryTest
-
-- (void)testEmptyKeyset {
-  Keyset keyset;
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
-  XCTAssertNotNil(handle);
-
-  NSError *error = nil;
-  id<TINKPublicKeySign> publicKeySign =
-      [TINKPublicKeySignFactory primitiveWithKeysetHandle:handle error:&error];
-  XCTAssertNil(publicKeySign);
-  XCTAssertNotNil(error);
-  XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-  XCTAssertTrue([error.localizedFailureReason containsString:@"at least one key"]);
-}
-
-- (void)testPrimitive {
-  // Prepare a Keyset.
-  Keyset keyset;
-  std::string key_type = "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
-
-  uint32_t key_id_1 = 1234543;
-  AddTinkKey(key_type, key_id_1, GetNewEcdsaPrivateKey(), KeyStatusType::ENABLED,
-             KeyData::ASYMMETRIC_PUBLIC, &keyset);
-
-  uint32_t key_id_2 = 726329;
-  AddTinkKey(key_type, key_id_2, GetNewEcdsaPrivateKey(), KeyStatusType::ENABLED,
-             KeyData::ASYMMETRIC_PUBLIC, &keyset);
-
-  uint32_t key_id_3 = 7213743;
-  AddTinkKey(key_type, key_id_3, GetNewEcdsaPrivateKey(), KeyStatusType::ENABLED,
-             KeyData::ASYMMETRIC_PUBLIC, &keyset);
-
-  keyset.set_primary_key_id(key_id_3);
-
-  NSError *error = nil;
-  TINKSignatureConfig *signatureConfig = [[TINKSignatureConfig alloc] initWithError:&error];
-  XCTAssertNotNil(signatureConfig);
-  XCTAssertNil(error);
-
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
-  XCTAssertNotNil(handle);
-
-  id<TINKPublicKeySign> publicKeySign =
-      [TINKPublicKeySignFactory primitiveWithKeysetHandle:handle error:&error];
-  XCTAssertNotNil(publicKeySign);
-  XCTAssertNil(error);
-  TINKPublicKeySignInternal *publicKeySignInternal = (TINKPublicKeySignInternal *)publicKeySign;
-  XCTAssertTrue(publicKeySignInternal.ccPublicKeySign != NULL);
-
-  // Test the PublicKeySign primitive.
-  NSData *data = [@"some data to sign" dataUsingEncoding:NSUTF8StringEncoding];
-  NSData *signature = [publicKeySign signatureForData:data error:&error];
-  XCTAssertNil(error);
-  XCTAssertFalse([signature isEqualToData:data]);
-}
-
-@end
diff --git a/objc/Tests/UnitTests/signature/TINKPublicKeySignVerifyFactoryTest.mm b/objc/Tests/UnitTests/signature/TINKPublicKeySignVerifyFactoryTest.mm
new file mode 100644
index 0000000..d429fa6
--- /dev/null
+++ b/objc/Tests/UnitTests/signature/TINKPublicKeySignVerifyFactoryTest.mm
@@ -0,0 +1,221 @@
+/**
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ **************************************************************************
+ */
+
+#import "TINKPublicKeySignFactory.h"
+#import "TINKPublicKeyVerifyFactory.h"
+
+#import <XCTest/XCTest.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/strings/escaping.h"
+
+#import "TINKAllConfig.h"
+#import "TINKJSONKeysetReader.h"
+#import "TINKKeysetHandle+Cleartext.h"
+#import "TINKKeysetHandle.h"
+#import "TINKPublicKeySign.h"
+#import "TINKPublicKeyVerify.h"
+#import "util/TINKStrings.h"
+
+// The serialized keysets were generated with Tinkey:
+// tinkey create-keyset --key-template ECDSA_P521 --out-format json \
+//   | tinkey add-key --key-template ECDSA_P521 --out-format json \
+//   | tinkey add-key --key-template ECDSA_P521 --out-format json
+//
+// After this, we converted it to a public keyset with:
+// tinkey create-public-keyset --out-format json < previous_stdin
+//
+// Then we edited the private keyset to get only the 3rd key in the public keyset.
+// (Plus automatic formatting)
+
+constexpr absl::string_view kEcdsaPublicKeyset = R"json(
+{
+  "primaryKeyId": 1588647101,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+        "value": "EgYIBBAEGAIaQgFLPqQQ0PC3pzkB95PixsKtcP6IBUqenHi3BafyD0wWL16JvYtWK5J1pFrFj0FK0WyY7F67gQWmWbz5gSoyBZX6kyJCAOAPAN14JQILzrIoWD9Rg1sV9AG45Sa0nR1VV570YUDycv73DDUbWSUjrmyumK3fUsk7Z/hLpK4yR+JsTeMRZbSB",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 1588647101,
+      "outputPrefixType": "TINK"
+    },
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+        "value": "EgYIBBAEGAIaQgDOp/Hpv7I/fp3A9IqpcY4jshNWfHNj6roxf62QviXEg19RUdWbd4eK+mHEWDGpp+XvA7X9bASLN4OEv4NhSkqnziJCAKI9ufXd2rQQjXboDfMFAtcgVim3L13TWP9kpGZ47v1SGV/niRZK0+RZxraXFXg/lT99Z3XTJtYvDL1NNG0IGLy8",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 1525489880,
+      "outputPrefixType": "TINK"
+    },
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EcdsaPublicKey",
+        "value": "EgYIBBAEGAIaQgGER6+b/6pXv2JqoxNWprDv8vAtDe4dzW33zODcPLe9W0efcn+FiTF5yi6jAlAc4bmN+hYAAr9bv5HDMafG9zuzGyJCAbRyxpNJAb+7APqT5x6Ad+e3yT+It9j7z6hgq1tNmt6im7VWLrWi/8mqDmycE0hzaNymFi8oQhMSNg9n98x+MjlZ",
+        "keyMaterialType": "ASYMMETRIC_PUBLIC"
+      },
+      "status": "ENABLED",
+      "keyId": 815459617,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
+)json";
+
+// Obtained with tinkey from the above; plus manual editing to split up the keyset.
+constexpr absl::string_view kKeyThreePrivateKeyset = R"json(
+{
+  "primaryKeyId": 815459617,
+  "key": [
+    {
+      "keyData": {
+        "typeUrl": "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey",
+        "value": "EpABEgYIBBAEGAIaQgGER6+b/6pXv2JqoxNWprDv8vAtDe4dzW33zODcPLe9W0efcn+FiTF5yi6jAlAc4bmN+hYAAr9bv5HDMafG9zuzGyJCAbRyxpNJAb+7APqT5x6Ad+e3yT+It9j7z6hgq1tNmt6im7VWLrWi/8mqDmycE0hzaNymFi8oQhMSNg9n98x+MjlZGkIAginhUHiaOORuoRCCGfNhiWcnE3EC6WXVlgughJ2bxk8KvYGZ5x8NtP1m8Tw5Cno3LiOWUrHGHurFAxiV8udoOOo=",
+        "keyMaterialType": "ASYMMETRIC_PRIVATE"
+      },
+      "status": "ENABLED",
+      "keyId": 815459617,
+      "outputPrefixType": "TINK"
+    }
+  ]
+}
+)json";
+
+@interface TINKPublicKeySignVerifyFactoryTest : XCTestCase
+@end
+
+@implementation TINKPublicKeySignVerifyFactoryTest
+
++ (void)setUp {
+  NSError *error = nil;
+  TINKAllConfig *allConfig = [[TINKAllConfig alloc] initWithError:&error];
+  XCTAssertNotNil(allConfig);
+  XCTAssertNil(error);
+}
+
+- (void)testCreatePublicKeySign {
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader = [[TINKJSONKeysetReader alloc]
+      initWithSerializedKeyset:TINKStringViewToNSData(kKeyThreePrivateKeyset)
+                         error:&error];
+  XCTAssertNil(error, @"TINKJSONKeysetReader creation failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNil(error, @"TINKKeysetHandle creation failed with %@", error);
+
+  id<TINKPublicKeySign> publicKeySign = [TINKPublicKeySignFactory primitiveWithKeysetHandle:handle
+                                                                                      error:&error];
+  XCTAssertNotNil(publicKeySign);
+  XCTAssertNil(error, @"TINKPublicKeySign creation failed with %@", error);
+}
+
+- (void)testCreatePublicKeyVerify {
+  NSError *error = nil;
+  TINKJSONKeysetReader *reader = [[TINKJSONKeysetReader alloc]
+      initWithSerializedKeyset:TINKStringViewToNSData(kEcdsaPublicKeyset)
+                         error:&error];
+  XCTAssertNil(error, @"TINKJSONKeysetReader creation failed with %@", error);
+
+  TINKKeysetHandle *handle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:reader error:&error];
+  XCTAssertNil(error, @"TINKKeysetHandle creation failed with %@", error);
+
+  id<TINKPublicKeyVerify> publicKeyVerify =
+      [TINKPublicKeyVerifyFactory primitiveWithKeysetHandle:handle error:&error];
+  XCTAssertNotNil(publicKeyVerify);
+  XCTAssertNil(error, @"TINKPublicKeyVerify creation failed with %@", error);
+}
+
+- (void)testSignThenVerify {
+  NSError *error = nil;
+  TINKJSONKeysetReader *privateReader = [[TINKJSONKeysetReader alloc]
+      initWithSerializedKeyset:TINKStringViewToNSData(kKeyThreePrivateKeyset)
+                         error:&error];
+  TINKKeysetHandle *privateHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:privateReader
+                                                                    error:&error];
+
+  id<TINKPublicKeySign> publicKeySign =
+      [TINKPublicKeySignFactory primitiveWithKeysetHandle:privateHandle error:&error];
+
+  TINKJSONKeysetReader *publicReader = [[TINKJSONKeysetReader alloc]
+      initWithSerializedKeyset:TINKStringViewToNSData(kEcdsaPublicKeyset)
+                         error:&error];
+
+  TINKKeysetHandle *publicHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:publicReader
+                                                                    error:&error];
+
+  id<TINKPublicKeyVerify> publicKeyVerify =
+      [TINKPublicKeyVerifyFactory primitiveWithKeysetHandle:publicHandle error:&error];
+
+  NSData *empty = [[NSData alloc] init];
+
+  NSData *signature = [publicKeySign signatureForData:empty error:&error];
+  XCTAssertNil(error, @"signatureForData failed with %@", error);
+
+  BOOL verification = [publicKeyVerify verifySignature:signature forData:empty error:&error];
+  XCTAssertTrue(verification);
+  XCTAssertNil(error, @"verifySignature failed with %@", error);
+}
+
+/** Tests that changing the message makes the signature fail. */
+- (void)testModifySignature_VerifyFails {
+  NSError *error = nil;
+  TINKJSONKeysetReader *privateReader = [[TINKJSONKeysetReader alloc]
+      initWithSerializedKeyset:TINKStringViewToNSData(kKeyThreePrivateKeyset)
+                         error:&error];
+  TINKKeysetHandle *privateHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:privateReader
+                                                                    error:&error];
+
+  id<TINKPublicKeySign> publicKeySign =
+      [TINKPublicKeySignFactory primitiveWithKeysetHandle:privateHandle error:&error];
+
+  TINKJSONKeysetReader *publicReader = [[TINKJSONKeysetReader alloc]
+      initWithSerializedKeyset:TINKStringViewToNSData(kEcdsaPublicKeyset)
+                         error:&error];
+
+  TINKKeysetHandle *publicHandle =
+      [[TINKKeysetHandle alloc] initCleartextKeysetHandleWithKeysetReader:publicReader
+                                                                    error:&error];
+
+  id<TINKPublicKeyVerify> publicKeyVerify =
+      [TINKPublicKeyVerifyFactory primitiveWithKeysetHandle:publicHandle error:&error];
+
+  NSData *empty = [[NSData alloc] init];
+
+  NSData *signature = [publicKeySign signatureForData:empty error:&error];
+  XCTAssertNil(error, @"signatureForData failed with %@", error);
+
+  NSData *wrongMessage = [NSData dataWithBytes:"hi" length:2];
+
+  BOOL verification = [publicKeyVerify verifySignature:signature forData:wrongMessage error:&error];
+  XCTAssertFalse(verification);
+  XCTAssertNotNil(error, @"verifySignature failed with %@", error);
+}
+
+@end
diff --git a/objc/Tests/UnitTests/signature/TINKPublicKeyVerifyFactory.mm b/objc/Tests/UnitTests/signature/TINKPublicKeyVerifyFactory.mm
deleted file mode 100644
index 09a8506..0000000
--- a/objc/Tests/UnitTests/signature/TINKPublicKeyVerifyFactory.mm
+++ /dev/null
@@ -1,213 +0,0 @@
-/**
- * Copyright 2018 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- **************************************************************************
- */
-
-#import "objc/TINKPublicKeyVerifyFactory.h"
-
-#import <Foundation/Foundation.h>
-#import <XCTest/XCTest.h>
-
-#import "objc/TINKKeysetHandle.h"
-#import "objc/TINKPublicKeySign.h"
-#import "objc/TINKPublicKeySignFactory.h"
-#import "objc/TINKPublicKeyVerify.h"
-#import "objc/TINKPublicKeyVerifyFactory.h"
-#import "objc/TINKSignatureConfig.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/signature/TINKPublicKeyVerifyInternal.h"
-#import "objc/util/TINKStrings.h"
-
-#include "absl/status/status.h"
-#include "tink/crypto_format.h"
-#include "tink/keyset_handle.h"
-#include "tink/signature/ecdsa_sign_key_manager.h"
-#include "tink/signature/signature_config.h"
-#include "tink/util/status.h"
-#include "tink/util/test_keyset_handle.h"
-#include "tink/util/test_util.h"
-#include "proto/ecdsa.pb.h"
-#include "proto/tink.pb.h"
-
-using crypto::tink::EcdsaSignKeyManager;
-using google::crypto::tink::EcdsaSignatureEncoding;
-using crypto::tink::KeyFactory;
-using crypto::tink::TestKeysetHandle;
-using crypto::tink::test::AddRawKey;
-using crypto::tink::test::AddTinkKey;
-using google::crypto::tink::EcdsaPublicKey;
-using google::crypto::tink::EcdsaPrivateKey;
-using google::crypto::tink::EllipticCurveType;
-using google::crypto::tink::HashType;
-using google::crypto::tink::KeyData;
-using google::crypto::tink::Keyset;
-using google::crypto::tink::KeyStatusType;
-
-static EcdsaPrivateKey GetNewEcdsaPrivateKey() {
-  return crypto::tink::test::GetEcdsaTestPrivateKey(EllipticCurveType::NIST_P256, HashType::SHA256,
-                                                    EcdsaSignatureEncoding::DER);
-}
-
-static EcdsaPublicKey GetEcdsaPublicKeyFromPrivate(EcdsaPrivateKey &privateKey) {
-  return privateKey.public_key();
-}
-
-@interface TINKPublicKeyVerifyFactoryTest : XCTestCase
-@end
-
-static Keyset privateKeyset;
-static Keyset publicKeyset;
-
-@implementation TINKPublicKeyVerifyFactoryTest
-
-+ (void)setUp {
-  [super setUp];
-
-  EcdsaPrivateKey keys[3] = {GetNewEcdsaPrivateKey(), GetNewEcdsaPrivateKey(),
-                             GetNewEcdsaPrivateKey()};
-
-  // Prepare a private keyset.
-  std::string key_type = "type.googleapis.com/google.crypto.tink.EcdsaPrivateKey";
-
-  uint32_t key_id_1 = 1234543;
-  AddTinkKey(key_type, key_id_1, keys[0], KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
-             &privateKeyset);
-
-  uint32_t key_id_2 = 726329;
-  AddTinkKey(key_type, key_id_2, keys[1], KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
-             &privateKeyset);
-
-  uint32_t key_id_3 = 7213743;
-  AddTinkKey(key_type, key_id_3, keys[2], KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC,
-             &privateKeyset);
-
-  privateKeyset.set_primary_key_id(key_id_3);
-
-  // Prepare the equivalent public keyset.
-  std::string public_key_type = "type.googleapis.com/google.crypto.tink.EcdsaPublicKey";
-
-  AddTinkKey(public_key_type, key_id_1, GetEcdsaPublicKeyFromPrivate(keys[0]),
-             KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC, &publicKeyset);
-
-  AddTinkKey(public_key_type, key_id_2, GetEcdsaPublicKeyFromPrivate(keys[1]),
-             KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC, &publicKeyset);
-
-  AddTinkKey(public_key_type, key_id_3, GetEcdsaPublicKeyFromPrivate(keys[2]),
-             KeyStatusType::ENABLED, KeyData::ASYMMETRIC_PUBLIC, &publicKeyset);
-
-  publicKeyset.set_primary_key_id(key_id_3);
-}
-
-- (void)testEmptyKeyset {
-  Keyset keyset;
-  TINKKeysetHandle *handle =
-      [[TINKKeysetHandle alloc] initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(keyset)];
-  XCTAssertNotNil(handle);
-
-  NSError *error = nil;
-  id<TINKPublicKeyVerify> publicKeyVerify =
-      [TINKPublicKeyVerifyFactory primitiveWithKeysetHandle:handle error:&error];
-  XCTAssertNil(publicKeyVerify);
-  XCTAssertNotNil(error);
-  XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-  XCTAssertTrue([error.localizedFailureReason containsString:@"at least one key"]);
-}
-
-- (void)testPrimitive {
-  NSError *error = nil;
-  TINKSignatureConfig *signatureConfig = [[TINKSignatureConfig alloc] initWithError:&error];
-  XCTAssertNotNil(signatureConfig);
-  XCTAssertNil(error);
-
-  TINKKeysetHandle *handlePrivate = [[TINKKeysetHandle alloc]
-      initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(privateKeyset)];
-  XCTAssertNotNil(handlePrivate);
-
-  TINKKeysetHandle *handlePublic = [[TINKKeysetHandle alloc]
-      initWithCCKeysetHandle:TestKeysetHandle::GetKeysetHandle(publicKeyset)];
-  XCTAssertNotNil(handlePublic);
-
-  id<TINKPublicKeySign> publicKeySign =
-      [TINKPublicKeySignFactory primitiveWithKeysetHandle:handlePrivate error:&error];
-  XCTAssertNotNil(publicKeySign);
-  XCTAssertNil(error);
-
-  // Sign something so we can test the verify primitive.
-  NSData *data = [@"some data to sign" dataUsingEncoding:NSUTF8StringEncoding];
-  NSData *signature = [publicKeySign signatureForData:data error:&error];
-  XCTAssertNil(error);
-  XCTAssertNotNil(signature);
-
-  id<TINKPublicKeyVerify> publicKeyVerify =
-      [TINKPublicKeyVerifyFactory primitiveWithKeysetHandle:handlePublic error:&error];
-  XCTAssertNotNil(publicKeyVerify);
-  XCTAssertNil(error);
-  TINKPublicKeyVerifyInternal *publicKeyVerifyInternal =
-      (TINKPublicKeyVerifyInternal *)publicKeyVerify;
-  XCTAssertTrue(publicKeyVerifyInternal.ccPublicKeyVerify != NULL);
-
-  // Test verification.
-  XCTAssertTrue([publicKeyVerify verifySignature:signature forData:data error:&error]);
-  XCTAssertNil(error);
-
-  // Flip every bit of the signature.
-  const char *signatureBytes = (const char *)signature.bytes;
-  for (NSUInteger byteIndex = 0; byteIndex < signature.length; byteIndex++) {
-    const char currentByte = signatureBytes[byteIndex];
-
-    for (NSUInteger bitIndex = 0; bitIndex < 8; bitIndex++) {
-      // Flip every bit on this byte.
-      char flippedByte = (currentByte ^ (1 << bitIndex));
-      XCTAssertTrue(flippedByte != currentByte);
-
-      // Replace the mutated byte in the original data.
-      NSMutableData *mutableSignature = signature.mutableCopy;
-      char *mutableBytes = (char *)mutableSignature.mutableBytes;
-      mutableBytes[byteIndex] = flippedByte;
-
-      error = nil;
-      XCTAssertFalse([mutableSignature isEqualToData:signature]);
-      XCTAssertFalse([publicKeyVerify verifySignature:mutableSignature forData:data error:&error]);
-      XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-      XCTAssertTrue([error.localizedFailureReason containsString:@"Invalid signature."]);
-    }
-  }
-
-  // Flip every bit of the data.
-  const char *dataBytes = (const char *)data.bytes;
-  for (NSUInteger byteIndex = 0; byteIndex < data.length; byteIndex++) {
-    const char currentByte = dataBytes[byteIndex];
-
-    for (NSUInteger bitIndex = 0; bitIndex < 8; bitIndex++) {
-      // Flip every bit on this byte.
-      char flippedByte = (currentByte ^ (1 << bitIndex));
-      XCTAssertTrue(flippedByte != currentByte);
-
-      // Replace the mutated byte in the original data.
-      NSMutableData *mutableData = data.mutableCopy;
-      char *mutableBytes = (char *)mutableData.mutableBytes;
-      mutableBytes[byteIndex] = flippedByte;
-
-      error = nil;
-      XCTAssertFalse([mutableData isEqualToData:data]);
-      XCTAssertFalse([publicKeyVerify verifySignature:signature forData:mutableData error:&error]);
-      XCTAssertEqual((absl::StatusCode)error.code, absl::StatusCode::kInvalidArgument);
-      XCTAssertTrue([error.localizedFailureReason containsString:@"Invalid signature."]);
-    }
-  }
-}
-
-@end
diff --git a/objc/Tests/UnitTests/signature/TINKSignatureKeyTemplateTest.mm b/objc/Tests/UnitTests/signature/TINKSignatureKeyTemplateTest.mm
index ef5626e..93010e3 100644
--- a/objc/Tests/UnitTests/signature/TINKSignatureKeyTemplateTest.mm
+++ b/objc/Tests/UnitTests/signature/TINKSignatureKeyTemplateTest.mm
@@ -16,12 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKSignatureKeyTemplate.h"
+#import "TINKSignatureKeyTemplate.h"
 
 #import <XCTest/XCTest.h>
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
+#include <memory>
+#include <string>
+#include <utility>
+
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
 #include "proto/common.pb.h"
 #include "proto/tink.pb.h"
 
diff --git a/objc/WORKSPACE b/objc/WORKSPACE
index 885fdbc..a1e5f41 100644
--- a/objc/WORKSPACE
+++ b/objc/WORKSPACE
@@ -1,21 +1,10 @@
 workspace(name = "tink_objc")
 
 local_repository(
-    name = "tink_base",
-    path = "..",
-)
-
-local_repository(
     name = "tink_cc",
     path = "../cc",
 )
 
-load("@tink_base//:tink_base_deps.bzl", "tink_base_deps")
-tink_base_deps()
-
-load("@tink_base//:tink_base_deps_init.bzl", "tink_base_deps_init")
-tink_base_deps_init()
-
 load("@tink_cc//:tink_cc_deps.bzl", "tink_cc_deps")
 tink_cc_deps()
 
diff --git a/objc/aead/TINKAeadConfig.mm b/objc/aead/TINKAeadConfig.mm
index 8baadc3..56d84da 100644
--- a/objc/aead/TINKAeadConfig.mm
+++ b/objc/aead/TINKAeadConfig.mm
@@ -16,15 +16,13 @@
  **************************************************************************
  */
 
-#import "objc/TINKAeadConfig.h"
+#import "TINKAeadConfig.h"
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 #include "tink/aead/aead_config.h"
 #include "tink/util/status.h"
-#include "proto/config.pb.h"
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@@ -39,8 +37,7 @@
     return nil;
   }
 
-  google::crypto::tink::RegistryConfig ccConfig = crypto::tink::AeadConfig::Latest();
-  return (self = [super initWithCcConfig:ccConfig]);
+  return (self = [super initWithError:error]);
 }
 
 @end
diff --git a/objc/aead/TINKAeadFactory.mm b/objc/aead/TINKAeadFactory.mm
index bd55987..268f8a4 100644
--- a/objc/aead/TINKAeadFactory.mm
+++ b/objc/aead/TINKAeadFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKAeadFactory.h"
+#import "TINKAeadFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKAead.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/aead/TINKAeadInternal.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKAead.h"
+#import "TINKKeysetHandle.h"
+#import "aead/TINKAeadInternal.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/aead/TINKAeadInternal.h b/objc/aead/TINKAeadInternal.h
index ae9efca..3fed8a7 100644
--- a/objc/aead/TINKAeadInternal.h
+++ b/objc/aead/TINKAeadInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKAead.h"
+#import "TINKAead.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/aead/TINKAeadInternal.mm b/objc/aead/TINKAeadInternal.mm
index 16d9fac..c466980 100644
--- a/objc/aead/TINKAeadInternal.mm
+++ b/objc/aead/TINKAeadInternal.mm
@@ -16,11 +16,11 @@
  **************************************************************************
  */
 
-#import "objc/aead/TINKAeadInternal.h"
+#import "aead/TINKAeadInternal.h"
 
-#import "objc/TINKAead.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKAead.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/aead.h"
diff --git a/objc/aead/TINKAeadKeyTemplate.mm b/objc/aead/TINKAeadKeyTemplate.mm
index b263493..562d4ed 100644
--- a/objc/aead/TINKAeadKeyTemplate.mm
+++ b/objc/aead/TINKAeadKeyTemplate.mm
@@ -16,16 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKAeadKeyTemplate.h"
+#import "TINKAeadKeyTemplate.h"
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "util/TINKErrors.h"
 
 #include "absl/status/status.h"
 #include "tink/aead/aead_key_templates.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKAeadKeyTemplate
 
diff --git a/objc/core/TINKAllConfig.mm b/objc/core/TINKAllConfig.mm
index 6b3f46a..100eb8e 100644
--- a/objc/core/TINKAllConfig.mm
+++ b/objc/core/TINKAllConfig.mm
@@ -16,16 +16,14 @@
  **************************************************************************
  */
 
-#import "objc/TINKAllConfig.h"
+#import "TINKAllConfig.h"
 
 #include "tink/config/tink_config.h"
-#include "proto/config.pb.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@@ -40,9 +38,7 @@
     return nil;
   }
 
-  google::crypto::tink::RegistryConfig ccConfig = crypto::tink::TinkConfig::Latest();
-
-  return (self = [super initWithCcConfig:ccConfig]);
+  return (self = [super initWithError:error]);
 }
 
 @end
diff --git a/objc/core/TINKBinaryKeysetReader.mm b/objc/core/TINKBinaryKeysetReader.mm
index 081bca8..3928ddc 100644
--- a/objc/core/TINKBinaryKeysetReader.mm
+++ b/objc/core/TINKBinaryKeysetReader.mm
@@ -16,17 +16,17 @@
  **************************************************************************
  */
 
-#import "objc/TINKBinaryKeysetReader.h"
+#import "TINKBinaryKeysetReader.h"
 
-#import "objc/TINKKeysetReader.h"
-#import "objc/core/TINKKeysetReader_Internal.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKKeysetReader.h"
+#import "core/TINKKeysetReader_Internal.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
 #include "tink/binary_keyset_reader.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKBinaryKeysetReader
 
diff --git a/objc/core/TINKConfig.mm b/objc/core/TINKConfig.mm
index 097da0f..12281e3 100644
--- a/objc/core/TINKConfig.mm
+++ b/objc/core/TINKConfig.mm
@@ -16,28 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKConfig.h"
+#import "TINKConfig.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
-
-#include "tink/config.h"
-#include "tink/util/errors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 @implementation TINKConfig
 
 + (BOOL)registerConfig:(TINKRegistryConfig *)config error:(NSError **)error {
-  auto st = crypto::tink::Config::Register(config.ccConfig);
-  if (!st.ok()) {
-    if (error) {
-      *error = TINKStatusToError(st);
-    }
-    return NO;
-  }
-
   return YES;
 }
 
diff --git a/objc/core/TINKJSONKeysetReader.mm b/objc/core/TINKJSONKeysetReader.mm
index c739e7f..0db42aa 100644
--- a/objc/core/TINKJSONKeysetReader.mm
+++ b/objc/core/TINKJSONKeysetReader.mm
@@ -16,17 +16,17 @@
  **************************************************************************
  */
 
-#import "objc/TINKJSONKeysetReader.h"
+#import "TINKJSONKeysetReader.h"
 
-#import "objc/TINKKeysetReader.h"
-#import "objc/core/TINKKeysetReader_Internal.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKKeysetReader.h"
+#import "core/TINKKeysetReader_Internal.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
 #include "tink/json_keyset_reader.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKJSONKeysetReader
 
diff --git a/objc/core/TINKKeyTemplate.mm b/objc/core/TINKKeyTemplate.mm
index 93f8535..bd52bd3 100644
--- a/objc/core/TINKKeyTemplate.mm
+++ b/objc/core/TINKKeyTemplate.mm
@@ -16,13 +16,13 @@
  **************************************************************************
  */
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "util/TINKErrors.h"
 
 #include "absl/status/status.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKKeyTemplate {
   google::crypto::tink::KeyTemplate *_ccKeyTemplate;
diff --git a/objc/core/TINKKeyTemplate_Internal.h b/objc/core/TINKKeyTemplate_Internal.h
index ec705d8..a9911c9 100644
--- a/objc/core/TINKKeyTemplate_Internal.h
+++ b/objc/core/TINKKeyTemplate_Internal.h
@@ -18,9 +18,9 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKKeyTemplate.h"
+#import "TINKKeyTemplate.h"
 
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @interface TINKKeyTemplate ()
 
diff --git a/objc/core/TINKKeysetHandle+Cleartext.mm b/objc/core/TINKKeysetHandle+Cleartext.mm
index 6ce0b8e..f92e5a9 100644
--- a/objc/core/TINKKeysetHandle+Cleartext.mm
+++ b/objc/core/TINKKeysetHandle+Cleartext.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKKeysetHandle+Cleartext.h"
+#import "TINKKeysetHandle+Cleartext.h"
 
 #include "tink/cleartext_keyset_handle.h"
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/core/TINKKeysetReader_Internal.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "core/TINKKeysetReader_Internal.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 @implementation TINKKeysetHandle (Cleartext)
 
diff --git a/objc/core/TINKKeysetHandle.mm b/objc/core/TINKKeysetHandle.mm
index 31663dd..471fc32 100644
--- a/objc/core/TINKKeysetHandle.mm
+++ b/objc/core/TINKKeysetHandle.mm
@@ -16,16 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKKeysetHandle.h"
+#import "TINKKeysetHandle.h"
 
-#import "objc/TINKAead.h"
-#import "objc/TINKKeyTemplate.h"
-#import "objc/TINKKeysetReader.h"
-#import "objc/aead/TINKAeadInternal.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/core/TINKKeysetReader_Internal.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKAead.h"
+#import "TINKKeyTemplate.h"
+#import "TINKKeysetReader.h"
+#import "aead/TINKAeadInternal.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "core/TINKKeysetReader_Internal.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include <iosfwd>
 #include <iostream>
@@ -40,7 +40,7 @@
 #include "tink/cleartext_keyset_handle.h"
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 static NSString *const kTinkService = @"com.google.crypto.tink";
 
@@ -365,7 +365,7 @@
 - (NSData *)serializedKeysetNoSecret:(NSError **)error {
   std::stringbuf buffer;
   auto writerResult = crypto::tink::BinaryKeysetWriter::New(
-      absl::make_unique<std::ostream>(&buffer));
+      std::make_unique<std::ostream>(&buffer));
   if (!writerResult.ok()) {
     if (error) {
       *error = TINKStatusToError(writerResult.status());
diff --git a/objc/core/TINKKeysetHandle_Internal.h b/objc/core/TINKKeysetHandle_Internal.h
index ddf9ad7..7009486 100644
--- a/objc/core/TINKKeysetHandle_Internal.h
+++ b/objc/core/TINKKeysetHandle_Internal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKKeysetHandle.h"
+#import "TINKKeysetHandle.h"
 
 #include "tink/keyset_handle.h"
 
diff --git a/objc/core/TINKKeysetReader.mm b/objc/core/TINKKeysetReader.mm
index e100b0a..e9b4bf3 100644
--- a/objc/core/TINKKeysetReader.mm
+++ b/objc/core/TINKKeysetReader.mm
@@ -18,8 +18,8 @@
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKKeysetReader.h"
-#import "objc/core/TINKKeysetReader_Internal.h"
+#import "TINKKeysetReader.h"
+#import "core/TINKKeysetReader_Internal.h"
 
 #include "tink/keyset_reader.h"
 
diff --git a/objc/core/TINKKeysetReader_Internal.h b/objc/core/TINKKeysetReader_Internal.h
index 64a96c8..577f7a6 100644
--- a/objc/core/TINKKeysetReader_Internal.h
+++ b/objc/core/TINKKeysetReader_Internal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKKeysetReader.h"
+#import "TINKKeysetReader.h"
 
 #include "tink/keyset_reader.h"
 
diff --git a/objc/core/TINKRegistryConfig.mm b/objc/core/TINKRegistryConfig.mm
index 3d5c078..7444648 100644
--- a/objc/core/TINKRegistryConfig.mm
+++ b/objc/core/TINKRegistryConfig.mm
@@ -16,34 +16,14 @@
  **************************************************************************
  */
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
+#import "TINKRegistryConfig.h"
 
-#include "proto/config.pb.h"
-
-@implementation TINKRegistryConfig {
-  google::crypto::tink::RegistryConfig _ccConfig;
-}
-
-- (instancetype)initWithCcConfig:(google::crypto::tink::RegistryConfig)ccConfig {
-  if ((self = [super init])) {
-    _ccConfig = ccConfig;
-  }
-  return self;
-}
-
-- (void)setCcConfig:(google::crypto::tink::RegistryConfig)ccConfig {
-  _ccConfig = ccConfig;
-}
-
-- (google::crypto::tink::RegistryConfig)ccConfig {
-  return _ccConfig;
-}
+@implementation TINKRegistryConfig
 
 - (nullable instancetype)initWithError:(NSError **)error {
   NSAssert(![self isMemberOfClass:[TINKRegistryConfig class]],
            @"Only instantiate from derived classes!");
-  return nil;
+  return self;
 }
 
 @end
diff --git a/objc/core/TINKRegistryConfig_Internal.h b/objc/core/TINKRegistryConfig_Internal.h
deleted file mode 100644
index fa90b28..0000000
--- a/objc/core/TINKRegistryConfig_Internal.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- **************************************************************************
- */
-
-#ifdef __cplusplus
-
-#import "objc/TINKRegistryConfig.h"
-
-#include "proto/config.pb.h"
-
-@interface TINKRegistryConfig ()
-
-- (instancetype)initWithCcConfig:(google::crypto::tink::RegistryConfig)ccConfig
-    NS_DESIGNATED_INITIALIZER;
-- (void)setCcConfig:(google::crypto::tink::RegistryConfig)ccConfig;
-- (google::crypto::tink::RegistryConfig)ccConfig;
-
-@end
-
-#endif  // __cplusplus
diff --git a/objc/core/TINKVersion.m.templ b/objc/core/TINKVersion.m.templ
index 4a6488c..cffb151 100644
--- a/objc/core/TINKVersion.m.templ
+++ b/objc/core/TINKVersion.m.templ
@@ -16,6 +16,6 @@
  **************************************************************************
  */
 
-#import "objc/TINKVersion.h"
+#import "TINKVersion.h"
 
 NSString *const TINKVersion = @"TINK_VERSION_LABEL";
diff --git a/objc/daead/TINKDeterministicAeadConfig.mm b/objc/daead/TINKDeterministicAeadConfig.mm
index 1fb30e7..e066405 100644
--- a/objc/daead/TINKDeterministicAeadConfig.mm
+++ b/objc/daead/TINKDeterministicAeadConfig.mm
@@ -16,15 +16,13 @@
  **************************************************************************
  */
 
-#import "objc/TINKDeterministicAeadConfig.h"
+#import "TINKDeterministicAeadConfig.h"
 
 #include "tink/daead/deterministic_aead_config.h"
 #include "tink/util/status.h"
-#include "proto/config.pb.h"
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@@ -39,8 +37,7 @@
     return nil;
   }
 
-  google::crypto::tink::RegistryConfig ccConfig = crypto::tink::DeterministicAeadConfig::Latest();
-  return (self = [super initWithCcConfig:ccConfig]);
+  return (self = [super initWithError:error]);
 }
 
 @end
diff --git a/objc/daead/TINKDeterministicAeadFactory.mm b/objc/daead/TINKDeterministicAeadFactory.mm
index 030dc6f..5c18ce6 100644
--- a/objc/daead/TINKDeterministicAeadFactory.mm
+++ b/objc/daead/TINKDeterministicAeadFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKDeterministicAeadFactory.h"
+#import "TINKDeterministicAeadFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKDeterministicAead.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/daead/TINKDeterministicAeadInternal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKDeterministicAead.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "daead/TINKDeterministicAeadInternal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/daead/TINKDeterministicAeadInternal.h b/objc/daead/TINKDeterministicAeadInternal.h
index 0e9295b..e0263a0 100644
--- a/objc/daead/TINKDeterministicAeadInternal.h
+++ b/objc/daead/TINKDeterministicAeadInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKDeterministicAead.h"
+#import "TINKDeterministicAead.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/daead/TINKDeterministicAeadInternal.mm b/objc/daead/TINKDeterministicAeadInternal.mm
index f2ae37b..fe5e251 100644
--- a/objc/daead/TINKDeterministicAeadInternal.mm
+++ b/objc/daead/TINKDeterministicAeadInternal.mm
@@ -16,18 +16,18 @@
  **************************************************************************
  */
 
-#import "objc/daead/TINKDeterministicAeadInternal.h"
-#import "objc/TINKDeterministicAead.h"
+#import "daead/TINKDeterministicAeadInternal.h"
+#import "TINKDeterministicAead.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/deterministic_aead.h"
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 @implementation TINKDeterministicAeadInternal {
   std::unique_ptr<crypto::tink::DeterministicAead> _ccDeterministicAead;
diff --git a/objc/daead/TINKDeterministicAeadKeyTemplate.mm b/objc/daead/TINKDeterministicAeadKeyTemplate.mm
index 216d047..ba6d376 100644
--- a/objc/daead/TINKDeterministicAeadKeyTemplate.mm
+++ b/objc/daead/TINKDeterministicAeadKeyTemplate.mm
@@ -16,16 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKDeterministicAeadKeyTemplate.h"
+#import "TINKDeterministicAeadKeyTemplate.h"
 
 #include "absl/status/status.h"
 #include "tink/daead/deterministic_aead_key_templates.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "util/TINKErrors.h"
 
 @implementation TINKDeterministicAeadKeyTemplate
 
diff --git a/examples/objc/helloworld/AeadPrimitive.h b/objc/examples/helloworld/AeadPrimitive.h
similarity index 100%
rename from examples/objc/helloworld/AeadPrimitive.h
rename to objc/examples/helloworld/AeadPrimitive.h
diff --git a/examples/objc/helloworld/AeadPrimitive.m b/objc/examples/helloworld/AeadPrimitive.m
similarity index 100%
rename from examples/objc/helloworld/AeadPrimitive.m
rename to objc/examples/helloworld/AeadPrimitive.m
diff --git a/examples/objc/helloworld/AppDelegate.h b/objc/examples/helloworld/AppDelegate.h
similarity index 100%
rename from examples/objc/helloworld/AppDelegate.h
rename to objc/examples/helloworld/AppDelegate.h
diff --git a/examples/objc/helloworld/AppDelegate.m b/objc/examples/helloworld/AppDelegate.m
similarity index 100%
rename from examples/objc/helloworld/AppDelegate.m
rename to objc/examples/helloworld/AppDelegate.m
diff --git a/examples/objc/helloworld/Assets.xcassets/AppIcon.appiconset/Contents.json b/objc/examples/helloworld/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from examples/objc/helloworld/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to objc/examples/helloworld/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/examples/objc/helloworld/Assets.xcassets/Contents.json b/objc/examples/helloworld/Assets.xcassets/Contents.json
similarity index 100%
rename from examples/objc/helloworld/Assets.xcassets/Contents.json
rename to objc/examples/helloworld/Assets.xcassets/Contents.json
diff --git a/examples/objc/helloworld/Base.lproj/LaunchScreen.storyboard b/objc/examples/helloworld/Base.lproj/LaunchScreen.storyboard
similarity index 100%
rename from examples/objc/helloworld/Base.lproj/LaunchScreen.storyboard
rename to objc/examples/helloworld/Base.lproj/LaunchScreen.storyboard
diff --git a/examples/objc/helloworld/Base.lproj/Main.storyboard b/objc/examples/helloworld/Base.lproj/Main.storyboard
similarity index 100%
rename from examples/objc/helloworld/Base.lproj/Main.storyboard
rename to objc/examples/helloworld/Base.lproj/Main.storyboard
diff --git a/examples/objc/helloworld/Info.plist b/objc/examples/helloworld/Info.plist
similarity index 100%
rename from examples/objc/helloworld/Info.plist
rename to objc/examples/helloworld/Info.plist
diff --git a/examples/objc/helloworld/Podfile b/objc/examples/helloworld/Podfile
similarity index 100%
rename from examples/objc/helloworld/Podfile
rename to objc/examples/helloworld/Podfile
diff --git a/objc/examples/helloworld/README.md b/objc/examples/helloworld/README.md
new file mode 100644
index 0000000..3d24662
--- /dev/null
+++ b/objc/examples/helloworld/README.md
@@ -0,0 +1,21 @@
+# Obj-C Hello World
+
+This is an example iOS application that can encrypt and decrypt text using
+[AEAD (Authenticated Encryption with Associated Data)](../../../docs/PRIMITIVES.md#authenticated-encryption-with-associated-data).
+
+It demonstrates the basic steps of using Tink, namely generating key material,
+obtaining a primitive, and using the primitive to do crypto.
+
+The example comes with a Podfile that demonstrates how to install Tink from
+Cocoapods.
+
+## Build and run
+
+### Cocoapods
+
+```shell
+git clone https://github.com/google/tink
+cd tink/objc/examples/helloworld
+pod install
+open TinkExampleApp.xcworkspace
+```
diff --git a/examples/objc/helloworld/ViewController.h b/objc/examples/helloworld/ViewController.h
similarity index 100%
rename from examples/objc/helloworld/ViewController.h
rename to objc/examples/helloworld/ViewController.h
diff --git a/examples/objc/helloworld/ViewController.m b/objc/examples/helloworld/ViewController.m
similarity index 100%
rename from examples/objc/helloworld/ViewController.m
rename to objc/examples/helloworld/ViewController.m
diff --git a/examples/objc/helloworld/main.m b/objc/examples/helloworld/main.m
similarity index 100%
rename from examples/objc/helloworld/main.m
rename to objc/examples/helloworld/main.m
diff --git a/examples/objc/helloworld/project.pbxproj b/objc/examples/helloworld/project.pbxproj
similarity index 100%
rename from examples/objc/helloworld/project.pbxproj
rename to objc/examples/helloworld/project.pbxproj
diff --git a/objc/hybrid/TINKHybridConfig.mm b/objc/hybrid/TINKHybridConfig.mm
index 0b9643b..a082517 100644
--- a/objc/hybrid/TINKHybridConfig.mm
+++ b/objc/hybrid/TINKHybridConfig.mm
@@ -16,15 +16,13 @@
  **************************************************************************
  */
 
-#import "objc/TINKHybridConfig.h"
+#import "TINKHybridConfig.h"
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 #include "tink/hybrid/hybrid_config.h"
 #include "tink/util/status.h"
-#include "proto/config.pb.h"
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@@ -39,9 +37,7 @@
     return nil;
   }
 
-  google::crypto::tink::RegistryConfig ccConfig = crypto::tink::HybridConfig::Latest();
-
-  return (self = [super initWithCcConfig:ccConfig]);
+  return (self = [super initWithError:error]);
 }
 
 @end
diff --git a/objc/hybrid/TINKHybridDecryptFactory.mm b/objc/hybrid/TINKHybridDecryptFactory.mm
index 6d3f75a..bf99c24 100644
--- a/objc/hybrid/TINKHybridDecryptFactory.mm
+++ b/objc/hybrid/TINKHybridDecryptFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKHybridDecryptFactory.h"
+#import "TINKHybridDecryptFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKHybridDecrypt.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/hybrid/TINKHybridDecryptInternal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKHybridDecrypt.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "hybrid/TINKHybridDecryptInternal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/hybrid/TINKHybridDecryptInternal.h b/objc/hybrid/TINKHybridDecryptInternal.h
index b59e6bc..20f6d8b 100644
--- a/objc/hybrid/TINKHybridDecryptInternal.h
+++ b/objc/hybrid/TINKHybridDecryptInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKHybridDecrypt.h"
+#import "TINKHybridDecrypt.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/hybrid/TINKHybridDecryptInternal.mm b/objc/hybrid/TINKHybridDecryptInternal.mm
index c3a468f..da53844 100644
--- a/objc/hybrid/TINKHybridDecryptInternal.mm
+++ b/objc/hybrid/TINKHybridDecryptInternal.mm
@@ -16,11 +16,11 @@
  **************************************************************************
  */
 
-#import "objc/hybrid/TINKHybridDecryptInternal.h"
+#import "hybrid/TINKHybridDecryptInternal.h"
 
-#import "objc/TINKHybridDecrypt.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKHybridDecrypt.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/hybrid_decrypt.h"
diff --git a/objc/hybrid/TINKHybridEncryptFactory.mm b/objc/hybrid/TINKHybridEncryptFactory.mm
index c02aabb..4969949 100644
--- a/objc/hybrid/TINKHybridEncryptFactory.mm
+++ b/objc/hybrid/TINKHybridEncryptFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKHybridEncryptFactory.h"
+#import "TINKHybridEncryptFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKHybridEncrypt.h"
-#import "objc/TINKKeysetHandle.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/hybrid/TINKHybridEncryptInternal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKHybridEncrypt.h"
+#import "TINKKeysetHandle.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "hybrid/TINKHybridEncryptInternal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/hybrid/TINKHybridEncryptInternal.h b/objc/hybrid/TINKHybridEncryptInternal.h
index 28df87e..ccc7690 100644
--- a/objc/hybrid/TINKHybridEncryptInternal.h
+++ b/objc/hybrid/TINKHybridEncryptInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKHybridEncrypt.h"
+#import "TINKHybridEncrypt.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/hybrid/TINKHybridEncryptInternal.mm b/objc/hybrid/TINKHybridEncryptInternal.mm
index 3988bff..b8d1024 100644
--- a/objc/hybrid/TINKHybridEncryptInternal.mm
+++ b/objc/hybrid/TINKHybridEncryptInternal.mm
@@ -16,11 +16,11 @@
  **************************************************************************
  */
 
-#import "objc/hybrid/TINKHybridEncryptInternal.h"
+#import "hybrid/TINKHybridEncryptInternal.h"
 
-#import "objc/TINKHybridEncrypt.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKHybridEncrypt.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/hybrid_encrypt.h"
diff --git a/objc/hybrid/TINKHybridKeyTemplate.mm b/objc/hybrid/TINKHybridKeyTemplate.mm
index cdc7e5c..e15ec66 100644
--- a/objc/hybrid/TINKHybridKeyTemplate.mm
+++ b/objc/hybrid/TINKHybridKeyTemplate.mm
@@ -16,16 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKHybridKeyTemplate.h"
+#import "TINKHybridKeyTemplate.h"
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "util/TINKErrors.h"
 
 #include "absl/status/status.h"
 #include "tink/hybrid/hybrid_key_templates.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKHybridKeyTemplate
 
diff --git a/objc/mac/TINKMacConfig.mm b/objc/mac/TINKMacConfig.mm
index 2f30a47..4cbbb07 100644
--- a/objc/mac/TINKMacConfig.mm
+++ b/objc/mac/TINKMacConfig.mm
@@ -16,15 +16,13 @@
  **************************************************************************
  */
 
-#import "objc/TINKMacConfig.h"
+#import "TINKMacConfig.h"
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 #include "tink/mac/mac_config.h"
 #include "tink/util/status.h"
-#include "proto/config.pb.h"
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@@ -39,9 +37,7 @@
     return nil;
   }
 
-  google::crypto::tink::RegistryConfig ccConfig = crypto::tink::MacConfig::Latest();
-
-  return (self = [super initWithCcConfig:ccConfig]);
+  return (self = [super initWithError:error]);
 }
 
 @end
diff --git a/objc/mac/TINKMacFactory.mm b/objc/mac/TINKMacFactory.mm
index b6042b3..15f950d 100644
--- a/objc/mac/TINKMacFactory.mm
+++ b/objc/mac/TINKMacFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKMacFactory.h"
+#import "TINKMacFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/TINKMac.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/mac/TINKMacInternal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeysetHandle.h"
+#import "TINKMac.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "mac/TINKMacInternal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/mac/TINKMacInternal.h b/objc/mac/TINKMacInternal.h
index 4a1f0ab..dff9386 100644
--- a/objc/mac/TINKMacInternal.h
+++ b/objc/mac/TINKMacInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKMac.h"
+#import "TINKMac.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/mac/TINKMacInternal.mm b/objc/mac/TINKMacInternal.mm
index 6f3f36e..6784ef6 100644
--- a/objc/mac/TINKMacInternal.mm
+++ b/objc/mac/TINKMacInternal.mm
@@ -16,10 +16,10 @@
  **************************************************************************
  */
 
-#import "objc/mac/TINKMacInternal.h"
+#import "mac/TINKMacInternal.h"
 
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/mac.h"
diff --git a/objc/mac/TINKMacKeyTemplate.mm b/objc/mac/TINKMacKeyTemplate.mm
index a3afaa9..cd88ce0 100644
--- a/objc/mac/TINKMacKeyTemplate.mm
+++ b/objc/mac/TINKMacKeyTemplate.mm
@@ -16,16 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKMacKeyTemplate.h"
+#import "TINKMacKeyTemplate.h"
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "util/TINKErrors.h"
 
 #include "absl/status/status.h"
 #include "tink/mac/mac_key_templates.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKMacKeyTemplate
 
diff --git a/objc/minimum_os.bzl b/objc/minimum_os.bzl
new file mode 100644
index 0000000..48cb72a
--- /dev/null
+++ b/objc/minimum_os.bzl
@@ -0,0 +1,3 @@
+"""Minimum OS version definitions"""
+
+IOS_MINIMUM_OS = "12.0"
diff --git a/objc/proto_redirect/BUILD.bazel b/objc/proto_redirect/BUILD.bazel
new file mode 100644
index 0000000..2f2b3be
--- /dev/null
+++ b/objc/proto_redirect/BUILD.bazel
@@ -0,0 +1,30 @@
+licenses(["notice"])
+
+package(default_visibility = ["//:__subpackages__"])
+
+cc_library(
+    name = "tink_cc_pb_redirect",
+    hdrs = ["tink_cc_pb_redirect.h"],
+    deps = ["@tink_cc//proto:tink_cc_proto"],
+)
+
+cc_library(
+    name = "aes_siv_cc_pb_redirect",
+    testonly = 1,
+    hdrs = ["aes_siv_cc_pb_redirect.h"],
+    deps = ["@tink_cc//proto:aes_siv_cc_proto"],
+)
+
+cc_library(
+    name = "aes_gcm_cc_pb_redirect",
+    testonly = 1,
+    hdrs = ["aes_gcm_cc_pb_redirect.h"],
+    deps = ["@tink_cc//proto:aes_gcm_cc_proto"],
+)
+
+cc_library(
+    name = "hmac_cc_pb_redirect",
+    testonly = 1,
+    hdrs = ["hmac_cc_pb_redirect.h"],
+    deps = ["@tink_cc//proto:hmac_cc_proto"],
+)
diff --git a/objc/proto_redirect/aes_gcm_cc_pb_redirect.h b/objc/proto_redirect/aes_gcm_cc_pb_redirect.h
new file mode 100644
index 0000000..b1ca850
--- /dev/null
+++ b/objc/proto_redirect/aes_gcm_cc_pb_redirect.h
@@ -0,0 +1,25 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_AES_GCM_CC_PB_REDIRECT_H_
+#define THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_AES_GCM_CC_PB_REDIRECT_H_
+
+// We export aes_gcm.pb.h -- this allows us to include a cc_library target from
+// objc instead of a cc_proto_library target, which does not seem to work at
+// the moment.
+#include "proto/aes_gcm.pb.h"
+
+#endif  // THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_AES_GCM_CC_PB_REDIRECT_H_
diff --git a/objc/proto_redirect/aes_siv_cc_pb_redirect.h b/objc/proto_redirect/aes_siv_cc_pb_redirect.h
new file mode 100644
index 0000000..c80ce25
--- /dev/null
+++ b/objc/proto_redirect/aes_siv_cc_pb_redirect.h
@@ -0,0 +1,25 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_AES_SIV_CC_PB_REDIRECT_H_
+#define THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_AES_SIV_CC_PB_REDIRECT_H_
+
+// We export aes_siv.pb.h -- this allows us to include a cc_library target from
+// objc instead of a cc_proto_library target, which does not seem to work at
+// the moment.
+#include "proto/aes_siv.pb.h"
+
+#endif  // THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_AES_SIV_CC_PB_REDIRECT_H_
diff --git a/objc/proto_redirect/hmac_cc_pb_redirect.h b/objc/proto_redirect/hmac_cc_pb_redirect.h
new file mode 100644
index 0000000..ed62160
--- /dev/null
+++ b/objc/proto_redirect/hmac_cc_pb_redirect.h
@@ -0,0 +1,24 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_HMAC_CC_PB_REDIRECT_H_
+#define THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_HMAC_CC_PB_REDIRECT_H_
+
+// We export hmac.pb.h -- this allows us to include a cc_library target from
+// objc instead of a cc_proto_library target, which does not seem to work at
+// the moment.
+#include "proto/hmac.pb.h"
+
+#endif  // THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_HMAC_CC_PB_REDIRECT_H_
diff --git a/objc/proto_redirect/tink_cc_pb_redirect.h b/objc/proto_redirect/tink_cc_pb_redirect.h
new file mode 100644
index 0000000..2406053
--- /dev/null
+++ b/objc/proto_redirect/tink_cc_pb_redirect.h
@@ -0,0 +1,24 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifndef THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_TINK_CC_PB_REDIRECT_H_
+#define THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_TINK_CC_PB_REDIRECT_H_
+
+// We export tink.pb.h -- this allows us to include a cc_library target from
+// objc instead of a cc_proto_library target, which does not seem to work at
+// the moment.
+#include "proto/tink.pb.h"
+
+#endif  // THIRD_PARTY_TINK_OBJC_PROTO_REDIRECT_TINK_CC_PB_REDIRECT_H_
diff --git a/objc/signature/TINKPublicKeySignFactory.mm b/objc/signature/TINKPublicKeySignFactory.mm
index 6310c2a..e546193 100644
--- a/objc/signature/TINKPublicKeySignFactory.mm
+++ b/objc/signature/TINKPublicKeySignFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKPublicKeySignFactory.h"
+#import "TINKPublicKeySignFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/TINKPublicKeySign.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/signature/TINKPublicKeySignInternal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeysetHandle.h"
+#import "TINKPublicKeySign.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "signature/TINKPublicKeySignInternal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/signature/TINKPublicKeySignInternal.h b/objc/signature/TINKPublicKeySignInternal.h
index 25882c2..a05fb33 100644
--- a/objc/signature/TINKPublicKeySignInternal.h
+++ b/objc/signature/TINKPublicKeySignInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKPublicKeySign.h"
+#import "TINKPublicKeySign.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/signature/TINKPublicKeySignInternal.mm b/objc/signature/TINKPublicKeySignInternal.mm
index 55d8041..9dcebe7 100644
--- a/objc/signature/TINKPublicKeySignInternal.mm
+++ b/objc/signature/TINKPublicKeySignInternal.mm
@@ -16,11 +16,11 @@
  **************************************************************************
  */
 
-#import "objc/signature/TINKPublicKeySignInternal.h"
+#import "signature/TINKPublicKeySignInternal.h"
 
-#import "objc/TINKPublicKeySign.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKPublicKeySign.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/public_key_sign.h"
diff --git a/objc/signature/TINKPublicKeyVerifyFactory.mm b/objc/signature/TINKPublicKeyVerifyFactory.mm
index f05153e..ada78e2 100644
--- a/objc/signature/TINKPublicKeyVerifyFactory.mm
+++ b/objc/signature/TINKPublicKeyVerifyFactory.mm
@@ -16,15 +16,15 @@
  **************************************************************************
  */
 
-#import "objc/TINKPublicKeyVerifyFactory.h"
+#import "TINKPublicKeyVerifyFactory.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/TINKKeysetHandle.h"
-#import "objc/TINKPublicKeyVerify.h"
-#import "objc/core/TINKKeysetHandle_Internal.h"
-#import "objc/signature/TINKPublicKeyVerifyInternal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeysetHandle.h"
+#import "TINKPublicKeyVerify.h"
+#import "core/TINKKeysetHandle_Internal.h"
+#import "signature/TINKPublicKeyVerifyInternal.h"
+#import "util/TINKErrors.h"
 
 #include "tink/keyset_handle.h"
 #include "tink/util/status.h"
diff --git a/objc/signature/TINKPublicKeyVerifyInternal.h b/objc/signature/TINKPublicKeyVerifyInternal.h
index cf57dc2..fed275a 100644
--- a/objc/signature/TINKPublicKeyVerifyInternal.h
+++ b/objc/signature/TINKPublicKeyVerifyInternal.h
@@ -18,7 +18,7 @@
 
 #ifdef __cplusplus
 
-#import "objc/TINKPublicKeyVerify.h"
+#import "TINKPublicKeyVerify.h"
 
 #import <Foundation/Foundation.h>
 
diff --git a/objc/signature/TINKPublicKeyVerifyInternal.mm b/objc/signature/TINKPublicKeyVerifyInternal.mm
index 58939d5..b8cc532 100644
--- a/objc/signature/TINKPublicKeyVerifyInternal.mm
+++ b/objc/signature/TINKPublicKeyVerifyInternal.mm
@@ -16,11 +16,11 @@
  **************************************************************************
  */
 
-#import "objc/signature/TINKPublicKeyVerifyInternal.h"
+#import "signature/TINKPublicKeyVerifyInternal.h"
 
-#import "objc/TINKPublicKeyVerify.h"
-#import "objc/util/TINKErrors.h"
-#import "objc/util/TINKStrings.h"
+#import "TINKPublicKeyVerify.h"
+#import "util/TINKErrors.h"
+#import "util/TINKStrings.h"
 
 #include "absl/strings/string_view.h"
 #include "tink/public_key_verify.h"
diff --git a/objc/signature/TINKSignatureConfig.mm b/objc/signature/TINKSignatureConfig.mm
index 28cf565..0b0c070 100644
--- a/objc/signature/TINKSignatureConfig.mm
+++ b/objc/signature/TINKSignatureConfig.mm
@@ -16,15 +16,13 @@
  **************************************************************************
  */
 
-#import "objc/TINKSignatureConfig.h"
+#import "TINKSignatureConfig.h"
 
-#import "objc/TINKRegistryConfig.h"
-#import "objc/core/TINKRegistryConfig_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKRegistryConfig.h"
+#import "util/TINKErrors.h"
 
 #include "tink/signature/signature_config.h"
 #include "tink/util/status.h"
-#include "proto/config.pb.h"
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wobjc-designated-initializers"
@@ -39,9 +37,7 @@
     return nil;
   }
 
-  google::crypto::tink::RegistryConfig ccConfig = crypto::tink::SignatureConfig::Latest();
-
-  return (self = [super initWithCcConfig:ccConfig]);
+  return (self = [super initWithError:error]);
 }
 
 @end
diff --git a/objc/signature/TINKSignatureKeyTemplate.mm b/objc/signature/TINKSignatureKeyTemplate.mm
index b1cdd13..4022c92 100644
--- a/objc/signature/TINKSignatureKeyTemplate.mm
+++ b/objc/signature/TINKSignatureKeyTemplate.mm
@@ -16,16 +16,16 @@
  **************************************************************************
  */
 
-#import "objc/TINKSignatureKeyTemplate.h"
+#import "TINKSignatureKeyTemplate.h"
 
-#import "objc/TINKKeyTemplate.h"
-#import "objc/core/TINKKeyTemplate_Internal.h"
-#import "objc/util/TINKErrors.h"
+#import "TINKKeyTemplate.h"
+#import "core/TINKKeyTemplate_Internal.h"
+#import "util/TINKErrors.h"
 
 #include "absl/status/status.h"
 #include "tink/signature/signature_key_templates.h"
 #include "tink/util/status.h"
-#include "proto/tink.pb.h"
+#include "third_party/tink/objc/proto_redirect/tink_cc_pb_redirect.h"
 
 @implementation TINKSignatureKeyTemplate
 
diff --git a/objc/tink_objc_deps.bzl b/objc/tink_objc_deps.bzl
index d02bfd4..10aa815 100644
--- a/objc/tink_objc_deps.bzl
+++ b/objc/tink_objc_deps.bzl
@@ -1,7 +1,49 @@
 """Dependencies of Tink ObjC."""
 
-# We currently rely on the cc dependencies to include build_bazel_rules_apple,
-# build_bazel_rules_swift, build_bazel_apple_support.
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
 def tink_objc_deps():
-    pass
+    """Dependencies for Tink Objective C."""
+    if not native.existing_rule("build_bazel_rules_apple"):
+        # Release from 2022-12-21.
+        http_archive(
+            strip_prefix = "rules_apple-2.0.0",
+            name = "build_bazel_rules_apple",
+            sha256 = "58fef2369c53b0d9a75441bc40489b586a7ccce24335c9b51ccfa7265623aeb4",
+            url = "https://github.com/bazelbuild/rules_apple/archive/refs/tags/2.0.0.zip",
+        )
+    if not native.existing_rule("build_bazel_rules_swift"):
+        # Release from 2022-09-16.
+        http_archive(
+            name = "build_bazel_rules_swift",
+            sha256 = "51efdaf85e04e51174de76ef563f255451d5a5cd24c61ad902feeadafc7046d9",
+            url = "https://github.com/bazelbuild/rules_swift/releases/download/1.2.0/rules_swift.1.2.0.tar.gz",
+        )
+    if not native.existing_rule("build_bazel_apple_support"):
+        # Release from 2022-10-31.
+        http_archive(
+            name = "build_bazel_apple_support",
+            sha256 = "2e3dc4d0000e8c2f5782ea7bb53162f37c485b5d8dc62bb3d7d7fc7c276f0d00",
+            url = "https://github.com/bazelbuild/apple_support/releases/download/1.3.2/apple_support.1.3.2.tar.gz",
+        )
+
+    # Currently required by ios_unit_test
+    if not native.existing_rule("xctestrunner"):
+        # Release from 2021-11-01.
+        http_archive(
+            name = "xctestrunner",
+            strip_prefix = "xctestrunner-0.2.15",
+            sha256 = "03ce1088f74d85e23d14a09e533383bd06368d2b453c962e6ce66f80b833feae",
+            url = "https://github.com/google/xctestrunner/archive/refs/tags/0.2.15.zip",
+        )
+
+    # Subpar is a utility for creating self-contained python executables. It is designed to work well with Bazel.
+    # Currently required by @xctestrunner
+    if not native.existing_rule("subpar"):
+        # Release from 2019-05-14.
+        http_archive(
+            name = "subpar",
+            strip_prefix = "subpar-2.0.0",
+            sha256 = "8876244a984d75f28b1c64d711b6e5dfab5f992a3b741480e63cfc5e26acba93",
+            url = "https://github.com/google/subpar/archive/refs/tags/2.0.0.zip",
+        )
diff --git a/objc/tink_version.bzl b/objc/tink_version.bzl
new file mode 100644
index 0000000..e468bfa
--- /dev/null
+++ b/objc/tink_version.bzl
@@ -0,0 +1,2 @@
+""" Version of the current release of Tink """
+TINK_VERSION_LABEL = "1.7.0"
diff --git a/objc/util/BUILD.bazel b/objc/util/BUILD.bazel
index 6157581..30a3cca 100644
--- a/objc/util/BUILD.bazel
+++ b/objc/util/BUILD.bazel
@@ -1,4 +1,4 @@
-package(default_visibility = ["//tools/build_defs:internal_pkg"])
+package(default_visibility = ["//:__subpackages__"])
 
 licenses(["notice"])
 
@@ -7,8 +7,8 @@
     srcs = ["TINKErrors.mm"],
     hdrs = ["TINKErrors.h"],
     deps = [
-        "//cc/util:status",
         "@com_google_absl//absl/status",
+        "@tink_cc//tink/util:status",
     ],
 )
 
@@ -18,44 +18,7 @@
     hdrs = ["TINKStrings.h"],
     deps = [
         ":errors",
-        "//cc/util:status",
         "@com_google_absl//absl/strings",
-    ],
-)
-
-objc_library(
-    name = "test_helpers",
-    testonly = 1,
-    srcs = ["TINKTestHelpers.mm"],
-    hdrs = ["TINKTestHelpers.h"],
-    deps = [
-        ":errors",
-        ":strings",
-        "//cc/subtle:subtle_util_boringssl",
-        "//proto:all_objc_proto",
-        "@com_google_absl//absl/strings",
-        "@com_google_protobuf//:objectivec",
-    ],
-)
-
-objc_library(
-    name = "proto_helpers",
-    testonly = 1,
-    srcs = ["TINKProtoHelpers.mm"],
-    hdrs = ["TINKProtoHelpers.h"],
-    deps = [
-        ":errors",
-        ":strings",
-        ":tink_cc_pb",
-        "//cc/util:status",
-        "//proto:all_objc_proto",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
-cc_library(
-    name = "tink_cc_pb",
-    deps = [
-        "//proto:tink_cc_proto",
+        "@tink_cc//tink/util:status",
     ],
 )
diff --git a/objc/util/TINKErrors.h b/objc/util/TINKErrors.h
index ca82d55..4eaead2 100644
--- a/objc/util/TINKErrors.h
+++ b/objc/util/TINKErrors.h
@@ -29,7 +29,8 @@
  * Creates an NSError given a Tink error code and a message.
  * @deprecated use absl::StatusCode as the first argument instead.
  */
-NSError* TINKError(crypto::tink::util::error::Code code, NSString* message);
+NSError* TINKError(crypto::tink::util::error::Code code, NSString* message)
+    __deprecated_msg("Use the API taking an absl::StatusCode instead");
 #endif
 
 /** Creates an NSError given an absl status code and a message. */
diff --git a/objc/util/TINKErrors.mm b/objc/util/TINKErrors.mm
index c132aac..1c3d792 100644
--- a/objc/util/TINKErrors.mm
+++ b/objc/util/TINKErrors.mm
@@ -15,7 +15,7 @@
  *
  **************************************************************************
  */
-#import "objc/util/TINKErrors.h"
+#import "util/TINKErrors.h"
 
 #include "absl/status/status.h"
 #include "tink/util/status.h"
diff --git a/objc/util/TINKStrings.mm b/objc/util/TINKStrings.mm
index e74c0e0..de02a8c 100644
--- a/objc/util/TINKStrings.mm
+++ b/objc/util/TINKStrings.mm
@@ -16,11 +16,11 @@
  **************************************************************************
  */
 
-#import "objc/util/TINKStrings.h"
+#import "util/TINKStrings.h"
 
 #import <Foundation/Foundation.h>
 
-#import "objc/util/TINKErrors.h"
+#import "util/TINKErrors.h"
 
 #include "absl/strings/string_view.h"
 
diff --git a/proto/aes_cmac.proto b/proto/aes_cmac.proto
index b214834..541ff58 100644
--- a/proto/aes_cmac.proto
+++ b/proto/aes_cmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_go_proto";
 
 message AesCmacParams {
   uint32 tag_size = 1;
diff --git a/proto/aes_cmac_prf.proto b/proto/aes_cmac_prf.proto
index 58e5f67..b2efc6d 100644
--- a/proto/aes_cmac_prf.proto
+++ b/proto/aes_cmac_prf.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_prf_go_proto";
 
 // key_type: type.googleapis.com/google.crypto.tink.AesCmacPrfKey
 message AesCmacPrfKey {
diff --git a/proto/aes_ctr.proto b/proto/aes_ctr.proto
index ecdb256..721699c 100644
--- a/proto/aes_ctr.proto
+++ b/proto/aes_ctr.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_go_proto";
 
 message AesCtrParams {
   uint32 iv_size = 1;
diff --git a/proto/aes_ctr_hmac_aead.proto b/proto/aes_ctr_hmac_aead.proto
index dcf541d..91ccb9b 100644
--- a/proto/aes_ctr_hmac_aead.proto
+++ b/proto/aes_ctr_hmac_aead.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_aead_go_proto";
 
 message AesCtrHmacAeadKeyFormat {
   AesCtrKeyFormat aes_ctr_key_format = 1;
diff --git a/proto/aes_ctr_hmac_streaming.proto b/proto/aes_ctr_hmac_streaming.proto
index 064b630..776e9bd 100644
--- a/proto/aes_ctr_hmac_streaming.proto
+++ b/proto/aes_ctr_hmac_streaming.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_streaming_go_proto";
 
 message AesCtrHmacStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/proto/aes_eax.proto b/proto/aes_eax.proto
index c673306..c1bf500 100644
--- a/proto/aes_eax.proto
+++ b/proto/aes_eax.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_eax_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_eax_go_proto";
 
 // only allowing tag size in bytes = 16
 message AesEaxParams {
diff --git a/proto/aes_gcm.proto b/proto/aes_gcm.proto
index fba7a89..2551aa4 100644
--- a/proto/aes_gcm.proto
+++ b/proto/aes_gcm.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_go_proto";
 option objc_class_prefix = "TINKPB";
 
 message AesGcmKeyFormat {
diff --git a/proto/aes_gcm_hkdf_streaming.proto b/proto/aes_gcm_hkdf_streaming.proto
index 61fb479..5ec7ca4 100644
--- a/proto/aes_gcm_hkdf_streaming.proto
+++ b/proto/aes_gcm_hkdf_streaming.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto";
 
 message AesGcmHkdfStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/proto/aes_gcm_siv.proto b/proto/aes_gcm_siv.proto
index df9fada..220d79f 100644
--- a/proto/aes_gcm_siv.proto
+++ b/proto/aes_gcm_siv.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_siv_go_proto";
 
 // The only allowed IV size is 12 bytes and tag size is 16 bytes.
 // Thus, accept no params.
diff --git a/proto/aes_siv.proto b/proto/aes_siv.proto
index 0023027..ccb8d3c 100644
--- a/proto/aes_siv.proto
+++ b/proto/aes_siv.proto
@@ -20,7 +20,15 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_siv_go_proto";
+
+// Tink implements RFC 5297 (https://www.rfc-editor.org/rfc/rfc5297) for
+// AES-SIV, putting the SIV/Tag at the beginning of the ciphertext.
+//
+// While the RFC 5297 supports a list of associated datas, Tink only supports
+// exactly one associated data, which corresponds to a list with one element in
+// RFC 5297. An empty associated data is a list with one empty element, and not
+// an empty list.
 
 message AesSivKeyFormat {
   // Only valid value is: 64.
diff --git a/proto/cached_dek_aead.proto b/proto/cached_dek_aead.proto
index 10bcde5..9b1a33f 100644
--- a/proto/cached_dek_aead.proto
+++ b/proto/cached_dek_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/cached_dek_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_aead_go_proto";
 
 message CachedDekAeadKeyFormat {
   // Required.
diff --git a/proto/cached_dek_envelope.proto b/proto/cached_dek_envelope.proto
index 1b096ad..8739b83 100644
--- a/proto/cached_dek_envelope.proto
+++ b/proto/cached_dek_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_multiple_files = true;
 option java_package = "com.google.crypto.tink.proto";
-option go_package = "github.com/google/tink/proto/cached_dek_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_envelope_go_proto";
 
 message CachedDekEnvelopeAeadKeyFormat {
   // Required.
diff --git a/proto/chacha20_poly1305.proto b/proto/chacha20_poly1305.proto
index 2cd6ead..ef8ab6e 100644
--- a/proto/chacha20_poly1305.proto
+++ b/proto/chacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/chacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/chacha20_poly1305_go_proto";
 
 message ChaCha20Poly1305KeyFormat {}
 
diff --git a/proto/common.proto b/proto/common.proto
index eaff8d3..4546064 100644
--- a/proto/common.proto
+++ b/proto/common.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/common_go_proto";
+option go_package = "github.com/google/tink/go/proto/common_go_proto";
 
 enum EllipticCurveType {
   UNKNOWN_CURVE = 0;
diff --git a/proto/config.proto b/proto/config.proto
index ebbd742..cff6506 100644
--- a/proto/config.proto
+++ b/proto/config.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/config_go_proto";
+option go_package = "github.com/google/tink/go/proto/config_go_proto";
 
 // An entry that describes a key type to be used with Tink library,
 // specifying the corresponding primitive, key manager, and deprecation status.
diff --git a/proto/ecdsa.proto b/proto/ecdsa.proto
index 6ba3970..2ce461f 100644
--- a/proto/ecdsa.proto
+++ b/proto/ecdsa.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecdsa_go_proto";
 
 enum EcdsaSignatureEncoding {
   UNKNOWN_ENCODING = 0;
@@ -80,4 +80,5 @@
 message EcdsaKeyFormat {
   // Required.
   EcdsaParams params = 2;
+  uint32 version = 3;
 }
diff --git a/proto/ecies_aead_hkdf.proto b/proto/ecies_aead_hkdf.proto
index 9470991..0c06ee3 100644
--- a/proto/ecies_aead_hkdf.proto
+++ b/proto/ecies_aead_hkdf.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecies_aead_hkdf_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecies_aead_hkdf_go_proto";
 
 // Protos for keys for ECIES with HKDF and AEAD encryption.
 //
diff --git a/proto/ed25519.proto b/proto/ed25519.proto
index 669f33a..613c59f 100644
--- a/proto/ed25519.proto
+++ b/proto/ed25519.proto
@@ -23,10 +23,10 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ed25519_go_proto";
+option go_package = "github.com/google/tink/go/proto/ed25519_go_proto";
 
 message Ed25519KeyFormat {
-    uint32 version = 1;
+  uint32 version = 1;
 }
 
 // key_type: type.googleapis.com/google.crypto.tink.Ed25519PublicKey
diff --git a/proto/empty.proto b/proto/empty.proto
index 33831a9..beeba07 100644
--- a/proto/empty.proto
+++ b/proto/empty.proto
@@ -20,6 +20,6 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/empty_go_proto";
+option go_package = "github.com/google/tink/go/proto/empty_go_proto";
 
 message Empty {}
diff --git a/proto/hkdf_prf.proto b/proto/hkdf_prf.proto
index 3d3cbe9..38e69c5 100644
--- a/proto/hkdf_prf.proto
+++ b/proto/hkdf_prf.proto
@@ -22,12 +22,16 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hkdf_prf_proto";
+option go_package = "github.com/google/tink/go/proto/hkdf_prf_proto";
 
 message HkdfPrfParams {
   HashType hash = 1;
-  // Salt, optional in RFC 5869. Using "" is equivalent to zeros of length up to
-  // the block length of the HMac.
+  // Optional.
+  //
+  // An unspecified or zero-length value is equivalent to a sequence of zeros
+  // (0x00) with a length equal to the output size of hash.
+  //
+  // See https://rfc-editor.org/rfc/rfc5869.
   bytes salt = 2;
 }
 
diff --git a/proto/hmac.proto b/proto/hmac.proto
index 2733e51..bdd4e8a 100644
--- a/proto/hmac.proto
+++ b/proto/hmac.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_go_proto";
 
 message HmacParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/proto/hmac_prf.proto b/proto/hmac_prf.proto
index 7b3c52d..87ef97d 100644
--- a/proto/hmac_prf.proto
+++ b/proto/hmac_prf.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_prf_go_proto";
 
 message HmacPrfParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/proto/hpke.proto b/proto/hpke.proto
index 847864a..f794e77 100644
--- a/proto/hpke.proto
+++ b/proto/hpke.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hpke_proto";
+option go_package = "github.com/google/tink/go/proto/hpke_proto";
 
 enum HpkeKem {
   KEM_UNKNOWN = 0;
diff --git a/proto/jwt_ecdsa.proto b/proto/jwt_ecdsa.proto
index 4c80fe1..ce78b04 100644
--- a/proto/jwt_ecdsa.proto
+++ b/proto/jwt_ecdsa.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_ecdsa_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.4
 enum JwtEcdsaAlgorithm {
diff --git a/proto/jwt_hmac.proto b/proto/jwt_hmac.proto
index e54a51d..a499638 100644
--- a/proto/jwt_hmac.proto
+++ b/proto/jwt_hmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_hmac_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
 enum JwtHmacAlgorithm {
diff --git a/proto/jwt_rsa_ssa_pkcs1.proto b/proto/jwt_rsa_ssa_pkcs1.proto
index adf31c8..54a9731 100644
--- a/proto/jwt_rsa_ssa_pkcs1.proto
+++ b/proto/jwt_rsa_ssa_pkcs1.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.3
 enum JwtRsaSsaPkcs1Algorithm {
diff --git a/proto/jwt_rsa_ssa_pss.proto b/proto/jwt_rsa_ssa_pss.proto
index 4312645..eb2d454 100644
--- a/proto/jwt_rsa_ssa_pss.proto
+++ b/proto/jwt_rsa_ssa_pss.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.5
 enum JwtRsaSsaPssAlgorithm {
diff --git a/proto/kms_aead.proto b/proto/kms_aead.proto
index e818788..16de8ee 100644
--- a/proto/kms_aead.proto
+++ b/proto/kms_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_aead_go_proto";
 
 message KmsAeadKeyFormat {
   // Required.
diff --git a/proto/kms_envelope.proto b/proto/kms_envelope.proto
index fa806e6..8b0fd83 100644
--- a/proto/kms_envelope.proto
+++ b/proto/kms_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_envelope_go_proto";
 
 message KmsEnvelopeAeadKeyFormat {
   // Required.
diff --git a/proto/portable_tink_filter_lite.asciipb b/proto/portable_tink_filter_lite.asciipb
deleted file mode 100644
index a0f3934..0000000
--- a/proto/portable_tink_filter_lite.asciipb
+++ /dev/null
@@ -1,27 +0,0 @@
-optimize_mode: LITE_RUNTIME
-allowed_file: "third_party/tink/proto/aes_cmac.proto"
-allowed_file: "third_party/tink/proto/aes_cmac_prf.proto"
-allowed_file: "third_party/tink/proto/aes_ctr.proto"
-allowed_file: "third_party/tink/proto/aes_ctr_hmac_aead.proto"
-allowed_file: "third_party/tink/proto/aes_ctr_hmac_streaming.proto"
-allowed_file: "third_party/tink/proto/aes_eax.proto"
-allowed_file: "third_party/tink/proto/aes_gcm.proto"
-allowed_file: "third_party/tink/proto/aes_gcm_siv.proto"
-allowed_file: "third_party/tink/proto/aes_gcm_hkdf_streaming.proto"
-allowed_file: "third_party/tink/proto/aes_siv.proto"
-allowed_file: "third_party/tink/proto/chacha20_poly1305.proto"
-allowed_file: "third_party/tink/proto/common.proto"
-allowed_file: "third_party/tink/proto/config.proto"
-allowed_file: "third_party/tink/proto/ecdsa.proto"
-allowed_file: "third_party/tink/proto/ecies_aead_hkdf.proto"
-allowed_file: "third_party/tink/proto/ed25519.proto"
-allowed_file: "third_party/tink/proto/empty.proto"
-allowed_file: "third_party/tink/proto/hkdf_prf.proto"
-allowed_file: "third_party/tink/proto/hmac.proto"
-allowed_file: "third_party/tink/proto/hmac_prf.proto"
-allowed_file: "third_party/tink/proto/kms_aead.proto"
-allowed_file: "third_party/tink/proto/kms_envelope.proto"
-allowed_file: "third_party/tink/proto/rsa_ssa_pkcs1.proto"
-allowed_file: "third_party/tink/proto/rsa_ssa_pss.proto"
-allowed_file: "third_party/tink/proto/tink.proto"
-allowed_file: "third_party/tink/proto/xchacha20_poly1305.proto"
diff --git a/proto/prf_based_deriver.proto b/proto/prf_based_deriver.proto
index 06dd334..e58a2cd 100644
--- a/proto/prf_based_deriver.proto
+++ b/proto/prf_based_deriver.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/prf_based_deriver_go_proto";
+option go_package = "github.com/google/tink/go/proto/prf_based_deriver_go_proto";
 
 message PrfBasedDeriverParams {
   KeyTemplate derived_key_template = 1;
diff --git a/proto/rsa_ssa_pkcs1.proto b/proto/rsa_ssa_pkcs1.proto
index 961189d..9797ee0 100644
--- a/proto/rsa_ssa_pkcs1.proto
+++ b/proto/rsa_ssa_pkcs1.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 message RsaSsaPkcs1Params {
   // Hash function used in computing hash of the signing message
diff --git a/proto/rsa_ssa_pss.proto b/proto/rsa_ssa_pss.proto
index 8e7903f..1150057 100644
--- a/proto/rsa_ssa_pss.proto
+++ b/proto/rsa_ssa_pss.proto
@@ -25,7 +25,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto";
 
 message RsaSsaPssParams {
   // Hash function used in computing hash of the signing message
diff --git a/proto/tink.proto b/proto/tink.proto
index 1787581..8b3d100 100644
--- a/proto/tink.proto
+++ b/proto/tink.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/tink_go_proto";
+option go_package = "github.com/google/tink/go/proto/tink_go_proto";
 option objc_class_prefix = "TINKPB";
 
 // Each instantiation of a Tink primitive is identified by type_url,
diff --git a/proto/xchacha20_poly1305.proto b/proto/xchacha20_poly1305.proto
index cc52624..a2613f1 100644
--- a/proto/xchacha20_poly1305.proto
+++ b/proto/xchacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/xchacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto";
 
 message XChaCha20Poly1305KeyFormat {
   uint32 version = 1;
diff --git a/python/.bazelrc b/python/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/python/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/python/.bazelversion b/python/.bazelversion
index ac14c3d..09b254e 100644
--- a/python/.bazelversion
+++ b/python/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/python/MANIFEST.in b/python/MANIFEST.in
index 52501fe..40e4c64 100644
--- a/python/MANIFEST.in
+++ b/python/MANIFEST.in
@@ -1,7 +1,9 @@
 # Files needed for Bazel to build the C++ bindings when creating a package
 # distribution. Note that this file is used when creating distributions with
 # "python setup.py sdist" or "python setup.py bdist_wheel".
+include requirements.in
 include requirements.txt
+include .bazelrc
 include VERSION
 include WORKSPACE
 include BUILD.bazel
diff --git a/python/README.md b/python/README.md
index e2a0fa8..1eabed5 100644
--- a/python/README.md
+++ b/python/README.md
@@ -18,5 +18,5 @@
 For an overview of using Tink in Python, see https://developers.google.com/tink.
 
 In addition, there are illustrative [examples of using Tink
-Python](https://github.com/google/tink/tree/master/examples/python/) which can
+Python](https://github.com/google/tink/tree/master/python/examples/) which can
 used as a jumping off point.
diff --git a/python/WORKSPACE b/python/WORKSPACE
index 3a34ee1..ffaffec 100644
--- a/python/WORKSPACE
+++ b/python/WORKSPACE
@@ -5,16 +5,6 @@
     path = "../cc",
 )
 
-local_repository(
-    name = "tink_cc_awskms",
-    path = "../cc/integration/awskms",
-)
-
-local_repository(
-    name = "tink_cc_gcpkms",
-    path = "../cc/integration/gcpkms",
-)
-
 # Need to load rules_python earlier as proto uses an older version which is
 # incompatible with our Python implementation will load
 load("@tink_py//:tink_py_deps.bzl", "tink_py_deps")
@@ -32,15 +22,3 @@
 load("@tink_cc//:tink_cc_deps_init.bzl", "tink_cc_deps_init")
 
 tink_cc_deps_init()
-
-load("@tink_cc_awskms//:tink_cc_awskms_deps.bzl", "tink_cc_awskms_deps")
-
-tink_cc_awskms_deps()
-
-load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps.bzl", "tink_cc_gcpkms_deps")
-
-tink_cc_gcpkms_deps()
-
-load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps_init.bzl", "tink_cc_gcpkms_deps_init")
-
-tink_cc_gcpkms_deps_init()
diff --git a/python/examples/.bazelrc b/python/examples/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/python/examples/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/python/examples/.bazelversion b/python/examples/.bazelversion
index ac14c3d..09b254e 100644
--- a/python/examples/.bazelversion
+++ b/python/examples/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/python/examples/WORKSPACE b/python/examples/WORKSPACE
index 4a952e8..262ab6b 100644
--- a/python/examples/WORKSPACE
+++ b/python/examples/WORKSPACE
@@ -32,26 +32,6 @@
 #     strip_prefix = "tink-master/cc",
 # )
 
-local_repository(
-    name = "tink_cc_awskms",
-    path = "../../cc/integration/awskms",
-)
-# http_archive(
-#     name = "tink_cc_awskms",
-#     urls = ["https://github.com/google/tink/archive/master.zip"],
-#     strip_prefix = "tink-master/cc/integration/awskms",
-# )
-
-local_repository(
-    name = "tink_cc_gcpkms",
-    path = "../../cc/integration/gcpkms",
-)
-# http_archive(
-#     name = "tink_cc_gcpkms",
-#     urls = ["https://github.com/google/tink/archive/master.zip"],
-#     strip_prefix = "tink-master/cc/integration/gcpkms",
-# )
-
 # Load Tink dependencies.
 
 load("@tink_py//:tink_py_deps.bzl", "tink_py_deps")
@@ -70,21 +50,10 @@
 
 tink_cc_deps_init()
 
-load("@tink_cc_awskms//:tink_cc_awskms_deps.bzl", "tink_cc_awskms_deps")
-
-tink_cc_awskms_deps()
-
-load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps.bzl", "tink_cc_gcpkms_deps")
-
-tink_cc_gcpkms_deps()
-
-load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps_init.bzl", "tink_cc_gcpkms_deps_init")
-
-tink_cc_gcpkms_deps_init()
-
 load("@rules_python//python:pip.bzl", "pip_install")
 
 pip_install(
     name = "pip_deps",
+    quiet = False,
     requirements = "@examples_python//:requirements.txt",
 )
diff --git a/python/examples/aead/BUILD.bazel b/python/examples/aead/BUILD.bazel
index 3629892..57d84f1 100644
--- a/python/examples/aead/BUILD.bazel
+++ b/python/examples/aead/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/aead/aead.py b/python/examples/aead/aead.py
index a026f73..074806c 100644
--- a/python/examples/aead/aead.py
+++ b/python/examples/aead/aead.py
@@ -17,10 +17,6 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/aead/aead_basic.py b/python/examples/aead/aead_basic.py
index e721a37..87701d0 100644
--- a/python/examples/aead/aead_basic.py
+++ b/python/examples/aead/aead_basic.py
@@ -45,9 +45,9 @@
 
   # Create a keyset handle from the cleartext keyset in the previous
   # step. The keyset handle provides abstract access to the underlying keyset to
-  # limit the exposure of accessing the raw key material. WARNING: In practice
-  # it is unlikely you will want to use a cleartext_keyset_handle, as it implies
-  # that your key material is passed in cleartext which is a security risk.
+  # limit access of the raw key material. WARNING: In practice, it is unlikely
+  # you will want to use a cleartext_keyset_handle, as it implies that your key
+  # material is passed in cleartext, which is a security risk.
   keyset_handle = cleartext_keyset_handle.read(tink.JsonKeysetReader(keyset))
 
   # Retrieve the Aead primitive we want to use from the keyset handle.
diff --git a/python/examples/cleartext_keyset/BUILD.bazel b/python/examples/cleartext_keyset/BUILD.bazel
index 22e1aca..e108df4 100644
--- a/python/examples/cleartext_keyset/BUILD.bazel
+++ b/python/examples/cleartext_keyset/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/cleartext_keyset/cleartext_keyset.py b/python/examples/cleartext_keyset/cleartext_keyset.py
index f796986..e80ad94 100644
--- a/python/examples/cleartext_keyset/cleartext_keyset.py
+++ b/python/examples/cleartext_keyset/cleartext_keyset.py
@@ -17,10 +17,6 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from absl import app
 from absl import flags
 from absl import logging
@@ -55,7 +51,7 @@
     # Generate a new keyset
     try:
       key_template = aead.aead_key_templates.AES128_GCM
-      keyset_handle = tink.KeysetHandle.generate_new(key_template)
+      keyset_handle = tink.new_keyset_handle(key_template)
     except tink.TinkError as e:
       logging.exception('Error creating primitive: %s', e)
       return 1
diff --git a/python/examples/deterministic_aead/BUILD.bazel b/python/examples/deterministic_aead/BUILD.bazel
index 0624d62..c3c2e62 100644
--- a/python/examples/deterministic_aead/BUILD.bazel
+++ b/python/examples/deterministic_aead/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/deterministic_aead/deterministic_aead.py b/python/examples/deterministic_aead/deterministic_aead.py
index 89258cf..c79b114 100644
--- a/python/examples/deterministic_aead/deterministic_aead.py
+++ b/python/examples/deterministic_aead/deterministic_aead.py
@@ -17,10 +17,6 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/deterministic_aead/deterministic_aead_basic.py b/python/examples/deterministic_aead/deterministic_aead_basic.py
index d7e106d..45ba017 100644
--- a/python/examples/deterministic_aead/deterministic_aead_basic.py
+++ b/python/examples/deterministic_aead/deterministic_aead_basic.py
@@ -45,7 +45,7 @@
 
   # Create a keyset handle from the cleartext keyset in the previous
   # step. The keyset handle provides abstract access to the underlying keyset to
-  # limit the exposure of accessing the raw key material. WARNING: In practice
+  # limit the exposure of accessing the raw key material. WARNING: In practice,
   # it is unlikely you will want to use a cleartext_keyset_handle, as it implies
   # that your key material is passed in cleartext which is a security risk.
   keyset_handle = cleartext_keyset_handle.read(tink.JsonKeysetReader(keyset))
diff --git a/python/examples/encrypted_keyset/BUILD.bazel b/python/examples/encrypted_keyset/BUILD.bazel
index 5bfc54d..9416436 100644
--- a/python/examples/encrypted_keyset/BUILD.bazel
+++ b/python/examples/encrypted_keyset/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/encrypted_keyset/encrypted_keyset.py b/python/examples/encrypted_keyset/encrypted_keyset.py
index 3fb022d..13d07a9 100644
--- a/python/examples/encrypted_keyset/encrypted_keyset.py
+++ b/python/examples/encrypted_keyset/encrypted_keyset.py
@@ -14,10 +14,6 @@
 # [START encrypted-keyset-example]
 """A command-line utility for generating, encrypting and storing keysets."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from absl import app
 from absl import flags
 from absl import logging
@@ -68,7 +64,7 @@
   # Create an AEAD primitive from the key-encryption key (KEK) for encrypting
   # Tink keysets
   try:
-    handle = tink.KeysetHandle.generate_new(
+    handle = tink.new_keyset_handle(
         aead.aead_key_templates.create_kms_aead_key_template(
             key_uri=FLAGS.kek_uri))
     gcp_aead = handle.primitive(aead.Aead)
@@ -81,7 +77,7 @@
     # Generate a new keyset
     try:
       key_template = aead.aead_key_templates.AES128_GCM
-      keyset_handle = tink.KeysetHandle.generate_new(key_template)
+      keyset_handle = tink.new_keyset_handle(key_template)
     except tink.TinkError as e:
       logging.exception('Error creating primitive: %s', e)
       return 1
diff --git a/python/examples/envelope_aead/BUILD.bazel b/python/examples/envelope_aead/BUILD.bazel
index 3f0d23f..4ae74e3 100644
--- a/python/examples/envelope_aead/BUILD.bazel
+++ b/python/examples/envelope_aead/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/envelope_aead/envelope.py b/python/examples/envelope_aead/envelope.py
index 1d99671..3cb47d6 100644
--- a/python/examples/envelope_aead/envelope.py
+++ b/python/examples/envelope_aead/envelope.py
@@ -14,10 +14,6 @@
 # [START envelope-example]
 """A command-line utility for encrypting small files using envelope encryption with GCP."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from absl import app
 from absl import flags
 from absl import logging
@@ -67,7 +63,7 @@
     template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
         kek_uri=FLAGS.kek_uri,
         dek_template=aead.aead_key_templates.AES256_GCM)
-    handle = tink.KeysetHandle.generate_new(template)
+    handle = tink.new_keyset_handle(template)
     env_aead = handle.primitive(aead.Aead)
   except tink.TinkError as e:
     logging.error('Error creating primitive: %s', e)
diff --git a/python/examples/gcs/BUILD.bazel b/python/examples/gcs/BUILD.bazel
index 78cc5e1..e1e0654 100644
--- a/python/examples/gcs/BUILD.bazel
+++ b/python/examples/gcs/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
@@ -11,7 +11,6 @@
     python_version = "PY3",
     deps = [
         requirement("absl-py"),
-        requirement("google-cloud-core"),
         requirement("google-cloud-storage"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/integration/gcpkms",
diff --git a/python/examples/gcs/gcs_envelope_aead.py b/python/examples/gcs/gcs_envelope_aead.py
index d5497fd..3eacfa7 100644
--- a/python/examples/gcs/gcs_envelope_aead.py
+++ b/python/examples/gcs/gcs_envelope_aead.py
@@ -18,10 +18,6 @@
 facilitates ciphertexts stored in GCS.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from absl import app
 from absl import flags
 from absl import logging
@@ -72,7 +68,7 @@
     template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
         kek_uri=FLAGS.kek_uri,
         dek_template=aead.aead_key_templates.AES256_GCM)
-    handle = tink.KeysetHandle.generate_new(template)
+    handle = tink.new_keyset_handle(template)
     env_aead = handle.primitive(aead.Aead)
   except tink.TinkError as e:
     logging.exception('Error creating primitive: %s', e)
diff --git a/python/examples/hybrid/BUILD.bazel b/python/examples/hybrid/BUILD.bazel
index 082b162..0812d1c 100644
--- a/python/examples/hybrid/BUILD.bazel
+++ b/python/examples/hybrid/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
@@ -55,3 +55,23 @@
         ":hybrid_test_public_keyset.json",
     ],
 )
+
+py_library(
+    name = "hybrid_basic",
+    srcs = ["hybrid_basic.py"],
+    deps = [
+        "@tink_py//tink:cleartext_keyset_handle",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/hybrid",
+    ],
+)
+
+py_test(
+    name = "hybrid_basic_test",
+    srcs = ["hybrid_basic_test.py"],
+    python_version = "PY3",
+    deps = [
+        requirement("absl-py"),
+        ":hybrid_basic",
+    ],
+)
diff --git a/python/examples/hybrid/hybrid.py b/python/examples/hybrid/hybrid.py
index d0b5c41..24661e9 100644
--- a/python/examples/hybrid/hybrid.py
+++ b/python/examples/hybrid/hybrid.py
@@ -17,11 +17,6 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/hybrid/hybrid_basic.py b/python/examples/hybrid/hybrid_basic.py
new file mode 100644
index 0000000..f25b115
--- /dev/null
+++ b/python/examples/hybrid/hybrid_basic.py
@@ -0,0 +1,97 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A basic example for using the hybrid encryption API."""
+# [START hybrid-basic-example]
+import tink
+from tink import cleartext_keyset_handle
+from tink import hybrid
+
+
+def example():
+  """Encrypt and decrypt using hybrid encryption."""
+  # Register the hybrid encryption key managers. This is needed to create
+  # HybridEncrypt and HybridDecrypt primitives later.
+  hybrid.register()
+
+  # A private keyset created with
+  # tinkey create-keyset \
+  #   --key-template=DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM \
+  #   --out private_keyset.cfg
+  # Note that this keyset has the secret key information in cleartext.
+  private_keyset = r"""{
+      "key": [{
+          "keyData": {
+              "keyMaterialType":
+                  "ASYMMETRIC_PRIVATE",
+              "typeUrl":
+                  "type.googleapis.com/google.crypto.tink.HpkePrivateKey",
+              "value":
+                  "EioSBggBEAEYAhogVWQpmQoz74jcAp5WOD36KiBQ71MVCpn2iWfOzWLtKV4aINfn8qlMbyijNJcCzrafjsgJ493ZZGN256KTfKw0WN+p"
+          },
+          "keyId": 958452012,
+          "outputPrefixType": "TINK",
+          "status": "ENABLED"
+      }],
+      "primaryKeyId": 958452012
+  }"""
+
+  # The corresponding public keyset created with
+  # "tinkey create-public-keyset --in private_keyset.cfg"
+  public_keyset = r"""{
+      "key": [{
+          "keyData": {
+              "keyMaterialType":
+                  "ASYMMETRIC_PUBLIC",
+              "typeUrl":
+                  "type.googleapis.com/google.crypto.tink.HpkePublicKey",
+              "value":
+                  "EgYIARABGAIaIFVkKZkKM++I3AKeVjg9+iogUO9TFQqZ9olnzs1i7Sle"          },
+          "keyId": 958452012,
+          "outputPrefixType": "TINK",
+          "status": "ENABLED"
+      }],
+      "primaryKeyId": 958452012
+  }"""
+
+  # Create a keyset handle from the keyset containing the public key. Because
+  # this keyset does not contain any secrets, we can use
+  # `tink.read_no_secret_keyset_handle`.
+  public_keyset_handle = tink.read_no_secret_keyset_handle(
+      tink.JsonKeysetReader(public_keyset))
+
+  # Retrieve the HybridEncrypt primitive from the keyset handle.
+  enc_primitive = public_keyset_handle.primitive(hybrid.HybridEncrypt)
+
+  # Use enc_primitive to encrypt a message. In this case the primary key of the
+  # keyset will be used (which is also the only key in this example).
+  ciphertext = enc_primitive.encrypt(b'message', b'context_info')
+
+  # Create a keyset handle from the private keyset. The keyset handle provides
+  # abstract access to the underlying keyset to limit the exposure of accessing
+  # the raw key material. WARNING: In practice, it is unlikely you will want to
+  # use a cleartext_keyset_handle, as it implies that your key material is
+  # passed in cleartext which is a security risk.
+  private_keyset_handle = cleartext_keyset_handle.read(
+      tink.JsonKeysetReader(private_keyset)
+  )
+
+  # Retrieve the HybridDecrypt primitive from the private keyset handle.
+  dec_primitive = private_keyset_handle.primitive(hybrid.HybridDecrypt)
+
+  # Use dec_primitive to decrypt the message. Decrypt finds the correct key in
+  # the keyset and decrypts the ciphertext. If no key is found or decryption
+  # fails, it raises an error.
+  decrypted = dec_primitive.decrypt(ciphertext, b'context_info')
+  # [END hybrid-basic-example]
+  assert decrypted == b'message'
diff --git a/python/examples/hybrid/hybrid_basic_test.py b/python/examples/hybrid/hybrid_basic_test.py
new file mode 100644
index 0000000..a89095d
--- /dev/null
+++ b/python/examples/hybrid/hybrid_basic_test.py
@@ -0,0 +1,27 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for hybrid_basic."""
+
+from absl.testing import absltest
+import hybrid_basic
+
+
+class HybridEncryptionSimpleTest(absltest.TestCase):
+
+  def test_example_works(self):
+    hybrid_basic.example()
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/jwt/BUILD.bazel b/python/examples/jwt/BUILD.bazel
index 15b069e..f4275f1 100644
--- a/python/examples/jwt/BUILD.bazel
+++ b/python/examples/jwt/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/jwt/jwt_generate_public_jwk_set.py b/python/examples/jwt/jwt_generate_public_jwk_set.py
index e93eb30..cecd2cf 100644
--- a/python/examples/jwt/jwt_generate_public_jwk_set.py
+++ b/python/examples/jwt/jwt_generate_public_jwk_set.py
@@ -16,11 +16,6 @@
 """A utility for generating the public JWK set from the public keyset.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/jwt/jwt_sign.py b/python/examples/jwt/jwt_sign.py
index 5b8de06..7fda7f2 100644
--- a/python/examples/jwt/jwt_sign.py
+++ b/python/examples/jwt/jwt_sign.py
@@ -18,13 +18,8 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import datetime
 
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/jwt/jwt_verify.py b/python/examples/jwt/jwt_verify.py
index a1fbb05..991a9e2 100644
--- a/python/examples/jwt/jwt_verify.py
+++ b/python/examples/jwt/jwt_verify.py
@@ -15,13 +15,8 @@
 # [START python-jwt-signature-example]
 """A utility for verifying Json Web Tokens (JWT)."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import datetime
 
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/mac/BUILD.bazel b/python/examples/mac/BUILD.bazel
index bbc004f..bcfebe2 100644
--- a/python/examples/mac/BUILD.bazel
+++ b/python/examples/mac/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
@@ -48,3 +48,23 @@
         ":mac_test_keyset.json",
     ],
 )
+
+py_library(
+    name = "mac_basic",
+    srcs = ["mac_basic.py"],
+    deps = [
+        "@tink_py//tink:cleartext_keyset_handle",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/mac",
+    ],
+)
+
+py_test(
+    name = "mac_basic_test",
+    srcs = ["mac_basic_test.py"],
+    python_version = "PY3",
+    deps = [
+        requirement("absl-py"),
+        ":mac_basic",
+    ],
+)
diff --git a/python/examples/mac/mac.py b/python/examples/mac/mac.py
index 2837a40..94de762 100644
--- a/python/examples/mac/mac.py
+++ b/python/examples/mac/mac.py
@@ -17,13 +17,8 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import binascii
 
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
@@ -65,7 +60,7 @@
 
   # Get the primitive.
   try:
-    cipher = keyset_handle.primitive(mac.Mac)
+    primitive = keyset_handle.primitive(mac.Mac)
   except tink.TinkError as e:
     logging.error('Error creating primitive: %s', e)
     return 1
@@ -75,20 +70,20 @@
 
   if FLAGS.mode == 'compute':
     # Compute the MAC.
-    code = cipher.compute_mac(data)
+    tag = primitive.compute_mac(data)
     with open(FLAGS.mac_path, 'wb') as mac_file:
-      mac_file.write(binascii.hexlify(code))
+      mac_file.write(binascii.hexlify(tag))
     return 0
 
   with open(FLAGS.mac_path, 'rb') as mac_file:
     try:
-      expected_mac = binascii.unhexlify(mac_file.read().strip())
+      expected_tag = binascii.unhexlify(mac_file.read().strip())
     except binascii.Error as e:
-      logging.exception('Error reading expected code: %s', e)
+      logging.exception('Error reading expected tag: %s', e)
       return 1
 
   try:
-    cipher.verify_mac(expected_mac, data)
+    primitive.verify_mac(expected_tag, data)
     logging.info('MAC verification succeeded.')
     return 0
   except tink.TinkError as e:
diff --git a/python/examples/mac/mac_basic.py b/python/examples/mac/mac_basic.py
new file mode 100644
index 0000000..d114db2
--- /dev/null
+++ b/python/examples/mac/mac_basic.py
@@ -0,0 +1,65 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A minimal example for using the AEAD API."""
+# [START mac-basic-example]
+import tink
+from tink import cleartext_keyset_handle
+from tink import mac
+
+
+def example():
+  """Compute and verify MAC tags."""
+  # Register the MAC key managers. This is needed to create a Mac primitive
+  # later.
+  mac.register()
+
+  # Created with "tinkey create-keyset --key-template=HMAC_SHA256_128BITTAG".
+  # Note that this keyset has the secret key information in cleartext.
+  keyset = r"""{
+      "key": [{
+          "keyData": {
+              "keyMaterialType":
+                  "SYMMETRIC",
+              "typeUrl":
+                  "type.googleapis.com/google.crypto.tink.HmacKey",
+              "value":
+                  "EgQIAxAQGiA0LQjovcydWhVQV3k8W9ZSRkd7Ei4Y/TRWApE8guwV4Q=="
+          },
+          "keyId": 1892702217,
+          "outputPrefixType": "TINK",
+          "status": "ENABLED"
+      }],
+      "primaryKeyId": 1892702217
+  }"""
+
+  # Create a keyset handle from the cleartext keyset in the previous
+  # step. The keyset handle provides abstract access to the underlying keyset to
+  # limit access of the raw key material. WARNING: In practice, it is unlikely
+  # you will want to use a cleartext_keyset_handle, as it implies that your key
+  # material is passed in cleartext, which is a security risk.
+  keyset_handle = cleartext_keyset_handle.read(tink.JsonKeysetReader(keyset))
+
+  # Retrieve the Mac primitive we want to use from the keyset handle.
+  primitive = keyset_handle.primitive(mac.Mac)
+
+  # Use the primitive to compute the MAC for a message. In this case the primary
+  # key of the keyset will be used (which is also the only key in this example).
+  data = b'data'
+  tag = primitive.compute_mac(data)
+
+  # Use the primitive to verify the MAC for the message. Verify finds the
+  # correct key in the keyset and verifies the MAC. If no key is found or
+  # verification fails, it raises an error.
+  primitive.verify_mac(tag, data)
+  # [END mac-basic-example]
diff --git a/python/examples/mac/mac_basic_test.py b/python/examples/mac/mac_basic_test.py
new file mode 100644
index 0000000..beea166
--- /dev/null
+++ b/python/examples/mac/mac_basic_test.py
@@ -0,0 +1,26 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for mac_basic."""
+
+from absl.testing import absltest
+import mac_basic
+
+
+class MacBasicTest(absltest.TestCase):
+
+  def test_example_works(self):
+    mac_basic.example()
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/requirements.in b/python/examples/requirements.in
new file mode 100644
index 0000000..84e4d41
--- /dev/null
+++ b/python/examples/requirements.in
@@ -0,0 +1,2 @@
+absl-py>=1.3.0
+google-cloud-storage>=2.5.0
diff --git a/python/examples/requirements.txt b/python/examples/requirements.txt
index c664de6..e4e362a 100644
--- a/python/examples/requirements.txt
+++ b/python/examples/requirements.txt
@@ -1,3 +1,245 @@
-absl-py
-google-cloud-core
-google-cloud-storage
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+#    pip-compile --generate-hashes --output-file=examples/requirements.txt examples/requirements.in
+#
+absl-py==1.4.0 \
+    --hash=sha256:0d3fe606adfa4f7db64792dd4c7aee4ee0c38ab75dfd353b7a83ed3e957fcb47 \
+    --hash=sha256:d2c244d01048ba476e7c080bd2c6df5e141d211de80223460d5b3b8a2a58433d
+    # via -r examples/requirements.in
+cachetools==5.3.1 \
+    --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \
+    --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b
+    # via google-auth
+certifi==2023.5.7 \
+    --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \
+    --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716
+    # via requests
+charset-normalizer==3.1.0 \
+    --hash=sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6 \
+    --hash=sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1 \
+    --hash=sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e \
+    --hash=sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373 \
+    --hash=sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62 \
+    --hash=sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230 \
+    --hash=sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be \
+    --hash=sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c \
+    --hash=sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0 \
+    --hash=sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448 \
+    --hash=sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f \
+    --hash=sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649 \
+    --hash=sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d \
+    --hash=sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0 \
+    --hash=sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706 \
+    --hash=sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a \
+    --hash=sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59 \
+    --hash=sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23 \
+    --hash=sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5 \
+    --hash=sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb \
+    --hash=sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e \
+    --hash=sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e \
+    --hash=sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c \
+    --hash=sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28 \
+    --hash=sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d \
+    --hash=sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41 \
+    --hash=sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974 \
+    --hash=sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce \
+    --hash=sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f \
+    --hash=sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1 \
+    --hash=sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d \
+    --hash=sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8 \
+    --hash=sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017 \
+    --hash=sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31 \
+    --hash=sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7 \
+    --hash=sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8 \
+    --hash=sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e \
+    --hash=sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14 \
+    --hash=sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd \
+    --hash=sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d \
+    --hash=sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795 \
+    --hash=sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b \
+    --hash=sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b \
+    --hash=sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b \
+    --hash=sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203 \
+    --hash=sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f \
+    --hash=sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19 \
+    --hash=sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1 \
+    --hash=sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a \
+    --hash=sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac \
+    --hash=sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9 \
+    --hash=sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0 \
+    --hash=sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137 \
+    --hash=sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f \
+    --hash=sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6 \
+    --hash=sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5 \
+    --hash=sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909 \
+    --hash=sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f \
+    --hash=sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0 \
+    --hash=sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324 \
+    --hash=sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755 \
+    --hash=sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb \
+    --hash=sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854 \
+    --hash=sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c \
+    --hash=sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60 \
+    --hash=sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84 \
+    --hash=sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0 \
+    --hash=sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b \
+    --hash=sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1 \
+    --hash=sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531 \
+    --hash=sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1 \
+    --hash=sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11 \
+    --hash=sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326 \
+    --hash=sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df \
+    --hash=sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab
+    # via requests
+google-api-core==2.11.0 \
+    --hash=sha256:4b9bb5d5a380a0befa0573b302651b8a9a89262c1730e37bf423cec511804c22 \
+    --hash=sha256:ce222e27b0de0d7bc63eb043b956996d6dccab14cc3b690aaea91c9cc99dc16e
+    # via
+    #   google-cloud-core
+    #   google-cloud-storage
+google-auth==2.19.1 \
+    --hash=sha256:a9cfa88b3e16196845e64a3658eb953992129d13ac7337b064c6546f77c17183 \
+    --hash=sha256:ea165e014c7cbd496558796b627c271aa8c18b4cba79dc1cc962b24c5efdfb85
+    # via
+    #   google-api-core
+    #   google-cloud-core
+    #   google-cloud-storage
+google-cloud-core==2.3.2 \
+    --hash=sha256:8417acf6466be2fa85123441696c4badda48db314c607cf1e5d543fa8bdc22fe \
+    --hash=sha256:b9529ee7047fd8d4bf4a2182de619154240df17fbe60ead399078c1ae152af9a
+    # via google-cloud-storage
+google-cloud-storage==2.9.0 \
+    --hash=sha256:83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94 \
+    --hash=sha256:9b6ae7b509fc294bdacb84d0f3ea8e20e2c54a8b4bbe39c5707635fec214eff3
+    # via -r examples/requirements.in
+google-crc32c==1.5.0 \
+    --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+    --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+    --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+    --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+    --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+    --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+    --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+    --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+    --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+    --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+    --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+    --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+    --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+    --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+    --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+    --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+    --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+    --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+    --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+    --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+    --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+    --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+    --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+    --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+    --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+    --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+    --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+    --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+    --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+    --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+    --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+    --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+    --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+    --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+    --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+    --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+    --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+    --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+    --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+    --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+    --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+    --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+    --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+    --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+    --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+    --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+    --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+    --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+    --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+    --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+    --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+    --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+    --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+    --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+    --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+    --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+    --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+    --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+    --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+    --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+    --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+    --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+    --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+    --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+    --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+    --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+    --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+    --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+    # via google-resumable-media
+google-resumable-media==2.5.0 \
+    --hash=sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93 \
+    --hash=sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec
+    # via google-cloud-storage
+googleapis-common-protos==1.59.0 \
+    --hash=sha256:4168fcb568a826a52f23510412da405abd93f4d23ba544bb68d943b14ba3cb44 \
+    --hash=sha256:b287dc48449d1d41af0c69f4ea26242b5ae4c3d7249a38b0984c86a4caffff1f
+    # via google-api-core
+idna==3.4 \
+    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+    # via requests
+protobuf==4.23.2 \
+    --hash=sha256:09310bce43353b46d73ba7e3bca78273b9bc50349509b9698e64d288c6372c2a \
+    --hash=sha256:20874e7ca4436f683b64ebdbee2129a5a2c301579a67d1a7dda2cdf62fb7f5f7 \
+    --hash=sha256:25e3370eda26469b58b602e29dff069cfaae8eaa0ef4550039cc5ef8dc004511 \
+    --hash=sha256:281342ea5eb631c86697e1e048cb7e73b8a4e85f3299a128c116f05f5c668f8f \
+    --hash=sha256:384dd44cb4c43f2ccddd3645389a23ae61aeb8cfa15ca3a0f60e7c3ea09b28b3 \
+    --hash=sha256:54a533b971288af3b9926e53850c7eb186886c0c84e61daa8444385a4720297f \
+    --hash=sha256:6c081863c379bb1741be8f8193e893511312b1d7329b4a75445d1ea9955be69e \
+    --hash=sha256:86df87016d290143c7ce3be3ad52d055714ebaebb57cc659c387e76cfacd81aa \
+    --hash=sha256:8da6070310d634c99c0db7df48f10da495cc283fd9e9234877f0cd182d43ab7f \
+    --hash=sha256:b2cfab63a230b39ae603834718db74ac11e52bccaaf19bf20f5cce1a84cf76df \
+    --hash=sha256:c52cfcbfba8eb791255edd675c1fe6056f723bf832fa67f0442218f8817c076e \
+    --hash=sha256:ce744938406de1e64b91410f473736e815f28c3b71201302612a68bf01517fea \
+    --hash=sha256:efabbbbac1ab519a514579ba9ec52f006c28ae19d97915951f69fa70da2c9e91
+    # via
+    #   google-api-core
+    #   googleapis-common-protos
+pyasn1==0.5.0 \
+    --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \
+    --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde
+    # via
+    #   pyasn1-modules
+    #   rsa
+pyasn1-modules==0.3.0 \
+    --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \
+    --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d
+    # via google-auth
+requests==2.31.0 \
+    --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+    # via
+    #   google-api-core
+    #   google-cloud-storage
+rsa==4.9 \
+    --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
+    --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
+    # via google-auth
+six==1.16.0 \
+    --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+    --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+    # via google-auth
+urllib3==1.26.16 \
+    --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \
+    --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14
+    # via
+    #   google-auth
+    #   requests
diff --git a/python/examples/signature/BUILD.bazel b/python/examples/signature/BUILD.bazel
index a69e316..eedc111 100644
--- a/python/examples/signature/BUILD.bazel
+++ b/python/examples/signature/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/signature/signature.py b/python/examples/signature/signature.py
index 9350679..bfa7d82 100644
--- a/python/examples/signature/signature.py
+++ b/python/examples/signature/signature.py
@@ -19,13 +19,8 @@
 It loads cleartext keys from disk - this is not recommended!
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import binascii
 
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/signature/signature_basic.py b/python/examples/signature/signature_basic.py
index fe336a9..0b888f6 100644
--- a/python/examples/signature/signature_basic.py
+++ b/python/examples/signature/signature_basic.py
@@ -65,7 +65,7 @@
 
   # Create a keyset handle from the cleartext keyset in the previous
   # step. The keyset handle provides abstract access to the underlying keyset to
-  # limit the exposure of accessing the raw key material. WARNING: In practice
+  # limit the exposure of accessing the raw key material. WARNING: In practice,
   # it is unlikely you will want to use a cleartext_keyset_handle, as it implies
   # that your key material is passed in cleartext which is a security risk.
   private_keyset_handle = cleartext_keyset_handle.read(
@@ -79,10 +79,10 @@
   # keyset will be used (which is also the only key in this example).
   sig = sign_primitive.sign(b'msg')
 
-  # Create a keyset handle from the keyset containing the public key. Note that
-  # we could have also created `kh_public` directly using
-  # `kh_priv.public_keyset_handle()`.
-  public_keyset_handle = cleartext_keyset_handle.read(
+  # Create a keyset handle from the keyset containing the public key. Because
+  # this keyset does not contain any secrets, we can use
+  # `tink.read_no_secret_keyset_handle`.
+  public_keyset_handle = tink.read_no_secret_keyset_handle(
       tink.JsonKeysetReader(public_keyset))
 
   # Retrieve the PublicKeyVerify primitive we want to use from the keyset
@@ -93,4 +93,10 @@
   # Verify finds the correct key in the keyset. If no key is found or
   # verification fails, it raises an error.
   verify_primitive.verify(sig, b'msg')
+
+  # Note that we can also get the public keyset handle from the private keyset
+  # handle. The verification works the same as above.
+  public_keyset_handle2 = private_keyset_handle.public_keyset_handle()
+  verify_primitive2 = public_keyset_handle2.primitive(signature.PublicKeyVerify)
+  verify_primitive2.verify(sig, b'msg')
   # [END signature-basic-example]
diff --git a/python/examples/streaming_aead/BUILD.bazel b/python/examples/streaming_aead/BUILD.bazel
index e3bd7a2..575dd42 100644
--- a/python/examples/streaming_aead/BUILD.bazel
+++ b/python/examples/streaming_aead/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@rules_python//python:defs.bzl", "py_binary")
 load("@pip_deps//:requirements.bzl", "requirement")
 
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = ["//visibility:private"])
 
 licenses(["notice"])
 
diff --git a/python/examples/streaming_aead/streaming_aead.py b/python/examples/streaming_aead/streaming_aead.py
index 3adf617..16b5da3 100644
--- a/python/examples/streaming_aead/streaming_aead.py
+++ b/python/examples/streaming_aead/streaming_aead.py
@@ -28,13 +28,8 @@
     provided as a string.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from typing import BinaryIO
 
-# Special imports
 from absl import app
 from absl import flags
 from absl import logging
diff --git a/python/examples/testdata/README.md b/python/examples/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/python/examples/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/python/examples/testdata/aws/README.md b/python/examples/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/python/examples/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/python/examples/testdata/gcp/README.md b/python/examples/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/python/examples/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/python/examples/tink_py_examples_WORKSPACE b/python/examples/tink_py_examples_WORKSPACE
deleted file mode 100644
index 58b0c63..0000000
--- a/python/examples/tink_py_examples_WORKSPACE
+++ /dev/null
@@ -1,64 +0,0 @@
-workspace(name = "examples_python")
-
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-
-http_archive(
-    name = "tink_cc",
-    urls = ["https://github.com/tink-crypto/tink-cc/archive/master.zip"],
-    strip_prefix = "tink-cc-master",
-)
-
-http_archive(
-    name = "tink_cc_awskms",
-    urls = ["https://github.com/tink-crypto/tink-cc-awskms/archive/main.zip"],
-    strip_prefix = "tink-cc-awskms-main",
-)
-
-http_archive(
-    name = "tink_cc_gcpkms",
-    urls = ["https://github.com/tink-crypto/tink-cc-gcpkms/archive/main.zip"],
-    strip_prefix = "tink-cc-gcpkms-main",
-)
-
-http_archive(
-    name = "tink_py",
-    urls = ["https://github.com/tink-crypto/tink-py/archive/main.zip"],
-    strip_prefix = "tink-py-main",
-)
-
-# Load Tink dependencies.
-
-load("@tink_py//:tink_py_deps.bzl", "tink_py_deps")
-
-tink_py_deps()
-
-load("@tink_py//:tink_py_deps_init.bzl", "tink_py_deps_init")
-
-tink_py_deps_init("tink_py")
-
-load("@tink_cc//:tink_cc_deps.bzl", "tink_cc_deps")
-
-tink_cc_deps()
-
-load("@tink_cc//:tink_cc_deps_init.bzl", "tink_cc_deps_init")
-
-tink_cc_deps_init()
-
-load("@tink_cc_awskms//:tink_cc_awskms_deps.bzl", "tink_cc_awskms_deps")
-
-tink_cc_awskms_deps()
-
-load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps.bzl", "tink_cc_gcpkms_deps")
-
-tink_cc_gcpkms_deps()
-
-load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps_init.bzl", "tink_cc_gcpkms_deps_init")
-
-tink_cc_gcpkms_deps_init()
-
-load("@rules_python//python:pip.bzl", "pip_install")
-
-pip_install(
-    name = "pip_deps",
-    requirements = "@examples_python//:requirements.txt",
-)
diff --git a/python/examples/walkthrough/BUILD.bazel b/python/examples/walkthrough/BUILD.bazel
new file mode 100644
index 0000000..a66c971
--- /dev/null
+++ b/python/examples/walkthrough/BUILD.bazel
@@ -0,0 +1,136 @@
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+load("@pip_deps//:requirements.bzl", "requirement")
+
+package(default_visibility = ["//visibility:private"])
+
+licenses(["notice"])
+
+py_library(
+    name = "create_keyset",
+    srcs = ["create_keyset.py"],
+    deps = [
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_test(
+    name = "create_keyset_test",
+    srcs = ["create_keyset_test.py"],
+    python_version = "PY3",
+    deps = [
+        requirement("absl-py"),
+        ":create_keyset",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_library(
+    name = "load_cleartext_keyset",
+    srcs = ["load_cleartext_keyset.py"],
+    deps = [
+        "@tink_py//tink:cleartext_keyset_handle",
+        "@tink_py//tink:tink_python",
+    ],
+)
+
+py_test(
+    name = "load_cleartext_keyset_test",
+    srcs = ["load_cleartext_keyset_test.py"],
+    python_version = "PY3",
+    deps = [
+        requirement("absl-py"),
+        ":load_cleartext_keyset",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_library(
+    name = "load_encrypted_keyset",
+    srcs = ["load_encrypted_keyset.py"],
+    deps = [
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_test(
+    name = "load_encrypted_keyset_test",
+    srcs = ["load_encrypted_keyset_test.py"],
+    python_version = "PY3",
+    deps = [
+        requirement("absl-py"),
+        ":load_encrypted_keyset",
+        "@tink_py//tink:cleartext_keyset_handle",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/testing:fake_kms",
+    ],
+)
+
+py_library(
+    name = "obtain_and_use_a_primitive",
+    srcs = ["obtain_and_use_a_primitive.py"],
+    deps = [
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_test(
+    name = "obtain_and_use_a_primitive_test",
+    srcs = ["obtain_and_use_a_primitive_test.py"],
+    python_version = "PY3",
+    deps = [
+        requirement("absl-py"),
+        ":load_cleartext_keyset",
+        ":obtain_and_use_a_primitive",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_library(
+    name = "write_keyset",
+    srcs = ["write_keyset.py"],
+    deps = [
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_test(
+    name = "write_keyset_test",
+    srcs = ["write_keyset_test.py"],
+    deps = [
+        ":create_keyset",
+        ":load_encrypted_keyset",
+        ":write_keyset",
+        requirement("absl-py"),
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/testing:fake_kms",
+    ],
+)
+
+py_library(
+    name = "write_cleartext_keyset",
+    srcs = ["write_cleartext_keyset.py"],
+    deps = [
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink:cleartext_keyset_handle",
+    ],
+)
+
+py_test(
+    name = "write_cleartext_keyset_test",
+    srcs = ["write_cleartext_keyset_test.py"],
+    deps = [
+        ":create_keyset",
+        ":load_cleartext_keyset",
+        ":write_cleartext_keyset",
+        requirement("absl-py"),
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
diff --git a/python/examples/walkthrough/create_keyset.py b/python/examples/walkthrough/create_keyset.py
new file mode 100644
index 0000000..9b77d1d
--- /dev/null
+++ b/python/examples/walkthrough/create_keyset.py
@@ -0,0 +1,40 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Example to showcase how to create a keyset."""
+# [START tink_walkthrough_create_keyset]
+import tink
+from tink import aead
+
+
+def CreateAead128GcmKeyset() -> tink.KeysetHandle:
+  """Creates a keyset with a single AES128-GCM key and return a handle to it.
+
+  Prerequisites:
+    - Register AEAD implementations of Tink.
+
+  Returns:
+    A handle to the created keyset.
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  # Tink provides pre-baked templates. For example, we generate a key template
+  # for AES128-GCM.
+  key_template = aead.aead_key_templates.AES128_GCM
+  # This will generate a new keyset with only *one* key and return a keyset
+  # handle to it.
+  return tink.new_keyset_handle(key_template)
+
+
+# [END tink_walkthrough_create_keyset]
diff --git a/python/examples/walkthrough/create_keyset_test.py b/python/examples/walkthrough/create_keyset_test.py
new file mode 100644
index 0000000..8ff7949
--- /dev/null
+++ b/python/examples/walkthrough/create_keyset_test.py
@@ -0,0 +1,37 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test for create_keyset."""
+from absl.testing import absltest
+
+from tink import aead
+
+import create_keyset
+
+
+class CreateKeysetTest(absltest.TestCase):
+
+  def test_create_keyset_produces_a_valid_keyset(self):
+    aead.register()
+    keyset_handle = create_keyset.CreateAead128GcmKeyset()
+    # Make sure that we can use this primitive.
+    aead_primitive = keyset_handle.primitive(aead.Aead)
+    cleartext = b'Some cleartext'
+    associated_data = b'Some associated data'
+    ciphertext = aead_primitive.encrypt(cleartext, associated_data)
+    self.assertEqual(
+        aead_primitive.decrypt(ciphertext, associated_data), cleartext)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/walkthrough/load_cleartext_keyset.py b/python/examples/walkthrough/load_cleartext_keyset.py
new file mode 100644
index 0000000..d3d0847
--- /dev/null
+++ b/python/examples/walkthrough/load_cleartext_keyset.py
@@ -0,0 +1,42 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Example to showcase how to load a cleartext keyset."""
+# [START tink_walkthrough_load_cleartext_keyset]
+import tink
+
+from tink import cleartext_keyset_handle
+
+
+def LoadKeyset(serialized_keyset: str) -> tink.KeysetHandle:
+  r"""Loads a JSON-serialized unencrypted keyset and returns a KeysetHandle.
+
+  Prerequisites for this example:
+    - Create an plaintext keyset in JSON, for example, using Tinkey:
+
+      tinkey create-key --key-template AES256_GCM --out-format json \
+        --out keyset.json
+
+  Args:
+    serialized_keyset: JSON serialized keyset.
+
+  Returns:
+    A handle to the loaded keyset.
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  return cleartext_keyset_handle.read(tink.JsonKeysetReader(serialized_keyset))
+
+
+# [END tink_walkthrough_load_cleartext_keyset]
diff --git a/python/examples/walkthrough/load_cleartext_keyset_test.py b/python/examples/walkthrough/load_cleartext_keyset_test.py
new file mode 100644
index 0000000..995c8d0
--- /dev/null
+++ b/python/examples/walkthrough/load_cleartext_keyset_test.py
@@ -0,0 +1,60 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test for load_cleartext_keyset."""
+from absl.testing import absltest
+
+import tink
+
+from tink import aead
+
+import load_cleartext_keyset
+
+_AES_GCM_KEYSET = r"""{
+      "key": [{
+          "keyData": {
+              "keyMaterialType":
+                  "SYMMETRIC",
+              "typeUrl":
+                  "type.googleapis.com/google.crypto.tink.AesGcmKey",
+              "value":
+                  "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+          },
+          "keyId": 294406504,
+          "outputPrefixType": "TINK",
+          "status": "ENABLED"
+      }],
+      "primaryKeyId": 294406504
+  }"""
+
+
+class LoadCleartextKeysetTest(absltest.TestCase):
+
+  def test_load_cleartext_keyset_fails_if_keyset_is_invalid(self):
+    with self.assertRaises(tink.TinkError):
+      load_cleartext_keyset.LoadKeyset('Invlid keyset')
+
+  def test_load_cleartext_keyset_produces_a_valid_keyset(self):
+    aead.register()
+    keyset_handle = load_cleartext_keyset.LoadKeyset(_AES_GCM_KEYSET)
+    # Make sure that we can use this primitive.
+    aead_primitive = keyset_handle.primitive(aead.Aead)
+    plaintext = b'Some plaintext'
+    associated_data = b'Some associated data'
+    ciphertext = aead_primitive.encrypt(plaintext, associated_data)
+    self.assertEqual(
+        aead_primitive.decrypt(ciphertext, associated_data), plaintext)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/walkthrough/load_encrypted_keyset.py b/python/examples/walkthrough/load_encrypted_keyset.py
new file mode 100644
index 0000000..05232e3
--- /dev/null
+++ b/python/examples/walkthrough/load_encrypted_keyset.py
@@ -0,0 +1,60 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Example to showcase how to load an encrypted keyset."""
+# [START tink_walkthrough_load_encrypted_keyset]
+import tink
+
+from tink import aead
+
+
+def LoadEncryptedKeyset(json_serialized_encrypted_keyset: str, kms_key_uri: str,
+                        associated_data: bytes) -> tink.KeysetHandle:
+  r"""Loads a JSON-serialized keyset that was encrypted with a KMS.
+
+  Prerequisites for this example:
+  - Register AEAD implementations of Tink.
+  - Register a KMS client for the given URI prefix. Tink Python provides
+    awskms.AwsKmsClient.register_client() and
+    gcpkms.GcpKmsClient.register_client() for AWS-KMS and Google Cloud KMS
+    respectively.
+  - Create a KMS encrypted keyset, for example using Tinkey with Google Cloud
+    KMS:
+
+    tinkey create-keyset --key-template AES128_GCM \
+      --out-format json --out encrypted_aead_keyset.json \
+      --master-key-uri gcp-kms://<KMS key uri> \
+      --credentials gcp_credentials.json
+
+  Args:
+    json_serialized_encrypted_keyset: JSON serialized keyset.
+    kms_key_uri: The URI of the KMS key to use to decrypt the keyset.
+    associated_data: Associated data.
+
+  Returns:
+    A handle to the loaded keyset.
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  # To obtain a primitive that uses the KMS to encrypt/decrypt we simply create
+  # keyset from the appropriate template and get an AEAD primitive from it.
+  template = aead.aead_key_templates.create_kms_aead_key_template(kms_key_uri)
+  keyset_handle = tink.new_keyset_handle(template)
+  kms_aead = keyset_handle.primitive(aead.Aead)
+  return tink.read_keyset_handle_with_associated_data(
+      tink.JsonKeysetReader(json_serialized_encrypted_keyset), kms_aead,
+      associated_data)
+
+
+# [END tink_walkthrough_load_encrypted_keyset]
diff --git a/python/examples/walkthrough/load_encrypted_keyset_test.py b/python/examples/walkthrough/load_encrypted_keyset_test.py
new file mode 100644
index 0000000..0e011e7
--- /dev/null
+++ b/python/examples/walkthrough/load_encrypted_keyset_test.py
@@ -0,0 +1,121 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test for load_encrypted_keyset."""
+from absl.testing import absltest
+
+import tink
+
+from tink import aead
+from tink import cleartext_keyset_handle
+
+import load_encrypted_keyset
+from tink.testing import fake_kms
+
+_FAKE_KMS_AEAD_KEYSET = r"""{
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+}"""
+
+_KEYSET_TO_ENCRYPT = r"""{
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GhD+9l0RANZjzZEZ8PDp7LRW"
+      },
+      "keyId": 1931667682,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 1931667682
+}"""
+
+# Encryption of _KEYSET_TO_ENCRYPT with _FAKE_KMS_AEAD_KEYSET with no
+# associated data.
+_ENCRYPTED_KEYSET = r"""{
+  "encryptedKeyset": "ARGMSWi6YHyZ/Oqxl00XSq631a0q2UPmf+rCvCIAggSZrwCmxFF797MpY0dqgaXu1fz2eQ8zFNhlyTXv9kwg1kY6COpyhY/68zNBUkyKX4CharLYfpg1LgRl+6rMzIQa0XDHh7ZDmp1CevzecZIKnG83uDRHxxSv3h8c/Kc="
+}"""
+
+# Fake KMS keys are base64-encoded keysets. This was generated from
+# _FAKE_KMS_AEAD_KEYSET by first serializing it to bytes using a
+# tink.BinaryKeysetWriter, and then encoding it as base64.
+_FAKE_KMS_KEY_URI = (
+    'fake-kms://COiSsYwBEmQKWAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnR'
+    'pbmsuQWVzR2NtS2V5EiIaIFbJR8aBiTdFNGGP8shTNK50haXKMJ-0I7KlOvSMI1IuGAEQARjok'
+    'rGMASAB')
+
+
+class LoadEncryptedKeysetTest(absltest.TestCase):
+
+  def setUp(self):
+    super().setUp()
+    aead.register()
+    fake_kms.register_client()
+
+  def test_load_encrypted_keyset_fails_if_kms_key_is_invalid(self):
+    with self.assertRaises(tink.TinkError):
+      load_encrypted_keyset.LoadEncryptedKeyset(
+          _ENCRYPTED_KEYSET,
+          kms_key_uri='fake-kms://invalid-kms-key',
+          associated_data=b'')
+
+  def test_load_encrypted_keyset_fails_if_keyset_is_invalid(self):
+    with self.assertRaises(tink.TinkError):
+      load_encrypted_keyset.LoadEncryptedKeyset(
+          'Invalid keyset', _FAKE_KMS_KEY_URI, associated_data=b'')
+
+  def test_load_encrypted_keyset_returns_a_valid_keyset(self):
+    keyset_handle = load_encrypted_keyset.LoadEncryptedKeyset(
+        _ENCRYPTED_KEYSET, _FAKE_KMS_KEY_URI, associated_data=b'')
+
+    # Make sure that we can use this primitive.
+    aead_primitive = keyset_handle.primitive(aead.Aead)
+    plaintext = b'Some plaintext'
+    associated_data = b'Some associated data'
+    ciphertext = aead_primitive.encrypt(plaintext, associated_data)
+    self.assertEqual(
+        aead_primitive.decrypt(ciphertext, associated_data), plaintext)
+
+    # Make sure we can use the loaded keyset to decrypt a ciphertext encrypted
+    # with _KEYSET_TO_ENCRYPT.
+    expected_keyset_handle = cleartext_keyset_handle.read(
+        tink.JsonKeysetReader(_KEYSET_TO_ENCRYPT))
+    expected_aead = expected_keyset_handle.primitive(aead.Aead)
+    self.assertEqual(
+        aead_primitive.decrypt(
+            expected_aead.encrypt(plaintext, associated_data), associated_data),
+        plaintext)
+    # Make sure we can use _KEYSET_TO_ENCRYPT to decrypt the ciphertext produced
+    # by the keyset we loaded.
+    self.assertEqual(
+        expected_aead.decrypt(
+            aead_primitive.encrypt(plaintext, associated_data),
+            associated_data), plaintext)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/walkthrough/obtain_and_use_a_primitive.py b/python/examples/walkthrough/obtain_and_use_a_primitive.py
new file mode 100644
index 0000000..2eafb99
--- /dev/null
+++ b/python/examples/walkthrough/obtain_and_use_a_primitive.py
@@ -0,0 +1,75 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Example to showcase how to obtain and use a primitive from a keyset."""
+# [START tink_walkthrough_obtain_and_use_a_primitive]
+import tink
+from tink import aead
+
+
+def AeadEncrypt(keyset_handle: tink.KeysetHandle, plaintext: bytes,
+                associated_data: bytes) -> bytes:
+  """AEAD encrypts a plaintext with the primary key in keyset_handle.
+
+  Prerequisites for this example:
+   - Register AEAD implementations of Tink.
+   - Create a keyset and get a handle to it.
+
+  Args:
+    keyset_handle: Keyset handle containing at least an AEAD key.
+    plaintext: Plaintext to encrypt.
+    associated_data: Associated data.
+
+  Returns:
+    The resulting ciphertext
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  # To facilitate key rotation, `primitive` returns an Aead primitive that
+  # "wraps" multiple Aead primitives in the keyset. It uses the primary key to
+  # encrypt; For the key we use in this example, the first 5 bytes of the
+  # ciphertext contain the ID of the encryption key.
+  aead_primitive = keyset_handle.primitive(aead.Aead)
+  return aead_primitive.encrypt(plaintext, associated_data)
+
+
+def AeadDecrypt(keyset_handle: tink.KeysetHandle, ciphertext: bytes,
+                associated_data: bytes) -> bytes:
+  """AEAD decrypts a ciphertext with the corresponding key in keyset_handle.
+
+  Prerequisites for this example:
+   - Register AEAD implementations of Tink.
+   - Create a keyset and get a handle to it.
+   - Encrypt a plaintext with an AEAD primitive in keyset_handle.
+
+  Args:
+    keyset_handle: Keyset handle containing at least an AEAD key.
+    ciphertext: Tink ciphertext to decrypt.
+    associated_data: Associated data.
+
+  Returns:
+    The resulting ciphertext
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  # To facilitate key rotation, `primitive` returns an Aead primitive that
+  # "wraps" multiple Aead primitives in the keyset. In this example, it uses the
+  # key that was used to encrypt looking it up by key ID; the ID is contained in
+  # the first 5 bytes of the ciphertext.
+  aead_primitive = keyset_handle.primitive(aead.Aead)
+  return aead_primitive.decrypt(ciphertext, associated_data)
+
+
+# [END tink_walkthrough_obtain_and_use_a_primitive]
diff --git a/python/examples/walkthrough/obtain_and_use_a_primitive_test.py b/python/examples/walkthrough/obtain_and_use_a_primitive_test.py
new file mode 100644
index 0000000..1b4de33
--- /dev/null
+++ b/python/examples/walkthrough/obtain_and_use_a_primitive_test.py
@@ -0,0 +1,60 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test for obtain_and_use_a_primitive."""
+from absl.testing import absltest
+
+from tink import aead
+
+import load_cleartext_keyset
+import obtain_and_use_a_primitive
+
+_KMS_AEAD_KEY = r"""{
+  "key": [
+    {
+      "keyData": {
+        "keyMaterialType": "SYMMETRIC",
+        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+        "value": "GiBWyUfGgYk3RTRhj/LIUzSudIWlyjCftCOypTr0jCNSLg=="
+      },
+      "keyId": 294406504,
+      "outputPrefixType": "TINK",
+      "status": "ENABLED"
+    }
+  ],
+  "primaryKeyId": 294406504
+}"""
+
+
+class ObtainAndUseAPrimitiveTest(absltest.TestCase):
+
+  def setUp(self):
+    super().setUp()
+    aead.register()
+
+  def test_obtain_and_use_a_primitive_encrypt_decrypt(self):
+    keyset_handle = load_cleartext_keyset.LoadKeyset(_KMS_AEAD_KEY)
+
+    # Encrypt/decrypt.
+    plaintext = b'Some plaintext'
+    associated_data = b'Some associated data'
+    ciphertext = obtain_and_use_a_primitive.AeadEncrypt(keyset_handle,
+                                                        plaintext,
+                                                        associated_data)
+    self.assertEqual(
+        obtain_and_use_a_primitive.AeadDecrypt(keyset_handle, ciphertext,
+                                               associated_data), plaintext)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/walkthrough/write_cleartext_keyset.py b/python/examples/walkthrough/write_cleartext_keyset.py
new file mode 100644
index 0000000..99867d5
--- /dev/null
+++ b/python/examples/walkthrough/write_cleartext_keyset.py
@@ -0,0 +1,36 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Example to showcase how to serialize a keyset as a cleartext."""
+# [START tink_walkthrough_write_cleartext_keyset]
+from typing import TextIO
+
+import tink
+
+from tink import cleartext_keyset_handle
+
+
+def WriteKeyset(keyset: tink.KeysetHandle, text_io_stream: TextIO) -> None:
+  """Serializes a keyset to JSON-serialized and writes it to text_io_stream.
+
+  Args:
+    keyset: Handle to a keyset to serialize.
+    text_io_stream: I/O stream where writng the Keyset to.
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  cleartext_keyset_handle.write(tink.JsonKeysetWriter(text_io_stream), keyset)
+
+
+# [END tink_walkthrough_write_cleartext_keyset]
diff --git a/python/examples/walkthrough/write_cleartext_keyset_test.py b/python/examples/walkthrough/write_cleartext_keyset_test.py
new file mode 100644
index 0000000..8527b83
--- /dev/null
+++ b/python/examples/walkthrough/write_cleartext_keyset_test.py
@@ -0,0 +1,57 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test for write_cleartext_keyset."""
+import io
+
+from absl.testing import absltest
+
+from tink import aead
+
+import create_keyset
+import load_cleartext_keyset
+import write_cleartext_keyset
+
+
+class LoadCleartextKeysetTest(absltest.TestCase):
+
+  def test_write_cleartext_keyset_serializes_a_keyset_correctly(self):
+    aead.register()
+    keyset_handle = create_keyset.CreateAead128GcmKeyset()
+    string_io = io.StringIO()
+    write_cleartext_keyset.WriteKeyset(keyset_handle, string_io)
+
+    # Make sure that we can deserialize the keyset and use the contained
+    # primitive.
+    deserialized_keyset_handle = load_cleartext_keyset.LoadKeyset(
+        string_io.getvalue())
+    deserialized_aead_primitive = deserialized_keyset_handle.primitive(
+        aead.Aead)
+    aead_primitive = keyset_handle.primitive(aead.Aead)
+
+    plaintext = b'Some plaintext'
+    associated_data = b'Some associated data'
+
+    self.assertEqual(
+        deserialized_aead_primitive.decrypt(
+            aead_primitive.encrypt(plaintext, associated_data),
+            associated_data), plaintext)
+
+    self.assertEqual(
+        aead_primitive.decrypt(
+            deserialized_aead_primitive.encrypt(plaintext, associated_data),
+            associated_data), plaintext)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/examples/walkthrough/write_keyset.py b/python/examples/walkthrough/write_keyset.py
new file mode 100644
index 0000000..aef8cff
--- /dev/null
+++ b/python/examples/walkthrough/write_keyset.py
@@ -0,0 +1,58 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Example to showcase how to create a keyset."""
+# [START tink_walkthrough_write_keyset]
+from typing import TextIO
+
+import tink
+from tink import aead
+
+
+def GetKmsAead(kms_kek_uri: str) -> aead.Aead:
+  """Returns an AEAD primitive from a KMS Key Encryption Key URI."""
+  # To obtain a primitive that uses the KMS to encrypt/decrypt we simply create
+  # keyset from the appropriate template and get an AEAD primitive from it.
+  template = aead.aead_key_templates.create_kms_aead_key_template(kms_kek_uri)
+  kms_aead_keyset_handle = tink.new_keyset_handle(template)
+  return kms_aead_keyset_handle.primitive(aead.Aead)
+
+
+def WriteEncryptedKeyset(keyset_handle: tink.KeysetHandle,
+                         text_io_stream: TextIO,
+                         kms_kek_uri: str,
+                         associated_data: bytes = b'') -> None:
+  """Encrypts keyset_hanlde with a KMS and writes it to text_io_stream as JSON.
+
+  The keyset is encrypted with a KMS using the KMS key kms_kek_uri.
+
+  Prerequisites:
+    - Register AEAD implementations of Tink.
+    - Register a KMS client that can use kms_kek_uri.
+    - Create a keyset and obtain a handle to it.
+
+  Args:
+    keyset_handle: Keyset to write.
+    text_io_stream: I/O stream where writng the Keyset to.
+    kms_kek_uri: URI of the KMS key to use to encrypt the keyset.
+    associated_data: Associated data to which tie the ciphertext.
+
+  Raises:
+    tink.TinkError in case of errors.
+  """
+  keyset_handle.write_with_associated_data(
+      tink.JsonKeysetWriter(text_io_stream), GetKmsAead(kms_kek_uri),
+      associated_data)
+
+
+# [END tink_walkthrough_write_keyset]
diff --git a/python/examples/walkthrough/write_keyset_test.py b/python/examples/walkthrough/write_keyset_test.py
new file mode 100644
index 0000000..fbfd8c5
--- /dev/null
+++ b/python/examples/walkthrough/write_keyset_test.py
@@ -0,0 +1,79 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test for write_keyset."""
+import io
+
+from absl.testing import absltest
+import tink
+from tink import aead
+
+import create_keyset
+import load_encrypted_keyset
+import write_keyset
+
+from tink.testing import fake_kms
+
+# Fake KMS keys are base64-encoded keysets. This was generated from
+# an AEAD keyser by first serializing it to bytes using a
+# tink.BinaryKeysetWriter, and then encoding it as base64.
+_FAKE_KMS_KEY_URI = (
+    'fake-kms://COiSsYwBEmQKWAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnR'
+    'pbmsuQWVzR2NtS2V5EiIaIFbJR8aBiTdFNGGP8shTNK50haXKMJ-0I7KlOvSMI1IuGAEQARjok'
+    'rGMASAB')
+
+
+class CreateKeysetTest(absltest.TestCase):
+
+  def setUp(self):
+    super().setUp()
+    aead.register()
+    fake_kms.register_client()
+
+  def test_write_keyset_fails_if_kms_key_is_invalid(self):
+    keyset_handle = create_keyset.CreateAead128GcmKeyset()
+    text_io = io.StringIO()
+    with self.assertRaises(tink.TinkError):
+      write_keyset.WriteEncryptedKeyset(
+          keyset_handle,
+          text_io,
+          kms_kek_uri='fake-kms://invalid-kms-key',
+          associated_data=b'')
+
+  def test_write_keyset_serializes_a_keyset_correctly(self):
+    associated_data = b'some associated data'
+    keyset_handle = create_keyset.CreateAead128GcmKeyset()
+    text_io = io.StringIO()
+    write_keyset.WriteEncryptedKeyset(keyset_handle, text_io, _FAKE_KMS_KEY_URI,
+                                      associated_data)
+
+    # Make sure that we can use this primitive.
+    aead_primitive = keyset_handle.primitive(aead.Aead)
+
+    loaded_keyset_handle = load_encrypted_keyset.LoadEncryptedKeyset(
+        text_io.getvalue(), _FAKE_KMS_KEY_URI, associated_data)
+    loaded_aead = loaded_keyset_handle.primitive(aead.Aead)
+    plaintext = b'some plaintext'
+
+    self.assertEqual(
+        loaded_aead.decrypt(
+            aead_primitive.encrypt(plaintext, associated_data),
+            associated_data), plaintext)
+    self.assertEqual(
+        aead_primitive.decrypt(
+            loaded_aead.encrypt(plaintext, associated_data), associated_data),
+        plaintext)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/requirements.in b/python/requirements.in
new file mode 100644
index 0000000..5bc60d2
--- /dev/null
+++ b/python/requirements.in
@@ -0,0 +1,8 @@
+absl-py>=1.3.0
+protobuf>=4.21.9
+# AWS KMS extension.
+boto3>=1.26.89
+# GCP KMS extension.
+google-auth>=2.16.2
+google-api-core>=2.11.0
+google-cloud-kms>=2.16.1
diff --git a/python/requirements.txt b/python/requirements.txt
index a122c92..25064a2 100644
--- a/python/requirements.txt
+++ b/python/requirements.txt
@@ -1,2 +1,262 @@
-absl-py
-protobuf==3.20.1
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+#    pip-compile --generate-hashes --output-file=requirements.txt requirements.in
+#
+absl-py==1.4.0 \
+    --hash=sha256:0d3fe606adfa4f7db64792dd4c7aee4ee0c38ab75dfd353b7a83ed3e957fcb47 \
+    --hash=sha256:d2c244d01048ba476e7c080bd2c6df5e141d211de80223460d5b3b8a2a58433d
+    # via -r requirements.in
+boto3==1.26.152 \
+    --hash=sha256:a2778c5729d3dc0b3688c9f0d103543d7ec5ff44a4fd0e84d0d542e2dff05e62 \
+    --hash=sha256:ee0b8f8d238d4e1cf50fa6a185e4e066955b6105e9838a80b1b6582cd327dfdf
+    # via -r requirements.in
+botocore==1.29.152 \
+    --hash=sha256:02a3205cc8579d4be6d537e63d72aebbf3f70f3aedcf40b3cae9dc2e24c774d0 \
+    --hash=sha256:f6319ecdbe3d325878f837cac2874e461b4d90691bb2d2186f980bce3b3cfcc8
+    # via
+    #   boto3
+    #   s3transfer
+cachetools==5.3.1 \
+    --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \
+    --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b
+    # via google-auth
+certifi==2023.5.7 \
+    --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \
+    --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716
+    # via requests
+charset-normalizer==3.1.0 \
+    --hash=sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6 \
+    --hash=sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1 \
+    --hash=sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e \
+    --hash=sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373 \
+    --hash=sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62 \
+    --hash=sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230 \
+    --hash=sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be \
+    --hash=sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c \
+    --hash=sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0 \
+    --hash=sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448 \
+    --hash=sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f \
+    --hash=sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649 \
+    --hash=sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d \
+    --hash=sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0 \
+    --hash=sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706 \
+    --hash=sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a \
+    --hash=sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59 \
+    --hash=sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23 \
+    --hash=sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5 \
+    --hash=sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb \
+    --hash=sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e \
+    --hash=sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e \
+    --hash=sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c \
+    --hash=sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28 \
+    --hash=sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d \
+    --hash=sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41 \
+    --hash=sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974 \
+    --hash=sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce \
+    --hash=sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f \
+    --hash=sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1 \
+    --hash=sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d \
+    --hash=sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8 \
+    --hash=sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017 \
+    --hash=sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31 \
+    --hash=sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7 \
+    --hash=sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8 \
+    --hash=sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e \
+    --hash=sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14 \
+    --hash=sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd \
+    --hash=sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d \
+    --hash=sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795 \
+    --hash=sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b \
+    --hash=sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b \
+    --hash=sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b \
+    --hash=sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203 \
+    --hash=sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f \
+    --hash=sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19 \
+    --hash=sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1 \
+    --hash=sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a \
+    --hash=sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac \
+    --hash=sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9 \
+    --hash=sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0 \
+    --hash=sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137 \
+    --hash=sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f \
+    --hash=sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6 \
+    --hash=sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5 \
+    --hash=sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909 \
+    --hash=sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f \
+    --hash=sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0 \
+    --hash=sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324 \
+    --hash=sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755 \
+    --hash=sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb \
+    --hash=sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854 \
+    --hash=sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c \
+    --hash=sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60 \
+    --hash=sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84 \
+    --hash=sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0 \
+    --hash=sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b \
+    --hash=sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1 \
+    --hash=sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531 \
+    --hash=sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1 \
+    --hash=sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11 \
+    --hash=sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326 \
+    --hash=sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df \
+    --hash=sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab
+    # via requests
+google-api-core[grpc]==2.11.0 \
+    --hash=sha256:4b9bb5d5a380a0befa0573b302651b8a9a89262c1730e37bf423cec511804c22 \
+    --hash=sha256:ce222e27b0de0d7bc63eb043b956996d6dccab14cc3b690aaea91c9cc99dc16e
+    # via
+    #   -r requirements.in
+    #   google-cloud-kms
+google-auth==2.19.1 \
+    --hash=sha256:a9cfa88b3e16196845e64a3658eb953992129d13ac7337b064c6546f77c17183 \
+    --hash=sha256:ea165e014c7cbd496558796b627c271aa8c18b4cba79dc1cc962b24c5efdfb85
+    # via
+    #   -r requirements.in
+    #   google-api-core
+google-cloud-kms==2.17.0 \
+    --hash=sha256:0282281a63af2f49800f9761b16c48908140c381bed62f52c8d1780f9238173d \
+    --hash=sha256:af7136da8372eee1efd21789c1679ccc8f8b11bf52e4231ede808a90bcdc0368
+    # via -r requirements.in
+googleapis-common-protos[grpc]==1.59.1 \
+    --hash=sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e \
+    --hash=sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a
+    # via
+    #   google-api-core
+    #   grpc-google-iam-v1
+    #   grpcio-status
+grpc-google-iam-v1==0.12.6 \
+    --hash=sha256:2bc4b8fdf22115a65d751c9317329322602c39b7c86a289c9b72d228d960ef5f \
+    --hash=sha256:5c10f3d8dc2d88678ab1a9b0cb5482735c5efee71e6c0cd59f872eef22913f5c
+    # via google-cloud-kms
+grpcio==1.54.2 \
+    --hash=sha256:0212e2f7fdf7592e4b9d365087da30cb4d71e16a6f213120c89b4f8fb35a3ab3 \
+    --hash=sha256:09d4bfd84686cd36fd11fd45a0732c7628308d094b14d28ea74a81db0bce2ed3 \
+    --hash=sha256:1e623e0cf99a0ac114f091b3083a1848dbc64b0b99e181473b5a4a68d4f6f821 \
+    --hash=sha256:2288d76e4d4aa7ef3fe7a73c1c470b66ea68e7969930e746a8cd8eca6ef2a2ea \
+    --hash=sha256:2296356b5c9605b73ed6a52660b538787094dae13786ba53080595d52df13a98 \
+    --hash=sha256:2a1e601ee31ef30a9e2c601d0867e236ac54c922d32ed9f727b70dd5d82600d5 \
+    --hash=sha256:2be88c081e33f20630ac3343d8ad9f1125f32987968e9c8c75c051c9800896e8 \
+    --hash=sha256:33d40954199bddbb6a78f8f6f2b2082660f381cd2583ec860a6c2fa7c8400c08 \
+    --hash=sha256:40e1cbf69d6741b40f750f3cccc64326f927ac6145a9914d33879e586002350c \
+    --hash=sha256:46a057329938b08e5f0e12ea3d7aed3ecb20a0c34c4a324ef34e00cecdb88a12 \
+    --hash=sha256:4864f99aac207e3e45c5e26c6cbb0ad82917869abc2f156283be86c05286485c \
+    --hash=sha256:4c44e1a765b31e175c391f22e8fc73b2a2ece0e5e6ff042743d8109b5d2eff9f \
+    --hash=sha256:4cb283f630624ebb16c834e5ac3d7880831b07cbe76cb08ab7a271eeaeb8943e \
+    --hash=sha256:5008964885e8d23313c8e5ea0d44433be9bfd7e24482574e8cc43c02c02fc796 \
+    --hash=sha256:50a9f075eeda5097aa9a182bb3877fe1272875e45370368ac0ee16ab9e22d019 \
+    --hash=sha256:51630c92591d6d3fe488a7c706bd30a61594d144bac7dee20c8e1ce78294f474 \
+    --hash=sha256:5cc928cfe6c360c1df636cf7991ab96f059666ac7b40b75a769410cc6217df9c \
+    --hash=sha256:61f7203e2767800edee7a1e1040aaaf124a35ce0c7fe0883965c6b762defe598 \
+    --hash=sha256:66233ccd2a9371158d96e05d082043d47dadb18cbb294dc5accfdafc2e6b02a7 \
+    --hash=sha256:70fcac7b94f4c904152809a050164650ac81c08e62c27aa9f156ac518029ebbe \
+    --hash=sha256:714242ad0afa63a2e6dabd522ae22e1d76e07060b5af2ddda5474ba4f14c2c94 \
+    --hash=sha256:782f4f8662a2157c4190d0f99eaaebc602899e84fb1e562a944e5025929e351c \
+    --hash=sha256:7fc2b4edb938c8faa4b3c3ea90ca0dd89b7565a049e8e4e11b77e60e4ed2cc05 \
+    --hash=sha256:881d058c5ccbea7cc2c92085a11947b572498a27ef37d3eef4887f499054dca8 \
+    --hash=sha256:89dde0ac72a858a44a2feb8e43dc68c0c66f7857a23f806e81e1b7cc7044c9cf \
+    --hash=sha256:8cdbcbd687e576d48f7886157c95052825ca9948c0ed2afdc0134305067be88b \
+    --hash=sha256:8d6192c37a30a115f4663592861f50e130caed33efc4eec24d92ec881c92d771 \
+    --hash=sha256:96a41817d2c763b1d0b32675abeb9179aa2371c72aefdf74b2d2b99a1b92417b \
+    --hash=sha256:9bdbb7624d65dc0ed2ed8e954e79ab1724526f09b1efa88dcd9a1815bf28be5f \
+    --hash=sha256:9bf88004fe086c786dc56ef8dd6cb49c026833fdd6f42cb853008bce3f907148 \
+    --hash=sha256:a08920fa1a97d4b8ee5db2f31195de4a9def1a91bc003544eb3c9e6b8977960a \
+    --hash=sha256:a2f5a1f1080ccdc7cbaf1171b2cf384d852496fe81ddedeb882d42b85727f610 \
+    --hash=sha256:b04202453941a63b36876a7172b45366dc0cde10d5fd7855c0f4a4e673c0357a \
+    --hash=sha256:b38b3de8cff5bc70f8f9c615f51b48eff7313fc9aca354f09f81b73036e7ddfa \
+    --hash=sha256:b52d00d1793d290c81ad6a27058f5224a7d5f527867e5b580742e1bd211afeee \
+    --hash=sha256:b74ae837368cfffeb3f6b498688a123e6b960951be4dec0e869de77e7fa0439e \
+    --hash=sha256:be48496b0e00460717225e7680de57c38be1d8629dc09dadcd1b3389d70d942b \
+    --hash=sha256:c0e3155fc5335ec7b3b70f15230234e529ca3607b20a562b6c75fb1b1218874c \
+    --hash=sha256:c2392f5b5d84b71d853918687d806c1aa4308109e5ca158a16e16a6be71041eb \
+    --hash=sha256:c72956972e4b508dd39fdc7646637a791a9665b478e768ffa5f4fe42123d5de1 \
+    --hash=sha256:dc80c9c6b608bf98066a038e0172013a49cfa9a08d53335aefefda2c64fc68f4 \
+    --hash=sha256:e416c8baf925b5a1aff31f7f5aecc0060b25d50cce3a5a7255dc5cf2f1d4e5eb \
+    --hash=sha256:f8da84bbc61a4e92af54dc96344f328e5822d574f767e9b08e1602bb5ddc254a \
+    --hash=sha256:f900ed4ad7a0f1f05d35f955e0943944d5a75f607a836958c6b8ab2a81730ef2 \
+    --hash=sha256:fd6c6c29717724acf9fc1847c4515d57e4dc12762452457b9cb37461f30a81bb
+    # via
+    #   google-api-core
+    #   googleapis-common-protos
+    #   grpc-google-iam-v1
+    #   grpcio-status
+grpcio-status==1.54.2 \
+    --hash=sha256:2a7cb4838225f1b53bd0448a3008c5b5837941e1f3a0b13fa38768f08a7b68c2 \
+    --hash=sha256:3255cbec5b7c706caa3d4dd584606c080e6415e15631bb2f6215e2b70055836d
+    # via google-api-core
+idna==3.4 \
+    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+    # via requests
+jmespath==1.0.1 \
+    --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \
+    --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe
+    # via
+    #   boto3
+    #   botocore
+proto-plus==1.22.2 \
+    --hash=sha256:0e8cda3d5a634d9895b75c573c9352c16486cb75deb0e078b5fda34db4243165 \
+    --hash=sha256:de34e52d6c9c6fcd704192f09767cb561bb4ee64e70eede20b0834d841f0be4d
+    # via google-cloud-kms
+protobuf==4.23.2 \
+    --hash=sha256:09310bce43353b46d73ba7e3bca78273b9bc50349509b9698e64d288c6372c2a \
+    --hash=sha256:20874e7ca4436f683b64ebdbee2129a5a2c301579a67d1a7dda2cdf62fb7f5f7 \
+    --hash=sha256:25e3370eda26469b58b602e29dff069cfaae8eaa0ef4550039cc5ef8dc004511 \
+    --hash=sha256:281342ea5eb631c86697e1e048cb7e73b8a4e85f3299a128c116f05f5c668f8f \
+    --hash=sha256:384dd44cb4c43f2ccddd3645389a23ae61aeb8cfa15ca3a0f60e7c3ea09b28b3 \
+    --hash=sha256:54a533b971288af3b9926e53850c7eb186886c0c84e61daa8444385a4720297f \
+    --hash=sha256:6c081863c379bb1741be8f8193e893511312b1d7329b4a75445d1ea9955be69e \
+    --hash=sha256:86df87016d290143c7ce3be3ad52d055714ebaebb57cc659c387e76cfacd81aa \
+    --hash=sha256:8da6070310d634c99c0db7df48f10da495cc283fd9e9234877f0cd182d43ab7f \
+    --hash=sha256:b2cfab63a230b39ae603834718db74ac11e52bccaaf19bf20f5cce1a84cf76df \
+    --hash=sha256:c52cfcbfba8eb791255edd675c1fe6056f723bf832fa67f0442218f8817c076e \
+    --hash=sha256:ce744938406de1e64b91410f473736e815f28c3b71201302612a68bf01517fea \
+    --hash=sha256:efabbbbac1ab519a514579ba9ec52f006c28ae19d97915951f69fa70da2c9e91
+    # via
+    #   -r requirements.in
+    #   google-api-core
+    #   google-cloud-kms
+    #   googleapis-common-protos
+    #   grpc-google-iam-v1
+    #   grpcio-status
+    #   proto-plus
+pyasn1==0.5.0 \
+    --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \
+    --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde
+    # via
+    #   pyasn1-modules
+    #   rsa
+pyasn1-modules==0.3.0 \
+    --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \
+    --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d
+    # via google-auth
+python-dateutil==2.8.2 \
+    --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
+    --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
+    # via botocore
+requests==2.31.0 \
+    --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+    # via google-api-core
+rsa==4.9 \
+    --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
+    --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
+    # via google-auth
+s3transfer==0.6.1 \
+    --hash=sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346 \
+    --hash=sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9
+    # via boto3
+six==1.16.0 \
+    --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+    --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+    # via
+    #   google-auth
+    #   python-dateutil
+urllib3==1.26.16 \
+    --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \
+    --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14
+    # via
+    #   botocore
+    #   google-auth
+    #   requests
diff --git a/python/setup.py b/python/setup.py
index cbc3993..4c13a1a 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -17,7 +17,6 @@
 from __future__ import division
 from __future__ import print_function
 
-from distutils import spawn
 import glob
 import os
 import posixpath
@@ -28,6 +27,7 @@
 
 import setuptools
 from setuptools.command import build_ext
+from setuptools.command import sdist
 
 
 _PROJECT_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -51,9 +51,9 @@
 
 def _get_bazel_command():
   """Finds the bazel command."""
-  if spawn.find_executable('bazelisk'):
+  if shutil.which('bazelisk'):
     return 'bazelisk'
-  elif spawn.find_executable('bazel'):
+  elif shutil.which('bazel'):
     return 'bazel'
   raise FileNotFoundError('Could not find bazel executable. Please install '
                           'bazel to compile the Tink Python package.')
@@ -63,10 +63,11 @@
   """Finds the protoc command."""
   if 'PROTOC' in os.environ and os.path.exists(os.environ['PROTOC']):
     return os.environ['PROTOC']
-  else:
-    return spawn.find_executable('protoc')
-  raise FileNotFoundError('Could not find protoc executable. Please install '
-                          'protoc to compile the Tink Python package.')
+  protoc_path = shutil.which('protoc')
+  if protoc_path is None:
+    raise FileNotFoundError('Could not find protoc executable. Please install '
+                            'protoc to compile the Tink Python package.')
+  return protoc_path
 
 
 def _generate_proto(protoc, source):
@@ -96,14 +97,19 @@
     ]
 
 
-def _patch_workspace(workspace_content):
+def _patch_workspace(workspace_file):
   """Update the Bazel workspace with valid repository references.
 
-  setuptools builds in a temporary folder which breaks the relative paths
-  defined in the Bazel workspace.  By default, the local_repository() rules will
-  be replaced with http_archive() rules which contain URLs that point to an
-  archive of the Tink GitHub repository as of the latest commit as of the master
-  branch.
+  When installing the sdist, e.g., with `pip install tink --no-binary` or
+  `python3 -m pip install -v path/to/sdist.tar.gz`, setuptools unpacks the
+  sdist in a temporary folder that contains only the python/ folder, and then
+  builds it. As a consequence, relative local_repository paths that are set by
+  default in python/WORKSPACE don't exist (or worst, they may exist by
+  chance!).
+
+  By default, the local_repository() rules will be replaced with http_archive()
+  rules which contain URLs that point to an archive of the Tink GitHub
+  repository as of the latest commit as of the master branch.
 
   This behavior can be modified via the following environment variables, in
   order of precedence:
@@ -119,50 +125,50 @@
         for creating release artifacts).
 
   Args:
-    workspace_content: The original tink/python WORKSPACE.
-  Returns:
-    The workspace_content using http_archive for tink_cc.
+    workspace_file: The tink/python WORKSPACE.
   """
 
+  with open(workspace_file, 'r') as f:
+    workspace_content = f.read()
+
   if 'TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH' in os.environ:
     base_path = os.environ['TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH']
-    return _patch_with_local_path(workspace_content, base_path)
+    workspace_content = _patch_with_local_path(workspace_content, base_path)
 
-  if 'TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION' in os.environ:
-    archive_filename = 'v{}.zip'.format(_TINK_VERSION)
-    archive_prefix = 'tink-{}'.format(_TINK_VERSION)
-    return _patch_with_http_archive(workspace_content,
-                                    archive_filename, archive_prefix)
+  elif 'TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION' in os.environ:
+    tagged_version = os.environ['TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION']
+    archive_filename = 'v{}.zip'.format(tagged_version)
+    archive_prefix = 'tink-{}'.format(tagged_version)
+    workspace_content = _patch_with_http_archive(workspace_content,
+                                                 archive_filename,
+                                                 archive_prefix)
+  else:
+    workspace_content = _patch_with_http_archive(workspace_content,
+                                                 'master.zip', 'tink-master')
 
-  return _patch_with_http_archive(workspace_content,
-                                  'master.zip', 'tink-master')
+  with open(workspace_file, 'w') as f:
+    f.write(workspace_content)
 
 
 def _patch_with_local_path(workspace_content, base_path):
   """Replaces the base paths in the local_repository() rules."""
-
-  workspace_content = re.sub(r'(?<="tink_cc",\n    path = ").*(?=\n)',
-                             base_path + '/cc",  # Modified by setup.py',
-                             workspace_content)
-  workspace_content = re.sub(
-      r'(?<="tink_cc_awskms",\n    path = ").*(?=\n)',
-      base_path + '/cc/integration/awskms",  # Modified by setup.py',
-      workspace_content)
-  workspace_content = re.sub(
-      r'(?<="tink_cc_gcpkms",\n    path = ").*(?=\n)',
-      base_path + '/cc/integration/gcpkms",  # Modified by setup.py',
-      workspace_content)
-  return workspace_content
+  return re.sub(
+      r'(?<="tink_cc",\n    path = ").*(?=\n)',
+      base_path + '/cc",  # Modified by setup.py',
+      workspace_content,
+  )
 
 
 def _patch_with_http_archive(workspace_content, filename, prefix):
   """Replaces local_repository() rules with http_archive() rules."""
 
   workspace_lines = workspace_content.split('\n')
-  http_archive_load = ('load("@bazel_tools//tools/build_defs/repo:http.bzl", '
-                       '"http_archive")')
-  workspace_content = '\n'.join([workspace_lines[0], http_archive_load] +
-                                workspace_lines[1:])
+  http_archive_load = (
+      'load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")')
+
+  if http_archive_load not in workspace_content:
+    workspace_content = '\n'.join([workspace_lines[0], http_archive_load] +
+                                  workspace_lines[1:])
 
   cc = textwrap.dedent(
       '''\
@@ -172,22 +178,6 @@
       )
       ''')
 
-  cc_awskms = textwrap.dedent(
-      '''\
-      local_repository(
-          name = "tink_cc_awskms",
-          path = "../cc/integration/awskms",
-      )
-      ''')
-
-  cc_gcpkms = textwrap.dedent(
-      '''\
-      local_repository(
-          name = "tink_cc_gcpkms",
-          path = "../cc/integration/gcpkms",
-      )
-      ''')
-
   cc_patched = textwrap.dedent(
       '''\
       # Modified by setup.py
@@ -198,30 +188,7 @@
       )
       '''.format(filename, prefix))
 
-  cc_awskms_patched = textwrap.dedent(
-      '''\
-      # Modified by setup.py
-      http_archive(
-          name = "tink_cc_awskms",
-          urls = ["https://github.com/google/tink/archive/{}"],
-          strip_prefix = "{}/cc/integration/awskms",
-      )
-      '''.format(filename, prefix))
-
-  cc_gcpkms_patched = textwrap.dedent(
-      '''\
-      # Modified by setup.py
-      http_archive(
-          name = "tink_cc_gcpkms",
-          urls = ["https://github.com/google/tink/archive/{}"],
-          strip_prefix = "{}/cc/integration/gcpkms",
-      )
-      '''.format(filename, prefix))
-
-  workspace_content = workspace_content.replace(cc, cc_patched)
-  workspace_content = workspace_content.replace(cc_awskms, cc_awskms_patched)
-  workspace_content = workspace_content.replace(cc_gcpkms, cc_gcpkms_patched)
-  return workspace_content
+  return workspace_content.replace(cc, cc_patched)
 
 
 class BazelExtension(setuptools.Extension):
@@ -252,10 +219,7 @@
 
   def bazel_build(self, ext):
     # Change WORKSPACE to include tink_cc from an archive
-    with open('WORKSPACE', 'r') as f:
-      workspace_contents = f.read()
-    with open('WORKSPACE', 'w') as f:
-      f.write(_patch_workspace(workspace_contents))
+    _patch_workspace('WORKSPACE')
 
     if not os.path.exists(self.build_temp):
       os.makedirs(self.build_temp)
@@ -267,10 +231,7 @@
 
     bazel_argv = [
         self.bazel_command, 'build', ext.bazel_target,
-        '--compilation_mode=' + ('dbg' if self.debug else 'opt'),
-        '--incompatible_linkopts_to_linklibs'
-        # TODO(https://github.com/bazelbuild/bazel/issues/9254): Remove linkopts
-        # flag when issue is fixed.
+        '--compilation_mode=' + ('dbg' if self.debug else 'opt')
     ]
     self.spawn(bazel_argv)
     ext_bazel_bin_path = os.path.join('bazel-bin', ext.relpath,
@@ -282,6 +243,14 @@
     shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
 
 
+class SdistCmd(sdist.sdist):
+  """A command that patches the workspace before creating an sdist."""
+
+  def run(self):
+    _patch_workspace('WORKSPACE')
+    sdist.sdist.run(self)
+
+
 def main():
   # Generate compiled protocol buffers.
   protoc_command = _get_protoc_command()
@@ -301,8 +270,11 @@
       long_description_content_type='text/markdown',
       # Contained modules and scripts.
       packages=setuptools.find_packages(),
-      install_requires=_parse_requirements('requirements.txt'),
-      cmdclass=dict(build_ext=BuildBazelExtension),
+      install_requires=_parse_requirements('requirements.in'),
+      cmdclass={
+          'build_ext': BuildBazelExtension,
+          'sdist': SdistCmd,
+      },
       ext_modules=[
           BazelExtension('//tink/cc/pybind:tink_bindings'),
       ],
@@ -312,6 +284,7 @@
           'Programming Language :: Python :: 3.7',
           'Programming Language :: Python :: 3.8',
           'Programming Language :: Python :: 3.9',
+          'Programming Language :: Python :: 3.10',
           'Topic :: Software Development :: Libraries',
       ],
       license='Apache 2.0',
diff --git a/python/testdata/README.md b/python/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/python/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/python/testdata/aws/README.md b/python/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/python/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/python/testdata/gcp/README.md b/python/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/python/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/python/tink/_keyset_reader_test.py b/python/tink/_keyset_reader_test.py
index ecad6bb..8f5c6b5 100644
--- a/python/tink/_keyset_reader_test.py
+++ b/python/tink/_keyset_reader_test.py
@@ -51,6 +51,49 @@
     with self.assertRaises(core.TinkError):
       reader.read()
 
+  def test_read_rejects_negative_key_id(self):
+    json_keyset = """
+        {
+          "primaryKeyId": -42,
+          "key": [
+            {
+              "keyData": {
+                "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+                "keyMaterialType": "SYMMETRIC",
+                "value": "GhCS/1+ejWpx68NfGt6ziYHd"
+              },
+              "outputPrefixType": "TINK",
+              "keyId": -42,
+              "status": "ENABLED"
+            }
+          ]
+        }"""
+    reader = tink.JsonKeysetReader(json_keyset)
+    with self.assertRaises(core.TinkError):
+      reader.read()
+
+  def test_read_rejects_key_id_larger_than_uint32(self):
+    # 4294967296 = 2^32, which is too large for uint32.
+    json_keyset = """
+        {
+          "primaryKeyId": 4294967296,
+          "key": [
+            {
+              "keyData": {
+                "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
+                "keyMaterialType": "SYMMETRIC",
+                "value": "GhCS/1+ejWpx68NfGt6ziYHd"
+              },
+              "outputPrefixType": "TINK",
+              "keyId": 4294967296,
+              "status": "ENABLED"
+            }
+          ]
+        }"""
+    reader = tink.JsonKeysetReader(json_keyset)
+    with self.assertRaises(core.TinkError):
+      reader.read()
+
   def test_read_encrypted(self):
     # encryptedKeyset is a base64-encoding of 'some ciphertext with keyset'
     json_encrypted_keyset = """
diff --git a/python/tink/aead/BUILD.bazel b/python/tink/aead/BUILD.bazel
index ad99da8..9753f08 100644
--- a/python/tink/aead/BUILD.bazel
+++ b/python/tink/aead/BUILD.bazel
@@ -14,6 +14,7 @@
         ":_aead",
         ":_aead_key_manager",
         ":_aead_key_templates",
+        ":_kms_aead_key_manager",
         ":_kms_envelope_aead",
     ],
 )
@@ -31,6 +32,7 @@
     deps = [
         ":_aead",
         ":_aead_wrapper",
+        ":_kms_aead_key_manager",
         "//tink/cc/pybind:tink_bindings",
         "//tink/core",
     ],
@@ -43,8 +45,10 @@
     deps = [
         ":aead",
         requirement("absl-py"),
+        "//tink:cleartext_keyset_handle",
         "//tink:tink_python",
         "//tink/core",
+        "//tink/mac",
         "//tink/proto:aes_ctr_hmac_aead_py_pb2",
         "//tink/proto:aes_eax_py_pb2",
         "//tink/proto:aes_gcm_py_pb2",
@@ -52,6 +56,7 @@
         "//tink/proto:common_py_pb2",
         "//tink/proto:tink_py_pb2",
         "//tink/proto:xchacha20_poly1305_py_pb2",
+        "//tink/testing:fake_kms",
     ],
 )
 
@@ -133,6 +138,20 @@
         requirement("absl-py"),
         "//tink:tink_python",
         "//tink/core",
+        "//tink/mac",
         "//tink/proto:aes_gcm_py_pb2",
     ],
 )
+
+py_library(
+    name = "_kms_aead_key_manager",
+    srcs = ["_kms_aead_key_manager.py"],
+    deps = [
+        ":_aead",
+        ":_kms_envelope_aead",
+        "//tink/core",
+        "//tink/proto:kms_aead_py_pb2",
+        "//tink/proto:kms_envelope_py_pb2",
+        "//tink/proto:tink_py_pb2",
+    ],
+)
diff --git a/python/tink/aead/_aead_key_manager.py b/python/tink/aead/_aead_key_manager.py
index c355134..ddb9d85 100644
--- a/python/tink/aead/_aead_key_manager.py
+++ b/python/tink/aead/_aead_key_manager.py
@@ -17,6 +17,7 @@
 from tink import core
 from tink.aead import _aead
 from tink.aead import _aead_wrapper
+from tink.aead import _kms_aead_key_manager
 from tink.cc.pybind import tink_bindings
 
 
@@ -44,8 +45,6 @@
       'AesGcmSivKey',
       'AesEaxKey',
       'XChaCha20Poly1305Key',
-      'KmsAeadKey',
-      'KmsEnvelopeAeadKey',
   ):
     type_url = 'type.googleapis.com/google.crypto.tink.{}'.format(ident)
     key_manager = core.KeyManagerCcToPyWrapper(
@@ -53,3 +52,9 @@
         AeadCcToPyWrapper)
     core.Registry.register_key_manager(key_manager, new_key_allowed=True)
   core.Registry.register_primitive_wrapper(_aead_wrapper.AeadWrapper())
+  core.Registry.register_key_manager(
+      _kms_aead_key_manager.KmsAeadKeyManager(), new_key_allowed=True
+  )
+  core.Registry.register_key_manager(
+      _kms_aead_key_manager.KmsEnvelopeAeadKeyManager(), new_key_allowed=True
+  )
diff --git a/python/tink/aead/_aead_key_manager_test.py b/python/tink/aead/_aead_key_manager_test.py
index e922421..ec8671d 100644
--- a/python/tink/aead/_aead_key_manager_test.py
+++ b/python/tink/aead/_aead_key_manager_test.py
@@ -25,11 +25,18 @@
 from tink.proto import xchacha20_poly1305_pb2
 import tink
 from tink import aead
+from tink import cleartext_keyset_handle
 from tink import core
+from tink import mac
+from tink.testing import fake_kms
+
+
+FAKE_KMS_URI = 'fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE'
 
 
 def setUpModule():
   aead.register()
+  fake_kms.register_client()
 
 
 class AeadKeyManagerTest(parameterized.TestCase):
@@ -139,6 +146,106 @@
     ciphertext = primitive.encrypt(plaintext, associated_data)
     self.assertEqual(primitive.decrypt(ciphertext, associated_data), plaintext)
 
+  def test_kms_aead_encrypt_decrypt_success(self):
+    template = aead.aead_key_templates.create_kms_aead_key_template(
+        key_uri=FAKE_KMS_URI)
+    keyset_handle = tink.new_keyset_handle(template)
+    primitive = keyset_handle.primitive(aead.Aead)
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+    ciphertext = primitive.encrypt(plaintext, associated_data)
+    self.assertEqual(primitive.decrypt(ciphertext, associated_data), plaintext)
+
+  def test_kms_aead_with_unknown_key_uri_fails(self):
+    template = aead.aead_key_templates.create_kms_aead_key_template(
+        key_uri='unknown-kms://key_uri')
+    handle = tink.new_keyset_handle(template)
+    with self.assertRaises(tink.TinkError):
+      handle.primitive(aead.Aead)
+
+  def test_kms_envelope_aead_encrypt_decrypt_success(self):
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri=FAKE_KMS_URI, dek_template=aead.aead_key_templates.AES128_GCM
+    )
+    keyset_handle = tink.new_keyset_handle(template)
+    primitive = keyset_handle.primitive(aead.Aead)
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+    ciphertext = primitive.encrypt(plaintext, associated_data)
+    self.assertEqual(primitive.decrypt(ciphertext, associated_data), plaintext)
+
+  def test_kms_envelope_aead_with_unknown_key_uri_fails(self):
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri='unknown-kms://key_uri',
+        dek_template=aead.aead_key_templates.AES128_GCM,
+    )
+    handle = tink.new_keyset_handle(template)
+    with self.assertRaises(tink.TinkError):
+      handle.primitive(aead.Aead)
+
+  def test_kms_envelope_aead_with_invalid_dek_template_fails(self):
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri=FAKE_KMS_URI,
+        dek_template=mac.mac_key_templates.HMAC_SHA256_128BITTAG,
+    )
+    with self.assertRaises(tink.TinkError):
+      _ = tink.new_keyset_handle(template)
+
+  def test_kms_envelope_aead_with_envelope_template_as_dek_template_fails(self):
+    env_template = (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            kek_uri=FAKE_KMS_URI,
+            dek_template=aead.aead_key_templates.AES128_GCM,
+        )
+    )
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri=FAKE_KMS_URI,
+        dek_template=env_template,
+    )
+    with self.assertRaises(tink.TinkError):
+      _ = tink.new_keyset_handle(template)
+
+  def test_kms_envelope_aead_with_kms_template_as_dek_template_fails(self):
+    kms_template = aead.aead_key_templates.create_kms_aead_key_template(
+        key_uri=FAKE_KMS_URI,
+    )
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri=FAKE_KMS_URI,
+        dek_template=kms_template,
+    )
+    with self.assertRaises(tink.TinkError):
+      _ = tink.new_keyset_handle(template)
+
+  def test_kms_envelope_aead_decrypt_fixed_ciphertext_success(self):
+    # This keyset contains a single KmsEnvelopeAeadKey with
+    # kek_uri = FAKE_KMS_URI and dek_template = AES128_GCM.
+    json_keyset = '''{
+      "primaryKeyId": 371374440,
+      "key": [
+        {
+          "keyData": {
+            "typeUrl": "type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey",
+            "value": "EsMBEjgYARICEBAKMHR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLmNyeXB0by50aW5rLkFlc0djbUtleQqGAWZha2Uta21zOi8vQ00yYjNfTURFbFFLU0Fvd2RIbHdaUzVuYjI5bmJHVmhjR2x6TG1OdmJTOW5iMjluYkdVdVkzSjVjSFJ2TG5ScGJtc3VRV1Z6UjJOdFMyVjVFaElhRUlLNzV0NUwtYWRsVXdWaFd2UnVXVXdZQVJBQkdNMmIzX01ESUFF",
+            "keyMaterialType": "REMOTE"
+          },
+          "status": "ENABLED",
+          "keyId": 371374440,
+          "outputPrefixType": "RAW"
+        }
+      ]
+    }'''
+    keyset_handle = cleartext_keyset_handle.read(
+        tink.JsonKeysetReader(json_keyset))
+
+    ciphertext = bytes.fromhex(
+        '00000033013e77cdcd39016d632a3bb83b694565d3d2d85329ccc3aff540'
+        'ef00a7dcb95c157f0199fa7af825a2da2cf2ed0818e6eb0becb39d73d1b1'
+        '26e60bdac2dbe7d742dabaedcc2b3809e429510d9a330dc9e2bb1dc9d5b59ecc'
+    )
+    primitive = keyset_handle.primitive(aead.Aead)
+    decrypted = primitive.decrypt(ciphertext, b'associated_data')
+    self.assertEqual(decrypted, b'plaintext')
+
   @parameterized.parameters([
       aead.aead_key_templates.AES128_EAX,
       aead.aead_key_templates.AES256_EAX,
diff --git a/python/tink/aead/_aead_key_templates.py b/python/tink/aead/_aead_key_templates.py
index 044c90f..414d5de 100644
--- a/python/tink/aead/_aead_key_templates.py
+++ b/python/tink/aead/_aead_key_templates.py
@@ -54,10 +54,11 @@
   key_format = aes_eax_pb2.AesEaxKeyFormat()
   key_format.params.iv_size = iv_size
   key_format.key_size = key_size
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_EAX_KEY_TYPE_URL
-  key_template.output_prefix_type = output_prefix_type
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_EAX_KEY_TYPE_URL,
+      output_prefix_type=output_prefix_type,
+  )
   return key_template
 
 
@@ -66,12 +67,12 @@
     output_prefix_type: tink_pb2.OutputPrefixType = tink_pb2.TINK
 ) -> tink_pb2.KeyTemplate:
   """Creates an AES GCM KeyTemplate, and fills in its values."""
-  key_format = aes_gcm_pb2.AesGcmKeyFormat()
-  key_format.key_size = key_size
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_GCM_KEY_TYPE_URL
-  key_template.output_prefix_type = output_prefix_type
+  key_format = aes_gcm_pb2.AesGcmKeyFormat(key_size=key_size)
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_GCM_KEY_TYPE_URL,
+      output_prefix_type=output_prefix_type,
+  )
   return key_template
 
 
@@ -80,12 +81,14 @@
     output_prefix_type: tink_pb2.OutputPrefixType = tink_pb2.TINK
 ) -> tink_pb2.KeyTemplate:
   """Creates an AES GCM SIV KeyTemplate, and fills in its values."""
-  key_format = aes_gcm_siv_pb2.AesGcmSivKeyFormat()
-  key_format.key_size = key_size
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_GCM_SIV_KEY_TYPE_URL
-  key_template.output_prefix_type = output_prefix_type
+  key_format = aes_gcm_siv_pb2.AesGcmSivKeyFormat(
+      key_size=key_size,
+  )
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_GCM_SIV_KEY_TYPE_URL,
+      output_prefix_type=output_prefix_type,
+  )
   return key_template
 
 
@@ -104,10 +107,11 @@
   key_format.hmac_key_format.params.hash = hash_type
   key_format.hmac_key_format.params.tag_size = tag_size
   key_format.hmac_key_format.key_size = hmac_key_size
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_CTR_HMAC_AEAD_KEY_TYPE_URL
-  key_template.output_prefix_type = output_prefix_type
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_CTR_HMAC_AEAD_KEY_TYPE_URL,
+      output_prefix_type=output_prefix_type,
+  )
   return key_template
 
 
@@ -124,12 +128,12 @@
   Returns:
     A KMS Aead KeyTemplate.
   """
-  key_format = kms_aead_pb2.KmsAeadKeyFormat()
-  key_format.key_uri = key_uri
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _KMS_AEAD_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.RAW
+  key_format = kms_aead_pb2.KmsAeadKeyFormat(key_uri=key_uri)
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_KMS_AEAD_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.RAW,
+  )
   return key_template
 
 
@@ -148,13 +152,14 @@
   Returns:
       the resulting key template
   """
-  key_format = kms_envelope_pb2.KmsEnvelopeAeadKeyFormat()
-  key_format.kek_uri = kek_uri
-  key_format.dek_template.MergeFrom(dek_template)
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _KMS_ENVELOPE_AEAD_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.RAW
+  key_format = kms_envelope_pb2.KmsEnvelopeAeadKeyFormat(
+      kek_uri=kek_uri, dek_template=dek_template
+  )
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_KMS_ENVELOPE_AEAD_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.RAW,
+  )
   return key_template
 
 
diff --git a/python/tink/aead/_kms_aead_key_manager.py b/python/tink/aead/_kms_aead_key_manager.py
new file mode 100644
index 0000000..cffa65f
--- /dev/null
+++ b/python/tink/aead/_kms_aead_key_manager.py
@@ -0,0 +1,147 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Python KMS AEAD key manager."""
+
+import abc
+
+from typing import List, Type
+
+from tink.proto import kms_aead_pb2
+from tink.proto import kms_envelope_pb2
+from tink.proto import tink_pb2
+from tink import core
+from tink.aead import _aead
+from tink.aead import _kms_envelope_aead
+
+
+class KmsClient(metaclass=abc.ABCMeta):
+
+  @abc.abstractmethod
+  def does_support(self, key_uri: str) -> bool:
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def get_aead(self, key_uri: str) -> _aead.Aead:
+    raise NotImplementedError()
+
+
+_kms_clients: List[KmsClient] = []
+
+
+def register_kms_client(client: KmsClient) -> None:
+  """Tink-internal function to register kms clients."""
+  _kms_clients.append(client)
+
+
+def reset_kms_clients() -> None:
+  """Removes all previously registered KMS clients. Used in tests."""
+  _kms_clients.clear()
+
+
+def _kms_client_from_uri(key_uri: str) -> KmsClient:
+  """Tink-internal function to get a KmsClient."""
+  for client in _kms_clients:
+    if client.does_support(key_uri):
+      return client
+  raise core.TinkError('No KMS client does support: ' + key_uri)
+
+
+_KMS_AEAD_KEY_TYPE_URL = 'type.googleapis.com/google.crypto.tink.KmsAeadKey'
+_KMS_ENVELOPE_AEAD_KEY_TYPE_URL = (
+    'type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey'
+)
+
+
+class KmsAeadKeyManager(core.KeyManager[_aead.Aead]):
+  """KmsAeadKeyManager."""
+
+  def primitive_class(self) -> Type[_aead.Aead]:
+    return _aead.Aead
+
+  def primitive(self, key_data: tink_pb2.KeyData) -> _aead.Aead:
+    if key_data.type_url != _KMS_AEAD_KEY_TYPE_URL:
+      raise core.TinkError('wrong key type: ' + key_data.type_url)
+    kms_key = kms_aead_pb2.KmsAeadKey.FromString(key_data.value)
+    client = _kms_client_from_uri(kms_key.params.key_uri)
+    return client.get_aead(key_uri=kms_key.params.key_uri)
+
+  def key_type(self) -> str:
+    return _KMS_AEAD_KEY_TYPE_URL
+
+  def new_key_data(
+      self, key_template: tink_pb2.KeyTemplate
+  ) -> tink_pb2.KeyData:
+    if key_template.type_url != _KMS_AEAD_KEY_TYPE_URL:
+      raise core.TinkError('wrong key type: ' + key_template.type_url)
+    key = kms_aead_pb2.KmsAeadKey(
+        version=0,
+        params=kms_aead_pb2.KmsAeadKeyFormat.FromString(key_template.value),
+    )
+    return tink_pb2.KeyData(
+        type_url=_KMS_AEAD_KEY_TYPE_URL,
+        value=key.SerializeToString(),
+        key_material_type=tink_pb2.KeyData.REMOTE,
+    )
+
+  def does_support(self, type_url: str) -> bool:
+    return self.key_type() == type_url
+
+
+class KmsEnvelopeAeadKeyManager(core.KeyManager[_aead.Aead]):
+  """KmsEnvelopeAeadKeyManager."""
+
+  def primitive_class(self) -> Type[_aead.Aead]:
+    return _aead.Aead
+
+  def primitive(self, key_data: tink_pb2.KeyData) -> _aead.Aead:
+    if key_data.type_url != _KMS_ENVELOPE_AEAD_KEY_TYPE_URL:
+      raise core.TinkError('wrong key type: ' + key_data.type_url)
+    env_key = kms_envelope_pb2.KmsEnvelopeAeadKey.FromString(key_data.value)
+    client = _kms_client_from_uri(env_key.params.kek_uri)
+
+    return _kms_envelope_aead.KmsEnvelopeAead(
+        env_key.params.dek_template,
+        client.get_aead(key_uri=env_key.params.kek_uri),
+    )
+
+  def key_type(self) -> str:
+    return _KMS_ENVELOPE_AEAD_KEY_TYPE_URL
+
+  def new_key_data(
+      self, key_template: tink_pb2.KeyTemplate
+  ) -> tink_pb2.KeyData:
+    if key_template.type_url != _KMS_ENVELOPE_AEAD_KEY_TYPE_URL:
+      raise core.TinkError('wrong key type: ' + key_template.type_url)
+    params = kms_envelope_pb2.KmsEnvelopeAeadKeyFormat.FromString(
+        key_template.value
+    )
+    if not _kms_envelope_aead.is_supported_dek_key_type(
+        params.dek_template.type_url
+    ):
+      raise core.TinkError(
+          'Unsupported DEK key type: %s' % key_template.type_url
+      )
+    env_key = kms_envelope_pb2.KmsEnvelopeAeadKey(
+        version=0,
+        params=params,
+    )
+    return tink_pb2.KeyData(
+        type_url=_KMS_ENVELOPE_AEAD_KEY_TYPE_URL,
+        value=env_key.SerializeToString(),
+        key_material_type=tink_pb2.KeyData.REMOTE,
+    )
+
+  def does_support(self, type_url: str) -> bool:
+    return self.key_type() == type_url
diff --git a/python/tink/aead/_kms_aead_key_manager_test.py b/python/tink/aead/_kms_aead_key_manager_test.py
new file mode 100644
index 0000000..9027e5c
--- /dev/null
+++ b/python/tink/aead/_kms_aead_key_manager_test.py
@@ -0,0 +1,145 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for kms_aead_key_manager."""
+
+from absl.testing import absltest
+from tink.proto import kms_aead_pb2
+from tink.proto import kms_envelope_pb2
+from tink.proto import tink_pb2
+import tink
+from tink import aead
+from tink.aead import _kms_aead_key_manager
+
+
+def setUpModule():
+  aead.register()
+
+
+class FakeClient(_kms_aead_key_manager.KmsClient):
+
+  def __init__(self, key_uri):
+    self.key_uri = key_uri
+
+  def does_support(self, key_uri: str) -> bool:
+    return key_uri == self.key_uri
+
+  def get_aead(self, key_uri: str) -> aead.Aead:
+    raise NotImplementedError('not implemented')
+
+
+KMS_AEAD_KEY_TYPE_URL = 'type.googleapis.com/google.crypto.tink.KmsAeadKey'
+KMS_ENVELOPE_AEAD_KEY_TYPE_URL = (
+    'type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey'
+)
+
+
+class KmsAeadKeyManagerTest(absltest.TestCase):
+
+  def test_register_get_and_reset_kms_clients(self):
+    client1 = FakeClient('key_uri1')
+    client2 = FakeClient('key_uri2')
+    client3 = FakeClient('key_uri3')
+    _kms_aead_key_manager.register_kms_client(client1)
+    _kms_aead_key_manager.register_kms_client(client2)
+    _kms_aead_key_manager.register_kms_client(client3)
+
+    # returns the first registered client that supports the uri.
+    self.assertEqual(
+        _kms_aead_key_manager._kms_client_from_uri('key_uri1'), client1
+    )
+    self.assertEqual(
+        _kms_aead_key_manager._kms_client_from_uri('key_uri2'), client2
+    )
+    with self.assertRaises(tink.TinkError):
+      _kms_aead_key_manager._kms_client_from_uri('unknown')
+
+    _kms_aead_key_manager.reset_kms_clients()
+    with self.assertRaises(tink.TinkError):
+      _kms_aead_key_manager._kms_client_from_uri('key_uri1')
+
+  def test_kms_aead_new_key_data_success(self):
+    client = FakeClient('key_uri')
+    _kms_aead_key_manager.register_kms_client(client)
+    template = aead.aead_key_templates.create_kms_aead_key_template(
+        key_uri='key_uri'
+    )
+    key_data = _kms_aead_key_manager.KmsAeadKeyManager().new_key_data(template)
+
+    self.assertEqual(key_data.type_url, KMS_AEAD_KEY_TYPE_URL)
+    self.assertEqual(key_data.key_material_type, tink_pb2.KeyData.REMOTE)
+    key = kms_aead_pb2.KmsAeadKey.FromString(key_data.value)
+    self.assertEqual(key.version, 0)
+    self.assertEqual(key.params.key_uri, 'key_uri')
+
+  def test_kms_aead_new_key_data_rejects_unknown_template(self):
+    with self.assertRaises(tink.TinkError):
+      _kms_aead_key_manager.KmsAeadKeyManager().new_key_data(
+          aead.aead_key_templates.XCHACHA20_POLY1305_RAW
+      )
+
+  def test_kms_aead_primitive_rejects_unknown_key_data(self):
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri='kek_uri',
+        dek_template=aead.aead_key_templates.XCHACHA20_POLY1305_RAW,
+    )
+    envelope_aead_key_data = (
+        _kms_aead_key_manager.KmsEnvelopeAeadKeyManager().new_key_data(template)
+    )
+
+    with self.assertRaises(tink.TinkError):
+      _kms_aead_key_manager.KmsAeadKeyManager().primitive(
+          envelope_aead_key_data
+      )
+
+  def test_kms_envelope_aead_new_key_data_success(self):
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri='kek_uri',
+        dek_template=aead.aead_key_templates.XCHACHA20_POLY1305_RAW,
+    )
+    key_data = _kms_aead_key_manager.KmsEnvelopeAeadKeyManager().new_key_data(
+        template
+    )
+
+    self.assertEqual(key_data.type_url, KMS_ENVELOPE_AEAD_KEY_TYPE_URL)
+    self.assertEqual(key_data.key_material_type, tink_pb2.KeyData.REMOTE)
+    key = kms_envelope_pb2.KmsEnvelopeAeadKey.FromString(key_data.value)
+    self.assertEqual(key.version, 0)
+    self.assertEqual(key.params.kek_uri, 'kek_uri')
+    self.assertEqual(
+        key.params.dek_template, aead.aead_key_templates.XCHACHA20_POLY1305_RAW
+    )
+
+  def test_kms_envelope_aead_rejects_unknown_template(self):
+    with self.assertRaises(tink.TinkError):
+      _kms_aead_key_manager.KmsEnvelopeAeadKeyManager().new_key_data(
+          aead.aead_key_templates.XCHACHA20_POLY1305_RAW
+      )
+
+  def test_kms_envelope_aead_primitive_rejects_unknown_key_data(self):
+    template = aead.aead_key_templates.create_kms_aead_key_template(
+        key_uri='key_uri'
+    )
+    kms_aead_key_data = _kms_aead_key_manager.KmsAeadKeyManager().new_key_data(
+        template
+    )
+
+    with self.assertRaises(tink.TinkError):
+      _kms_aead_key_manager.KmsEnvelopeAeadKeyManager().primitive(
+          kms_aead_key_data
+      )
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/tink/aead/_kms_envelope_aead.py b/python/tink/aead/_kms_envelope_aead.py
index 201d26f..823c40e 100644
--- a/python/tink/aead/_kms_envelope_aead.py
+++ b/python/tink/aead/_kms_envelope_aead.py
@@ -19,27 +19,57 @@
 from tink import core
 from tink.aead import _aead
 
+_SUPPORTED_DEK_KEY_TYPES = frozenset({
+    'type.googleapis.com/google.crypto.tink.AesGcmKey',
+    'type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key',
+    'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey',
+    'type.googleapis.com/google.crypto.tink.AesEaxKey',
+    'type.googleapis.com/google.crypto.tink.AesGcmSivKey',
+})
+
+
+def is_supported_dek_key_type(type_url: str) -> bool:
+  return type_url in _SUPPORTED_DEK_KEY_TYPES
+
 
 class KmsEnvelopeAead(_aead.Aead):
   """Implements envelope encryption.
 
-     Envelope encryption generates a data encryption key (DEK) which is used
-     to encrypt the payload. The DEK is then send to a KMS to be encrypted and
-     the encrypted DEK is attached to the ciphertext. In order to decrypt the
-     ciphertext, the DEK first has to be decrypted by the KMS, and then the DEK
-     can be used to decrypt the ciphertext. For further information see
-     https://cloud.google.com/kms/docs/envelope-encryption.
+  Envelope encryption generates a data encryption key (DEK) which is used
+  to encrypt the payload. The DEK is then send to a KMS to be encrypted and
+  the encrypted DEK is attached to the ciphertext. In order to decrypt the
+  ciphertext, the DEK first has to be decrypted by the KMS, and then the DEK
+  can be used to decrypt the ciphertext. For further information see
+  https://cloud.google.com/kms/docs/envelope-encryption.
 
-     The ciphertext structure is as follows:
-     * Length of the encrypted DEK: 4 bytes (big endian)
-     * Encrypted DEK: variable length, specified by the previous 4 bytes
-     * AEAD payload: variable length
+  DEK key template must be a KeyTemplate for any of these Tink AEAD key types
+  (any other key template will be rejected):
+  * AesGcmKey
+  * XChaCha20Poly1305
+  * AesCtrHmacAeadKey
+  * AesEaxKey
+  * AesGcmSivKey
+
+  The ciphertext structure is as follows:
+  * Length of the encrypted DEK: 4 bytes (big endian)
+  * Encrypted DEK: variable length, specified by the previous 4 bytes
+  * AEAD payload: variable length
   """
 
   # Defines in how many bytes the DEK length will be encoded.
   DEK_LEN_BYTES = 4
 
   def __init__(self, key_template: tink_pb2.KeyTemplate, remote: _aead.Aead):
+    if not is_supported_dek_key_type(key_template.type_url):
+      raise core.TinkError(
+          'Unsupported DEK key type: %s' % key_template.type_url
+      )
+    # Create a dek to make sure that it works, so that KmsEnvelopeAead already
+    # fails when it is created, and not just when it is used.
+    # The C++ implementation does the same check, and we want this
+    # implementation to be consistent with the C++ implementation.
+    _ = core.Registry.new_key_data(key_template)
+
     self.key_template = key_template
     self.remote_aead = remote
 
@@ -77,10 +107,11 @@
     dek_bytes = self.remote_aead.decrypt(encrypted_dek_bytes, b'')
 
     # Get AEAD primitive based on DEK
-    dek = tink_pb2.KeyData()
-    dek.type_url = self.key_template.type_url
-    dek.value = dek_bytes
-    dek.key_material_type = tink_pb2.KeyData.SYMMETRIC
+    dek = tink_pb2.KeyData(
+        type_url=self.key_template.type_url,
+        value=dek_bytes,
+        key_material_type=tink_pb2.KeyData.SYMMETRIC,
+    )
     dek_aead = core.Registry.primitive(dek, _aead.Aead)
 
     # Extract ciphertext payload and decrypt
diff --git a/python/tink/aead/_kms_envelope_aead_test.py b/python/tink/aead/_kms_envelope_aead_test.py
index aad40f1..12afb86 100644
--- a/python/tink/aead/_kms_envelope_aead_test.py
+++ b/python/tink/aead/_kms_envelope_aead_test.py
@@ -14,29 +14,46 @@
 """Tests for tink.python.tink.aead.aead."""
 
 import struct
+
 from absl.testing import absltest
+from absl.testing import parameterized
 
 from tink.proto import aes_gcm_pb2
 import tink
 from tink import aead
 from tink import core
+from tink import mac
 
 
 def setUpModule():
   aead.register()
 
 
-class KmsEnvelopeAeadTest(absltest.TestCase):
+class KmsEnvelopeAeadTest(parameterized.TestCase):
 
-  def test_encrypt_decrypt(self):
-    key_template = aead.aead_key_templates.AES256_GCM
-    keyset_handle = tink.new_keyset_handle(key_template)
+  @parameterized.parameters([
+      aead.aead_key_templates.AES128_EAX,
+      aead.aead_key_templates.AES256_EAX,
+      aead.aead_key_templates.AES128_GCM,
+      aead.aead_key_templates.AES256_GCM,
+      aead.aead_key_templates.AES128_GCM_SIV,
+      aead.aead_key_templates.AES256_GCM_SIV,
+      aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
+      aead.aead_key_templates.AES256_CTR_HMAC_SHA256,
+      aead.aead_key_templates.XCHACHA20_POLY1305,
+  ])
+  def test_encrypt_decrypt(self, dek_template):
+    keyset_handle = tink.new_keyset_handle(dek_template)
     remote_aead = keyset_handle.primitive(aead.Aead)
-    env_aead = aead.KmsEnvelopeAead(key_template, remote_aead)
+    env_aead = aead.KmsEnvelopeAead(dek_template, remote_aead)
 
-    plaintext = b'helloworld'
-    ciphertext = env_aead.encrypt(plaintext, b'')
-    self.assertEqual(plaintext, env_aead.decrypt(ciphertext, b''))
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+    ciphertext = env_aead.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, env_aead.decrypt(ciphertext, associated_data))
+
+    with self.assertRaises(core.TinkError):
+      _ = env_aead.decrypt(ciphertext, b'invalid_associated_data')
 
   def test_encrypt_decrypt_missing_ad(self):
     key_template = aead.aead_key_templates.AES256_GCM
@@ -47,7 +64,16 @@
     plaintext = b'helloworld'
     ciphertext = env_aead.encrypt(plaintext, b'envelope_ad')
     with self.assertRaises(core.TinkError):
-      plaintext = env_aead.decrypt(ciphertext, b'')
+      _ = env_aead.decrypt(ciphertext, b'')
+
+  def test_invalid_dek_template_fails(self):
+    key_template = aead.aead_key_templates.AES256_GCM
+    keyset_handle = tink.new_keyset_handle(key_template)
+    remote_aead = keyset_handle.primitive(aead.Aead)
+
+    with self.assertRaises(tink.TinkError):
+      aead.KmsEnvelopeAead(
+          mac.mac_key_templates.HMAC_SHA256_128BITTAG, remote_aead)
 
   def test_corrupted_ciphertext(self):
     key_template = aead.aead_key_templates.AES256_GCM
@@ -61,7 +87,7 @@
     corrupted_ciphertext = bytes(ciphertext)
 
     with self.assertRaises(core.TinkError):
-      plaintext = env_aead.decrypt(corrupted_ciphertext, b'some ad')
+      _ = env_aead.decrypt(corrupted_ciphertext, b'some ad')
 
   def test_corrupted_dek(self):
     key_template = aead.aead_key_templates.AES256_GCM
@@ -75,7 +101,7 @@
     corrupted_ciphertext = bytes(ciphertext)
 
     with self.assertRaises(core.TinkError):
-      plaintext = env_aead.decrypt(corrupted_ciphertext, b'some ad')
+      _ = env_aead.decrypt(corrupted_ciphertext, b'some ad')
 
   def test_ciphertext_too_short(self):
     key_template = aead.aead_key_templates.AES256_GCM
@@ -84,7 +110,7 @@
     env_aead = aead.KmsEnvelopeAead(key_template, remote_aead)
 
     with self.assertRaises(core.TinkError):
-      env_aead.decrypt(b'foo', b'some ad')
+      _ = env_aead.decrypt(b'foo', b'some ad')
 
   def test_malformed_dek_length(self):
     key_template = aead.aead_key_templates.AES256_GCM
@@ -98,15 +124,15 @@
     corrupted_ciphertext = bytes(ciphertext)
 
     with self.assertRaises(core.TinkError):
-      plaintext = env_aead.decrypt(corrupted_ciphertext, b'some ad')
+      _ = env_aead.decrypt(corrupted_ciphertext, b'some ad')
 
     ciphertext[0:3] = [0, 0, 0, 0]
     corrupted_ciphertext = bytes(ciphertext)
 
     with self.assertRaises(core.TinkError):
-      plaintext = env_aead.decrypt(corrupted_ciphertext, b'some ad')
+      _ = env_aead.decrypt(corrupted_ciphertext, b'some ad')
 
-  def test_dek_extraction(self):
+  def test_ciphertext_wire_format(self):
     key_template = aead.aead_key_templates.AES256_GCM
     keyset_handle = tink.new_keyset_handle(key_template)
     remote_aead = keyset_handle.primitive(aead.Aead)
@@ -115,7 +141,8 @@
     plaintext = b'helloworld'
     ciphertext = bytearray(env_aead.encrypt(plaintext, b'some ad'))
 
-    # Decrypt DEK
+    # test that ciphertext has the wire format described here:
+    # https://developers.google.com/tink/wire-format#envelope_encryption
     dek_len = struct.unpack('>I',
                             ciphertext[0:aead.KmsEnvelopeAead.DEK_LEN_BYTES])[0]
     encrypted_dek_bytes = bytes(ciphertext[
@@ -125,8 +152,8 @@
 
     # Try to deserialize key
     key = aes_gcm_pb2.AesGcmKey.FromString(dek_bytes)
-
     self.assertLen(key.key_value, 32)
 
+
 if __name__ == '__main__':
   absltest.main()
diff --git a/python/tink/cc/BUILD.bazel b/python/tink/cc/BUILD.bazel
index a73f817..69ff663 100644
--- a/python/tink/cc/BUILD.bazel
+++ b/python/tink/cc/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@tink_py//tools/build_defs:tink_python_rules.bzl", "tink_pybind_library")
+load("@pybind11_bazel//:build_defs.bzl", "pybind_library")
 
 package(default_visibility = ["//:__subpackages__"])
 
@@ -19,7 +19,7 @@
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "cc_tink_config",
     srcs = ["cc_tink_config.cc"],
     hdrs = ["cc_tink_config.h"],
@@ -181,8 +181,8 @@
     deps = [
         ":python_file_object_adapter",
         "@com_google_absl//absl/memory",
-        "@com_google_absl//absl/strings",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
         "@tink_cc//:input_stream",
         "@tink_cc//subtle:subtle_util",
         "@tink_cc//util:status",
diff --git a/python/tink/cc/cc_jwt_config.h b/python/tink/cc/cc_jwt_config.h
index ab52e81..eb27ce8 100644
--- a/python/tink/cc/cc_jwt_config.h
+++ b/python/tink/cc/cc_jwt_config.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_CC_JWT_CONFIG_H_
-#define TINK_PYTHON_CC_CC_JWT_CONFIG_H_
+#ifndef TINK_PYTHON_TINK_CC_CC_JWT_CONFIG_H_
+#define TINK_PYTHON_TINK_CC_CC_JWT_CONFIG_H_
 
 #include "tink/util/status.h"
 #include "tink/registry.h"
@@ -28,4 +28,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_CC_JWT_CONFIG_H_
+#endif  // TINK_PYTHON_TINK_CC_CC_JWT_CONFIG_H_
diff --git a/python/tink/cc/cc_key_manager.h b/python/tink/cc/cc_key_manager.h
index c6a6800..ddfbfb2 100644
--- a/python/tink/cc/cc_key_manager.h
+++ b/python/tink/cc/cc_key_manager.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_CC_KEY_MANAGER_H_
-#define TINK_PYTHON_CC_CC_KEY_MANAGER_H_
+#ifndef TINK_PYTHON_TINK_CC_CC_KEY_MANAGER_H_
+#define TINK_PYTHON_TINK_CC_CC_KEY_MANAGER_H_
 
 #include <string>
 #include <utility>
@@ -123,4 +123,4 @@
 
 }  // namespace tink
 }  // namespace crypto
-#endif  // TINK_PYTHON_CC_CC_KEY_MANAGER_H_
+#endif  // TINK_PYTHON_TINK_CC_CC_KEY_MANAGER_H_
diff --git a/python/tink/cc/cc_streaming_aead_wrappers.h b/python/tink/cc/cc_streaming_aead_wrappers.h
index e03146c..934b1a8 100644
--- a/python/tink/cc/cc_streaming_aead_wrappers.h
+++ b/python/tink/cc/cc_streaming_aead_wrappers.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_CC_STREAMING_AEAD_WRAPPERS_H_
-#define TINK_PYTHON_CC_CC_STREAMING_AEAD_WRAPPERS_H_
+#ifndef TINK_PYTHON_TINK_CC_CC_STREAMING_AEAD_WRAPPERS_H_
+#define TINK_PYTHON_TINK_CC_CC_STREAMING_AEAD_WRAPPERS_H_
 
 #include <memory>
 
@@ -56,4 +56,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_CC_STREAMING_AEAD_WRAPPERS_H_
+#endif  // TINK_PYTHON_TINK_CC_CC_STREAMING_AEAD_WRAPPERS_H_
diff --git a/python/tink/cc/cc_tink_config.h b/python/tink/cc/cc_tink_config.h
index 034f743..d2cfbbd 100644
--- a/python/tink/cc/cc_tink_config.h
+++ b/python/tink/cc/cc_tink_config.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_CC_TINK_CONFIG_H_
-#define TINK_PYTHON_CC_CC_TINK_CONFIG_H_
+#ifndef TINK_PYTHON_TINK_CC_CC_TINK_CONFIG_H_
+#define TINK_PYTHON_TINK_CC_CC_TINK_CONFIG_H_
 
 #include "tink/util/status.h"
 #include "tink/registry.h"
@@ -28,4 +28,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_CC_TINK_CONFIG_H_
+#endif  // TINK_PYTHON_TINK_CC_CC_TINK_CONFIG_H_
diff --git a/python/tink/cc/input_stream_adapter.h b/python/tink/cc/input_stream_adapter.h
index 1043eee..159ce57 100644
--- a/python/tink/cc/input_stream_adapter.h
+++ b/python/tink/cc/input_stream_adapter.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_INPUT_STREAM_ADAPTER_H_
-#define TINK_PYTHON_CC_INPUT_STREAM_ADAPTER_H_
+#ifndef TINK_PYTHON_TINK_CC_INPUT_STREAM_ADAPTER_H_
+#define TINK_PYTHON_TINK_CC_INPUT_STREAM_ADAPTER_H_
 
 #include <memory>
 #include <string>
@@ -46,4 +46,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_INPUT_STREAM_ADAPTER_H_
+#endif  // TINK_PYTHON_TINK_CC_INPUT_STREAM_ADAPTER_H_
diff --git a/python/tink/cc/output_stream_adapter.h b/python/tink/cc/output_stream_adapter.h
index 30f2708..2994c66 100644
--- a/python/tink/cc/output_stream_adapter.h
+++ b/python/tink/cc/output_stream_adapter.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_OUTPUT_STREAM_ADAPTER_H_
-#define TINK_PYTHON_CC_OUTPUT_STREAM_ADAPTER_H_
+#ifndef TINK_PYTHON_TINK_CC_OUTPUT_STREAM_ADAPTER_H_
+#define TINK_PYTHON_TINK_CC_OUTPUT_STREAM_ADAPTER_H_
 
 #include <memory>
 #include <utility>
@@ -51,4 +51,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_OUTPUT_STREAM_ADAPTER_H_
+#endif  // TINK_PYTHON_TINK_CC_OUTPUT_STREAM_ADAPTER_H_
diff --git a/python/tink/cc/pybind/BUILD.bazel b/python/tink/cc/pybind/BUILD.bazel
index 0c5369c..68d2edb 100644
--- a/python/tink/cc/pybind/BUILD.bazel
+++ b/python/tink/cc/pybind/BUILD.bazel
@@ -1,5 +1,5 @@
-load("@tink_py//tools/build_defs:tink_python_rules.bzl", "tink_pybind_extension", "tink_pybind_library")
 load("@tink_py_pip_deps//:requirements.bzl", "requirement")
+load("@pybind11_bazel//:build_defs.bzl", "pybind_extension", "pybind_library")
 
 package(
     default_visibility = ["//:__subpackages__"],
@@ -7,25 +7,21 @@
 
 licenses(["notice"])
 
-tink_pybind_library(
+pybind_library(
     name = "import_helper",
     srcs = ["import_helper.cc"],
     hdrs = ["import_helper.h"],
     visibility = ["//visibility:private"],
-    deps = [
-        "@com_google_absl//absl/strings",
-        "@pybind11",
-    ],
+    deps = ["@com_google_absl//absl/strings"],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "cc_key_manager",
     srcs = ["cc_key_manager.cc"],
     hdrs = ["cc_key_manager.h"],
     deps = [
         ":import_helper",
         "//tink/cc:cc_key_manager",
-        "@pybind11",
         "@tink_cc//:aead",
         "@tink_cc//:deterministic_aead",
         "@tink_cc//:hybrid_decrypt",
@@ -60,35 +56,30 @@
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "cc_tink_config",
     srcs = ["cc_tink_config.cc"],
     hdrs = ["cc_tink_config.h"],
-    deps = [
-        "//tink/cc:cc_tink_config",
-        "@pybind11",
-    ],
+    deps = ["//tink/cc:cc_tink_config"],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "cc_jwt_config",
     srcs = ["cc_jwt_config.cc"],
     hdrs = ["cc_jwt_config.h"],
     deps = [
         ":tink_exception",
         "//tink/cc:cc_jwt_config",
-        "@pybind11",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "cc_hpke_config",
     srcs = ["cc_hpke_config.cc"],
     hdrs = ["cc_hpke_config.h"],
     deps = [
         ":tink_exception",
         "//tink/cc:cc_hpke_config",
-        "@pybind11",
     ],
 )
 
@@ -101,28 +92,24 @@
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "python_file_object_adapter",
     srcs = ["python_file_object_adapter.cc"],
     hdrs = ["python_file_object_adapter.h"],
-    deps = [
-        "//tink/cc:python_file_object_adapter",
-        "@pybind11",
-    ],
+    deps = ["//tink/cc:python_file_object_adapter"],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "output_stream_adapter",
     srcs = ["output_stream_adapter.cc"],
     hdrs = ["output_stream_adapter.h"],
     deps = [
         ":tink_exception",
         "//tink/cc:output_stream_adapter",
-        "@pybind11",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "cc_streaming_aead_wrappers",
     srcs = ["cc_streaming_aead_wrappers.cc"],
     hdrs = ["cc_streaming_aead_wrappers.h"],
@@ -130,208 +117,165 @@
         ":import_helper",
         ":tink_exception",
         "//tink/cc:cc_streaming_aead_wrappers",
-        "@pybind11",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "input_stream_adapter",
     srcs = ["input_stream_adapter.cc"],
     hdrs = ["input_stream_adapter.h"],
     deps = [
         ":tink_exception",
         "//tink/cc:input_stream_adapter",
-        "@pybind11",
     ],
 )
 
 # Pybind11 Status, StatusOr casters and extension.
 
-tink_pybind_library(
+pybind_library(
     name = "aead",
     srcs = ["aead.cc"],
     hdrs = ["aead.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:aead",
         "@tink_cc//util:statusor",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "deterministic_aead",
     srcs = ["deterministic_aead.cc"],
     hdrs = ["deterministic_aead.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:deterministic_aead",
         "@tink_cc//util:statusor",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "streaming_aead",
     srcs = ["streaming_aead.cc"],
     hdrs = ["streaming_aead.h"],
-    deps = [
-        "@pybind11",
-        "@tink_cc//:streaming_aead",
-    ],
+    deps = ["@tink_cc//:streaming_aead"],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "hybrid_decrypt",
     srcs = ["hybrid_decrypt.cc"],
     hdrs = ["hybrid_decrypt.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:hybrid_decrypt",
         "@tink_cc//util:statusor",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "hybrid_encrypt",
     srcs = ["hybrid_encrypt.cc"],
     hdrs = ["hybrid_encrypt.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:hybrid_encrypt",
         "@tink_cc//util:statusor",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "mac",
     srcs = ["mac.cc"],
     hdrs = ["mac.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:mac",
         "@tink_cc//util:status",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "prf",
     srcs = ["prf.cc"],
     hdrs = ["prf.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//prf:prf_set",
         "@tink_cc//util:status",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "public_key_sign",
     srcs = ["public_key_sign.cc"],
     hdrs = ["public_key_sign.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:public_key_sign",
         "@tink_cc//util:statusor",
     ],
 )
 
-tink_pybind_library(
+pybind_library(
     name = "public_key_verify",
     srcs = ["public_key_verify.cc"],
     hdrs = ["public_key_verify.h"],
     deps = [
         ":tink_exception",
-        "@pybind11",
         "@tink_cc//:public_key_verify",
         "@tink_cc//util:status",
     ],
 )
 
-tink_pybind_library(
-    name = "cc_gcp_kms_client",
-    srcs = ["cc_gcp_kms_client.cc"],
-    hdrs = ["cc_gcp_kms_client.h"],
-    deps = [
-        ":tink_exception",
-        "//tink/cc:output_stream_adapter",
-        "@pybind11",
-        "@tink_cc//:kms_clients",
-        "@tink_cc_gcpkms//:gcp_kms_client",
-    ],
-)
+_PYBIND_EXTENSION_DEPS = [
+    ":aead",
+    ":cc_hpke_config",
+    ":cc_jwt_config",
+    ":cc_key_manager",
+    ":cc_streaming_aead_wrappers",
+    ":cc_tink_config",
+    ":deterministic_aead",
+    ":hybrid_decrypt",
+    ":hybrid_encrypt",
+    ":input_stream_adapter",
+    ":mac",
+    ":output_stream_adapter",
+    ":prf",
+    ":public_key_sign",
+    ":public_key_verify",
+    ":python_file_object_adapter",
+    ":streaming_aead",
+    ":tink_exception",
+    "@tink_cc//:aead",
+    "@tink_cc//:deterministic_aead",
+    "@tink_cc//:hybrid_decrypt",
+    "@tink_cc//:hybrid_encrypt",
+    "@tink_cc//:mac",
+    "@tink_cc//:public_key_sign",
+    "@tink_cc//:public_key_verify",
+    "@tink_cc//:streaming_aead",
+    "@tink_cc//prf:prf_set",
+    "@tink_cc//util:status",
+    "@tink_cc//util:statusor",
+]
 
-tink_pybind_library(
-    name = "cc_aws_kms_client",
-    srcs = ["cc_aws_kms_client.cc"],
-    hdrs = ["cc_aws_kms_client.h"],
-    deps = [
-        ":tink_exception",
-        "//tink/cc:output_stream_adapter",
-        "@pybind11",
-        "@tink_cc_awskms//:aws_kms_client",
-    ],
-)
-
-tink_pybind_library(
-    name = "cc_fake_kms_client_testonly",
-    srcs = ["cc_fake_kms_client_testonly.cc"],
-    hdrs = ["cc_fake_kms_client_testonly.h"],
-    deps = [
-        ":tink_exception",
-        "@pybind11",
-        "@tink_cc//util:fake_kms_client_pybind",
-        "@tink_cc//util:statusor",
-    ],
-)
-
-# To avoid getting multiple instances of KmsClients, ":aead" and
-# ":cc_fake_kms_client_testonly" need to be in the same pybind exension.
-tink_pybind_extension(
+# This internally generates a tink_bindings.so cc_binary target, so there is no
+# name conflict with the py_library rule below.
+pybind_extension(
     name = "tink_bindings",
     srcs = ["tink_bindings.cc"],
-    deps = [
-        ":aead",
-        ":cc_aws_kms_client",
-        ":cc_fake_kms_client_testonly",
-        ":cc_gcp_kms_client",
-        ":cc_hpke_config",
-        ":cc_jwt_config",
-        ":cc_key_manager",
-        ":cc_streaming_aead_wrappers",
-        ":cc_tink_config",
-        ":deterministic_aead",
-        ":hybrid_decrypt",
-        ":hybrid_encrypt",
-        ":input_stream_adapter",
-        ":mac",
-        ":output_stream_adapter",
-        ":prf",
-        ":public_key_sign",
-        ":public_key_verify",
-        ":python_file_object_adapter",
-        ":streaming_aead",
-        ":tink_exception",
-        "@tink_cc//:aead",
-        "@tink_cc//:deterministic_aead",
-        "@tink_cc//:hybrid_decrypt",
-        "@tink_cc//:hybrid_encrypt",
-        "@tink_cc//:mac",
-        "@tink_cc//:public_key_sign",
-        "@tink_cc//:public_key_verify",
-        "@tink_cc//:streaming_aead",
-        "@tink_cc//prf:prf_set",
-        "@tink_cc//util:status",
-        "@tink_cc//util:statusor",
-    ],
+    deps = _PYBIND_EXTENSION_DEPS,
 )
 
-tink_pybind_library(
+py_library(
+    name = "tink_bindings",
+    # Generated by the pybind_extension rule above.
+    data = ["tink_bindings.so"],
+    # Extract Python targets from deps.
+    deps = [dep[:-3] for dep in _PYBIND_EXTENSION_DEPS if dep.endswith("_cc")],
+)
+
+pybind_library(
     name = "tink_exception",
     hdrs = ["tink_exception.h"],
     visibility = ["//tink/cc:__pkg__"],
diff --git a/python/tink/cc/pybind/cc_aws_kms_client.cc b/python/tink/cc/pybind/cc_aws_kms_client.cc
deleted file mode 100644
index a0d0006..0000000
--- a/python/tink/cc/pybind/cc_aws_kms_client.cc
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/cc/pybind/cc_aws_kms_client.h"
-
-#include "pybind11/pybind11.h"
-#include "tink/integration/awskms/aws_kms_client.h"
-#include "tink/util/statusor.h"
-#include "tink/cc/pybind/tink_exception.h"
-
-namespace crypto {
-namespace tink {
-namespace integration {
-namespace awskms {
-
-using pybind11::google_tink::TinkException;
-
-void PybindRegisterCcAwsKmsClient(pybind11::module* module) {
-  namespace py = pybind11;
-  py::module& m = *module;
-
-  py::class_<AwsKmsClient>(m, "AwsKmsClient", "Wrapper for C++ AwsKMS.")
-      .def(py::init(
-          [](const std::string& key_uri, const std::string& credentials_path) {
-            auto client_result = AwsKmsClient::New(key_uri, credentials_path);
-            if (!client_result.ok()) {
-              throw pybind11::value_error("Could not create client.");
-            }
-            return std::move(client_result.value());
-          }))
-      .def(
-          "does_support",
-          [](const AwsKmsClient& self, const std::string& key_uri) -> bool {
-            return self.DoesSupport(key_uri);
-          },
-          py::arg("key_uri"), "URI of the key to be checked.")
-      .def(
-          "get_aead",
-          [](const AwsKmsClient& self,
-             const std::string& key_uri) -> std::unique_ptr<Aead> {
-            crypto::tink::util::StatusOr<std::unique_ptr<Aead>> aead_result =
-                self.GetAead(key_uri);
-            if (!aead_result.ok()) {
-              throw TinkException(aead_result.status());
-            }
-            return *std::move(aead_result);
-          },
-          py::arg("key_uri"), "URI of the key which should be used.")
-      .def_static(
-          "register_client",
-          [](const std::string& key_uri,
-             const std::string& credentials_path) -> void {
-            crypto::tink::util::Status result =
-                AwsKmsClient::RegisterNewClient(key_uri, credentials_path);
-            if (!result.ok()) {
-              throw TinkException(result);
-            }
-          },
-          py::arg("key_uri"), "URI of the key which should be used.",
-          py::arg("credentials_path"),
-          "Path to the credentials for the client.");
-}
-
-}  // namespace awskms
-}  // namespace integration
-}  // namespace tink
-}  // namespace crypto
diff --git a/python/tink/cc/pybind/cc_aws_kms_client.h b/python/tink/cc/pybind/cc_aws_kms_client.h
deleted file mode 100644
index eca3206..0000000
--- a/python/tink/cc/pybind/cc_aws_kms_client.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-#ifndef TINK_PYTHON_TINK_CC_PYBIND_CC_AWS_KMS_CLIENT_H_
-#define TINK_PYTHON_TINK_CC_PYBIND_CC_AWS_KMS_CLIENT_H_
-
-#include "pybind11/pybind11.h"
-
-namespace crypto {
-namespace tink {
-namespace integration {
-namespace awskms {
-
-void PybindRegisterCcAwsKmsClient(pybind11::module* m);
-
-}  // namespace awskms
-}  // namespace integration
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_PYTHON_TINK_CC_PYBIND_CC_AWS_KMS_CLIENT_H_
diff --git a/python/tink/cc/pybind/cc_fake_kms_client_testonly.cc b/python/tink/cc/pybind/cc_fake_kms_client_testonly.cc
deleted file mode 100644
index 0f7350d..0000000
--- a/python/tink/cc/pybind/cc_fake_kms_client_testonly.cc
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/cc/pybind/cc_fake_kms_client_testonly.h"
-
-#include <string>
-#include <utility>
-
-#include "pybind11/pybind11.h"
-#include "tink/util/fake_kms_client.h"
-#include "tink/util/statusor.h"
-#include "tink/cc/pybind/tink_exception.h"
-
-namespace crypto {
-namespace tink {
-namespace test {
-
-using pybind11::google_tink::TinkException;
-
-void PybindRegisterCcFakeKmsClientTestonly(pybind11::module* module) {
-  namespace py = pybind11;
-  py::module& m = *module;
-  m.def(
-      "register_fake_kms_client_testonly",
-      [](const std::string& key_uri,
-         const std::string& credentials_path) -> void {
-        crypto::tink::util::Status result =
-            FakeKmsClient::RegisterNewClient(key_uri, credentials_path);
-        if (!result.ok()) {
-          throw TinkException(result);
-        }
-      },
-      py::arg("key_uri"), "URI of the key which should be used.",
-      py::arg("credentials_path"), "Path to the credentials for the client.");
-}
-
-}  // namespace test
-}  // namespace tink
-}  // namespace crypto
diff --git a/python/tink/cc/pybind/cc_fake_kms_client_testonly.h b/python/tink/cc/pybind/cc_fake_kms_client_testonly.h
deleted file mode 100644
index b494579..0000000
--- a/python/tink/cc/pybind/cc_fake_kms_client_testonly.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-#ifndef TINK_PYTHON_TINK_CC_PYBIND_CC_FAKE_KMS_CLIENT_H_
-#define TINK_PYTHON_TINK_CC_PYBIND_CC_FAKE_KMS_CLIENT_H_
-
-#include "pybind11/pybind11.h"
-
-namespace crypto {
-namespace tink {
-namespace test {
-
-void PybindRegisterCcFakeKmsClientTestonly(pybind11::module* m);
-
-}  // namespace test
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_PYTHON_TINK_CC_PYBIND_CC_FAKE_KMS_CLIENT_H_
diff --git a/python/tink/cc/pybind/cc_gcp_kms_client.cc b/python/tink/cc/pybind/cc_gcp_kms_client.cc
deleted file mode 100644
index a10f627..0000000
--- a/python/tink/cc/pybind/cc_gcp_kms_client.cc
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#include "tink/cc/pybind/cc_gcp_kms_client.h"
-
-#include "pybind11/pybind11.h"
-#include "tink/integration/gcpkms/gcp_kms_client.h"
-#include "tink/kms_clients.h"
-#include "tink/util/statusor.h"
-#include "tink/cc/pybind/tink_exception.h"
-
-namespace crypto {
-namespace tink {
-namespace integration {
-namespace gcpkms {
-
-using pybind11::google_tink::TinkException;
-
-void PybindRegisterCcGcpKmsClient(pybind11::module* module) {
-  namespace py = pybind11;
-  py::module& m = *module;
-
-  py::class_<GcpKmsClient>(m, "GcpKmsClient", "Wrapper for C++ GcpKMS.")
-      .def(py::init(
-          [](const std::string& key_uri, const std::string& credentials_path) {
-            auto client_result = GcpKmsClient::New(key_uri, credentials_path);
-            if (!client_result.ok()) {
-              throw pybind11::value_error("Could not create client.");
-            }
-
-            return std::move(client_result.value());
-          }))
-      .def(
-          "does_support",
-          [](const GcpKmsClient& self, const std::string& key_uri) -> bool {
-            return self.DoesSupport(key_uri);
-          },
-          py::arg("key_uri"), "URI of the key to be checked.")
-      .def(
-          "get_aead",
-          [](const GcpKmsClient& self,
-             const std::string& key_uri) -> std::unique_ptr<Aead> {
-            crypto::tink::util::StatusOr<std::unique_ptr<Aead>> aead_result =
-                self.GetAead(key_uri);
-            if (!aead_result.ok()) {
-              throw TinkException(aead_result.status());
-            }
-            return *std::move(aead_result);
-          },
-          py::arg("key_uri"), "URI of the key which should be used.")
-      .def_static(
-          "register_client",
-          [](const std::string& key_uri,
-             const std::string& credentials_path) -> void {
-            crypto::tink::util::Status result =
-                GcpKmsClient::RegisterNewClient(key_uri, credentials_path);
-            if (!result.ok()) {
-              throw TinkException(result);
-            }
-          },
-          py::arg("key_uri"), "URI of the key which should be used.",
-          py::arg("credentials_path"),
-          "Path to the credentials for the client.");
-}
-
-}  // namespace gcpkms
-}  // namespace integration
-}  // namespace tink
-}  // namespace crypto
diff --git a/python/tink/cc/pybind/cc_gcp_kms_client.h b/python/tink/cc/pybind/cc_gcp_kms_client.h
deleted file mode 100644
index 8eb135c..0000000
--- a/python/tink/cc/pybind/cc_gcp_kms_client.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-#ifndef TINK_PYTHON_TINK_CC_PYBIND_CC_GCP_KMS_CLIENT_H_
-#define TINK_PYTHON_TINK_CC_PYBIND_CC_GCP_KMS_CLIENT_H_
-
-#include "pybind11/pybind11.h"
-
-namespace crypto {
-namespace tink {
-namespace integration {
-namespace gcpkms {
-
-void PybindRegisterCcGcpKmsClient(pybind11::module* m);
-
-}  // namespace gcpkms
-}  // namespace integration
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_PYTHON_TINK_CC_PYBIND_CC_GCP_KMS_CLIENT_H_
diff --git a/python/tink/cc/pybind/tink_bindings.cc b/python/tink/cc/pybind/tink_bindings.cc
index b2b8ecb..729eb92 100644
--- a/python/tink/cc/pybind/tink_bindings.cc
+++ b/python/tink/cc/pybind/tink_bindings.cc
@@ -18,9 +18,6 @@
 #include "tink/aead.h"
 #include "tink/util/statusor.h"
 #include "tink/cc/pybind/aead.h"
-#include "tink/cc/pybind/cc_aws_kms_client.h"
-#include "tink/cc/pybind/cc_gcp_kms_client.h"
-#include "tink/cc/pybind/cc_fake_kms_client_testonly.h"
 #include "tink/cc/pybind/cc_hpke_config.h"
 #include "tink/cc/pybind/cc_jwt_config.h"
 #include "tink/cc/pybind/cc_key_manager.h"
@@ -39,12 +36,11 @@
 #include "tink/cc/pybind/streaming_aead.h"
 #include "tink/cc/pybind/tink_exception.h"
 
+
 namespace crypto {
 namespace tink {
 
 PYBIND11_MODULE(tink_bindings, m) {
-  integration::awskms::PybindRegisterCcAwsKmsClient(&m);
-  integration::gcpkms::PybindRegisterCcGcpKmsClient(&m);
   namespace py = pybind11;
 
   py::register_exception<pybind11::google_tink::TinkException>(
@@ -59,7 +55,6 @@
   PybindRegisterDeterministicAead(&m);
   PybindRegisterPublicKeySign(&m);
   PybindRegisterMac(&m);
-  test::PybindRegisterCcFakeKmsClientTestonly(&m);
   PybindRegisterPrf(&m);
   PybindRegisterHybridDecrypt(&m);
   PybindRegisterOutputStreamAdapter(&m);
diff --git a/python/tink/cc/python_file_object_adapter.h b/python/tink/cc/python_file_object_adapter.h
index 6ed32f9..5760af9 100644
--- a/python/tink/cc/python_file_object_adapter.h
+++ b/python/tink/cc/python_file_object_adapter.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_PYTHON_FILE_OBJECT_ADAPTER_H_
-#define TINK_PYTHON_CC_PYTHON_FILE_OBJECT_ADAPTER_H_
+#ifndef TINK_PYTHON_TINK_CC_PYTHON_FILE_OBJECT_ADAPTER_H_
+#define TINK_PYTHON_TINK_CC_PYTHON_FILE_OBJECT_ADAPTER_H_
 
 #include <string>
 
@@ -48,4 +48,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_PYTHON_FILE_OBJECT_ADAPTER_H_
+#endif  // TINK_PYTHON_TINK_CC_PYTHON_FILE_OBJECT_ADAPTER_H_
diff --git a/python/tink/cc/python_input_stream.h b/python/tink/cc/python_input_stream.h
index 87faa68..941b133 100644
--- a/python/tink/cc/python_input_stream.h
+++ b/python/tink/cc/python_input_stream.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_PYTHON_INPUT_STREAM_H_
-#define TINK_PYTHON_CC_PYTHON_INPUT_STREAM_H_
+#ifndef TINK_PYTHON_TINK_CC_PYTHON_INPUT_STREAM_H_
+#define TINK_PYTHON_TINK_CC_PYTHON_INPUT_STREAM_H_
 
 #include <memory>
 #include <string>
@@ -61,4 +61,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_PYTHON_INPUT_STREAM_H_
+#endif  // TINK_PYTHON_TINK_CC_PYTHON_INPUT_STREAM_H_
diff --git a/python/tink/cc/python_output_stream.h b/python/tink/cc/python_output_stream.h
index f8f390d..dd562ba 100644
--- a/python/tink/cc/python_output_stream.h
+++ b/python/tink/cc/python_output_stream.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_PYTHON_OUTPUT_STREAM_H_
-#define TINK_PYTHON_CC_PYTHON_OUTPUT_STREAM_H_
+#ifndef TINK_PYTHON_TINK_CC_PYTHON_OUTPUT_STREAM_H_
+#define TINK_PYTHON_TINK_CC_PYTHON_OUTPUT_STREAM_H_
 
 #include <memory>
 #include <string>
@@ -63,4 +63,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_PYTHON_CC_PYTHON_OUTPUT_STREAM_H_
+#endif  // TINK_PYTHON_TINK_CC_PYTHON_OUTPUT_STREAM_H_
diff --git a/python/tink/cc/test_util.h b/python/tink/cc/test_util.h
index ee75a03..8ccdc2f 100644
--- a/python/tink/cc/test_util.h
+++ b/python/tink/cc/test_util.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_PYTHON_CC_TEST_UTIL_H_
-#define TINK_PYTHON_CC_TEST_UTIL_H_
+#ifndef TINK_PYTHON_TINK_CC_TEST_UTIL_H_
+#define TINK_PYTHON_TINK_CC_TEST_UTIL_H_
 
 #include <algorithm>
 #include <string>
@@ -313,5 +313,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-
-#endif  // TINK_PYTHON_CC_TEST_UTIL_H_
+#endif  // TINK_PYTHON_TINK_CC_TEST_UTIL_H_
diff --git a/python/tink/daead/_deterministic_aead_key_templates.py b/python/tink/daead/_deterministic_aead_key_templates.py
index 6aa554b..bf3dbeb 100644
--- a/python/tink/daead/_deterministic_aead_key_templates.py
+++ b/python/tink/daead/_deterministic_aead_key_templates.py
@@ -31,12 +31,14 @@
 
 def _create_aes_siv_key_template(key_size: int) -> tink_pb2.KeyTemplate:
   """Creates an AES EAX KeyTemplate, and fills in its values."""
-  key_format = aes_siv_pb2.AesSivKeyFormat()
-  key_format.key_size = key_size
-  key_template = tink_pb2.KeyTemplate()
-  key_template.type_url = _AES_SIV_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.TINK
-  key_template.value = key_format.SerializeToString()
+  key_format = aes_siv_pb2.AesSivKeyFormat(
+      key_size=key_size,
+  )
+  key_template = tink_pb2.KeyTemplate(
+      type_url=_AES_SIV_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.TINK,
+      value=key_format.SerializeToString(),
+  )
   return key_template
 
 
diff --git a/python/tink/hybrid/_hybrid_key_templates.py b/python/tink/hybrid/_hybrid_key_templates.py
index e21c82c..9ebbf47 100644
--- a/python/tink/hybrid/_hybrid_key_templates.py
+++ b/python/tink/hybrid/_hybrid_key_templates.py
@@ -58,11 +58,11 @@
   key_format.params.kdf = hpke_kdf
   key_format.params.aead = hpke_aead
 
-  key_template = tink_pb2.KeyTemplate()
-  key_template.type_url = (
-      'type.googleapis.com/google.crypto.tink.HpkePrivateKey')
-  key_template.value = key_format.SerializeToString()
-  key_template.output_prefix_type = output_prefix_type
+  key_template = tink_pb2.KeyTemplate(
+      type_url='type.googleapis.com/google.crypto.tink.HpkePrivateKey',
+      value=key_format.SerializeToString(),
+      output_prefix_type=output_prefix_type,
+  )
   return key_template
 
 
diff --git a/python/tink/integration/awskms/BUILD.bazel b/python/tink/integration/awskms/BUILD.bazel
index 0a20804..011ab7a 100644
--- a/python/tink/integration/awskms/BUILD.bazel
+++ b/python/tink/integration/awskms/BUILD.bazel
@@ -18,9 +18,11 @@
     srcs = ["_aws_kms_client.py"],
     srcs_version = "PY3",
     deps = [
+        "//tink:tink_python",
         "//tink/aead",
-        "//tink/cc/pybind:tink_bindings",
+        "//tink/aead:_kms_aead_key_manager",
         "//tink/core",
+        requirement("boto3"),
     ],
 )
 
@@ -28,29 +30,34 @@
     name = "_aws_kms_client_test",
     srcs = ["_aws_kms_client_test.py"],
     data = [
-        "//testdata/aws:credentials",
         "//testdata/aws:bad_credentials",
+        "//testdata/aws:credentials",
     ],
     srcs_version = "PY3",
     deps = [
         ":awskms",
+        ":_aws_kms_client",
+        "//tink:tink_python",
         "//tink/testing:helper",
         requirement("absl-py"),
     ],
 )
 
 py_test(
-    name = "_aws_kms_aead_test",
-    srcs = ["_aws_kms_aead_test.py"],
+    name = "_aws_kms_integration_test",
+    srcs = ["_aws_kms_integration_test.py"],
     data = [
-        "//testdata/aws:credentials",
         "//testdata/aws:bad_credentials",
+        "//testdata/aws:credentials",
     ],
+    srcs_version = "PY3",
     # This test require valid AWS KMS credentials so we set it as `manual`.
     tags = ["manual"],
-    srcs_version = "PY3",
     deps = [
         ":awskms",
+        "//tink:tink_python",
+        "//tink/aead",
+        "//tink/aead:_kms_aead_key_manager",
         "//tink/testing:helper",
         requirement("absl-py"),
     ],
diff --git a/python/tink/integration/awskms/__init__.py b/python/tink/integration/awskms/__init__.py
index a678c35..c86dc9c 100644
--- a/python/tink/integration/awskms/__init__.py
+++ b/python/tink/integration/awskms/__init__.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""StreamingAead package."""
+"""AWS KMS integration package."""
 
 from tink.integration.awskms import _aws_kms_client
 
diff --git a/python/tink/integration/awskms/_aws_kms_aead_test.py b/python/tink/integration/awskms/_aws_kms_aead_test.py
deleted file mode 100644
index c1c369c..0000000
--- a/python/tink/integration/awskms/_aws_kms_aead_test.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Tests for tink.python.tink.integration.aws_kms_aead."""
-
-import os
-
-from absl.testing import absltest
-
-from tink import core
-from tink.integration import awskms
-from tink.testing import helper
-
-CREDENTIAL_PATH = os.path.join(helper.tink_py_testdata_path(),
-                               'aws/credentials.ini')
-BAD_CREDENTIALS_PATH = os.path.join(helper.tink_py_testdata_path(),
-                                    'aws/credentials_bad.ini')
-KEY_URI = 'aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f'
-BAD_KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key'
-
-
-class AwsKmsAeadTest(absltest.TestCase):
-
-  def test_encrypt_decrypt(self):
-    aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
-    aead = aws_client.get_aead(KEY_URI)
-
-    plaintext = b'hello'
-    associated_data = b'world'
-    ciphertext = aead.encrypt(plaintext, associated_data)
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, associated_data))
-
-    plaintext = b'hello'
-    ciphertext = aead.encrypt(plaintext, b'')
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, b''))
-
-  def test_corrupted_ciphertext(self):
-    aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
-    aead = aws_client.get_aead(KEY_URI)
-
-    plaintext = b'helloworld'
-    ciphertext = aead.encrypt(plaintext, b'')
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, b''))
-
-    # Corrupt each byte once and check that decryption fails
-    # NOTE: Skipping two bytes as they are malleable
-    for byte_idx in [b for b in range(len(ciphertext)) if b not in [77, 123]]:
-      tmp_ciphertext = list(ciphertext)
-      tmp_ciphertext[byte_idx] ^= 1
-      corrupted_ciphertext = bytes(tmp_ciphertext)
-      with self.assertRaises(core.TinkError):
-        aead.decrypt(corrupted_ciphertext, b'')
-
-  def test_encrypt_with_bad_uri(self):
-    with self.assertRaises(core.TinkError):
-      aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
-      aws_client.get_aead(BAD_KEY_URI)
-
-  def test_encrypt_with_bad_credentials(self):
-    aws_client = awskms.AwsKmsClient(KEY_URI, BAD_CREDENTIALS_PATH)
-    aead = aws_client.get_aead(KEY_URI)
-
-    plaintext = b'hello'
-    associated_data = b'world'
-    with self.assertRaises(core.TinkError):
-      aead.encrypt(plaintext, associated_data)
-
-
-if __name__ == '__main__':
-  absltest.main()
diff --git a/python/tink/integration/awskms/_aws_kms_client.py b/python/tink/integration/awskms/_aws_kms_client.py
index 6008201..d17d70b 100644
--- a/python/tink/integration/awskms/_aws_kms_client.py
+++ b/python/tink/integration/awskms/_aws_kms_client.py
@@ -13,49 +13,128 @@
 # limitations under the License.
 """A client for AWS KMS."""
 
+import binascii
+import configparser
 import re
 
+from typing import Optional, Tuple, Any, Dict
 
+import boto3
+from botocore import exceptions
+
+import tink
 from tink import aead
-from tink import core
-from tink.cc.pybind import tink_bindings
+from tink.aead import _kms_aead_key_manager
 
 
-class AwsKmsClient:
+AWS_KEYURI_PREFIX = 'aws-kms://'
+
+
+def _encryption_context(associated_data: bytes) -> Dict[str, str]:
+  if associated_data:
+    hex_associated_data = binascii.hexlify(associated_data).decode('utf-8')
+    return {'associatedData': hex_associated_data}
+  else:
+    return dict()
+
+
+class _AwsKmsAead(aead.Aead):
+  """Implements the Aead interface for AWS KMS."""
+
+  def __init__(self, client: Any, key_arn: str) -> None:
+    self.client = client
+    self.key_arn = key_arn
+
+  def encrypt(self, plaintext: bytes, associated_data: bytes) -> bytes:
+    try:
+      response = self.client.encrypt(
+          KeyId=self.key_arn,
+          Plaintext=plaintext,
+          EncryptionContext=_encryption_context(associated_data),
+      )
+      return response['CiphertextBlob']
+    except exceptions.ClientError as e:
+      raise tink.TinkError(e)
+
+  def decrypt(self, ciphertext: bytes, associated_data: bytes) -> bytes:
+    try:
+      response = self.client.decrypt(
+          KeyId=self.key_arn,
+          CiphertextBlob=ciphertext,
+          EncryptionContext=_encryption_context(associated_data),
+      )
+      return response['Plaintext']
+    except exceptions.ClientError as e:
+      raise tink.TinkError(e)
+
+
+def _key_uri_to_key_arn(key_uri: str) -> str:
+  if not key_uri.startswith(AWS_KEYURI_PREFIX):
+    raise tink.TinkError('invalid key URI')
+  return key_uri[len(AWS_KEYURI_PREFIX) :]
+
+
+def _parse_config(config_path: str) -> Tuple[str, str]:
+  """Returns ('aws_access_key_id', 'aws_secret_access_key') from a config."""
+  config = configparser.ConfigParser()
+  config.read(config_path)
+  if 'default' not in config:
+    raise ValueError('invalid config: default not found')
+  default = config['default']
+  if 'aws_access_key_id' not in default:
+    raise ValueError('invalid config: aws_access_key_id not found')
+  aws_access_key_id = default['aws_access_key_id']
+  if 'aws_secret_access_key' not in default:
+    raise ValueError('invalid config: aws_secret_access_key not found')
+  aws_secret_access_key = default['aws_secret_access_key']
+  return (aws_access_key_id, aws_secret_access_key)
+
+
+def _get_region_from_key_arn(key_arn: str) -> str:
+  # An AWS key ARN is of the form
+  # arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab.
+  key_arn_parts = key_arn.split(':')
+  if len(key_arn_parts) < 6:
+    raise tink.TinkError('invalid key id')
+  return key_arn_parts[3]
+
+
+class AwsKmsClient(_kms_aead_key_manager.KmsClient):
   """Basic AWS client for AEAD."""
 
-  def __init__(self, key_uri: str, credentials_path: str):
+  def __init__(self, key_uri: Optional[str], credentials_path: Optional[str]):
     """Creates a new AwsKmsClient that is bound to the key specified in 'key_uri'.
 
-    Uses the specified credentials when communicating with the KMS. Either of
-    arguments can be empty.
-
-    If 'key_uri' is empty, then the client is not bound to any particular key.
-    If 'credential_path' is empty, then default credentials will be used.
     For more information on credentials and in which order they are loaded see
     https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html.
 
     Args:
-      key_uri: Text, URI of the key the client should be bound to.
-      credentials_path: Text, Path to the file with the access credentials.
+      key_uri: The URI of the key the client should be bound to. If it is None
+          or empty, then the client is not bound to any particular key.
+      credentials_path: Path to the file with the access credentials. If it is
+          None or empty, then default credentials will be used.
 
     Raises:
       ValueError: If the path or filename of the credentials is invalid.
       TinkError: If the key uri is not valid.
     """
-
-    match = re.match('aws-kms://arn:aws:kms:([a-z0-9-]+):', key_uri)
     if not key_uri:
-      self._key_uri = ''
-    elif match:
-      self._key_uri = key_uri
+      self._key_arn = None
     else:
-      raise core.TinkError
-
-    self.cc_client = tink_bindings.AwsKmsClient(key_uri, credentials_path)
+      match = re.match('aws-kms://arn:aws:kms:([a-z0-9-]+):', key_uri)
+      if not match:
+        raise tink.TinkError('invalid key URI')
+      self._key_arn = _key_uri_to_key_arn(key_uri)
+    if not credentials_path:
+      self._aws_access_key_id = None
+      self._aws_secret_access_key = None
+    else:
+      aws_access_key_id, aws_secret_access_key = _parse_config(credentials_path)
+      self._aws_access_key_id = aws_access_key_id
+      self._aws_secret_access_key = aws_secret_access_key
 
   def does_support(self, key_uri: str) -> bool:
-    """Returns true iff this client supports KMS key specified in 'key_uri'.
+    """Returns true if this client supports KMS key specified in 'key_uri'.
 
     Args:
       key_uri: Text, URI of the key to be checked.
@@ -63,9 +142,12 @@
     Returns: A boolean value which is true if the key is supported and false
       otherwise.
     """
-    return self.cc_client.does_support(key_uri)
+    if not key_uri.startswith(AWS_KEYURI_PREFIX):
+      return False
+    if not self._key_arn:
+      return True
+    return _key_uri_to_key_arn(key_uri) == self._key_arn
 
-  @core.use_tink_errors
   def get_aead(self, key_uri: str) -> aead.Aead:
     """Returns an Aead-primitive backed by KMS key specified by 'key_uri'.
 
@@ -78,10 +160,26 @@
     Raises:
       TinkError: If the key_uri is not supported.
     """
-
-    return aead.AeadCcToPyWrapper(self.cc_client.get_aead(key_uri))
+    if not self.does_support(key_uri):
+      if self._key_arn:
+        raise tink.TinkError(
+            'This client is bound to %s and cannot use key %s' %
+            (self._key_arn, key_uri))
+      raise tink.TinkError(
+          'This client does not support key %s' % key_uri)
+    key_arn = _key_uri_to_key_arn(key_uri)
+    session = boto3.session.Session(
+        aws_access_key_id=self._aws_access_key_id,
+        aws_secret_access_key=self._aws_secret_access_key,
+        region_name=_get_region_from_key_arn(key_arn),
+    )
+    return _AwsKmsAead(session.client('kms'), key_arn)
 
   @classmethod
-  def register_client(cls, key_uri, credentials_path) -> None:
+  def register_client(
+      cls, key_uri: Optional[str], credentials_path: Optional[str]
+  ) -> None:
     """Registers the KMS client internally."""
-    tink_bindings.AwsKmsClient.register_client(key_uri, credentials_path)
+    _kms_aead_key_manager.register_kms_client(  # pylint: disable=protected-access
+        AwsKmsClient(key_uri, credentials_path)
+    )
diff --git a/python/tink/integration/awskms/_aws_kms_client_test.py b/python/tink/integration/awskms/_aws_kms_client_test.py
index 24861bb..1968c7b 100644
--- a/python/tink/integration/awskms/_aws_kms_client_test.py
+++ b/python/tink/integration/awskms/_aws_kms_client_test.py
@@ -15,44 +15,120 @@
 
 import os
 
+import tempfile
+
 from absl.testing import absltest
 
-from tink import core
+import tink
 from tink.integration import awskms
+from tink.integration.awskms import _aws_kms_client
 from tink.testing import helper
 
+
 CREDENTIAL_PATH = os.path.join(helper.tink_py_testdata_path(),
                                'aws/credentials.ini')
-KEY_URI = 'aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f'
-BAD_KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key'
+
+KEY_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+           '3ee50705-5a82-4f5b-9753-05c4f473922f')
+
+# An alias for KEY_URI.
+KEY_ALIAS_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:alias/'
+                 'unit-and-integration-testing')
+
+KEY_URI_2 = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+             'b3ca2efd-a8fb-47f2-b541-7e20f8c5cd11')
+
+GCP_KEY_URI = ('gcp-kms://projects/tink-test-infrastructure/locations/global/'
+               'keyRings/unit-and-integration-testing/cryptoKeys/aead-key')
 
 
 class AwsKmsClientTest(absltest.TestCase):
 
-  def test_client_generation(self):
-    aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
-    self.assertNotEqual(aws_client, None)
-
-  def test_wrong_key_uri(self):
-    with self.assertRaises(core.TinkError):
-      awskms.AwsKmsClient(BAD_KEY_URI, CREDENTIAL_PATH)
-
-  def test_client_registration(self):
-    aws_client = awskms.AwsKmsClient('', CREDENTIAL_PATH)
-    aws_client.register_client('', CREDENTIAL_PATH)
-
-  def test_client_not_bound(self):
-    gcp_key1 = 'gcp-kms://projects/someProject/.../cryptoKeys/key1'
-
+  def test_client_bound_to_key_uri(self):
     aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
 
     self.assertEqual(aws_client.does_support(KEY_URI), True)
-    self.assertEqual(aws_client.does_support(gcp_key1), False)
+    self.assertEqual(aws_client.does_support(KEY_ALIAS_URI), False)
+    self.assertEqual(aws_client.does_support(KEY_URI_2), False)
+    self.assertEqual(aws_client.does_support(GCP_KEY_URI), False)
+
+  def test_client_not_bound_to_key_uri(self):
+    aws_client = awskms.AwsKmsClient('', CREDENTIAL_PATH)
+
+    self.assertEqual(aws_client.does_support(KEY_URI), True)
+    self.assertEqual(aws_client.does_support(KEY_ALIAS_URI), True)
+    self.assertEqual(aws_client.does_support(KEY_URI_2), True)
+    self.assertEqual(aws_client.does_support(GCP_KEY_URI), False)
+
+  def test_wrong_key_uri(self):
+    with self.assertRaises(tink.TinkError):
+      awskms.AwsKmsClient(GCP_KEY_URI, CREDENTIAL_PATH)
+
+  def test_client_empty_key_uri(self):
+    aws_client = awskms.AwsKmsClient('', CREDENTIAL_PATH)
+    self.assertEqual(aws_client.does_support(KEY_URI), True)
+
+  def test_client_invalid_path(self):
+    with self.assertRaises(ValueError):
+      awskms.AwsKmsClient('', CREDENTIAL_PATH + 'corrupted')
 
   def test_wrong_credentials_path(self):
     with self.assertRaises(ValueError):
       awskms.AwsKmsClient(KEY_URI, '../credentials.txt')
 
+  def test_parse_valid_credentials_works(self):
+    config_file = tempfile.NamedTemporaryFile(delete=False)
+    with open(config_file.name, 'w') as f:
+      f.write("""
+[otherSection]
+aws_access_key_id = other_key_id
+aws_secret_access_key = other_key
+
+[default]
+aws_access_key_id = key_id_123
+aws_secret_access_key = key_123""")
+
+    aws_access_key_id, aws_secret_access_key = _aws_kms_client._parse_config(
+        config_file.name
+    )
+    self.assertEqual(aws_access_key_id, 'key_id_123')
+    self.assertEqual(aws_secret_access_key, 'key_123')
+
+    os.unlink(config_file.name)
+
+  def test_parse_credentials_without_key_id_fails(self):
+    config_file = tempfile.NamedTemporaryFile(delete=False)
+    with open(config_file.name, 'w') as f:
+      f.write("""
+[default]
+aws_secret_access_key = key_123""")
+    with self.assertRaises(ValueError):
+      _aws_kms_client._parse_config(config_file.name)
+
+    os.unlink(config_file.name)
+
+  def test_parse_credentials_without_key_fails(self):
+    config_file = tempfile.NamedTemporaryFile(delete=False)
+    with open(config_file.name, 'w') as f:
+      f.write("""
+[default]
+aws_secret_access_key = key_123""")
+    with self.assertRaises(ValueError):
+      _aws_kms_client._parse_config(config_file.name)
+
+    os.unlink(config_file.name)
+
+  def test_parse_credentials_without_default_section_fails(self):
+    config_file = tempfile.NamedTemporaryFile(delete=False)
+    with open(config_file.name, 'w') as f:
+      f.write("""
+[otherSection]
+aws_access_key_id = other_key_id
+aws_secret_access_key = other_key""")
+    with self.assertRaises(ValueError):
+      _aws_kms_client._parse_config(config_file.name)
+
+    os.unlink(config_file.name)
 
 if __name__ == '__main__':
   absltest.main()
diff --git a/python/tink/integration/awskms/_aws_kms_integration_test.py b/python/tink/integration/awskms/_aws_kms_integration_test.py
new file mode 100644
index 0000000..eeb1e21
--- /dev/null
+++ b/python/tink/integration/awskms/_aws_kms_integration_test.py
@@ -0,0 +1,207 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for tink.python.tink.integration.aws_kms_aead."""
+
+import os
+
+from absl.testing import absltest
+import botocore
+
+import tink
+from tink import aead
+from tink.aead import _kms_aead_key_manager
+from tink.integration import awskms
+from tink.integration.awskms import _aws_kms_client
+from tink.testing import helper
+
+
+CREDENTIAL_PATH = os.path.join(helper.tink_py_testdata_path(),
+                               'aws/credentials.ini')
+
+BAD_CREDENTIALS_PATH = os.path.join(helper.tink_py_testdata_path(),
+                                    'aws/credentials_bad.ini')
+
+KEY_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+           '3ee50705-5a82-4f5b-9753-05c4f473922f')
+
+# An alias for KEY_URI.
+KEY_ALIAS_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:alias/'
+                 'unit-and-integration-testing')
+
+KEY_URI_2 = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+             'b3ca2efd-a8fb-47f2-b541-7e20f8c5cd11')
+
+GCP_KEY_URI = ('gcp-kms://projects/tink-test-infrastructure/locations/global/'
+               'keyRings/unit-and-integration-testing/cryptoKeys/aead-key')
+
+
+def setUpModule():
+  aead.register()
+
+
+class AwsKmsAeadTest(absltest.TestCase):
+
+  def tearDown(self):
+    super().tearDown()
+    _kms_aead_key_manager.reset_kms_clients()
+
+  def test_encrypt_decrypt(self):
+    aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
+    aws_aead = aws_client.get_aead(KEY_URI)
+
+    plaintext = b'hello'
+    associated_data = b'world'
+    ciphertext = aws_aead.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, aws_aead.decrypt(ciphertext, associated_data))
+
+    plaintext = b'hello'
+    ciphertext = aws_aead.encrypt(plaintext, b'')
+    self.assertEqual(plaintext, aws_aead.decrypt(ciphertext, b''))
+
+  def test_encrypt_decrypt_with_key_alias(self):
+    aws_client = awskms.AwsKmsClient(KEY_ALIAS_URI, CREDENTIAL_PATH)
+    aws_aead = aws_client.get_aead(KEY_ALIAS_URI)
+
+    plaintext = b'hello'
+    associated_data = b'world'
+    ciphertext = aws_aead.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, aws_aead.decrypt(ciphertext, associated_data))
+
+    plaintext = b'hello'
+    ciphertext = aws_aead.encrypt(plaintext, b'')
+    self.assertEqual(plaintext, aws_aead.decrypt(ciphertext, b''))
+
+  def test_corrupted_ciphertext(self):
+    aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
+    aws_aead = aws_client.get_aead(KEY_URI)
+
+    plaintext = b'helloworld'
+    ciphertext = aws_aead.encrypt(plaintext, b'')
+    self.assertEqual(plaintext, aws_aead.decrypt(ciphertext, b''))
+
+    # Corrupt each byte once and check that decryption fails
+    # NOTE: Skipping two bytes as they are malleable
+    for byte_idx in [b for b in range(len(ciphertext)) if b not in [77, 123]]:
+      tmp_ciphertext = list(ciphertext)
+      tmp_ciphertext[byte_idx] ^= 1
+      corrupted_ciphertext = bytes(tmp_ciphertext)
+      with self.assertRaises(tink.TinkError):
+        aws_aead.decrypt(corrupted_ciphertext, b'')
+
+  def test_encrypt_with_bad_uri(self):
+    with self.assertRaises(tink.TinkError):
+      aws_client = awskms.AwsKmsClient(KEY_URI, CREDENTIAL_PATH)
+      aws_client.get_aead(GCP_KEY_URI)
+
+  def test_encrypt_with_bad_credentials(self):
+    aws_client = awskms.AwsKmsClient(KEY_URI, BAD_CREDENTIALS_PATH)
+    aws_aead = aws_client.get_aead(KEY_URI)
+
+    plaintext = b'hello'
+    associated_data = b'world'
+    with self.assertRaises(tink.TinkError):
+      aws_aead.encrypt(plaintext, associated_data)
+
+  def test_client_registration(self):
+    # Register AWS KMS Client bound to KEY_URI.
+    awskms.AwsKmsClient.register_client(KEY_URI, CREDENTIAL_PATH)
+
+    # Create a keyset handle for KEY_URI and use it.
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(KEY_URI)
+    )
+    aws_aead = handle.primitive(aead.Aead)
+    ciphertext = aws_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', aws_aead.decrypt(ciphertext, b'associated_data')
+    )
+
+    # It fails for any other key URI.
+    with self.assertRaises(tink.TinkError):
+      handle2 = tink.new_keyset_handle(
+          aead.aead_key_templates.create_kms_aead_key_template(KEY_URI_2)
+      )
+      gcp_aead = handle2.primitive(aead.Aead)
+      gcp_aead.encrypt(b'plaintext', b'associated_data')
+
+  def test_encrypt_with_default_credentials(self):
+    # If no credentials_path is provided, this path here is used by default.
+    os.environ['AWS_SHARED_CREDENTIALS_FILE'] = CREDENTIAL_PATH
+
+    aws_client = awskms.AwsKmsClient(key_uri=KEY_URI, credentials_path=None)
+    aws_aead = aws_client.get_aead(KEY_URI)
+
+    ciphertext = aws_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', aws_aead.decrypt(ciphertext, b'associated_data')
+    )
+
+  def test_server_side_key_commitment(self):
+    # TODO(b/242678738): Remove direct usage of KMS client and protected
+    # functions in this test once client side key ID verifiaction is removed.
+
+    plaintext = b'hello'
+    associated_data = b'world'
+    encryption_context = _aws_kms_client._encryption_context(associated_data)
+
+    # Confirm that KEY_URI and KEY_ALIAS_URI are interchangeable while the
+    # KEY_ALIAS_URI continues to reference KEY_URI. This no longer holds if
+    # KEY_ALIAS_URI is updated to reference a different key.
+    for k in (KEY_URI, KEY_ALIAS_URI):
+      # Create a ciphertext with k.
+      aws_client = awskms.AwsKmsClient(k, CREDENTIAL_PATH)
+      aws_aead = aws_client.get_aead(k)
+      ciphertext = aws_aead.encrypt(plaintext, associated_data)
+
+      # NOTE: The following operations directly utilize the KMS client to bypass
+      # client-side key commitment checks and to verify KMS behavior for
+      # requests not produced by this implementation (e.g. no KeyId specified).
+
+      # Decrypt with KEY_URI.
+      response = aws_aead.client.decrypt(
+          KeyId=_aws_kms_client._key_uri_to_key_arn(KEY_URI),
+          CiphertextBlob=ciphertext,
+          EncryptionContext=encryption_context,
+      )
+      self.assertEqual(plaintext, response['Plaintext'])
+
+      # Decrypt with KEY_ALIAS_URI.
+      response = aws_aead.client.decrypt(
+          KeyId=_aws_kms_client._key_uri_to_key_arn(KEY_ALIAS_URI),
+          CiphertextBlob=ciphertext,
+          EncryptionContext=encryption_context,
+      )
+      self.assertEqual(plaintext, response['Plaintext'])
+      # AWS KMS always includes resolved key ID in responses, not aliases.
+      self.assertEqual(
+          _aws_kms_client._key_uri_to_key_arn(KEY_URI), response['KeyId'])
+
+      # Decrypt without specifying a key ID in the request.
+      response = aws_aead.client.decrypt(
+          CiphertextBlob=ciphertext,
+          EncryptionContext=encryption_context,
+      )
+      self.assertEqual(plaintext, response['Plaintext'])
+
+      # Attempt to decrypt with KEY_URI_2.
+      with self.assertRaises(botocore.exceptions.ClientError):
+        response = aws_aead.client.decrypt(
+            KeyId=_aws_kms_client._key_uri_to_key_arn(KEY_URI_2),
+            CiphertextBlob=ciphertext,
+            EncryptionContext=encryption_context,
+        )
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/tink/integration/gcpkms/BUILD.bazel b/python/tink/integration/gcpkms/BUILD.bazel
index e98ebe3..ee25408 100644
--- a/python/tink/integration/gcpkms/BUILD.bazel
+++ b/python/tink/integration/gcpkms/BUILD.bazel
@@ -18,9 +18,12 @@
     srcs = ["_gcp_kms_client.py"],
     srcs_version = "PY3",
     deps = [
+        "//tink:tink_python",
         "//tink/aead",
-        "//tink/cc/pybind:tink_bindings",
-        "//tink/core",
+        "//tink/aead:_kms_aead_key_manager",
+        requirement("google-auth"),
+        requirement("google-api-core"),
+        requirement("google-cloud-kms"),
     ],
 )
 
@@ -37,8 +40,8 @@
 )
 
 py_test(
-    name = "_gcp_kms_aead_test",
-    srcs = ["_gcp_kms_aead_test.py"],
+    name = "_gcp_kms_client_integration_test",
+    srcs = ["_gcp_kms_client_integration_test.py"],
     data = [
         "//testdata/gcp:credentials",
         "@google_root_pem//file",
@@ -47,6 +50,28 @@
     tags = ["manual"],
     deps = [
         ":gcpkms",
+        "//tink:tink_python",
+        "//tink/aead",
+        "//tink/testing:helper",
+        requirement("absl-py"),
+    ],
+)
+
+py_test(
+    name = "_gcp_kms_integration_test",
+    srcs = ["_gcp_kms_integration_test.py"],
+    data = [
+        "//testdata/gcp:credentials",
+        "@google_root_pem//file",
+    ],
+    srcs_version = "PY3",
+    tags = ["manual"],
+    deps = [
+        ":gcpkms",
+        "//tink:cleartext_keyset_handle",
+        "//tink:tink_python",
+        "//tink/aead",
+        "//tink/aead:_kms_aead_key_manager",
         "//tink/testing:helper",
         requirement("absl-py"),
     ],
diff --git a/python/tink/integration/gcpkms/_gcp_kms_aead_test.py b/python/tink/integration/gcpkms/_gcp_kms_aead_test.py
deleted file mode 100644
index 938a510..0000000
--- a/python/tink/integration/gcpkms/_gcp_kms_aead_test.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Tests for tink.python.tink.integration.gcp_kms_aead."""
-
-import os
-
-from absl.testing import absltest
-
-from tink import core
-from tink.integration import gcpkms
-from tink.testing import helper
-
-CREDENTIAL_PATH = os.path.join(helper.tink_py_testdata_path(),
-                               'gcp/credential.json')
-KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key'
-LOCAL_KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/europe-west1/keyRings/unit-and-integration-test/cryptoKeys/aead-key'
-BAD_KEY_URI = 'aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f'
-
-if 'TEST_SRCDIR' in os.environ:
-  # Set root certificates for gRPC in Bazel Test which are needed on MacOS
-  os.environ['GRPC_DEFAULT_SSL_ROOTS_FILE_PATH'] = os.path.join(
-      os.environ['TEST_SRCDIR'], 'google_root_pem/file/downloaded')
-
-
-class GcpKmsAeadTest(absltest.TestCase):
-
-  def test_encrypt_decrypt(self):
-    gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
-    aead = gcp_client.get_aead(KEY_URI)
-
-    plaintext = b'helloworld'
-    ciphertext = aead.encrypt(plaintext, b'')
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, b''))
-
-    plaintext = b'hello'
-    associated_data = b'world'
-    ciphertext = aead.encrypt(plaintext, associated_data)
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, associated_data))
-
-  def test_encrypt_decrypt_localized_uri(self):
-    gcp_client = gcpkms.GcpKmsClient(LOCAL_KEY_URI, CREDENTIAL_PATH)
-    aead = gcp_client.get_aead(LOCAL_KEY_URI)
-
-    plaintext = b'helloworld'
-    ciphertext = aead.encrypt(plaintext, b'')
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, b''))
-
-    plaintext = b'hello'
-    associated_data = b'world'
-    ciphertext = aead.encrypt(plaintext, associated_data)
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, associated_data))
-
-  def test_encrypt_with_bad_uri(self):
-    with self.assertRaises(core.TinkError):
-      gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
-      gcp_client.get_aead(BAD_KEY_URI)
-
-  def test_corrupted_ciphertext(self):
-    gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
-    aead = gcp_client.get_aead(KEY_URI)
-
-    plaintext = b'helloworld'
-    ciphertext = aead.encrypt(plaintext, b'')
-    self.assertEqual(plaintext, aead.decrypt(ciphertext, b''))
-
-    # Corrupt each byte once and check that decryption fails
-    # NOTE: Only starting at 4th byte here, as the 3rd byte is malleable
-    #      (see b/146633745).
-    for byte_idx in range(3, len(ciphertext)):
-      tmp_ciphertext = list(ciphertext)
-      tmp_ciphertext[byte_idx] ^= 1
-      corrupted_ciphertext = bytes(tmp_ciphertext)
-      with self.assertRaises(core.TinkError):
-        aead.decrypt(corrupted_ciphertext, b'')
-
-if __name__ == '__main__':
-  absltest.main()
diff --git a/python/tink/integration/gcpkms/_gcp_kms_client.py b/python/tink/integration/gcpkms/_gcp_kms_client.py
index c79c70b..1ba8d64 100644
--- a/python/tink/integration/gcpkms/_gcp_kms_client.py
+++ b/python/tink/integration/gcpkms/_gcp_kms_client.py
@@ -13,28 +13,70 @@
 # limitations under the License.
 """A client for Google Cloud KMS."""
 
+from typing import Optional
+
+from google.api_core import exceptions as core_exceptions
+from google.cloud import kms_v1
+from google.oauth2 import service_account
+
+import tink
 from tink import aead
-from tink import core
-from tink.cc.pybind import tink_bindings
+from tink.aead import _kms_aead_key_manager
 
 GCP_KEYURI_PREFIX = 'gcp-kms://'
 
 
-class GcpKmsClient:
+class _GcpKmsAead(aead.Aead):
+  """Implements the Aead interface for GCP KMS."""
+
+  def __init__(
+      self, client: kms_v1.KeyManagementServiceClient, name: str
+  ) -> None:
+    self.client = client
+    self.name = name
+
+  def encrypt(self, plaintext: bytes, associated_data: bytes) -> bytes:
+    try:
+      response = self.client.encrypt(
+          request=kms_v1.types.service.EncryptRequest(
+              name=self.name,
+              plaintext=plaintext,
+              additional_authenticated_data=associated_data,
+          )
+      )
+      return response.ciphertext
+    except core_exceptions.GoogleAPIError as e:
+      raise tink.TinkError(e)
+
+  def decrypt(self, ciphertext: bytes, associated_data: bytes) -> bytes:
+    try:
+      response = self.client.decrypt(
+         request=kms_v1.types.service.DecryptRequest(
+             name=self.name,
+             ciphertext=ciphertext,
+             additional_authenticated_data=associated_data
+         )
+      )
+      return response.plaintext
+    except core_exceptions.GoogleAPIError as e:
+      raise tink.TinkError(e)
+
+
+class GcpKmsClient(_kms_aead_key_manager.KmsClient):
   """Basic GCP client for AEAD."""
 
-  def __init__(self, key_uri: str, credentials_path: str):
+  def __init__(
+      self, key_uri: Optional[str], credentials_path: Optional[str]
+  ) -> None:
     """Creates a new GcpKmsClient that is bound to the key specified in 'key_uri'.
 
-    Uses the specified credentials when communicating with the KMS. Either of
-    arguments can be empty.
-
-    If 'key_uri' is empty, then the client is not bound to any particular key.
-    If 'credential_path' is empty, then default credentials will be used.
+    Uses the specified credentials when communicating with the KMS.
 
     Args:
-      key_uri: Text, URI of the key the client should be bound to.
-      credentials_path: Text, Path to the file with the access credentials.
+      key_uri: The URI of the key the client should be bound to. If it is None
+          or empty, then the client is not bound to any particular key.
+      credentials_path: Path to the file with the access credentials. If it is
+          None or empty, then default credentials will be used.
 
     Raises:
       ValueError: If the path or filename of the credentials is invalid.
@@ -42,40 +84,58 @@
     """
 
     if not key_uri:
-      self._key_uri = ''
+      self._key_uri = None
     elif key_uri.startswith(GCP_KEYURI_PREFIX):
       self._key_uri = key_uri
     else:
-      raise core.TinkError
-
-    # Use the C++ GCP KMS client
-    self.cc_client = tink_bindings.GcpKmsClient(key_uri, credentials_path)
+      raise tink.TinkError('Invalid key_uri.')
+    if not credentials_path:
+      credentials_path = ''
+    if not credentials_path:
+      self._client = kms_v1.KeyManagementServiceClient()
+      return
+    credentials = service_account.Credentials.from_service_account_file(
+        credentials_path
+    )
+    self._client = kms_v1.KeyManagementServiceClient(credentials=credentials)
 
   def does_support(self, key_uri: str) -> bool:
     """Returns true iff this client supports KMS key specified in 'key_uri'.
 
     Args:
-      key_uri: Text, URI of the key to be checked.
+      key_uri: URI of the key to be checked.
 
     Returns:
       A boolean value which is true if the key is supported and false otherwise.
     """
-    return self.cc_client.does_support(key_uri)
+    if not self._key_uri:
+      return key_uri.startswith(GCP_KEYURI_PREFIX)
+    return key_uri == self._key_uri
 
-  @core.use_tink_errors
   def get_aead(self, key_uri: str) -> aead.Aead:
     """Returns an Aead-primitive backed by KMS key specified by 'key_uri'.
 
     Args:
-      key_uri: Text, URI of the key which should be used.
+      key_uri: URI of the key which should be used.
 
     Returns:
-      The AEAD object...
+      An Aead object.
     """
-
-    return aead.AeadCcToPyWrapper(self.cc_client.get_aead(key_uri))
+    if self._key_uri and self._key_uri != key_uri:
+      raise tink.TinkError(
+          'This client is bound to %s and cannot use key %s'
+          % (self._key_uri, key_uri)
+      )
+    if not key_uri.startswith(GCP_KEYURI_PREFIX):
+      raise tink.TinkError('Invalid key_uri.')
+    key_id = key_uri[len(GCP_KEYURI_PREFIX) :]
+    return _GcpKmsAead(self._client, key_id)
 
   @classmethod
-  def register_client(cls, key_uri, credentials_path) -> None:
+  def register_client(
+      cls, key_uri: Optional[str], credentials_path: Optional[str]
+  ) -> None:
     """Registers the KMS client internally."""
-    tink_bindings.GcpKmsClient.register_client(key_uri, credentials_path)
+    _kms_aead_key_manager.register_kms_client(  # pylint: disable=protected-access
+        GcpKmsClient(key_uri, credentials_path)
+    )
diff --git a/python/tink/integration/gcpkms/_gcp_kms_client_integration_test.py b/python/tink/integration/gcpkms/_gcp_kms_client_integration_test.py
new file mode 100644
index 0000000..cf9adf5
--- /dev/null
+++ b/python/tink/integration/gcpkms/_gcp_kms_client_integration_test.py
@@ -0,0 +1,150 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for tink.python.tink.integration.gcp_kms_aead."""
+
+import os
+
+from absl.testing import absltest
+
+import tink
+from tink import aead
+from tink.integration import gcpkms
+from tink.testing import helper
+
+CREDENTIAL_PATH = os.path.join(helper.tink_py_testdata_path(),
+                               'gcp/credential.json')
+KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key'
+LOCAL_KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/europe-west1/keyRings/unit-and-integration-test/cryptoKeys/aead-key'
+BAD_KEY_URI = 'aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f'
+
+KEY2_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead2-key'
+
+if 'TEST_SRCDIR' in os.environ:
+  # Set root certificates for gRPC in Bazel Test which are needed on MacOS
+  os.environ['GRPC_DEFAULT_SSL_ROOTS_FILE_PATH'] = os.path.join(
+      os.environ['TEST_SRCDIR'], 'google_root_pem/file/downloaded')
+
+
+def setUpModule():
+  aead.register()
+
+
+class GcpKmsAeadTest(absltest.TestCase):
+
+  def test_bound_to_key_uri_encrypt_decrypt(self):
+    gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
+
+    self.assertTrue(gcp_client.does_support(KEY_URI))
+    self.assertFalse(gcp_client.does_support(KEY2_URI))
+    self.assertFalse(gcp_client.does_support(BAD_KEY_URI))
+
+    gcp_aead = gcp_client.get_aead(KEY_URI)
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+    ciphertext = gcp_aead.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, gcp_aead.decrypt(ciphertext, associated_data))
+
+    ciphertext = gcp_aead.encrypt(plaintext, b'')
+    self.assertEqual(plaintext, gcp_aead.decrypt(ciphertext, b''))
+
+    with self.assertRaises(tink.TinkError):
+      gcp_client.get_aead(KEY2_URI)
+
+  def test_not_bound_to_key_uri_encrypt_decrypt(self):
+    gcp_client = gcpkms.GcpKmsClient(None, CREDENTIAL_PATH)
+
+    self.assertTrue(gcp_client.does_support(KEY_URI))
+    self.assertTrue(gcp_client.does_support(KEY2_URI))
+    self.assertFalse(gcp_client.does_support(BAD_KEY_URI))
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+
+    gcp_aead = gcp_client.get_aead(KEY_URI)
+    ciphertext = gcp_aead.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, gcp_aead.decrypt(ciphertext, associated_data))
+
+    gcp_aead2 = gcp_client.get_aead(KEY_URI)
+    ciphertext2 = gcp_aead2.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, gcp_aead2.decrypt(ciphertext2, associated_data))
+
+  def test_decrypt_with_wrong_ad_fails(self):
+    gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
+    gcp_aead = gcp_client.get_aead(KEY_URI)
+
+    ciphertext = gcp_aead.encrypt(b'plaintext', b'associated_data')
+    with self.assertRaises(tink.TinkError):
+      gcp_aead.decrypt(ciphertext, b'wrong_associated_data')
+
+  def test_decrypt_with_wrong_key_fails(self):
+    gcp_client = gcpkms.GcpKmsClient(None, CREDENTIAL_PATH)
+    gcp_aead1 = gcp_client.get_aead(KEY_URI)
+    gcp_aead2 = gcp_client.get_aead(KEY2_URI)
+
+    ciphertext1 = gcp_aead1.encrypt(b'plaintext', b'associated_data')
+    ciphertext2 = gcp_aead2.encrypt(b'plaintext', b'associated_data')
+
+    # First, verify that both key URIs work.
+    self.assertEqual(
+        b'plaintext', gcp_aead1.decrypt(ciphertext1, b'associated_data')
+    )
+    self.assertEqual(
+        b'plaintext', gcp_aead2.decrypt(ciphertext2, b'associated_data')
+    )
+
+    with self.assertRaises(tink.TinkError):
+      gcp_aead2.decrypt(ciphertext1, b'associated_data')
+    with self.assertRaises(tink.TinkError):
+      gcp_aead1.decrypt(ciphertext2, b'associated_data')
+
+  def test_encrypt_decrypt_localized_uri(self):
+    gcp_client = gcpkms.GcpKmsClient(LOCAL_KEY_URI, CREDENTIAL_PATH)
+    gcp_aead = gcp_client.get_aead(LOCAL_KEY_URI)
+
+    plaintext = b'helloworld'
+    ciphertext = gcp_aead.encrypt(plaintext, b'')
+    self.assertEqual(plaintext, gcp_aead.decrypt(ciphertext, b''))
+
+    plaintext = b'hello'
+    associated_data = b'world'
+    ciphertext = gcp_aead.encrypt(plaintext, associated_data)
+    self.assertEqual(plaintext, gcp_aead.decrypt(ciphertext, associated_data))
+
+  def test_encrypt_with_bad_uri(self):
+    with self.assertRaises(tink.TinkError):
+      gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
+      gcp_client.get_aead(BAD_KEY_URI)
+
+  def test_corrupted_ciphertext(self):
+    gcp_client = gcpkms.GcpKmsClient(KEY_URI, CREDENTIAL_PATH)
+    gcp_aead = gcp_client.get_aead(KEY_URI)
+
+    plaintext = b'helloworld'
+    ciphertext = gcp_aead.encrypt(plaintext, b'')
+    self.assertEqual(plaintext, gcp_aead.decrypt(ciphertext, b''))
+
+    # Corrupt each byte once and check that decryption fails
+    # NOTE: Only starting at 4th byte here, as the 3rd byte is malleable
+    #      (see b/146633745).
+    for byte_idx in range(3, len(ciphertext)):
+      tmp_ciphertext = list(ciphertext)
+      tmp_ciphertext[byte_idx] ^= 1
+      corrupted_ciphertext = bytes(tmp_ciphertext)
+      with self.assertRaises(tink.TinkError):
+        gcp_aead.decrypt(corrupted_ciphertext, b'')
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/tink/integration/gcpkms/_gcp_kms_client_test.py b/python/tink/integration/gcpkms/_gcp_kms_client_test.py
index 02cbfba..877b2b1 100644
--- a/python/tink/integration/gcpkms/_gcp_kms_client_test.py
+++ b/python/tink/integration/gcpkms/_gcp_kms_client_test.py
@@ -28,28 +28,37 @@
 
 class GcpKmsClientTest(absltest.TestCase):
 
-  def test_client_generation(self):
-    gcp_client = gcpkms.GcpKmsClient('', CREDENTIAL_PATH)
-    self.assertNotEqual(gcp_client, None)
-
-  def test_client_registration(self):
-    gcp_client = gcpkms.GcpKmsClient('', CREDENTIAL_PATH)
-    gcp_client.register_client('', CREDENTIAL_PATH)
-
-  def test_client_invalid_path(self):
-    with self.assertRaises(ValueError):
-      gcpkms.GcpKmsClient('', CREDENTIAL_PATH + 'corrupted')
-
-  def test_client_not_bound(self):
+  def test_client_bound_to_key_uri(self):
     gcp_key1 = 'gcp-kms://projects/someProject/.../cryptoKeys/key1'
     gcp_key2 = 'gcp-kms://projects/otherProject/.../cryptoKeys/key2'
     non_gcp_key = 'aws-kms://arn:aws:kms:us-west-2:acc:other/key3'
 
-    gcp_client = gcpkms.GcpKmsClient('', CREDENTIAL_PATH)
+    gcp_client = gcpkms.GcpKmsClient(gcp_key1, CREDENTIAL_PATH)
+
+    self.assertEqual(gcp_client.does_support(gcp_key1), True)
+    self.assertEqual(gcp_client.does_support(gcp_key2), False)
+    self.assertEqual(gcp_client.does_support(non_gcp_key), False)
+
+  def test_client_not_bound_to_key_uri(self):
+    gcp_key1 = 'gcp-kms://projects/someProject/.../cryptoKeys/key1'
+    gcp_key2 = 'gcp-kms://projects/otherProject/.../cryptoKeys/key2'
+    non_gcp_key = 'aws-kms://arn:aws:kms:us-west-2:acc:other/key3'
+
+    gcp_client = gcpkms.GcpKmsClient(None, CREDENTIAL_PATH)
 
     self.assertEqual(gcp_client.does_support(gcp_key1), True)
     self.assertEqual(gcp_client.does_support(gcp_key2), True)
     self.assertEqual(gcp_client.does_support(non_gcp_key), False)
 
+  def test_client_empty_key_uri(self):
+    gcp_key = 'gcp-kms://projects/someProject/.../cryptoKeys/key1'
+    gcp_client = gcpkms.GcpKmsClient('', CREDENTIAL_PATH)
+    self.assertEqual(gcp_client.does_support(gcp_key), True)
+
+  def test_client_invalid_path(self):
+    with self.assertRaises(FileNotFoundError):
+      gcpkms.GcpKmsClient(None, CREDENTIAL_PATH + 'corrupted')
+
+
 if __name__ == '__main__':
   absltest.main()
diff --git a/python/tink/integration/gcpkms/_gcp_kms_integration_test.py b/python/tink/integration/gcpkms/_gcp_kms_integration_test.py
new file mode 100644
index 0000000..d328cee
--- /dev/null
+++ b/python/tink/integration/gcpkms/_gcp_kms_integration_test.py
@@ -0,0 +1,208 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Integration Tests for Tink GCP KMS integration."""
+
+import base64
+import os
+
+from absl.testing import absltest
+
+import tink
+from tink import aead
+from tink import cleartext_keyset_handle
+from tink.aead import _kms_aead_key_manager
+from tink.integration import gcpkms
+from tink.testing import helper
+
+CREDENTIAL_PATH = os.path.join(
+    helper.tink_py_testdata_path(), 'gcp/credential.json'
+)
+BAD_CREDENTIAL_PATH = os.path.join(
+    helper.tink_py_testdata_path(), 'gcp/credential_bad.json'
+)
+
+KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key'
+LOCAL_KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/europe-west1/keyRings/unit-and-integration-test/cryptoKeys/aead-key'
+KEY2_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead2-key'
+
+if 'TEST_SRCDIR' in os.environ:
+  # Set root certificates for gRPC in Bazel Test which are needed on MacOS
+  os.environ['GRPC_DEFAULT_SSL_ROOTS_FILE_PATH'] = os.path.join(
+      os.environ['TEST_SRCDIR'], 'google_root_pem/file/downloaded')
+
+
+def setUpModule():
+  aead.register()
+
+
+class GcpKmsIntegrationTest(absltest.TestCase):
+
+  def setUp(self):
+    super().setUp()
+    # Make sure default credentials are not set.
+    os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = ''
+
+  def tearDown(self):
+    super().tearDown()
+    _kms_aead_key_manager.reset_kms_clients()
+
+  def test_aead_from_keyset_handle_for_key_uri_works(self):
+    # Register client not bound to a key URI.
+    gcpkms.GcpKmsClient.register_client('', CREDENTIAL_PATH)
+
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(KEY_URI)
+    )
+    gcp_aead = handle.primitive(aead.Aead)
+
+    ciphertext = gcp_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', gcp_aead.decrypt(ciphertext, b'associated_data')
+    )
+    with self.assertRaises(tink.TinkError):
+      gcp_aead.decrypt(ciphertext, b'invalid')
+
+  def test_envelope_aead_from_keyset_handle(self):
+    # Register client not bound to a key URI.
+    gcpkms.GcpKmsClient.register_client('', CREDENTIAL_PATH)
+
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            KEY_URI, aead.aead_key_templates.AES128_GCM_SIV
+        )
+    )
+    envelope_aead = handle.primitive(aead.Aead)
+
+    ciphertext = envelope_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', envelope_aead.decrypt(ciphertext, b'associated_data')
+    )
+    with self.assertRaises(tink.TinkError):
+      envelope_aead.decrypt(ciphertext, b'invalid')
+
+  def test_aead_from_keyset_handle_for_key2_uri(self):
+    # Register client not bound to a key URI.
+    gcpkms.GcpKmsClient.register_client('', CREDENTIAL_PATH)
+
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(KEY2_URI)
+    )
+    gcp_aead = handle.primitive(aead.Aead)
+
+    ciphertext = gcp_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', gcp_aead.decrypt(ciphertext, b'associated_data')
+    )
+
+  def test_aead_from_keyset_handle_with_invalid_key_uri_fails(self):
+    # Register client not bound to a key URI.
+    gcpkms.GcpKmsClient.register_client('', CREDENTIAL_PATH)
+
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(
+            'aws-kms://arn:aws:kms:us-west-2:acc:other/key3'
+        )
+    )
+    with self.assertRaises(tink.TinkError):
+      gcp_aead = handle.primitive(aead.Aead)
+      gcp_aead.encrypt(b'plaintext', b'associated_data')
+
+  def test_decrypt_ciphertext_encrypted_in_bigquery_using_a_wrapped_keyset(
+      self,
+  ):
+    # Register client not bound to a key URI.
+    gcpkms.GcpKmsClient.register_client('', CREDENTIAL_PATH)
+
+    # This wrapped keyset was generated in BigQuery using this command:
+    # DECLARE kms_key_uri STRING;
+    # SET kms_key_uri =
+    # 'gcp-kms://projects/tink-test-infrastructure/locations/us/keyRings/big-query-test-key/cryptoKeys/aead-key';
+    # SELECT KEYS.NEW_WRAPPED_KEYSET(kms_key_uri, 'AEAD_AES_GCM_256')
+    wrapped_keyset = base64.urlsafe_b64decode(
+        'CiQAv82D2I7RT2gQRd/01m+Md8WAmOyehVog50vs5uPq2B+R36YSlQEAba+J9rC0gfmX9F'
+        'Ss8PsWIpCVbIvPiflsaHRxq5GQjknVgYuJLIMDXlGhQBa3NrfJSmj1T/KDHQ3EzCcPAXtO'
+        'AbAExZr/7jsgiCzo/YQINyPb2rGkW4ofo/BVyvhZ/Pk40iuPHv8Q/PXVrNsq3Y2vkkpsyb'
+        '3QUhJZseURGjjeQnZde6i3EmvDejXhOZJ3XdQUjwgorA=='
+    )
+
+    # This ciphertext was generated in BigQuery using this command:
+    # DECLARE kms_key_uri STRING;
+    # DECLARE wrapped_key BYTES;
+    # SET kms_key_uri =
+    # 'gcp-kms://projects/tink-test-infrastructure/locations/us/keyRings/big-query-test-key/cryptoKeys/aead-key';
+    # SET wrapped_key =
+    # FROM_BASE64('CiQAv82D2I7RT2gQRd/01m+Md8WAmOyehVog50vs5uPq2B+R36YSlQEAba+J9rC0gfmX9FSs8PsWIpCVbIvPiflsaHRxq5GQjknVgYuJLIMDXlGhQBa3NrfJSmj1T/KDHQ3EzCcPAXtOAbAExZr/7jsgiCzo/YQINyPb2rGkW4ofo/BVyvhZ/Pk40iuPHv8Q/PXVrNsq3Y2vkkpsyb3QUhJZseURGjjeQnZde6i3EmvDejXhOZJ3XdQUjwgorA==');
+    # SELECT AEAD.ENCRYPT(KEYS.KEYSET_CHAIN(kms_key_uri, wrapped_key),
+    #     'elephant', 'animal') AS encrypted_animal;
+    ciphertext = base64.urlsafe_b64decode(
+        'AcpCNBevmQnr9momhlKKEyKDOCj5bMfizqC22N/hLZd58LFpC+r99C0='
+    )
+
+    key_uri = 'gcp-kms://projects/tink-test-infrastructure/locations/us/keyRings/big-query-test-key/cryptoKeys/aead-key'
+    gcp_aead_keyset_handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(key_uri)
+    )
+    gcp_aead = gcp_aead_keyset_handle.primitive(aead.Aead)
+
+    unwrapped_keyset = gcp_aead.decrypt(wrapped_keyset, b'')
+    keyset_handle = cleartext_keyset_handle.read(
+        tink.BinaryKeysetReader(unwrapped_keyset))
+    primitive = keyset_handle.primitive(aead.Aead)
+    decrypted = primitive.decrypt(ciphertext, b'animal')
+    self.assertEqual(decrypted, b'elephant')
+
+  def test_registration_client_bound_to_uri_works(self):
+    # Register client bound to KEY_URI.
+    gcpkms.GcpKmsClient.register_client(KEY_URI, CREDENTIAL_PATH)
+
+    # Create a keyset handle for KEY_URI and use it. This works.
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(KEY_URI)
+    )
+    gcp_aead = handle.primitive(aead.Aead)
+    ciphertext = gcp_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', gcp_aead.decrypt(ciphertext, b'associated_data')
+    )
+
+    # But it fails for LOCAL_KEY_URI, since the URI is different.
+    with self.assertRaises(tink.TinkError):
+      handle2 = tink.new_keyset_handle(
+          aead.aead_key_templates.create_kms_aead_key_template(LOCAL_KEY_URI)
+      )
+      gcp_aead = handle2.primitive(aead.Aead)
+      gcp_aead.encrypt(b'plaintext', b'associated_data')
+
+  def test_registration_client_with_default_credentials_works(self):
+    # Set default credentials, see
+    # https://cloud.google.com/docs/authentication/application-default-credentials
+    os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = CREDENTIAL_PATH
+
+    # register_client with credentials_path=None will use the default
+    # credentials.
+    gcpkms.GcpKmsClient.register_client(key_uri=KEY2_URI, credentials_path=None)
+
+    handle = tink.new_keyset_handle(
+        aead.aead_key_templates.create_kms_aead_key_template(KEY2_URI)
+    )
+    gcp_aead = handle.primitive(aead.Aead)
+    ciphertext = gcp_aead.encrypt(b'plaintext', b'associated_data')
+    self.assertEqual(
+        b'plaintext', gcp_aead.decrypt(ciphertext, b'associated_data')
+    )
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/python/tink/jwt/_json_util.py b/python/tink/jwt/_json_util.py
index 18b72e2..f1a589d 100644
--- a/python/tink/jwt/_json_util.py
+++ b/python/tink/jwt/_json_util.py
@@ -23,26 +23,40 @@
   return json.dumps(json_data, separators=(',', ':'))
 
 
-def validate_all_strings(json_data: Any):
-  """Recursivly visits all strings and raises UnicodeEncodeError if invalid."""
-  if isinstance(json_data, str):
-    # We use encode('utf8') to validate that the string is valid.
-    json_data.encode('utf8')
-  if isinstance(json_data, list):
-    for item in json_data:
-      validate_all_strings(item)
-  if isinstance(json_data, dict):
-    for key, value in json_data.items():
-      key.encode('utf8')
-      validate_all_strings(value)
+def _validate_str(value: str) -> None:
+  """Uses encode('utf8') to check if value is a valid string."""
+  _ = value.encode('utf8')
+
+
+def _validate_value(value: Any) -> None:
+  """Validates strings in a JSON object value."""
+  # We don't need to check strings inside dicts, because json.loads will call
+  # _dict_with_validation for these.
+  if isinstance(value, str):
+    _validate_str(value)
+  if isinstance(value, list):
+    for item in value:
+      if isinstance(item, str):
+        _validate_str(item)
+
+
+def _dict_with_validation(pairs):
+  """Validates pairs and returns a dict."""
+  keys = set()
+  for key, value in pairs:
+    _validate_str(key)
+    if key in keys:
+      raise _jwt_error.JwtInvalidError(
+          'Failed to parse JSON string, duplicated key')
+    keys.add(key)
+    _validate_value(value)
+  return dict(pairs)
 
 
 def json_loads(json_text: str) -> Any:
   """Does the same as json.loads, but with some additional validation."""
   try:
-    json_data = json.loads(json_text)
-    validate_all_strings(json_data)
-    return json_data
+    return json.loads(json_text, object_pairs_hook=_dict_with_validation)
   except json.decoder.JSONDecodeError:
     raise _jwt_error.JwtInvalidError('Failed to parse JSON string')
   except RecursionError:
diff --git a/python/tink/jwt/_json_util_test.py b/python/tink/jwt/_json_util_test.py
index 60168e5..527a779 100644
--- a/python/tink/jwt/_json_util_test.py
+++ b/python/tink/jwt/_json_util_test.py
@@ -32,6 +32,10 @@
     with self.assertRaises(_jwt_error.JwtInvalidError):
       _json_util.json_loads('{invalid')
 
+  def test_json_loads_duplidate_entries_fails(self):
+    with self.assertRaises(_jwt_error.JwtInvalidError):
+      _json_util.json_loads('{"a":"a1", "a":"a2"}')
+
   def test_json_loads_recursion(self):
     num_recursions = 1000
     recursive_json = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
diff --git a/python/tink/jwt/_raw_jwt_test.py b/python/tink/jwt/_raw_jwt_test.py
index 8e53aaa..906bc75 100644
--- a/python/tink/jwt/_raw_jwt_test.py
+++ b/python/tink/jwt/_raw_jwt_test.py
@@ -289,6 +289,12 @@
     token = jwt.RawJwt._from_json(None, json.dumps(payload))
     self.assertEqual(json.loads(token.json_payload()), payload)
 
+  def test_integer_is_encoded_as_integer(self):
+    token = jwt.new_raw_jwt(
+        without_expiration=True,
+        custom_claims={'num': 1})
+    self.assertEqual(token.json_payload(), '{"num":1}')
+
   def test_exp_to_payload(self):
     expiration = datetime.datetime.fromtimestamp(2218027244,
                                                  datetime.timezone.utc)
@@ -382,6 +388,14 @@
     with self.assertRaises(jwt.JwtInvalidError):
       jwt.RawJwt._from_json(None, u'{"a":{"a":{"a":"\\uD834"}}}')
 
+  def test_from_payload_with_duplicate_map_keys_fails(self):
+    with self.assertRaises(jwt.JwtInvalidError):
+      jwt.RawJwt._from_json(None, '{"claim": "claim1", "claim": "claim2"}')
+    with self.assertRaises(jwt.JwtInvalidError):
+      jwt.RawJwt._from_json(None, '{"nested": {"a: "a1", "a": "a2"}}')
+    # this is fine, since the two 'a' keys are not in the same map
+    _ = jwt.RawJwt._from_json(None, '{"a": "a1", "b": {"a": "a2"}}')
+
   def test_modification(self):
     audiences = ['alice', 'bob']
     my_claim = {'one': 'two'}
diff --git a/python/tink/mac/_mac_key_templates.py b/python/tink/mac/_mac_key_templates.py
index 3b57844..926f774 100644
--- a/python/tink/mac/_mac_key_templates.py
+++ b/python/tink/mac/_mac_key_templates.py
@@ -35,10 +35,11 @@
   key_format.params.hash = hash_type
   key_format.params.tag_size = tag_size
   key_format.key_size = key_size
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = 'type.googleapis.com/google.crypto.tink.HmacKey'
-  key_template.output_prefix_type = tink_pb2.TINK
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url='type.googleapis.com/google.crypto.tink.HmacKey',
+      output_prefix_type=tink_pb2.TINK,
+  )
   return key_template
 
 
diff --git a/python/tink/prf/_prf_key_templates.py b/python/tink/prf/_prf_key_templates.py
index 274b5d3..22d5054 100644
--- a/python/tink/prf/_prf_key_templates.py
+++ b/python/tink/prf/_prf_key_templates.py
@@ -37,10 +37,11 @@
   key_format = aes_cmac_prf_pb2.AesCmacPrfKeyFormat()
   key_format.key_size = key_size
   key_format.version = 0
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_CMAC_PRF_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.RAW
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_CMAC_PRF_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.RAW,
+  )
   return key_template
 
 
@@ -51,10 +52,11 @@
   key_format.params.hash = hash_type
   key_format.key_size = key_size
   key_format.version = 0
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _HMAC_PRF_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.RAW
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_HMAC_PRF_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.RAW,
+  )
   return key_template
 
 
diff --git a/python/tink/proto/aes_cmac.proto b/python/tink/proto/aes_cmac.proto
index b214834..541ff58 100644
--- a/python/tink/proto/aes_cmac.proto
+++ b/python/tink/proto/aes_cmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_go_proto";
 
 message AesCmacParams {
   uint32 tag_size = 1;
diff --git a/python/tink/proto/aes_cmac_prf.proto b/python/tink/proto/aes_cmac_prf.proto
index 58e5f67..b2efc6d 100644
--- a/python/tink/proto/aes_cmac_prf.proto
+++ b/python/tink/proto/aes_cmac_prf.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_cmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_cmac_prf_go_proto";
 
 // key_type: type.googleapis.com/google.crypto.tink.AesCmacPrfKey
 message AesCmacPrfKey {
diff --git a/python/tink/proto/aes_ctr.proto b/python/tink/proto/aes_ctr.proto
index ecdb256..721699c 100644
--- a/python/tink/proto/aes_ctr.proto
+++ b/python/tink/proto/aes_ctr.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_go_proto";
 
 message AesCtrParams {
   uint32 iv_size = 1;
diff --git a/python/tink/proto/aes_ctr_hmac_aead.proto b/python/tink/proto/aes_ctr_hmac_aead.proto
index bde8649..71a6c49 100644
--- a/python/tink/proto/aes_ctr_hmac_aead.proto
+++ b/python/tink/proto/aes_ctr_hmac_aead.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_aead_go_proto";
 
 message AesCtrHmacAeadKeyFormat {
   AesCtrKeyFormat aes_ctr_key_format = 1;
diff --git a/python/tink/proto/aes_ctr_hmac_streaming.proto b/python/tink/proto/aes_ctr_hmac_streaming.proto
index 520a890..45963e5 100644
--- a/python/tink/proto/aes_ctr_hmac_streaming.proto
+++ b/python/tink/proto/aes_ctr_hmac_streaming.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_ctr_hmac_streaming_go_proto";
 
 message AesCtrHmacStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/python/tink/proto/aes_eax.proto b/python/tink/proto/aes_eax.proto
index c673306..c1bf500 100644
--- a/python/tink/proto/aes_eax.proto
+++ b/python/tink/proto/aes_eax.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_eax_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_eax_go_proto";
 
 // only allowing tag size in bytes = 16
 message AesEaxParams {
diff --git a/python/tink/proto/aes_gcm.proto b/python/tink/proto/aes_gcm.proto
index fba7a89..2551aa4 100644
--- a/python/tink/proto/aes_gcm.proto
+++ b/python/tink/proto/aes_gcm.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_go_proto";
 option objc_class_prefix = "TINKPB";
 
 message AesGcmKeyFormat {
diff --git a/python/tink/proto/aes_gcm_hkdf_streaming.proto b/python/tink/proto/aes_gcm_hkdf_streaming.proto
index 15985f0..1d921ef 100644
--- a/python/tink/proto/aes_gcm_hkdf_streaming.proto
+++ b/python/tink/proto/aes_gcm_hkdf_streaming.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_hkdf_streaming_go_proto";
 
 message AesGcmHkdfStreamingParams {
   uint32 ciphertext_segment_size = 1;
diff --git a/python/tink/proto/aes_gcm_siv.proto b/python/tink/proto/aes_gcm_siv.proto
index df9fada..220d79f 100644
--- a/python/tink/proto/aes_gcm_siv.proto
+++ b/python/tink/proto/aes_gcm_siv.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_gcm_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_gcm_siv_go_proto";
 
 // The only allowed IV size is 12 bytes and tag size is 16 bytes.
 // Thus, accept no params.
diff --git a/python/tink/proto/aes_siv.proto b/python/tink/proto/aes_siv.proto
index 0023027..ccb8d3c 100644
--- a/python/tink/proto/aes_siv.proto
+++ b/python/tink/proto/aes_siv.proto
@@ -20,7 +20,15 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/aes_siv_go_proto";
+option go_package = "github.com/google/tink/go/proto/aes_siv_go_proto";
+
+// Tink implements RFC 5297 (https://www.rfc-editor.org/rfc/rfc5297) for
+// AES-SIV, putting the SIV/Tag at the beginning of the ciphertext.
+//
+// While the RFC 5297 supports a list of associated datas, Tink only supports
+// exactly one associated data, which corresponds to a list with one element in
+// RFC 5297. An empty associated data is a list with one empty element, and not
+// an empty list.
 
 message AesSivKeyFormat {
   // Only valid value is: 64.
diff --git a/python/tink/proto/cached_dek_aead.proto b/python/tink/proto/cached_dek_aead.proto
index 10bcde5..9b1a33f 100644
--- a/python/tink/proto/cached_dek_aead.proto
+++ b/python/tink/proto/cached_dek_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/cached_dek_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_aead_go_proto";
 
 message CachedDekAeadKeyFormat {
   // Required.
diff --git a/python/tink/proto/cached_dek_envelope.proto b/python/tink/proto/cached_dek_envelope.proto
index 84fbc66..17f330f01 100644
--- a/python/tink/proto/cached_dek_envelope.proto
+++ b/python/tink/proto/cached_dek_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_multiple_files = true;
 option java_package = "com.google.crypto.tink.proto";
-option go_package = "github.com/google/tink/proto/cached_dek_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/cached_dek_envelope_go_proto";
 
 message CachedDekEnvelopeAeadKeyFormat {
   // Required.
diff --git a/python/tink/proto/chacha20_poly1305.proto b/python/tink/proto/chacha20_poly1305.proto
index 2cd6ead..ef8ab6e 100644
--- a/python/tink/proto/chacha20_poly1305.proto
+++ b/python/tink/proto/chacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/chacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/chacha20_poly1305_go_proto";
 
 message ChaCha20Poly1305KeyFormat {}
 
diff --git a/python/tink/proto/common.proto b/python/tink/proto/common.proto
index eaff8d3..4546064 100644
--- a/python/tink/proto/common.proto
+++ b/python/tink/proto/common.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/common_go_proto";
+option go_package = "github.com/google/tink/go/proto/common_go_proto";
 
 enum EllipticCurveType {
   UNKNOWN_CURVE = 0;
diff --git a/python/tink/proto/config.proto b/python/tink/proto/config.proto
index ebbd742..cff6506 100644
--- a/python/tink/proto/config.proto
+++ b/python/tink/proto/config.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/config_go_proto";
+option go_package = "github.com/google/tink/go/proto/config_go_proto";
 
 // An entry that describes a key type to be used with Tink library,
 // specifying the corresponding primitive, key manager, and deprecation status.
diff --git a/python/tink/proto/ecdsa.proto b/python/tink/proto/ecdsa.proto
index 043a3d8..63351c5 100644
--- a/python/tink/proto/ecdsa.proto
+++ b/python/tink/proto/ecdsa.proto
@@ -23,7 +23,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecdsa_go_proto";
 
 enum EcdsaSignatureEncoding {
   UNKNOWN_ENCODING = 0;
@@ -80,4 +80,5 @@
 message EcdsaKeyFormat {
   // Required.
   EcdsaParams params = 2;
+  uint32 version = 3;
 }
diff --git a/python/tink/proto/ecies_aead_hkdf.proto b/python/tink/proto/ecies_aead_hkdf.proto
index 08ffcb2..1f01069 100644
--- a/python/tink/proto/ecies_aead_hkdf.proto
+++ b/python/tink/proto/ecies_aead_hkdf.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ecies_aead_hkdf_go_proto";
+option go_package = "github.com/google/tink/go/proto/ecies_aead_hkdf_go_proto";
 
 // Protos for keys for ECIES with HKDF and AEAD encryption.
 //
diff --git a/python/tink/proto/ed25519.proto b/python/tink/proto/ed25519.proto
index 669f33a..613c59f 100644
--- a/python/tink/proto/ed25519.proto
+++ b/python/tink/proto/ed25519.proto
@@ -23,10 +23,10 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/ed25519_go_proto";
+option go_package = "github.com/google/tink/go/proto/ed25519_go_proto";
 
 message Ed25519KeyFormat {
-    uint32 version = 1;
+  uint32 version = 1;
 }
 
 // key_type: type.googleapis.com/google.crypto.tink.Ed25519PublicKey
diff --git a/python/tink/proto/empty.proto b/python/tink/proto/empty.proto
index 33831a9..beeba07 100644
--- a/python/tink/proto/empty.proto
+++ b/python/tink/proto/empty.proto
@@ -20,6 +20,6 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/empty_go_proto";
+option go_package = "github.com/google/tink/go/proto/empty_go_proto";
 
 message Empty {}
diff --git a/python/tink/proto/hkdf_prf.proto b/python/tink/proto/hkdf_prf.proto
index 9782850..1ac6331 100644
--- a/python/tink/proto/hkdf_prf.proto
+++ b/python/tink/proto/hkdf_prf.proto
@@ -22,12 +22,16 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hkdf_prf_proto";
+option go_package = "github.com/google/tink/go/proto/hkdf_prf_proto";
 
 message HkdfPrfParams {
   HashType hash = 1;
-  // Salt, optional in RFC 5869. Using "" is equivalent to zeros of length up to
-  // the block length of the HMac.
+  // Optional.
+  //
+  // An unspecified or zero-length value is equivalent to a sequence of zeros
+  // (0x00) with a length equal to the output size of hash.
+  //
+  // See https://rfc-editor.org/rfc/rfc5869.
   bytes salt = 2;
 }
 
diff --git a/python/tink/proto/hmac.proto b/python/tink/proto/hmac.proto
index 9395845..e85f5fe 100644
--- a/python/tink/proto/hmac.proto
+++ b/python/tink/proto/hmac.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_go_proto";
 
 message HmacParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/python/tink/proto/hmac_prf.proto b/python/tink/proto/hmac_prf.proto
index 386c921..23be4e8 100644
--- a/python/tink/proto/hmac_prf.proto
+++ b/python/tink/proto/hmac_prf.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hmac_prf_go_proto";
+option go_package = "github.com/google/tink/go/proto/hmac_prf_go_proto";
 
 message HmacPrfParams {
   HashType hash = 1;  // HashType is an enum.
diff --git a/python/tink/proto/hpke.proto b/python/tink/proto/hpke.proto
index 847864a..f794e77 100644
--- a/python/tink/proto/hpke.proto
+++ b/python/tink/proto/hpke.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/hpke_proto";
+option go_package = "github.com/google/tink/go/proto/hpke_proto";
 
 enum HpkeKem {
   KEM_UNKNOWN = 0;
diff --git a/python/tink/proto/jwt_ecdsa.proto b/python/tink/proto/jwt_ecdsa.proto
index 4c80fe1..ce78b04 100644
--- a/python/tink/proto/jwt_ecdsa.proto
+++ b/python/tink/proto/jwt_ecdsa.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_ecdsa_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_ecdsa_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.4
 enum JwtEcdsaAlgorithm {
diff --git a/python/tink/proto/jwt_hmac.proto b/python/tink/proto/jwt_hmac.proto
index e54a51d..a499638 100644
--- a/python/tink/proto/jwt_hmac.proto
+++ b/python/tink/proto/jwt_hmac.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_hmac_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_hmac_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
 enum JwtHmacAlgorithm {
diff --git a/python/tink/proto/jwt_rsa_ssa_pkcs1.proto b/python/tink/proto/jwt_rsa_ssa_pkcs1.proto
index adf31c8..54a9731 100644
--- a/python/tink/proto/jwt_rsa_ssa_pkcs1.proto
+++ b/python/tink/proto/jwt_rsa_ssa_pkcs1.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.3
 enum JwtRsaSsaPkcs1Algorithm {
diff --git a/python/tink/proto/jwt_rsa_ssa_pss.proto b/python/tink/proto/jwt_rsa_ssa_pss.proto
index 4312645..eb2d454 100644
--- a/python/tink/proto/jwt_rsa_ssa_pss.proto
+++ b/python/tink/proto/jwt_rsa_ssa_pss.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/jwt_rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/jwt_rsa_ssa_pss_go_proto";
 
 // See https://datatracker.ietf.org/doc/html/rfc7518#section-3.5
 enum JwtRsaSsaPssAlgorithm {
diff --git a/python/tink/proto/kms_aead.proto b/python/tink/proto/kms_aead.proto
index e818788..16de8ee 100644
--- a/python/tink/proto/kms_aead.proto
+++ b/python/tink/proto/kms_aead.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_aead_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_aead_go_proto";
 
 message KmsAeadKeyFormat {
   // Required.
diff --git a/python/tink/proto/kms_envelope.proto b/python/tink/proto/kms_envelope.proto
index 17888d9..8db39cb 100644
--- a/python/tink/proto/kms_envelope.proto
+++ b/python/tink/proto/kms_envelope.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/kms_envelope_go_proto";
+option go_package = "github.com/google/tink/go/proto/kms_envelope_go_proto";
 
 message KmsEnvelopeAeadKeyFormat {
   // Required.
diff --git a/python/tink/proto/prf_based_deriver.proto b/python/tink/proto/prf_based_deriver.proto
index c0364ae..5164d96 100644
--- a/python/tink/proto/prf_based_deriver.proto
+++ b/python/tink/proto/prf_based_deriver.proto
@@ -22,7 +22,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/prf_based_deriver_go_proto";
+option go_package = "github.com/google/tink/go/proto/prf_based_deriver_go_proto";
 
 message PrfBasedDeriverParams {
   KeyTemplate derived_key_template = 1;
diff --git a/python/tink/proto/rsa_ssa_pkcs1.proto b/python/tink/proto/rsa_ssa_pkcs1.proto
index fd5fd4e..558ed81 100644
--- a/python/tink/proto/rsa_ssa_pkcs1.proto
+++ b/python/tink/proto/rsa_ssa_pkcs1.proto
@@ -24,7 +24,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pkcs1_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pkcs1_go_proto";
 
 message RsaSsaPkcs1Params {
   // Hash function used in computing hash of the signing message
diff --git a/python/tink/proto/rsa_ssa_pss.proto b/python/tink/proto/rsa_ssa_pss.proto
index a4817c0..bbb290d 100644
--- a/python/tink/proto/rsa_ssa_pss.proto
+++ b/python/tink/proto/rsa_ssa_pss.proto
@@ -25,7 +25,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/rsa_ssa_pss_go_proto";
+option go_package = "github.com/google/tink/go/proto/rsa_ssa_pss_go_proto";
 
 message RsaSsaPssParams {
   // Hash function used in computing hash of the signing message
diff --git a/python/tink/proto/tink.proto b/python/tink/proto/tink.proto
index 1787581..8b3d100 100644
--- a/python/tink/proto/tink.proto
+++ b/python/tink/proto/tink.proto
@@ -21,7 +21,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/tink_go_proto";
+option go_package = "github.com/google/tink/go/proto/tink_go_proto";
 option objc_class_prefix = "TINKPB";
 
 // Each instantiation of a Tink primitive is identified by type_url,
diff --git a/python/tink/proto/xchacha20_poly1305.proto b/python/tink/proto/xchacha20_poly1305.proto
index cc52624..a2613f1 100644
--- a/python/tink/proto/xchacha20_poly1305.proto
+++ b/python/tink/proto/xchacha20_poly1305.proto
@@ -20,7 +20,7 @@
 
 option java_package = "com.google.crypto.tink.proto";
 option java_multiple_files = true;
-option go_package = "github.com/google/tink/proto/xchacha20_poly1305_go_proto";
+option go_package = "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto";
 
 message XChaCha20Poly1305KeyFormat {
   uint32 version = 1;
diff --git a/python/tink/streaming_aead/BUILD.bazel b/python/tink/streaming_aead/BUILD.bazel
index aa589b5..1419621 100644
--- a/python/tink/streaming_aead/BUILD.bazel
+++ b/python/tink/streaming_aead/BUILD.bazel
@@ -170,7 +170,11 @@
     deps = [
         ":streaming_aead",
         requirement("absl-py"),
+        "//tink:cleartext_keyset_handle",
         "//tink:tink_python",
+        "//tink/proto:aes_gcm_hkdf_streaming_py_pb2",
+        "//tink/proto:common_py_pb2",
+        "//tink/proto:tink_py_pb2",
         "//tink/testing:bytes_io",
         "//tink/testing:keyset_builder",
     ],
diff --git a/python/tink/streaming_aead/_streaming_aead_key_manager_test.py b/python/tink/streaming_aead/_streaming_aead_key_manager_test.py
index c7c0ff0..1273689 100644
--- a/python/tink/streaming_aead/_streaming_aead_key_manager_test.py
+++ b/python/tink/streaming_aead/_streaming_aead_key_manager_test.py
@@ -21,6 +21,7 @@
 from tink.proto import aes_gcm_hkdf_streaming_pb2
 from tink.proto import common_pb2
 from tink.proto import tink_pb2
+import tink
 from tink import core
 from tink import streaming_aead
 from tink.streaming_aead import _raw_streaming_aead
@@ -220,5 +221,39 @@
       with self.assertRaises(core.TinkError):
         ds.read()
 
+  @parameterized.parameters([
+      streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_4KB,
+      streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_1MB,
+      streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_4KB,
+      streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_1MB,
+      streaming_aead.streaming_aead_key_templates.AES128_CTR_HMAC_SHA256_4KB,
+      streaming_aead.streaming_aead_key_templates.AES128_CTR_HMAC_SHA256_1MB,
+      streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_4KB,
+      streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_1MB
+  ])
+  def test_encrypt_decrypt_success(self, template):
+    keyset_handle = tink.new_keyset_handle(template)
+    primitive = keyset_handle.primitive(streaming_aead.StreamingAead)
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+
+    # Encrypt
+    ciphertext_destination = bytes_io.BytesIOWithValueAfterClose()
+    with primitive.new_encrypting_stream(ciphertext_destination,
+                                         associated_data) as encryption_stream:
+      encryption_stream.write(plaintext)
+
+    ciphertext = ciphertext_destination.value_after_close()
+
+    # Decrypt
+    ciphertext_source = io.BytesIO(ciphertext)
+    decrypted = None
+    with primitive.new_decrypting_stream(ciphertext_source,
+                                         associated_data) as decryption_stream:
+      decrypted = decryption_stream.read()
+
+    self.assertEqual(decrypted, plaintext)
+
 if __name__ == '__main__':
   absltest.main()
diff --git a/python/tink/streaming_aead/_streaming_aead_key_templates.py b/python/tink/streaming_aead/_streaming_aead_key_templates.py
index ddfc54b..526fd4b 100644
--- a/python/tink/streaming_aead/_streaming_aead_key_templates.py
+++ b/python/tink/streaming_aead/_streaming_aead_key_templates.py
@@ -48,10 +48,11 @@
   key_format.params.derived_key_size = derived_key_size
   key_format.params.ciphertext_segment_size = ciphertext_segment_size
 
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_GCM_HKDF_STREAMING_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.RAW
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_GCM_HKDF_STREAMING_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.RAW,
+  )
   return key_template
 
 
@@ -70,10 +71,11 @@
   key_format.params.hmac_params.hash = mac_hash_type
   key_format.params.hmac_params.tag_size = tag_size
 
-  key_template = tink_pb2.KeyTemplate()
-  key_template.value = key_format.SerializeToString()
-  key_template.type_url = _AES_CTR_HMAC_STREAMING_KEY_TYPE_URL
-  key_template.output_prefix_type = tink_pb2.RAW
+  key_template = tink_pb2.KeyTemplate(
+      value=key_format.SerializeToString(),
+      type_url=_AES_CTR_HMAC_STREAMING_KEY_TYPE_URL,
+      output_prefix_type=tink_pb2.RAW,
+  )
   return key_template
 
 
diff --git a/python/tink/streaming_aead/_streaming_aead_wrapper.py b/python/tink/streaming_aead/_streaming_aead_wrapper.py
index 0388e85..830d0f7 100644
--- a/python/tink/streaming_aead/_streaming_aead_wrapper.py
+++ b/python/tink/streaming_aead/_streaming_aead_wrapper.py
@@ -47,8 +47,12 @@
         ciphertext_source)
     self._associated_data = associated_data
     self._matching_stream = None
-    self._remaining_primitives = [
-        entry.primitive for entry in primitive_set.raw_primitives()]
+    self._remaining_primitives = []
+    # For legacy reasons (Tink always encrypted with non-RAW keys) we use all
+    # primitives, even those which have output_prefix_type != RAW.
+    for entry_list in primitive_set.all():
+      for e in entry_list:
+        self._remaining_primitives.append(e.primitive)
     self._attempting_stream = self._next_decrypting_stream()
 
   def _next_decrypting_stream(self) -> io.RawIOBase:
diff --git a/python/tink/streaming_aead/_streaming_aead_wrapper_test.py b/python/tink/streaming_aead/_streaming_aead_wrapper_test.py
index 0e2d2a6..93af3cc 100644
--- a/python/tink/streaming_aead/_streaming_aead_wrapper_test.py
+++ b/python/tink/streaming_aead/_streaming_aead_wrapper_test.py
@@ -18,13 +18,18 @@
 
 from absl.testing import absltest
 from absl.testing import parameterized
+from tink.proto import aes_gcm_hkdf_streaming_pb2
+from tink.proto import common_pb2
+from tink.proto import tink_pb2
 import tink
+from tink import cleartext_keyset_handle
 from tink import streaming_aead
 from tink.testing import bytes_io
 from tink.testing import keyset_builder
 
 
 TEMPLATE = streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_4KB
+TYPE_URL = 'type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey'
 
 
 def setUpModule():
@@ -220,6 +225,68 @@
         cast(BinaryIO, input_stream_factory(ciphertext4)), b'aad4') as ds:
       self.assertEqual(ds.read(), plaintext4)
 
+  def test_decrypt_tink_output_prefix(self):
+    key = aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey(
+        version=0,
+        params=aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingParams(
+            ciphertext_segment_size=512,
+            derived_key_size=16,
+            hkdf_hash_type=common_pb2.HashType.SHA256,
+        ),
+        key_value=b'0123456789abcdef',
+    )
+    value1 = key.SerializeToString()
+    key.key_value = b'ABCDEF0123456789'
+    value2 = key.SerializeToString()
+
+    # We use two keys so that we have at least 1 raw key in the keyset: Tink
+    # has a check that creating a new StreamingAead fails when the keyset does
+    # not contain any raw keys, and we only want this to fail on decryption.
+    keyset = tink_pb2.Keyset(
+        primary_key_id=1,
+        key=[
+            tink_pb2.Keyset.Key(
+                key_data=tink_pb2.KeyData(
+                    type_url=TYPE_URL,
+                    value=value1,
+                    key_material_type=tink_pb2.KeyData.SYMMETRIC,
+                ),
+                output_prefix_type=tink_pb2.OutputPrefixType.TINK,
+                status=tink_pb2.KeyStatusType.ENABLED,
+                key_id=1,
+            ),
+            tink_pb2.Keyset.Key(
+                key_data=tink_pb2.KeyData(
+                    type_url=TYPE_URL,
+                    value=value2,
+                    key_material_type=tink_pb2.KeyData.SYMMETRIC,
+                ),
+                output_prefix_type=tink_pb2.OutputPrefixType.RAW,
+                status=tink_pb2.KeyStatusType.ENABLED,
+                key_id=2,
+            ),
+        ],
+    )
+
+    keyset_handle = cleartext_keyset_handle.from_keyset(keyset)
+    primitive = keyset_handle.primitive(streaming_aead.StreamingAead)
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+
+    ciphertext_dest = bytes_io.BytesIOWithValueAfterClose()
+    with primitive.new_encrypting_stream(
+        ciphertext_dest, associated_data
+    ) as es:
+      self.assertLen(plaintext, es.write(plaintext))
+    self.assertTrue(ciphertext_dest.closed)
+
+    ciphertext_src = io.BytesIO(ciphertext_dest.value_after_close())
+    with primitive.new_decrypting_stream(ciphertext_src, associated_data) as ds:
+      output = ds.read()
+    self.assertTrue(ciphertext_src.closed)
+    self.assertEqual(output, plaintext)
+
 
 if __name__ == '__main__':
   absltest.main()
diff --git a/python/tink/testing/BUILD.bazel b/python/tink/testing/BUILD.bazel
index a8fb41f..98b3504 100644
--- a/python/tink/testing/BUILD.bazel
+++ b/python/tink/testing/BUILD.bazel
@@ -84,7 +84,11 @@
     srcs = ["fake_kms.py"],
     srcs_version = "PY3",
     deps = [
-        "//tink/cc/pybind:tink_bindings",
+        "//tink:cleartext_keyset_handle",
+        "//tink:tink_python",
+        "//tink/aead",
+        "//tink/aead:_kms_aead_key_manager",
+        "//tink/core",
     ],
 )
 
diff --git a/python/tink/testing/fake_kms.py b/python/tink/testing/fake_kms.py
index ae4fba9..bc6875a 100644
--- a/python/tink/testing/fake_kms.py
+++ b/python/tink/testing/fake_kms.py
@@ -13,13 +13,49 @@
 # limitations under the License.
 """A client for Fake KMS."""
 
-from tink.cc.pybind import tink_bindings
+import base64
+
+from typing import Optional
+
+import tink
+from tink import aead
+from tink import cleartext_keyset_handle
+from tink.aead import _kms_aead_key_manager
+
+
+FAKE_KMS_PREFIX = 'fake-kms://'
+
+
+class FakeKmsClient(_kms_aead_key_manager.KmsClient):
+  """A fake KMS client."""
+
+  def __init__(self, key_uri: Optional[str] = None):
+    if not key_uri:
+      self._key_uri = None
+    elif key_uri.startswith(FAKE_KMS_PREFIX):
+      self._key_uri = key_uri
+    else:
+      raise tink.TinkError('invalid key URI')
+
+  def does_support(self, key_uri: str) -> bool:
+    if not key_uri.startswith(FAKE_KMS_PREFIX):
+      return False
+    if not self._key_uri:
+      return True
+    return key_uri == self._key_uri
+
+  def get_aead(self, key_uri: str) -> aead.Aead:
+    if not key_uri.startswith(FAKE_KMS_PREFIX):
+      raise tink.TinkError('invalid key URI')
+    key_id = key_uri[len(FAKE_KMS_PREFIX) :]
+    serialized_key = base64.urlsafe_b64decode(key_id.encode('utf-8') + b'===')
+    handle = cleartext_keyset_handle.read(
+        tink.BinaryKeysetReader(serialized_key)
+    )
+    return handle.primitive(aead.Aead)
 
 
 def register_client(key_uri=None, credentials_path=None) -> None:
   """Registers a fake KMS client."""
-  if not key_uri:
-    key_uri = ''
-  if not credentials_path:
-    credentials_path = ''
-  tink_bindings.register_fake_kms_client_testonly(key_uri, credentials_path)
+  _ = credentials_path
+  _kms_aead_key_manager.register_kms_client(FakeKmsClient(key_uri))
diff --git a/python/tink/testing/fake_kms_test.py b/python/tink/testing/fake_kms_test.py
index 9913e34..ae86629 100644
--- a/python/tink/testing/fake_kms_test.py
+++ b/python/tink/testing/fake_kms_test.py
@@ -31,6 +31,12 @@
 
 class FakeKmsTest(absltest.TestCase):
 
+  def test_fake_kms_doesn_not_support_other_kms(self):
+    client = fake_kms.FakeKmsClient()
+    self.assertFalse(
+        client.does_support('aws-kms://arn:aws:kms:us-east-2:12345:key/12345')
+    )
+
   def test_fake_kms_aead_encrypt_decrypt(self):
     template = aead.aead_key_templates.create_kms_aead_key_template(
         key_uri=KEY_URI)
diff --git a/python/tink_py_deps.bzl b/python/tink_py_deps.bzl
index f27a0e2..9e7dc5b 100644
--- a/python/tink_py_deps.bzl
+++ b/python/tink_py_deps.bzl
@@ -1,20 +1,24 @@
-"""
-Dependencies of Python Tink
-"""
+"""tink-py dependencies."""
 
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
 
 def tink_py_deps():
-    """ Loads dependencies of Python Tink.
-    """
+    """Loads dependencies of tink-py."""
+    if not native.existing_rule("google_root_pem"):
+        http_file(
+            name = "google_root_pem",
+            executable = 0,
+            urls = ["https://pki.goog/roots.pem"],
+            sha256 = "9c9b9685ad319b9747c3fe69b46a61c11a0efabdfa09ca6a8b0c3da421036d27",
+        )
 
     if not native.existing_rule("rules_python"):
-        # Release from 2022-01-05
+        # Release from 2022-07-15
         http_archive(
             name = "rules_python",
-            sha256 = "a30abdfc7126d497a7698c29c46ea9901c6392d6ed315171a6df5ce433aa4502",
-            strip_prefix = "rules_python-0.6.0",
-            url = "https://github.com/bazelbuild/rules_python/archive/0.6.0.tar.gz",
+            sha256 = "a3a6e99f497be089f81ec082882e40246bfd435f52f4e82f37e89449b04573f6",
+            strip_prefix = "rules_python-0.10.2",
+            url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.10.2.tar.gz",
         )
 
     if not native.existing_rule("pybind11"):
diff --git a/python/tink_py_deps_init.bzl b/python/tink_py_deps_init.bzl
index 5095d33..01fdc43 100644
--- a/python/tink_py_deps_init.bzl
+++ b/python/tink_py_deps_init.bzl
@@ -1,11 +1,17 @@
-"""
-Initialization of dependencies of Python Tink
-"""
+"""Initialization of tink-py dependencies."""
 
 load("@rules_python//python:pip.bzl", "pip_install")
+load("@pybind11_bazel//:python_configure.bzl", "python_configure")
 
 def tink_py_deps_init(workspace_name):
     pip_install(
         name = "tink_py_pip_deps",
+        quiet = False,
         requirements = "@" + workspace_name + "//:requirements.txt",
     )
+
+    # Use `which python3` by default [1] unless PYTHON_BIN_PATH is specified [2].
+    #
+    # [1] https://github.com/pybind/pybind11_bazel/blob/fc56ce8a8b51e3dd941139d329b63ccfea1d304b/python_configure.bzl#L434
+    # [2] https://github.com/pybind/pybind11_bazel/blob/fc56ce8a8b51e3dd941139d329b63ccfea1d304b/python_configure.bzl#L162
+    python_configure(name = "local_config_python", python_version = "3")
diff --git a/python/tools/build_defs/BUILD.bazel b/python/tools/build_defs/BUILD.bazel
deleted file mode 100644
index e6d05e7..0000000
--- a/python/tools/build_defs/BUILD.bazel
+++ /dev/null
@@ -1,3 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
diff --git a/python/tools/build_defs/tink_python_rules.bzl b/python/tools/build_defs/tink_python_rules.bzl
deleted file mode 100644
index 9cf7db5..0000000
--- a/python/tools/build_defs/tink_python_rules.bzl
+++ /dev/null
@@ -1,67 +0,0 @@
-"""Tink rules for python."""
-
-def tink_pybind_extension(
-        name,
-        srcs = [],
-        hdrs = [],
-        copts = [],
-        linkopts = [],
-        features = ["-use_header_modules"],
-        deps = []):
-    """
-    Pybind Extension for Tink.
-
-    Creates Bazel targets for a pybind module:
-    - A py_library with the taret name
-    - A cc_binary with the target name.so
-
-    Args:
-      name: name of the target
-      srcs: source files corresponding to the target
-      hdrs: header files corresponding to the target
-      copts: flags for the compiler
-      linkopts: flags for the linker
-      features: features enabled for Bazel
-      deps: dependencies of the target
-
-    Returns:
-      A py_library target.
-    """
-    shared_lib_name = name + ".so"
-    native.cc_binary(
-        name = shared_lib_name,
-        linkshared = 1,
-        linkstatic = 1,
-        srcs = srcs + hdrs,
-        copts = copts + ["-fvisibility=hidden"],
-        linkopts = linkopts + select({
-            "@pybind11//:osx": [],
-            "//conditions:default": ["-Wl,-Bsymbolic"],
-        }),
-        features = features,
-        deps = deps,
-    )
-
-    # Extract Python targets from deps
-    pybind_deps = [dep[:-3] for dep in deps if dep.endswith("_cc")]
-    native.py_library(
-        name = name,
-        data = [shared_lib_name],
-        deps = pybind_deps,
-    )
-
-def tink_pybind_library(
-        name,
-        copts = [],
-        features = ["-use_header_modules"],
-        tags = [],
-        deps = [],
-        **kwargs):
-    native.cc_library(
-        name = name,
-        copts = copts,
-        features = features,
-        tags = tags,
-        deps = deps,
-        **kwargs
-    )
diff --git a/python/tools/distribution/build_linux_binary_wheels.sh b/python/tools/distribution/build_linux_binary_wheels.sh
index 115dafa..a0958d1 100755
--- a/python/tools/distribution/build_linux_binary_wheels.sh
+++ b/python/tools/distribution/build_linux_binary_wheels.sh
@@ -37,7 +37,8 @@
 export TINK_PYTHON_ROOT_PATH="${PWD}"
 
 readonly BAZEL_VERSION="$(cat ${TINK_PYTHON_ROOT_PATH}/.bazelversion)"
-readonly PROTOC_VERSION="3.19.3"
+# Contains python version 4.21.9 of protobuf
+readonly PROTOC_RELEASE_TAG="21.9"
 
 # Get dependencies which are needed for building Tink.
 
@@ -47,25 +48,22 @@
 ./"bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh"
 
 # Install protoc. Needed for protocol buffer compilation.
-PROTOC_ZIP="protoc-${PROTOC_VERSION}-linux-x86_64.zip"
-curl -OL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP}"
+PROTOC_ZIP="protoc-${PROTOC_RELEASE_TAG}-linux-x86_64.zip"
+curl -OL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_RELEASE_TAG}/${PROTOC_ZIP}"
 unzip -o "${PROTOC_ZIP}" -d /usr/local bin/protoc
 
-# Setup required for Tink.
-export TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH="${TINK_PYTHON_ROOT_PATH}/.."
-
-# Workaround for grpc which expects a python2 installation, which is not present
-# in the manylinux2014 container. Cannot be an empty string, otherwise Bazel
-# will fail.
-export PYTHON2_BIN_PATH="non_existent_file"
-export PYTHON2_LIB_PATH="non_existent_file"
-
 # Required to fix https://github.com/pypa/manylinux/issues/357.
 export LD_LIBRARY_PATH="/usr/local/lib"
 
 for v in "${!PYTHON_VERSIONS[@]}"; do
   (
     # Executing in a subshell to make the PATH modification temporary.
+    # This makes shure that `which python3 ==
+    # /opt/python/${PYTHON_VERSIONS[$v]}/bin/python3`, which is a symlink of
+    # `/opt/python/${PYTHON_VERSIONS[$v]}/bin/python${v}`. This should allow
+    # pybind11_bazel to pick up the correct Python binary [1].
+    #
+    # [1] https://github.com/pybind/pybind11_bazel/blob/fc56ce8a8b51e3dd941139d329b63ccfea1d304b/python_configure.bzl#L434
     export PATH="${PATH}:/opt/python/${PYTHON_VERSIONS[$v]}/bin"
     pip wheel .
   )
diff --git a/python/tools/distribution/create_release.sh b/python/tools/distribution/create_release.sh
index 90700f6..a15d9c4 100755
--- a/python/tools/distribution/create_release.sh
+++ b/python/tools/distribution/create_release.sh
@@ -18,7 +18,7 @@
 # source distribution and binary wheels for Linux and macOS.  All Python tests
 # are exectued for each binary wheel and the source distribution.
 
-set -euo pipefail
+set -euox pipefail
 
 declare -a PYTHON_VERSIONS=
 PYTHON_VERSIONS+=("3.7")
@@ -50,8 +50,6 @@
 #######################################
 __create_and_test_wheels_for_linux() {
   echo "### Building and testing Linux binary wheels ###"
-  local -r tink_py_relative_path="${PWD##*/}"
-  local -r workdir="/tmp/tink/${tink_py_relative_path}"
   # Use signatures for getting images from registry (see
   # https://docs.docker.com/engine/security/trust/content_trust/).
   export DOCKER_CONTENT_TRUST=1
@@ -60,16 +58,20 @@
   # file so we save a copy for backup.
   cp WORKSPACE WORKSPACE.bak
 
+  local -r tink_base_dir="/tmp/tink"
+  local -r tink_py_relative_path="${PWD##*/}"
+  local -r workdir="${tink_base_dir}/${tink_py_relative_path}"
   # Build binary wheels.
   docker run \
-    --volume "${TINK_PYTHON_ROOT_PATH}/..:/tmp/tink" \
+    --volume "${TINK_PYTHON_ROOT_PATH}/..:${tink_base_dir}" \
     --workdir "${workdir}" \
+    -e TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH="${tink_base_dir}" \
     "${IMAGE}" \
     "${workdir}/tools/distribution/build_linux_binary_wheels.sh"
 
   ## Test binary wheels.
   docker run \
-    --volume "${TINK_PYTHON_ROOT_PATH}/..:/tmp/tink" \
+    --volume "${TINK_PYTHON_ROOT_PATH}/..:${tink_base_dir}" \
     --workdir "${workdir}" \
     "${IMAGE}" \
     "${workdir}/tools/distribution/test_linux_binary_wheels.sh"
@@ -98,16 +100,30 @@
   local latest="${sorted[${#sorted[@]}-1]}"
   enable_py_version "${latest}"
 
+  # Patch the workspace to use http_archive rules which specify the release tag.
+  #
+  # This is done so that an already patched version of WORKSPACE is present in
+  # the sdist. Then, when building from the sdist, the default patching logic
+  # in performed by setup.py will be a no-op.
+  #
+  # TODO(b/281635529): Use a container for a more hermetic testing environment.
+  cp WORKSPACE WORKSPACE.bak
+
   # Build source distribution.
-  export TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH="${TINK_PYTHON_ROOT_PATH}/.."
-  python3 setup.py sdist --owner=root --group=root
+  TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION="${TINK_VERSION}" \
+    python3 setup.py sdist --owner=root --group=root
   local sdist_filename="tink-${TINK_VERSION}.tar.gz"
   cp "dist/${sdist_filename}" release/
 
+  # Restore the original WORKSPACE.
+  mv WORKSPACE.bak WORKSPACE
+
   # Test install from source distribution.
   python3 --version
   python3 -m pip list
-  python3 -m pip install -v "release/${sdist_filename}"
+  # Install Tink dependencies.
+  python3 -m pip install --require-hashes -r requirements.txt
+  python3 -m pip install --no-deps --no-index -v "release/${sdist_filename}"
   python3 -m pip list
   find tink/ -not -path "*cc/pybind*" -type f -name "*_test.py" -print0 \
     | xargs -0 -n1 python3
@@ -164,9 +180,8 @@
   pyenv shell "${version}"
 
   # Update environment.
-  python3 -m pip install --upgrade pip
-  python3 -m pip install --upgrade setuptools
-  python3 -m pip install --upgrade wheel
+  python3 -m pip install --require-hashes -r \
+    "${TINK_PYTHON_ROOT_PATH}/tools/distribution/requirements.txt"
 }
 
 main() {
diff --git a/python/tools/distribution/requirements.in b/python/tools/distribution/requirements.in
new file mode 100644
index 0000000..7015e2e
--- /dev/null
+++ b/python/tools/distribution/requirements.in
@@ -0,0 +1,3 @@
+pip
+setuptools
+wheel
diff --git a/python/tools/distribution/requirements.txt b/python/tools/distribution/requirements.txt
new file mode 100644
index 0000000..6d0fa18
--- /dev/null
+++ b/python/tools/distribution/requirements.txt
@@ -0,0 +1,20 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+#    pip-compile --allow-unsafe --generate-hashes requirements.in
+#
+wheel==0.38.4 \
+    --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \
+    --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8
+    # via -r requirements.in
+
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.0 \
+    --hash=sha256:aee438284e82c8def684b0bcc50b1f6ed5e941af97fa940e83e2e8ef1a59da9b \
+    --hash=sha256:b5f88adff801f5ef052bcdef3daa31b55eb67b0fccd6d0106c206fa248e0463c
+    # via -r requirements.in
+setuptools==67.3.2 \
+    --hash=sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012 \
+    --hash=sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48
+    # via -r requirements.in
diff --git a/testing/cc/.bazelrc b/testing/cc/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/testing/cc/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/testing/cc/.bazelversion b/testing/cc/.bazelversion
index ac14c3d..09b254e 100644
--- a/testing/cc/.bazelversion
+++ b/testing/cc/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/testing/cc/BUILD.bazel b/testing/cc/BUILD.bazel
index 535544a..83d2329 100644
--- a/testing/cc/BUILD.bazel
+++ b/testing/cc/BUILD.bazel
@@ -9,7 +9,7 @@
 
 cpp_grpc_library(
     name = "testing_api_cpp_library",
-    protos = ["//proto:testing_api_proto"],
+    protos = ["//protos:testing_api_proto"],
     service_namespace = "testing_api",
 )
 
@@ -82,6 +82,7 @@
     srcs = ["aead_impl.cc"],
     hdrs = ["aead_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@tink_cc//:binary_keyset_reader",
@@ -107,6 +108,7 @@
     srcs = ["deterministic_aead_impl.cc"],
     hdrs = ["deterministic_aead_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@tink_cc",
@@ -123,6 +125,7 @@
         ":testing_api_cpp_library",
         "@com_google_googletest//:gtest_main",
         "@tink_cc//:binary_keyset_writer",
+        "@tink_cc//:cleartext_keyset_handle",
         "@tink_cc//daead:deterministic_aead_config",
         "@tink_cc//daead:deterministic_aead_key_templates",
     ],
@@ -133,6 +136,7 @@
     srcs = ["streaming_aead_impl.cc"],
     hdrs = ["streaming_aead_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/status",
@@ -163,6 +167,7 @@
     srcs = ["mac_impl.cc"],
     hdrs = ["mac_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@tink_cc",
@@ -179,6 +184,7 @@
         ":testing_api_cpp_library",
         "@com_google_googletest//:gtest_main",
         "@tink_cc//:binary_keyset_writer",
+        "@tink_cc//:cleartext_keyset_handle",
         "@tink_cc//mac:mac_config",
         "@tink_cc//mac:mac_key_templates",
     ],
@@ -189,11 +195,13 @@
     srcs = ["hybrid_impl.cc"],
     hdrs = ["hybrid_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@tink_cc",
         "@tink_cc//:binary_keyset_reader",
         "@tink_cc//:cleartext_keyset_handle",
+        "@tink_cc//util:statusor",
     ],
 )
 
@@ -215,6 +223,7 @@
     srcs = ["signature_impl.cc"],
     hdrs = ["signature_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@tink_cc",
@@ -241,6 +250,7 @@
     srcs = ["prf_set_impl.cc"],
     hdrs = ["prf_set_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@tink_cc",
@@ -257,6 +267,7 @@
         ":testing_api_cpp_library",
         "@com_google_googletest//:gtest_main",
         "@tink_cc//:binary_keyset_writer",
+        "@tink_cc//:cleartext_keyset_handle",
         "@tink_cc//prf:prf_config",
         "@tink_cc//prf:prf_key_templates",
     ],
@@ -267,6 +278,7 @@
     srcs = ["jwt_impl.cc"],
     hdrs = ["jwt_impl.h"],
     deps = [
+        ":create",
         ":testing_api_cpp_library",
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/time",
@@ -299,9 +311,38 @@
     ],
 )
 
+cc_library(
+    name = "create",
+    hdrs = ["create.h"],
+    deps = [
+        ":testing_api_cpp_library",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@tink_cc//:binary_keyset_reader",
+        "@tink_cc//:cleartext_keyset_handle",
+        "@tink_cc//:keyset_handle",
+        "@tink_cc//:keyset_reader",
+    ],
+)
+
+cc_test(
+    name = "create_test",
+    srcs = ["create_test.cc"],
+    deps = [
+        ":create",
+        ":testing_api_cpp_library",
+        "@com_google_googletest//:gtest_main",
+        "@tink_cc//:binary_keyset_writer",
+        "@tink_cc//:keyset_handle",
+        "@tink_cc//:mac",
+        "@tink_cc//aead:aead_config",
+        "@tink_cc//aead:aead_key_templates",
+    ],
+)
+
 cc_binary(
     name = "testing_server",
     srcs = ["testing_server.cc"],
+    local_defines = ["TINK_CROSS_LANG_TESTS_AWSKMS"],
     deps = [
         ":aead_impl",
         ":deterministic_aead_impl",
@@ -317,10 +358,14 @@
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/flags:flag",
         "@com_google_absl//absl/flags:parse",
+        "@com_google_absl//absl/strings",
         "@tink_cc//config:tink_config",
         "@tink_cc//hybrid:hpke_config",
         "@tink_cc//jwt:jwt_mac_config",
         "@tink_cc//jwt:jwt_signature_config",
         "@tink_cc//util:fake_kms_client",
+        "@tink_cc//util:status",
+        "@tink_cc_awskms//:aws_kms_client",
+        "@tink_cc_gcpkms//:gcp_kms_client",
     ],
 )
diff --git a/testing/cc/WORKSPACE b/testing/cc/WORKSPACE
index 5614642..77f7c93 100644
--- a/testing/cc/WORKSPACE
+++ b/testing/cc/WORKSPACE
@@ -7,12 +7,31 @@
     path = "../../cc",
 )
 
+local_repository(
+    name = "tink_cc_gcpkms",
+    path = "../../cc/integration/gcpkms",
+)
+
+local_repository(
+    name = "tink_cc_awskms",
+    path = "../../cc/integration/awskms",
+)
+
 load("@tink_cc//:tink_cc_deps.bzl", "tink_cc_deps")
 tink_cc_deps()
 
 load("@tink_cc//:tink_cc_deps_init.bzl", "tink_cc_deps_init")
 tink_cc_deps_init()
 
+load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps.bzl", "tink_cc_gcpkms_deps")
+tink_cc_gcpkms_deps()
+
+load("@tink_cc_gcpkms//:tink_cc_gcpkms_deps_init.bzl", "tink_cc_gcpkms_deps_init")
+tink_cc_gcpkms_deps_init()
+
+load("@tink_cc_awskms//:tink_cc_awskms_deps.bzl", "tink_cc_awskms_deps")
+tink_cc_awskms_deps()
+
 # Release from 2021-12-12
 http_archive(
     name = "rules_proto_grpc",
diff --git a/testing/cc/aead_impl.cc b/testing/cc/aead_impl.cc
index 3fd751b..2b7e2b9 100644
--- a/testing/cc/aead_impl.cc
+++ b/testing/cc/aead_impl.cc
@@ -17,79 +17,65 @@
 // Implementation of an AEAD Service.
 #include "aead_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "tink/aead.h"
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
-#include "proto/testing_api.grpc.pb.h"
+#include "create.h"
 
 namespace tink_testing_api {
 
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
-using ::grpc::ServerContext;
-using ::grpc::Status;
+using ::crypto::tink::util::StatusOr;
 
-// Encrypts a message
+::grpc::Status AeadImpl::Create(grpc::ServerContext* context,
+                                const CreationRequest* request,
+                                CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::Aead>(request, response);
+}
+
 ::grpc::Status AeadImpl::Encrypt(grpc::ServerContext* context,
                                  const AeadEncryptRequest* request,
                                  AeadEncryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
+  StatusOr<std::unique_ptr<crypto::tink::Aead>> aead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Aead>(
+          request->annotated_keyset());
+  if (!aead.ok()) {
+    return grpc::Status(
+        grpc::StatusCode::FAILED_PRECONDITION,
+        absl::StrCat("Creating primitive failed: ", aead.status().message()));
   }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
+
+  StatusOr<std::string> ciphertext =
+      (*aead)->Encrypt(request->plaintext(), request->associated_data());
+  if (!ciphertext.ok()) {
+    response->set_err(std::string(ciphertext.status().message()));
+    return grpc::Status::OK;
   }
-  auto aead_result = handle_result.value()->GetPrimitive<crypto::tink::Aead>();
-  if (!aead_result.ok()) {
-    response->set_err(std::string(aead_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto encrypt_result = aead_result.value()->Encrypt(
-      request->plaintext(), request->associated_data());
-  if (!encrypt_result.ok()) {
-    response->set_err(std::string(encrypt_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  response->set_ciphertext(encrypt_result.value());
-  return ::grpc::Status::OK;
+  response->set_ciphertext(*ciphertext);
+  return grpc::Status::OK;
 }
 
-// Decrypts a ciphertext
-::grpc::Status AeadImpl::Decrypt(grpc::ServerContext* context,
+grpc::Status AeadImpl::Decrypt(grpc::ServerContext* context,
                                  const AeadDecryptRequest* request,
                                  AeadDecryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
+  StatusOr<std::unique_ptr<crypto::tink::Aead>> aead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Aead>(
+          request->annotated_keyset());
+  if (!aead.ok()) {
+    return grpc::Status(
+        grpc::StatusCode::FAILED_PRECONDITION,
+        absl::StrCat("Creating primitive failed: ", aead.status().message()));
   }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
+
+  StatusOr<std::string> plaintext =
+      (*aead)->Decrypt(request->ciphertext(), request->associated_data());
+  if (!plaintext.ok()) {
+    response->set_err(std::string(plaintext.status().message()));
+    return grpc::Status::OK;
   }
-  auto aead_result = handle_result.value()->GetPrimitive<crypto::tink::Aead>();
-  if (!aead_result.ok()) {
-    response->set_err(std::string(aead_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto decrypt_result = aead_result.value()->Decrypt(
-      request->ciphertext(), request->associated_data());
-  if (!decrypt_result.ok()) {
-    response->set_err(std::string(decrypt_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  response->set_plaintext(decrypt_result.value());
-  return ::grpc::Status::OK;
+  response->set_plaintext(*plaintext);
+  return grpc::Status::OK;
 }
 
 }  // namespace tink_testing_api
diff --git a/testing/cc/aead_impl.h b/testing/cc/aead_impl.h
index 4f348d7..8b73549 100644
--- a/testing/cc/aead_impl.h
+++ b/testing/cc/aead_impl.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_TESTING_SERIVCES_AEAD_IMPL_H_
-#define TINK_TESTING_SERIVCES_AEAD_IMPL_H_
+#ifndef TINK_TESTING_AEAD_IMPL_H_
+#define TINK_TESTING_AEAD_IMPL_H_
 
 #include <grpcpp/grpcpp.h>
 #include <grpcpp/server_context.h>
@@ -28,6 +28,10 @@
 // An Aead Service.
 class AeadImpl final : public Aead::Service {
  public:
+  grpc::Status Create(grpc::ServerContext* context,
+                      const CreationRequest* request,
+                      CreationResponse* response) override;
+
   grpc::Status Encrypt(grpc::ServerContext* context,
                        const AeadEncryptRequest* request,
                        AeadEncryptResponse* response) override;
@@ -39,4 +43,4 @@
 
 }  // namespace tink_testing_api
 
-#endif  // TINK_TESTING_SERIVCES_AEAD_IMPL_H_
+#endif  // TINK_TESTING_AEAD_IMPL_H_
diff --git a/testing/cc/aead_impl_test.cc b/testing/cc/aead_impl_test.cc
index 4d285e2..4eb8fbe 100644
--- a/testing/cc/aead_impl_test.cc
+++ b/testing/cc/aead_impl_test.cc
@@ -43,6 +43,8 @@
 
 using crypto::tink::KeysetHandle;
 using google::crypto::tink::KeyTemplate;
+using tink_testing_api::CreationRequest;
+using tink_testing_api::CreationResponse;
 
 std::string ValidKeyset() {
   const KeyTemplate& key_template = AeadKeyTemplates::Aes128Eax();
@@ -64,11 +66,32 @@
   static void SetUpTestSuite() { ASSERT_TRUE(AeadConfig::Register().ok()); }
 };
 
+TEST_F(AeadImplTest, CreateAeadSuccess) {
+  tink_testing_api::AeadImpl aead;
+  std::string keyset = ValidKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(aead.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(AeadImplTest, CreateAeadFails) {
+  tink_testing_api::AeadImpl aead;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
+  CreationResponse response;
+
+  EXPECT_TRUE(aead.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(AeadImplTest, EncryptDecryptSuccess) {
   tink_testing_api::AeadImpl aead;
   std::string keyset = ValidKeyset();
   AeadEncryptRequest enc_request;
-  enc_request.set_keyset(keyset);
+  enc_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   enc_request.set_plaintext("Plain text");
   enc_request.set_associated_data("ad");
   AeadEncryptResponse enc_response;
@@ -77,7 +100,7 @@
   EXPECT_THAT(enc_response.err(), IsEmpty());
 
   AeadDecryptRequest dec_request;
-  dec_request.set_keyset(keyset);
+  dec_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   dec_request.set_ciphertext(enc_response.ciphertext());
   dec_request.set_associated_data("ad");
   AeadDecryptResponse dec_response;
@@ -90,20 +113,19 @@
 TEST_F(AeadImplTest, EncryptBadKeysetFail) {
   tink_testing_api::AeadImpl aead;
   AeadEncryptRequest enc_request;
-  enc_request.set_keyset("bad keyset");
+  enc_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   enc_request.set_plaintext("Plain text");
   enc_request.set_associated_data("ad");
   AeadEncryptResponse enc_response;
 
-  EXPECT_TRUE(aead.Encrypt(nullptr, &enc_request, &enc_response).ok());
-  EXPECT_THAT(enc_response.err(), Not(IsEmpty()));
+  EXPECT_FALSE(aead.Encrypt(nullptr, &enc_request, &enc_response).ok());
 }
 
 TEST_F(AeadImplTest, DecryptBadCiphertextFail) {
   tink_testing_api::AeadImpl aead;
   std::string keyset = ValidKeyset();
   AeadDecryptRequest dec_request;
-  dec_request.set_keyset(keyset);
+  dec_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   dec_request.set_ciphertext("bad ciphertext");
   dec_request.set_associated_data("ad");
   AeadDecryptResponse dec_response;
diff --git a/testing/cc/create.h b/testing/cc/create.h
new file mode 100644
index 0000000..ec1db1c
--- /dev/null
+++ b/testing/cc/create.h
@@ -0,0 +1,79 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_TESTING_CREATE_H_
+#define TINK_TESTING_CREATE_H_
+
+#include <grpcpp/grpcpp.h>
+#include <grpcpp/server_context.h>
+#include <grpcpp/support/status.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/container/flat_hash_map.h"
+#include "tink/binary_keyset_reader.h"
+#include "tink/cleartext_keyset_handle.h"
+#include "tink/keyset_handle.h"
+#include "tink/keyset_reader.h"
+#include "proto/testing_api.grpc.pb.h"
+
+namespace tink_testing_api {
+
+// Tries to create a primitive from a keyset serialized in binary proto format.
+// This function might be better in Tink itself (except that it should take an
+// optional SecretKeyAccessToken).
+template <typename T>
+crypto::tink::util::StatusOr<std::unique_ptr<T>>
+PrimitiveFromSerializedBinaryProtoKeyset(
+    const AnnotatedKeyset& annotated_keyset) {
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetReader>>
+      reader = crypto::tink::BinaryKeysetReader::New(
+          annotated_keyset.serialized_keyset());
+  if (!reader.ok()) {
+    return reader.status();
+  }
+  absl::flat_hash_map<std::string, std::string> annotations;
+  for (const auto& annotation : annotated_keyset.annotations()) {
+    annotations[annotation.first] = annotation.second;
+  }
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::KeysetHandle>>
+      handle = crypto::tink::CleartextKeysetHandle::Read(*std::move(reader),
+                                                         annotations);
+  if (!handle.ok()) {
+    return handle.status();
+  }
+  return (*handle)->GetPrimitive<T>();
+}
+
+// Tries to create a primitive of type T from the creation request and
+// populates the response accordingly. This can be used in implementations
+// of the "Create" RPC calls in the Tink Services.
+template <typename T>
+grpc::Status CreatePrimitiveForRpc(const CreationRequest* request,
+                                   CreationResponse* response) {
+  crypto::tink::util::StatusOr<std::unique_ptr<T>> primitive =
+      PrimitiveFromSerializedBinaryProtoKeyset<T>(request->annotated_keyset());
+  if (!primitive.ok()) {
+    response->set_err(std::string(primitive.status().message()));
+  }
+  return grpc::Status::OK;
+}
+
+}  // namespace tink_testing_api
+
+#endif  // TINK_TESTING_CREATE_H_
diff --git a/testing/cc/create_test.cc b/testing/cc/create_test.cc
new file mode 100644
index 0000000..d8975bb
--- /dev/null
+++ b/testing/cc/create_test.cc
@@ -0,0 +1,117 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "create.h"
+
+#include <memory>
+#include <ostream>
+#include <sstream>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aead_config.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/binary_keyset_writer.h"
+#include "tink/keyset_handle.h"
+#include "tink/mac.h"
+
+namespace tink_testing_api {
+
+namespace {
+
+using ::google::crypto::tink::KeyTemplate;
+using ::testing::IsEmpty;
+using ::testing::Not;
+using ::testing::NotNull;
+
+std::string ValidAeadKeyset() {
+  const KeyTemplate& key_template = crypto::tink::AeadKeyTemplates::Aes128Eax();
+  auto handle_result = crypto::tink::KeysetHandle::GenerateNew(key_template);
+  EXPECT_TRUE(handle_result.ok());
+  std::stringbuf keyset;
+  auto writer_result = crypto::tink::BinaryKeysetWriter::New(
+      absl::make_unique<std::ostream>(&keyset));
+  EXPECT_TRUE(writer_result.ok());
+
+  auto status = crypto::tink::CleartextKeysetHandle::Write(
+      writer_result.value().get(), *handle_result.value());
+  EXPECT_TRUE(status.ok());
+  return keyset.str();
+}
+
+class CreateTest : public ::testing::Test {
+ protected:
+  static void SetUpTestSuite() {
+    ASSERT_TRUE(crypto::tink::AeadConfig::Register().ok());
+  }
+};
+
+TEST_F(CreateTest, RpcHelperSuccess) {
+  std::string keyset = ValidAeadKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(
+      CreatePrimitiveForRpc<crypto::tink::Aead>(&request, &response)
+          .ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(CreateTest, RpcHelperWrongPrimitiveFails) {
+  std::string keyset = ValidAeadKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+  EXPECT_TRUE(
+      CreatePrimitiveForRpc<crypto::tink::Mac>(&request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
+TEST_F(CreateTest, PrimitiveCreationWorks) {
+  AnnotatedKeyset annotated_keyset;
+  annotated_keyset.set_serialized_keyset(ValidAeadKeyset());
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::Aead>> aead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Aead>(
+          annotated_keyset);
+  ASSERT_TRUE(aead.status().ok()) << aead.status();
+  EXPECT_THAT(*aead, NotNull());
+}
+
+TEST_F(CreateTest, PrimitiveCreationWrongPrimitiveFails) {
+  AnnotatedKeyset annotated_keyset;
+  annotated_keyset.set_serialized_keyset(ValidAeadKeyset());
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::Mac>> aead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Mac>(
+          annotated_keyset);
+  ASSERT_FALSE(aead.status().ok());
+}
+
+TEST_F(CreateTest, PrimitiveWithAnnotationsWorks) {
+  AnnotatedKeyset annotated_keyset;
+  annotated_keyset.set_serialized_keyset(ValidAeadKeyset());
+  annotated_keyset.mutable_annotations()->insert({"key1", "value1"});
+  crypto::tink::util::StatusOr<std::unique_ptr<crypto::tink::Aead>> aead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Aead>(
+          annotated_keyset);
+  ASSERT_TRUE(aead.status().ok());
+  EXPECT_THAT(*aead, NotNull());
+}
+
+}  // namespace
+
+}  // namespace tink_testing_api
diff --git a/testing/cc/deterministic_aead_impl.cc b/testing/cc/deterministic_aead_impl.cc
index c434898..6a32ac7 100644
--- a/testing/cc/deterministic_aead_impl.cc
+++ b/testing/cc/deterministic_aead_impl.cc
@@ -17,83 +17,67 @@
 // Implementation of an Deterministic AEAD Service.
 #include "deterministic_aead_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
 #include "tink/deterministic_aead.h"
-#include "proto/testing_api.grpc.pb.h"
+#include "create.h"
 
 namespace tink_testing_api {
 
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
-using ::grpc::ServerContext;
-using ::grpc::Status;
+using ::crypto::tink::util::StatusOr;
 
-// Encrypts a message
-::grpc::Status DeterministicAeadImpl::EncryptDeterministically(
+grpc::Status DeterministicAeadImpl::Create(grpc::ServerContext* context,
+                                           const CreationRequest* request,
+                                           CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::DeterministicAead>(request,
+                                                                response);
+}
+
+grpc::Status DeterministicAeadImpl::EncryptDeterministically(
     grpc::ServerContext* context,
     const DeterministicAeadEncryptRequest* request,
     DeterministicAeadEncryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
+  StatusOr<std::unique_ptr<crypto::tink::DeterministicAead>> daead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::DeterministicAead>(
+          request->annotated_keyset());
+  if (!daead.ok()) {
+    return grpc::Status(
+        grpc::StatusCode::FAILED_PRECONDITION,
+        absl::StrCat("Creating primitive failed: ", daead.status().message()));
   }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto daead_result =
-      handle_result.value()->GetPrimitive<crypto::tink::DeterministicAead>();
-  if (!daead_result.ok()) {
-    response->set_err(std::string(daead_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto encrypt_result = daead_result.value()->EncryptDeterministically(
+
+  StatusOr<std::string> ciphertext = (*daead)->EncryptDeterministically(
       request->plaintext(), request->associated_data());
-  if (!encrypt_result.ok()) {
-    response->set_err(std::string(encrypt_result.status().message()));
-    return ::grpc::Status::OK;
+  if (!ciphertext.ok()) {
+    response->set_err(std::string(ciphertext.status().message()));
+    return grpc::Status::OK;
   }
-  response->set_ciphertext(encrypt_result.value());
-  return ::grpc::Status::OK;
+  response->set_ciphertext(*ciphertext);
+  return grpc::Status::OK;
 }
 
-// Decrypts a ciphertext
-::grpc::Status DeterministicAeadImpl::DecryptDeterministically(
+grpc::Status DeterministicAeadImpl::DecryptDeterministically(
     grpc::ServerContext* context,
     const DeterministicAeadDecryptRequest* request,
     DeterministicAeadDecryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
+  StatusOr<std::unique_ptr<crypto::tink::DeterministicAead>> daead =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::DeterministicAead>(
+          request->annotated_keyset());
+  if (!daead.ok()) {
+    return grpc::Status(
+        grpc::StatusCode::FAILED_PRECONDITION,
+        absl::StrCat("Creating primitive failed: ", daead.status().message()));
   }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto daead_result =
-      handle_result.value()->GetPrimitive<crypto::tink::DeterministicAead>();
-  if (!daead_result.ok()) {
-    response->set_err(std::string(daead_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto decrypt_result = daead_result.value()->DecryptDeterministically(
+  StatusOr<std::string> plaintext = (*daead)->DecryptDeterministically(
       request->ciphertext(), request->associated_data());
-  if (!decrypt_result.ok()) {
-    response->set_err(std::string(decrypt_result.status().message()));
-    return ::grpc::Status::OK;
+  if (!plaintext.ok()) {
+    response->set_err(std::string(plaintext.status().message()));
+    return grpc::Status::OK;
   }
-  response->set_plaintext(decrypt_result.value());
-  return ::grpc::Status::OK;
+  response->set_plaintext(*plaintext);
+  return grpc::Status::OK;
 }
 
 }  // namespace tink_testing_api
diff --git a/testing/cc/deterministic_aead_impl.h b/testing/cc/deterministic_aead_impl.h
index 1949c6c..12def8c 100644
--- a/testing/cc/deterministic_aead_impl.h
+++ b/testing/cc/deterministic_aead_impl.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_TESTING_SERIVCES_DETERMINISTIC_AEAD_IMPL_H_
-#define TINK_TESTING_SERIVCES_DETERMINISTIC_AEAD_IMPL_H_
+#ifndef TINK_TESTING_DETERMINISTIC_AEAD_IMPL_H_
+#define TINK_TESTING_DETERMINISTIC_AEAD_IMPL_H_
 
 #include <grpcpp/grpcpp.h>
 #include <grpcpp/server_context.h>
@@ -28,6 +28,10 @@
 // An DeterministicAead Service.
 class DeterministicAeadImpl final : public DeterministicAead::Service {
  public:
+  grpc::Status Create(grpc::ServerContext* context,
+                      const CreationRequest* request,
+                      CreationResponse* response) override;
+
   grpc::Status EncryptDeterministically(
       grpc::ServerContext* context,
       const DeterministicAeadEncryptRequest* request,
@@ -41,4 +45,4 @@
 
 }  // namespace tink_testing_api
 
-#endif  // TINK_TESTING_SERIVCES_DETERMINISTIC_AEAD_IMPL_H_
+#endif  // TINK_TESTING_DETERMINISTIC_AEAD_IMPL_H_
diff --git a/testing/cc/deterministic_aead_impl_test.cc b/testing/cc/deterministic_aead_impl_test.cc
index 18033df..bfd320f 100644
--- a/testing/cc/deterministic_aead_impl_test.cc
+++ b/testing/cc/deterministic_aead_impl_test.cc
@@ -43,6 +43,8 @@
 
 using crypto::tink::KeysetHandle;
 using google::crypto::tink::KeyTemplate;
+using tink_testing_api::CreationRequest;
+using tink_testing_api::CreationResponse;
 
 std::string ValidKeyset() {
   const KeyTemplate& key_template = DeterministicAeadKeyTemplates::Aes256Siv();
@@ -66,11 +68,32 @@
   }
 };
 
+TEST_F(DeterministicAeadImplTest, CreateSuccess) {
+  tink_testing_api::DeterministicAeadImpl aead;
+  std::string keyset = ValidKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(aead.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(DeterministicAeadImplTest, CreateAeadFails) {
+  tink_testing_api::DeterministicAeadImpl aead;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
+  CreationResponse response;
+
+  EXPECT_TRUE(aead.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(DeterministicAeadImplTest, EncryptDecryptSuccess) {
   tink_testing_api::DeterministicAeadImpl daead;
   std::string keyset = ValidKeyset();
   DeterministicAeadEncryptRequest enc_request;
-  enc_request.set_keyset(keyset);
+  enc_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   enc_request.set_plaintext("Plain text");
   enc_request.set_associated_data("ad");
   DeterministicAeadEncryptResponse enc_response;
@@ -81,7 +104,7 @@
   EXPECT_THAT(enc_response.err(), IsEmpty());
 
   DeterministicAeadDecryptRequest dec_request;
-  dec_request.set_keyset(keyset);
+  dec_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   dec_request.set_ciphertext(enc_response.ciphertext());
   dec_request.set_associated_data("ad");
   DeterministicAeadDecryptResponse dec_response;
@@ -96,22 +119,21 @@
 TEST_F(DeterministicAeadImplTest, EncryptBadKeysetFail) {
   tink_testing_api::DeterministicAeadImpl daead;
   DeterministicAeadEncryptRequest enc_request;
-  enc_request.set_keyset("bad keyset");
+  enc_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   enc_request.set_plaintext("Plain text");
   enc_request.set_associated_data("ad");
   DeterministicAeadEncryptResponse enc_response;
 
-  EXPECT_TRUE(
+  EXPECT_FALSE(
       daead.EncryptDeterministically(nullptr, &enc_request, &enc_response)
           .ok());
-  EXPECT_THAT(enc_response.err(), Not(IsEmpty()));
 }
 
 TEST_F(DeterministicAeadImplTest, DecryptBadCiphertextFail) {
   tink_testing_api::DeterministicAeadImpl daead;
   std::string keyset = ValidKeyset();
   DeterministicAeadDecryptRequest dec_request;
-  dec_request.set_keyset(keyset);
+  dec_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   dec_request.set_ciphertext("bad ciphertext");
   dec_request.set_associated_data("ad");
   DeterministicAeadDecryptResponse dec_response;
diff --git a/testing/cc/hybrid_impl.cc b/testing/cc/hybrid_impl.cc
index 92f5faf..eb68fa5 100644
--- a/testing/cc/hybrid_impl.cc
+++ b/testing/cc/hybrid_impl.cc
@@ -17,39 +17,38 @@
 // Implementation of a Hybrid encryption service
 #include "hybrid_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
 #include "tink/hybrid_decrypt.h"
 #include "tink/hybrid_encrypt.h"
+#include "tink/util/statusor.h"
+#include "create.h"
 #include "proto/testing_api.grpc.pb.h"
 
 namespace tink_testing_api {
 
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
-using ::grpc::ServerContext;
-using ::grpc::Status;
+using ::crypto::tink::util::StatusOr;
 
-// Encrypts a message
+::grpc::Status HybridImpl::CreateHybridEncrypt(grpc::ServerContext* context,
+                                               const CreationRequest* request,
+                                               CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::HybridEncrypt>(request, response);
+}
+
+::grpc::Status HybridImpl::CreateHybridDecrypt(grpc::ServerContext* context,
+                                               const CreationRequest* request,
+                                               CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::HybridDecrypt>(request, response);
+}
+
 ::grpc::Status HybridImpl::Encrypt(grpc::ServerContext* context,
                                    const HybridEncryptRequest* request,
                                    HybridEncryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->public_keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto public_handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!public_handle_result.ok()) {
-    response->set_err(std::string(public_handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto hybrid_encrypt_result =
-      public_handle_result.value()->GetPrimitive<crypto::tink::HybridEncrypt>();
+  StatusOr<std::unique_ptr<crypto::tink::HybridEncrypt>> hybrid_encrypt_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::HybridEncrypt>(
+          request->public_annotated_keyset());
   if (!hybrid_encrypt_result.ok()) {
     response->set_err(std::string(hybrid_encrypt_result.status().message()));
     return ::grpc::Status::OK;
@@ -68,20 +67,9 @@
 ::grpc::Status HybridImpl::Decrypt(grpc::ServerContext* context,
                                    const HybridDecryptRequest* request,
                                    HybridDecryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->private_keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto private_handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!private_handle_result.ok()) {
-    response->set_err(std::string(private_handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto hybrid_decrypt_result =
-      private_handle_result.value()
-          ->GetPrimitive<crypto::tink::HybridDecrypt>();
+  StatusOr<std::unique_ptr<crypto::tink::HybridDecrypt>> hybrid_decrypt_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::HybridDecrypt>(
+          request->private_annotated_keyset());
   if (!hybrid_decrypt_result.ok()) {
     response->set_err(std::string(hybrid_decrypt_result.status().message()));
     return ::grpc::Status::OK;
diff --git a/testing/cc/hybrid_impl.h b/testing/cc/hybrid_impl.h
index 83dc5f1..36d445f 100644
--- a/testing/cc/hybrid_impl.h
+++ b/testing/cc/hybrid_impl.h
@@ -28,6 +28,14 @@
 // A Hybrid encryption Service
 class HybridImpl final : public Hybrid::Service {
  public:
+  grpc::Status CreateHybridEncrypt(grpc::ServerContext* context,
+                                   const CreationRequest* request,
+                                   CreationResponse* response) override;
+
+  grpc::Status CreateHybridDecrypt(grpc::ServerContext* context,
+                                   const CreationRequest* request,
+                                   CreationResponse* response) override;
+
   grpc::Status Encrypt(grpc::ServerContext* context,
                        const HybridEncryptRequest* request,
                        HybridEncryptResponse* response) override;
diff --git a/testing/cc/hybrid_impl_test.cc b/testing/cc/hybrid_impl_test.cc
index 010dbda..5dcab80 100644
--- a/testing/cc/hybrid_impl_test.cc
+++ b/testing/cc/hybrid_impl_test.cc
@@ -16,6 +16,9 @@
 
 #include "hybrid_impl.h"
 
+#include <memory>
+#include <ostream>
+#include <sstream>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -36,6 +39,8 @@
 
 using ::testing::Eq;
 using ::testing::IsEmpty;
+using ::tink_testing_api::CreationRequest;
+using ::tink_testing_api::CreationResponse;
 using ::tink_testing_api::HybridDecryptRequest;
 using ::tink_testing_api::HybridDecryptResponse;
 using ::tink_testing_api::HybridEncryptRequest;
@@ -60,6 +65,68 @@
   static void SetUpTestSuite() { ASSERT_TRUE(HybridConfig::Register().ok()); }
 };
 
+TEST_F(HybridImplTest, CreateHybridDecryptSuccess) {
+  tink_testing_api::HybridImpl hybrid;
+  const KeyTemplate& key_template =
+      HybridKeyTemplates::EciesP256HkdfHmacSha256Aes128Gcm();
+  ::crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+      private_keyset_handle = KeysetHandle::GenerateNew(key_template);
+  ASSERT_TRUE(private_keyset_handle.status().ok())
+      << private_keyset_handle.status();
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(**private_keyset_handle));
+  CreationResponse response;
+
+  EXPECT_TRUE(hybrid.CreateHybridDecrypt(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(HybridImplTest, CreateHybridDecryptFailure) {
+  tink_testing_api::HybridImpl hybrid;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("\x80");
+  CreationResponse response;
+
+  EXPECT_TRUE(hybrid.CreateHybridDecrypt(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
+TEST_F(HybridImplTest, CreateHybridEncryptSuccess) {
+  tink_testing_api::HybridImpl hybrid;
+  const KeyTemplate& key_template =
+      HybridKeyTemplates::EciesP256HkdfHmacSha256Aes128Gcm();
+  ::crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+      private_keyset_handle = KeysetHandle::GenerateNew(key_template);
+  ASSERT_TRUE(private_keyset_handle.status().ok())
+      << private_keyset_handle.status();
+  ::crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+      public_keyset_handle = (*private_keyset_handle)->GetPublicKeysetHandle();
+  ASSERT_TRUE(public_keyset_handle.status().ok())
+      << public_keyset_handle.status();
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(**public_keyset_handle));
+  CreationResponse response;
+
+  EXPECT_TRUE(hybrid.CreateHybridEncrypt(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(HybridImplTest, CreateHybridEncryptFailure) {
+  tink_testing_api::HybridImpl hybrid;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("\x80");
+  CreationResponse response;
+
+  EXPECT_TRUE(hybrid.CreateHybridEncrypt(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(HybridImplTest, EncryptDecryptSuccess) {
   tink_testing_api::HybridImpl hybrid;
   const KeyTemplate& key_template =
@@ -71,7 +138,8 @@
   EXPECT_TRUE(public_handle_result.ok());
 
   HybridEncryptRequest enc_request;
-  enc_request.set_public_keyset(KeysetBytes(*public_handle_result.value()));
+  enc_request.mutable_public_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(*public_handle_result.value()));
   enc_request.set_plaintext("Plain text");
   enc_request.set_context_info("context");
   HybridEncryptResponse enc_response;
@@ -80,7 +148,8 @@
   EXPECT_THAT(enc_response.err(), IsEmpty());
 
   HybridDecryptRequest dec_request;
-  dec_request.set_private_keyset(KeysetBytes(*private_handle_result.value()));
+  dec_request.mutable_private_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(*private_handle_result.value()));
   dec_request.set_ciphertext(enc_response.ciphertext());
   dec_request.set_context_info("context");
   HybridDecryptResponse dec_response;
@@ -93,7 +162,8 @@
 TEST_F(HybridImplTest, EncryptBadKeysetFail) {
   tink_testing_api::HybridImpl hybrid;
   HybridEncryptRequest enc_request;
-  enc_request.set_public_keyset("bad keyset");
+  enc_request.mutable_public_annotated_keyset()->set_serialized_keyset(
+      "bad keyset");
   enc_request.set_plaintext("Plain text");
   enc_request.set_context_info("context");
   HybridEncryptResponse enc_response;
@@ -110,7 +180,8 @@
   EXPECT_TRUE(private_handle_result.ok());
 
   HybridDecryptRequest dec_request;
-  dec_request.set_private_keyset(KeysetBytes(*private_handle_result.value()));
+  dec_request.mutable_private_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(*private_handle_result.value()));
   dec_request.set_ciphertext("bad ciphertext");
   dec_request.set_context_info("context");
   HybridDecryptResponse dec_response;
diff --git a/testing/cc/jwt_impl.cc b/testing/cc/jwt_impl.cc
index ee46f20..501a404 100644
--- a/testing/cc/jwt_impl.cc
+++ b/testing/cc/jwt_impl.cc
@@ -17,9 +17,9 @@
 // Implementation of a JWT Service.
 #include "jwt_impl.h"
 
+#include <memory>
 #include <ostream>
 #include <sstream>
-#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -34,6 +34,7 @@
 #include "tink/jwt/jwt_public_key_verify.h"
 #include "tink/jwt/raw_jwt.h"
 #include "tink/util/status.h"
+#include "create.h"
 
 namespace tink_testing_api {
 
@@ -43,7 +44,6 @@
 using ::crypto::tink::JwtMac;
 using ::crypto::tink::JwtPublicKeySign;
 using ::crypto::tink::JwtPublicKeyVerify;
-using ::crypto::tink::KeysetHandle;
 using ::crypto::tink::KeysetReader;
 using ::crypto::tink::RawJwt;
 using ::crypto::tink::VerifiedJwt;
@@ -210,24 +210,31 @@
   return builder.Build();
 }
 
+::grpc::Status JwtImpl::CreateJwtMac(grpc::ServerContext* context,
+                                     const CreationRequest* request,
+                                     CreationResponse* response) {
+  return CreatePrimitiveForRpc<JwtMac>(request, response);
+}
+
+::grpc::Status JwtImpl::CreateJwtPublicKeySign(grpc::ServerContext* context,
+                                               const CreationRequest* request,
+                                               CreationResponse* response) {
+  return CreatePrimitiveForRpc<JwtPublicKeySign>(request, response);
+}
+
+::grpc::Status JwtImpl::CreateJwtPublicKeyVerify(grpc::ServerContext* context,
+                                                 const CreationRequest* request,
+                                                 CreationResponse* response) {
+  return CreatePrimitiveForRpc<JwtPublicKeyVerify>(request, response);
+}
+
 // Computes a MAC and generates a signed compact JWT
 grpc::Status JwtImpl::ComputeMacAndEncode(grpc::ServerContext* context,
                                             const JwtSignRequest* request,
                                             JwtSignResponse* response) {
-  StatusOr<std::unique_ptr<KeysetReader>> reader =
-      BinaryKeysetReader::New(request->keyset());
-  if (!reader.ok()) {
-    response->set_err(std::string(reader.status().message()));
-    return grpc::Status::OK;
-  }
-  StatusOr<std::unique_ptr<KeysetHandle>> handle =
-      CleartextKeysetHandle::Read(*std::move(reader));
-  if (!handle.ok()) {
-    response->set_err(std::string(handle.status().message()));
-    return grpc::Status::OK;
-  }
   StatusOr<std::unique_ptr<JwtMac>> jwt_mac =
-      (*handle)->GetPrimitive<JwtMac>();
+      PrimitiveFromSerializedBinaryProtoKeyset<JwtMac>(
+          request->annotated_keyset());
   if (!jwt_mac.ok()) {
     response->set_err(std::string(jwt_mac.status().message()));
     return grpc::Status::OK;
@@ -251,19 +258,9 @@
 grpc::Status JwtImpl::VerifyMacAndDecode(grpc::ServerContext* context,
                                            const JwtVerifyRequest* request,
                                            JwtVerifyResponse* response) {
-  StatusOr<std::unique_ptr<KeysetReader>> reader =
-      BinaryKeysetReader::New(request->keyset());
-  if (!reader.ok()) {
-    response->set_err(std::string(reader.status().message()));
-    return grpc::Status::OK;
-  }
-  StatusOr<std::unique_ptr<KeysetHandle>> handle =
-      CleartextKeysetHandle::Read(*std::move(reader));
-  if (!handle.ok()) {
-    response->set_err(std::string(handle.status().message()));
-    return grpc::Status::OK;
-  }
-  StatusOr<std::unique_ptr<JwtMac>> jwt_mac = (*handle)->GetPrimitive<JwtMac>();
+  StatusOr<std::unique_ptr<JwtMac>> jwt_mac =
+      PrimitiveFromSerializedBinaryProtoKeyset<JwtMac>(
+          request->annotated_keyset());
   if (!jwt_mac.ok()) {
     response->set_err(std::string(jwt_mac.status().message()));
     return grpc::Status::OK;
@@ -283,20 +280,9 @@
 grpc::Status JwtImpl::PublicKeySignAndEncode(grpc::ServerContext* context,
                                    const JwtSignRequest* request,
                                    JwtSignResponse* response) {
-  StatusOr<std::unique_ptr<KeysetReader>> reader =
-      BinaryKeysetReader::New(request->keyset());
-  if (!reader.ok()) {
-    response->set_err(std::string(reader.status().message()));
-    return grpc::Status::OK;
-  }
-  StatusOr<std::unique_ptr<KeysetHandle>> handle =
-      CleartextKeysetHandle::Read(*std::move(reader));
-  if (!handle.ok()) {
-    response->set_err(std::string(handle.status().message()));
-    return grpc::Status::OK;
-  }
   StatusOr<std::unique_ptr<JwtPublicKeySign>> jwt_sign =
-      (*handle)->GetPrimitive<JwtPublicKeySign>();
+      PrimitiveFromSerializedBinaryProtoKeyset<JwtPublicKeySign>(
+          request->annotated_keyset());
   if (!jwt_sign.ok()) {
     response->set_err(std::string(jwt_sign.status().message()));
     return grpc::Status::OK;
@@ -318,20 +304,9 @@
 grpc::Status JwtImpl::PublicKeyVerifyAndDecode(grpc::ServerContext* context,
                                         const JwtVerifyRequest* request,
                                         JwtVerifyResponse* response) {
-  StatusOr<std::unique_ptr<KeysetReader>> reader =
-      BinaryKeysetReader::New(request->keyset());
-  if (!reader.ok()) {
-    response->set_err(std::string(reader.status().message()));
-    return grpc::Status::OK;
-  }
-  StatusOr<std::unique_ptr<KeysetHandle>> handle =
-      CleartextKeysetHandle::Read(*std::move(reader));
-  if (!handle.ok()) {
-    response->set_err(std::string(handle.status().message()));
-    return grpc::Status::OK;
-  }
   StatusOr<std::unique_ptr<JwtPublicKeyVerify>> jwt_verify =
-      (*handle)->GetPrimitive<JwtPublicKeyVerify>();
+      PrimitiveFromSerializedBinaryProtoKeyset<JwtPublicKeyVerify>(
+          request->annotated_keyset());
   if (!jwt_verify.ok()) {
     response->set_err(std::string(jwt_verify.status().message()));
     return grpc::Status::OK;
@@ -357,7 +332,7 @@
     response->set_err(std::string(reader.status().message()));
     return ::grpc::Status::OK;
   }
-  StatusOr<std::unique_ptr<KeysetHandle>> handle =
+  StatusOr<std::unique_ptr<::crypto::tink::KeysetHandle>> handle =
       CleartextKeysetHandle::Read(*std::move(reader));
   if (!handle.ok()) {
     response->set_err(std::string(handle.status().message()));
@@ -375,7 +350,7 @@
 ::grpc::Status JwtImpl::FromJwkSet(grpc::ServerContext* context,
                                    const JwtFromJwkSetRequest* request,
                                    JwtFromJwkSetResponse* response) {
-  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+  StatusOr<std::unique_ptr<::crypto::tink::KeysetHandle>> keyset_handle =
       JwkSetToPublicKeysetHandle(request->jwk_set());
   if (!keyset_handle.ok()) {
     response->set_err(std::string(keyset_handle.status().message()));
diff --git a/testing/cc/jwt_impl.h b/testing/cc/jwt_impl.h
index c8cb228..fd8fb5d 100644
--- a/testing/cc/jwt_impl.h
+++ b/testing/cc/jwt_impl.h
@@ -28,6 +28,18 @@
 // A Jwt Service.
 class JwtImpl final : public Jwt::Service {
  public:
+  grpc::Status CreateJwtMac(grpc::ServerContext* context,
+                            const CreationRequest* request,
+                            CreationResponse* response) override;
+
+  grpc::Status CreateJwtPublicKeySign(grpc::ServerContext* context,
+                                      const CreationRequest* request,
+                                      CreationResponse* response) override;
+
+  grpc::Status CreateJwtPublicKeyVerify(grpc::ServerContext* context,
+                                        const CreationRequest* request,
+                                        CreationResponse* response) override;
+
   grpc::Status ComputeMacAndEncode(grpc::ServerContext* context,
                                    const JwtSignRequest* request,
                                    JwtSignResponse* response) override;
diff --git a/testing/cc/jwt_impl_test.cc b/testing/cc/jwt_impl_test.cc
index 87f0cb5..3a81ffb 100644
--- a/testing/cc/jwt_impl_test.cc
+++ b/testing/cc/jwt_impl_test.cc
@@ -16,6 +16,9 @@
 
 #include "jwt_impl.h"
 
+#include <memory>
+#include <ostream>
+#include <sstream>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -41,6 +44,8 @@
 using ::testing::Eq;
 using ::testing::IsEmpty;
 using ::testing::Not;
+using ::tink_testing_api::CreationRequest;
+using ::tink_testing_api::CreationResponse;
 using ::tink_testing_api::JwtFromJwkSetRequest;
 using ::tink_testing_api::JwtFromJwkSetResponse;
 using ::tink_testing_api::JwtSignRequest;
@@ -73,11 +78,32 @@
   static void SetUpTestSuite() { ASSERT_THAT(JwtMacRegister(), IsOk()); }
 };
 
+TEST_F(JwtImplMacTest, CreateJwtMacSuccess) {
+  tink_testing_api::JwtImpl jwt;
+  std::string keyset = ValidKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(jwt.CreateJwtMac(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(JwtImplMacTest, CreateJwtMacFails) {
+  tink_testing_api::JwtImpl jwt;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
+  CreationResponse response;
+
+  EXPECT_TRUE(jwt.CreateJwtMac(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(JwtImplMacTest, MacComputeVerifySuccess) {
   tink_testing_api::JwtImpl jwt;
   std::string keyset = ValidKeyset();
   JwtSignRequest comp_request;
-  comp_request.set_keyset(keyset);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   JwtToken* raw_jwt = comp_request.mutable_raw_jwt();
   raw_jwt->mutable_type_header()->set_value("type_header");
   raw_jwt->mutable_issuer()->set_value("issuer");
@@ -101,7 +127,7 @@
   EXPECT_THAT(comp_response.err(), IsEmpty());
 
   JwtVerifyRequest verify_request;
-  verify_request.set_keyset(keyset);
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   verify_request.set_signed_compact_jwt(comp_response.signed_compact_jwt());
   JwtValidator* validator = verify_request.mutable_validator();
   validator->mutable_expected_type_header()->set_value("type_header");
@@ -140,7 +166,7 @@
 TEST_F(JwtImplMacTest, ComputeBadKeysetFail) {
   tink_testing_api::JwtImpl jwt;
   JwtSignRequest comp_request;
-  comp_request.set_keyset("bad keyset");
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   comp_request.mutable_raw_jwt()->mutable_issuer()->set_value("issuer");
   JwtSignResponse comp_response;
 
@@ -153,7 +179,7 @@
   tink_testing_api::JwtImpl jwt;
   std::string keyset = ValidKeyset();
   JwtSignRequest comp_request;
-  comp_request.set_keyset(keyset);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   comp_request.mutable_raw_jwt()->mutable_issuer()->set_value("unknown");
   JwtSignResponse comp_response;
   EXPECT_TRUE(
@@ -161,7 +187,7 @@
   EXPECT_THAT(comp_response.err(), IsEmpty());
 
   JwtVerifyRequest verify_request;
-  verify_request.set_keyset(keyset);
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   verify_request.set_signed_compact_jwt(comp_response.signed_compact_jwt());
   verify_request.mutable_validator()->mutable_expected_issuer()->set_value(
       "issuer");
@@ -209,10 +235,54 @@
   std::string public_keyset_;
 };
 
+TEST_F(JwtImplSignatureTest, CreatePublicKeySignSuccess) {
+  tink_testing_api::JwtImpl jwt;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(private_keyset_);
+  CreationResponse response;
+
+  EXPECT_TRUE(jwt.CreateJwtPublicKeySign(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(JwtImplSignatureTest, CreatePublicKeySignFailure) {
+  tink_testing_api::JwtImpl jwt;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("\x80");
+  CreationResponse response;
+
+  EXPECT_TRUE(jwt.CreateJwtPublicKeySign(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
+TEST_F(JwtImplSignatureTest, CreatePublicKeyVerifySuccess) {
+  tink_testing_api::JwtImpl jwt;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(public_keyset_);
+  CreationResponse response;
+
+  EXPECT_TRUE(jwt.CreateJwtPublicKeyVerify(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(JwtImplSignatureTest, CreatePublicKeyVerifyFailure) {
+  tink_testing_api::JwtImpl jwt;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("\x80");
+  CreationResponse response;
+
+  EXPECT_TRUE(jwt.CreateJwtPublicKeyVerify(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(JwtImplSignatureTest, SignVerifySuccess) {
   tink_testing_api::JwtImpl jwt;
   JwtSignRequest comp_request;
-  comp_request.set_keyset(private_keyset_);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(
+      private_keyset_);
   JwtToken* raw_jwt = comp_request.mutable_raw_jwt();
   raw_jwt->mutable_type_header()->set_value("type_header");
   raw_jwt->mutable_issuer()->set_value("issuer");
@@ -235,7 +305,8 @@
   EXPECT_THAT(comp_response.err(), IsEmpty());
 
   JwtVerifyRequest verify_request;
-  verify_request.set_keyset(public_keyset_);
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(
+      public_keyset_);
   verify_request.set_signed_compact_jwt(comp_response.signed_compact_jwt());
   JwtValidator* validator = verify_request.mutable_validator();
   validator->mutable_expected_type_header()->set_value("type_header");
@@ -271,7 +342,7 @@
 TEST_F(JwtImplSignatureTest, SignWithBadKeysetFails) {
   tink_testing_api::JwtImpl jwt;
   JwtSignRequest comp_request;
-  comp_request.set_keyset("bad keyset");
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   comp_request.mutable_raw_jwt()->mutable_issuer()->set_value("issuer");
   JwtSignResponse comp_response;
 
@@ -283,7 +354,8 @@
 TEST_F(JwtImplSignatureTest, VerifyWithWrongIssuerFails) {
   tink_testing_api::JwtImpl jwt;
   JwtSignRequest comp_request;
-  comp_request.set_keyset(private_keyset_);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(
+      private_keyset_);
   comp_request.mutable_raw_jwt()->mutable_issuer()->set_value("unknown");
   JwtSignResponse comp_response;
   EXPECT_TRUE(
@@ -291,7 +363,8 @@
   EXPECT_THAT(comp_response.err(), IsEmpty());
 
   JwtVerifyRequest verify_request;
-  verify_request.set_keyset(public_keyset_);
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(
+      public_keyset_);
   verify_request.set_signed_compact_jwt(comp_response.signed_compact_jwt());
   verify_request.mutable_validator()->mutable_expected_issuer()->set_value(
       "issuer");
@@ -308,7 +381,8 @@
 
   // Create a signed token
   JwtSignRequest comp_request;
-  comp_request.set_keyset(private_keyset_);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(
+      private_keyset_);
   JwtToken* raw_jwt = comp_request.mutable_raw_jwt();
   raw_jwt->mutable_issuer()->set_value("issuer");
   raw_jwt->mutable_expiration()->set_seconds(34567);
@@ -336,7 +410,8 @@
 
   // Verify the token using the public keyset
   JwtVerifyRequest verify_request;
-  verify_request.set_keyset(from_jwk_response.keyset());
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(
+      from_jwk_response.keyset());
   verify_request.set_signed_compact_jwt(comp_response.signed_compact_jwt());
   JwtValidator* validator = verify_request.mutable_validator();
   validator->mutable_expected_issuer()->set_value("issuer");
diff --git a/testing/cc/keyset_impl.cc b/testing/cc/keyset_impl.cc
index 351f3b3..83fdb88 100644
--- a/testing/cc/keyset_impl.cc
+++ b/testing/cc/keyset_impl.cc
@@ -77,8 +77,12 @@
       crypto::tink::DeterministicAeadKeyTemplates::Aes256Siv();
   key_templates_["AES128_CTR_HMAC_SHA256_4KB"] =
       crypto::tink::StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment4KB();
+  key_templates_["AES128_CTR_HMAC_SHA256_1MB"] =
+      crypto::tink::StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment1MB();
   key_templates_["AES256_CTR_HMAC_SHA256_4KB"] =
       crypto::tink::StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment4KB();
+  key_templates_["AES256_CTR_HMAC_SHA256_1MB"] =
+      crypto::tink::StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment1MB();
   key_templates_["AES128_GCM_HKDF_4KB"] =
       crypto::tink::StreamingAeadKeyTemplates::Aes128GcmHkdf4KB();
   key_templates_["AES256_GCM_HKDF_4KB"] =
@@ -225,7 +229,7 @@
     response->set_err("Could not parse the key template");
     return grpc::Status::OK;
   }
-  auto handle_result = KeysetHandle::GenerateNew(key_template);
+  auto handle_result = ::crypto::tink::KeysetHandle::GenerateNew(key_template);
   if (!handle_result.ok()) {
     response->set_err(std::string(handle_result.status().message()));
     return grpc::Status::OK;
@@ -478,7 +482,7 @@
   std::unique_ptr<KeysetHandle> keyset_handle;
   if (request->has_associated_data()) {
     StatusOr<std::unique_ptr<KeysetHandle>> read_result =
-        KeysetHandle::ReadWithAssociatedData(
+        ::crypto::tink::KeysetHandle::ReadWithAssociatedData(
             std::move(keyset_reader), **master_aead,
             request->associated_data().value());
     if (!read_result.ok()) {
@@ -488,7 +492,8 @@
     keyset_handle = *std::move(read_result);
   } else {
     StatusOr<std::unique_ptr<KeysetHandle>> read_result =
-        KeysetHandle::Read(std::move(keyset_reader), **master_aead);
+        ::crypto::tink::KeysetHandle::Read(std::move(keyset_reader),
+                                           **master_aead);
     if (!read_result.ok()) {
       response->set_err(std::string(read_result.status().message()));
       return grpc::Status::OK;
diff --git a/testing/cc/keyset_impl.h b/testing/cc/keyset_impl.h
index 35a8747..cd68790 100644
--- a/testing/cc/keyset_impl.h
+++ b/testing/cc/keyset_impl.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_TESTING_SERIVCES_KEYSET_IMPL_H_
-#define TINK_TESTING_SERIVCES_KEYSET_IMPL_H_
+#ifndef TINK_TESTING_KEYSET_IMPL_H_
+#define TINK_TESTING_KEYSET_IMPL_H_
 
 #include <grpcpp/grpcpp.h>
 #include <grpcpp/server_context.h>
@@ -75,4 +75,4 @@
 
 }  // namespace tink_testing_api
 
-#endif  // TINK_TESTING_SERIVCES_KEYSET_IMPL_H_
+#endif  // TINK_TESTING_KEYSET_IMPL_H_
diff --git a/testing/cc/mac_impl.cc b/testing/cc/mac_impl.cc
index d6b9007..1301ff3 100644
--- a/testing/cc/mac_impl.cc
+++ b/testing/cc/mac_impl.cc
@@ -17,37 +17,31 @@
 // Implementation of a MAC Service.
 #include "mac_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
 #include "tink/mac.h"
+#include "create.h"
 #include "proto/testing_api.grpc.pb.h"
 
 namespace tink_testing_api {
 
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
-using ::grpc::ServerContext;
-using ::grpc::Status;
+using ::crypto::tink::util::StatusOr;
+
+::grpc::Status MacImpl::Create(grpc::ServerContext* context,
+                               const CreationRequest* request,
+                               CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::Mac>(request, response);
+}
 
 // Computes a MAC
 ::grpc::Status MacImpl::ComputeMac(grpc::ServerContext* context,
                                    const ComputeMacRequest* request,
                                    ComputeMacResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto mac_result = handle_result.value()->GetPrimitive<crypto::tink::Mac>();
+  StatusOr<std::unique_ptr<crypto::tink::Mac>> mac_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Mac>(
+          request->annotated_keyset());
   if (!mac_result.ok()) {
     response->set_err(std::string(mac_result.status().message()));
     return ::grpc::Status::OK;
@@ -65,18 +59,9 @@
 ::grpc::Status MacImpl::VerifyMac(grpc::ServerContext* context,
                                   const VerifyMacRequest* request,
                                   VerifyMacResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto mac_result = handle_result.value()->GetPrimitive<crypto::tink::Mac>();
+  StatusOr<std::unique_ptr<crypto::tink::Mac>> mac_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::Mac>(
+          request->annotated_keyset());
   if (!mac_result.ok()) {
     response->set_err(std::string(mac_result.status().message()));
     return ::grpc::Status::OK;
diff --git a/testing/cc/mac_impl.h b/testing/cc/mac_impl.h
index f14e5eb..0cab647 100644
--- a/testing/cc/mac_impl.h
+++ b/testing/cc/mac_impl.h
@@ -28,6 +28,10 @@
 // A MAC Service.
 class MacImpl final : public Mac::Service {
  public:
+  grpc::Status Create(grpc::ServerContext* context,
+                      const CreationRequest* request,
+                      CreationResponse* response) override;
+
   grpc::Status ComputeMac(grpc::ServerContext* context,
                           const ComputeMacRequest* request,
                           ComputeMacResponse* response) override;
diff --git a/testing/cc/mac_impl_test.cc b/testing/cc/mac_impl_test.cc
index ede0a8e..06ee00b 100644
--- a/testing/cc/mac_impl_test.cc
+++ b/testing/cc/mac_impl_test.cc
@@ -37,6 +37,8 @@
 using ::testing::IsEmpty;
 using ::tink_testing_api::ComputeMacRequest;
 using ::tink_testing_api::ComputeMacResponse;
+using ::tink_testing_api::CreationRequest;
+using ::tink_testing_api::CreationResponse;
 using ::tink_testing_api::VerifyMacRequest;
 using ::tink_testing_api::VerifyMacResponse;
 
@@ -63,11 +65,32 @@
   static void SetUpTestSuite() { ASSERT_TRUE(MacConfig::Register().ok()); }
 };
 
+TEST_F(MacImplTest, CreateMacSuccess) {
+  tink_testing_api::MacImpl mac;
+  std::string keyset = ValidKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(mac.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(MacImplTest, CreateMacFails) {
+  tink_testing_api::MacImpl mac;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
+  CreationResponse response;
+
+  EXPECT_TRUE(mac.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(MacImplTest, ComputeVerifySuccess) {
   tink_testing_api::MacImpl mac;
   std::string keyset = ValidKeyset();
   ComputeMacRequest comp_request;
-  comp_request.set_keyset(keyset);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   comp_request.set_data("some data");
   ComputeMacResponse comp_response;
 
@@ -75,7 +98,7 @@
   EXPECT_THAT(comp_response.err(), IsEmpty());
 
   VerifyMacRequest verify_request;
-  verify_request.set_keyset(keyset);
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   verify_request.set_mac_value(comp_response.mac_value());
   verify_request.set_data("some data");
   VerifyMacResponse verify_response;
@@ -87,7 +110,7 @@
 TEST_F(MacImplTest, ComputeBadKeysetFail) {
   tink_testing_api::MacImpl mac;
   ComputeMacRequest comp_request;
-  comp_request.set_keyset("bad keyset");
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   comp_request.set_data("some data");
   ComputeMacResponse comp_response;
 
@@ -99,7 +122,7 @@
   tink_testing_api::MacImpl mac;
   std::string keyset = ValidKeyset();
   VerifyMacRequest verify_request;
-  verify_request.set_keyset(keyset);
+  verify_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   verify_request.set_mac_value("bad mac value");
   verify_request.set_data("some data");
   VerifyMacResponse verify_response;
diff --git a/testing/cc/metadata_impl.cc b/testing/cc/metadata_impl.cc
index 8dfe7fb..2977b25 100644
--- a/testing/cc/metadata_impl.cc
+++ b/testing/cc/metadata_impl.cc
@@ -20,9 +20,6 @@
 
 namespace tink_testing_api {
 
-using ::grpc::ServerContext;
-using ::grpc::Status;
-
 // Returns server info.
 grpc::Status MetadataImpl::GetServerInfo(grpc::ServerContext* context,
                                          const ServerInfoRequest* request,
diff --git a/testing/cc/prf_set_impl.cc b/testing/cc/prf_set_impl.cc
index a21f0d4..3f44e4e 100644
--- a/testing/cc/prf_set_impl.cc
+++ b/testing/cc/prf_set_impl.cc
@@ -17,38 +17,32 @@
 // Implementation of a PrfSet Service.
 #include "prf_set_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
 #include "tink/prf/prf_set.h"
+#include "create.h"
 #include "proto/testing_api.grpc.pb.h"
 
 namespace tink_testing_api {
 
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
+using ::crypto::tink::util::StatusOr;
 using ::grpc::ServerContext;
-using ::grpc::Status;
+
+::grpc::Status PrfSetImpl::Create(grpc::ServerContext* context,
+                                  const CreationRequest* request,
+                                  CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::PrfSet>(request, response);
+}
 
 // Returns the Key Ids of the Keyset.
 ::grpc::Status PrfSetImpl::KeyIds(ServerContext* context,
                                   const PrfSetKeyIdsRequest* request,
                                   PrfSetKeyIdsResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto prf_set_result =
-      handle_result.value()->GetPrimitive<crypto::tink::PrfSet>();
+  StatusOr<std::unique_ptr<crypto::tink::PrfSet>> prf_set_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::PrfSet>(
+          request->annotated_keyset());
   if (!prf_set_result.ok()) {
     response->set_err(std::string(prf_set_result.status().message()));
     return ::grpc::Status::OK;
@@ -65,19 +59,9 @@
 ::grpc::Status PrfSetImpl::Compute(ServerContext* context,
                                    const PrfSetComputeRequest* request,
                                    PrfSetComputeResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto prf_set_result =
-      handle_result.value()->GetPrimitive<crypto::tink::PrfSet>();
+  StatusOr<std::unique_ptr<crypto::tink::PrfSet>> prf_set_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::PrfSet>(
+          request->annotated_keyset());
   if (!prf_set_result.ok()) {
     response->set_err(std::string(prf_set_result.status().message()));
     return ::grpc::Status::OK;
diff --git a/testing/cc/prf_set_impl.h b/testing/cc/prf_set_impl.h
index 02d0e38..dfcd15e 100644
--- a/testing/cc/prf_set_impl.h
+++ b/testing/cc/prf_set_impl.h
@@ -28,6 +28,10 @@
 // A PrfSet Service.
 class PrfSetImpl final : public PrfSet::Service {
  public:
+  grpc::Status Create(grpc::ServerContext* context,
+                      const CreationRequest* request,
+                      CreationResponse* response) override;
+
   grpc::Status KeyIds(grpc::ServerContext* context,
                       const PrfSetKeyIdsRequest* request,
                       PrfSetKeyIdsResponse* response) override;
diff --git a/testing/cc/prf_set_impl_test.cc b/testing/cc/prf_set_impl_test.cc
index 62ffd71..9314026 100644
--- a/testing/cc/prf_set_impl_test.cc
+++ b/testing/cc/prf_set_impl_test.cc
@@ -37,6 +37,8 @@
 using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::IsEmpty;
+using ::tink_testing_api::CreationRequest;
+using ::tink_testing_api::CreationResponse;
 using ::tink_testing_api::PrfSetComputeRequest;
 using ::tink_testing_api::PrfSetComputeResponse;
 using ::tink_testing_api::PrfSetKeyIdsRequest;
@@ -65,12 +67,33 @@
   static void SetUpTestSuite() { ASSERT_TRUE(PrfConfig::Register().ok()); }
 };
 
+TEST_F(PrfSetImplTest, CreateAeadSuccess) {
+  tink_testing_api::PrfSetImpl prfset;
+  std::string keyset = ValidKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(prfset.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(PrfSetImplTest, CreateAeadFails) {
+  tink_testing_api::PrfSetImpl prfset;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
+  CreationResponse response;
+
+  EXPECT_TRUE(prfset.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(PrfSetImplTest, KeyIdsComputeSuccess) {
   tink_testing_api::PrfSetImpl prfset;
   std::string keyset = ValidKeyset();
 
   PrfSetKeyIdsRequest key_id_request;
-  key_id_request.set_keyset(keyset);
+  key_id_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   PrfSetKeyIdsResponse key_id_response;
 
   EXPECT_TRUE(prfset.KeyIds(nullptr, &key_id_request, &key_id_response).ok());
@@ -79,7 +102,7 @@
               ElementsAre(key_id_response.output().primary_key_id()));
 
   PrfSetComputeRequest comp_request;
-  comp_request.set_keyset(keyset);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   comp_request.set_key_id(key_id_response.output().primary_key_id());
   comp_request.set_input_data("some data");
   comp_request.set_output_length(16);
@@ -93,7 +116,8 @@
 TEST_F(PrfSetImplTest, KeyIdsBadKeysetFail) {
   tink_testing_api::PrfSetImpl prfset;
   PrfSetKeyIdsRequest key_id_request;
-  key_id_request.set_keyset("bad keyset");
+  key_id_request.mutable_annotated_keyset()->set_serialized_keyset(
+      "bad keyset");
   PrfSetKeyIdsResponse key_id_response;
 
   EXPECT_TRUE(prfset.KeyIds(nullptr, &key_id_request, &key_id_response).ok());
@@ -103,7 +127,7 @@
 TEST_F(PrfSetImplTest, ComputeBadKeysetFail) {
   tink_testing_api::PrfSetImpl prfset;
   PrfSetComputeRequest comp_request;
-  comp_request.set_keyset("bad keyset");
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   comp_request.set_key_id(1234);
   comp_request.set_input_data("some data");
   comp_request.set_output_length(16);
@@ -117,13 +141,13 @@
   tink_testing_api::PrfSetImpl prfset;
   std::string keyset = ValidKeyset();
   PrfSetKeyIdsRequest key_id_request;
-  key_id_request.set_keyset(keyset);
+  key_id_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   PrfSetKeyIdsResponse key_id_response;
   EXPECT_TRUE(prfset.KeyIds(nullptr, &key_id_request, &key_id_response).ok());
   EXPECT_THAT(key_id_response.err(), IsEmpty());
 
   PrfSetComputeRequest comp_request;
-  comp_request.set_keyset(keyset);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   comp_request.set_key_id(key_id_response.output().primary_key_id());
   comp_request.set_input_data("some data");
   comp_request.set_output_length(123456);  // bad output length
@@ -137,7 +161,7 @@
   std::string keyset = ValidKeyset();
 
   PrfSetComputeRequest comp_request;
-  comp_request.set_keyset(keyset);
+  comp_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   comp_request.set_key_id(12345);  // bad key id
   comp_request.set_input_data("some data");
   comp_request.set_output_length(16);
diff --git a/testing/cc/proto/testing_api.proto b/testing/cc/proto/testing_api.proto
deleted file mode 100644
index 2fb0e9e..0000000
--- a/testing/cc/proto/testing_api.proto
+++ /dev/null
@@ -1,503 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-syntax = "proto3";
-
-package tink_testing_api;
-
-import "google/protobuf/duration.proto";
-import "google/protobuf/timestamp.proto";
-import "google/protobuf/wrappers.proto";
-
-option java_package = "com.google.crypto.tink.testing.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
-// Placeholder for java_stubby_library
-
-// Service providing metadata about the server.
-service Metadata {
-  // Returns some server information. A test may use this information to verify
-  // that it is talking to the right server.
-  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
-}
-
-message ServerInfoRequest {}
-
-message ServerInfoResponse {
-  string tink_version = 1;  // For example '1.4'
-  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
-}
-
-// Service for Keyset operations.
-service Keyset {
-  // Generates a key template from a key template name.
-  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
-  // Generates a new keyset from a template.
-  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
-  // Generates a public-key keyset from a private-key keyset.
-  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
-  // Converts a Keyset from Binary to Json Format
-  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
-  // Converts a Keyset from Json to Binary Format
-  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
-  // Reads an encrypted keyset using KeysetHandle.read() or
-  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
-  rpc ReadEncrypted(KeysetReadEncryptedRequest)
-      returns (KeysetReadEncryptedResponse) {}
-  // Writes an encrypted keyset using KeysetHandle.write() or
-  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
-  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
-      returns (KeysetWriteEncryptedResponse) {}
-}
-
-message KeysetTemplateRequest {
-  string template_name = 1;  // template name used by Tinkey
-}
-
-message KeysetTemplateResponse {
-  oneof result {
-    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
-    string err = 2;
-  }
-}
-
-message KeysetGenerateRequest {
-  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
-}
-
-message KeysetGenerateResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetPublicRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetPublicResponse {
-  oneof result {
-    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetToJsonRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetToJsonResponse {
-  oneof result {
-    string json_keyset = 1;
-    string err = 2;
-  }
-}
-
-message KeysetFromJsonRequest {
-  string json_keyset = 1;
-}
-
-message KeysetFromJsonResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-// Copy of google.protobuf.BytesValue
-message BytesValue {
-  // The bytes value.
-  bytes value = 1;
-}
-
-enum KeysetReaderType {
-  KEYSET_READER_UNKNOWN = 0;
-  KEYSET_READER_BINARY = 1;
-  KEYSET_READER_JSON = 2;
-}
-
-message KeysetReadEncryptedRequest {
-  bytes encrypted_keyset = 1;
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetReaderType keyset_reader_type = 4;
-}
-
-message KeysetReadEncryptedResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-enum KeysetWriterType {
-  KEYSET_WRITER_UNKNOWN = 0;
-  KEYSET_WRITER_BINARY = 1;
-  KEYSET_WRITER_JSON = 2;
-}
-
-message KeysetWriteEncryptedRequest {
-  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetWriterType keyset_writer_type = 4;
-}
-
-message KeysetWriteEncryptedResponse {
-  oneof result {
-    bytes encrypted_keyset = 1;
-    string err = 2;
-  }
-}
-
-// Service for AEAD encryption and decryption
-service Aead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
-}
-
-message AeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message AeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Deterministic AEAD encryption and decryption
-service DeterministicAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
-      returns (DeterministicAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
-      returns (DeterministicAeadDecryptResponse) {}
-}
-
-message DeterministicAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message DeterministicAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Streaming AEAD encryption and decryption
-service StreamingAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(StreamingAeadEncryptRequest)
-      returns (StreamingAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(StreamingAeadDecryptRequest)
-      returns (StreamingAeadDecryptResponse) {}
-}
-
-message StreamingAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message StreamingAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to compute and verify MACs
-service Mac {
-  // Computes a MAC for given data
-  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
-  // Verifies the validity of the MAC value, no error means success
-  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
-}
-
-message ComputeMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message ComputeMacResponse {
-  oneof result {
-    bytes mac_value = 1;
-    string err = 2;
-  }
-}
-
-message VerifyMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes mac_value = 2;
-  bytes data = 3;
-}
-
-message VerifyMacResponse {
-  string err = 1;
-}
-
-// Service to hybrid encrypt and decrypt
-service Hybrid {
-  // Encrypts plaintext binding context_info to the resulting ciphertext
-  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
-  // Decrypts ciphertext verifying the integrity of context_info
-  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
-}
-
-message HybridEncryptRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes context_info = 3;
-}
-
-message HybridEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message HybridDecryptRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes context_info = 3;
-}
-
-message HybridDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to sign and verify signatures.
-service Signature {
-  // Computes the signature for data
-  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
-  // Verifies that signature is a digital signature for data
-  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
-}
-
-message SignatureSignRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message SignatureSignResponse {
-  oneof result {
-    bytes signature = 1;
-    string err = 2;
-  }
-}
-
-message SignatureVerifyRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes signature = 2;
-  bytes data = 3;
-}
-
-message SignatureVerifyResponse {
-  string err = 1;
-}
-
-// Service for PrfSet computation
-service PrfSet {
-  // Returns the key ids and the primary key id in the keyset.
-  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
-  // Computes the output of the PRF with the given key_id in the PrfSet.
-  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
-}
-
-message PrfSetKeyIdsRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message PrfSetKeyIdsResponse {
-  message Output {
-    uint32 primary_key_id = 1;
-    repeated uint32 key_id = 2;
-  }
-  oneof result {
-    Output output = 1;
-    string err = 2;
-  }
-}
-
-message PrfSetComputeRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  uint32 key_id = 2;
-  bytes input_data = 3;
-  int32 output_length = 4;
-}
-
-message PrfSetComputeResponse {
-  oneof result {
-    bytes output = 1;
-    string err = 2;
-  }
-}
-
-// Service for JSON Web Tokens (JWT)
-service Jwt {
-  // Computes a signed compact JWT token.
-  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Computes a signed compact JWT token.
-  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Converts a Keyset from Tink Binary to JWK Set Format
-  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
-  // Converts a Keyset from JWK Set to Tink Binary Format
-  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
-}
-
-//  Used to represent the JSON null value.
-enum NullValue {
-  NULL_VALUE = 0;
-}
-
-message JwtClaimValue {
-  oneof kind {
-    NullValue null_value = 2;
-    double number_value = 3;
-    string string_value = 4;
-    bool bool_value = 5;
-    string json_object_value = 6;
-    string json_array_value = 7;
-  }
-}
-
-message JwtToken {
-  google.protobuf.StringValue issuer = 1;
-  google.protobuf.StringValue subject = 2;
-  repeated string audiences = 3;
-  google.protobuf.StringValue jwt_id = 4;
-  google.protobuf.Timestamp expiration = 5;
-  google.protobuf.Timestamp not_before = 6;
-  google.protobuf.Timestamp issued_at = 7;
-  map<string, JwtClaimValue> custom_claims = 8;
-  google.protobuf.StringValue type_header = 9;
-}
-
-message JwtValidator {
-  google.protobuf.StringValue expected_type_header = 7;
-  google.protobuf.StringValue expected_issuer = 1;
-  google.protobuf.StringValue expected_audience = 3;
-  bool ignore_type_header = 8;
-  bool ignore_issuer = 9;
-  bool ignore_audience = 11;
-  bool allow_missing_expiration = 12;
-  bool expect_issued_in_the_past = 13;
-  google.protobuf.Timestamp now = 5;
-  google.protobuf.Duration clock_skew = 6;
-}
-
-message JwtSignRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  JwtToken raw_jwt = 2;
-}
-
-message JwtSignResponse {
-  oneof result {
-    string signed_compact_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtVerifyRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  string signed_compact_jwt = 2;
-  JwtValidator validator = 3;
-}
-
-message JwtVerifyResponse {
-  oneof result {
-    JwtToken verified_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtToJwkSetRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message JwtToJwkSetResponse {
-  oneof result {
-    string jwk_set = 1;
-    string err = 2;
-  }
-}
-
-message JwtFromJwkSetRequest {
-  string jwk_set = 1;
-}
-
-message JwtFromJwkSetResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
diff --git a/testing/cc/proto/BUILD.bazel b/testing/cc/protos/BUILD.bazel
similarity index 100%
rename from testing/cc/proto/BUILD.bazel
rename to testing/cc/protos/BUILD.bazel
diff --git a/testing/cc/protos/testing_api.proto b/testing/cc/protos/testing_api.proto
new file mode 100644
index 0000000..7a02517
--- /dev/null
+++ b/testing/cc/protos/testing_api.proto
@@ -0,0 +1,566 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+syntax = "proto3";
+
+package tink_testing_api;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+option java_package = "com.google.crypto.tink.testing.proto";
+option java_multiple_files = true;
+option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
+// Placeholder for java_stubby_library
+
+// Service providing metadata about the server.
+service Metadata {
+  // Returns some server information. A test may use this information to verify
+  // that it is talking to the right server.
+  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
+}
+
+message ServerInfoRequest {}
+
+message ServerInfoResponse {
+  string tink_version = 1;  // For example '1.4'
+  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
+}
+
+// Service for Keyset operations.
+service Keyset {
+  // Generates a key template from a key template name.
+  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
+  // Generates a new keyset from a template.
+  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
+  // Generates a public-key keyset from a private-key keyset.
+  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
+  // Converts a Keyset from Binary to Json Format
+  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
+  // Converts a Keyset from Json to Binary Format
+  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
+  // Reads an encrypted keyset using KeysetHandle.read() or
+  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
+  rpc ReadEncrypted(KeysetReadEncryptedRequest)
+      returns (KeysetReadEncryptedResponse) {}
+  // Writes an encrypted keyset using KeysetHandle.write() or
+  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
+  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
+      returns (KeysetWriteEncryptedResponse) {}
+}
+
+message KeysetTemplateRequest {
+  string template_name = 1;  // template name used by Tinkey
+}
+
+message KeysetTemplateResponse {
+  oneof result {
+    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
+    string err = 2;
+  }
+}
+
+message KeysetGenerateRequest {
+  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
+}
+
+message KeysetGenerateResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetPublicRequest {
+  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetPublicResponse {
+  oneof result {
+    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetToJsonRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetToJsonResponse {
+  oneof result {
+    string json_keyset = 1;
+    string err = 2;
+  }
+}
+
+message KeysetFromJsonRequest {
+  string json_keyset = 1;
+}
+
+message KeysetFromJsonResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+// Copy of google.protobuf.BytesValue
+message BytesValue {
+  // The bytes value.
+  bytes value = 1;
+}
+
+enum KeysetReaderType {
+  KEYSET_READER_UNKNOWN = 0;
+  KEYSET_READER_BINARY = 1;
+  KEYSET_READER_JSON = 2;
+}
+
+message KeysetReadEncryptedRequest {
+  bytes encrypted_keyset = 1;
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetReaderType keyset_reader_type = 4;
+}
+
+message KeysetReadEncryptedResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+enum KeysetWriterType {
+  KEYSET_WRITER_UNKNOWN = 0;
+  KEYSET_WRITER_BINARY = 1;
+  KEYSET_WRITER_JSON = 2;
+}
+
+message KeysetWriteEncryptedRequest {
+  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetWriterType keyset_writer_type = 4;
+}
+
+message KeysetWriteEncryptedResponse {
+  oneof result {
+    bytes encrypted_keyset = 1;
+    string err = 2;
+  }
+}
+
+message AnnotatedKeyset {
+  bytes serialized_keyset = 1;  // serialized google.crypto.tink.Keyset.
+  map<string, string> annotations = 2;
+}
+
+message CreationRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message CreationResponse {
+  // Empty means no error
+  string err = 1;
+}
+
+// Service for AEAD encryption and decryption
+service Aead {
+  // Creates an Aead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
+}
+
+message AeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message AeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Deterministic AEAD encryption and decryption
+service DeterministicAead {
+  // Creates a Deterministic AEAD object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
+      returns (DeterministicAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
+      returns (DeterministicAeadDecryptResponse) {}
+}
+
+message DeterministicAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message DeterministicAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Streaming AEAD encryption and decryption
+service StreamingAead {
+  // Creates a StreamingAead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(StreamingAeadEncryptRequest)
+      returns (StreamingAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(StreamingAeadDecryptRequest)
+      returns (StreamingAeadDecryptResponse) {}
+}
+
+message StreamingAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message StreamingAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to compute and verify MACs
+service Mac {
+  // Creates a Mac object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Computes a MAC for given data. The client must call "Create" first to see
+  // if creation succeeds before calling this.
+  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
+  // Verifies the validity of the MAC value, no error means success. The client
+  // must call "Create" first to see if creation succeeds before calling this.
+  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
+}
+
+message ComputeMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message ComputeMacResponse {
+  oneof result {
+    bytes mac_value = 1;
+    string err = 2;
+  }
+}
+
+message VerifyMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes mac_value = 2;
+  bytes data = 3;
+}
+
+message VerifyMacResponse {
+  string err = 1;
+}
+
+// Service to hybrid encrypt and decrypt
+service Hybrid {
+  // Creates a HybridEncrypt object without using it.
+  rpc CreateHybridEncrypt(CreationRequest) returns (CreationResponse) {}
+  // Creates a HybridDecrypt object without using it.
+  rpc CreateHybridDecrypt(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts plaintext binding context_info to the resulting ciphertext. The
+  // client must call "CreateHybridEncrypt" first to see if creation succeeds
+  // before calling this.
+  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
+  // Decrypts ciphertext verifying the integrity of context_info. The client
+  // must call "CreateHybridDecrypt" first to see if creation succeeds before
+  // calling this.
+  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
+}
+
+message HybridEncryptRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes context_info = 3;
+}
+
+message HybridEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message HybridDecryptRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes context_info = 3;
+}
+
+message HybridDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to sign and verify signatures.
+service Signature {
+  // Creates a PublicKeySign object without using it.
+  rpc CreatePublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a PublicKeyVerify object without using it.
+  rpc CreatePublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes the signature for data. The client must call "CreatePublicKeySign"
+  // first to see if creation succeeds before calling this.
+  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
+  // Verifies that signature is a digital signature for data. The client must
+  // call "CreatePublicKeyVerify" first to see if creation succeeds before
+  // calling this.
+  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
+}
+
+message SignatureSignRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message SignatureSignResponse {
+  oneof result {
+    bytes signature = 1;
+    string err = 2;
+  }
+}
+
+message SignatureVerifyRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes signature = 2;
+  bytes data = 3;
+}
+
+message SignatureVerifyResponse {
+  string err = 1;
+}
+
+// Service for PrfSet computation
+service PrfSet {
+  // Creates a PrfSet object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Returns the key ids and the primary key id in the keyset.The client must
+  // call "Create" first to see if creation succeeds before calling this.
+  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
+  // Computes the output of the PRF with the given key_id in the PrfSet.The
+  // client must call "Create" first to see if creation succeeds before calling
+  // this.
+  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
+}
+
+message PrfSetKeyIdsRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message PrfSetKeyIdsResponse {
+  message Output {
+    uint32 primary_key_id = 1;
+    repeated uint32 key_id = 2;
+  }
+  oneof result {
+    Output output = 1;
+    string err = 2;
+  }
+}
+
+message PrfSetComputeRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  uint32 key_id = 2;
+  bytes input_data = 3;
+  int32 output_length = 4;
+}
+
+message PrfSetComputeResponse {
+  oneof result {
+    bytes output = 1;
+    string err = 2;
+  }
+}
+
+// Service for JSON Web Tokens (JWT)
+service Jwt {
+  // Creates a JwtMac object without using it.
+  rpc CreateJwtMac(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeySign object without using it.
+  rpc CreateJwtPublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeyVerify object without using it.
+  rpc CreateJwtPublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes a signed compact JWT token.
+  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Computes a signed compact JWT token.
+  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Converts a Keyset from Tink Binary to JWK Set Format
+  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
+  // Converts a Keyset from JWK Set to Tink Binary Format
+  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
+}
+
+//  Used to represent the JSON null value.
+enum NullValue {
+  NULL_VALUE = 0;
+}
+
+message JwtClaimValue {
+  oneof kind {
+    NullValue null_value = 2;
+    double number_value = 3;
+    string string_value = 4;
+    bool bool_value = 5;
+    string json_object_value = 6;
+    string json_array_value = 7;
+  }
+}
+
+message JwtToken {
+  google.protobuf.StringValue issuer = 1;
+  google.protobuf.StringValue subject = 2;
+  repeated string audiences = 3;
+  google.protobuf.StringValue jwt_id = 4;
+  google.protobuf.Timestamp expiration = 5;
+  google.protobuf.Timestamp not_before = 6;
+  google.protobuf.Timestamp issued_at = 7;
+  map<string, JwtClaimValue> custom_claims = 8;
+  google.protobuf.StringValue type_header = 9;
+}
+
+message JwtValidator {
+  google.protobuf.StringValue expected_type_header = 7;
+  google.protobuf.StringValue expected_issuer = 1;
+  google.protobuf.StringValue expected_audience = 3;
+  bool ignore_type_header = 8;
+  bool ignore_issuer = 9;
+  bool ignore_audience = 11;
+  bool allow_missing_expiration = 12;
+  bool expect_issued_in_the_past = 13;
+  google.protobuf.Timestamp now = 5;
+  google.protobuf.Duration clock_skew = 6;
+}
+
+message JwtSignRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  JwtToken raw_jwt = 2;
+}
+
+message JwtSignResponse {
+  oneof result {
+    string signed_compact_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtVerifyRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  string signed_compact_jwt = 2;
+  JwtValidator validator = 3;
+}
+
+message JwtVerifyResponse {
+  oneof result {
+    JwtToken verified_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtToJwkSetRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message JwtToJwkSetResponse {
+  oneof result {
+    string jwk_set = 1;
+    string err = 2;
+  }
+}
+
+message JwtFromJwkSetRequest {
+  string jwk_set = 1;
+}
+
+message JwtFromJwkSetResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
diff --git a/testing/cc/signature_impl.cc b/testing/cc/signature_impl.cc
index 9fdfa1b..f96396f 100644
--- a/testing/cc/signature_impl.cc
+++ b/testing/cc/signature_impl.cc
@@ -17,39 +17,39 @@
 // Implementation of a Signature Service
 #include "signature_impl.h"
 
+#include <memory>
 #include <string>
 #include <utility>
 
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
+#include "create.h"
 #include "proto/testing_api.grpc.pb.h"
 
 namespace tink_testing_api {
 
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
-using ::grpc::ServerContext;
-using ::grpc::Status;
+using ::crypto::tink::util::StatusOr;
+
+::grpc::Status SignatureImpl::CreatePublicKeySign(
+    grpc::ServerContext* context, const CreationRequest* request,
+    CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::PublicKeySign>(request, response);
+}
+
+::grpc::Status SignatureImpl::CreatePublicKeyVerify(
+    grpc::ServerContext* context, const CreationRequest* request,
+    CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::PublicKeyVerify>(request,
+                                                              response);
+}
 
 // Signs a message
 ::grpc::Status SignatureImpl::Sign(grpc::ServerContext* context,
                                    const SignatureSignRequest* request,
                                    SignatureSignResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->private_keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto private_handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!private_handle_result.ok()) {
-    response->set_err(std::string(private_handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto signer_result = private_handle_result.value()
-                           ->GetPrimitive<crypto::tink::PublicKeySign>();
+  StatusOr<std::unique_ptr<crypto::tink::PublicKeySign>> signer_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::PublicKeySign>(
+          request->private_annotated_keyset());
   if (!signer_result.ok()) {
     response->set_err(std::string(signer_result.status().message()));
     return ::grpc::Status::OK;
@@ -67,19 +67,9 @@
 ::grpc::Status SignatureImpl::Verify(grpc::ServerContext* context,
                                      const SignatureVerifyRequest* request,
                                      SignatureVerifyResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->public_keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto public_handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!public_handle_result.ok()) {
-    response->set_err(std::string(public_handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto verifier_result = public_handle_result.value()
-                             ->GetPrimitive<crypto::tink::PublicKeyVerify>();
+  StatusOr<std::unique_ptr<crypto::tink::PublicKeyVerify>> verifier_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::PublicKeyVerify>(
+          request->public_annotated_keyset());
   if (!verifier_result.ok()) {
     response->set_err(std::string(verifier_result.status().message()));
     return ::grpc::Status::OK;
diff --git a/testing/cc/signature_impl.h b/testing/cc/signature_impl.h
index 023c89f..7245430 100644
--- a/testing/cc/signature_impl.h
+++ b/testing/cc/signature_impl.h
@@ -28,6 +28,14 @@
 // A Signature Service
 class SignatureImpl final : public Signature::Service {
  public:
+  grpc::Status CreatePublicKeySign(grpc::ServerContext* context,
+                                   const CreationRequest* request,
+                                   CreationResponse* response) override;
+
+  grpc::Status CreatePublicKeyVerify(grpc::ServerContext* context,
+                                     const CreationRequest* request,
+                                     CreationResponse* response) override;
+
   grpc::Status Sign(grpc::ServerContext* context,
                     const SignatureSignRequest* request,
                     SignatureSignResponse* response) override;
diff --git a/testing/cc/signature_impl_test.cc b/testing/cc/signature_impl_test.cc
index e01ff2f..76d0b25 100644
--- a/testing/cc/signature_impl_test.cc
+++ b/testing/cc/signature_impl_test.cc
@@ -16,6 +16,9 @@
 
 #include "signature_impl.h"
 
+#include <memory>
+#include <ostream>
+#include <sstream>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -24,7 +27,6 @@
 #include "tink/cleartext_keyset_handle.h"
 #include "tink/signature/signature_config.h"
 #include "tink/signature/signature_key_templates.h"
-#include "proto/testing_api.grpc.pb.h"
 
 namespace crypto {
 namespace tink {
@@ -35,6 +37,8 @@
 using ::crypto::tink::SignatureKeyTemplates;
 
 using ::testing::IsEmpty;
+using ::tink_testing_api::CreationRequest;
+using ::tink_testing_api::CreationResponse;
 using ::tink_testing_api::SignatureSignRequest;
 using ::tink_testing_api::SignatureSignResponse;
 using ::tink_testing_api::SignatureVerifyRequest;
@@ -61,6 +65,68 @@
   }
 };
 
+TEST_F(SignatureImplTest, CreatePublicKeySignSuccess) {
+  tink_testing_api::SignatureImpl signature;
+  const KeyTemplate& key_template = SignatureKeyTemplates::EcdsaP256();
+  ::crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+      private_keyset_handle = KeysetHandle::GenerateNew(key_template);
+  ASSERT_TRUE(private_keyset_handle.status().ok())
+      << private_keyset_handle.status();
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(**private_keyset_handle));
+  CreationResponse response;
+
+  EXPECT_TRUE(signature.CreatePublicKeySign(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(SignatureImplTest, CreatePublicKeySignFailure) {
+  tink_testing_api::SignatureImpl signature;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("\x80");
+  CreationResponse response;
+
+  EXPECT_TRUE(signature.CreatePublicKeySign(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
+TEST_F(SignatureImplTest, CreatePublicKeyVerifySuccess) {
+  tink_testing_api::SignatureImpl signature;
+  const KeyTemplate& key_template = SignatureKeyTemplates::EcdsaP256();
+  ::crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+      private_keyset_handle = KeysetHandle::GenerateNew(key_template);
+  ASSERT_TRUE(private_keyset_handle.status().ok())
+      << private_keyset_handle.status();
+  ::crypto::tink::util::StatusOr<std::unique_ptr<KeysetHandle>>
+      public_keyset_handle = (*private_keyset_handle)->GetPublicKeysetHandle();
+  ASSERT_TRUE(public_keyset_handle.status().ok())
+      << public_keyset_handle.status();
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(**public_keyset_handle));
+  CreationResponse response;
+
+  EXPECT_TRUE(
+      signature.CreatePublicKeyVerify(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(SignatureImplTest, CreatePublicKeyVerifyFailure) {
+  tink_testing_api::SignatureImpl signature;
+
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("\x80");
+  CreationResponse response;
+
+  EXPECT_TRUE(
+      signature.CreatePublicKeyVerify(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
 TEST_F(SignatureImplTest, SignVerifySuccess) {
   tink_testing_api::SignatureImpl signature;
   const KeyTemplate& key_template = SignatureKeyTemplates::EcdsaP256();
@@ -71,7 +137,8 @@
   EXPECT_TRUE(public_handle_result.ok());
 
   SignatureSignRequest sign_request;
-  sign_request.set_private_keyset(KeysetBytes(*private_handle_result.value()));
+  sign_request.mutable_private_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(*private_handle_result.value()));
   sign_request.set_data("some data");
   SignatureSignResponse sign_response;
 
@@ -79,7 +146,8 @@
   EXPECT_THAT(sign_response.err(), IsEmpty());
 
   SignatureVerifyRequest verify_request;
-  verify_request.set_public_keyset(KeysetBytes(*public_handle_result.value()));
+  verify_request.mutable_public_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(*public_handle_result.value()));
   verify_request.set_signature(sign_response.signature());
   verify_request.set_data("some data");
   SignatureVerifyResponse verify_response;
@@ -92,7 +160,8 @@
 TEST_F(SignatureImplTest, SignBadKeysetFail) {
   tink_testing_api::SignatureImpl signature;
   SignatureSignRequest sign_request;
-  sign_request.set_private_keyset("bad private keyset");
+  sign_request.mutable_private_annotated_keyset()->set_serialized_keyset(
+      "bad private keyset");
   sign_request.set_data("some data");
   SignatureSignResponse sign_response;
 
@@ -110,7 +179,8 @@
   EXPECT_TRUE(public_handle_result.ok());
 
   SignatureVerifyRequest verify_request;
-  verify_request.set_public_keyset(KeysetBytes(*public_handle_result.value()));
+  verify_request.mutable_public_annotated_keyset()->set_serialized_keyset(
+      KeysetBytes(*public_handle_result.value()));
   verify_request.set_signature("bad signature");
   verify_request.set_data("some data");
   SignatureVerifyResponse verify_response;
diff --git a/testing/cc/streaming_aead_impl.cc b/testing/cc/streaming_aead_impl.cc
index f168698..b3552ab 100644
--- a/testing/cc/streaming_aead_impl.cc
+++ b/testing/cc/streaming_aead_impl.cc
@@ -18,48 +18,39 @@
 #include "streaming_aead_impl.h"
 
 #include <algorithm>
+#include <memory>
 #include <string>
 #include <utility>
 
 #include "absl/status/status.h"
-#include "tink/binary_keyset_reader.h"
-#include "tink/cleartext_keyset_handle.h"
 #include "tink/streaming_aead.h"
 #include "tink/util/istream_input_stream.h"
 #include "tink/util/ostream_output_stream.h"
 #include "tink/util/status.h"
+#include "create.h"
 #include "proto/testing_api.grpc.pb.h"
 
 namespace tink_testing_api {
 
-namespace tinkutil = ::crypto::tink::util;
-
-using ::crypto::tink::BinaryKeysetReader;
-using ::crypto::tink::CleartextKeysetHandle;
 using ::crypto::tink::InputStream;
 using ::crypto::tink::util::IstreamInputStream;
 using ::crypto::tink::util::OstreamOutputStream;
-using ::grpc::ServerContext;
-using ::grpc::Status;
+using ::crypto::tink::util::StatusOr;
+
+::grpc::Status StreamingAeadImpl::Create(grpc::ServerContext* context,
+                                         const CreationRequest* request,
+                                         CreationResponse* response) {
+  return CreatePrimitiveForRpc<crypto::tink::StreamingAead>(request, response);
+}
 
 // Encrypts a message
 ::grpc::Status StreamingAeadImpl::Encrypt(
     grpc::ServerContext* context,
     const StreamingAeadEncryptRequest* request,
     StreamingAeadEncryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto streaming_aead_result =
-      handle_result.value()->GetPrimitive<crypto::tink::StreamingAead>();
+  StatusOr<std::unique_ptr<crypto::tink::StreamingAead>> streaming_aead_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::StreamingAead>(
+          request->annotated_keyset());
   if (!streaming_aead_result.ok()) {
     response->set_err(std::string(streaming_aead_result.status().message()));
     return ::grpc::Status::OK;
@@ -115,19 +106,9 @@
     grpc::ServerContext* context,
     const StreamingAeadDecryptRequest* request,
     StreamingAeadDecryptResponse* response) {
-  auto reader_result = BinaryKeysetReader::New(request->keyset());
-  if (!reader_result.ok()) {
-    response->set_err(std::string(reader_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto handle_result =
-      CleartextKeysetHandle::Read(std::move(reader_result.value()));
-  if (!handle_result.ok()) {
-    response->set_err(std::string(handle_result.status().message()));
-    return ::grpc::Status::OK;
-  }
-  auto streaming_aead_result =
-      handle_result.value()->GetPrimitive<crypto::tink::StreamingAead>();
+  StatusOr<std::unique_ptr<crypto::tink::StreamingAead>> streaming_aead_result =
+      PrimitiveFromSerializedBinaryProtoKeyset<crypto::tink::StreamingAead>(
+          request->annotated_keyset());
   if (!streaming_aead_result.ok()) {
     response->set_err(std::string(streaming_aead_result.status().message()));
     return ::grpc::Status::OK;
diff --git a/testing/cc/streaming_aead_impl.h b/testing/cc/streaming_aead_impl.h
index ea72d49..cbe5bc6 100644
--- a/testing/cc/streaming_aead_impl.h
+++ b/testing/cc/streaming_aead_impl.h
@@ -14,8 +14,8 @@
 //
 ///////////////////////////////////////////////////////////////////////////////
 
-#ifndef TINK_TESTING_SERIVCES_STREAMING_AEAD_IMPL_H_
-#define TINK_TESTING_SERIVCES_STREAMING_AEAD_IMPL_H_
+#ifndef TINK_TESTING_STREAMING_AEAD_IMPL_H_
+#define TINK_TESTING_STREAMING_AEAD_IMPL_H_
 
 #include <grpcpp/grpcpp.h>
 #include <grpcpp/server_context.h>
@@ -28,6 +28,10 @@
 // A StreamingAead Service.
 class StreamingAeadImpl final : public StreamingAead::Service {
  public:
+  grpc::Status Create(grpc::ServerContext* context,
+                      const CreationRequest* request,
+                      CreationResponse* response) override;
+
   grpc::Status Encrypt(grpc::ServerContext* context,
                        const StreamingAeadEncryptRequest* request,
                        StreamingAeadEncryptResponse* response) override;
@@ -39,4 +43,4 @@
 
 }  // namespace tink_testing_api
 
-#endif  // TINK_TESTING_SERIVCES_STREAMING_AEAD_IMPL_H_
+#endif  // TINK_TESTING_STREAMING_AEAD_IMPL_H_
diff --git a/testing/cc/streaming_aead_impl_test.cc b/testing/cc/streaming_aead_impl_test.cc
index c2ee5a1..2e1aa08 100644
--- a/testing/cc/streaming_aead_impl_test.cc
+++ b/testing/cc/streaming_aead_impl_test.cc
@@ -16,6 +16,8 @@
 
 #include "streaming_aead_impl.h"
 
+#include <ostream>
+#include <sstream>
 #include <string>
 
 #include "gmock/gmock.h"
@@ -36,10 +38,12 @@
 
 using ::testing::Eq;
 using ::testing::IsEmpty;
+using ::tink_testing_api::CreationRequest;
+using ::tink_testing_api::CreationResponse;
 using ::tink_testing_api::StreamingAeadDecryptRequest;
+using ::tink_testing_api::StreamingAeadDecryptResponse;
 using ::tink_testing_api::StreamingAeadEncryptRequest;
 using ::tink_testing_api::StreamingAeadEncryptResponse;
-using ::tink_testing_api::StreamingAeadDecryptResponse;
 
 using crypto::tink::KeysetHandle;
 using google::crypto::tink::KeyTemplate;
@@ -67,11 +71,33 @@
   }
 };
 
+TEST_F(StreamingAeadImplTest, CreateSuccess) {
+  tink_testing_api::StreamingAeadImpl streaming_aead;
+  std::string keyset = ValidKeyset();
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
+  CreationResponse response;
+
+  EXPECT_TRUE(streaming_aead.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), IsEmpty());
+}
+
+TEST_F(StreamingAeadImplTest, CreateFails) {
+  tink_testing_api::StreamingAeadImpl streaming_aead;
+  CreationRequest request;
+  request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
+  CreationResponse response;
+
+  EXPECT_TRUE(streaming_aead.Create(nullptr, &request, &response).ok());
+  EXPECT_THAT(response.err(), Not(IsEmpty()));
+}
+
+
 TEST_F(StreamingAeadImplTest, EncryptDecryptSuccess) {
   tink_testing_api::StreamingAeadImpl streaming_aead;
   std::string keyset = ValidKeyset();
   StreamingAeadEncryptRequest enc_request;
-  enc_request.set_keyset(keyset);
+  enc_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   enc_request.set_plaintext("Plain text");
   enc_request.set_associated_data("ad");
   StreamingAeadEncryptResponse enc_response;
@@ -81,7 +107,7 @@
   EXPECT_THAT(enc_response.err(), IsEmpty());
 
   StreamingAeadDecryptRequest dec_request;
-  dec_request.set_keyset(keyset);
+  dec_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   dec_request.set_ciphertext(enc_response.ciphertext());
   dec_request.set_associated_data("ad");
   StreamingAeadDecryptResponse dec_response;
@@ -95,7 +121,7 @@
 TEST_F(StreamingAeadImplTest, EncryptBadKeysetFail) {
   tink_testing_api::StreamingAeadImpl streaming_aead;
   StreamingAeadEncryptRequest enc_request;
-  enc_request.set_keyset("bad keyset");
+  enc_request.mutable_annotated_keyset()->set_serialized_keyset("bad keyset");
   enc_request.set_plaintext("Plain text");
   enc_request.set_associated_data("ad");
   StreamingAeadEncryptResponse enc_response;
@@ -109,7 +135,7 @@
   tink_testing_api::StreamingAeadImpl streaming_aead;
   std::string keyset = ValidKeyset();
   StreamingAeadDecryptRequest dec_request;
-  dec_request.set_keyset(keyset);
+  dec_request.mutable_annotated_keyset()->set_serialized_keyset(keyset);
   dec_request.set_ciphertext("bad ciphertext");
   dec_request.set_associated_data("ad");
   StreamingAeadDecryptResponse dec_response;
diff --git a/testing/cc/testing_server.cc b/testing/cc/testing_server.cc
index e2c8964..8943fb6 100644
--- a/testing/cc/testing_server.cc
+++ b/testing/cc/testing_server.cc
@@ -16,15 +16,24 @@
 
 #include <grpcpp/grpcpp.h>
 
+#include <iostream>
+#include <memory>
+#include <ostream>
 #include <string>
 
 #include "absl/flags/flag.h"
 #include "absl/flags/parse.h"
+#include "absl/strings/str_cat.h"
 #include "tink/config/tink_config.h"
 #include "tink/hybrid/hpke_config.h"
+#ifdef TINK_CROSS_LANG_TESTS_AWSKMS
+#include "tink/integration/awskms/aws_kms_client.h"
+#endif  // TINK_CROSS_LANG_TESTS_AWSKMS
+#include "tink/integration/gcpkms/gcp_kms_client.h"
 #include "tink/jwt/jwt_mac_config.h"
 #include "tink/jwt/jwt_signature_config.h"
 #include "tink/util/fake_kms_client.h"
+#include "tink/util/status.h"
 #include "aead_impl.h"
 #include "deterministic_aead_impl.h"
 #include "hybrid_impl.h"
@@ -38,6 +47,19 @@
 #include "proto/testing_api.grpc.pb.h"
 
 ABSL_FLAG(int, port, 23456, "the port");
+ABSL_FLAG(std::string, gcp_credentials_path, "",
+          "Google Cloud KMS credentials path");
+ABSL_FLAG(
+    std::string, gcp_key_uri, "",
+    absl::StrCat("Google Cloud KMS key URL of the form: ",
+                 "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*."));
+ABSL_FLAG(std::string, aws_credentials_path, "", "AWS KMS credentials path");
+ABSL_FLAG(
+    std::string, aws_key_uri, "",
+    absl::StrCat("AWS KMS key URL of the form: ",
+                 "aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>."));
+
+namespace tink_testing_api {
 
 void RunServer() {
   auto status = crypto::tink::TinkConfig::Register();
@@ -71,20 +93,46 @@
               << register_fake_kms_client_status.message() << std::endl;
     return;
   }
+  std::string gcp_credentials_path = absl::GetFlag(FLAGS_gcp_credentials_path);
+  std::string gcp_key_uri = absl::GetFlag(FLAGS_gcp_key_uri);
+  crypto::tink::util::Status register_gcpkms_client_status =
+      crypto::tink::integration::gcpkms::GcpKmsClient::RegisterNewClient(
+          gcp_key_uri, gcp_credentials_path);
+  if (!register_gcpkms_client_status.ok()) {
+    std::cerr << "GcpKmsClient::RegisterNewClient(\"\", \""
+              << gcp_credentials_path
+              << "\") failed: " << register_gcpkms_client_status.message()
+              << std::endl;
+    return;
+  }
+#ifdef TINK_CROSS_LANG_TESTS_AWSKMS
+  std::string aws_credentials_path = absl::GetFlag(FLAGS_aws_credentials_path);
+  std::string aws_key_uri = absl::GetFlag(FLAGS_aws_key_uri);
+  crypto::tink::util::Status register_awskms_client_status =
+      crypto::tink::integration::awskms::AwsKmsClient::RegisterNewClient(
+          aws_key_uri, aws_credentials_path);
+  if (!register_awskms_client_status.ok()) {
+    std::cerr << "AwsKmsClient::RegisterNewClient(\"\", \""
+              << aws_credentials_path
+              << "\") failed: " << register_awskms_client_status.message()
+              << std::endl;
+    return;
+  }
+#endif  // TINK_CROSS_LANG_TESTS_AWSKMS
 
   const int port = absl::GetFlag(FLAGS_port);
   std::string server_address = absl::StrCat("[::]:", port);
 
-  tink_testing_api::MetadataImpl metadata;
-  tink_testing_api::KeysetImpl keyset;
-  tink_testing_api::AeadImpl aead;
-  tink_testing_api::DeterministicAeadImpl deterministic_aead;
-  tink_testing_api::HybridImpl hybrid;
-  tink_testing_api::MacImpl mac;
-  tink_testing_api::SignatureImpl signature;
-  tink_testing_api::StreamingAeadImpl streaming_aead;
-  tink_testing_api::PrfSetImpl prf_set;
-  tink_testing_api::JwtImpl jwt;
+  MetadataImpl metadata;
+  KeysetImpl keyset;
+  AeadImpl aead;
+  DeterministicAeadImpl deterministic_aead;
+  HybridImpl hybrid;
+  MacImpl mac;
+  SignatureImpl signature;
+  StreamingAeadImpl streaming_aead;
+  PrfSetImpl prf_set;
+  JwtImpl jwt;
 
   grpc::ServerBuilder builder;
   builder.AddListeningPort(
@@ -106,8 +154,10 @@
   server->Wait();
 }
 
+}  // namespace tink_testing_api
+
 int main(int argc, char** argv) {
   absl::ParseCommandLine(argc, argv);
-  RunServer();
+  tink_testing_api::RunServer();
   return 0;
 }
diff --git a/testing/cross_language/.bazelrc b/testing/cross_language/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/testing/cross_language/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/testing/cross_language/.bazelversion b/testing/cross_language/.bazelversion
index ac14c3d..09b254e 100644
--- a/testing/cross_language/.bazelversion
+++ b/testing/cross_language/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/testing/cross_language/BUILD.bazel b/testing/cross_language/BUILD.bazel
index 6f09598..9a68829 100644
--- a/testing/cross_language/BUILD.bazel
+++ b/testing/cross_language/BUILD.bazel
@@ -11,8 +11,9 @@
     name = "key_generation_consistency_test",
     srcs = ["key_generation_consistency_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/aead",
@@ -22,8 +23,8 @@
         "@tink_py//tink/prf",
         "@tink_py//tink/proto:common_py_pb2",
         "@tink_py//tink/proto:ecdsa_py_pb2",
-        "@tink_py//tink/proto:tink_py_pb2",
         "@tink_py//tink/proto:jwt_hmac_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
         "@tink_py//tink/signature",
     ],
 )
@@ -32,8 +33,9 @@
     name = "key_version_test",
     srcs = ["key_version_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/aead",
@@ -62,12 +64,19 @@
     name = "keyset_validation_test",
     srcs = ["keyset_validation_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/daead",
+        "@tink_py//tink/hybrid",
         "@tink_py//tink/jwt",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/prf",
         "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/signature",
     ],
 )
 
@@ -75,8 +84,9 @@
     name = "aead_test",
     srcs = ["aead_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         requirement("protobuf"),
         "@tink_py//tink:tink_python",
@@ -87,11 +97,27 @@
 )
 
 py_test(
+    name = "kms_aead_test",
+    srcs = ["kms_aead_test.py"],
+    tags = ["manual"],
+    deps = [
+        "//tink_config",
+        "//util:testing_servers",
+        "//util:utilities",
+        requirement("absl-py"),
+        requirement("protobuf"),
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+    ],
+)
+
+py_test(
     name = "aead_consistency_test",
     srcs = ["aead_consistency_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/aead",
@@ -107,8 +133,9 @@
     name = "deterministic_aead_test",
     srcs = ["deterministic_aead_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/daead",
@@ -121,8 +148,9 @@
     name = "streaming_aead_test",
     srcs = ["streaming_aead_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         "@tink_py//tink/testing:keyset_builder",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
@@ -134,8 +162,9 @@
     name = "mac_test",
     srcs = ["mac_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/mac",
@@ -148,8 +177,9 @@
     name = "signature_test",
     srcs = ["signature_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/proto:tink_py_pb2",
@@ -162,8 +192,9 @@
     name = "hybrid_encryption_test",
     srcs = ["hybrid_encryption_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/aead",
@@ -179,8 +210,9 @@
     name = "prf_set_test",
     srcs = ["prf_set_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/prf",
@@ -192,8 +224,9 @@
     name = "jwt_test",
     srcs = ["jwt_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/jwt",
@@ -204,7 +237,7 @@
     name = "jwt_validation_test",
     srcs = ["jwt_validation_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
         requirement("absl-py"),
         "@tink_py//tink:cleartext_keyset_handle",
@@ -222,8 +255,9 @@
     name = "jwt_kid_test",
     srcs = ["jwt_kid_test.py"],
     deps = [
-        "//util:supported_key_types",
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
         requirement("absl-py"),
         "@tink_py//tink:tink_python",
         "@tink_py//tink/jwt",
@@ -240,9 +274,10 @@
     name = "key_template_consistency_test",
     srcs = ["key_template_consistency_test.py"],
     deps = [
+        "//util:utilities",
         requirement("absl-py"),
+        "//tink_config",
         "//util:key_util",
-        "//util:supported_key_types",
         "//util:testing_servers",
         "@tink_py//tink:tink_python",
     ],
@@ -252,13 +287,28 @@
     name = "keyset_read_write_test",
     srcs = ["keyset_read_write_test.py"],
     deps = [
+        "//util:utilities",
         requirement("absl-py"),
+        "//tink_config",
+        "//util:key_util",
+        "//util:testing_servers",
         "@com_google_protobuf//:protobuf_python",
         "@tink_py//tink:tink_python",
         "@tink_py//tink/aead",
         "@tink_py//tink/proto:tink_py_pb2",
-        "//util:key_util",
-        "//util:supported_key_types",
+    ],
+)
+
+py_test(
+    name = "primitive_creation_test",
+    srcs = ["primitive_creation_test.py"],
+    deps = [
+        requirement("absl-py"),
+        "//tink_config",
         "//util:testing_servers",
+        "//util:utilities",
+        "//util/test_keys",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/proto:tink_py_pb2",
     ],
 )
diff --git a/testing/cross_language/WORKSPACE b/testing/cross_language/WORKSPACE
index ae2aa07..adc2658 100644
--- a/testing/cross_language/WORKSPACE
+++ b/testing/cross_language/WORKSPACE
@@ -1,4 +1,4 @@
-workspace(name = "testing_python")
+workspace(name = "cross_language_test")
 
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
@@ -67,10 +67,10 @@
 http_archive(
     name = "org_python_pypi_portpicker",
     urls = [
-        "https://pypi.python.org/packages/d9/f4/0188bc07d38b5f9dd192b8329c5e098e3b23552c01a96fd08973dba9e315/portpicker-1.3.1.tar.gz",
+        "https://files.pythonhosted.org/packages/3b/34/bfbd5236c7726452080a92a9f6ea9770cd65f51b05cef319ccb767ed32bf/portpicker-1.5.2.tar.gz",
     ],
-    sha256 = "d2cdc776873635ed421315c4d22e63280042456bbfa07397817e687b142b9667",
-    strip_prefix = "portpicker-1.3.1/src",
+    sha256 = "c55683ad725f5c00a41bc7db0225223e8be024b1fa564d039ed3390e4fd48fb3",
+    strip_prefix = "portpicker-1.5.2/src",
     build_file = "portpicker.BUILD.bazel",
 )
 
diff --git a/testing/cross_language/aead_consistency_test.py b/testing/cross_language/aead_consistency_test.py
index 47ee586..4ecff92 100644
--- a/testing/cross_language/aead_consistency_test.py
+++ b/testing/cross_language/aead_consistency_test.py
@@ -19,35 +19,30 @@
 """
 
 import itertools
+from typing import List
+from typing import Tuple
 
 from absl.testing import absltest
 from absl.testing import parameterized
-
 import tink
-from tink import aead
 
 from tink.proto import aes_ctr_hmac_aead_pb2
 from tink.proto import aes_eax_pb2
 from tink.proto import aes_gcm_pb2
 from tink.proto import common_pb2
 from tink.proto import tink_pb2
-from util import supported_key_types
+import tink_config
 from util import testing_servers
+from util import utilities
 
 HASH_TYPES = [
     common_pb2.UNKNOWN_HASH, common_pb2.SHA1, common_pb2.SHA224,
     common_pb2.SHA256, common_pb2.SHA384, common_pb2.SHA512
 ]
 
-# Test cases that succeed in a language but should fail
-SUCCEEDS_BUT_SHOULD_FAIL = []
-
-# Test cases that fail in a language but should succeed
-FAILS_BUT_SHOULD_SUCCEED = []
-
 
 def setUpModule():
-  aead.register()
+  tink.aead.register()
   testing_servers.start('aead_consistency')
 
 
@@ -128,20 +123,19 @@
     key.hmac_key.key_value = _gen_key_value(hmac_key_size)
     keyset = _gen_keyset(
         'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey',
-        key.SerializeToString(),
-        tink_pb2.KeyData.SYMMETRIC)
+        key.SerializeToString(), tink_pb2.KeyData.SYMMETRIC)
     return ('AesCtrHmacAeadKey(%d,%d,%d,%d,%s,%d,%d,%d)' %
             (aes_key_size, iv_size, hmac_key_size, hmac_tag_size,
-             common_pb2.HashType.Name(hash_type),
-             key_version, aes_ctr_version, hmac_version), keyset)
+             common_pb2.HashType.Name(hash_type), key_version, aes_ctr_version,
+             hmac_version), keyset)
+
   yield _test_case()
   for aes_key_size in [15, 16, 24, 32, 64, 96]:
     for iv_size in [11, 12, 16, 17, 24, 32]:
       yield _test_case(aes_key_size=aes_key_size, iv_size=iv_size)
   for hmac_key_size in [15, 16, 24, 32, 64, 96]:
     for hmac_tag_size in [9, 10, 16, 20, 21, 24, 32, 33, 64, 65]:
-      yield _test_case(hmac_key_size=hmac_key_size,
-                       hmac_tag_size=hmac_tag_size)
+      yield _test_case(hmac_key_size=hmac_key_size, hmac_tag_size=hmac_tag_size)
   for hash_type in HASH_TYPES:
     yield _test_case(hash_type=hash_type)
   yield _test_case(key_version=1)
@@ -157,51 +151,79 @@
   that is done for each key independently.
   """
 
+  def _create_aeads_ignore_errors(
+      self, keyset: tink_pb2.Keyset) -> List[Tuple[tink.aead.Aead, str]]:
+    """Creates AEADs for the given keyset in each language.
+
+    Args:
+      keyset: A keyset as 'keyset' proto.
+
+    Returns:
+      A list of pairs (aead, language)
+    """
+
+    result = []
+    for lang in utilities.ALL_LANGUAGES:
+      try:
+        aead = testing_servers.remote_primitive(lang,
+                                                keyset.SerializeToString(),
+                                                tink.aead.Aead)
+        result.append((aead, lang))
+      except tink.TinkError:
+        pass
+    return result
+
   @parameterized.parameters(
-      itertools.chain(aes_eax_key_test_cases(),
-                      aes_gcm_key_test_cases(),
+      itertools.chain(aes_eax_key_test_cases(), aes_gcm_key_test_cases(),
                       aes_ctr_hmac_aead_key_test_cases()))
-  def test_keyset_validation_consistency(self, name, keyset):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES[
-        supported_key_types.KEY_TYPE_FROM_URL[keyset.key[0].key_data.type_url]]
-    supported_aeads = [
-        testing_servers.aead(lang, keyset.SerializeToString())
-        for lang in supported_langs
-    ]
+  def test_aead_creation_supported_languages_consistent(self, name, keyset):
+    """Tests that AEAD creation is consistent in all supporeted languages."""
+    supported_langs = tink_config.supported_languages_for_key_type(
+        tink_config.key_type_from_type_url(keyset.key[0].key_data.type_url))
+
+    langs = [lang for _, lang in self._create_aeads_ignore_errors(keyset)]
+
+    if langs:
+      with self.subTest('When creating AEAD objects for %s' % name):
+        self.assertEqual(langs, supported_langs)
+
+  @parameterized.parameters(
+      itertools.chain(aes_eax_key_test_cases(), aes_gcm_key_test_cases(),
+                      aes_ctr_hmac_aead_key_test_cases()))
+  def test_aead_creation_non_supported_languages_fail(self, name, keyset):
+    """Tests that AEAD creation fails in all unsupported languages."""
+    supported_langs = tink_config.supported_languages_for_key_type(
+        tink_config.key_type_from_type_url(keyset.key[0].key_data.type_url))
+
+    langs = [lang for _, lang in self._create_aeads_ignore_errors(keyset)]
+
+    for lang in utilities.ALL_LANGUAGES:
+      if lang not in supported_langs:
+        self.assertNotIn(
+            lang, langs,
+            'AEAD-Creation should fail in language %s for %s' % (lang, name))
+
+  @parameterized.parameters(
+      itertools.chain(aes_eax_key_test_cases(), aes_gcm_key_test_cases(),
+                      aes_ctr_hmac_aead_key_test_cases()))
+  def test_aead_pairwise_consistency(self, name, keyset):
+    """Tests that created AEADS behave consistently."""
+
+    aead_and_lang = self._create_aeads_ignore_errors(keyset)
     plaintext = b'plaintext'
     associated_data = b'associated_data'
-    failures = 0
-    ciphertexts = {}
-    results = {}
-    for p in supported_aeads:
-      try:
-        ciphertexts[p.lang] = p.encrypt(plaintext, associated_data)
-        if (name, p.lang) in SUCCEEDS_BUT_SHOULD_FAIL:
-          failures += 1
-          del ciphertexts[p.lang]
-        if (name, p.lang) in FAILS_BUT_SHOULD_SUCCEED:
-          self.fail('(%s, %s) succeeded, but is in FAILS_BUT_SHOULD_SUCCEED' %
-                    (name, p.lang))
-        results[p.lang] = 'success'
-      except tink.TinkError as e:
-        if (name, p.lang) not in FAILS_BUT_SHOULD_SUCCEED:
-          failures += 1
-        if (name, p.lang) in SUCCEEDS_BUT_SHOULD_FAIL:
-          self.fail(
-              '(%s, %s) is in SUCCEEDS_BUT_SHOULD_FAIL, but failed with %s' %
-              (name, p.lang, e))
-        results[p.lang] = e
-    # Test that either all supported langs accept the key, or all reject.
-    if failures not in [0, len(supported_langs)]:
-      self.fail('encryption for key %s is inconsistent: %s' %
-                (name, results))
-    # Test all generated ciphertexts can be decypted.
-    for enc_lang, ciphertext in ciphertexts.items():
-      dec_aead = supported_aeads[0]
-      output = dec_aead.decrypt(ciphertext, associated_data)
-      if output != plaintext:
-        self.fail('ciphertext encrypted with key %s in lang %s could not be'
-                  'decrypted in lang %s.' % (name, enc_lang, dec_aead.lang))
+
+    for aead, lang in aead_and_lang:
+      aead0 = aead_and_lang[0][0]
+      lang0 = aead_and_lang[0][1]
+
+      with self.subTest('Comparing %s-aead to %s-aead for %s ' %
+                        (lang0, lang, name)):
+        ciphertext = aead.encrypt(plaintext, associated_data)
+        self.assertEqual(aead0.decrypt(ciphertext, associated_data), plaintext)
+
+        ciphertext = aead0.encrypt(plaintext, associated_data)
+        self.assertEqual(aead.decrypt(ciphertext, associated_data), plaintext)
 
 
 if __name__ == '__main__':
diff --git a/testing/cross_language/aead_test.py b/testing/cross_language/aead_test.py
index 8fbc9e4..b3a71d4 100644
--- a/testing/cross_language/aead_test.py
+++ b/testing/cross_language/aead_test.py
@@ -17,18 +17,18 @@
 interoperate with each other.
 """
 
-from typing import Iterable, Tuple
+from typing import Iterable, List, Tuple
 
 from absl.testing import absltest
 from absl.testing import parameterized
-
 import tink
 from tink import aead
 
 from tink.proto import tink_pb2
 from tink.testing import keyset_builder
-from util import supported_key_types
+import tink_config
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['aead']
 
@@ -49,75 +49,175 @@
     'LnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE')
 
 
-# maps from key_template_name to (key_template, key_type)
+# maps from key_template_name to (key_template, supported_langs).  Contains all
+# templates we want to test which do not have a name in Tinkey.
 _ADDITIONAL_KEY_TEMPLATES = {
-    'FAKE_KMS_AEAD': (
+    '_FAKE_KMS_AEAD': (
         aead.aead_key_templates.create_kms_aead_key_template(_FAKE_KMS_KEY_URI),
-        'KmsAeadKey'),
-    'FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_GCM':
-        (aead.aead_key_templates.create_kms_envelope_aead_key_template(
-            _FAKE_KMS_KEY_URI,
-            aead.aead_key_templates.AES128_GCM), 'KmsEnvelopeAeadKey')
+        tink_config.supported_languages_for_key_type('KmsAeadKey'),
+    ),
+    # Since the key type KmsEnvelopeAeadKey is supported by all languages, the
+    # following templates should be supported if the DEK Template is supported
+    # by the language.
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_GCM': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_GCM
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES128_GCM'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_GCM': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_GCM
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES256_GCM'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_EAX': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_EAX
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES128_EAX'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_EAX': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_EAX
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES256_EAX'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_GCM_SIV': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_GCM_SIV
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES128_GCM_SIV'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_GCM_SIV': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_GCM_SIV
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['AES256_GCM_SIV'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES128_CTR_HMAC_SHA256': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES128_CTR_HMAC_SHA256
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+            'AES128_CTR_HMAC_SHA256'
+        ],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_AES256_CTR_HMAC_SHA256': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.AES256_CTR_HMAC_SHA256
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+            'AES256_CTR_HMAC_SHA256'
+        ],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_CHACHA20_POLY1305': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, utilities.KEY_TEMPLATE['CHACHA20_POLY1305']
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['CHACHA20_POLY1305'],
+    ),
+    '_FAKE_KMS_ENVELOPE_AEAD_WITH_XCHACHA20_POLY1305': (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            _FAKE_KMS_KEY_URI, aead.aead_key_templates.XCHACHA20_POLY1305
+        ),
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME['XCHACHA20_POLY1305'],
+    ),
 }
 
 
-def all_aead_key_template_names() -> Iterable[str]:
-  """Yields all AEAD key template names."""
-  for key_type in supported_key_types.AEAD_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-  for key_template_name in _ADDITIONAL_KEY_TEMPLATES:
-    yield key_template_name
-
-
 class AeadPythonTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_aead_key_template_names())
-  def test_encrypt_decrypt(self, key_template_name):
+  def _create_aeads_ignore_errors(self,
+                                  keyset: bytes) -> List[Tuple[aead.Aead, str]]:
+    """Creates AEADs for the given keyset in each language.
+
+    Args:
+      keyset: A keyset as a serialized 'keyset' proto.
+
+    Returns:
+      A list of pairs (aead, language)
+    """
+
+    result = []
+    for lang in utilities.ALL_LANGUAGES:
+      try:
+        aead_p = testing_servers.remote_primitive(lang, keyset, aead.Aead)
+        result.append((aead_p, lang))
+      except tink.TinkError:
+        pass
+    return result
+
+  def _langs_from_key_template_name(self, key_template_name: str) -> List[str]:
     if key_template_name in _ADDITIONAL_KEY_TEMPLATES:
-      key_template, key_type = _ADDITIONAL_KEY_TEMPLATES[
-          key_template_name]
-      supported_langs = supported_key_types.SUPPORTED_LANGUAGES[key_type]
+      _, supported_langs = _ADDITIONAL_KEY_TEMPLATES[key_template_name]
+      return supported_langs
     else:
-      key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-      supported_langs = (
-          supported_key_types
-          .SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name])
-    self.assertNotEmpty(supported_langs)
+      return utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name]
+
+  def _as_proto_template(self, key_template_name: str) -> tink_pb2.KeyTemplate:
+    if key_template_name in _ADDITIONAL_KEY_TEMPLATES:
+      key_template, _ = _ADDITIONAL_KEY_TEMPLATES[key_template_name]
+      return key_template
+    else:
+      return utilities.KEY_TEMPLATE[key_template_name]
+
+  @parameterized.parameters([
+      *utilities.tinkey_template_names_for(aead.Aead),
+      *_ADDITIONAL_KEY_TEMPLATES.keys()
+  ])
+  def test_encrypt_decrypt(self, key_template_name):
+    langs = self._langs_from_key_template_name(key_template_name)
+    self.assertNotEmpty(langs)
+    proto_template = self._as_proto_template(key_template_name)
     # Take the first supported language to generate the keyset.
-    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
-    supported_aeads = [
-        testing_servers.aead(lang, keyset) for lang in supported_langs
-    ]
-    unsupported_aeads = [
-        testing_servers.aead(lang, keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
-    for p in supported_aeads:
+    keyset = testing_servers.new_keyset(langs[0], proto_template)
+
+    supported_aeads = self._create_aeads_ignore_errors(keyset)
+    self.assertEqual(set([lang for (_, lang) in supported_aeads]), set(langs))
+    for (p, lang) in supported_aeads:
       plaintext = (
           b'This is some plaintext message to be encrypted using key_template '
-          b'%s using %s for encryption.'
-          % (key_template_name.encode('utf8'), p.lang.encode('utf8')))
+          b'%s using %s for encryption.' %
+          (key_template_name.encode('utf8'), lang.encode('utf8')))
       associated_data = (
           b'Some associated data for %s using %s for encryption.' %
-          (key_template_name.encode('utf8'), p.lang.encode('utf8')))
+          (key_template_name.encode('utf8'), lang.encode('utf8')))
       ciphertext = p.encrypt(plaintext, associated_data)
-      for p2 in supported_aeads:
+      for (p2, lang2) in supported_aeads:
         output = p2.decrypt(ciphertext, associated_data)
-        self.assertEqual(output, plaintext)
-      for p2 in unsupported_aeads:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='Language %s supports AEAD decrypt with %s unexpectedly' %
-            (p2.lang, key_template_name)):
-          p2.decrypt(ciphertext, associated_data)
-    for p in unsupported_aeads:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports AEAD encrypt with %s unexpectedly' % (
-              p.lang, key_template_name)):
-        p.encrypt(b'plaintext', b'associated_data')
+        self.assertEqual(
+            output, plaintext,
+            'While encrypting in %s an decrypting in %s' % (lang, lang2))
+
+  @parameterized.parameters(
+      tink_config.supported_languages_for_key_type('KmsEnvelopeAeadKey')
+  )
+  def test_envelope_encryption_rejects_envelope_templates_as_dek(self, lang):
+    dek_template = (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            kek_uri=_FAKE_KMS_KEY_URI,
+            dek_template=aead.aead_key_templates.AES128_GCM,
+        )
+    )
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri=_FAKE_KMS_KEY_URI, dek_template=dek_template
+    )
+    with self.assertRaises(tink.TinkError):
+      _ = testing_servers.new_keyset(lang, template)
+
+  @parameterized.parameters(
+      tink_config.supported_languages_for_key_type('KmsAeadKey')
+  )
+  def test_envelope_encryption_rejects_kms_templates_as_dek(self, lang):
+    dek_template = aead.aead_key_templates.create_kms_aead_key_template(
+        key_uri=_FAKE_KMS_KEY_URI
+    )
+    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
+        kek_uri=_FAKE_KMS_KEY_URI, dek_template=dek_template
+    )
+    with self.assertRaises(tink.TinkError):
+      _ = testing_servers.new_keyset(lang, template)
 
 
 # If the implementations work fine for keysets with single keys, then key
@@ -150,19 +250,27 @@
     builder = keyset_builder.new_keyset_builder()
     older_key_id = builder.add_new_key(old_key_tmpl)
     builder.set_primary_key(older_key_id)
-    enc_aead1 = testing_servers.aead(enc_lang, builder.keyset())
-    dec_aead1 = testing_servers.aead(dec_lang, builder.keyset())
+    enc_aead1 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                 aead.Aead)
+    dec_aead1 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                 aead.Aead)
     newer_key_id = builder.add_new_key(new_key_tmpl)
-    enc_aead2 = testing_servers.aead(enc_lang, builder.keyset())
-    dec_aead2 = testing_servers.aead(dec_lang, builder.keyset())
+    enc_aead2 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                 aead.Aead)
+    dec_aead2 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                 aead.Aead)
 
     builder.set_primary_key(newer_key_id)
-    enc_aead3 = testing_servers.aead(enc_lang, builder.keyset())
-    dec_aead3 = testing_servers.aead(dec_lang, builder.keyset())
+    enc_aead3 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                 aead.Aead)
+    dec_aead3 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                 aead.Aead)
 
     builder.disable_key(older_key_id)
-    enc_aead4 = testing_servers.aead(enc_lang, builder.keyset())
-    dec_aead4 = testing_servers.aead(dec_lang, builder.keyset())
+    enc_aead4 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                 aead.Aead)
+    dec_aead4 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                 aead.Aead)
 
     self.assertNotEqual(older_key_id, newer_key_id)
     # 1 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
diff --git a/testing/cross_language/deterministic_aead_test.py b/testing/cross_language/deterministic_aead_test.py
index 8e5f6df..178f52c 100644
--- a/testing/cross_language/deterministic_aead_test.py
+++ b/testing/cross_language/deterministic_aead_test.py
@@ -13,16 +13,14 @@
 # limitations under the License.
 """Cross-language tests for the DeterministicAead primitive."""
 
-from typing import Iterable
-
 from absl.testing import absltest
 from absl.testing import parameterized
 
 import tink
 from tink import daead
 from tink.testing import keyset_builder
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['daead']
 
@@ -36,33 +34,22 @@
   testing_servers.stop()
 
 
-def all_deterministic_aead_key_template_names() -> Iterable[str]:
-  """Yields all Deterministic AEAD key template names."""
-  for key_type in supported_key_types.DAEAD_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-
-
 class DeterministicAeadTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_deterministic_aead_key_template_names())
+  @parameterized.parameters(
+      utilities.tinkey_template_names_for(daead.DeterministicAead))
   def test_encrypt_decrypt(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     self.assertNotEmpty(supported_langs)
-    key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
     # Take the first supported language to generate the keyset.
     keyset = testing_servers.new_keyset(supported_langs[0], key_template)
     supported_daeads = [
-        testing_servers.deterministic_aead(lang, keyset)
+        testing_servers.remote_primitive(lang, keyset, daead.DeterministicAead)
         for lang in supported_langs
     ]
     self.assertNotEmpty(supported_daeads)
-    unsupported_daeads = [
-        testing_servers.deterministic_aead(lang, keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
     plaintext = (
         b'This is some plaintext message to be encrypted using '
         b'key_template %s.' % key_template_name.encode('utf8'))
@@ -79,18 +66,34 @@
     for p2 in supported_daeads:
       output = p2.decrypt_deterministically(ciphertext, associated_data)
       self.assertEqual(output, plaintext)
-    for p2 in unsupported_daeads:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports decrypt_deterministically with %s '
-          'unexpectedly' % (p2.lang, key_template_name)):
-        p2.decrypt_deterministically(ciphertext, associated_data)
-    for p in unsupported_daeads:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports encrypt_deterministically with %s '
-          'unexpectedly' % (p.lang, key_template_name)):
-        p.encrypt_deterministically(b'plaintext', b'associated_data')
+
+  @parameterized.parameters(
+      utilities.tinkey_template_names_for(daead.DeterministicAead))
+  def test_encrypt_decrypt_without_associated_data(self, key_template_name):
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+        key_template_name]
+    self.assertNotEmpty(supported_langs)
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
+    # Take the first supported language to generate the keyset.
+    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
+    supported_daeads = [
+        testing_servers.remote_primitive(lang, keyset, daead.DeterministicAead)
+        for lang in supported_langs
+    ]
+    self.assertNotEmpty(supported_daeads)
+    plaintext = b'plaintext'
+    associated_data = b''
+    ciphertext = None
+    for p in supported_daeads:
+      if ciphertext:
+        self.assertEqual(
+            ciphertext,
+            p.encrypt_deterministically(plaintext, associated_data))
+      else:
+        ciphertext = p.encrypt_deterministically(plaintext, associated_data)
+    for p2 in supported_daeads:
+      output = p2.decrypt_deterministically(ciphertext, associated_data)
+      self.assertEqual(output, plaintext)
 
 
 # If the implementations work fine for keysets with single keys, then key
@@ -123,19 +126,27 @@
     builder = keyset_builder.new_keyset_builder()
     older_key_id = builder.add_new_key(old_key_tmpl)
     builder.set_primary_key(older_key_id)
-    enc_daead1 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
-    dec_daead1 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+    enc_daead1 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
+    dec_daead1 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
     newer_key_id = builder.add_new_key(new_key_tmpl)
-    enc_daead2 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
-    dec_daead2 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+    enc_daead2 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
+    dec_daead2 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
 
     builder.set_primary_key(newer_key_id)
-    enc_daead3 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
-    dec_daead3 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+    enc_daead3 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
+    dec_daead3 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
 
     builder.disable_key(older_key_id)
-    enc_daead4 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
-    dec_daead4 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+    enc_daead4 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
+    dec_daead4 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                                  daead.DeterministicAead)
 
     self.assertNotEqual(older_key_id, newer_key_id)
     # 1 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
diff --git a/testing/cross_language/external/portpicker.BUILD.bazel b/testing/cross_language/external/portpicker.BUILD.bazel
index 57e2ec3..ead74b9 100644
--- a/testing/cross_language/external/portpicker.BUILD.bazel
+++ b/testing/cross_language/external/portpicker.BUILD.bazel
@@ -8,6 +8,6 @@
 py_library(
     name = "portpicker",
     srcs = glob(["*.py"]),
-    srcs_version = "PY2AND3",
+    srcs_version = "PY3",
     visibility = ["//visibility:public"],
 )
diff --git a/testing/cross_language/hybrid_encryption_test.py b/testing/cross_language/hybrid_encryption_test.py
index 29ea526..9d4526f 100644
--- a/testing/cross_language/hybrid_encryption_test.py
+++ b/testing/cross_language/hybrid_encryption_test.py
@@ -26,8 +26,8 @@
 from tink.proto import common_pb2
 from tink.proto import tink_pb2
 from tink.testing import keyset_builder
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['hybrid']
 
@@ -98,75 +98,47 @@
 }
 
 
-def all_hybrid_private_key_template_names() -> Iterable[str]:
-  """Yields all Hybrid Encryption private key template names."""
-  for key_type in supported_key_types.HYBRID_PRIVATE_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-  for key_template_name in _ADDITIONAL_KEY_TEMPLATES:
-    yield key_template_name
-
-
 class HybridEncryptionTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_hybrid_private_key_template_names())
+  @parameterized.parameters([
+      *utilities.tinkey_template_names_for(hybrid.HybridDecrypt),
+      *_ADDITIONAL_KEY_TEMPLATES.keys()
+  ])
   def test_encrypt_decrypt(self, key_template_name):
     if key_template_name in _ADDITIONAL_KEY_TEMPLATES:
       key_template, supported_langs = _ADDITIONAL_KEY_TEMPLATES[
           key_template_name]
     else:
-      key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+      key_template = utilities.KEY_TEMPLATE[key_template_name]
       supported_langs = (
-          supported_key_types
-          .SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name])
+          utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name])
     self.assertNotEmpty(supported_langs)
     # Take the first supported language to generate the private keyset.
     private_keyset = testing_servers.new_keyset(supported_langs[0],
                                                 key_template)
     supported_decs = [
-        testing_servers.hybrid_decrypt(lang, private_keyset)
+        testing_servers.remote_primitive(lang, private_keyset,
+                                         hybrid.HybridDecrypt)
         for lang in supported_langs
     ]
-    unsupported_decs = [
-        testing_servers.hybrid_decrypt(lang, private_keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
     public_keyset = testing_servers.public_keyset(supported_langs[0],
                                                   private_keyset)
-    supported_encs = [
-        testing_servers.hybrid_encrypt(lang, public_keyset)
+    supported_encs = {
+        lang: testing_servers.remote_primitive(lang, public_keyset,
+                                               hybrid.HybridEncrypt)
         for lang in supported_langs
-    ]
-    unsupported_encs = [
-        testing_servers.hybrid_encrypt(lang, public_keyset)
-        for lang in testing_servers.LANGUAGES
-        if lang not in supported_langs
-    ]
-    for enc in supported_encs:
+    }
+    for lang, hybrid_encrypt in supported_encs.items():
       plaintext = (
           b'This is some plaintext message to be encrypted using key_template '
-          b'%s in %s.' % (key_template_name.encode('utf8'),
-                          enc.lang.encode('utf8')))
-      context_info = (
-          b'Some context info for %s using %s for encryption.' %
-          (key_template_name.encode('utf8'), enc.lang.encode('utf8')))
-      ciphertext = enc.encrypt(plaintext, context_info)
+          b'%s in %s.' %
+          (key_template_name.encode('utf8'), lang.encode('utf8')))
+      context_info = (b'Some context info for %s using %s for encryption.' %
+                      (key_template_name.encode('utf8'), lang.encode('utf8')))
+      ciphertext = hybrid_encrypt.encrypt(plaintext, context_info)
       for dec in supported_decs:
         output = dec.decrypt(ciphertext, context_info)
         self.assertEqual(output, plaintext)
-      for dec in unsupported_decs:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='Language %s supports hybrid decrypt with %s unexpectedly' %
-            (dec.lang, key_template_name)):
-          dec.decrypt(ciphertext, context_info)
-    for enc in unsupported_encs:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports hybrid encrypt with %s unexpectedly' % (
-              enc.lang, key_template_name)):
-        enc.encrypt(b'plaintext', b'context_info')
 
 
 # If the implementations work fine for keysets with single keys, then key
@@ -200,19 +172,27 @@
     builder = keyset_builder.new_keyset_builder()
     older_key_id = builder.add_new_key(old_key_tmpl)
     builder.set_primary_key(older_key_id)
-    dec1 = testing_servers.hybrid_decrypt(enc_lang, builder.keyset())
-    enc1 = testing_servers.hybrid_encrypt(dec_lang, builder.public_keyset())
+    dec1 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            hybrid.HybridDecrypt)
+    enc1 = testing_servers.remote_primitive(dec_lang, builder.public_keyset(),
+                                            hybrid.HybridEncrypt)
     newer_key_id = builder.add_new_key(new_key_tmpl)
-    dec2 = testing_servers.hybrid_decrypt(enc_lang, builder.keyset())
-    enc2 = testing_servers.hybrid_encrypt(dec_lang, builder.public_keyset())
+    dec2 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            hybrid.HybridDecrypt)
+    enc2 = testing_servers.remote_primitive(dec_lang, builder.public_keyset(),
+                                            hybrid.HybridEncrypt)
 
     builder.set_primary_key(newer_key_id)
-    dec3 = testing_servers.hybrid_decrypt(enc_lang, builder.keyset())
-    enc3 = testing_servers.hybrid_encrypt(dec_lang, builder.public_keyset())
+    dec3 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            hybrid.HybridDecrypt)
+    enc3 = testing_servers.remote_primitive(dec_lang, builder.public_keyset(),
+                                            hybrid.HybridEncrypt)
 
     builder.disable_key(older_key_id)
-    dec4 = testing_servers.hybrid_decrypt(enc_lang, builder.keyset())
-    enc4 = testing_servers.hybrid_encrypt(dec_lang, builder.public_keyset())
+    dec4 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            hybrid.HybridDecrypt)
+    enc4 = testing_servers.remote_primitive(dec_lang, builder.public_keyset(),
+                                            hybrid.HybridEncrypt)
     self.assertNotEqual(older_key_id, newer_key_id)
 
     # p1 encrypts with the older key. So p1, p2 and p3 can decrypt it,
diff --git a/testing/cross_language/jwt_kid_test.py b/testing/cross_language/jwt_kid_test.py
index 8c91a08..ff57f69 100644
--- a/testing/cross_language/jwt_kid_test.py
+++ b/testing/cross_language/jwt_kid_test.py
@@ -27,8 +27,8 @@
 from tink.proto import jwt_rsa_ssa_pkcs1_pb2
 from tink.proto import jwt_rsa_ssa_pss_pb2
 from tink.proto import tink_pb2
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['jwt']
 
@@ -57,7 +57,7 @@
 
 def generate_jwt_mac_keyset_with_custom_kid(
     template_name: str, custom_kid: str) -> tink_pb2.Keyset:
-  key_template = supported_key_types.KEY_TEMPLATE[template_name]
+  key_template = utilities.KEY_TEMPLATE[template_name]
   keyset_handle = tink.new_keyset_handle(key_template)
   # parse key_data.value, set custom_kid and serialize
   key_data_value = keyset_handle._keyset.key[0].key_data.value
@@ -73,7 +73,7 @@
 
 def generate_jwt_signature_keyset_with_custom_kid(
     template_name: str, custom_kid: str) -> tink_pb2.Keyset:
-  key_template = supported_key_types.KEY_TEMPLATE[template_name]
+  key_template = utilities.KEY_TEMPLATE[template_name]
   keyset_handle = tink.new_keyset_handle(key_template)
   # parse key_data.value, set custom_kid and serialize
   key_data_value = keyset_handle._keyset.key[0].key_data.value
@@ -103,34 +103,35 @@
 
   @parameterized.parameters(['JWT_HS256'])
   def test_jwt_mac_sets_kid_for_tink_templates(self, template_name):
-    key_template = supported_key_types.KEY_TEMPLATE[template_name]
+    key_template = utilities.KEY_TEMPLATE[template_name]
     keyset = testing_servers.new_keyset('cc', key_template)
     raw_jwt = jwt.new_raw_jwt(without_expiration=True)
     for lang in SUPPORTED_LANGUAGES:
-      jwt_mac = testing_servers.jwt_mac(lang, keyset)
+      jwt_mac = testing_servers.remote_primitive(lang, keyset, jwt.JwtMac)
       compact = jwt_mac.compute_mac_and_encode(raw_jwt)
       self.assertIsNotNone(decode_kid(compact))
 
   @parameterized.parameters(['JWT_HS256_RAW'])
   def test_jwt_mac_does_not_sets_kid_for_raw_templates(self, template_name):
-    key_template = supported_key_types.KEY_TEMPLATE[template_name]
+    key_template = utilities.KEY_TEMPLATE[template_name]
     keyset = testing_servers.new_keyset('cc', key_template)
     raw_jwt = jwt.new_raw_jwt(without_expiration=True)
     for lang in SUPPORTED_LANGUAGES:
-      jwt_mac = testing_servers.jwt_mac(lang, keyset)
+      jwt_mac = testing_servers.remote_primitive(lang, keyset, jwt.JwtMac)
       compact = jwt_mac.compute_mac_and_encode(raw_jwt)
       self.assertIsNone(decode_kid(compact))
 
   @parameterized.parameters(
       ['JWT_ES256', 'JWT_RS256_2048_F4', 'JWT_PS256_2048_F4'])
   def test_jwt_public_key_sign_sets_kid_for_tink_templates(self, template_name):
-    key_template = supported_key_types.KEY_TEMPLATE[template_name]
+    key_template = utilities.KEY_TEMPLATE[template_name]
     keyset = testing_servers.new_keyset('cc', key_template)
     raw_jwt = jwt.new_raw_jwt(without_expiration=True)
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         template_name]
     for lang in supported_langs:
-      jwt_sign = testing_servers.jwt_public_key_sign(lang, keyset)
+      jwt_sign = testing_servers.remote_primitive(lang, keyset,
+                                                  jwt.JwtPublicKeySign)
       compact = jwt_sign.sign_and_encode(raw_jwt)
       self.assertIsNotNone(decode_kid(compact))
 
@@ -138,13 +139,14 @@
       ['JWT_ES256_RAW', 'JWT_RS256_2048_F4_RAW', 'JWT_PS256_2048_F4_RAW'])
   def test_jwt_public_key_sign_does_not_sets_kid_for_raw_templates(
       self, template_name):
-    key_template = supported_key_types.KEY_TEMPLATE[template_name]
+    key_template = utilities.KEY_TEMPLATE[template_name]
     keyset = testing_servers.new_keyset('cc', key_template)
     raw_jwt = jwt.new_raw_jwt(without_expiration=True)
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         template_name]
     for lang in supported_langs:
-      jwt_sign = testing_servers.jwt_public_key_sign(lang, keyset)
+      jwt_sign = testing_servers.remote_primitive(lang, keyset,
+                                                  jwt.JwtPublicKeySign)
       compact = jwt_sign.sign_and_encode(raw_jwt)
       self.assertIsNone(decode_kid(compact))
 
@@ -154,7 +156,9 @@
         template_name=template_name, custom_kid='my kid')
     raw_jwt = jwt.new_raw_jwt(without_expiration=True)
     for lang in SUPPORTED_LANGUAGES:
-      jwt_mac = testing_servers.jwt_mac(lang, keyset.SerializeToString())
+      jwt_mac = testing_servers.remote_primitive(lang,
+                                                 keyset.SerializeToString(),
+                                                 jwt.JwtMac)
       compact = jwt_mac.compute_mac_and_encode(raw_jwt)
       self.assertEqual(decode_kid(compact), 'my kid')
 
@@ -168,7 +172,9 @@
           tink.TinkError,
           msg=('%s supports JWT mac keys with TINK output prefix type '
                'and custom_kid set unexpectedly') % lang):
-        jwt_mac = testing_servers.jwt_mac(lang, keyset.SerializeToString())
+        jwt_mac = testing_servers.remote_primitive(lang,
+                                                   keyset.SerializeToString(),
+                                                   jwt.JwtMac)
         jwt_mac.compute_mac_and_encode(raw_jwt)
 
   @parameterized.parameters(
@@ -178,11 +184,12 @@
     keyset = generate_jwt_signature_keyset_with_custom_kid(
         template_name=template_name, custom_kid='my kid')
     raw_jwt = jwt.new_raw_jwt(without_expiration=True)
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         template_name]
     for lang in supported_langs:
-      jwt_sign = testing_servers.jwt_public_key_sign(lang,
-                                                     keyset.SerializeToString())
+      jwt_sign = testing_servers.remote_primitive(lang,
+                                                  keyset.SerializeToString(),
+                                                  jwt.JwtPublicKeySign)
       compact = jwt_sign.sign_and_encode(raw_jwt)
       self.assertEqual(decode_kid(compact), 'my kid')
 
@@ -198,8 +205,9 @@
           tink.TinkError,
           msg=('%s supports JWT signature keys with TINK output prefix type '
                'and custom_kid set unexpectedly') % lang):
-        jwt_sign = testing_servers.jwt_public_key_sign(
-            lang, keyset.SerializeToString())
+        jwt_sign = testing_servers.remote_primitive(lang,
+                                                    keyset.SerializeToString(),
+                                                    jwt.JwtPublicKeySign)
         jwt_sign.sign_and_encode(raw_jwt)
 
 
diff --git a/testing/cross_language/jwt_test.py b/testing/cross_language/jwt_test.py
index 0e8eef9..e078c50 100644
--- a/testing/cross_language/jwt_test.py
+++ b/testing/cross_language/jwt_test.py
@@ -15,7 +15,6 @@
 
 import datetime
 import json
-from typing import Iterable
 
 from absl.testing import absltest
 from absl.testing import parameterized
@@ -23,9 +22,8 @@
 import tink
 from tink import jwt
 
-from util import supported_key_types
 from util import testing_servers
-
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['jwt']
 
@@ -38,38 +36,20 @@
   testing_servers.stop()
 
 
-def all_jwt_mac_key_template_names() -> Iterable[str]:
-  """Yields all JWT MAC key template names."""
-  for key_type in supported_key_types.JWT_MAC_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-
-
-def all_jwt_signature_key_template_names() -> Iterable[str]:
-  """Yields all JWT signature key template names."""
-  for key_type in supported_key_types.JWT_SIGNATURE_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-
-
 class JwtTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_jwt_mac_key_template_names())
+  @parameterized.parameters(utilities.tinkey_template_names_for(jwt.JwtMac))
   def test_compute_verify_jwt_mac(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     self.assertNotEmpty(supported_langs)
-    key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
     # Take the first supported language to generate the keyset.
     keyset = testing_servers.new_keyset(supported_langs[0], key_template)
-    supported_jwt_macs = [
-        testing_servers.jwt_mac(lang, keyset) for lang in supported_langs
-    ]
-    unsupported_jwt_macs = [
-        testing_servers.jwt_mac(lang, keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
+    supported_jwt_macs = []
+    for lang in supported_langs:
+      supported_jwt_macs.append(
+          testing_servers.remote_primitive(lang, keyset, jwt.JwtMac))
     now = datetime.datetime.now(tz=datetime.timezone.utc)
     raw_jwt = jwt.new_raw_jwt(
         issuer='issuer',
@@ -80,76 +60,43 @@
       for p2 in supported_jwt_macs:
         verified_jwt = p2.verify_mac_and_decode(compact, validator)
         self.assertEqual(verified_jwt.issuer(), 'issuer')
-      for p2 in unsupported_jwt_macs:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='%s supports verify_mac_and_decode with %s unexpectedly'
-            % (p2.lang, key_template_name)):
-          p2.verify_mac_and_decode(compact, validator)
-    for p in unsupported_jwt_macs:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='%s supports compute_mac_and_encode with %s unexpectedly' %
-          (p.lang, key_template_name)):
-        p.compute_mac_and_encode(raw_jwt)
 
-  @parameterized.parameters(all_jwt_signature_key_template_names())
+  @parameterized.parameters(
+      utilities.tinkey_template_names_for(jwt.JwtPublicKeySign))
   def test_jwt_public_key_sign_verify(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
     self.assertNotEmpty(supported_langs)
-    key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
     # Take the first supported language to generate the private keyset.
     private_keyset = testing_servers.new_keyset(supported_langs[0],
                                                 key_template)
-    supported_signers = [
-        testing_servers.jwt_public_key_sign(lang, private_keyset)
-        for lang in supported_langs
-    ]
-    unsupported_signers = [
-        testing_servers.jwt_public_key_sign(lang, private_keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
+    supported_signers = {}
+    for lang in supported_langs:
+      supported_signers[lang] = testing_servers.remote_primitive(
+          lang, private_keyset, jwt.JwtPublicKeySign)
     public_keyset = testing_servers.public_keyset('java', private_keyset)
-    supported_verifiers = [
-        testing_servers.jwt_public_key_verify(lang, public_keyset)
-        for lang in supported_langs
-    ]
-    unsupported_verifiers = [
-        testing_servers.jwt_public_key_verify(lang, public_keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
+    supported_verifiers = {}
+    for lang in supported_langs:
+      supported_verifiers[lang] = testing_servers.remote_primitive(
+          lang, public_keyset, jwt.JwtPublicKeyVerify)
     now = datetime.datetime.now(tz=datetime.timezone.utc)
     raw_jwt = jwt.new_raw_jwt(
-        issuer='issuer',
-        expiration=now + datetime.timedelta(seconds=100))
-    for signer in supported_signers:
+        issuer='issuer', expiration=now + datetime.timedelta(seconds=100))
+    for signer in supported_signers.values():
       compact = signer.sign_and_encode(raw_jwt)
       validator = jwt.new_validator(expected_issuer='issuer', fixed_now=now)
-      for verifier in supported_verifiers:
+      for verifier in supported_verifiers.values():
         verified_jwt = verifier.verify_and_decode(compact, validator)
         self.assertEqual(verified_jwt.issuer(), 'issuer')
-      for verifier in unsupported_verifiers:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='%s supports jwt_public_key_verify with %s unexpectedly' %
-            (verifier.lang, key_template_name)):
-          verifier.verify_and_decode(compact, validator)
-    for signer in unsupported_signers:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='%s supports jwt_public_key_sign with %s unexpectedly' %
-          (signer.lang, key_template_name)):
-        _ = signer.sign_and_encode(raw_jwt)
 
-  @parameterized.parameters(all_jwt_signature_key_template_names())
+  @parameterized.parameters(
+      utilities.tinkey_template_names_for(jwt.JwtPublicKeySign))
   def test_jwt_public_key_sign_export_import_verify(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     self.assertNotEmpty(supported_langs)
-    key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
     # Take the first supported language to generate the private keyset.
     private_keyset = testing_servers.new_keyset(supported_langs[0],
                                                 key_template)
@@ -160,16 +107,16 @@
 
     for lang1 in supported_langs:
       # in lang1: sign token and export public keyset to a JWK set
-      signer = testing_servers.jwt_public_key_sign(lang1, private_keyset)
+      signer = testing_servers.remote_primitive(lang1, private_keyset,
+                                                jwt.JwtPublicKeySign)
       compact = signer.sign_and_encode(raw_jwt)
       public_keyset = testing_servers.public_keyset(lang1, private_keyset)
-      public_jwk_set = testing_servers.jwk_set_from_keyset(
-          lang1, public_keyset)
+      public_jwk_set = testing_servers.jwk_set_from_keyset(lang1, public_keyset)
       for lang2 in supported_langs:
         # in lang2: import the public JWK set and verify the token
-        public_keyset = testing_servers.jwk_set_to_keyset(
-            lang2, public_jwk_set)
-        verifier = testing_servers.jwt_public_key_verify(lang2, public_keyset)
+        public_keyset = testing_servers.jwk_set_to_keyset(lang2, public_jwk_set)
+        verifier = testing_servers.remote_primitive(lang2, public_keyset,
+                                                    jwt.JwtPublicKeyVerify)
         verified_jwt = verifier.verify_and_decode(compact, validator)
         self.assertEqual(verified_jwt.issuer(), 'issuer')
 
@@ -183,7 +130,8 @@
           jwks['keys'][0]['kid'] = 'unknown kid'
           public_keyset = testing_servers.jwk_set_to_keyset(
               lang2, json.dumps(jwks))
-          verifier = testing_servers.jwt_public_key_verify(lang2, public_keyset)
+          verifier = testing_servers.remote_primitive(lang2, public_keyset,
+                                                      jwt.JwtPublicKeyVerify)
           with self.assertRaises(
               tink.TinkError,
               msg='%s accepts tokens with an incorrect kid unexpectedly' %
@@ -194,7 +142,8 @@
           del jwks['keys'][0]['kid']
           public_keyset = testing_servers.jwk_set_to_keyset(
               lang2, json.dumps(jwks))
-          verifier = testing_servers.jwt_public_key_verify(lang2, public_keyset)
+          verifier = testing_servers.remote_primitive(lang2, public_keyset,
+                                                      jwt.JwtPublicKeyVerify)
           verified_jwt = verifier.verify_and_decode(compact, validator)
           self.assertEqual(verified_jwt.issuer(), 'issuer')
         else:
@@ -202,9 +151,11 @@
           jwks['keys'][0]['kid'] = 'unknown kid'
           public_keyset = testing_servers.jwk_set_to_keyset(
               lang2, json.dumps(jwks))
-          verifier = testing_servers.jwt_public_key_verify(lang2, public_keyset)
+          verifier = testing_servers.remote_primitive(lang2, public_keyset,
+                                                      jwt.JwtPublicKeyVerify)
           verified_jwt = verifier.verify_and_decode(compact, validator)
           self.assertEqual(verified_jwt.issuer(), 'issuer')
 
+
 if __name__ == '__main__':
   absltest.main()
diff --git a/testing/cross_language/jwt_validation_test.py b/testing/cross_language/jwt_validation_test.py
index 09f954a..8b57662 100644
--- a/testing/cross_language/jwt_validation_test.py
+++ b/testing/cross_language/jwt_validation_test.py
@@ -126,7 +126,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_valid(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     verified_jwt = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
     self.assertEqual(verified_jwt.jwt_id(), '123')
 
@@ -134,7 +134,7 @@
   def test_verify_unknown_header_valid(self, lang):
     token = generate_token('{"alg":"HS256", "unknown":{"a":"b"}}',
                            '{"jti":"123"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     verified_jwt = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
     self.assertEqual(verified_jwt.jwt_id(), '123')
 
@@ -142,7 +142,7 @@
   def test_verify_empty_crit_header_invalid(self, lang):
     # See https://tools.ietf.org/html/rfc7515#section-4.1.11
     token = generate_token('{"alg":"HS256", "crit":[]}', '{"jti":"123"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
@@ -152,7 +152,7 @@
     token = generate_token(
         '{"alg":"HS256","crit":["http://example.invalid/UNDEFINED"],'
         '"http://example.invalid/UNDEFINED":true}', '{"jti":"123"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
@@ -160,7 +160,7 @@
   def test_verify_typ_header(self, lang):
     token = generate_token(
         '{"typ":"typeHeader", "alg":"HS256"}', '{"jti":"123"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     validator_with_correct_type_header = jwt.new_validator(
         expected_type_header='typeHeader', allow_missing_expiration=True)
@@ -182,7 +182,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_expiration(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123", "exp":1234}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     # same time is expired.
     validator_with_same_time = jwt.new_validator(
@@ -212,7 +212,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_float_expiration(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123", "exp":1234.5}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     validate_after = jwt.new_validator(
         fixed_now=datetime.datetime.fromtimestamp(1235.5,
@@ -228,33 +228,33 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_exp_expiration_is_fine(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"exp":1e10}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_large_expiration_is_fine(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"exp":253402300799}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_too_large_expiration_is_invalid(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"exp":253402300800}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_way_too_large_expiration_is_invalid(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"exp":1e30}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_infinity_expiration_is_invalid(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123", "exp":Infinity}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@@ -262,7 +262,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_not_before(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123", "nbf":1234}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     # same time as not-before fine.
     validator_same_time = jwt.new_validator(
@@ -295,7 +295,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_float_not_before(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123", "nbf":1234.5}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     validator_before = jwt.new_validator(
         allow_missing_expiration=True,
@@ -313,7 +313,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_issued_at(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"jti":"123", "iat":1234}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     # same time as issued-at fine.
     validator_same_time = jwt.new_validator(
@@ -356,7 +356,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_issuer(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"iss":"joe"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     validator_with_correct_issuer = jwt.new_validator(
         expected_issuer='joe', allow_missing_expiration=True)
@@ -383,25 +383,17 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_duplicated_issuer(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"iss":"joe", "iss":"jane"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
-    if lang != 'java' and lang != 'python':
-      validator_with_second_issuer = jwt.new_validator(
-          ignore_issuer=True, allow_missing_expiration=True)
-      with self.assertRaises(tink.TinkError):
-        jwt_mac.verify_mac_and_decode(token, validator_with_second_issuer)
-    else:
-      # Currently, this is accepted in Java and Python, and always the last
-      # entry is used.
-      # TODO(b/241828611): This should be rejected.
-      validator_with_second_issuer = jwt.new_validator(
-          expected_issuer='jane', allow_missing_expiration=True)
+    validator_with_second_issuer = jwt.new_validator(
+        ignore_issuer=True, allow_missing_expiration=True)
+    with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, validator_with_second_issuer)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_empty_string_issuer(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"iss":""}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     jwt_mac.verify_mac_and_decode(
         token,
         jwt.new_validator(expected_issuer='', allow_missing_expiration=True))
@@ -409,7 +401,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_issuer_with_wrong_type(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"iss":123}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@@ -418,14 +410,14 @@
   def test_verify_invalid_utf8_in_header(self, lang):
     token = generate_token_from_bytes(b'{"alg":"HS256", "a":"\xc2"}',
                                       b'{"iss":"joe"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_invalid_utf8_in_payload(self, lang):
     token = generate_token_from_bytes(b'{"alg":"HS256"}', b'{"jti":"joe\xc2"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
@@ -434,7 +426,7 @@
     # The JSON string contains the G clef character (U+1D11E) in UTF8.
     token = generate_token_from_bytes(b'{"alg":"HS256"}',
                                       b'{"jti":"\xF0\x9D\x84\x9E"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     token = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
     self.assertEqual(token.jwt_id(), u'\U0001d11e')
 
@@ -443,7 +435,7 @@
     # The JSON string contains "\uD834\uDD1E", which should decode to
     # the G clef character (U+1D11E).
     token = generate_token('{"alg":"HS256"}', '{"jti":"\\uD834\\uDD1E"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     token = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
     self.assertEqual(token.jwt_id(), u'\U0001d11e')
 
@@ -452,7 +444,7 @@
     # The JSON string contains "\uD834", which gets decoded into an invalid
     # UTF16 character.
     token = generate_token('{"alg":"HS256"}', '{"jti":"\\uD834"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
@@ -460,14 +452,14 @@
   def test_verify_with_invalid_json_escaped_utf16_in_claim_name(self, lang):
     token = generate_token('{"alg":"HS256"}',
                            '{"\\uD800\\uD800claim":"value"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_audience(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"aud":["joe", "jane"]}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     validator_with_correct_audience = jwt.new_validator(
         expected_audience='joe', allow_missing_expiration=True)
@@ -499,7 +491,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_audience_string(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"aud":"joe"}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     val1 = jwt.new_validator(
         expected_audience='joe', allow_missing_expiration=True)
@@ -512,7 +504,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_audiences_with_wrong_type(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"aud":["joe", 123]}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@@ -520,7 +512,7 @@
   @parameterized.parameters(SUPPORTED_LANGUAGES)
   def test_verify_token_with_empty_audiences(self, lang):
     token = generate_token('{"alg":"HS256"}', '{"aud":[]}')
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@@ -529,7 +521,7 @@
   def test_verify_token_with_utf_16_encoded_payload_fails(self, lang):
     token = generate_token_from_bytes('{"alg":"HS256"}'.encode('utf-8'),
                                       '{"iss":"joe"}'.encode('utf-16'))
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@@ -538,7 +530,7 @@
   def test_verify_token_with_utf_32_encoded_payload_fails(self, lang):
     token = generate_token_from_bytes('{"alg":"HS256"}'.encode('utf-8'),
                                       '{"iss":"joe"}'.encode('utf-32'))
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
 
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@@ -548,7 +540,7 @@
     num_recursions = 10
     payload = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
     token = generate_token('{"alg":"HS256"}', payload)
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
   @parameterized.parameters(SUPPORTED_LANGUAGES)
@@ -561,7 +553,7 @@
     num_recursions = 10000
     payload = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
     token = generate_token('{"alg":"HS256"}', payload)
-    jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
+    jwt_mac = testing_servers.remote_primitive(lang, KEYSET, jwt.JwtMac)
     with self.assertRaises(tink.TinkError):
       jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
 
diff --git a/testing/cross_language/key_generation_consistency_test.py b/testing/cross_language/key_generation_consistency_test.py
index 2bd0267..8ccb921 100644
--- a/testing/cross_language/key_generation_consistency_test.py
+++ b/testing/cross_language/key_generation_consistency_test.py
@@ -32,7 +32,7 @@
 from tink.proto import ecdsa_pb2
 from tink.proto import jwt_hmac_pb2
 from tink.proto import tink_pb2
-from util import supported_key_types
+import tink_config
 from util import testing_servers
 
 # Test cases that succeed in a language but should fail
@@ -394,8 +394,8 @@
                       rsa_ssa_pkcs1_test_cases(),
                       rsa_ssa_pss_test_cases()))
   def test_key_generation_consistency(self, name, template):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES[
-        supported_key_types.KEY_TYPE_FROM_URL[template.type_url]]
+    supported_langs = tink_config.supported_languages_for_key_type(
+        tink_config.key_type_from_type_url(template.type_url))
     failures = 0
     results = {}
     for lang in supported_langs:
diff --git a/testing/cross_language/key_template_consistency_test.py b/testing/cross_language/key_template_consistency_test.py
index 562b04e..27bb908 100644
--- a/testing/cross_language/key_template_consistency_test.py
+++ b/testing/cross_language/key_template_consistency_test.py
@@ -21,37 +21,33 @@
 import tink
 
 from util import key_util
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 
 def all_template_names() -> Iterable[str]:
-  for names in supported_key_types.KEY_TEMPLATE_NAMES.values():
+  for names in utilities.KEY_TEMPLATE_NAMES.values():
     for name in names:
       yield name
 
 
 # These key templates are not defined in these languages.
 UNDEFINED_TEMPLATES = [
+    ('ECDSA_P384', 'go'),
     ('ECDSA_P384_SHA384_IEEE_P1363', 'cc'),
     ('ECDSA_P384_SHA384_IEEE_P1363', 'java'),
     ('ECDSA_P384_SHA384_IEEE_P1363', 'go'),
     ('AES128_GCM_HKDF_1MB', 'cc'),
-    ('AES128_CTR_HMAC_SHA256_1MB', 'cc'),
-    ('AES256_CTR_HMAC_SHA256_1MB', 'cc'),
     ('ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM', 'go'),
     ('ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256', 'go'),
     ('ECDSA_P256_IEEE_P1363', 'go'),
     ('ECDSA_P384_IEEE_P1363', 'go'),
     ('ECDSA_P521_IEEE_P1363', 'go'),
-    ('AES128_GCM_SIV', 'go'),
-    ('AES256_GCM_SIV', 'go'),
     ('AES128_EAX_RAW', 'cc'),
     ('AES256_EAX_RAW', 'cc'),
     ('AES128_GCM_SIV_RAW', 'cc'),
     ('AES256_GCM_SIV_RAW', 'cc'),
     ('AES128_GCM_SIV_RAW', 'go'),
-    ('AES256_GCM_SIV_RAW', 'go'),
     ('AES128_GCM_RAW', 'go'),
     ('AES128_CTR_HMAC_SHA256_RAW', 'cc'),
     ('AES256_CTR_HMAC_SHA256_RAW', 'cc'),
@@ -75,7 +71,7 @@
 
   @parameterized.parameters(all_template_names())
   def test_key_template_is_consistent(self, template_name):
-    langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         template_name]
     templates = {}
     for lang in langs:
diff --git a/testing/cross_language/key_version_test.py b/testing/cross_language/key_version_test.py
index 8e8c48d..56a3805 100644
--- a/testing/cross_language/key_version_test.py
+++ b/testing/cross_language/key_version_test.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 """Tests that keys with higher version numbers are rejected."""
 
-from typing import List
+from typing import Iterable
 
 from absl.testing import absltest
 from absl.testing import parameterized
@@ -40,8 +40,9 @@
 from tink.proto import tink_pb2
 from tink.proto import xchacha20_poly1305_pb2
 
-from util import supported_key_types
+import tink_config
 from util import testing_servers
+from util import utilities
 
 
 KEY_TYPE_TO_PROTO_CLASS = {
@@ -66,7 +67,7 @@
   """Parses keyset and generates modified keyset with incremented version."""
   keyset_proto = tink_pb2.Keyset.FromString(keyset)
   for key in keyset_proto.key:
-    key_type = supported_key_types.KEY_TYPE_FROM_URL[key.key_data.type_url]
+    key_type = tink_config.key_type_from_type_url(key.key_data.type_url)
     key_class = KEY_TYPE_TO_PROTO_CLASS[key_type]
 
     default_val = key.key_data.value
@@ -92,10 +93,10 @@
     key.key_data.value = default_val
 
 
-def test_cases(key_types: List[str]):
+def test_cases(key_types: Iterable[str]):
   for key_type in key_types:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      for lang in supported_key_types.SUPPORTED_LANGUAGES[key_type]:
+    for key_template_name in utilities.KEY_TEMPLATE_NAMES[key_type]:
+      for lang in tink_config.supported_languages_for_key_type(key_type):
         yield (key_template_name, lang)
 
 
@@ -119,50 +120,53 @@
   incremented version.
   """
 
-  @parameterized.parameters(test_cases(supported_key_types.AEAD_KEY_TYPES))
+  @parameterized.parameters(
+      test_cases(tink_config.key_types_for_primitive(aead.Aead)))
   def test_inc_version_aead(self, key_template_name, lang):
     """Increments the key version by one and checks they can't be used."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    _ = testing_servers.aead(lang, keyset).encrypt(b'foo', b'bar')
+    _ = testing_servers.remote_primitive(lang, keyset,
+                                         aead.Aead).encrypt(b'foo', b'bar')
     for keyset1 in gen_inc_versions(keyset):
-      aead_primitive = testing_servers.aead(lang, keyset1)
       with self.assertRaises(tink.TinkError):
-        _ = aead_primitive.encrypt(b'foo', b'bar')
+        _ = testing_servers.remote_primitive(lang, keyset1, aead.Aead)
 
-  @parameterized.parameters(test_cases(supported_key_types.DAEAD_KEY_TYPES))
+  @parameterized.parameters(
+      test_cases(tink_config.key_types_for_primitive(daead.DeterministicAead)))
   def test_inc_version_daead(self, key_template_name, lang):
     """Increments the key version by one and checks they can't be used."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    p = testing_servers.deterministic_aead(lang, keyset)
+    p = testing_servers.remote_primitive(lang, keyset, daead.DeterministicAead)
     _ = p.encrypt_deterministically(b'foo', b'bar')
     for keyset1 in gen_inc_versions(keyset):
-      daead_primitive = testing_servers.deterministic_aead(lang, keyset1)
       with self.assertRaises(tink.TinkError):
-        _ = daead_primitive.encrypt_deterministically(b'foo', b'bar')
+        _ = testing_servers.remote_primitive(lang, keyset1,
+                                             daead.DeterministicAead)
 
-  @parameterized.parameters(test_cases(supported_key_types.MAC_KEY_TYPES))
+  @parameterized.parameters(
+      test_cases(tink_config.key_types_for_primitive(mac.Mac)))
   def test_inc_version_mac(self, key_template_name, lang):
     """Increments the key version by one and checks they can't be used."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    _ = testing_servers.mac(lang, keyset).compute_mac(b'foo')
+    _ = testing_servers.remote_primitive(lang, keyset, mac.Mac)
     for keyset1 in gen_inc_versions(keyset):
-      mac_primitive1 = testing_servers.mac(lang, keyset1)
       with self.assertRaises(tink.TinkError):
-        _ = mac_primitive1.compute_mac(b'foo')
+        _ = testing_servers.remote_primitive(lang, keyset1, mac.Mac)
 
-  @parameterized.parameters(test_cases(supported_key_types.PRF_KEY_TYPES))
+  @parameterized.parameters(
+      test_cases(tink_config.key_types_for_primitive(prf.PrfSet)))
   def test_inc_version_prf(self, key_template_name, lang):
     """Increments the key version by one and checks they can't be used."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    _ = testing_servers.prf_set(lang, keyset).primary().compute(b'foo', 16)
+    prf_set = testing_servers.remote_primitive(lang, keyset, prf.PrfSet)
+    _ = prf_set.primary().compute(b'foo', 16)
     for keyset1 in gen_inc_versions(keyset):
-      prf_set_primitive = testing_servers.prf_set(lang, keyset1)
       with self.assertRaises(tink.TinkError):
-        _ = prf_set_primitive.primary().compute(b'foo', 16)
+        _ = testing_servers.remote_primitive(lang, keyset1, prf.PrfSet)
 
 
 if __name__ == '__main__':
diff --git a/testing/cross_language/keyset_validation_test.py b/testing/cross_language/keyset_validation_test.py
index b16a21b..886cbef 100644
--- a/testing/cross_language/keyset_validation_test.py
+++ b/testing/cross_language/keyset_validation_test.py
@@ -17,19 +17,25 @@
 """
 
 import datetime
-
-from typing import List
+from typing import Any
+from typing import Iterable
+from typing import Tuple
 
 from absl.testing import absltest
 from absl.testing import parameterized
-
 import tink
+from tink import aead
+from tink import daead
+from tink import hybrid
 from tink import jwt
+from tink import mac
+from tink import prf
+from tink import signature
 
 from tink.proto import tink_pb2
-
-from util import supported_key_types
+import tink_config
 from util import testing_servers
+from util import utilities
 
 
 def unset_primary(keyset: bytes) -> bytes:
@@ -39,10 +45,10 @@
   return keyset_proto.SerializeToString()
 
 
-def test_cases(key_types: List[str]):
-  for key_type in key_types:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      for lang in supported_key_types.SUPPORTED_LANGUAGES[key_type]:
+def test_cases(primitive: Any) -> Iterable[Tuple[str, str]]:
+  for key_type in tink_config.key_types_for_primitive(primitive):
+    for key_template_name in utilities.KEY_TEMPLATE_NAMES[key_type]:
+      for lang in tink_config.supported_languages_for_key_type(key_type):
         yield (key_template_name, lang)
 
 
@@ -57,109 +63,97 @@
 class KeysetValidationTest(parameterized.TestCase):
   """These tests verify keysets are properly validated."""
 
-  @parameterized.parameters(test_cases(supported_key_types.AEAD_KEY_TYPES))
+  @parameterized.parameters(test_cases(aead.Aead))
   def test_aead_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to use an AEAD primitive."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    ciphertext = testing_servers.aead(lang, keyset).encrypt(b'foo', b'bar')
 
-    aead_without_primary = testing_servers.aead(lang, unset_primary(keyset))
     with self.assertRaises(tink.TinkError):
-      _ = aead_without_primary.encrypt(b'foo', b'bar')
-    with self.assertRaises(tink.TinkError):
-      _ = aead_without_primary.decrypt(ciphertext, b'bar')
+      _ = testing_servers.remote_primitive(lang, unset_primary(keyset),
+                                           aead.Aead)
 
-  @parameterized.parameters(test_cases(supported_key_types.DAEAD_KEY_TYPES))
+  @parameterized.parameters(test_cases(daead.DeterministicAead))
   def test_daead_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to use a DAEAD primitive."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    p = testing_servers.deterministic_aead(lang, keyset)
-    ciphertext = p.encrypt_deterministically(b'foo', b'bar')
-    daead_without_primary = testing_servers.deterministic_aead(
-        lang, unset_primary(keyset))
     with self.assertRaises(tink.TinkError):
-      _ = daead_without_primary.encrypt_deterministically(b'foo', b'bar')
-    with self.assertRaises(tink.TinkError):
-      _ = daead_without_primary.decrypt_deterministically(ciphertext, b'bar')
+      _ = testing_servers.remote_primitive(lang, unset_primary(keyset),
+                                           daead.DeterministicAead)
 
-  @parameterized.parameters(test_cases(supported_key_types.MAC_KEY_TYPES))
+  @parameterized.parameters(test_cases(mac.Mac))
   def test_mac_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to use a MAC primitive."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    mac_value = testing_servers.mac(lang, keyset).compute_mac(b'foo')
-    mac_without_primary = testing_servers.mac(lang, unset_primary(keyset))
     with self.assertRaises(tink.TinkError):
-      _ = mac_without_primary.compute_mac(b'foo')
-    with self.assertRaises(tink.TinkError):
-      mac_without_primary.verify_mac(mac_value, b'foo')
+      testing_servers.remote_primitive(lang, unset_primary(keyset), mac.Mac)
 
-  @parameterized.parameters(test_cases(supported_key_types.PRF_KEY_TYPES))
+  @parameterized.parameters(test_cases(prf.PrfSet))
   def test_prf_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to use a PRF set primitive."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    _ = testing_servers.prf_set(lang, keyset).primary().compute(b'foo', 16)
-    prf_set_without_primary = testing_servers.prf_set(lang,
-                                                      unset_primary(keyset))
     with self.assertRaises(tink.TinkError):
-      _ = prf_set_without_primary.primary().compute(b'foo', 16)
+      _ = testing_servers.remote_primitive(lang, unset_primary(keyset),
+                                           prf.PrfSet)
 
-  # We skip RSA keys to make the tests run faster. It shouldn't make a
-  # difference since the logic does not really depend on the key type.
-  @parameterized.parameters(test_cases(['EcdsaPrivateKey']))
+  @parameterized.parameters(test_cases(signature.PublicKeySign))
   def test_signature_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to sign and verify signatures."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     private_keyset = testing_servers.new_keyset(lang, template)
     public_keyset = testing_servers.public_keyset(lang, private_keyset)
-    sig = testing_servers.public_key_sign(lang, private_keyset).sign(b'foo')
-    testing_servers.public_key_verify(lang, public_keyset).verify(sig, b'foo')
-
-    signer_without_primary = testing_servers.public_key_sign(
-        lang, unset_primary(private_keyset))
-    verifier_without_primary = testing_servers.public_key_verify(
-        lang, unset_primary(public_keyset))
+    signer = testing_servers.remote_primitive(lang, private_keyset,
+                                              signature.PublicKeySign)
+    sig = signer.sign(b'foo')
+    verifier = testing_servers.remote_primitive(lang, public_keyset,
+                                                signature.PublicKeyVerify)
+    verifier.verify(sig, b'foo')
+    private_keyset_without_primary = unset_primary(private_keyset)
+    public_keyset_without_primary = unset_primary(public_keyset)
     with self.assertRaises(tink.TinkError):
-      signer_without_primary.sign(b'foo')
-    if lang in ['java', 'python']:
-      # Java and Python currently allow this.
-      verifier_without_primary.verify(sig, b'foo')
-    else:
+      _ = testing_servers.remote_primitive(
+          lang, private_keyset_without_primary, signature.PublicKeySign)
+    if lang not in ['python']:
       with self.assertRaises(tink.TinkError):
-        verifier_without_primary.verify(sig, b'foo')
+        _ = testing_servers.remote_primitive(
+            lang, public_keyset_without_primary, signature.PublicKeyVerify)
+    if lang in ['python']:
+      # TODO(b/252792776) This should fail.
+      verifier_without_primary = testing_servers.remote_primitive(
+          lang, public_keyset_without_primary, signature.PublicKeyVerify)
+      verifier_without_primary.verify(sig, b'foo')
 
-  @parameterized.parameters(
-      test_cases(supported_key_types.HYBRID_PRIVATE_KEY_TYPES))
+  @parameterized.parameters(test_cases(hybrid.HybridDecrypt))
   def test_hybrid_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to use hybrid encryption."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     private_keyset = testing_servers.new_keyset(lang, template)
     public_keyset = testing_servers.public_keyset(lang, private_keyset)
-    ciphertext = testing_servers.hybrid_encrypt(lang, public_keyset).encrypt(
-        b'foo', b'context_info')
 
-    dec_without_primary = testing_servers.hybrid_decrypt(
-        lang, unset_primary(private_keyset))
+    private_keyset_without_primary = unset_primary(private_keyset)
     with self.assertRaises(tink.TinkError):
-      dec_without_primary.decrypt(ciphertext, b'context_info')
+      testing_servers.remote_primitive(
+          lang, unset_primary(private_keyset_without_primary),
+          hybrid.HybridDecrypt)
 
-    enc_without_primary = testing_servers.hybrid_encrypt(
-        lang, unset_primary(public_keyset))
+    public_keyset_without_primary = unset_primary(public_keyset)
     with self.assertRaises(tink.TinkError):
+      enc_without_primary = testing_servers.remote_primitive(
+          lang, public_keyset_without_primary, hybrid.HybridEncrypt)
+      # TODO(b/228140127) This should fail above already.
       enc_without_primary.encrypt(b'foo', b'context_info')
 
-  # We skip RSA keys to make the tests run faster. It shouldn't make a
-  # difference since the logic does not really depend on the key type.
-  @parameterized.parameters(test_cases(['JwtEcdsaPrivateKey']))
+  @parameterized.parameters(test_cases(jwt.JwtPublicKeySign))
   def test_jwt_signature_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to sign and verify JWT signatures."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     private_keyset = testing_servers.new_keyset(lang, template)
     public_keyset = testing_servers.public_keyset(lang, private_keyset)
-    signer = testing_servers.jwt_public_key_sign(lang, private_keyset)
+    signer = testing_servers.remote_primitive(lang, private_keyset,
+                                              jwt.JwtPublicKeySign)
 
     now = datetime.datetime.now(tz=datetime.timezone.utc)
     raw_jwt = jwt.new_raw_jwt(
@@ -167,43 +161,34 @@
         expiration=now + datetime.timedelta(seconds=100))
     token = signer.sign_and_encode(raw_jwt)
 
-    signer_without_primary = testing_servers.jwt_public_key_sign(
-        lang, unset_primary(private_keyset))
+    private_keyset_without_primary = unset_primary(private_keyset)
+    public_keyset_without_primary = unset_primary(public_keyset)
+
     with self.assertRaises(tink.TinkError):
-      signer_without_primary.sign_and_encode(raw_jwt)
+      _ = testing_servers.remote_primitive(
+          lang, private_keyset_without_primary, jwt.JwtPublicKeySign)
 
-    verifier_without_primary = testing_servers.jwt_public_key_verify(
-        lang, unset_primary(public_keyset))
-    validator = jwt.new_validator(expected_issuer='issuer', fixed_now=now)
-    if lang in ['cc', 'java', 'python']:
-      # C++, Java and Python currently allow this.
-      verifier_without_primary.verify_and_decode(token, validator)
-    else:
+    if lang not in ['cc', 'python']:
       with self.assertRaises(tink.TinkError):
-        verifier_without_primary.verify_and_decode(token, validator)
+        _ = testing_servers.remote_primitive(lang,
+                                             public_keyset_without_primary,
+                                             jwt.JwtPublicKeyVerify)
+    if lang in ['cc', 'python']:
+      # TODO(b/252792776) This should fail.
+      verifier_without_primary = testing_servers.remote_primitive(
+          lang, public_keyset_without_primary, jwt.JwtPublicKeyVerify)
+      validator = jwt.new_validator(expected_issuer='issuer', fixed_now=now)
+      verifier_without_primary.verify_and_decode(token, validator)
 
-  @parameterized.parameters(
-      test_cases(supported_key_types.JWT_MAC_KEY_TYPES))
+  @parameterized.parameters(test_cases(jwt.JwtMac))
   def test_jwt_mac_without_primary(self, key_template_name, lang):
     """Unsets the primary key and tries to create and verify JWT MACs."""
-    template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    template = utilities.KEY_TEMPLATE[key_template_name]
     keyset = testing_servers.new_keyset(lang, template)
-    jwt_mac = testing_servers.jwt_mac(lang, keyset)
 
-    now = datetime.datetime.now(tz=datetime.timezone.utc)
-    raw_jwt = jwt.new_raw_jwt(
-        issuer='issuer',
-        expiration=now + datetime.timedelta(seconds=100))
-    token = jwt_mac.compute_mac_and_encode(raw_jwt)
-
-    jwt_mac_without_primary = testing_servers.jwt_mac(
-        lang, unset_primary(keyset))
     with self.assertRaises(tink.TinkError):
-      jwt_mac_without_primary.compute_mac_and_encode(raw_jwt)
-
-    validator = jwt.new_validator(expected_issuer='issuer', fixed_now=now)
-    with self.assertRaises(tink.TinkError):
-      jwt_mac_without_primary.verify_mac_and_decode(token, validator)
+      _ = testing_servers.remote_primitive(lang, unset_primary(keyset),
+                                           jwt.JwtMac)
 
 
 if __name__ == '__main__':
diff --git a/testing/cross_language/kms_aead_test.py b/testing/cross_language/kms_aead_test.py
new file mode 100644
index 0000000..24d3385
--- /dev/null
+++ b/testing/cross_language/kms_aead_test.py
@@ -0,0 +1,358 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Cross-language tests for the KMS Envelope AEAD primitive with AWS and GCP."""
+from typing import Dict, Iterable, List, Sequence, Tuple
+
+from absl.testing import absltest
+from absl.testing import parameterized
+import tink
+from tink import aead
+
+from tink.proto import tink_pb2
+from util import testing_servers
+from util import utilities
+
+# AWS Key with alias "unit-and-integration-testing"
+_AWS_KEY_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+                '3ee50705-5a82-4f5b-9753-05c4f473922f')
+_AWS_KEY_ALIAS_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:alias/'
+                      'unit-and-integration-testing')
+
+
+# 2nd AWS Key with alias "unit-and-integration-testing-2"
+_AWS_KEY_2_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+                  'b3ca2efd-a8fb-47f2-b541-7e20f8c5cd11')
+_AWS_KEY_2_ALIAS_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:alias/'
+                        'unit-and-integration-testing-2')
+
+_AWS_UNKNOWN_KEY_URI = ('aws-kms://arn:aws:kms:us-east-2:235739564943:key/'
+                        '4ee50705-5a82-4f5b-9753-05c4f473922f')
+_AWS_UNKNOWN_KEY_ALIAS_URI = (
+    'aws-kms://arn:aws:kms:us-east-2:235739564943:alias/'
+    'unknown-unit-and-integration-testing')
+
+_GCP_KEY_URI = ('gcp-kms://projects/tink-test-infrastructure/locations/global/'
+                'keyRings/unit-and-integration-testing/cryptoKeys/aead-key')
+_GCP_KEY_2_URI = (
+    'gcp-kms://projects/tink-test-infrastructure/locations/global/'
+    'keyRings/unit-and-integration-testing/cryptoKeys/aead2-key')
+_GCP_UNKNOWN_KEY_URI = (
+    'gcp-kms://projects/tink-test-infrastructure/locations/global/'
+    'keyRings/unit-and-integration-testing/cryptoKeys/unknown')
+
+_KMS_KEY_URI = {
+    'GCP': _GCP_KEY_URI,
+    'AWS': _AWS_KEY_URI,
+}
+
+_DEK_TEMPLATE = utilities.KEY_TEMPLATE['AES128_GCM']
+
+
+def _kms_envelope_aead_templates(
+    kms_services: Sequence[str]) -> Dict[str, tink_pb2.KeyTemplate]:
+  """Generates a map from KMS envelope AEAD template name to key template."""
+  kms_key_templates = {}
+  for kms_service in kms_services:
+    key_uri = _KMS_KEY_URI[kms_service]
+    kms_envelope_aead_key_template = (
+        aead.aead_key_templates.create_kms_envelope_aead_key_template(
+            key_uri, _DEK_TEMPLATE))
+    kms_envelope_aead_template_name = '%s_KMS_ENVELOPE_AEAD' % kms_service
+    kms_key_templates[kms_envelope_aead_template_name] = (
+        kms_envelope_aead_key_template)
+  return kms_key_templates
+
+
+_KMS_ENVELOPE_AEAD_KEY_TEMPLATES = _kms_envelope_aead_templates(['GCP', 'AWS'])
+_SUPPORTED_LANGUAGES_FOR_KMS_ENVELOPE_AEAD = ('python', 'cc', 'go', 'java')
+
+_SUPPORTED_LANGUAGES_FOR_KMS_AEAD = {
+    'AWS': ('python', 'cc', 'go', 'java'),
+    'GCP': ('python', 'cc', 'go', 'java'),
+}
+
+
+def setUpModule():
+  aead.register()
+  testing_servers.start('aead')
+
+
+def tearDownModule():
+  testing_servers.stop()
+
+
+def _get_lang_tuples(langs: List[str]) -> Iterable[Tuple[str, str]]:
+  """Yields language tuples to run cross-language tests.
+
+  Ideally, we would want to the test all possible tuples of languages. But
+  that results in a quadratic number of tuples. It is not really necessary,
+  because if an implementation in one language does something different, then
+  any cross-language test with another language will fail. So it is enough to
+  only use every implementation once for encryption and once for decryption.
+
+  Args:
+    langs: List of language names.
+
+  Yields:
+    Tuples of 2 languages.
+  """
+  for i, _ in enumerate(langs):
+    yield (langs[i], langs[((i + 1) % len(langs))])
+
+
+def _get_plaintext_and_aad(key_template_name: str,
+                           lang: str) -> Tuple[bytes, bytes]:
+  """Creates test plaintext and associated data from a key template and lang."""
+  plaintext = (
+      b'This is some plaintext message to be encrypted using key_template '
+      b'%s using %s for encryption.' %
+      (key_template_name.encode('utf8'), lang.encode('utf8')))
+  associated_data = (b'Some associated data for %s using %s for encryption.' %
+                     (key_template_name.encode('utf8'), lang.encode('utf8')))
+  return (plaintext, associated_data)
+
+
+def _kms_aead_test_cases() -> Iterable[Tuple[str, str, str]]:
+  """Yields (KMS service, encrypt lang, decrypt lang)."""
+  for kms_service, supported_langs in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.items():
+    for encrypt_lang, decrypt_lang in _get_lang_tuples(supported_langs):
+      yield (kms_service, encrypt_lang, decrypt_lang)
+
+
+def _two_key_uris_test_cases():
+  for lang in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.get('AWS', []):
+    yield (lang, _AWS_KEY_URI, _AWS_KEY_2_URI)
+  for lang in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.get('GCP', []):
+    yield (lang, _GCP_KEY_URI, _GCP_KEY_2_URI)
+
+
+def _key_uris_with_alias_test_cases():
+  for lang in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.get('AWS', []):
+    yield (lang, _AWS_KEY_ALIAS_URI)
+
+
+def _two_key_uris_with_alias_test_cases():
+  for lang in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.get('AWS', []):
+    yield (lang, _AWS_KEY_ALIAS_URI, _AWS_KEY_2_ALIAS_URI)
+
+
+def _unknown_key_uris_test_cases():
+  for lang in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.get('AWS', []):
+    yield (lang, _AWS_UNKNOWN_KEY_URI)
+    yield (lang, _AWS_UNKNOWN_KEY_ALIAS_URI)
+  for lang in _SUPPORTED_LANGUAGES_FOR_KMS_AEAD.get('GCP', []):
+    yield (lang, _GCP_UNKNOWN_KEY_URI)
+
+
+class KmsAeadTest(parameterized.TestCase):
+
+  def test_get_lang_tuples(self):
+    self.assertEqual(
+        list(_get_lang_tuples(['cc', 'java', 'go', 'python'])),
+        [('cc', 'java'), ('java', 'go'), ('go', 'python'), ('python', 'cc')],
+    )
+    self.assertEqual(list(_get_lang_tuples([])), [])
+
+  @parameterized.parameters(_kms_aead_test_cases())
+  def test_encrypt_decrypt_with_associated_data(
+      self, kms_service, encrypt_lang, decrypt_lang
+  ):
+    kms_key_uri = _KMS_KEY_URI[kms_service]
+    kms_aead_template_name = '%s_KMS_AEAD' % kms_service
+    key_template = aead.aead_key_templates.create_kms_aead_key_template(
+        kms_key_uri)
+    keyset = testing_servers.new_keyset(encrypt_lang, key_template)
+    encrypt_primitive = testing_servers.remote_primitive(
+        lang=encrypt_lang, keyset=keyset, primitive_class=aead.Aead)
+    plaintext, associated_data = _get_plaintext_and_aad(kms_aead_template_name,
+                                                        encrypt_primitive.lang)
+    ciphertext = encrypt_primitive.encrypt(plaintext, associated_data)
+    decrypt_primitive = testing_servers.remote_primitive(
+        decrypt_lang, keyset, aead.Aead)
+    output = decrypt_primitive.decrypt(ciphertext, associated_data)
+    self.assertEqual(output, plaintext)
+
+  @parameterized.parameters(_kms_aead_test_cases())
+  def test_encrypt_decrypt_with_empty_associated_data(
+      self, kms_service, encrypt_lang, decrypt_lang
+  ):
+    kms_key_uri = _KMS_KEY_URI[kms_service]
+    key_template = aead.aead_key_templates.create_kms_aead_key_template(
+        kms_key_uri)
+    keyset = testing_servers.new_keyset(encrypt_lang, key_template)
+    encrypt_primitive = testing_servers.remote_primitive(
+        lang=encrypt_lang, keyset=keyset, primitive_class=aead.Aead)
+    plaintext = b'plaintext'
+    associated_data = b''
+    ciphertext = encrypt_primitive.encrypt(plaintext, associated_data)
+    decrypt_primitive = testing_servers.remote_primitive(
+        decrypt_lang, keyset, aead.Aead)
+    output = decrypt_primitive.decrypt(ciphertext, associated_data)
+    self.assertEqual(output, plaintext)
+
+  @parameterized.parameters(_two_key_uris_test_cases())
+  def test_cannot_decrypt_ciphertext_of_other_key_uri(self, lang, key_uri,
+                                                      key_uri_2):
+    keyset = testing_servers.new_keyset(
+        lang, aead.aead_key_templates.create_kms_aead_key_template(key_uri))
+    keyset_2 = testing_servers.new_keyset(
+        lang, aead.aead_key_templates.create_kms_aead_key_template(key_uri_2))
+
+    primitive = testing_servers.remote_primitive(
+        lang=lang, keyset=keyset, primitive_class=aead.Aead)
+    primitive_2 = testing_servers.remote_primitive(
+        lang=lang, keyset=keyset_2, primitive_class=aead.Aead)
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+
+    ciphertext = primitive.encrypt(plaintext, associated_data)
+    ciphertext_2 = primitive_2.encrypt(plaintext, associated_data)
+
+    # Can be decrypted by the primtive that created the ciphertext.
+    self.assertEqual(primitive.decrypt(ciphertext, associated_data), plaintext)
+    self.assertEqual(
+        primitive_2.decrypt(ciphertext_2, associated_data), plaintext)
+
+    # Cannot be decrypted by the other primitive.
+    with self.assertRaises(tink.TinkError):
+      primitive.decrypt(ciphertext_2, associated_data)
+    with self.assertRaises(tink.TinkError):
+      primitive_2.decrypt(ciphertext, associated_data)
+
+  @parameterized.parameters(_key_uris_with_alias_test_cases())
+  def test_encrypt_decrypt_with_key_aliases(self, lang, alias_key_uri):
+    keyset = testing_servers.new_keyset(
+        lang,
+        aead.aead_key_templates.create_kms_aead_key_template(alias_key_uri))
+    primitive = testing_servers.remote_primitive(
+        lang=lang, keyset=keyset, primitive_class=aead.Aead)
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+    ciphertext = primitive.encrypt(plaintext, associated_data)
+    self.assertEqual(
+        primitive.decrypt(ciphertext, associated_data), plaintext)
+
+  @parameterized.parameters(_two_key_uris_with_alias_test_cases())
+  def test_cannot_decrypt_ciphertext_of_other_alias_key_uri(
+      self, lang, alias_key_uri, alias_key_uri_2):
+    keyset = testing_servers.new_keyset(
+        lang,
+        aead.aead_key_templates.create_kms_aead_key_template(alias_key_uri))
+    keyset_2 = testing_servers.new_keyset(
+        lang,
+        aead.aead_key_templates.create_kms_aead_key_template(alias_key_uri_2))
+
+    primitive = testing_servers.remote_primitive(
+        lang=lang, keyset=keyset, primitive_class=aead.Aead)
+    primitive_2 = testing_servers.remote_primitive(
+        lang=lang, keyset=keyset_2, primitive_class=aead.Aead)
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+
+    ciphertext = primitive.encrypt(plaintext, associated_data)
+    ciphertext_2 = primitive_2.encrypt(plaintext, associated_data)
+
+    # Can be decrypted by the primtive that created the ciphertext.
+    self.assertEqual(primitive.decrypt(ciphertext, associated_data), plaintext)
+    self.assertEqual(
+        primitive_2.decrypt(ciphertext_2, associated_data), plaintext)
+
+    # Cannot be decrypted by the other primitive.
+    with self.assertRaises(tink.TinkError):
+      primitive.decrypt(ciphertext_2, associated_data)
+    with self.assertRaises(tink.TinkError):
+      primitive_2.decrypt(ciphertext, associated_data)
+
+  @parameterized.parameters(_unknown_key_uris_test_cases())
+  def test_encrypt_fails_with_unknown_key_uri(self, lang, unknown_key_uri):
+    key_template = aead.aead_key_templates.create_kms_aead_key_template(
+        unknown_key_uri)
+    keyset = testing_servers.new_keyset(lang, key_template)
+    primitive = testing_servers.remote_primitive(
+        lang=lang, keyset=keyset, primitive_class=aead.Aead)
+
+    plaintext = b'plaintext'
+    associated_data = b'associated_data'
+
+    with self.assertRaises(tink.TinkError):
+      primitive.encrypt(plaintext, associated_data)
+
+
+def _kms_envelope_aead_test_cases() -> Iterable[Tuple[str, str, str]]:
+  """Yields (KMS Envelope AEAD template names, encrypt lang, decrypt lang)."""
+  for key_template_name in _KMS_ENVELOPE_AEAD_KEY_TEMPLATES:
+    # Make sure to test languages that support the pritive used for DEK.
+    supported_langs = _SUPPORTED_LANGUAGES_FOR_KMS_ENVELOPE_AEAD
+    for encrypt_lang, decrypt_lang in _get_lang_tuples(supported_langs):
+      yield (key_template_name, encrypt_lang, decrypt_lang)
+
+
+class KmsEnvelopeAeadTest(parameterized.TestCase):
+
+  @parameterized.parameters(_kms_envelope_aead_test_cases())
+  def test_encrypt_decrypt_with_associated_data(
+      self, key_template_name, encrypt_lang, decrypt_lang
+  ):
+    key_template = _KMS_ENVELOPE_AEAD_KEY_TEMPLATES[key_template_name]
+    # Use the encryption language to generate the keyset proto.
+    keyset = testing_servers.new_keyset(encrypt_lang, key_template)
+    encrypt_primitive = testing_servers.remote_primitive(
+        encrypt_lang, keyset, aead.Aead)
+    plaintext, associated_data = _get_plaintext_and_aad(key_template_name,
+                                                        encrypt_primitive.lang)
+    ciphertext = encrypt_primitive.encrypt(plaintext, associated_data)
+
+    # Decrypt.
+    decrypt_primitive = testing_servers.remote_primitive(
+        decrypt_lang, keyset, aead.Aead)
+    output = decrypt_primitive.decrypt(ciphertext, associated_data)
+    self.assertEqual(output, plaintext)
+
+  @parameterized.parameters(_kms_envelope_aead_test_cases())
+  def test_encrypt_decrypt_with_empty_associated_data(
+      self, key_template_name, encrypt_lang, decrypt_lang
+  ):
+    key_template = _KMS_ENVELOPE_AEAD_KEY_TEMPLATES[key_template_name]
+    # Use the encryption language to generate the keyset proto.
+    keyset = testing_servers.new_keyset(encrypt_lang, key_template)
+    encrypt_primitive = testing_servers.remote_primitive(
+        encrypt_lang, keyset, aead.Aead)
+    plaintext = b'plaintext'
+    associated_data = b''
+    ciphertext = encrypt_primitive.encrypt(plaintext, associated_data)
+    decrypt_primitive = testing_servers.remote_primitive(
+        decrypt_lang, keyset, aead.Aead)
+    output = decrypt_primitive.decrypt(ciphertext, associated_data)
+    self.assertEqual(output, plaintext)
+
+  @parameterized.parameters(_kms_envelope_aead_test_cases())
+  def test_decryption_fails_with_wrong_aad(self, key_template_name,
+                                           encrypt_lang, decrypt_lang):
+    key_template = _KMS_ENVELOPE_AEAD_KEY_TEMPLATES[key_template_name]
+    # Use the encryption language to generate the keyset proto.
+    keyset = testing_servers.new_keyset(encrypt_lang, key_template)
+    encrypt_primitive = testing_servers.remote_primitive(
+        encrypt_lang, keyset, aead.Aead)
+    plaintext, associated_data = _get_plaintext_and_aad(key_template_name,
+                                                        encrypt_primitive.lang)
+    ciphertext = encrypt_primitive.encrypt(plaintext, associated_data)
+    decrypt_primitive = testing_servers.remote_primitive(
+        decrypt_lang, keyset, aead.Aead)
+    with self.assertRaises(tink.TinkError, msg='decryption failed'):
+      decrypt_primitive.decrypt(ciphertext, b'wrong aad')
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/mac/BUILD.bazel b/testing/cross_language/mac/BUILD.bazel
new file mode 100644
index 0000000..8df4e56
--- /dev/null
+++ b/testing/cross_language/mac/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@pip_deps//:requirements.bzl", "requirement")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+py_test(
+    name = "hmac_test",
+    srcs = ["hmac_test.py"],
+    deps = [
+        "//tink_config",
+        "//util:testing_servers",
+        "//util:utilities",
+        "@tink_py//tink/testing:keyset_builder",
+        requirement("absl-py"),
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/proto:common_py_pb2",
+        "@tink_py//tink/proto:hmac_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/mac",
+    ],
+)
diff --git a/testing/cross_language/mac/hmac_test.py b/testing/cross_language/mac/hmac_test.py
new file mode 100644
index 0000000..d47a0d6
--- /dev/null
+++ b/testing/cross_language/mac/hmac_test.py
@@ -0,0 +1,285 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import random
+
+from absl.testing import absltest
+from absl.testing import parameterized
+import tink
+
+from tink.proto import common_pb2
+from tink.proto import hmac_pb2
+from tink.proto import tink_pb2
+import tink_config
+from util import testing_servers
+
+
+def setUpModule():
+  tink.mac.register()
+  testing_servers.start('aes_ctr_hmac_streaming_key_test')
+
+
+def tearDownModule():
+  testing_servers.stop()
+
+
+def to_keyset(
+    key: hmac_pb2.HmacKey, output_prefix_type: tink_pb2.OutputPrefixType
+) -> tink_pb2.Keyset:
+  """Embeds a HmacKey with the output_prefix_type in a keyset."""
+  return tink_pb2.Keyset(
+      primary_key_id=1234,
+      key=[
+          tink_pb2.Keyset.Key(
+              key_data=tink_pb2.KeyData(
+                  type_url='type.googleapis.com/google.crypto.tink.HmacKey',
+                  value=key.SerializeToString(),
+                  key_material_type='SYMMETRIC',
+              ),
+              output_prefix_type=output_prefix_type,
+              status=tink_pb2.KeyStatusType.ENABLED,
+              key_id=1234,
+          )
+      ],
+  )
+
+
+def valid_keys():
+  return [
+      # Try SHA1 tag sizes
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=10
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=15
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=20
+          ),
+      ),
+      # Try SHA1 key sizes
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=10
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(17),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=10
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(30),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=10
+          ),
+      ),
+      # Very large key
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(1274),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=10
+          ),
+      ),
+      # Different hash functions, min tag & key size.
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA224, tag_size=10
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA256, tag_size=10
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA384, tag_size=10
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA512, tag_size=10
+          ),
+      ),
+  ]
+
+
+def invalid_keys():
+  return [
+      # Short key size
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(9),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=10
+          ),
+      ),
+      # Too short tag
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=9
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA224, tag_size=9
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA256, tag_size=9
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA384, tag_size=9
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA512, tag_size=9
+          ),
+      ),
+      # Too long tag
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA1, tag_size=21
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA224, tag_size=29
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA256, tag_size=33
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA384, tag_size=49
+          ),
+      ),
+      hmac_pb2.HmacKey(
+          version=0,
+          key_value=os.urandom(16),
+          params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA512, tag_size=65
+          ),
+      ),
+  ]
+
+
+def valid_lang_and_key():
+  for lang in tink_config.supported_languages_for_key_type('HmacKey'):
+    for key in valid_keys():
+      yield (lang, key)
+
+
+def consistency_test_cases():
+  for lang1 in tink_config.supported_languages_for_key_type('HmacKey'):
+    for lang2 in tink_config.supported_languages_for_key_type('HmacKey'):
+      for key in valid_keys():
+        for output_prefix_type in [tink_pb2.OutputPrefixType.TINK,
+                                   tink_pb2.OutputPrefixType.LEGACY,
+                                   tink_pb2.OutputPrefixType.RAW,
+                                   tink_pb2.OutputPrefixType.CRUNCHY]:
+          yield (lang1, lang2, key, output_prefix_type)
+
+
+def invalid_lang_and_key():
+  for lang in tink_config.supported_languages_for_key_type('HmacKey'):
+    for key in invalid_keys():
+      yield (lang, key)
+
+
+class HmacKeyTest(parameterized.TestCase):
+  """Tests specific for keys of type HmacKey."""
+
+  @parameterized.parameters(valid_lang_and_key())
+  def test_create_mac(
+      self, lang: str, key: hmac_pb2.HmacKey
+  ):
+    keyset = to_keyset(key, tink_pb2.OutputPrefixType.TINK)
+    testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), tink.mac.Mac
+    )
+
+  @parameterized.parameters(consistency_test_cases())
+  def test_compute_mac_lang1_lang2(
+      self,
+      lang1: str,
+      lang2: str,
+      key: hmac_pb2.HmacKey,
+      output_prefix_type: tink_pb2.OutputPrefixType,
+  ):
+    keyset = to_keyset(key, output_prefix_type)
+    mac1 = testing_servers.remote_primitive(
+        lang1, keyset.SerializeToString(), tink.mac.Mac
+    )
+    mac2 = testing_servers.remote_primitive(
+        lang2, keyset.SerializeToString(), tink.mac.Mac
+    )
+    message = os.urandom(random.choice([0, 1, 17, 31, 1027]))
+    mac2.verify_mac(mac1.compute_mac(message), message)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/mac_test.py b/testing/cross_language/mac_test.py
index d6b3178..b3a3121 100644
--- a/testing/cross_language/mac_test.py
+++ b/testing/cross_language/mac_test.py
@@ -23,8 +23,9 @@
 
 from tink.proto import tink_pb2
 from tink.testing import keyset_builder
-from util import supported_key_types
+import tink_config
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['mac']
 
@@ -52,58 +53,36 @@
 }
 
 
-def mac_key_template_names() -> Iterable[str]:
-  for key_type in supported_key_types.MAC_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-  for key_template_name in _ADDITIONAL_KEY_TEMPLATES:
-    yield key_template_name
-
-
 class MacTest(parameterized.TestCase):
 
-  @parameterized.parameters(mac_key_template_names())
+  @parameterized.parameters([
+      *utilities.tinkey_template_names_for(mac.Mac),
+      *_ADDITIONAL_KEY_TEMPLATES.keys()
+  ])
   def test_compute_verify_mac(self, key_template_name):
     if key_template_name in _ADDITIONAL_KEY_TEMPLATES:
       key_template, key_type = _ADDITIONAL_KEY_TEMPLATES[
           key_template_name]
-      supported_langs = supported_key_types.SUPPORTED_LANGUAGES[key_type]
+      supported_langs = tink_config.supported_languages_for_key_type(key_type)
     else:
-      key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+      key_template = utilities.KEY_TEMPLATE[key_template_name]
       supported_langs = (
-          supported_key_types
-          .SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name])
+          utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[key_template_name])
     self.assertNotEmpty(supported_langs)
     # Take the first supported language to generate the keyset.
     keyset = testing_servers.new_keyset(supported_langs[0], key_template)
-    supported_macs = [
-        testing_servers.mac(lang, keyset) for lang in supported_langs
-    ]
-    unsupported_macs = [
-        testing_servers.mac(lang, keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
-    for p in supported_macs:
+    supported_macs = {}
+    for lang in supported_langs:
+      supported_macs[lang] = testing_servers.remote_primitive(
+          lang, keyset, mac.Mac)
+    for lang, p in supported_macs.items():
       data = (
           b'This is some data to be authenticated using key_template '
           b'%s in %s.' % (key_template_name.encode('utf8'),
-                          p.lang.encode('utf8')))
+                          lang.encode('utf8')))
       mac_value = p.compute_mac(data)
-      for p2 in supported_macs:
+      for _, p2 in supported_macs.items():
         self.assertIsNone(p2.verify_mac(mac_value, data))
-      for p2 in unsupported_macs:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='Language %s supports verify_mac with %s unexpectedly' %
-            (p2.lang, key_template_name)):
-          p2.verify_mac(mac_value, data)
-    for p in unsupported_macs:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports compute_mac with %s unexpectedly' %
-          (p.lang, key_template_name)):
-        p.compute_mac(data)
 
 
 # If the implementations work fine for keysets with single keys, then key
@@ -139,19 +118,27 @@
     builder = keyset_builder.new_keyset_builder()
     older_key_id = builder.add_new_key(old_key_tmpl)
     builder.set_primary_key(older_key_id)
-    compute_mac1 = testing_servers.mac(compute_lang, builder.keyset())
-    verify_mac1 = testing_servers.mac(verify_lang, builder.keyset())
+    compute_mac1 = testing_servers.remote_primitive(compute_lang,
+                                                    builder.keyset(), mac.Mac)
+    verify_mac1 = testing_servers.remote_primitive(verify_lang,
+                                                   builder.keyset(), mac.Mac)
     newer_key_id = builder.add_new_key(new_key_tmpl)
-    compute_mac2 = testing_servers.mac(compute_lang, builder.keyset())
-    verify_mac2 = testing_servers.mac(verify_lang, builder.keyset())
+    compute_mac2 = testing_servers.remote_primitive(compute_lang,
+                                                    builder.keyset(), mac.Mac)
+    verify_mac2 = testing_servers.remote_primitive(verify_lang,
+                                                   builder.keyset(), mac.Mac)
 
     builder.set_primary_key(newer_key_id)
-    compute_mac3 = testing_servers.mac(compute_lang, builder.keyset())
-    verify_mac3 = testing_servers.mac(verify_lang, builder.keyset())
+    compute_mac3 = testing_servers.remote_primitive(compute_lang,
+                                                    builder.keyset(), mac.Mac)
+    verify_mac3 = testing_servers.remote_primitive(verify_lang,
+                                                   builder.keyset(), mac.Mac)
 
     builder.disable_key(older_key_id)
-    compute_mac4 = testing_servers.mac(compute_lang, builder.keyset())
-    verify_mac4 = testing_servers.mac(verify_lang, builder.keyset())
+    compute_mac4 = testing_servers.remote_primitive(compute_lang,
+                                                    builder.keyset(), mac.Mac)
+    verify_mac4 = testing_servers.remote_primitive(verify_lang,
+                                                   builder.keyset(), mac.Mac)
 
     self.assertNotEqual(older_key_id, newer_key_id)
     # 1 uses the older key. So 1, 2 and 3 can verify the mac, but not 4.
diff --git a/testing/cross_language/prf_set_test.py b/testing/cross_language/prf_set_test.py
index 0fa5354..00c7870 100644
--- a/testing/cross_language/prf_set_test.py
+++ b/testing/cross_language/prf_set_test.py
@@ -13,8 +13,6 @@
 # limitations under the License.
 """Cross-language tests for the PrfSet primitive."""
 
-from typing import Iterable
-
 from absl.testing import absltest
 from absl.testing import parameterized
 
@@ -22,8 +20,8 @@
 from tink import prf
 
 from tink.testing import keyset_builder
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['prf']
 
@@ -32,25 +30,17 @@
 ]
 
 
-def all_prf_key_template_names() -> Iterable[str]:
-  """Yields all PRF key template names."""
-  for key_type in supported_key_types.PRF_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-
-
 def all_prf_key_template_names_with_some_output_length():
   """Yields (prf_key_template_name, output_length) tuples."""
-  for key_type in supported_key_types.PRF_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      for output_length in OUTPUT_LENGTHS:
-        yield (key_template_name, output_length)
+  for key_template_name in utilities.tinkey_template_names_for(prf.PrfSet):
+    for output_length in OUTPUT_LENGTHS:
+      yield (key_template_name, output_length)
 
 
 def gen_keyset(key_template_name: str) -> bytes:
   builder = keyset_builder.new_keyset_builder()
   primary_key_id = builder.add_new_key(
-      supported_key_types.KEY_TEMPLATE[key_template_name])
+      utilities.KEY_TEMPLATE[key_template_name])
   builder.set_primary_key(primary_key_id)
   return builder.keyset()
 
@@ -74,33 +64,16 @@
 
 class PrfSetPythonTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_prf_key_template_names())
-  def test_unsupported(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
-        key_template_name]
-    self.assertNotEmpty(supported_langs)
-    keyset = gen_keyset(key_template_name)
-    unsupported_languages = [
-        lang for lang in SUPPORTED_LANGUAGES if lang not in supported_langs
-    ]
-    for lang in unsupported_languages:
-      p = testing_servers.prf_set(lang, keyset)
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports PRF compute with %s unexpectedly' %
-          (p.lang, key_template_name)):
-        p.primary().compute(b'input_data', output_length=16)
-
-  @parameterized.parameters(all_prf_key_template_names())
+  @parameterized.parameters(utilities.tinkey_template_names_for(prf.PrfSet))
   def test_supported(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     self.assertNotEmpty(supported_langs)
     keyset = gen_keyset(key_template_name)
     input_data = b'This is some input data.'
     outputs = []
     for lang in supported_langs:
-      p = testing_servers.prf_set(lang, keyset)
+      p = testing_servers.remote_primitive(lang, keyset, prf.PrfSet)
       outputs.append(p.primary().compute(input_data, 16))
     self.assertLen(outputs, len(supported_langs))
     self.assertLen(outputs[0], 16)
@@ -110,7 +83,7 @@
       all_prf_key_template_names_with_some_output_length())
   def test_compute_consistent_for_output_length(self, key_template_name,
                                                 output_length):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     # This test checks that for a given output_length, either all
     # implementations fail or all produce the same value.
@@ -121,7 +94,7 @@
     outputs = {}
     for lang in supported_langs:
       try:
-        p = testing_servers.prf_set(lang, keyset)
+        p = testing_servers.remote_primitive(lang, keyset, prf.PrfSet)
         outputs[lang] = p.primary().compute(input_data, output_length)
       except tink.TinkError as e:
         errors[lang] = e
@@ -137,7 +110,7 @@
     keyset = gen_keyset_with_2_prfs()
     input_data = b'This is some input data.'
     output_length = 15
-    p = testing_servers.prf_set(lang, keyset)
+    p = testing_servers.remote_primitive(lang, keyset, prf.PrfSet)
     primary_output = p.primary().compute(input_data, output_length)
     primary_id = p.primary_id()
     all_outputs = {
diff --git a/testing/cross_language/primitive_creation_test.py b/testing/cross_language/primitive_creation_test.py
new file mode 100644
index 0000000..f0e1f29
--- /dev/null
+++ b/testing/cross_language/primitive_creation_test.py
@@ -0,0 +1,231 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Primitive Creation consistency tests."""
+
+from typing import Any
+
+from absl.testing import absltest
+from absl.testing import parameterized
+import tink
+from tink import aead
+from tink import daead
+from tink import hybrid
+from tink import jwt
+from tink import mac
+from tink import prf
+from tink import signature
+from tink import streaming_aead
+
+from tink.proto import tink_pb2
+import tink_config
+from util import test_keys
+from util import testing_servers
+from util import utilities
+
+# We register the primitives here because creation of the keysets happens
+# before "setUpModule" is called.
+aead.register()
+daead.register()
+jwt.register_jwt_mac()
+jwt.register_jwt_signature()
+mac.register()
+hybrid.register()
+prf.register()
+signature.register()
+streaming_aead.register()
+
+
+def setUpModule():
+  testing_servers.start('primitive_creation')
+
+
+def tearDownModule():
+  testing_servers.stop()
+
+
+def single_key_keysets():
+  """Produces single key keysets which can be produced from a template.
+
+  This does not produce keysets which have public keys.
+  Yields:
+    valid keysets generated from templates
+  """
+  for _, template in utilities.KEY_TEMPLATE.items():
+    yield test_keys.new_or_stored_keyset(template)
+
+
+def named_testcases():
+  case_num = 0
+  for lang in utilities.ALL_LANGUAGES:
+    for keyset in single_key_keysets():
+      for primitive in tink_config.all_primitives():
+        yield {
+            'testcase_name':
+                str(case_num) + '-' + lang + '-' + primitive.__name__ + '-' +
+                utilities.key_types_in_keyset(keyset)[0],
+            'lang':
+                lang,
+            'primitive':
+                primitive,
+            'keyset':
+                keyset,
+        }
+        case_num += 1
+
+
+def _is_b243759652_test_case(lang: str, keyset: bytes, primitive: Any) -> bool:
+  """Returns whether the test case falls under b/243759652.
+
+  When calling hybrid.NewHybridDecrypt or hybrid.NewHybridEncrypt, Tink asks
+  each key manager to create a primitive (whose type is fixed for each key
+  manager). Because of duck-typing, if the key manager returns an Aead, Tink
+  happily carries on in case it wants a HybridEncrypt/HybridDecrypt.
+
+  Args:
+    lang: A string describing the language.
+    keyset: A serialized keyset
+    primitive: One of the primitives
+
+  Returns:
+    True iff this test case falls under b/243759652.
+  """
+  # The bug only exists in go.
+  if lang != 'go':
+    return False
+  # The bug only happens if we create a HybridEncrypt or a HybridDecrypt
+  if primitive not in [tink.hybrid.HybridDecrypt, tink.hybrid.HybridEncrypt]:
+    return False
+
+  keytypes = utilities.key_types_in_keyset(keyset)
+  primitives = [tink_config.primitive_for_keytype(k) for k in keytypes]
+  # For the bug to occur, we must only at least one AEAD keytype (as it only
+  # happens if at least one key type should *not* work).
+  if not any(p == aead.Aead for p in primitives):
+    return False
+  # For the bug to occur, all key types must be either for 'primitive' or
+  # for Aead (otherwise primitive creation fails).
+  if not all(p == aead.Aead or p == primitive for p in primitives):
+    return False
+  # For the bug to occur, we must not have an AesEaxKey: these are unsupported
+  # in go, and so if we have them, primitive creation fails.
+  if any(k == 'AesEaxKey' for k in keytypes):
+    return False
+  return True
+
+
+class SupportedKeyTypesTest(parameterized.TestCase):
+  """Tests if creation of primitives succeeds as described in tink_config.
+
+  This test tries to see if creation of primitives is consistent with what is
+  configured in tink_config._key_types. For this, we enumerate as many triples
+  (lang, keyset, primitive) as possible, and compute, for each of them, the
+  expected result from the config, and the actual result by creating the
+  primitive.
+  """
+
+  @parameterized.named_parameters(named_testcases())
+  def test_create(self, lang: str, keyset: bytes, primitive: Any):
+    """Tests primitive creation (see top level comment).
+
+    This test should pass for every keyset, as long as the keyset can be
+    correctly parsed.
+
+    Args:
+      lang: The language to test
+      keyset: A byte string representing a keyset. The keyset needs to be valid.
+      primitive: The primitive to try and instantiate
+    """
+    keytypes = utilities.key_types_in_keyset(keyset)
+    keytype = keytypes[0]
+
+    if _is_b243759652_test_case(lang, keyset, primitive):
+      # TODO(b/243759652): This should raise a TinkError, but doesn't
+      _ = testing_servers.remote_primitive(lang, keyset, primitive)
+      return
+
+    if (lang in tink_config.supported_languages_for_key_type(keytype) and
+        primitive == tink_config.primitive_for_keytype(keytype)):
+      _ = testing_servers.remote_primitive(lang, keyset, primitive)
+    else:
+      with self.assertRaises(tink.TinkError):
+        _ = testing_servers.remote_primitive(lang, keyset, primitive)
+
+  @parameterized.named_parameters(named_testcases())
+  def test_create_with_public_keyset(self, lang: str, keyset: bytes,
+                                     primitive: Any):
+    """Tests primitive creation, after getting a public keyset.
+
+    It would be somewhat better if the test cases above produce all keysets --
+    however, this currently doesn't happen.
+
+    Args:
+      lang: the language to use
+      keyset: the serialized keyset, must be valid
+      primitive: the primitive to test
+    """
+    try:
+      public_keyset = testing_servers.public_keyset(lang, keyset)
+    except tink.TinkError:
+      self.skipTest('Cannot get the public keyset')
+
+    keytypes = utilities.key_types_in_keyset(public_keyset)
+    self.assertLen(keytypes, 1)
+    keytype = keytypes[0]
+
+    if _is_b243759652_test_case(lang, public_keyset, primitive):
+      # TODO(b/243759652): This should raise a TinkError, but doesn't
+      _ = testing_servers.remote_primitive(lang, public_keyset, primitive)
+      return
+
+    if (lang in tink_config.supported_languages_for_key_type(keytype) and
+        primitive == tink_config.primitive_for_keytype(keytype)):
+      _ = testing_servers.remote_primitive(lang, public_keyset, primitive)
+    else:
+      with self.assertRaises(tink.TinkError):
+        _ = testing_servers.remote_primitive(lang, public_keyset, primitive)
+
+  @parameterized.named_parameters(named_testcases())
+  def test_create_with_key_id_0(self, lang: str, keyset: bytes, primitive: Any):
+    """Tests primitive creation when key ID is 0.
+
+    Args:
+      lang: The language to test
+      keyset: A byte string representing a keyset. The keyset needs to be valid.
+      primitive: The primitive to try and instantiate
+    """
+    keyset_proto = tink_pb2.Keyset.FromString(keyset)
+    for key in keyset_proto.key:
+      if key.key_id == keyset_proto.primary_key_id:
+        key.key_id = 0
+    keyset_proto.primary_key_id = 0
+    modified_keyset = keyset_proto.SerializeToString()
+
+    keytypes = utilities.key_types_in_keyset(keyset)
+    keytype = keytypes[0]
+
+    if _is_b243759652_test_case(lang, modified_keyset, primitive):
+      # TODO(b/243759652): This should raise a TinkError, but doesn't
+      _ = testing_servers.remote_primitive(lang, modified_keyset, primitive)
+      return
+
+    if (lang in tink_config.supported_languages_for_key_type(keytype) and
+        primitive == tink_config.primitive_for_keytype(keytype)):
+      _ = testing_servers.remote_primitive(lang, modified_keyset, primitive)
+    else:
+      with self.assertRaises(tink.TinkError):
+        _ = testing_servers.remote_primitive(lang, modified_keyset, primitive)
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/proto/testing_api.proto b/testing/cross_language/proto/testing_api.proto
deleted file mode 100644
index 2fb0e9e..0000000
--- a/testing/cross_language/proto/testing_api.proto
+++ /dev/null
@@ -1,503 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-syntax = "proto3";
-
-package tink_testing_api;
-
-import "google/protobuf/duration.proto";
-import "google/protobuf/timestamp.proto";
-import "google/protobuf/wrappers.proto";
-
-option java_package = "com.google.crypto.tink.testing.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
-// Placeholder for java_stubby_library
-
-// Service providing metadata about the server.
-service Metadata {
-  // Returns some server information. A test may use this information to verify
-  // that it is talking to the right server.
-  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
-}
-
-message ServerInfoRequest {}
-
-message ServerInfoResponse {
-  string tink_version = 1;  // For example '1.4'
-  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
-}
-
-// Service for Keyset operations.
-service Keyset {
-  // Generates a key template from a key template name.
-  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
-  // Generates a new keyset from a template.
-  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
-  // Generates a public-key keyset from a private-key keyset.
-  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
-  // Converts a Keyset from Binary to Json Format
-  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
-  // Converts a Keyset from Json to Binary Format
-  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
-  // Reads an encrypted keyset using KeysetHandle.read() or
-  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
-  rpc ReadEncrypted(KeysetReadEncryptedRequest)
-      returns (KeysetReadEncryptedResponse) {}
-  // Writes an encrypted keyset using KeysetHandle.write() or
-  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
-  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
-      returns (KeysetWriteEncryptedResponse) {}
-}
-
-message KeysetTemplateRequest {
-  string template_name = 1;  // template name used by Tinkey
-}
-
-message KeysetTemplateResponse {
-  oneof result {
-    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
-    string err = 2;
-  }
-}
-
-message KeysetGenerateRequest {
-  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
-}
-
-message KeysetGenerateResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetPublicRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetPublicResponse {
-  oneof result {
-    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetToJsonRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetToJsonResponse {
-  oneof result {
-    string json_keyset = 1;
-    string err = 2;
-  }
-}
-
-message KeysetFromJsonRequest {
-  string json_keyset = 1;
-}
-
-message KeysetFromJsonResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-// Copy of google.protobuf.BytesValue
-message BytesValue {
-  // The bytes value.
-  bytes value = 1;
-}
-
-enum KeysetReaderType {
-  KEYSET_READER_UNKNOWN = 0;
-  KEYSET_READER_BINARY = 1;
-  KEYSET_READER_JSON = 2;
-}
-
-message KeysetReadEncryptedRequest {
-  bytes encrypted_keyset = 1;
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetReaderType keyset_reader_type = 4;
-}
-
-message KeysetReadEncryptedResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-enum KeysetWriterType {
-  KEYSET_WRITER_UNKNOWN = 0;
-  KEYSET_WRITER_BINARY = 1;
-  KEYSET_WRITER_JSON = 2;
-}
-
-message KeysetWriteEncryptedRequest {
-  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetWriterType keyset_writer_type = 4;
-}
-
-message KeysetWriteEncryptedResponse {
-  oneof result {
-    bytes encrypted_keyset = 1;
-    string err = 2;
-  }
-}
-
-// Service for AEAD encryption and decryption
-service Aead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
-}
-
-message AeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message AeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Deterministic AEAD encryption and decryption
-service DeterministicAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
-      returns (DeterministicAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
-      returns (DeterministicAeadDecryptResponse) {}
-}
-
-message DeterministicAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message DeterministicAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Streaming AEAD encryption and decryption
-service StreamingAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(StreamingAeadEncryptRequest)
-      returns (StreamingAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(StreamingAeadDecryptRequest)
-      returns (StreamingAeadDecryptResponse) {}
-}
-
-message StreamingAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message StreamingAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to compute and verify MACs
-service Mac {
-  // Computes a MAC for given data
-  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
-  // Verifies the validity of the MAC value, no error means success
-  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
-}
-
-message ComputeMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message ComputeMacResponse {
-  oneof result {
-    bytes mac_value = 1;
-    string err = 2;
-  }
-}
-
-message VerifyMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes mac_value = 2;
-  bytes data = 3;
-}
-
-message VerifyMacResponse {
-  string err = 1;
-}
-
-// Service to hybrid encrypt and decrypt
-service Hybrid {
-  // Encrypts plaintext binding context_info to the resulting ciphertext
-  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
-  // Decrypts ciphertext verifying the integrity of context_info
-  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
-}
-
-message HybridEncryptRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes context_info = 3;
-}
-
-message HybridEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message HybridDecryptRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes context_info = 3;
-}
-
-message HybridDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to sign and verify signatures.
-service Signature {
-  // Computes the signature for data
-  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
-  // Verifies that signature is a digital signature for data
-  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
-}
-
-message SignatureSignRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message SignatureSignResponse {
-  oneof result {
-    bytes signature = 1;
-    string err = 2;
-  }
-}
-
-message SignatureVerifyRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes signature = 2;
-  bytes data = 3;
-}
-
-message SignatureVerifyResponse {
-  string err = 1;
-}
-
-// Service for PrfSet computation
-service PrfSet {
-  // Returns the key ids and the primary key id in the keyset.
-  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
-  // Computes the output of the PRF with the given key_id in the PrfSet.
-  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
-}
-
-message PrfSetKeyIdsRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message PrfSetKeyIdsResponse {
-  message Output {
-    uint32 primary_key_id = 1;
-    repeated uint32 key_id = 2;
-  }
-  oneof result {
-    Output output = 1;
-    string err = 2;
-  }
-}
-
-message PrfSetComputeRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  uint32 key_id = 2;
-  bytes input_data = 3;
-  int32 output_length = 4;
-}
-
-message PrfSetComputeResponse {
-  oneof result {
-    bytes output = 1;
-    string err = 2;
-  }
-}
-
-// Service for JSON Web Tokens (JWT)
-service Jwt {
-  // Computes a signed compact JWT token.
-  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Computes a signed compact JWT token.
-  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Converts a Keyset from Tink Binary to JWK Set Format
-  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
-  // Converts a Keyset from JWK Set to Tink Binary Format
-  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
-}
-
-//  Used to represent the JSON null value.
-enum NullValue {
-  NULL_VALUE = 0;
-}
-
-message JwtClaimValue {
-  oneof kind {
-    NullValue null_value = 2;
-    double number_value = 3;
-    string string_value = 4;
-    bool bool_value = 5;
-    string json_object_value = 6;
-    string json_array_value = 7;
-  }
-}
-
-message JwtToken {
-  google.protobuf.StringValue issuer = 1;
-  google.protobuf.StringValue subject = 2;
-  repeated string audiences = 3;
-  google.protobuf.StringValue jwt_id = 4;
-  google.protobuf.Timestamp expiration = 5;
-  google.protobuf.Timestamp not_before = 6;
-  google.protobuf.Timestamp issued_at = 7;
-  map<string, JwtClaimValue> custom_claims = 8;
-  google.protobuf.StringValue type_header = 9;
-}
-
-message JwtValidator {
-  google.protobuf.StringValue expected_type_header = 7;
-  google.protobuf.StringValue expected_issuer = 1;
-  google.protobuf.StringValue expected_audience = 3;
-  bool ignore_type_header = 8;
-  bool ignore_issuer = 9;
-  bool ignore_audience = 11;
-  bool allow_missing_expiration = 12;
-  bool expect_issued_in_the_past = 13;
-  google.protobuf.Timestamp now = 5;
-  google.protobuf.Duration clock_skew = 6;
-}
-
-message JwtSignRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  JwtToken raw_jwt = 2;
-}
-
-message JwtSignResponse {
-  oneof result {
-    string signed_compact_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtVerifyRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  string signed_compact_jwt = 2;
-  JwtValidator validator = 3;
-}
-
-message JwtVerifyResponse {
-  oneof result {
-    JwtToken verified_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtToJwkSetRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message JwtToJwkSetResponse {
-  oneof result {
-    string jwk_set = 1;
-    string err = 2;
-  }
-}
-
-message JwtFromJwkSetRequest {
-  string jwk_set = 1;
-}
-
-message JwtFromJwkSetResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
diff --git a/testing/cross_language/proto/BUILD.bazel b/testing/cross_language/protos/BUILD.bazel
similarity index 100%
rename from testing/cross_language/proto/BUILD.bazel
rename to testing/cross_language/protos/BUILD.bazel
diff --git a/testing/cross_language/protos/testing_api.proto b/testing/cross_language/protos/testing_api.proto
new file mode 100644
index 0000000..7a02517
--- /dev/null
+++ b/testing/cross_language/protos/testing_api.proto
@@ -0,0 +1,566 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+syntax = "proto3";
+
+package tink_testing_api;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+option java_package = "com.google.crypto.tink.testing.proto";
+option java_multiple_files = true;
+option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
+// Placeholder for java_stubby_library
+
+// Service providing metadata about the server.
+service Metadata {
+  // Returns some server information. A test may use this information to verify
+  // that it is talking to the right server.
+  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
+}
+
+message ServerInfoRequest {}
+
+message ServerInfoResponse {
+  string tink_version = 1;  // For example '1.4'
+  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
+}
+
+// Service for Keyset operations.
+service Keyset {
+  // Generates a key template from a key template name.
+  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
+  // Generates a new keyset from a template.
+  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
+  // Generates a public-key keyset from a private-key keyset.
+  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
+  // Converts a Keyset from Binary to Json Format
+  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
+  // Converts a Keyset from Json to Binary Format
+  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
+  // Reads an encrypted keyset using KeysetHandle.read() or
+  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
+  rpc ReadEncrypted(KeysetReadEncryptedRequest)
+      returns (KeysetReadEncryptedResponse) {}
+  // Writes an encrypted keyset using KeysetHandle.write() or
+  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
+  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
+      returns (KeysetWriteEncryptedResponse) {}
+}
+
+message KeysetTemplateRequest {
+  string template_name = 1;  // template name used by Tinkey
+}
+
+message KeysetTemplateResponse {
+  oneof result {
+    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
+    string err = 2;
+  }
+}
+
+message KeysetGenerateRequest {
+  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
+}
+
+message KeysetGenerateResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetPublicRequest {
+  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetPublicResponse {
+  oneof result {
+    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetToJsonRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetToJsonResponse {
+  oneof result {
+    string json_keyset = 1;
+    string err = 2;
+  }
+}
+
+message KeysetFromJsonRequest {
+  string json_keyset = 1;
+}
+
+message KeysetFromJsonResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+// Copy of google.protobuf.BytesValue
+message BytesValue {
+  // The bytes value.
+  bytes value = 1;
+}
+
+enum KeysetReaderType {
+  KEYSET_READER_UNKNOWN = 0;
+  KEYSET_READER_BINARY = 1;
+  KEYSET_READER_JSON = 2;
+}
+
+message KeysetReadEncryptedRequest {
+  bytes encrypted_keyset = 1;
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetReaderType keyset_reader_type = 4;
+}
+
+message KeysetReadEncryptedResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+enum KeysetWriterType {
+  KEYSET_WRITER_UNKNOWN = 0;
+  KEYSET_WRITER_BINARY = 1;
+  KEYSET_WRITER_JSON = 2;
+}
+
+message KeysetWriteEncryptedRequest {
+  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetWriterType keyset_writer_type = 4;
+}
+
+message KeysetWriteEncryptedResponse {
+  oneof result {
+    bytes encrypted_keyset = 1;
+    string err = 2;
+  }
+}
+
+message AnnotatedKeyset {
+  bytes serialized_keyset = 1;  // serialized google.crypto.tink.Keyset.
+  map<string, string> annotations = 2;
+}
+
+message CreationRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message CreationResponse {
+  // Empty means no error
+  string err = 1;
+}
+
+// Service for AEAD encryption and decryption
+service Aead {
+  // Creates an Aead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
+}
+
+message AeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message AeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Deterministic AEAD encryption and decryption
+service DeterministicAead {
+  // Creates a Deterministic AEAD object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
+      returns (DeterministicAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
+      returns (DeterministicAeadDecryptResponse) {}
+}
+
+message DeterministicAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message DeterministicAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Streaming AEAD encryption and decryption
+service StreamingAead {
+  // Creates a StreamingAead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(StreamingAeadEncryptRequest)
+      returns (StreamingAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(StreamingAeadDecryptRequest)
+      returns (StreamingAeadDecryptResponse) {}
+}
+
+message StreamingAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message StreamingAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to compute and verify MACs
+service Mac {
+  // Creates a Mac object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Computes a MAC for given data. The client must call "Create" first to see
+  // if creation succeeds before calling this.
+  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
+  // Verifies the validity of the MAC value, no error means success. The client
+  // must call "Create" first to see if creation succeeds before calling this.
+  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
+}
+
+message ComputeMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message ComputeMacResponse {
+  oneof result {
+    bytes mac_value = 1;
+    string err = 2;
+  }
+}
+
+message VerifyMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes mac_value = 2;
+  bytes data = 3;
+}
+
+message VerifyMacResponse {
+  string err = 1;
+}
+
+// Service to hybrid encrypt and decrypt
+service Hybrid {
+  // Creates a HybridEncrypt object without using it.
+  rpc CreateHybridEncrypt(CreationRequest) returns (CreationResponse) {}
+  // Creates a HybridDecrypt object without using it.
+  rpc CreateHybridDecrypt(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts plaintext binding context_info to the resulting ciphertext. The
+  // client must call "CreateHybridEncrypt" first to see if creation succeeds
+  // before calling this.
+  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
+  // Decrypts ciphertext verifying the integrity of context_info. The client
+  // must call "CreateHybridDecrypt" first to see if creation succeeds before
+  // calling this.
+  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
+}
+
+message HybridEncryptRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes context_info = 3;
+}
+
+message HybridEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message HybridDecryptRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes context_info = 3;
+}
+
+message HybridDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to sign and verify signatures.
+service Signature {
+  // Creates a PublicKeySign object without using it.
+  rpc CreatePublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a PublicKeyVerify object without using it.
+  rpc CreatePublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes the signature for data. The client must call "CreatePublicKeySign"
+  // first to see if creation succeeds before calling this.
+  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
+  // Verifies that signature is a digital signature for data. The client must
+  // call "CreatePublicKeyVerify" first to see if creation succeeds before
+  // calling this.
+  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
+}
+
+message SignatureSignRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message SignatureSignResponse {
+  oneof result {
+    bytes signature = 1;
+    string err = 2;
+  }
+}
+
+message SignatureVerifyRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes signature = 2;
+  bytes data = 3;
+}
+
+message SignatureVerifyResponse {
+  string err = 1;
+}
+
+// Service for PrfSet computation
+service PrfSet {
+  // Creates a PrfSet object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Returns the key ids and the primary key id in the keyset.The client must
+  // call "Create" first to see if creation succeeds before calling this.
+  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
+  // Computes the output of the PRF with the given key_id in the PrfSet.The
+  // client must call "Create" first to see if creation succeeds before calling
+  // this.
+  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
+}
+
+message PrfSetKeyIdsRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message PrfSetKeyIdsResponse {
+  message Output {
+    uint32 primary_key_id = 1;
+    repeated uint32 key_id = 2;
+  }
+  oneof result {
+    Output output = 1;
+    string err = 2;
+  }
+}
+
+message PrfSetComputeRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  uint32 key_id = 2;
+  bytes input_data = 3;
+  int32 output_length = 4;
+}
+
+message PrfSetComputeResponse {
+  oneof result {
+    bytes output = 1;
+    string err = 2;
+  }
+}
+
+// Service for JSON Web Tokens (JWT)
+service Jwt {
+  // Creates a JwtMac object without using it.
+  rpc CreateJwtMac(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeySign object without using it.
+  rpc CreateJwtPublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeyVerify object without using it.
+  rpc CreateJwtPublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes a signed compact JWT token.
+  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Computes a signed compact JWT token.
+  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Converts a Keyset from Tink Binary to JWK Set Format
+  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
+  // Converts a Keyset from JWK Set to Tink Binary Format
+  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
+}
+
+//  Used to represent the JSON null value.
+enum NullValue {
+  NULL_VALUE = 0;
+}
+
+message JwtClaimValue {
+  oneof kind {
+    NullValue null_value = 2;
+    double number_value = 3;
+    string string_value = 4;
+    bool bool_value = 5;
+    string json_object_value = 6;
+    string json_array_value = 7;
+  }
+}
+
+message JwtToken {
+  google.protobuf.StringValue issuer = 1;
+  google.protobuf.StringValue subject = 2;
+  repeated string audiences = 3;
+  google.protobuf.StringValue jwt_id = 4;
+  google.protobuf.Timestamp expiration = 5;
+  google.protobuf.Timestamp not_before = 6;
+  google.protobuf.Timestamp issued_at = 7;
+  map<string, JwtClaimValue> custom_claims = 8;
+  google.protobuf.StringValue type_header = 9;
+}
+
+message JwtValidator {
+  google.protobuf.StringValue expected_type_header = 7;
+  google.protobuf.StringValue expected_issuer = 1;
+  google.protobuf.StringValue expected_audience = 3;
+  bool ignore_type_header = 8;
+  bool ignore_issuer = 9;
+  bool ignore_audience = 11;
+  bool allow_missing_expiration = 12;
+  bool expect_issued_in_the_past = 13;
+  google.protobuf.Timestamp now = 5;
+  google.protobuf.Duration clock_skew = 6;
+}
+
+message JwtSignRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  JwtToken raw_jwt = 2;
+}
+
+message JwtSignResponse {
+  oneof result {
+    string signed_compact_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtVerifyRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  string signed_compact_jwt = 2;
+  JwtValidator validator = 3;
+}
+
+message JwtVerifyResponse {
+  oneof result {
+    JwtToken verified_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtToJwkSetRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message JwtToJwkSetResponse {
+  oneof result {
+    string jwk_set = 1;
+    string err = 2;
+  }
+}
+
+message JwtFromJwkSetRequest {
+  string jwk_set = 1;
+}
+
+message JwtFromJwkSetResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
diff --git a/testing/cross_language/signature/BUILD.bazel b/testing/cross_language/signature/BUILD.bazel
new file mode 100644
index 0000000..4ae6523
--- /dev/null
+++ b/testing/cross_language/signature/BUILD.bazel
@@ -0,0 +1,22 @@
+load("@pip_deps//:requirements.bzl", "requirement")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+py_test(
+    name = "rsa_ssa_pkcs1_test",
+    srcs = ["rsa_ssa_pkcs1_test.py"],
+    deps = [
+        requirement("absl-py"),
+        "//tink_config",
+        "//util:testing_servers",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/proto:common_py_pb2",
+        "@tink_py//tink/proto:rsa_ssa_pkcs1_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
+    ],
+)
diff --git a/testing/cross_language/signature/rsa_ssa_pkcs1_test.py b/testing/cross_language/signature/rsa_ssa_pkcs1_test.py
new file mode 100644
index 0000000..6d49df7
--- /dev/null
+++ b/testing/cross_language/signature/rsa_ssa_pkcs1_test.py
@@ -0,0 +1,384 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List, Tuple
+
+from absl.testing import absltest
+from absl.testing import parameterized
+
+import tink
+from tink import signature
+
+from tink.proto import common_pb2
+from tink.proto import rsa_ssa_pkcs1_pb2
+from tink.proto import tink_pb2
+import tink_config
+from util import testing_servers
+
+# 2048-bit modulus of the first test vector in
+# https://github.com/google/wycheproof/blob/master/testvectors/rsa_pkcs1_2048_test.json
+# This modulus uses the minimal two's-complement big-endian encoding, that's
+# why it starts with a zero and has a leading 0 byte and has 257 bytes.
+MODULUS_BYTES = bytes.fromhex(
+    '00b3510a2bcd4ce644c5b594ae5059e12b2f054b658d5da5959a2fdf1871b808'
+    'bc3df3e628d2792e51aad5c124b43bda453dca5cde4bcf28e7bd4effba0cb4b7'
+    '42bbb6d5a013cb63d1aa3a89e02627ef5398b52c0cfd97d208abeb8d7c9bce0b'
+    'beb019a86ddb589beb29a5b74bf861075c677c81d430f030c265247af9d3c914'
+    '0ccb65309d07e0adc1efd15cf17e7b055d7da3868e4648cc3a180f0ee7f8e1e7'
+    'b18098a3391b4ce7161e98d57af8a947e201a463e2d6bbca8059e5706e9dfed8'
+    'f4856465ffa712ed1aa18e888d12dc6aa09ce95ecfca83cc5b0b15db09c8647f'
+    '5d524c0f2e7620a3416b9623cadc0f097af573261c98c8400aa12af38e43cad84d'
+)
+
+# Same as MODULUS_BYTES, but with the least significant byte set to 0.
+# Hence this modulus has 256 as a factor.
+WEIRD_MODULUS_BYTES = bytes.fromhex(
+    '00b3510a2bcd4ce644c5b594ae5059e12b2f054b658d5da5959a2fdf1871b808'
+    'bc3df3e628d2792e51aad5c124b43bda453dca5cde4bcf28e7bd4effba0cb4b7'
+    '42bbb6d5a013cb63d1aa3a89e02627ef5398b52c0cfd97d208abeb8d7c9bce0b'
+    'beb019a86ddb589beb29a5b74bf861075c677c81d430f030c265247af9d3c914'
+    '0ccb65309d07e0adc1efd15cf17e7b055d7da3868e4648cc3a180f0ee7f8e1e7'
+    'b18098a3391b4ce7161e98d57af8a947e201a463e2d6bbca8059e5706e9dfed8'
+    'f4856465ffa712ed1aa18e888d12dc6aa09ce95ecfca83cc5b0b15db09c8647f'
+    '5d524c0f2e7620a3416b9623cadc0f097af573261c98c8400aa12af38e43cad800'
+)
+
+# Same as MODULUS_BYTES, but with the most significant bit set to 0, and
+# the 2nd most significant bit set to 1. So this modulus has 2047 bits.
+SHORT_MODULUS_BYTES = bytes.fromhex(
+    '0073510a2bcd4ce644c5b594ae5059e12b2f054b658d5da5959a2fdf1871b808'
+    'bc3df3e628d2792e51aad5c124b43bda453dca5cde4bcf28e7bd4effba0cb4b7'
+    '42bbb6d5a013cb63d1aa3a89e02627ef5398b52c0cfd97d208abeb8d7c9bce0b'
+    'beb019a86ddb589beb29a5b74bf861075c677c81d430f030c265247af9d3c914'
+    '0ccb65309d07e0adc1efd15cf17e7b055d7da3868e4648cc3a180f0ee7f8e1e7'
+    'b18098a3391b4ce7161e98d57af8a947e201a463e2d6bbca8059e5706e9dfed8'
+    'f4856465ffa712ed1aa18e888d12dc6aa09ce95ecfca83cc5b0b15db09c8647f'
+    '5d524c0f2e7620a3416b9623cadc0f097af573261c98c8400aa12af38e43cad84d'
+)
+
+# big-endian encoding of 4th Fermat number F4 = 65537 = 2^16 + 1
+F4_BYTES = bytes.fromhex('010001')
+
+RSA_SSA_PKCS1_PUBLIC_KEY_TYPE_URL = (
+    'type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PublicKey'
+)
+
+
+def setUpModule():
+  signature.register()
+  testing_servers.start('aes_ctr_hmac_streaming_key_test')
+
+
+def tearDownModule():
+  testing_servers.stop()
+
+
+def public_key_to_keyset(
+    public_key: rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey,
+    output_prefix_type: tink_pb2.OutputPrefixType,
+) -> tink_pb2.Keyset:
+  """Embeds a RsaSsaPkcs1PrivateKey with the output_prefix_type in a keyset."""
+  return tink_pb2.Keyset(
+      primary_key_id=1234,
+      key=[
+          tink_pb2.Keyset.Key(
+              key_data=tink_pb2.KeyData(
+                  type_url=RSA_SSA_PKCS1_PUBLIC_KEY_TYPE_URL,
+                  value=public_key.SerializeToString(),
+                  key_material_type=tink_pb2.KeyData.ASYMMETRIC_PUBLIC,
+              ),
+              output_prefix_type=output_prefix_type,
+              status=tink_pb2.KeyStatusType.ENABLED,
+              key_id=1234,
+          )
+      ],
+  )
+
+
+def valid_public_keys() -> (
+    List[Tuple[str, rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey]]
+):
+  return [
+      (
+          '2048-bit public key with SHA256',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with SHA384',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA384
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with SHA512',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA512
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with SHA1',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with e=2^16+3',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=bytes.fromhex('010003'),
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with e=2^32-1',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=bytes.fromhex('ffffffff'),
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with many leading zeros in the modulus',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=bytes.fromhex('00000000') + MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key without any leading zeros in the modulus',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES[1:],
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with modulus divisible by 256',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=WEIRD_MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+  ]
+
+
+def invalid_public_keys() -> (
+    List[Tuple[str, rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey]]
+):
+  return [
+      (
+          '2048-bit public key with SHA224',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA224
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with small e',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=bytes.fromhex('03'),
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA1
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with 2^16-1',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              e=bytes.fromhex('00ffff'),
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA1
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with an invalid e',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              # This e is even, which is invalid since e must be co-prime
+              # with p-1 and q-1, which both are also even.
+              e=bytes.fromhex('010002'),
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with e=2^32+1',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=MODULUS_BYTES,
+              # BoringSSL (which gets used in C++, Python and Go) rejects values
+              # for e with more than 32 bits.
+              e=bytes.fromhex('0100000001'),
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2047-bit public key',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=0,
+              n=SHORT_MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+      (
+          '2048-bit public key with version 1',
+          rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+              version=1,
+              n=MODULUS_BYTES,
+              e=F4_BYTES,
+              params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+                  hash_type=common_pb2.HashType.SHA256
+              ),
+          ),
+      ),
+  ]
+
+
+def valid_key_testcases():
+  for lang in tink_config.supported_languages_for_key_type(
+      'RsaSsaPkcs1PublicKey'
+  ):
+    for key_desc, key in valid_public_keys():
+      if lang == 'go' and (
+          key_desc == '2048-bit public key with e=2^16+3'
+          or key_desc == '2048-bit public key with e=2^32-1'
+      ):
+        # Go only accepts e = F4 = 2^16 + 1 = 65537. See also b/274605582.
+        continue
+      yield ('%s: %s' % (key_desc, lang), lang, key)
+
+
+def invalid_key_testcases():
+  for lang in tink_config.supported_languages_for_key_type(
+      'RsaSsaPkcs1PublicKey'
+  ):
+    for key_desc, key in invalid_public_keys():
+      if lang == 'java' and key_desc == '2048-bit public key with e=2^32+1':
+        # Java accepts large values for e. See also b/274605582.
+        continue
+      yield ('%s: %s' % (key_desc, lang), lang, key)
+
+
+class RsaSsaPkcs1PublicKeyTest(parameterized.TestCase):
+  """Tests specific for keys of type RsaSsaPkcs1PublicKey."""
+
+  @parameterized.named_parameters(valid_key_testcases())
+  def test_create_signature_verify_with_valid_key_success(
+      self, lang: str, key: rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey
+  ):
+    keyset = public_key_to_keyset(key, tink_pb2.OutputPrefixType.TINK)
+    testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), signature.PublicKeyVerify
+    )
+
+  @parameterized.named_parameters(invalid_key_testcases())
+  def test_create_signature_verify_with_invalid_key_fails(
+      self, lang: str, key: rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey
+  ):
+    keyset = public_key_to_keyset(key, tink_pb2.OutputPrefixType.TINK)
+    with self.assertRaises(tink.TinkError):
+      testing_servers.remote_primitive(
+          lang, keyset.SerializeToString(), signature.PublicKeyVerify
+      )
+
+  def test_golang_rejects_f4_plus_2(self):
+    """See also b/274605582."""
+    key = rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+        version=0,
+        n=MODULUS_BYTES,
+        e=bytes.fromhex('010003'),
+        params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+            hash_type=common_pb2.HashType.SHA256
+        ),
+    )
+    keyset = public_key_to_keyset(key, tink_pb2.OutputPrefixType.TINK)
+    with self.assertRaises(tink.TinkError):
+      testing_servers.remote_primitive(
+          'go', keyset.SerializeToString(), signature.PublicKeyVerify
+      )
+
+  def test_java_accepts_large_e(self):
+    """See also b/274605582."""
+    key = rsa_ssa_pkcs1_pb2.RsaSsaPkcs1PublicKey(
+        version=0,
+        n=MODULUS_BYTES,
+        # 2^32 + 1
+        e=bytes.fromhex('0100000001'),
+        params=rsa_ssa_pkcs1_pb2.RsaSsaPkcs1Params(
+            hash_type=common_pb2.HashType.SHA256
+        ),
+    )
+    keyset = public_key_to_keyset(key, tink_pb2.OutputPrefixType.TINK)
+    testing_servers.remote_primitive(
+        'java', keyset.SerializeToString(), signature.PublicKeyVerify
+    )
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/signature_test.py b/testing/cross_language/signature_test.py
index 582fb47..0795921 100644
--- a/testing/cross_language/signature_test.py
+++ b/testing/cross_language/signature_test.py
@@ -23,8 +23,8 @@
 
 from tink.proto import tink_pb2
 from tink.testing import keyset_builder
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = (testing_servers
                        .SUPPORTED_LANGUAGES_BY_PRIMITIVE['signature'])
@@ -39,63 +39,34 @@
   testing_servers.stop()
 
 
-def all_signature_private_key_template_names() -> Iterable[str]:
-  """Yields all Signature private key template names."""
-  for key_type in supported_key_types.SIGNATURE_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-
-
 class SignatureTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_signature_private_key_template_names())
+  @parameterized.parameters(
+      utilities.tinkey_template_names_for(signature.PublicKeySign))
   def test_sign_verify(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     self.assertNotEmpty(supported_langs)
-    key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
     # Take the first supported language to generate the private keyset.
     private_keyset = testing_servers.new_keyset(supported_langs[0],
                                                 key_template)
-    supported_signers = [
-        testing_servers.public_key_sign(lang, private_keyset)
-        for lang in supported_langs
-    ]
-    unsupported_signers = [
-        testing_servers.public_key_sign(lang, private_keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
+    supported_signers = {}
+    for lang in supported_langs:
+      supported_signers[lang] = testing_servers.remote_primitive(
+          lang, private_keyset, signature.PublicKeySign)
     public_keyset = testing_servers.public_keyset('java', private_keyset)
-    supported_verifiers = [
-        testing_servers.public_key_verify(lang, public_keyset)
-        for lang in supported_langs
-    ]
-    unsupported_verifiers = [
-        testing_servers.public_key_verify(lang, public_keyset)
-        for lang in testing_servers.LANGUAGES
-        if lang not in supported_langs
-    ]
-    for signer in supported_signers:
+    supported_verifiers = {}
+    for lang in supported_verifiers:
+      supported_verifiers[lang] = testing_servers.remote_primitive(
+          lang, public_keyset, signature.PublicKeyVerify)
+    for lang, signer in supported_signers.items():
       message = (
           b'A message to be signed using key_template %s in %s.'
-          % (key_template_name.encode('utf8'), signer.lang.encode('utf8')))
+          % (key_template_name.encode('utf8'), lang.encode('utf8')))
       sign = signer.sign(message)
-      for verifier in supported_verifiers:
+      for _, verifier in supported_verifiers.items():
         self.assertIsNone(verifier.verify(sign, message))
-      for verifier in unsupported_verifiers:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='Language %s supports signature verify with %s unexpectedly' %
-            (verifier.lang, key_template_name)):
-          verifier.verify(sign, message)
-    for signer in unsupported_signers:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports signature sign with %s unexpectedly' %
-          (signer.lang, key_template_name)):
-        _ = signer.sign(message)
-
 
 # If the implementations work fine for keysets with single keys, then key
 # rotation should work if the primitive wrapper is implemented correctly.
@@ -129,23 +100,31 @@
     builder = keyset_builder.new_keyset_builder()
     older_key_id = builder.add_new_key(old_key_tmpl)
     builder.set_primary_key(older_key_id)
-    sign1 = testing_servers.public_key_sign(enc_lang, builder.keyset())
-    verify1 = testing_servers.public_key_verify(dec_lang,
-                                                builder.public_keyset())
+    sign1 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                             signature.PublicKeySign)
+    verify1 = testing_servers.remote_primitive(dec_lang,
+                                               builder.public_keyset(),
+                                               signature.PublicKeyVerify)
     newer_key_id = builder.add_new_key(new_key_tmpl)
-    sign2 = testing_servers.public_key_sign(enc_lang, builder.keyset())
-    verify2 = testing_servers.public_key_verify(dec_lang,
-                                                builder.public_keyset())
+    sign2 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                             signature.PublicKeySign)
+    verify2 = testing_servers.remote_primitive(dec_lang,
+                                               builder.public_keyset(),
+                                               signature.PublicKeyVerify)
 
     builder.set_primary_key(newer_key_id)
-    sign3 = testing_servers.public_key_sign(enc_lang, builder.keyset())
-    verify3 = testing_servers.public_key_verify(dec_lang,
-                                                builder.public_keyset())
+    sign3 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                             signature.PublicKeySign)
+    verify3 = testing_servers.remote_primitive(dec_lang,
+                                               builder.public_keyset(),
+                                               signature.PublicKeyVerify)
 
     builder.disable_key(older_key_id)
-    sign4 = testing_servers.public_key_sign(enc_lang, builder.keyset())
-    verify4 = testing_servers.public_key_verify(dec_lang,
-                                                builder.public_keyset())
+    sign4 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                             signature.PublicKeySign)
+    verify4 = testing_servers.remote_primitive(dec_lang,
+                                               builder.public_keyset(),
+                                               signature.PublicKeyVerify)
     self.assertNotEqual(older_key_id, newer_key_id)
 
     # 1 signs with the older key. So 1, 2 and 3 can verify it, but not 4.
diff --git a/testing/cross_language/streaming_aead/BUILD.bazel b/testing/cross_language/streaming_aead/BUILD.bazel
new file mode 100644
index 0000000..f192d44
--- /dev/null
+++ b/testing/cross_language/streaming_aead/BUILD.bazel
@@ -0,0 +1,43 @@
+load("@pip_deps//:requirements.bzl", "requirement")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+py_test(
+    name = "aes_ctr_hmac_streaming_key_test",
+    srcs = ["aes_ctr_hmac_streaming_key_test.py"],
+    deps = [
+        "//tink_config",
+        "//util:testing_servers",
+        "//util:utilities",
+        "@tink_py//tink/testing:keyset_builder",
+        requirement("absl-py"),
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/proto:aes_ctr_hmac_streaming_py_pb2",
+        "@tink_py//tink/proto:common_py_pb2",
+        "@tink_py//tink/proto:hmac_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/streaming_aead",
+    ],
+)
+
+py_test(
+    name = "aes_gcm_hkdf_streaming_key_test",
+    srcs = ["aes_gcm_hkdf_streaming_key_test.py"],
+    deps = [
+        "//tink_config",
+        "//util:testing_servers",
+        "//util:utilities",
+        "@tink_py//tink/testing:keyset_builder",
+        requirement("absl-py"),
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/proto:aes_gcm_hkdf_streaming_py_pb2",
+        "@tink_py//tink/proto:common_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/streaming_aead",
+    ],
+)
diff --git a/testing/cross_language/streaming_aead/aes_ctr_hmac_streaming_key_test.py b/testing/cross_language/streaming_aead/aes_ctr_hmac_streaming_key_test.py
new file mode 100644
index 0000000..1a614e3
--- /dev/null
+++ b/testing/cross_language/streaming_aead/aes_ctr_hmac_streaming_key_test.py
@@ -0,0 +1,466 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import binascii
+import io
+import random
+
+from absl.testing import absltest
+from absl.testing import parameterized
+import tink
+from tink import streaming_aead
+
+from tink.proto import aes_ctr_hmac_streaming_pb2
+from tink.proto import common_pb2
+from tink.proto import hmac_pb2
+from tink.proto import tink_pb2
+import tink_config
+from util import testing_servers
+
+
+def setUpModule():
+  streaming_aead.register()
+  testing_servers.start('aes_ctr_hmac_streaming_key_test')
+
+
+def tearDownModule():
+  testing_servers.stop()
+
+
+def to_keyset(
+    key: aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey,
+) -> tink_pb2.Keyset:
+  """Embeds a AesCtrHmacStreamingKey in some way in a keyset."""
+  return tink_pb2.Keyset(
+      primary_key_id=1234,
+      key=[
+          tink_pb2.Keyset.Key(
+              key_data=tink_pb2.KeyData(
+                  type_url='type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey',
+                  value=key.SerializeToString(),
+                  key_material_type='SYMMETRIC',
+              ),
+              output_prefix_type=tink_pb2.OutputPrefixType.RAW,
+              status=tink_pb2.KeyStatusType.ENABLED,
+              key_id=1234,
+          )
+      ],
+  )
+
+
+def simple_valid_key() -> (
+    aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey
+):
+  """Creates a simple, valid AesCtrHmacStreamingKey object."""
+  return aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey(
+      version=0,
+      params=aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingParams(
+          ciphertext_segment_size=512,
+          derived_key_size=16,
+          hkdf_hash_type=common_pb2.HashType.SHA256,
+          hmac_params=hmac_pb2.HmacParams(
+              hash=common_pb2.HashType.SHA256, tag_size=16
+          ),
+      ),
+      key_value=b'0123456789abcdef',
+  )
+
+
+def lang_and_valid_keys_create_and_encrypt():
+  result = []
+  langs = tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+
+  key = simple_valid_key()
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  assert key.params.derived_key_size == 16
+  key.params.derived_key_size = 32
+  key.key_value = b'0123456789abcdef0123456789abcdef'
+  for lang in langs:
+    result.append((lang, key))
+
+  ## TAG SIZES
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA1
+  key.params.hmac_params.tag_size = 10
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA1
+  key.params.hmac_params.tag_size = 11
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA1
+  key.params.hmac_params.tag_size = 20
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA256
+  key.params.hmac_params.tag_size = 10
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA256
+  key.params.hmac_params.tag_size = 11
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA256
+  key.params.hmac_params.tag_size = 32
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA512
+  key.params.hmac_params.tag_size = 10
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA512
+  key.params.hmac_params.tag_size = 11
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA512
+  key.params.hmac_params.tag_size = 64
+  for lang in langs:
+    result.append((lang, key))
+
+  # HKDF Hash Type:
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA1
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA256
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA512
+  for lang in langs:
+    result.append((lang, key))
+
+  # Minimum ciphertext_segment_size
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = (
+      key.params.derived_key_size + key.params.hmac_params.tag_size + 9
+  )
+  for lang in langs:
+    result.append((lang, key))
+
+  return result
+
+
+def lang_and_valid_keys_create_only():
+  result = lang_and_valid_keys_create_and_encrypt()
+  langs = tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+
+  # TODO(b/268193523): Java crashes with ciphertext_segment_size = 2**31 - 1
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = 2**31 - 1
+  for lang in langs:
+    result.append((lang, key))
+
+  return result
+
+
+def lang_and_invalid_keys():
+  result = []
+  langs = tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+
+  key = simple_valid_key()
+  key.params.derived_key_size = 24
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA224
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA384
+  for lang in langs:
+    result.append((lang, key))
+
+  # Check requirement len(InitialKeyMaterial) >= DerivedKeySize
+  key = simple_valid_key()
+  key.key_value = b'0123456789abcdef'
+  key.params.derived_key_size = 32
+  for lang in langs:
+    result.append((lang, key))
+
+  # HKDF Hash Type:
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.UNKNOWN_HASH
+  for lang in langs:
+    result.append((lang, key))
+
+  # Minimum ciphertext_segment_size
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = (
+      key.params.derived_key_size + key.params.hmac_params.tag_size + 8
+  )
+  for lang in langs:
+    result.append((lang, key))
+
+  ## Tag sizes
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA1
+  key.params.hmac_params.tag_size = 9
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA1
+  key.params.hmac_params.tag_size = 21
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA256
+  key.params.hmac_params.tag_size = 9
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA256
+  key.params.hmac_params.tag_size = 33
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA512
+  key.params.hmac_params.tag_size = 9
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA512
+  key.params.hmac_params.tag_size = 65
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA224
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hmac_params.hash = common_pb2.HashType.SHA384
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = 2**31
+  for lang in langs:
+    result.append((lang, key))
+
+  return result
+
+
+class AesCtrHmacStreamingKeyTest(parameterized.TestCase):
+  """Tests specific for keys of type AesCtrHmacStreamingKey.
+
+  See https://developers.google.com/tink/streaming-aead/aes_ctr_hmac_streaming
+  for the documentation.
+  """
+
+  @parameterized.parameters(lang_and_valid_keys_create_only())
+  def test_create_streaming_aead(
+      self, lang: str, key: aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey
+  ):
+    keyset = to_keyset(key)
+    testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+    )
+
+  @parameterized.parameters(lang_and_valid_keys_create_and_encrypt())
+  def test_create_streaming_aead_encrypt_decrypt(
+      self, lang: str, key: aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey
+  ):
+    keyset = to_keyset(key)
+    saead = testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+    )
+    plaintext = b'some plaintext'
+    ad = b'associated_data'
+    ciphertext = saead.new_encrypting_stream(
+        io.BytesIO(plaintext), ad
+    ).read()
+    self.assertEqual(
+        saead.new_decrypting_stream(
+            io.BytesIO(ciphertext), ad
+        ).read(),
+        plaintext,
+    )
+
+  @parameterized.parameters(lang_and_invalid_keys())
+  def test_create_streaming_aead_invalid_key_fails(
+      self, lang: str, key: aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey
+  ):
+    keyset = to_keyset(key)
+    with self.assertRaises(tink.TinkError):
+      testing_servers.remote_primitive(
+          lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+      )
+
+  def test_output_prefix_ignored(self):
+    lang_1 = random.choice(
+        tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+    )
+    lang_2 = random.choice(
+        tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+    )
+    output_prefix_1 = random.choice(
+        [tink_pb2.RAW, tink_pb2.CRUNCHY, tink_pb2.LEGACY, tink_pb2.TINK]
+    )
+    output_prefix_2 = random.choice(
+        [tink_pb2.RAW, tink_pb2.CRUNCHY, tink_pb2.LEGACY, tink_pb2.TINK]
+    )
+    with self.subTest(
+        f'Testing with languages ({lang_1}, {lang_2}) and output prefix types '
+        f'({tink_pb2.OutputPrefixType.Name(output_prefix_1)}, '
+        f'{tink_pb2.OutputPrefixType.Name(output_prefix_2)})'
+    ):
+      keyset = to_keyset(simple_valid_key())
+      keyset.key[0].output_prefix_type = output_prefix_1
+      saead_1 = testing_servers.remote_primitive(
+          lang_1, keyset.SerializeToString(), streaming_aead.StreamingAead
+      )
+      keyset.key[0].output_prefix_type = output_prefix_2
+      saead_2 = testing_servers.remote_primitive(
+          lang_2, keyset.SerializeToString(), streaming_aead.StreamingAead
+      )
+      plaintext = b'some plaintext'
+      associated_data = b'associated_data'
+      ciphertext = saead_1.new_encrypting_stream(
+          io.BytesIO(plaintext), associated_data
+      ).read()
+      self.assertEqual(
+          saead_2.new_decrypting_stream(
+              io.BytesIO(ciphertext), associated_data
+          ).read(),
+          plaintext,
+      )
+
+  @parameterized.parameters(
+      tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+  )
+  def test_manually_created_test_vector(self, lang: str):
+    """Tests using a ciphertext created by looking at the documentation.
+
+    See https://developers.google.com/tink/streaming-aead/aes_ctr_hmac_streaming
+    for the documentation. The goal is to ensure that the documentation is
+    clear; we expect readers to read this with the documentation.
+
+    Args:
+      lang: The language to test.
+    """
+
+    def xor(b1: bytes, b2: bytes) -> bytes:
+      return bytes(i ^ j for (i, j) in zip(b1, b2))
+
+    h2b = binascii.a2b_hex
+
+    key = aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingKey(
+        version=0,
+        params=aes_ctr_hmac_streaming_pb2.AesCtrHmacStreamingParams(
+            ciphertext_segment_size=64,
+            derived_key_size=16,
+            hkdf_hash_type=common_pb2.HashType.SHA1,
+            hmac_params=hmac_pb2.HmacParams(
+                hash=common_pb2.HashType.SHA256, tag_size=32
+            ),
+        ),
+        key_value=h2b('6eb56cdc726dfbe5d57f2fcdc6e9345b')
+    )
+    # We set the message to be:
+    msg = b'This is a fairly long plaintext. However, it is not crazy long.'
+    #
+    # We set the associated data to be:
+    aad = b'aad'
+
+    # We picked the header at random: Note the length is 24 = 0x18.
+    header_length = h2b('18')
+    salt = h2b('93b3af5e14ab378d065addfc8484da64')
+    nonce_prefix = h2b('2c0862877baea8')
+    header = header_length + salt + nonce_prefix
+    # hkdf.hkdf_sha1(ikm=key_value, salt=header_salt, info=aad, size=48) gives
+    # '66dd511791296a6cfc94a24041fcab9f' +
+    # '0f736d6e85c448c2c8cc30f094d7e2d89e1a4c6a2dea4e9c8d1d2015e54c609a'
+    # aes_key = h2b('66dd511791296a6cfc94a24041fcab9f')
+    # hmac_key = h2b(
+    #        '0f736d6e85c448c2c8cc30f094d7e2d89e1a4c6a2dea4e9c8d1d2015e54c609a')
+
+    # We next split the message:
+    # len(msg) = 63.
+    # len(M_0) = 8 = CiphertextSegmentSize(64) - Headerlength(24) - TagSize(32)
+    # len(M_1) = 32 = CiphertextSegmentSize(64) - TagSize(32)
+    # len(M_2) = 23 < CiphertextSegmentSize(64) - TagSize(32)
+    msg_0 = msg[:8]
+    msg_1 = msg[8:40]
+    msg_2 = msg[40:]
+
+    # Relevant AES computations with key = 66dd511791296a6cfc94a24041fcab9f
+    #
+    # nonce_prefix + segment_nr + b + i   | Out
+    # -----------------------------------------------------------------------
+    # 2c0862877baea8 00000000 00 00000000 | ea8e18301bd57bfdd2f903025950c827
+    # 2c0862877baea8 00000001 00 00000000 | 2999c8ea5401704243c8cd77929fd526
+    # 2c0862877baea8 00000001 00 00000001 | 17fec5542a842446251bb2f3a81f6249
+    # 2c0862877baea8 00000002 01 00000000 | 70fe58e44835a6602952749e763637d9
+    # 2c0862877baea8 00000002 01 00000001 | d973bca83580867766f38b056d735902
+    #
+    c0 = xor(msg_0, h2b(b'ea8e18301bd57bfd'))
+    c1 = xor(msg_1[:16], h2b('2999c8ea5401704243c8cd77929fd526')) + xor(
+        msg_1[16:32], h2b('17fec5542a842446251bb2f3a81f6249')
+    )
+    c2 = xor(msg_2[:16], h2b('70fe58e44835a6602952749e763637d9')) + xor(
+        msg_2[16:], h2b('d973bca8358086')
+    )
+
+    # T0 = hmac(key = hmac_key, h2b('2c0862877baea8000000000000000000' + c0)
+    t0 = h2b('8303ca71c04d8e06e1b01cff7c1178af47dac031517b1f6a2d9be84105677a68')
+    # T1 = hmac(key = hmac_key, h2b('2c0862877baea8000000010000000000' + c1)
+    t1 = h2b('834d890839f37f762caddc029cc673300ff107fd51f9a62058fcd00befc362e5')
+    # T2 = hmac(key = hmac_key, h2b('2c0862877baea8000000020100000000' + c2)
+    t2 = h2b('5fb0c893903271af38380c2f355cb85e5ec571648513123321bde0c6042f43c7')
+
+    ciphertext = header + c0 + t0 + c1 + t1 + c2 + t2
+
+    keyset = to_keyset(key)
+    saead = testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+    )
+
+    self.assertEqual(
+        saead.new_decrypting_stream(io.BytesIO(ciphertext), aad).read(),
+        msg,
+    )
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/streaming_aead/aes_gcm_hkdf_streaming_key_test.py b/testing/cross_language/streaming_aead/aes_gcm_hkdf_streaming_key_test.py
new file mode 100644
index 0000000..350562f
--- /dev/null
+++ b/testing/cross_language/streaming_aead/aes_gcm_hkdf_streaming_key_test.py
@@ -0,0 +1,350 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import binascii
+import io
+import random
+
+from absl.testing import absltest
+from absl.testing import parameterized
+import tink
+from tink import streaming_aead
+
+from tink.proto import aes_gcm_hkdf_streaming_pb2
+from tink.proto import common_pb2
+from tink.proto import tink_pb2
+import tink_config
+from util import testing_servers
+
+
+def setUpModule():
+  streaming_aead.register()
+  testing_servers.start('aes_gcm_hkdf_streaming_key_test')
+
+
+def tearDownModule():
+  testing_servers.stop()
+
+
+def to_keyset(
+    key: aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey,
+) -> tink_pb2.Keyset:
+  """Embeds a AesGcmHkdfStreamingKey in some way in a keyset."""
+  return tink_pb2.Keyset(
+      primary_key_id=1234,
+      key=[
+          tink_pb2.Keyset.Key(
+              key_data=tink_pb2.KeyData(
+                  type_url='type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey',
+                  value=key.SerializeToString(),
+                  key_material_type='SYMMETRIC',
+              ),
+              output_prefix_type=tink_pb2.OutputPrefixType.RAW,
+              status=tink_pb2.KeyStatusType.ENABLED,
+              key_id=1234,
+          )
+      ],
+  )
+
+
+def simple_valid_key() -> (
+    aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey
+):
+  """Creates a simple, valid AesGcmHkdfStreamingKey object."""
+  return aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey(
+      version=0,
+      params=aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingParams(
+          ciphertext_segment_size=512,
+          derived_key_size=16,
+          hkdf_hash_type=common_pb2.HashType.SHA256,
+      ),
+      key_value=b'0123456789abcdef',
+  )
+
+
+def lang_and_valid_keys_create_and_encrypt():
+  result = []
+  langs = tink_config.supported_languages_for_key_type('AesGcmHkdfStreamingKey')
+
+  key = simple_valid_key()
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  assert key.params.derived_key_size == 16
+  key.params.derived_key_size = 32
+  key.key_value = b'0123456789abcdef0123456789abcdef'
+  for lang in langs:
+    result.append((lang, key))
+
+  # HKDF Hash Type:
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA1
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA256
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA512
+  for lang in langs:
+    result.append((lang, key))
+
+  # Minimum ciphertext_segment_size
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = key.params.derived_key_size + 25
+  for lang in langs:
+    result.append((lang, key))
+
+  return result
+
+
+def lang_and_valid_keys_create_only():
+  result = lang_and_valid_keys_create_and_encrypt()
+  langs = tink_config.supported_languages_for_key_type('AesGcmHkdfStreamingKey')
+
+  # TODO(b/268193523): Java crashes with ciphertext_segment_size = 2**31 - 1
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = 2**31 - 1
+  for lang in langs:
+    result.append((lang, key))
+
+  return result
+
+
+def lang_and_invalid_keys():
+  result = []
+  langs = tink_config.supported_languages_for_key_type('AesGcmHkdfStreamingKey')
+
+  key = simple_valid_key()
+  key.params.derived_key_size = 24
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA224
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.SHA384
+  for lang in langs:
+    result.append((lang, key))
+
+  # Check requirement len(InitialKeyMaterial) >= DerivedKeySize
+  key = simple_valid_key()
+  key.key_value = b'0123456789abcdef'
+  key.params.derived_key_size = 32
+  for lang in langs:
+    result.append((lang, key))
+
+  # HKDF Hash Type:
+  key = simple_valid_key()
+  key.params.hkdf_hash_type = common_pb2.HashType.UNKNOWN_HASH
+  for lang in langs:
+    result.append((lang, key))
+
+  # Minimum ciphertext_segment_size
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = key.params.derived_key_size + 24
+  for lang in langs:
+    result.append((lang, key))
+
+  key = simple_valid_key()
+  key.params.ciphertext_segment_size = 2**31
+  for lang in langs:
+    result.append((lang, key))
+
+  return result
+
+
+class AesGcmHkdfStreamingKeyTest(parameterized.TestCase):
+  """Tests specific for keys of type AesGcmHkdfStreamingKey.
+
+  See https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming
+  for the documentation.
+  """
+
+  @parameterized.parameters(lang_and_valid_keys_create_only())
+  def test_create_streaming_aead(
+      self, lang: str, key: aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey
+  ):
+    keyset = to_keyset(key)
+    testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+    )
+
+  @parameterized.parameters(lang_and_valid_keys_create_and_encrypt())
+  def test_create_streaming_aead_encrypt_decrypt(
+      self, lang: str, key: aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey
+  ):
+    keyset = to_keyset(key)
+    saead = testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+    )
+    plaintext = b'some plaintext'
+    ad = b'associated_data'
+    ciphertext = saead.new_encrypting_stream(
+        io.BytesIO(plaintext), ad
+    ).read()
+    self.assertEqual(
+        saead.new_decrypting_stream(
+            io.BytesIO(ciphertext), ad
+        ).read(),
+        plaintext,
+    )
+
+  @parameterized.parameters(lang_and_invalid_keys())
+  def test_create_streaming_aead_invalid_key_fails(
+      self, lang: str, key: aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey
+  ):
+    keyset = to_keyset(key)
+    with self.assertRaises(tink.TinkError):
+      testing_servers.remote_primitive(
+          lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+      )
+
+  def test_output_prefix_ignored(self):
+    lang_1 = random.choice(
+        tink_config.supported_languages_for_key_type('AesGcmHkdfStreamingKey')
+    )
+    lang_2 = random.choice(
+        tink_config.supported_languages_for_key_type('AesGcmHkdfStreamingKey')
+    )
+    output_prefix_1 = random.choice(
+        [tink_pb2.RAW, tink_pb2.CRUNCHY, tink_pb2.LEGACY, tink_pb2.TINK]
+    )
+    output_prefix_2 = random.choice(
+        [tink_pb2.RAW, tink_pb2.CRUNCHY, tink_pb2.LEGACY, tink_pb2.TINK]
+    )
+    with self.subTest(
+        f'Testing with languages ({lang_1}, {lang_2}) and output prefix types '
+        f'({tink_pb2.OutputPrefixType.Name(output_prefix_1)}, '
+        f'{tink_pb2.OutputPrefixType.Name(output_prefix_2)})'
+    ):
+      keyset = to_keyset(simple_valid_key())
+      keyset.key[0].output_prefix_type = output_prefix_1
+      saead_1 = testing_servers.remote_primitive(
+          lang_1, keyset.SerializeToString(), streaming_aead.StreamingAead
+      )
+      keyset.key[0].output_prefix_type = output_prefix_2
+      saead_2 = testing_servers.remote_primitive(
+          lang_2, keyset.SerializeToString(), streaming_aead.StreamingAead
+      )
+      plaintext = b'some plaintext'
+      associated_data = b'associated_data'
+      ciphertext = saead_1.new_encrypting_stream(
+          io.BytesIO(plaintext), associated_data
+      ).read()
+      self.assertEqual(
+          saead_2.new_decrypting_stream(
+              io.BytesIO(ciphertext), associated_data
+          ).read(),
+          plaintext,
+      )
+
+  @parameterized.parameters(
+      tink_config.supported_languages_for_key_type('AesCtrHmacStreamingKey')
+  )
+  def test_manually_created_test_vector(self, lang: str):
+    """This test uses a ciphertext created by looking at the documentation.
+
+    See https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming
+    for the documentation. The goal is to ensure that the documentation is
+    clear; we expect readers to read this with the documentation.
+
+    Args:
+      lang: the language to test
+    """
+
+    h2b = binascii.a2b_hex
+
+    key = aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingKey(
+        version=0,
+        params=aes_gcm_hkdf_streaming_pb2.AesGcmHkdfStreamingParams(
+            ciphertext_segment_size=64,
+            derived_key_size=16,
+            hkdf_hash_type=common_pb2.HashType.SHA1,
+        ),
+        key_value=h2b('6eb56cdc726dfbe5d57f2fcdc6e9345b')
+    )
+    # We set the message to be:
+    msg = (
+        b'This is a fairly long plaintext. '
+        + b'It is of the exact length to create three output blocks. '
+    )
+    #
+    # We set the associated data to be:
+    associated_data = b'aad'
+
+    # We picked the header at random: Note the length is 24 = 0x18.
+    header_length = h2b('18')
+    salt = h2b('93b3af5e14ab378d065addfc8484da64')
+    nonce_prefix = h2b('2c0862877baea8')
+    header = header_length + salt + nonce_prefix
+    # hkdf.hkdf_sha1(ikm=key_value, salt=salt, info=aad, size=16) gives
+    # '66dd511791296a6cfc94a24041fcab9f'
+    # aes_key = h2b('66dd511791296a6cfc94a24041fcab9f')
+
+    # We next split the message:
+    # len(msg) = 90
+    # len(M_0) = 24 = CiphertextSegmentSize(64) - Headerlength(24) - 16
+    # len(M_1) = 48 = CiphertextSegmentSize(64) - 16
+    # len(M_2) = 18 < CiphertextSegmentSize(64) - 16
+    # msg_0 = msg[:24]
+    # msg_1 = msg[24:72]
+    # msg_2 = msg[72:]
+    #
+    # AES GCM computations with key = 66dd511791296a6cfc94a24041fcab9f
+    #
+    #
+    # IV = nonce_prefix + segment_nr + b | plaintext | result
+    # -----------------------------------------------------------------------
+    # 2c0862877baea8 00000000 00         | msg_0     | c_0
+    # 2c0862877baea8 00000001 00         | msg_1     | c_0
+    # 2c0862877baea8 00000002 01         | msg_2     | c_0
+    c0 = h2b(
+        b'db92d9c77406a406168478821c4298eab3e6d531277f4c1a'
+        + b'051714faebcaefcbca7b7be05e9445ea'
+    )
+    c1 = h2b(
+        b'a0bb2904153398a25084dd80ae0edcd1c3079fcea2cd3770'
+        + b'630ee36f7539207b8ec9d754956d486b71cdf989f0ed6fba'
+        + b'6779b63558be0a66e668df14e1603cd2'
+    )
+    c2 = h2b(
+        b'af8944844078345286d0b292e772e7190775'
+        + b'c51a0f83e40c0b75821027e7e538e111'
+    )
+
+    ciphertext = header + c0 + c1 + c2
+
+    keyset = to_keyset(key)
+    saead = testing_servers.remote_primitive(
+        lang, keyset.SerializeToString(), streaming_aead.StreamingAead
+    )
+
+    self.assertEqual(
+        saead.new_decrypting_stream(
+            io.BytesIO(ciphertext), associated_data
+        ).read(),
+        msg,
+    )
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/streaming_aead_test.py b/testing/cross_language/streaming_aead_test.py
index b0c42ca..5c287e8 100644
--- a/testing/cross_language/streaming_aead_test.py
+++ b/testing/cross_language/streaming_aead_test.py
@@ -14,7 +14,6 @@
 """Cross-language tests for the StreamingAead primitive."""
 
 import io
-from typing import Iterable
 
 from absl.testing import absltest
 from absl.testing import parameterized
@@ -23,8 +22,8 @@
 from tink import streaming_aead
 
 from tink.testing import keyset_builder
-from util import supported_key_types
 from util import testing_servers
+from util import utilities
 
 SUPPORTED_LANGUAGES = (testing_servers
                        .SUPPORTED_LANGUAGES_BY_PRIMITIVE['streaming_aead'])
@@ -48,65 +47,39 @@
   testing_servers.stop()
 
 
-def all_streaming_aead_key_template_names() -> Iterable[str]:
-  """Yields all Streaming AEAD key template names."""
-  for key_type in supported_key_types.STREAMING_AEAD_KEY_TYPES:
-    for key_template_name in supported_key_types.KEY_TEMPLATE_NAMES[key_type]:
-      yield key_template_name
-
-
 class StreamingAeadPythonTest(parameterized.TestCase):
 
-  @parameterized.parameters(all_streaming_aead_key_template_names())
+  @parameterized.parameters(
+      utilities.tinkey_template_names_for(streaming_aead.StreamingAead))
   def test_encrypt_decrypt(self, key_template_name):
-    supported_langs = supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+    supported_langs = utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
         key_template_name]
     self.assertNotEmpty(supported_langs)
-    key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
+    key_template = utilities.KEY_TEMPLATE[key_template_name]
     # Take the first supported language to generate the keyset.
     keyset = testing_servers.new_keyset(supported_langs[0], key_template)
-    supported_streaming_aeads = [
-        testing_servers.streaming_aead(lang, keyset) for lang in supported_langs
-    ]
-    unsupported_streaming_aeads = [
-        testing_servers.streaming_aead(lang, keyset)
-        for lang in SUPPORTED_LANGUAGES
-        if lang not in supported_langs
-    ]
-    for p in supported_streaming_aeads:
+    supported_streaming_aeads = {}
+    for lang in supported_langs:
+      supported_streaming_aeads[lang] = testing_servers.remote_primitive(
+          lang, keyset, streaming_aead.StreamingAead)
+    for lang, p in supported_streaming_aeads.items():
       desc = (
           b'This is some plaintext message to be encrypted using key_template '
           b'%s using %s for encryption.'
-          % (key_template_name.encode('utf8'), p.lang.encode('utf8')))
+          % (key_template_name.encode('utf8'), lang.encode('utf8')))
       plaintext = desc + LONG_PLAINTEXT
       associated_data = (
           b'Some associated data for %s using %s for encryption.' %
-          (key_template_name.encode('utf8'), p.lang.encode('utf8')))
+          (key_template_name.encode('utf8'), lang.encode('utf8')))
       plaintext_stream = io.BytesIO(plaintext)
       ciphertext_result_stream = p.new_encrypting_stream(
           plaintext_stream, associated_data)
       ciphertext = ciphertext_result_stream.read()
-      for p2 in supported_streaming_aeads:
+      for _, p2 in supported_streaming_aeads.items():
         ciphertext_stream = io.BytesIO(ciphertext)
         decrypted_stream = p2.new_decrypting_stream(
             ciphertext_stream, associated_data)
         self.assertEqual(decrypted_stream.read(), plaintext)
-      for p2 in unsupported_streaming_aeads:
-        with self.assertRaises(
-            tink.TinkError,
-            msg='Language %s supports streaming AEAD decryption with %s '
-            'unexpectedly' % (p2.lang, key_template_name)):
-          ciphertext_stream = io.BytesIO(ciphertext)
-          decrypted_stream = p2.new_decrypting_stream(
-              ciphertext_stream, associated_data)
-    for p in unsupported_streaming_aeads:
-      with self.assertRaises(
-          tink.TinkError,
-          msg='Language %s supports streaming AEAD encryption with %s '
-          'unexpectedly' % (p.lang, key_template_name)):
-        plaintext_stream = io.BytesIO(b'plaintext')
-        ciphertext_result_stream = p.new_encrypting_stream(
-            plaintext_stream, b'associated_data')
 
   @parameterized.parameters(key_rotation_test_cases())
   def test_key_rotation(self, enc_lang, dec_lang):
@@ -116,20 +89,28 @@
     older_key_id = builder.add_new_key(
         streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_4KB)
     builder.set_primary_key(older_key_id)
-    enc1 = testing_servers.streaming_aead(enc_lang, builder.keyset())
-    dec1 = testing_servers.streaming_aead(dec_lang, builder.keyset())
+    enc1 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
+    dec1 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
     newer_key_id = builder.add_new_key(
         streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_4KB)
-    enc2 = testing_servers.streaming_aead(enc_lang, builder.keyset())
-    dec2 = testing_servers.streaming_aead(dec_lang, builder.keyset())
+    enc2 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
+    dec2 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
 
     builder.set_primary_key(newer_key_id)
-    enc3 = testing_servers.streaming_aead(enc_lang, builder.keyset())
-    dec3 = testing_servers.streaming_aead(dec_lang, builder.keyset())
+    enc3 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
+    dec3 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
 
     builder.disable_key(older_key_id)
-    enc4 = testing_servers.streaming_aead(enc_lang, builder.keyset())
-    dec4 = testing_servers.streaming_aead(dec_lang, builder.keyset())
+    enc4 = testing_servers.remote_primitive(enc_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
+    dec4 = testing_servers.remote_primitive(dec_lang, builder.keyset(),
+                                            streaming_aead.StreamingAead)
 
     self.assertNotEqual(older_key_id, newer_key_id)
     # 1 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
diff --git a/testing/cross_language/testdata/aws/BUILD.bazel b/testing/cross_language/testdata/aws/BUILD.bazel
new file mode 100644
index 0000000..c83665f
--- /dev/null
+++ b/testing/cross_language/testdata/aws/BUILD.bazel
@@ -0,0 +1,25 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+filegroup(
+    name = "credentials",
+    testonly = 1,
+    srcs = [
+        "credentials.cred",
+        "credentials.csv",
+        "credentials.ini",
+        "key_arn.txt",
+    ],
+)
+
+filegroup(
+    name = "bad_credentials",
+    testonly = 1,
+    srcs = [
+        "access_keys_bad.csv",
+        "credentials_bad.csv",
+        "credentials_bad.ini",
+        "key_arn_bad.txt",
+    ],
+)
diff --git a/testing/cross_language/testdata/aws/README.md b/testing/cross_language/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/testing/cross_language/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/testing/cross_language/testdata/aws/access_keys_bad.csv b/testing/cross_language/testdata/aws/access_keys_bad.csv
new file mode 100644
index 0000000..a9ec78f
--- /dev/null
+++ b/testing/cross_language/testdata/aws/access_keys_bad.csv
@@ -0,0 +1,2 @@
+Access key ID,Secret access key
+AKIAIOSFODNN7EXAMPLE,wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
diff --git a/testing/cross_language/testdata/aws/credentials.cred b/testing/cross_language/testdata/aws/credentials.cred
new file mode 100644
index 0000000..dff4bec
--- /dev/null
+++ b/testing/cross_language/testdata/aws/credentials.cred
@@ -0,0 +1,3 @@
+[default]
+accessKey = AKIAIIK5X7P3NAHNSNUQ
+secretKey = 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
diff --git a/testing/cross_language/testdata/aws/credentials.csv b/testing/cross_language/testdata/aws/credentials.csv
new file mode 100644
index 0000000..6d21110
--- /dev/null
+++ b/testing/cross_language/testdata/aws/credentials.csv
@@ -0,0 +1,3 @@
+User name,Password,Access key ID,Secret access key,Console login link
+tink-user1,,AKIAIIK5X7P3NAHNSNUQ,c7k55Gw83QlN1gYBhVPEEn1pKV909sxbll8JOvHF,https://235739564943.signin.aws.amazon.com/console
+
diff --git a/testing/cross_language/testdata/aws/credentials.ini b/testing/cross_language/testdata/aws/credentials.ini
new file mode 100644
index 0000000..1f96397
--- /dev/null
+++ b/testing/cross_language/testdata/aws/credentials.ini
@@ -0,0 +1,3 @@
+[default]
+aws_access_key_id = AKIAIIK5X7P3NAHNSNUQ
+aws_secret_access_key = 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
diff --git a/testing/cross_language/testdata/aws/credentials_bad.csv b/testing/cross_language/testdata/aws/credentials_bad.csv
new file mode 100644
index 0000000..9a2e0c4
--- /dev/null
+++ b/testing/cross_language/testdata/aws/credentials_bad.csv
@@ -0,0 +1,2 @@
+User name,Password,Access key ID,Secret access key
+,,AKIAIOSFODNN7EXAMPLE,wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
diff --git a/testing/cross_language/testdata/aws/credentials_bad.ini b/testing/cross_language/testdata/aws/credentials_bad.ini
new file mode 100644
index 0000000..6f491d7
--- /dev/null
+++ b/testing/cross_language/testdata/aws/credentials_bad.ini
@@ -0,0 +1,3 @@
+[default]
+aws_access_key_id = AKIAIOSFODNN7EXAMPLE
+aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
diff --git a/testing/cross_language/testdata/aws/key_arn.txt b/testing/cross_language/testdata/aws/key_arn.txt
new file mode 100644
index 0000000..a62cbd9
--- /dev/null
+++ b/testing/cross_language/testdata/aws/key_arn.txt
@@ -0,0 +1 @@
+arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f
diff --git a/testing/cross_language/testdata/aws/key_arn_bad.txt b/testing/cross_language/testdata/aws/key_arn_bad.txt
new file mode 100644
index 0000000..96d4187
--- /dev/null
+++ b/testing/cross_language/testdata/aws/key_arn_bad.txt
@@ -0,0 +1 @@
+arn:aws:kms:us-east-2:123456789012:key/12345678-1234-1234-1234-123456789012
diff --git a/testing/cross_language/testdata/gcp/BUILD.bazel b/testing/cross_language/testdata/gcp/BUILD.bazel
new file mode 100644
index 0000000..9483195
--- /dev/null
+++ b/testing/cross_language/testdata/gcp/BUILD.bazel
@@ -0,0 +1,23 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+exports_files(srcs = ["credential.json"])
+
+filegroup(
+    name = "credentials",
+    testonly = 1,
+    srcs = [
+        "credential.json",
+        "key_name.txt",
+    ],
+)
+
+filegroup(
+    name = "bad_credentials",
+    testonly = 1,
+    srcs = [
+        "credential_bad.json",
+        "key_name_bad.txt",
+    ],
+)
diff --git a/testing/cross_language/testdata/gcp/README.md b/testing/cross_language/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/testing/cross_language/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/testing/cross_language/testdata/gcp/credential.json b/testing/cross_language/testdata/gcp/credential.json
new file mode 100644
index 0000000..07fdc62
--- /dev/null
+++ b/testing/cross_language/testdata/gcp/credential.json
@@ -0,0 +1,12 @@
+{
+  "type": "service_account",
+  "project_id": "tink-test-infrastructure",
+  "private_key_id": "1b5021e241ac26833fcd1ced64509d447ff0a25a",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMtJlaQD79xGIC28\nowTpj7wkdi34piSubtDKttgC3lL00ioyQf/WMqLnyDWySNufCjhavQ7/sxXQAUCL\n5B3WDwM8+mFqQM2wJB18NBWBSfGOFSMwVQyWv7Y/1AFr+PvNKVlw4RZ4G8VuJzXZ\n9v/+5zyKv8py66sGVoHPI+LGfIprAgMBAAECgYEAxcgX8PVrnrITiKwpJxReJbyL\nxnpOmw2i/zza3BseVzOebjNrhw/NQDWl0qhcvmBjvyR5IGiiwiwXq8bu8CBdhRiE\nw3vKf1iuVOKhH07RB2wvCaGbVlB/p15gYau3sTRn5nej0tjYHX7xa/St/DwPk2H/\nxYGTRhyYtNL6wdtMjYECQQD+LVVJf0rLnxyPADTcz7Wdb+FUX79nWtMlzQOEB09+\nJj4ie0kD0cIvTQFjV3pOsg3uW2khFpjg110TXpJJfPjhAkEAzL7RhhfDdL7Dn2zl\n1orUthcGa2pzEAmg1tGBNb1pOg7LbVHKSa3GOOwyPRsActoyrPw18/fXaJdEfByY\ne9kwywJAB7rHMjH9y01uZ+bgtKpYYo5JcvBqeLEpZKfkaHp0b2ioURIguU4Csr+L\nwEKjxIrjo5ECFHCEe6nw+arRlgyH4QJBAIfQmEn733LEzB0n7npXU2yKb363eSYN\nTPzSsoREZdXWVIjqtWYUeKXvwA+apryJEw5+qwdvwxslJI+zpE6bLusCQE6M1lO9\nN6A3PtQv7Z3XwrEE/sPEVv4M4VHj0YHLs/32UuSXq5taMizKILfis1Stry4WjRHp\nQxEqdLrIkb13NH8=\n-----END PRIVATE KEY-----\n",
+  "client_email": "unit-and-integration-testing@tink-test-infrastructure.iam.gserviceaccount.com",
+  "client_id": "111876397550362269561",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://accounts.google.com/o/oauth2/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/unit-and-integration-testing%40tink-test-infrastructure.iam.gserviceaccount.com"
+}
diff --git a/testing/cross_language/testdata/gcp/credential_bad.json b/testing/cross_language/testdata/gcp/credential_bad.json
new file mode 100644
index 0000000..2b84cec
--- /dev/null
+++ b/testing/cross_language/testdata/gcp/credential_bad.json
@@ -0,0 +1,12 @@
+{
+  "type": "service_account",
+  "project_id": "tink-test-infrastructure",
+  "private_key_id": "some_bad_private_key_id",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMtJlaQD79xGIC28\nowTpj7wkdi34piSubtDKttgC3lL00ioyQf/WMqLnyDWySNufCjhavQ7/sxXQAUCL\n5B3WDwM8+mFqQM2wJB18NBWBSfGOFSMwVQyWv7Y/1AFr+PvNKVlw4RZ4G8VuJzXZ\n9v/+5zyKv8py66sGVoHPI+LGfIprAgMBAAECgYEAxcgX8PVrnrITiKwpJxReJbyL\nxnpOmw2i/zza3BseVzOebjNrhw/NQDWl0qhcvmBjvyR5IGiiwiwXq8bu8CBdhRiE\nw3vKf1iuVOKhH07RB2wvCaGbVlB/p15gYau3sTRn5nej0tjYHX7xa/St/DwPk2H/\nxYGTRhyYtNL6wdtMjYECQQD+LVVJf0rLnxyPADTcz7Wdb+FUX79nWtMlzQOEB09+\nJj4ie0kD0cIvTQFjV3pOsg3uW2khFpjg110TXpJJfPjhAkEAzL7RhhfDdL7Dn2zl\n1orUthcGa2pzEAmg1tGBNb1pOg7LbVHKSa3GOOwyPRsActoyrPw18/fXaJdEfByY\ne9kwywJAB7rHMjH9y01uZ+bgtKpYYo5JcvBqeLEpZKfkaHp0b2ioURIguU4Csr+L\nwEKjxIrjo5ECFHCEe6nw+arRlgyH4QJBAIfQmEn733LEzB0n7npXU2yKb363eSYN\nTPzSsoREZdXWVIjqtWYUeKXvwA+apryJEw5+qwdvwxslJI+zpE6bLusCQE6M1lO9\nN6A3PtQv7Z3XwrEE/sPEVv4M4VHj0YHLs/32UuSXq5taMizKILfis1Stry4WjRHp\nQxEqdLrIkb13NH8=\n-----END PRIVATE KEY-----",
+  "client_email": "unit-and-integration-testing@tink-test-infrastructure.iam.gserviceaccount.com",
+  "client_id": "111876397550362269561",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://accounts.google.com/o/oauth2/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/unit-and-integration-testing%40tink-test-infrastructure.iam.gserviceaccount.com"
+}
diff --git a/testing/cross_language/testdata/gcp/key_name.txt b/testing/cross_language/testdata/gcp/key_name.txt
new file mode 100644
index 0000000..60f69de
--- /dev/null
+++ b/testing/cross_language/testdata/gcp/key_name.txt
@@ -0,0 +1,2 @@
+projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key
+
diff --git a/testing/cross_language/testdata/gcp/key_name_bad.txt b/testing/cross_language/testdata/gcp/key_name_bad.txt
new file mode 100644
index 0000000..5bd7826
--- /dev/null
+++ b/testing/cross_language/testdata/gcp/key_name_bad.txt
@@ -0,0 +1,2 @@
+projects/non-existing-project/locations/global/keyRings/some-key-ring/cryptoKeys/aead-key
+
diff --git a/testing/cross_language/tink_config/BUILD.bazel b/testing/cross_language/tink_config/BUILD.bazel
new file mode 100644
index 0000000..49f97da
--- /dev/null
+++ b/testing/cross_language/tink_config/BUILD.bazel
@@ -0,0 +1,64 @@
+load("@rules_python//python:defs.bzl", "py_library")
+load("@pip_deps//:requirements.bzl", "requirement")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//visibility:public"],
+)
+
+py_library(
+    name = "tink_config",
+    srcs = ["__init__.py"],
+    deps = [
+        "_helpers",
+        ":_key_types",
+    ],
+)
+
+py_library(
+    name = "_key_types",
+    srcs = ["_key_types.py"],
+    deps = [
+        "@tink_py//tink/aead",
+        "@tink_py//tink/daead",
+        "@tink_py//tink/hybrid",
+        "@tink_py//tink/jwt",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/prf",
+        "@tink_py//tink/signature",
+        "@tink_py//tink/streaming_aead",
+    ],
+)
+
+py_test(
+    name = "_key_types_test",
+    srcs = ["_key_types_test.py"],
+    deps = [
+        ":_helpers",
+        ":_key_types",
+        requirement("absl-py"),
+    ],
+)
+
+py_library(
+    name = "_helpers",
+    srcs = ["_helpers.py"],
+    deps = [
+        ":_key_types",
+        "@tink_py//tink/proto:tink_py_pb2",
+    ],
+)
+
+py_test(
+    name = "_helpers_test",
+    srcs = ["_helpers_test.py"],
+    deps = [
+        ":_helpers",
+        requirement("absl-py"),
+        "//util/test_keys",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/hybrid",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/proto:tink_py_pb2",
+    ],
+)
diff --git a/testing/cross_language/tink_config/__init__.py b/testing/cross_language/tink_config/__init__.py
new file mode 100644
index 0000000..154eb78
--- /dev/null
+++ b/testing/cross_language/tink_config/__init__.py
@@ -0,0 +1,36 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Describes the capabilities of Tink in different languages.
+
+This package describes the state of Tink in all languages. The cross language
+tests obtain information from here and use the gRPC servers ensure the
+information is correct.
+
+TODO(tholenst): Move all files describing the configuration into this directory,
+and add functions to access the information from the outside.
+"""
+
+from tink_config import _helpers
+from tink_config import _key_types
+
+all_key_types = _helpers.all_key_types
+key_types_for_primitive = _helpers.key_types_for_primitive
+key_type_from_type_url = _helpers.key_type_from_type_url
+supported_languages_for_key_type = _helpers.supported_languages_for_key_type
+supported_languages_for_primitive = _helpers.supported_languages_for_primitive
+all_primitives = _helpers.all_primitives
+primitive_for_keytype = _helpers.primitive_for_keytype
+is_asymmetric_public_key_primitive = _helpers.is_asymmetric_public_key_primitive
+get_private_key_primitive = _helpers.get_private_key_primitive
+keyset_supported = _helpers.keyset_supported
diff --git a/testing/cross_language/tink_config/_helpers.py b/testing/cross_language/tink_config/_helpers.py
new file mode 100644
index 0000000..cd4075a
--- /dev/null
+++ b/testing/cross_language/tink_config/_helpers.py
@@ -0,0 +1,158 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Helper functions to access the information in this module.
+"""
+
+from typing import Any, Iterable, List
+
+from tink.proto import tink_pb2
+from tink_config import _key_types
+
+_TYPE_URL_PREFIX = 'type.googleapis.com/google.crypto.tink.'
+
+
+def all_key_types() -> List[str]:
+  """Returns all key types which Tink currently knows in short format.
+
+  The related TypeUrl equals the short format returned here, but prefixed with
+  type.googleapis.com/google.crypto.tink.
+  """
+  result = []
+  for key_types_for_single_primitive in _key_types.KEY_TYPES.values():
+    result += key_types_for_single_primitive
+  return result
+
+
+def key_types_for_primitive(p: Any) -> List[str]:
+  """Returns all key types for the given primitive which Tink currently has.
+
+  The related TypeUrl equals the short format returned here, but prefixed with
+  type.googleapis.com/google.crypto.tink.
+  Args:
+    p: The class of the primitive (e.g. tink.Aead)
+  Returns:
+    The list of key types (e.g. ['AesGcmKey', 'AesEaxKey'])
+  """
+  return list(_key_types.KEY_TYPES[p])
+
+
+def key_type_from_type_url(type_url: str) -> str:
+  """Returns the key type from a given TypeUrl.
+
+  If the TypeUrl is invalid throws an exception.
+  Args:
+    type_url: For example 'type.googleapis.com/google.crypto.tink.AesGcmKey'
+  Returns:
+    The stripped version (e.g. AesGcmKey)
+  Raises:
+    ValueError if the type url is unknown or in a bad format.
+  """
+  if not type_url.startswith(_TYPE_URL_PREFIX):
+    raise ValueError('Invalid type_url: ' + type_url)
+  # removeprefix does not yet exist in all our supported python versions.
+  key_type = type_url[len(_TYPE_URL_PREFIX):]
+  if key_type not in all_key_types():
+    raise ValueError('key type unknown: ' + key_type)
+  return key_type
+
+
+def supported_languages_for_key_type(key_type: str) -> List[str]:
+  """Returns the list of supported languages for a given KeyType.
+
+    Throws an except if the key type is unkonwn.
+  Args:
+    key_type: The shortened type URL (e.g. 'AesGcmKey')
+  Returns:
+    The list of languages which this key type supportes.
+  Raises:
+    ValueError if the key type is unknown.
+  """
+  if key_type not in all_key_types():
+    raise ValueError('key_type unknown: ' + key_type)
+  return _key_types.SUPPORTED_LANGUAGES[key_type]
+
+
+def supported_languages_for_primitive(p: Any) -> List[str]:
+  """Returns the list of languages which support a primitive.
+
+    Throws an except if the key type is unkonwn.
+  Args:
+    p: The Primitive
+  Returns:
+    The list of languages which this primitive supportes.
+  Raises:
+    ValueError if the key type is unknown.
+  """
+  result = set()
+  for key_type in key_types_for_primitive(p):
+    result.update(set(supported_languages_for_key_type(key_type)))
+  return list(result)
+
+
+def all_primitives() -> Iterable[Any]:
+  """Returns all the primitive types (such as tink.aead.Aead)."""
+  return [p for p, _ in _key_types.KEY_TYPES.items()]
+
+
+def primitive_for_keytype(key_type: str) -> Any:
+  """Returns the primitive for the given key type."""
+
+  for p, key_types in _key_types.KEY_TYPES.items():
+    if key_type in key_types:
+      return p
+  raise ValueError('Unknown key type: ' + key_type)
+
+
+def is_asymmetric_public_key_primitive(p: Any) -> bool:
+  """Returns true iff this p is the public part of an asymmetric scheme."""
+  return p in _key_types.PRIVATE_TO_PUBLIC_PRIMITIVE.values()
+
+
+def get_private_key_primitive(p: Any) -> Any:
+  """Returns the private primitive corresponding to this public part."""
+  inverted = {v: k for (k, v) in _key_types.PRIVATE_TO_PUBLIC_PRIMITIVE.items()}
+  return inverted[p]
+
+
+def _key_types_in_keyset(keyset: bytes) -> List[str]:
+  parsed_keyset = tink_pb2.Keyset.FromString(keyset)
+  type_urls = [k.key_data.type_url for k in parsed_keyset.key]
+  return [key_type_from_type_url(t) for t in type_urls]
+
+
+def keyset_supported(keyset: bytes, p: Any, lang: str) -> bool:
+  """Checks if the given keyset can be instantiated as 'p' in the 'lang'.
+
+  Returns true if it is expected that the keyset can be instantiated in language
+  'lang', according to the current configuration stored in tink_config. This
+  only looks at the key types in the keyset, and does not check if the keys
+  themselves are valid. It also does not check that the keyset is valid.
+
+  Args:
+    keyset: The serialized keyset
+    p: The primitive class, e.g. aead.Aead
+    lang: The language, e.g. 'python' or 'java'.
+
+  Returns:
+    True iff all key types are for this primitive and supported in the given
+    language.
+  """
+
+  key_types = _key_types_in_keyset(keyset)
+  for key_type in key_types:
+    if primitive_for_keytype(key_type) != p:
+      return False
+    if lang not in supported_languages_for_key_type(key_type):
+      return False
+  return True
diff --git a/testing/cross_language/tink_config/_helpers_test.py b/testing/cross_language/tink_config/_helpers_test.py
new file mode 100644
index 0000000..165a3da
--- /dev/null
+++ b/testing/cross_language/tink_config/_helpers_test.py
@@ -0,0 +1,130 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for _helpers."""
+
+from absl.testing import absltest
+from tink import aead
+from tink import hybrid
+from tink import mac
+
+from tink.proto import tink_pb2
+from tink_config import _helpers
+from util import test_keys
+
+
+def setUpModule():
+  aead.register()
+  mac.register()
+
+
+class HelpersTest(absltest.TestCase):
+
+  def test_get_all_key_types(self):
+    self.assertNotEmpty(_helpers.all_key_types())
+
+  def test_get_aead_key_types(self):
+    self.assertNotEmpty(_helpers.key_types_for_primitive(aead.Aead))
+
+  def test_key_type_from_type_url(self):
+    self.assertEqual(
+        _helpers.key_type_from_type_url(
+            'type.googleapis.com/google.crypto.tink.AesGcmKey'), 'AesGcmKey')
+
+  def test_key_type_from_type_url_wrong_prefix_throws(self):
+    with self.assertRaises(ValueError):
+      _helpers.key_type_from_type_url(
+          'type.googleapis.com/google.crypto.tinkAesGcmKey')
+
+  def test_key_type_from_type_url_wrong_key_type_throws(self):
+    with self.assertRaises(ValueError):
+      _helpers.key_type_from_type_url(
+          'type.googleapis.com/google.crypto.tink.InvalidKeyType29981')
+
+  def test_supported_languages_for_key_type(self):
+    self.assertCountEqual(
+        _helpers.supported_languages_for_key_type('AesGcmKey'),
+        ['cc', 'java', 'go', 'python'])
+
+  def test_supported_languages_for_key_type_invalid(self):
+    with self.assertRaises(ValueError):
+      _helpers.supported_languages_for_key_type('InvalidKeyType21b9a1')
+
+  def test_supported_languages_for_primitive(self):
+    self.assertCountEqual(
+        _helpers.supported_languages_for_primitive(aead.Aead),
+        ['cc', 'java', 'go', 'python'])
+
+  def test_supported_languages_for_primitive_invalid(self):
+    with self.assertRaises(KeyError):
+      _helpers.supported_languages_for_primitive('not a primitive, a string')
+
+  def test_all_primitives(self):
+    self.assertContainsSubset(
+        [aead.Aead, hybrid.HybridEncrypt, hybrid.HybridEncrypt],
+        _helpers.all_primitives())
+
+  def test_primitive_for_keytype(self):
+    self.assertEqual(_helpers.primitive_for_keytype('AesGcmKey'), aead.Aead)
+
+  def test_primitive_for_keytype_throws_invalid(self):
+    with self.assertRaises(ValueError):
+      _helpers.primitive_for_keytype('InvalidKeyType776611')
+
+  def test_is_asymmetric_public_key_primitive(self):
+    self.assertFalse(_helpers.is_asymmetric_public_key_primitive(aead.Aead))
+    self.assertFalse(
+        _helpers.is_asymmetric_public_key_primitive(hybrid.HybridDecrypt))
+    self.assertTrue(
+        _helpers.is_asymmetric_public_key_primitive(hybrid.HybridEncrypt))
+
+  def test_get_private_key_primitive(self):
+    self.assertEqual(
+        _helpers.get_private_key_primitive(hybrid.HybridEncrypt),
+        hybrid.HybridDecrypt)
+
+  def test_keyset_supported_true(self):
+    keyset = test_keys.some_keyset_for_primitive(aead.Aead)
+    self.assertTrue(_helpers.keyset_supported(keyset, aead.Aead, 'python'))
+
+  def test_keyset_supported_keyset_wrong_primitive_false(self):
+    keyset = test_keys.some_keyset_for_primitive(aead.Aead)
+    self.assertFalse(_helpers.keyset_supported(keyset, mac.Mac, 'python'))
+
+  def test_keyset_supported_keyset_wrong_language_false(self):
+    keyset = test_keys.some_keyset_for_primitive(aead.Aead)
+    self.assertFalse(
+        _helpers.keyset_supported(keyset, aead.Aead, 'non-existing-language'))
+
+  def test_keyset_two_keys_supported_true(self):
+    keyset = test_keys.some_keyset_for_primitive(aead.Aead)
+    parsed_keyset = tink_pb2.Keyset.FromString(keyset)
+    key0 = parsed_keyset.key[0]
+    parsed_keyset.key.append(key0)
+    parsed_keyset.key[1].key_id += 1
+    self.assertTrue(
+        _helpers.keyset_supported(parsed_keyset.SerializeToString(), aead.Aead,
+                                  'python'))
+
+  def test_keyset_two_keys_unsupported_false(self):
+    keyset0 = test_keys.some_keyset_for_primitive(aead.Aead)
+    keyset1 = test_keys.some_keyset_for_primitive(mac.Mac)
+    parsed_keyset0 = tink_pb2.Keyset.FromString(keyset0)
+    parsed_keyset1 = tink_pb2.Keyset.FromString(keyset1)
+    parsed_keyset0.key.append(parsed_keyset1.key[0])
+    self.assertFalse(
+        _helpers.keyset_supported(parsed_keyset0.SerializeToString(), aead.Aead,
+                                  'python'))
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/tink_config/_key_types.py b/testing/cross_language/tink_config/_key_types.py
new file mode 100644
index 0000000..bf7e6fc
--- /dev/null
+++ b/testing/cross_language/tink_config/_key_types.py
@@ -0,0 +1,141 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""This file gives an overview of the key types supported in Tink.
+
+This file is the authorative reference of which key types Tink currently
+understands in which language, and for which primitive. The correctness of this
+file is checked by the cross language tests.
+"""
+
+from tink import aead
+from tink import daead
+from tink import hybrid
+from tink import jwt
+from tink import mac
+from tink import prf
+from tink import signature
+from tink import streaming_aead
+
+# Map from the primitives to the KeyTypes (without the prefix
+# 'type.googleapis.com/google.crypto.tink.')
+KEY_TYPES = {
+    aead.Aead: (
+        'AesEaxKey',
+        'AesGcmKey',
+        'AesGcmSivKey',
+        'AesCtrHmacAeadKey',
+        'ChaCha20Poly1305Key',
+        'XChaCha20Poly1305Key',
+        'KmsAeadKey',
+        'KmsEnvelopeAeadKey',
+    ),
+    daead.DeterministicAead: ('AesSivKey',),
+    streaming_aead.StreamingAead: (
+        'AesCtrHmacStreamingKey',
+        'AesGcmHkdfStreamingKey',
+    ),
+    hybrid.HybridDecrypt: ('EciesAeadHkdfPrivateKey', 'HpkePrivateKey'),
+    hybrid.HybridEncrypt: ('EciesAeadHkdfPublicKey', 'HpkePublicKey'),
+    mac.Mac: (
+        'AesCmacKey',
+        'HmacKey',
+    ),
+    signature.PublicKeySign: (
+        'EcdsaPrivateKey',
+        'Ed25519PrivateKey',
+        'RsaSsaPkcs1PrivateKey',
+        'RsaSsaPssPrivateKey',
+    ),
+    signature.PublicKeyVerify: (
+        'EcdsaPublicKey',
+        'Ed25519PublicKey',
+        'RsaSsaPkcs1PublicKey',
+        'RsaSsaPssPublicKey',
+    ),
+    prf.PrfSet: (
+        'AesCmacPrfKey',
+        'HmacPrfKey',
+        'HkdfPrfKey',
+    ),
+    jwt.JwtMac: ('JwtHmacKey',),
+    jwt.JwtPublicKeySign: (
+        'JwtEcdsaPrivateKey',
+        'JwtRsaSsaPkcs1PrivateKey',
+        'JwtRsaSsaPssPrivateKey',
+    ),
+    jwt.JwtPublicKeyVerify: (
+        'JwtEcdsaPublicKey',
+        'JwtRsaSsaPkcs1PublicKey',
+        'JwtRsaSsaPssPublicKey',
+    )
+}
+
+# Map from Asymmetric Private Primitive to Asymmetric Public Primitive
+PRIVATE_TO_PUBLIC_PRIMITIVE = {
+    hybrid.HybridDecrypt: hybrid.HybridEncrypt,
+    signature.PublicKeySign: signature.PublicKeyVerify,
+    jwt.JwtPublicKeySign: jwt.JwtPublicKeyVerify,
+}
+
+# Map from Private Key Types to Public Key Types
+PRIVATE_TO_PUBLIC_KEY = {
+    'EciesAeadHkdfPrivateKey': 'EciesAeadHkdfPublicKey',
+    'HpkePrivateKey': 'HpkePublicKey',
+    'EcdsaPrivateKey': 'EcdsaPublicKey',
+    'Ed25519PrivateKey': 'Ed25519PublicKey',
+    'RsaSsaPkcs1PrivateKey': 'RsaSsaPkcs1PublicKey',
+    'RsaSsaPssPrivateKey': 'RsaSsaPssPublicKey',
+    'JwtEcdsaPrivateKey': 'JwtEcdsaPublicKey',
+    'JwtRsaSsaPkcs1PrivateKey': 'JwtRsaSsaPkcs1PublicKey',
+    'JwtRsaSsaPssPrivateKey': 'JwtRsaSsaPssPublicKey',
+}
+
+# All languages that are supported by a KeyType
+SUPPORTED_LANGUAGES = {
+    'AesEaxKey': ['cc', 'java', 'python'],
+    'AesGcmKey': ['cc', 'java', 'go', 'python'],
+    'AesGcmSivKey': ['cc', 'go', 'python'],
+    'AesCtrHmacAeadKey': ['cc', 'java', 'go', 'python'],
+    'ChaCha20Poly1305Key': ['java', 'go'],
+    'XChaCha20Poly1305Key': ['cc', 'java', 'go', 'python'],
+    'KmsAeadKey': ['cc', 'java', 'go', 'python'],  # go only supported in tests.
+    'KmsEnvelopeAeadKey': ['cc', 'java', 'go', 'python'],
+    'AesSivKey': ['cc', 'java', 'go', 'python'],
+    'AesCtrHmacStreamingKey': ['cc', 'java', 'go', 'python'],
+    'AesGcmHkdfStreamingKey': ['cc', 'java', 'go', 'python'],
+    'EciesAeadHkdfPrivateKey': ['cc', 'java', 'go', 'python'],
+    'EciesAeadHkdfPublicKey': ['cc', 'java', 'go', 'python'],
+    'HpkePrivateKey': ['cc', 'java', 'go', 'python'],
+    'HpkePublicKey': ['cc', 'java', 'go', 'python'],
+    'AesCmacKey': ['cc', 'java', 'go', 'python'],
+    'HmacKey': ['cc', 'java', 'go', 'python'],
+    'EcdsaPrivateKey': ['cc', 'java', 'go', 'python'],
+    'EcdsaPublicKey': ['cc', 'java', 'go', 'python'],
+    'Ed25519PrivateKey': ['cc', 'java', 'go', 'python'],
+    'Ed25519PublicKey': ['cc', 'java', 'go', 'python'],
+    'RsaSsaPkcs1PrivateKey': ['cc', 'java', 'go', 'python'],
+    'RsaSsaPkcs1PublicKey': ['cc', 'java', 'go', 'python'],
+    'RsaSsaPssPrivateKey': ['cc', 'java', 'go', 'python'],
+    'RsaSsaPssPublicKey': ['cc', 'java', 'go', 'python'],
+    'AesCmacPrfKey': ['cc', 'java', 'go', 'python'],
+    'HmacPrfKey': ['cc', 'java', 'go', 'python'],
+    'HkdfPrfKey': ['cc', 'java', 'go', 'python'],
+    'JwtHmacKey': ['cc', 'java', 'go', 'python'],
+    'JwtEcdsaPrivateKey': ['cc', 'java', 'go', 'python'],
+    'JwtEcdsaPublicKey': ['cc', 'java', 'go', 'python'],
+    'JwtRsaSsaPkcs1PrivateKey': ['cc', 'java', 'go', 'python'],
+    'JwtRsaSsaPkcs1PublicKey': ['cc', 'java', 'go', 'python'],
+    'JwtRsaSsaPssPrivateKey': ['cc', 'java', 'go', 'python'],
+    'JwtRsaSsaPssPublicKey': ['cc', 'java', 'go', 'python'],
+}
diff --git a/testing/cross_language/tink_config/_key_types_test.py b/testing/cross_language/tink_config/_key_types_test.py
new file mode 100644
index 0000000..8b0bee9
--- /dev/null
+++ b/testing/cross_language/tink_config/_key_types_test.py
@@ -0,0 +1,60 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for _key_types."""
+
+from absl.testing import absltest
+
+from tink_config import _helpers
+from tink_config import _key_types
+
+
+class KeyTypesTest(absltest.TestCase):
+  """Sanity tests for _key_types.py.
+
+  This verifies configuration invariants which we always expect to be satisfied.
+  For example, each key type which corresponds to a primitive which is in
+  PRIVATE_TO_PUBLIC_PRIMITIVE should be correspondingly in
+  PRIVATE_TO_PUBLIC_KEY.
+  """
+
+  def test_private_keytype_maps_to_public_key_types(self):
+    """A test.
+
+    Tests that every item in PRIVATE_TO_PUBLIC_KEY corresponds to an item
+    in PRIVATE_TO_PUBLIC_PRIMITIVE.
+    """
+
+    for key_types in _key_types.PRIVATE_TO_PUBLIC_KEY.items():
+      private_key_type, public_key_type = key_types
+      private_primitive = _helpers.primitive_for_keytype(private_key_type)
+      public_primitive = _helpers.primitive_for_keytype(public_key_type)
+      self.assertIn((private_primitive, public_primitive),
+                    _key_types.PRIVATE_TO_PUBLIC_PRIMITIVE.items())
+
+  def test_all_private_keytypes_have_public_keytypes(self):
+    """A test.
+
+    Tests that for each pair in PRIVATE_TO_PUBLIC_PRIMITIVE, all corresponding
+    key types map to each other.
+    """
+    for primitives in _key_types.PRIVATE_TO_PUBLIC_PRIMITIVE.items():
+      private_primitive, public_primitive = primitives
+      for private_key_type in _key_types.KEY_TYPES[private_primitive]:
+        public_key_type = _key_types.PRIVATE_TO_PUBLIC_KEY[private_key_type]
+        self.assertIn(public_key_type, _key_types.KEY_TYPES[public_primitive])
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/util/BUILD.bazel b/testing/cross_language/util/BUILD.bazel
index cd08bc7..347e505 100644
--- a/testing/cross_language/util/BUILD.bazel
+++ b/testing/cross_language/util/BUILD.bazel
@@ -11,36 +11,7 @@
 
 python_grpc_library(
     name = "testing_api_python_library",
-    protos = ["//proto:testing_api_proto"],
-)
-
-py_library(
-    name = "supported_key_types",
-    testonly = 1,
-    srcs = ["supported_key_types.py"],
-    deps = [
-        "@tink_py//tink/aead",
-        "@tink_py//tink/daead",
-        "@tink_py//tink/hybrid",
-        "@tink_py//tink/jwt",
-        "@tink_py//tink/mac",
-        "@tink_py//tink/prf",
-        "@tink_py//tink/proto:common_py_pb2",
-        "@tink_py//tink/proto:tink_py_pb2",
-        "@tink_py//tink/signature",
-        "@tink_py//tink/streaming_aead",
-    ],
-)
-
-py_test(
-    name = "supported_key_types_test",
-    srcs = ["supported_key_types_test.py"],
-    python_version = "PY3",
-    srcs_version = "PY3",
-    deps = [
-        ":supported_key_types",
-        requirement("absl-py"),
-    ],
+    protos = ["//protos:testing_api_proto"],
 )
 
 py_library(
@@ -79,11 +50,18 @@
 py_library(
     name = "testing_servers",
     srcs = ["testing_servers.py"],
+    data = [
+        "//testdata/aws:credentials",
+        "//testdata/gcp:credentials",
+    ],
     srcs_version = "PY3",
     deps = [
         ":_primitives",
+        ":key_util",
         ":testing_api_python_library",
         "@com_google_protobuf//:protobuf_python",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/proto:tink_py_pb2",
         requirement("absl-py"),
         "@org_python_pypi_portpicker//:portpicker",
     ],
@@ -100,6 +78,7 @@
     deps = [
         ":testing_api_python_library",
         requirement("absl-py"),
+        "//util/test_keys",
         "@org_python_pypi_portpicker//:portpicker",
         "@tink_py//tink/aead",
         "@tink_py//tink/daead",
@@ -163,3 +142,35 @@
         "@tink_py//tink/proto:tink_py_pb2",
     ],
 )
+
+py_library(
+    name = "utilities",
+    srcs = ["utilities.py"],
+    deps = [
+        "//tink_config",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/daead",
+        "@tink_py//tink/hybrid",
+        "@tink_py//tink/jwt",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/prf",
+        "@tink_py//tink/proto:common_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/signature",
+        "@tink_py//tink/streaming_aead",
+    ],
+)
+
+py_test(
+    name = "utilities_test",
+    srcs = ["utilities_test.py"],
+    deps = [
+        ":utilities",
+        "//tink_config",
+        "//util/test_keys",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/proto:tink_py_pb2",
+        requirement("absl-py"),
+    ],
+)
diff --git a/testing/cross_language/util/_primitives.py b/testing/cross_language/util/_primitives.py
index 5cbe582..9c3ab3b 100644
--- a/testing/cross_language/util/_primitives.py
+++ b/testing/cross_language/util/_primitives.py
@@ -16,7 +16,7 @@
 import datetime
 import io
 import json
-from typing import BinaryIO, Optional, Mapping, Tuple
+from typing import BinaryIO, Dict, Optional, Mapping, Tuple
 
 import tink
 from tink import aead
@@ -29,8 +29,8 @@
 from tink import streaming_aead
 
 from tink.proto import tink_pb2
-from proto import testing_api_pb2
-from proto import testing_api_pb2_grpc
+from protos import testing_api_pb2
+from protos import testing_api_pb2_grpc
 
 
 def key_template(stub: testing_api_pb2_grpc.KeysetStub,
@@ -139,14 +139,23 @@
   """Wraps AEAD service stub into an Aead primitive."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.AeadStub,
-               keyset: bytes) -> None:
+               keyset: bytes, annotations: Optional[Dict[str, str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    self._annotations = annotations
+    creation_response = self._stub.Create(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset,
+                annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def encrypt(self, plaintext: bytes, associated_data: bytes) -> bytes:
     enc_request = testing_api_pb2.AeadEncryptRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
         plaintext=plaintext,
         associated_data=associated_data)
     enc_response = self._stub.Encrypt(enc_request)
@@ -156,7 +165,8 @@
 
   def decrypt(self, ciphertext: bytes, associated_data: bytes) -> bytes:
     dec_request = testing_api_pb2.AeadDecryptRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
         ciphertext=ciphertext,
         associated_data=associated_data)
     dec_response = self._stub.Decrypt(dec_request)
@@ -169,17 +179,25 @@
   """Wraps DAEAD services stub into an DeterministicAead primitive."""
 
   def __init__(self, lang: str,
-               stub: testing_api_pb2_grpc.DeterministicAeadStub,
-               keyset: bytes) -> None:
+               stub: testing_api_pb2_grpc.DeterministicAeadStub, keyset: bytes,
+               annotations: Optional[Dict[str, str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    self._annotations = annotations
+    creation_response = self._stub.Create(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                annotations=self._annotations, serialized_keyset=self._keyset)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def encrypt_deterministically(self, plaintext: bytes,
                                 associated_data: bytes) -> bytes:
     """Encrypts."""
     enc_request = testing_api_pb2.DeterministicAeadEncryptRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
         plaintext=plaintext,
         associated_data=associated_data)
     enc_response = self._stub.EncryptDeterministically(enc_request)
@@ -191,7 +209,8 @@
                                 associated_data: bytes) -> bytes:
     """Decrypts."""
     dec_request = testing_api_pb2.DeterministicAeadDecryptRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
         ciphertext=ciphertext,
         associated_data=associated_data)
     dec_response = self._stub.DecryptDeterministically(dec_request)
@@ -208,11 +227,18 @@
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    creation_response = self._stub.Create(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def new_encrypting_stream(self, plaintext: BinaryIO,
                             associated_data: bytes) -> BinaryIO:
     enc_request = testing_api_pb2.StreamingAeadEncryptRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset),
         plaintext=plaintext.read(),
         associated_data=associated_data)
     enc_response = self._stub.Encrypt(enc_request)
@@ -223,7 +249,8 @@
   def new_decrypting_stream(self, ciphertext: BinaryIO,
                             associated_data: bytes) -> BinaryIO:
     dec_request = testing_api_pb2.StreamingAeadDecryptRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset),
         ciphertext=ciphertext.read(),
         associated_data=associated_data)
     dec_response = self._stub.Decrypt(dec_request)
@@ -236,13 +263,23 @@
   """Wraps MAC service stub into an Mac primitive."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.MacStub,
-               keyset: bytes) -> None:
+               keyset: bytes, annotations: Optional[Dict[str, str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    self._annotations = annotations
+    creation_response = self._stub.Create(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset, annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def compute_mac(self, data: bytes) -> bytes:
-    request = testing_api_pb2.ComputeMacRequest(keyset=self._keyset, data=data)
+    request = testing_api_pb2.ComputeMacRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
+        data=data)
     response = self._stub.ComputeMac(request)
     if response.err:
       raise tink.TinkError(response.err)
@@ -250,7 +287,10 @@
 
   def verify_mac(self, mac_value: bytes, data: bytes) -> None:
     request = testing_api_pb2.VerifyMacRequest(
-        keyset=self._keyset, mac_value=mac_value, data=data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
+        mac_value=mac_value,
+        data=data)
     response = self._stub.VerifyMac(request)
     if response.err:
       raise tink.TinkError(response.err)
@@ -260,14 +300,25 @@
   """Implements the HybridEncrypt primitive using a hybrid service stub."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.HybridStub,
-               public_handle: bytes) -> None:
+               public_handle: bytes, annotations: Optional[Dict[str,
+                                                                str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._public_handle = public_handle
+    self._annotations = annotations
+    creation_response = self._stub.CreateHybridEncrypt(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._public_handle,
+                annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def encrypt(self, plaintext: bytes, context_info: bytes) -> bytes:
     enc_request = testing_api_pb2.HybridEncryptRequest(
-        public_keyset=self._public_handle,
+        public_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._public_handle,
+            annotations=self._annotations),
         plaintext=plaintext,
         context_info=context_info)
     enc_response = self._stub.Encrypt(enc_request)
@@ -280,14 +331,25 @@
   """Implements the HybridDecrypt primitive using a hybrid service stub."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.HybridStub,
-               private_handle: bytes) -> None:
+               private_handle: bytes, annotations: Optional[Dict[str,
+                                                                 str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._private_handle = private_handle
+    self._annotations = annotations
+    creation_response = self._stub.CreateHybridDecrypt(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._private_handle,
+                annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def decrypt(self, ciphertext: bytes, context_info: bytes) -> bytes:
     dec_request = testing_api_pb2.HybridDecryptRequest(
-        private_keyset=self._private_handle,
+        private_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._private_handle,
+            annotations=self._annotations),
         ciphertext=ciphertext,
         context_info=context_info)
     dec_response = self._stub.Decrypt(dec_request)
@@ -300,14 +362,26 @@
   """Implements the PublicKeySign primitive using a signature service stub."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.SignatureStub,
-               private_handle: bytes) -> None:
+               private_handle: bytes, annotations: Optional[Dict[str,
+                                                                 str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._private_handle = private_handle
+    self._annotations = annotations
+    creation_response = self._stub.CreatePublicKeySign(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._private_handle,
+                annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def sign(self, data: bytes) -> bytes:
     request = testing_api_pb2.SignatureSignRequest(
-        private_keyset=self._private_handle, data=data)
+        private_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._private_handle,
+            annotations=self._annotations),
+        data=data)
     response = self._stub.Sign(request)
     if response.err:
       raise tink.TinkError(response.err)
@@ -318,14 +392,27 @@
   """Implements the PublicKeyVerify primitive using a signature service stub."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.SignatureStub,
-               public_handle: bytes) -> None:
+               public_handle: bytes, annotations: Optional[Dict[str,
+                                                                str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._public_handle = public_handle
+    self._annotations = annotations
+    creation_response = self._stub.CreatePublicKeyVerify(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._public_handle,
+                annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
-  def verify(self, signature: bytes, data: bytes) -> None:
+  def verify(self, signature: bytes, data: bytes) -> None:  # pytype: disable=signature-mismatch  # overriding-return-type-checks
     request = testing_api_pb2.SignatureVerifyRequest(
-        public_keyset=self._public_handle, signature=signature, data=data)
+        public_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._public_handle,
+            annotations=self._annotations),
+        signature=signature,
+        data=data)
     response = self._stub.Verify(request)
     if response.err:
       raise tink.TinkError(response.err)
@@ -335,15 +422,18 @@
   """Implements a Prf from a PrfSet service stub."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.PrfSetStub,
-               keyset: bytes, key_id: int) -> None:
+               keyset: bytes, key_id: int,
+               annotations: Optional[Dict[str, str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
     self._key_id = key_id
+    self._annotations = annotations
 
   def compute(self, input_data: bytes, output_length: int) -> bytes:
     request = testing_api_pb2.PrfSetComputeRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset, annotations=self._annotations),
         key_id=self._key_id,
         input_data=input_data,
         output_length=output_length)
@@ -357,24 +447,34 @@
   """Implements a PrfSet from a PrfSet service stub."""
 
   def __init__(self, lang: str, stub: testing_api_pb2_grpc.PrfSetStub,
-               keyset: bytes) -> None:
+               keyset: bytes, annotations: Optional[Dict[str, str]]) -> None:
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
     self._key_ids_initialized = False
     self._primary_key_id = None
     self._prfs = None
+    self._annotations = annotations
+    creation_response = self._stub.Create(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset, annotations=self._annotations)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def _initialize_key_ids(self) -> None:
     if not self._key_ids_initialized:
-      request = testing_api_pb2.PrfSetKeyIdsRequest(keyset=self._keyset)
+      request = testing_api_pb2.PrfSetKeyIdsRequest(
+          annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+              serialized_keyset=self._keyset, annotations=self._annotations))
       response = self._stub.KeyIds(request)
       if response.err:
         raise tink.TinkError(response.err)
       self._primary_key_id = response.output.primary_key_id
       self._prfs = {}
       for key_id in response.output.key_id:
-        self._prfs[key_id] = _Prf(self.lang, self._stub, self._keyset, key_id)
+        self._prfs[key_id] = _Prf(self.lang, self._stub, self._keyset, key_id,
+                                  self._annotations)
       self._key_ids_initialized = True
 
   def primary_id(self) -> int:
@@ -537,10 +637,18 @@
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    creation_response = self._stub.CreateJwtMac(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def compute_mac_and_encode(self, raw_jwt: jwt.RawJwt) -> str:
     request = testing_api_pb2.JwtSignRequest(
-        keyset=self._keyset, raw_jwt=raw_jwt_to_proto(raw_jwt))
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset),
+        raw_jwt=raw_jwt_to_proto(raw_jwt))
     response = self._stub.ComputeMacAndEncode(request)
     if response.err:
       raise tink.TinkError(response.err)
@@ -548,8 +656,20 @@
 
   def verify_mac_and_decode(self, signed_compact_jwt: str,
                             validator: jwt.JwtValidator) -> jwt.VerifiedJwt:
+    """verifies and decodes a jwt in compact serialization using a mac.
+
+    Args:
+      signed_compact_jwt: the sign jwt in compact serialization form.
+      validator: validator to validate the jwt.
+
+    Returns:
+
+    Raises:
+      tink.TinkError: if verification or validation fails.
+    """
     request = testing_api_pb2.JwtVerifyRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset),
         validator=jwt_validator_to_proto(validator),
         signed_compact_jwt=signed_compact_jwt)
     response = self._stub.VerifyMacAndDecode(request)
@@ -566,10 +686,18 @@
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    creation_response = self._stub.CreateJwtPublicKeySign(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def sign_and_encode(self, raw_jwt: jwt.RawJwt) -> str:
     request = testing_api_pb2.JwtSignRequest(
-        keyset=self._keyset, raw_jwt=raw_jwt_to_proto(raw_jwt))
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset),
+        raw_jwt=raw_jwt_to_proto(raw_jwt))
     response = self._stub.PublicKeySignAndEncode(request)
     if response.err:
       raise tink.TinkError(response.err)
@@ -584,11 +712,29 @@
     self.lang = lang
     self._stub = stub
     self._keyset = keyset
+    creation_response = self._stub.CreateJwtPublicKeyVerify(
+        testing_api_pb2.CreationRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=self._keyset)))
+    if creation_response.err:
+      raise tink.TinkError(creation_response.err)
 
   def verify_and_decode(self, signed_compact_jwt: str,
                         validator: jwt.JwtValidator) -> jwt.VerifiedJwt:
+    """verifies and decodes a jwt in compact serialization using a digital signature.
+
+    Args:
+      signed_compact_jwt: the sign jwt in compact serialization form.
+      validator: validator to validate the jwt.
+
+    Returns:
+
+    Raises:
+      tink.TinkError: if verification or validation fails.
+    """
     request = testing_api_pb2.JwtVerifyRequest(
-        keyset=self._keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=self._keyset),
         validator=jwt_validator_to_proto(validator),
         signed_compact_jwt=signed_compact_jwt)
     response = self._stub.PublicKeyVerifyAndDecode(request)
diff --git a/testing/cross_language/util/_primitives_test.py b/testing/cross_language/util/_primitives_test.py
index 1a5ee37..3d526a2 100644
--- a/testing/cross_language/util/_primitives_test.py
+++ b/testing/cross_language/util/_primitives_test.py
@@ -19,7 +19,7 @@
 from tink import jwt
 
 from util import _primitives
-from proto import testing_api_pb2
+from protos import testing_api_pb2
 
 
 class PrimitivesTest(absltest.TestCase):
diff --git a/testing/cross_language/util/key_util.py b/testing/cross_language/util/key_util.py
index 451e281..8a97d78 100644
--- a/testing/cross_language/util/key_util.py
+++ b/testing/cross_language/util/key_util.py
@@ -42,11 +42,14 @@
 not equal, the function tries to output a meaningfull error message.
 """
 
+import copy
 from typing import Any, Optional
 
+# copybara:tink_placeholder(encoder)
 from google.protobuf import descriptor
 from google.protobuf import message
 from google.protobuf import text_encoding
+from google.protobuf import text_format as proto_text_format
 from tink.proto import aes_cmac_pb2
 from tink.proto import aes_cmac_prf_pb2
 from tink.proto import aes_ctr_hmac_aead_pb2
@@ -166,12 +169,13 @@
 
 def _text_format_field(value: Any,
                        field: descriptor.FieldDescriptor,
-                       indent: str, remove_value: bool) -> str:
+                       indent: str) -> str:
   """Returns a text formated proto field."""
   if field.type == TYPE_MESSAGE:
     output = [
         indent + field.name + ' {',
-        _text_format_message(value, indent + '  ', remove_value), indent + '}'
+        _normalize_and_text_format_message(value, indent + '  '),
+        indent + '}'
     ]
     return '\n'.join(output)
   elif field.type == TYPE_ENUM:
@@ -184,64 +188,76 @@
     return indent + field.name + ': ' + str(value)
 
 
-def _text_format_message(msg: message.Message, indent: str,
-                         remove_value: bool) -> str:
-  """Returns a text formated proto message.
+def _normalize_and_text_format_message(msg: message.Message,
+                                       indent: str) -> str:
+  """Returns a text formated proto message and changes msg to be canonical.
 
   Args:
     msg: the proto to be formated.
     indent: the indentation prefix of each line in the output.
-    remove_value: if True, replaced the value fields of tink's custom any protos
-      with '<removed>'. This is useful to compare protos, but should not be used
-      otherwise.
 
   Returns:
     A proto text format output, where serialized fields are deserialized in
     a comment.
   """
   output = []
-  fields = msg.DESCRIPTOR.fields
-  if (len(fields) >= 2 and fields[0].name == 'type_url' and
-      fields[1].name == 'value'):
-    # special case for custom 'any' proto.
-    if getattr(msg, 'type_url'):
-      type_url = getattr(msg, 'type_url')
-      output.append(
-          _text_format_field(type_url, fields[0], indent, remove_value))
-      if getattr(msg, 'value'):
-        value = getattr(msg, 'value')
-        if msg.DESCRIPTOR.full_name == 'google.crypto.tink.KeyTemplate':
-          # In KeyTemplates, type_url does not match the proto type used.
-          proto_type = KeyProto.format_from_url(type_url)
-        else:
-          proto_type = KeyProto.from_url(type_url)
-        # parse 'value' and text format the content in a comment.
-        field_proto = proto_type.FromString(value)
-        output.append(indent + '# value: [' + TYPE_PREFIX +
-                      proto_type.DESCRIPTOR.full_name + '] {')
-        output.append(
-            _text_format_message(field_proto, indent + '#   ', remove_value))
-        output.append(indent + '# }')
-        if remove_value:
-          output.append(
-              _text_format_field('<removed>', fields[1], indent, remove_value))
-        else:
-          output.append(
-              _text_format_field(value, fields[1], indent, remove_value))
+  fields = list(msg.DESCRIPTOR.fields)
+  # special case for Tinks custom 'any' proto.
+  if (msg.DESCRIPTOR.full_name == 'google.crypto.tink.KeyTemplate' or
+      msg.DESCRIPTOR.full_name == 'google.crypto.tink.KeyData'):
+    type_url = getattr(msg, 'type_url')  # Pytype requires to use getattr
+    output.append(
+        _text_format_field(type_url, fields[0], indent))
+    value = getattr(msg, 'value')
+    if msg.DESCRIPTOR.full_name == 'google.crypto.tink.KeyTemplate':
+      # In KeyTemplates, type_url does not match the proto type used.
+      proto_type = KeyProto.format_from_url(type_url)
+    else:
+      proto_type = KeyProto.from_url(type_url)
+    # parse 'value' and text format the content in a comment.
+    field_proto = proto_type.FromString(value)
+    output.append(indent + '# value: [' + TYPE_PREFIX +
+                  proto_type.DESCRIPTOR.full_name + '] {')
+    formatted_message = _normalize_and_text_format_message(
+        field_proto, indent + '#   ')
+    if formatted_message:
+      output.append(formatted_message)
+    output.append(indent + '# }')
+    # Serialize message again so it is canonicalized
+    # We require here that proto serialization is in increasing field order
+    # (Tink protos are basically unchangeable, so we don't need to worry about
+    # unknown fields). This is not guaranteed by proto, but is currently the
+    # case. If this ever changes we either hopefully have already a better
+    # solution in Tink, or else the proto team provides us with a reflection
+    # based API to do this (as they do in C++.) In this case, we simply use the
+    # slow API here.
+    value = field_proto.SerializeToString(deterministic = True)
+    setattr(msg, 'value', value)
+    output.append(
+        _text_format_field(value, fields[1], indent))
     fields = fields[2:]
   for field in fields:
     if field.label == LABEL_REPEATED:
       for value in getattr(msg, field.name):
-        output.append(_text_format_field(value, field, indent, remove_value))
+        output.append(_text_format_field(value, field, indent))
     else:
       output.append(
           _text_format_field(
-              getattr(msg, field.name), field, indent, remove_value))
+              getattr(msg, field.name), field, indent))
   return '\n'.join(output)
 
 
 def text_format(msg: message.Message) -> str:
-  return _text_format_message(msg, '', False)
+  msgcopy = copy.deepcopy(msg)
+  return _normalize_and_text_format_message(msgcopy, '')
+
+
+def parse_text_format(serialized: str, msg: message.Message) -> None:
+  # Different from binary parsing, text_format.Parse does not Clear the message.
+  msg.Clear()
+  proto_text_format.Parse(serialized, msg)
+  serialized_copy = text_format(msg)
+  assert serialized_copy == serialized, serialized_copy
 
 
 def assert_tink_proto_equal(self,
@@ -249,7 +265,10 @@
                             b: message.Message,
                             msg: Optional[str] = None) -> None:
   """Fails with a useful error if a and b aren't equal."""
+  a_copy = copy.deepcopy(a)
+  b_copy = copy.deepcopy(b)
+
   self.assertMultiLineEqual(
-      _text_format_message(a, '', True),
-      _text_format_message(b, '', True),
+      _normalize_and_text_format_message(a_copy, ''),
+      _normalize_and_text_format_message(b_copy, ''),
       msg=msg)
diff --git a/testing/cross_language/util/key_util_test.py b/testing/cross_language/util/key_util_test.py
index 81f69c7..945197c 100644
--- a/testing/cross_language/util/key_util_test.py
+++ b/testing/cross_language/util/key_util_test.py
@@ -20,34 +20,55 @@
 from tink.proto import tink_pb2
 from util import key_util
 
-KEYSET_A = r"""
-  primary_key_id: 4223424880
-  key {
-    key_data {
-      type_url: "type.googleapis.com/google.crypto.tink.EcdsaPublicKey"
-      value: "\" \321L\232\025n*\335.w\023\311\242\035\252^B\024w\305lwa\261K\341\037\2353Yy\3521\032 ^}`\277\341|\364\355\013\035>\275&\3726fu\003\364\373\267\213e\320\030(\234\246b\352\362\311\022\006\030\002\020\002\010\003"
-      key_material_type: ASYMMETRIC_PUBLIC
-    }
-    status: ENABLED
-    key_id: 4223424880
-    output_prefix_type: CRUNCHY
-  }"""
-
-# The same keyset as A, but here EcdsaPublicKey is encoded differently
-KEYSET_B = r"""
-  primary_key_id: 4223424880
-  key {
-    key_data {
-      type_url: "type.googleapis.com/google.crypto.tink.EcdsaPublicKey"
-      value: "\022\006\010\003\020\002\030\002\032 ^}`\277\341|\364\355\013\035>\275&\3726fu\003\364\373\267\213e\320\030(\234\246b\352\362\311\" \321L\232\025n*\335.w\023\311\242\035\252^B\024w\305lwa\261K\341\037\2353Yy\3521"
-      key_material_type: ASYMMETRIC_PUBLIC
-    }
-    status: ENABLED
-    key_id: 4223424880
-    output_prefix_type: CRUNCHY
-  }
+KEY_TEMPLATE_1 = r"""
+ type_url: "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey"
+ value: "\n@\022>\022<\n0type.googleapis.com/google.crypto.tink.AesEaxKey\022\006\n\002\010\020\020\020\030\001"
+ output_prefix_type: RAW
 """
 
+# The same template as 1, but here AesEaxKeyFormat is encoded differently
+KEY_TEMPLATE_1_NOT_NORMALIZED = r"""
+ type_url: "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey"
+ value: "\n@\022>\022<\n0type.googleapis.com/google.crypto.tink.AesEaxKey\022\006\020\020\n\002\010\020\030\001"
+ output_prefix_type: RAW
+"""
+
+# The same template as 1, but the inner AesEaxKeyFormat has a different iv_size
+KEY_TEMPLATE_2 = r"""
+ type_url: "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey"
+ value: "\n@\022>\022<\n0type.googleapis.com/google.crypto.tink.AesEaxKey\022\006\n\002\010\020\020\022\030\001"
+ output_prefix_type: RAW
+"""
+
+KEY_TEMPLATE_1_COMMENTED_FORMAT = r"""
+type_url: "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey"
+# value: [type.googleapis.com/google.crypto.tink.EciesAeadHkdfKeyFormat] {
+#   params {
+#     kem_params {
+#       curve_type: UNKNOWN_CURVE
+#       hkdf_hash_type: UNKNOWN_HASH
+#       hkdf_salt: ""
+#     }
+#     dem_params {
+#       aead_dem {
+#         type_url: "type.googleapis.com/google.crypto.tink.AesEaxKey"
+#         # value: [type.googleapis.com/google.crypto.tink.AesEaxKeyFormat] {
+#         #   params {
+#         #     iv_size: 16
+#         #   }
+#         #   key_size: 16
+#         # }
+#         value: "\n\002\010\020\020\020"
+#         output_prefix_type: TINK
+#       }
+#     }
+#     ec_point_format: UNKNOWN_FORMAT
+#   }
+# }
+value: "\n@\022>\022<\n0type.googleapis.com/google.crypto.tink.AesEaxKey\022\006\n\002\010\020\020\020\030\001"
+output_prefix_type: RAW
+""".strip()
+
 
 class KeyUtilTest(parameterized.TestCase):
 
@@ -83,6 +104,7 @@
 #     curve: NIST_P384
 #     encoding: DER
 #   }
+#   version: 0
 # }
 value: "\022\006\010\004\020\003\030\002"
 output_prefix_type: TINK"""
@@ -92,6 +114,28 @@
     self.assertEqual(
         text_format.Parse(output, tink_pb2.KeyTemplate()), template)
 
+  def test_text_format_recursive_template(self):
+    template = tink_pb2.KeyTemplate()
+    text_format.Parse(KEY_TEMPLATE_1, template)
+    output = key_util.text_format(template)
+    self.assertEqual(output, KEY_TEMPLATE_1_COMMENTED_FORMAT)
+
+  def test_text_format_normalizes_recursive_template(self):
+    template1a = tink_pb2.KeyTemplate()
+    text_format.Parse(KEY_TEMPLATE_1, template1a)
+
+    template = tink_pb2.KeyTemplate()
+    text_format.Parse(KEY_TEMPLATE_1_NOT_NORMALIZED, template)
+    # Before the call, the value is different (different serializations)
+    self.assertNotEqual(template1a.value, template.value)
+
+    normalized_template = key_util.text_format(template)
+    self.assertEqual(normalized_template, KEY_TEMPLATE_1_COMMENTED_FORMAT)
+
+    # We explicitly test that the value has not been changed (since this
+    # requirement needs an explicit copy in the code)
+    self.assertNotEqual(template1a.value, template.value)
+
   def test_text_format_keyset(self):
     key = tink_pb2.Keyset.Key(
         key_data=tink_pb2.KeyData(
@@ -126,13 +170,87 @@
 
   def test_compare_tink_messages(self):
     """Tests that all testdata have the expected format, including comments."""
-    keyset_a = text_format.Parse(KEYSET_A, tink_pb2.Keyset())
-    keyset_b = text_format.Parse(KEYSET_B, tink_pb2.Keyset())
-    key_util.assert_tink_proto_equal(self, keyset_a, keyset_b)
+    key_template_1 = text_format.Parse(KEY_TEMPLATE_1, tink_pb2.KeyTemplate())
+    key_template_1_not_normalized = text_format.Parse(
+        KEY_TEMPLATE_1_NOT_NORMALIZED, tink_pb2.KeyTemplate())
+    key_util.assert_tink_proto_equal(self, key_template_1,
+                                     key_template_1_not_normalized)
+    key_template_2 = text_format.Parse(KEY_TEMPLATE_2, tink_pb2.KeyTemplate())
 
-    keyset_b.primary_key_id = 4223424881
     with self.assertRaises(AssertionError):
-      key_util.assert_tink_proto_equal(self, keyset_a, keyset_b)
+      key_util.assert_tink_proto_equal(self, key_template_1, key_template_2)
+
+  def test_parse_text_format_symmetric_key_template(self):
+    serialized = r"""type_url: "type.googleapis.com/google.crypto.tink.AesEaxKey"
+# value: [type.googleapis.com/google.crypto.tink.AesEaxKeyFormat] {
+#   params {
+#     iv_size: 16
+#   }
+#   key_size: 16
+# }
+value: "\n\002\010\020\020\020"
+output_prefix_type: TINK"""
+    expected = tink_pb2.KeyTemplate(
+        type_url='type.googleapis.com/google.crypto.tink.AesEaxKey',
+        value=b'\n\x02\x08\x10\x10\x10',
+        output_prefix_type=tink_pb2.TINK)
+
+    parsed_template = tink_pb2.KeyTemplate()
+    key_util.parse_text_format(serialized, parsed_template)
+    self.assertEqual(parsed_template, expected)
+
+  def test_parse_text_format_wrong_comment(self):
+    serialized = r"""type_url: "type.googleapis.com/google.crypto.tink.AesEaxKey"
+value: "\n\002\010\020\020\020"
+output_prefix_type: TINK"""
+
+    parsed_template = tink_pb2.KeyTemplate()
+    with self.assertRaises(AssertionError):
+      key_util.parse_text_format(serialized, parsed_template)
+
+  def test_parse_text_format_missing_comment(self):
+    serialized = r"""type_url: "type.googleapis.com/google.crypto.tink.AesEaxKey"
+# value: [type.googleapis.com/google.crypto.tink.AesEaxKeyFormat] {
+#   params {
+#     iv_size: 16
+#   }
+#   key_size: 18
+# }
+value: "\n\002\010\020\020\020"
+output_prefix_type: TINK"""
+
+    parsed_template = tink_pb2.KeyTemplate()
+    with self.assertRaises(AssertionError):
+      key_util.parse_text_format(serialized, parsed_template)
+
+  def test_assert_tink_proto_equal_does_not_modify_messages(self):
+    """Tests that assert_tink_proto_equal does not modify the message."""
+    key_template_1 = text_format.Parse(KEY_TEMPLATE_1, tink_pb2.KeyTemplate())
+    key_template_1_original = text_format.Parse(
+        KEY_TEMPLATE_1_NOT_NORMALIZED, tink_pb2.KeyTemplate())
+    key_template_1_not_normalized = text_format.Parse(
+        KEY_TEMPLATE_1_NOT_NORMALIZED, tink_pb2.KeyTemplate())
+    key_util.assert_tink_proto_equal(self, key_template_1,
+                                     key_template_1_not_normalized)
+    self.assertEqual(key_template_1_original.value,
+                     key_template_1_not_normalized.value)
+    key_util.assert_tink_proto_equal(self, key_template_1_not_normalized,
+                                     key_template_1)
+    self.assertEqual(key_template_1_original.value,
+                     key_template_1_not_normalized.value)
+
+  def test_text_format_with_empty_value(self):
+    expected = r"""type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+# value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+# }
+value: ""
+output_prefix_type: RAW"""
+
+    template = tink_pb2.KeyTemplate(
+        type_url='type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key',
+        output_prefix_type=tink_pb2.RAW)
+    formatted = key_util.text_format(template)
+    self.assertEqual(formatted, expected)
 
 
 if __name__ == '__main__':
diff --git a/testing/cross_language/util/supported_key_types.py b/testing/cross_language/util/supported_key_types.py
deleted file mode 100644
index e86de6c..0000000
--- a/testing/cross_language/util/supported_key_types.py
+++ /dev/null
@@ -1,428 +0,0 @@
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""All KeyTypes and which languages support them."""
-
-from typing import List
-
-from tink import aead
-from tink import daead
-from tink import hybrid
-from tink import jwt
-from tink import mac
-from tink import prf
-from tink import signature
-from tink import streaming_aead
-
-from tink.proto import tink_pb2
-
-# All languages supported by cross-language tests.
-ALL_LANGUAGES = ['cc', 'java', 'go', 'python']
-
-# All KeyTypes (without the prefix 'type.googleapis.com/google.crypto.tink.')
-AEAD_KEY_TYPES = [
-    'AesEaxKey',
-    'AesGcmKey',
-    'AesGcmSivKey',
-    'AesCtrHmacAeadKey',
-    'ChaCha20Poly1305Key',
-    'XChaCha20Poly1305Key',
-    'KmsAeadKey',
-    'KmsEnvelopeAeadKey',
-]
-DAEAD_KEY_TYPES = ['AesSivKey']
-STREAMING_AEAD_KEY_TYPES = [
-    'AesCtrHmacStreamingKey',
-    'AesGcmHkdfStreamingKey',
-]
-HYBRID_PRIVATE_KEY_TYPES = ['EciesAeadHkdfPrivateKey', 'HpkePrivateKey']
-MAC_KEY_TYPES = [
-    'AesCmacKey',
-    'HmacKey',
-]
-SIGNATURE_KEY_TYPES = [
-    'EcdsaPrivateKey',
-    'Ed25519PrivateKey',
-    'RsaSsaPkcs1PrivateKey',
-    'RsaSsaPssPrivateKey',
-]
-PRF_KEY_TYPES = [
-    'AesCmacPrfKey',
-    'HmacPrfKey',
-    'HkdfPrfKey',
-]
-JWT_MAC_KEY_TYPES = [
-    'JwtHmacKey',
-]
-JWT_SIGNATURE_KEY_TYPES = [
-    'JwtEcdsaPrivateKey',
-    'JwtRsaSsaPkcs1PrivateKey',
-    'JwtRsaSsaPssPrivateKey',
-]
-
-ALL_KEY_TYPES = (
-    AEAD_KEY_TYPES + DAEAD_KEY_TYPES + STREAMING_AEAD_KEY_TYPES +
-    HYBRID_PRIVATE_KEY_TYPES + MAC_KEY_TYPES + SIGNATURE_KEY_TYPES +
-    PRF_KEY_TYPES + JWT_MAC_KEY_TYPES + JWT_SIGNATURE_KEY_TYPES)
-
-
-# All languages that are supported by a KeyType
-SUPPORTED_LANGUAGES = {
-    'AesEaxKey': ['cc', 'java', 'python'],
-    'AesGcmKey': ['cc', 'java', 'go', 'python'],
-    'AesGcmSivKey': ['cc', 'go', 'python'],
-    'AesCtrHmacAeadKey': ['cc', 'java', 'go', 'python'],
-    'ChaCha20Poly1305Key': ['java', 'go'],
-    'XChaCha20Poly1305Key': ['cc', 'java', 'go', 'python'],
-    'KmsAeadKey': ['cc', 'java', 'python'],
-    'KmsEnvelopeAeadKey': ['cc', 'java', 'go', 'python'],
-    'AesSivKey': ['cc', 'java', 'go', 'python'],
-    'AesCtrHmacStreamingKey': ['cc', 'java', 'go', 'python'],
-    'AesGcmHkdfStreamingKey': ['cc', 'java', 'go', 'python'],
-    'EciesAeadHkdfPrivateKey': ['cc', 'java', 'go', 'python'],
-    'HpkePrivateKey': ['cc', 'java', 'go', 'python'],
-    'AesCmacKey': ['cc', 'java', 'go', 'python'],
-    'HmacKey': ['cc', 'java', 'go', 'python'],
-    'EcdsaPrivateKey': ['cc', 'java', 'go', 'python'],
-    'Ed25519PrivateKey': ['cc', 'java', 'go', 'python'],
-    'RsaSsaPkcs1PrivateKey': ['cc', 'java', 'go', 'python'],
-    'RsaSsaPssPrivateKey': ['cc', 'java', 'python'],
-    'AesCmacPrfKey': ['cc', 'java', 'go', 'python'],
-    'HmacPrfKey': ['cc', 'java', 'go', 'python'],
-    'HkdfPrfKey': ['cc', 'java', 'go', 'python'],
-    'JwtHmacKey': ['cc', 'java', 'go', 'python'],
-    'JwtEcdsaPrivateKey': ['cc', 'java', 'go', 'python'],
-    'JwtRsaSsaPkcs1PrivateKey': ['cc', 'java', 'python'],
-    'JwtRsaSsaPssPrivateKey': ['cc', 'java', 'python'],
-}
-
-KEY_TYPE_FROM_URL = {
-    'type.googleapis.com/google.crypto.tink.' + key_type: key_type
-    for key_type in ALL_KEY_TYPES}
-
-# For each KeyType, a list of Tinkey KeyTemplate names.
-# TODO(juerg): Add missing key template names, and remove deprecated names.
-KEY_TEMPLATE_NAMES = {
-    'AesEaxKey': [
-        'AES128_EAX', 'AES128_EAX_RAW', 'AES256_EAX', 'AES256_EAX_RAW'
-    ],
-    'AesGcmKey': [
-        'AES128_GCM', 'AES128_GCM_RAW', 'AES256_GCM', 'AES256_GCM_RAW'
-    ],
-    'AesGcmSivKey': [
-        'AES128_GCM_SIV', 'AES128_GCM_SIV_RAW', 'AES256_GCM_SIV',
-        'AES256_GCM_SIV_RAW'
-    ],
-    'AesCtrHmacAeadKey': [
-        'AES128_CTR_HMAC_SHA256', 'AES128_CTR_HMAC_SHA256_RAW',
-        'AES256_CTR_HMAC_SHA256', 'AES256_CTR_HMAC_SHA256_RAW'
-    ],
-    'ChaCha20Poly1305Key': ['CHACHA20_POLY1305', 'CHACHA20_POLY1305_RAW'],
-    'XChaCha20Poly1305Key': ['XCHACHA20_POLY1305', 'XCHACHA20_POLY1305_RAW'],
-    'KmsAeadKey': [],
-    'KmsEnvelopeAeadKey': [],
-    'AesSivKey': ['AES256_SIV'],
-    'AesCtrHmacStreamingKey': [
-        'AES128_CTR_HMAC_SHA256_4KB',
-        'AES128_CTR_HMAC_SHA256_1MB',
-        'AES256_CTR_HMAC_SHA256_4KB',
-        'AES256_CTR_HMAC_SHA256_1MB',
-    ],
-    'AesGcmHkdfStreamingKey': [
-        'AES128_GCM_HKDF_4KB',
-        'AES128_GCM_HKDF_1MB',
-        'AES256_GCM_HKDF_4KB',
-        'AES256_GCM_HKDF_1MB',
-    ],
-    'EciesAeadHkdfPrivateKey': [
-        'ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM',
-        'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM',
-        'ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256',
-        'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256',
-    ],
-    'HpkePrivateKey': [
-        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM',
-        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW',
-        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM',
-        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW',
-        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305',
-        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW',
-    ],
-    'AesCmacKey': ['AES_CMAC'],
-    'HmacKey': [
-        'HMAC_SHA256_128BITTAG', 'HMAC_SHA256_256BITTAG',
-        'HMAC_SHA512_256BITTAG', 'HMAC_SHA512_512BITTAG'
-    ],
-    'EcdsaPrivateKey': [
-        'ECDSA_P256', 'ECDSA_P256_RAW', 'ECDSA_P384', 'ECDSA_P384_SHA384',
-        'ECDSA_P384_SHA512', 'ECDSA_P521', 'ECDSA_P256_IEEE_P1363',
-        'ECDSA_P384_IEEE_P1363', 'ECDSA_P384_SHA384_IEEE_P1363',
-        'ECDSA_P521_IEEE_P1363'
-    ],
-    'Ed25519PrivateKey': ['ED25519'],
-    'RsaSsaPkcs1PrivateKey': [
-        'RSA_SSA_PKCS1_3072_SHA256_F4', 'RSA_SSA_PKCS1_4096_SHA512_F4'
-    ],
-    'RsaSsaPssPrivateKey': [
-        'RSA_SSA_PSS_3072_SHA256_SHA256_32_F4',
-        'RSA_SSA_PSS_4096_SHA512_SHA512_64_F4'
-    ],
-    'AesCmacPrfKey': ['AES_CMAC_PRF'],
-    'HmacPrfKey': ['HMAC_SHA256_PRF', 'HMAC_SHA512_PRF'],
-    'HkdfPrfKey': ['HKDF_SHA256'],
-    'JwtHmacKey': [
-        'JWT_HS256', 'JWT_HS256_RAW', 'JWT_HS384', 'JWT_HS384_RAW', 'JWT_HS512',
-        'JWT_HS512_RAW'
-    ],
-    'JwtEcdsaPrivateKey': [
-        'JWT_ES256', 'JWT_ES256_RAW', 'JWT_ES384', 'JWT_ES384_RAW', 'JWT_ES512',
-        'JWT_ES512_RAW'
-    ],
-    'JwtRsaSsaPkcs1PrivateKey': [
-        'JWT_RS256_2048_F4', 'JWT_RS256_2048_F4_RAW', 'JWT_RS256_3072_F4',
-        'JWT_RS256_3072_F4_RAW', 'JWT_RS384_3072_F4', 'JWT_RS384_3072_F4_RAW',
-        'JWT_RS512_4096_F4', 'JWT_RS512_4096_F4_RAW'
-    ],
-    'JwtRsaSsaPssPrivateKey': [
-        'JWT_PS256_2048_F4', 'JWT_PS256_2048_F4_RAW', 'JWT_PS256_3072_F4',
-        'JWT_PS256_3072_F4_RAW', 'JWT_PS384_3072_F4', 'JWT_PS384_3072_F4_RAW',
-        'JWT_PS512_4096_F4', 'JWT_PS512_4096_F4_RAW'
-    ],
-}
-
-# KeyTemplate (as Protobuf) for each KeyTemplate name.
-KEY_TEMPLATE = {
-    'AES128_EAX':
-        aead.aead_key_templates.AES128_EAX,
-    'AES128_EAX_RAW':
-        aead.aead_key_templates.AES128_EAX_RAW,
-    'AES256_EAX':
-        aead.aead_key_templates.AES256_EAX,
-    'AES256_EAX_RAW':
-        aead.aead_key_templates.AES256_EAX_RAW,
-    'AES128_GCM':
-        aead.aead_key_templates.AES128_GCM,
-    'AES128_GCM_RAW':
-        aead.aead_key_templates.AES128_GCM_RAW,
-    'AES256_GCM':
-        aead.aead_key_templates.AES256_GCM,
-    'AES256_GCM_RAW':
-        aead.aead_key_templates.AES256_GCM_RAW,
-    'AES128_GCM_SIV':
-        aead.aead_key_templates.AES128_GCM_SIV,
-    'AES128_GCM_SIV_RAW':
-        aead.aead_key_templates.AES128_GCM_SIV_RAW,
-    'AES256_GCM_SIV':
-        aead.aead_key_templates.AES256_GCM_SIV,
-    'AES256_GCM_SIV_RAW':
-        aead.aead_key_templates.AES256_GCM_SIV_RAW,
-    'AES128_CTR_HMAC_SHA256':
-        aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
-    'AES128_CTR_HMAC_SHA256_RAW':
-        aead.aead_key_templates.AES128_CTR_HMAC_SHA256_RAW,
-    'AES256_CTR_HMAC_SHA256':
-        aead.aead_key_templates.AES256_CTR_HMAC_SHA256,
-    'AES256_CTR_HMAC_SHA256_RAW':
-        aead.aead_key_templates.AES256_CTR_HMAC_SHA256_RAW,
-    'CHACHA20_POLY1305':
-        tink_pb2.KeyTemplate(
-            type_url=('type.googleapis.com/google.crypto.tink.' +
-                      'ChaCha20Poly1305Key'),
-            output_prefix_type=tink_pb2.TINK),
-    'CHACHA20_POLY1305_RAW':
-        tink_pb2.KeyTemplate(
-            type_url=('type.googleapis.com/google.crypto.tink.' +
-                      'ChaCha20Poly1305Key'),
-            output_prefix_type=tink_pb2.RAW),
-    'XCHACHA20_POLY1305':
-        aead.aead_key_templates.XCHACHA20_POLY1305,
-    'XCHACHA20_POLY1305_RAW':
-        aead.aead_key_templates.XCHACHA20_POLY1305_RAW,
-    'AES256_SIV':
-        daead.deterministic_aead_key_templates.AES256_SIV,
-    'AES128_CTR_HMAC_SHA256_4KB':
-        streaming_aead.streaming_aead_key_templates.AES128_CTR_HMAC_SHA256_4KB,
-    'AES128_CTR_HMAC_SHA256_1MB':
-        streaming_aead.streaming_aead_key_templates.AES128_CTR_HMAC_SHA256_1MB,
-    'AES256_CTR_HMAC_SHA256_4KB':
-        streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_4KB,
-    'AES256_CTR_HMAC_SHA256_1MB':
-        streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_1MB,
-    'AES128_GCM_HKDF_4KB':
-        streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_4KB,
-    'AES128_GCM_HKDF_1MB':
-        streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_1MB,
-    'AES256_GCM_HKDF_4KB':
-        streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_4KB,
-    'AES256_GCM_HKDF_1MB':
-        streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_1MB,
-    'ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM':
-        hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM,
-    'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM':
-        hybrid.hybrid_key_templates
-        .ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM,
-    'ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256':
-        hybrid.hybrid_key_templates
-        .ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256,
-    'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256':
-        hybrid.hybrid_key_templates
-        .ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256,
-    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM':
-        hybrid.hybrid_key_templates
-        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM,
-    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW':
-        hybrid.hybrid_key_templates
-        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW,
-    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM':
-        hybrid.hybrid_key_templates
-        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM,
-    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW':
-        hybrid.hybrid_key_templates
-        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW,
-    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305':
-        hybrid.hybrid_key_templates
-        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305,
-    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW':
-        hybrid.hybrid_key_templates
-        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW,
-    'AES_CMAC':
-        mac.mac_key_templates.AES_CMAC,
-    'HMAC_SHA256_128BITTAG':
-        mac.mac_key_templates.HMAC_SHA256_128BITTAG,
-    'HMAC_SHA256_256BITTAG':
-        mac.mac_key_templates.HMAC_SHA256_256BITTAG,
-    'HMAC_SHA512_256BITTAG':
-        mac.mac_key_templates.HMAC_SHA512_256BITTAG,
-    'HMAC_SHA512_512BITTAG':
-        mac.mac_key_templates.HMAC_SHA512_512BITTAG,
-    'ECDSA_P256':
-        signature.signature_key_templates.ECDSA_P256,
-    'ECDSA_P256_RAW':
-        signature.signature_key_templates.ECDSA_P256_RAW,
-    'ECDSA_P384':
-        signature.signature_key_templates.ECDSA_P384,
-    'ECDSA_P384_SHA384':
-        signature.signature_key_templates.ECDSA_P384_SHA384,
-    'ECDSA_P384_SHA512':
-        signature.signature_key_templates.ECDSA_P384_SHA512,
-    'ECDSA_P521':
-        signature.signature_key_templates.ECDSA_P521,
-    'ECDSA_P256_IEEE_P1363':
-        signature.signature_key_templates.ECDSA_P256_IEEE_P1363,
-    'ECDSA_P384_IEEE_P1363':
-        signature.signature_key_templates.ECDSA_P384_IEEE_P1363,
-    'ECDSA_P384_SHA384_IEEE_P1363':
-        signature.signature_key_templates.ECDSA_P384_SHA384_IEEE_P1363,
-    'ECDSA_P521_IEEE_P1363':
-        signature.signature_key_templates.ECDSA_P521_IEEE_P1363,
-    'ED25519':
-        signature.signature_key_templates.ED25519,
-    'RSA_SSA_PKCS1_3072_SHA256_F4':
-        signature.signature_key_templates.RSA_SSA_PKCS1_3072_SHA256_F4,
-    'RSA_SSA_PKCS1_4096_SHA512_F4':
-        signature.signature_key_templates.RSA_SSA_PKCS1_4096_SHA512_F4,
-    'RSA_SSA_PSS_3072_SHA256_SHA256_32_F4':
-        signature.signature_key_templates.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4,
-    'RSA_SSA_PSS_4096_SHA512_SHA512_64_F4':
-        signature.signature_key_templates.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4,
-    'AES_CMAC_PRF':
-        prf.prf_key_templates.AES_CMAC,
-    'HMAC_SHA256_PRF':
-        prf.prf_key_templates.HMAC_SHA256,
-    'HMAC_SHA512_PRF':
-        prf.prf_key_templates.HMAC_SHA512,
-    'HKDF_SHA256':
-        prf.prf_key_templates.HKDF_SHA256,
-    'JWT_HS256':
-        jwt.jwt_hs256_template(),
-    'JWT_HS256_RAW':
-        jwt.raw_jwt_hs256_template(),
-    'JWT_HS384':
-        jwt.jwt_hs384_template(),
-    'JWT_HS384_RAW':
-        jwt.raw_jwt_hs384_template(),
-    'JWT_HS512':
-        jwt.jwt_hs512_template(),
-    'JWT_HS512_RAW':
-        jwt.raw_jwt_hs512_template(),
-    'JWT_ES256':
-        jwt.jwt_es256_template(),
-    'JWT_ES256_RAW':
-        jwt.raw_jwt_es256_template(),
-    'JWT_ES384':
-        jwt.jwt_es384_template(),
-    'JWT_ES384_RAW':
-        jwt.raw_jwt_es384_template(),
-    'JWT_ES512':
-        jwt.jwt_es512_template(),
-    'JWT_ES512_RAW':
-        jwt.raw_jwt_es512_template(),
-    'JWT_RS256_2048_F4':
-        jwt.jwt_rs256_2048_f4_template(),
-    'JWT_RS256_2048_F4_RAW':
-        jwt.raw_jwt_rs256_2048_f4_template(),
-    'JWT_RS256_3072_F4':
-        jwt.jwt_rs256_3072_f4_template(),
-    'JWT_RS256_3072_F4_RAW':
-        jwt.raw_jwt_rs256_3072_f4_template(),
-    'JWT_RS384_3072_F4':
-        jwt.jwt_rs384_3072_f4_template(),
-    'JWT_RS384_3072_F4_RAW':
-        jwt.raw_jwt_rs384_3072_f4_template(),
-    'JWT_RS512_4096_F4':
-        jwt.jwt_rs512_4096_f4_template(),
-    'JWT_RS512_4096_F4_RAW':
-        jwt.raw_jwt_rs512_4096_f4_template(),
-    'JWT_PS256_2048_F4':
-        jwt.jwt_ps256_2048_f4_template(),
-    'JWT_PS256_2048_F4_RAW':
-        jwt.raw_jwt_ps256_2048_f4_template(),
-    'JWT_PS256_3072_F4':
-        jwt.jwt_ps256_3072_f4_template(),
-    'JWT_PS256_3072_F4_RAW':
-        jwt.raw_jwt_ps256_3072_f4_template(),
-    'JWT_PS384_3072_F4':
-        jwt.jwt_ps384_3072_f4_template(),
-    'JWT_PS384_3072_F4_RAW':
-        jwt.raw_jwt_ps384_3072_f4_template(),
-    'JWT_PS512_4096_F4':
-        jwt.jwt_ps512_4096_f4_template(),
-    'JWT_PS512_4096_F4_RAW':
-        jwt.raw_jwt_ps512_4096_f4_template(),
-}
-
-
-# Key template names for which the list of supported languages is different from
-# the list of supported languages of the whole key type.
-_CUSTOM_SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME = {
-    # currently empty.
-}
-
-
-def _supported_languages_by_template(
-    template_name: str, key_type: str) -> List[str]:
-  if template_name in _CUSTOM_SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME:
-    return _CUSTOM_SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[template_name]
-  return SUPPORTED_LANGUAGES[key_type]
-
-
-def _all_key_template_names_with_key_type():
-  for key_type, template_names in KEY_TEMPLATE_NAMES.items():
-    for template_name in template_names:
-      yield (template_name, key_type)
-
-
-SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME = {
-    name: _supported_languages_by_template(name, template)
-    for name, template in _all_key_template_names_with_key_type()
-}
diff --git a/testing/cross_language/util/supported_key_types_test.py b/testing/cross_language/util/supported_key_types_test.py
deleted file mode 100644
index 7ec3696..0000000
--- a/testing/cross_language/util/supported_key_types_test.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Tests for tink.testing.cross_language.supported_key_types."""
-
-from absl.testing import absltest
-from util import supported_key_types
-
-
-def all_key_template_names():
-  for _, names in supported_key_types.KEY_TEMPLATE_NAMES.items():
-    for name in names:
-      yield name
-
-
-class SupportedKeyTypesTest(absltest.TestCase):
-
-  def test_all_key_types_present(self):
-    self.assertEqual(
-        list(supported_key_types.SUPPORTED_LANGUAGES.keys()),
-        supported_key_types.ALL_KEY_TYPES)
-    self.assertEqual(
-        list(supported_key_types.KEY_TEMPLATE_NAMES.keys()),
-        supported_key_types.ALL_KEY_TYPES)
-
-  def test_all_key_templates_present(self):
-    self.assertEqual(
-        list(all_key_template_names()),
-        list(supported_key_types.KEY_TEMPLATE.keys()))
-
-  def test_supported_lang_by_template_name_all_present(self):
-    self.assertEqual(
-        list(all_key_template_names()),
-        list(supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME.keys()))
-
-  def test_supported_langauges_by_template_name(self):
-    self.assertEqual(
-        supported_key_types.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
-            'ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM'],
-        ['cc', 'java', 'go', 'python'])
-
-
-if __name__ == '__main__':
-  absltest.main()
diff --git a/testing/cross_language/util/test_keys/BUILD.bazel b/testing/cross_language/util/test_keys/BUILD.bazel
new file mode 100644
index 0000000..b1224fc
--- /dev/null
+++ b/testing/cross_language/util/test_keys/BUILD.bazel
@@ -0,0 +1,87 @@
+load("@rules_python//python:defs.bzl", "py_library")
+load("@pip_deps//:requirements.bzl", "requirement")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//visibility:public"],
+)
+
+licenses(["notice"])
+
+py_library(
+    name = "test_keys",
+    srcs = ["__init__.py"],
+    deps = [
+        ":_create_test_key",
+        ":_test_keys_container",
+    ],
+)
+
+py_library(
+    name = "_test_keys_container",
+    srcs = ["_test_keys_container.py"],
+    deps = [
+        "//util:key_util",
+        "@tink_py//tink/proto:tink_py_pb2",
+    ],
+)
+
+py_test(
+    name = "_test_keys_container_test",
+    srcs = ["_test_keys_container_test.py"],
+    deps = [
+        ":test_keys",
+        requirement("absl-py"),
+        "@tink_py//tink/proto:tink_py_pb2",
+    ],
+)
+
+py_library(
+    name = "_test_keys_db",
+    srcs = ["_test_keys_db.py"],
+    deps = [":_test_keys_container"],
+)
+
+py_library(
+    name = "_create_test_key",
+    srcs = ["_create_test_key.py"],
+    deps = [
+        ":_test_keys_container",
+        ":_test_keys_db",
+        "//tink_config",
+        "//util:key_util",
+        "@tink_py//tink:cleartext_keyset_handle",
+        "@tink_py//tink:tink_python",
+        "@tink_py//tink/aead",
+        "@tink_py//tink/daead",
+        "@tink_py//tink/hybrid",
+        "@tink_py//tink/jwt",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/prf",
+        "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/signature",
+        "@tink_py//tink/streaming_aead",
+    ],
+)
+
+py_test(
+    name = "_create_test_key_test",
+    srcs = ["_create_test_key_test.py"],
+    deps = [
+        ":test_keys",
+        "//tink_config",
+        "//util:key_util",
+        "//util:utilities",
+        requirement("absl-py"),
+        "@tink_py//tink/aead",
+        "@tink_py//tink/daead",
+        "@tink_py//tink/hybrid",
+        "@tink_py//tink/jwt",
+        "@tink_py//tink/mac",
+        "@tink_py//tink/prf",
+        "@tink_py//tink/proto:aes_gcm_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
+        "@tink_py//tink/signature",
+        "@tink_py//tink/streaming_aead",
+    ],
+)
diff --git a/testing/cross_language/util/test_keys/__init__.py b/testing/cross_language/util/test_keys/__init__.py
new file mode 100644
index 0000000..bd12dac
--- /dev/null
+++ b/testing/cross_language/util/test_keys/__init__.py
@@ -0,0 +1,23 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""test_keys package."""
+
+from util.test_keys import _create_test_key
+from util.test_keys import _test_keys_container
+
+new_or_stored_key = _create_test_key.new_or_stored_key
+new_or_stored_keyset = _create_test_key.new_or_stored_keyset
+some_keyset_for_primitive = _create_test_key.some_keyset_for_primitive
+
+TestKeysContainer = _test_keys_container.TestKeysContainer
diff --git a/testing/cross_language/util/test_keys/_create_test_key.py b/testing/cross_language/util/test_keys/_create_test_key.py
new file mode 100644
index 0000000..1f0a16f
--- /dev/null
+++ b/testing/cross_language/util/test_keys/_create_test_key.py
@@ -0,0 +1,176 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Provides methods to create keys and keysets in cross language tests.
+"""
+
+import io
+from typing import Any
+from typing import Callable
+
+import tink
+from tink import aead
+from tink import cleartext_keyset_handle
+from tink import daead
+from tink import hybrid
+from tink import jwt
+from tink import mac
+from tink import prf
+from tink import signature
+from tink import streaming_aead
+
+from tink.proto import tink_pb2
+import tink_config
+from util import key_util
+from util.test_keys import _test_keys_container
+from util.test_keys import _test_keys_db
+
+
+_CREATE_NEW_KEY_MESSAGE_TEMPLATE = """
+Unable to retrieve stored key for template:
+{text_format}
+To create a new key with this template, run:
+blaze test --trim_test_configuration \\
+  //third_party/tink/testing/cross_language/util:testing_servers_test \\
+  --test_arg=--force_failure_for_adding_key_to_db \\
+  --test_arg=--hex_template={hex_template} \\
+  --test_output=errors
+""".strip()
+
+
+def _use_stored_key(template: tink_pb2.KeyTemplate) -> bool:
+  """Returns true for templates for which we should use _test_keys_db.py."""
+  # We cannot yet create ChaCha20Poly1305Keys in Python.
+  if (template.type_url ==
+      'type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key'):
+    return True
+  # Creating RSA Keys is very slow.
+  if (template.type_url ==
+      'type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey'):
+    return True
+  # Creating RSA Keys is very slow.
+  if (template.type_url ==
+      'type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey'):
+    return True
+  # Creating RSA Keys is very slow.
+  if (template.type_url ==
+      'type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey'):
+    return True
+  # Creating RSA Keys is very slow.
+  if (template.type_url ==
+      'type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey'):
+    return True
+  return False
+
+
+def new_or_stored_key(
+    template: tink_pb2.KeyTemplate,
+    container: _test_keys_container.TestKeysContainer = _test_keys_db.db,
+    use_stored_key: Callable[[tink_pb2.KeyTemplate], bool] = _use_stored_key
+) -> tink_pb2.Keyset.Key:
+  """Returns either a new key or one which is stored in the passed in db.
+
+  The arguments 'container' and 'use_stored_key' are for testing and typically
+  do not need to be used.
+
+  Args:
+    template: the template for which to get a key
+    container: the container with test keys, per default the container defined
+      globally in _test_keys_db
+    use_stored_key: a function which returns for a given template whether we
+      should use a precomputed key, defaults to an internal function
+  """
+
+  if not use_stored_key(template):
+    handle = tink.new_keyset_handle(template)
+    buf = io.BytesIO()
+    writer = tink.BinaryKeysetWriter(buf)
+    cleartext_keyset_handle.write(writer, handle)
+    keyset = tink_pb2.Keyset.FromString(buf.getvalue())
+    return keyset.key[0]
+
+  try:
+    return container.get_key(template)
+  except KeyError:
+    raise ValueError(
+        _CREATE_NEW_KEY_MESSAGE_TEMPLATE.format(
+            text_format=key_util.text_format(template),
+            hex_template=template.SerializeToString().hex())) from None
+
+
+def new_or_stored_keyset(
+    template: tink_pb2.KeyTemplate,
+    container: _test_keys_container.TestKeysContainer = _test_keys_db.db,
+    use_stored_key: Callable[[tink_pb2.KeyTemplate], bool] = _use_stored_key
+) -> bytes:
+  """Returns a new keyset with a single new or stored key.
+
+  The arguments 'container' and 'use_stored_key' are for testing and typically
+  do not need to be used.
+
+  Args:
+    template: the template for which to get a key
+    container: the container with test keys, per default the container defined
+      globally in _test_keys_db
+    use_stored_key: a function which returns for a given template whether we
+      should use a precomputed key, defaults to an internal function
+  """
+  key = new_or_stored_key(template, container, use_stored_key)
+  keyset = tink_pb2.Keyset(key=[key], primary_key_id=key.key_id)
+  return keyset.SerializeToString()
+
+
+def _some_template_for_primitive(primitive: Any) -> tink_pb2.KeyTemplate:
+  """Returns an arbitrary template for the given primitive."""
+  if primitive == aead.Aead:
+    return aead.aead_key_templates.AES128_GCM
+  if primitive == daead.DeterministicAead:
+    return daead.deterministic_aead_key_templates.AES256_SIV
+  if primitive == streaming_aead.StreamingAead:
+    return streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_1MB
+  if primitive == hybrid.HybridDecrypt:
+    return hybrid.hybrid_key_templates.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW
+  if primitive == mac.Mac:
+    return mac.mac_key_templates.HMAC_SHA256_256BITTAG
+  if primitive == signature.PublicKeySign:
+    return signature.signature_key_templates.RSA_SSA_PKCS1_4096_SHA512_F4
+  if primitive == prf.PrfSet:
+    return prf.prf_key_templates.HKDF_SHA256
+  if primitive == jwt.JwtMac:
+    return jwt.jwt_hs256_template()
+  if primitive == jwt.JwtPublicKeySign:
+    return jwt.jwt_ps512_4096_f4_template()
+  raise ValueError('Unknown primitive in _some_template_for_primitive')
+
+
+def _get_public_keyset(private_keyset: bytes) -> bytes:
+  reader = tink.BinaryKeysetReader(private_keyset)
+  keyset_handle = cleartext_keyset_handle.read(reader)
+  public_keyset_handle = keyset_handle.public_keyset_handle()
+  public_keyset = io.BytesIO()
+  cleartext_keyset_handle.write(
+      tink.BinaryKeysetWriter(public_keyset), public_keyset_handle)
+  return public_keyset.getvalue()
+
+
+def some_keyset_for_primitive(primitive: Any) -> bytes:
+  """Returns an arbitrary keyset for the given primitive."""
+  if not tink_config.is_asymmetric_public_key_primitive(primitive):
+    return new_or_stored_keyset(_some_template_for_primitive(primitive))
+
+  private_key_primitive = tink_config.get_private_key_primitive(primitive)
+  private_keyset = new_or_stored_keyset(
+      _some_template_for_primitive(private_key_primitive))
+
+  return _get_public_keyset(private_keyset)
diff --git a/testing/cross_language/util/test_keys/_create_test_key_test.py b/testing/cross_language/util/test_keys/_create_test_key_test.py
new file mode 100644
index 0000000..89ebfa4
--- /dev/null
+++ b/testing/cross_language/util/test_keys/_create_test_key_test.py
@@ -0,0 +1,190 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for create_test_key."""
+
+from absl.testing import absltest
+from absl.testing import parameterized
+from tink import aead
+from tink import daead
+from tink import hybrid
+from tink import jwt
+from tink import mac
+from tink import prf
+from tink import signature
+from tink import streaming_aead
+
+from tink.proto import aes_gcm_pb2
+from tink.proto import tink_pb2
+import tink_config
+from util import key_util
+from util import test_keys
+from util import utilities
+
+
+def _do_not_use_stored_key(_: tink_pb2.KeyTemplate) -> bool:
+  return False
+
+
+def _use_stored_key(_: tink_pb2.KeyTemplate) -> bool:
+  return True
+
+
+def setUpModule():
+  aead.register()
+  daead.register()
+  hybrid.register()
+  jwt.register_jwt_mac()
+  jwt.register_jwt_signature()
+  mac.register()
+  prf.register()
+  signature.register()
+  streaming_aead.register()
+
+
+class CreateTestKeyTest(parameterized.TestCase):
+
+  def test_get_new_aes_gcm_key(self):
+    """Tests that AES GCM Keys can be generated on the fly."""
+    template = aead.aead_key_templates.AES128_GCM
+    key = test_keys.new_or_stored_key(template, test_keys.TestKeysContainer(),
+                                      _do_not_use_stored_key)
+    self.assertEqual(key.key_data.type_url,
+                     'type.googleapis.com/google.crypto.tink.AesGcmKey')
+    parsed_key = aes_gcm_pb2.AesGcmKey()
+    parsed_key.ParseFromString(key.key_data.value)
+    self.assertLen(parsed_key.key_value, 16)
+
+  def test_get_precomputed_aes_gcm_key(self):
+    """Tests a key in the container will be retrieved if needed."""
+
+    # First, create a template and a key manually
+    template = aead.aead_key_templates.AES128_GCM
+    key = test_keys.new_or_stored_key(template, test_keys.TestKeysContainer(),
+                                      _do_not_use_stored_key)
+    # Insert the key into a container
+    container_with_aes_gcm_key = test_keys.TestKeysContainer()
+    container_with_aes_gcm_key.add_key(
+        key_util.text_format(template), key_util.text_format(key))
+    key_from_create = test_keys.new_or_stored_key(template,
+                                                  container_with_aes_gcm_key,
+                                                  _use_stored_key)
+    # It suffices to compare the key material to check if the keys are the same
+    self.assertEqual(key.key_data.value, key_from_create.key_data.value)
+
+  def test_get_non_existing_precomputed_aes_gcm_key(self):
+    """Tests a key in the container will be retrieved if needed."""
+
+    template = aead.aead_key_templates.AES128_GCM
+    container = test_keys.TestKeysContainer()
+    with self.assertRaises(ValueError):
+      test_keys.new_or_stored_key(template, container, _use_stored_key)
+
+  def test_get_keyset_new_aes_gcm_key(self):
+    """Tests that AES GCM Keys can be generated on the fly."""
+    template = aead.aead_key_templates.AES128_GCM
+    serialized_keyset = test_keys.new_or_stored_keyset(
+        template,
+        test_keys.TestKeysContainer(),
+        _do_not_use_stored_key)
+    keyset = tink_pb2.Keyset.FromString(serialized_keyset)
+    self.assertLen(keyset.key, 1)
+    self.assertEqual(keyset.primary_key_id, keyset.key[0].key_id)
+    self.assertEqual(keyset.key[0].key_data.type_url,
+                     'type.googleapis.com/google.crypto.tink.AesGcmKey')
+    parsed_key = aes_gcm_pb2.AesGcmKey()
+    parsed_key.ParseFromString(keyset.key[0].key_data.value)
+    self.assertLen(parsed_key.key_value, 16)
+
+  def test_get_keyset_precomputed_aes_gcm_key(self):
+    """Tests a key in the container will be retrieved if needed."""
+
+    # First, create a template and a key manually
+    template = aead.aead_key_templates.AES128_GCM
+    key = test_keys.new_or_stored_key(template, test_keys.TestKeysContainer(),
+                                      _do_not_use_stored_key)
+    # Insert the key into a container
+    container_with_aes_gcm_key = test_keys.TestKeysContainer()
+    container_with_aes_gcm_key.add_key(
+        key_util.text_format(template), key_util.text_format(key))
+    serialized_keyset = test_keys.new_or_stored_keyset(
+        template, container_with_aes_gcm_key, _use_stored_key)
+    keyset = tink_pb2.Keyset.FromString(serialized_keyset)
+    # It suffices to compare the key material to check if the keys are the same
+    self.assertLen(keyset.key, 1)
+    self.assertEqual(keyset.primary_key_id, keyset.key[0].key_id)
+    self.assertEqual(keyset.key[0].key_data.type_url,
+                     'type.googleapis.com/google.crypto.tink.AesGcmKey')
+    self.assertEqual(key.key_data.value, keyset.key[0].key_data.value)
+
+  def test_get_keyset_non_existing_precomputed_aes_gcm_key(self):
+    """Tests a key in the container will be retrieved if needed."""
+
+    template = aead.aead_key_templates.AES128_GCM
+    container = test_keys.TestKeysContainer()
+    with self.assertRaises(ValueError):
+      test_keys.new_or_stored_keyset(template, container, _use_stored_key)
+
+  def test_key_from_test_keys_db_get_chacha_key(self):
+    """Tests that with only one arguments we get keys from _test_keys_db.py."""
+
+    parsed_template = tink_pb2.KeyTemplate()
+    key_util.parse_text_format(
+        serialized=r"""type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+# value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+# }
+value: ""
+output_prefix_type: RAW""",
+        msg=parsed_template)
+    key = test_keys.new_or_stored_key(parsed_template)
+    # The same value as in _test_keys_db for the raw key.
+    self.assertEqual(
+        key.key_data.value,
+        b'\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611'
+    )
+
+  def test_keyset_from_test_keys_db_get_chacha_key(self):
+    """Tests that with only one arguments we get keys from _test_keys_db.py."""
+
+    parsed_template = tink_pb2.KeyTemplate()
+    key_util.parse_text_format(
+        serialized=r"""type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+# value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+# }
+value: ""
+output_prefix_type: RAW""",
+        msg=parsed_template)
+    serialized_keyset = test_keys.new_or_stored_keyset(parsed_template)
+    keyset = tink_pb2.Keyset.FromString(serialized_keyset)
+    self.assertLen(keyset.key, 1)
+    # The same value as in _test_keys_db for the raw key.
+    self.assertEqual(
+        keyset.key[0].key_data.value,
+        b'\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611'
+    )
+
+  @parameterized.parameters([
+      aead.Aead, daead.DeterministicAead, streaming_aead.StreamingAead,
+      hybrid.HybridDecrypt, hybrid.HybridEncrypt, mac.Mac,
+      signature.PublicKeySign, signature.PublicKeyVerify, prf.PrfSet,
+      jwt.JwtMac, jwt.JwtPublicKeySign, jwt.JwtPublicKeyVerify
+  ])
+  def test_create_test_keys_for_primitive(self, primitive):
+    keyset = test_keys.some_keyset_for_primitive(primitive)
+    key_types = utilities.key_types_in_keyset(keyset)
+    for key_type in key_types:
+      self.assertIn(key_type, tink_config.key_types_for_primitive(primitive))
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/util/test_keys/_test_keys_container.py b/testing/cross_language/util/test_keys/_test_keys_container.py
new file mode 100644
index 0000000..986fd91
--- /dev/null
+++ b/testing/cross_language/util/test_keys/_test_keys_container.py
@@ -0,0 +1,66 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A container to store precomputed keys."""
+
+import textwrap
+from typing import Dict
+
+from tink.proto import tink_pb2
+from util import key_util
+
+
+class TestKeysContainer():
+  """Container for test keys."""
+
+  _map: Dict[str, tink_pb2.Keyset.Key]
+
+  def __init__(self):
+    self._map = {}
+
+  def add_key(self, template: str, key: str) -> None:
+    """Adds a new key to the list of precomputed keys.
+
+    The arguments need to be in the format produced by key_util.text_format,
+    but can be additionally indented and have whitespace (it needs to be in the
+    format after calling textwrap.dedent() and strip()).
+
+    The key will be stored in a map keyed by the template, unless a key-value
+    pair with this template as key was previously inserted in a call to
+    'add_key'.
+
+    Args:
+      template: A key template in the format created by key_util.text_format,
+        possibly indented, and with additional spaces at the beginning and the
+        end.
+      key: A key corresponding to the template in the format created by
+        key_util.text_format, possibly indented, and with additional spaces.
+    """
+
+    dedented_template = textwrap.dedent(template).strip()
+    dedented_key = textwrap.dedent(key).strip()
+    parsed_template = tink_pb2.KeyTemplate()
+    # We parse to check the correctness of the formatting
+    key_util.parse_text_format(dedented_template, parsed_template)
+
+    parsed_key = tink_pb2.Keyset.Key()
+    key_util.parse_text_format(dedented_key, parsed_key)
+    if dedented_template in self._map:
+      raise ValueError('Template already present')
+    self._map[dedented_template] = parsed_key
+
+  def get_key(self, template: tink_pb2.KeyTemplate) -> tink_pb2.Keyset.Key:
+    """Returns a previously stored key for this template."""
+
+    template_text_format = key_util.text_format(template)
+    return self._map[template_text_format]
diff --git a/testing/cross_language/util/test_keys/_test_keys_container_test.py b/testing/cross_language/util/test_keys/_test_keys_container_test.py
new file mode 100644
index 0000000..b606144
--- /dev/null
+++ b/testing/cross_language/util/test_keys/_test_keys_container_test.py
@@ -0,0 +1,194 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for _test_keys_container."""
+
+from absl.testing import absltest
+
+from tink.proto import tink_pb2
+from util import test_keys
+
+
+class TestKeysContainerTest(absltest.TestCase):
+
+  def test_insert_and_retrieve(self):
+    container = test_keys.TestKeysContainer()
+    container.add_key(
+        template=r"""
+          type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+          # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+          # }
+          value: ""
+          output_prefix_type: RAW""",
+        key=r"""
+          key_data {
+            type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+            # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+            #   version: 0
+            #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            # }
+            value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            key_material_type: SYMMETRIC
+          }
+          status: ENABLED
+          key_id: 1349954765
+          output_prefix_type: RAW""")
+    template = tink_pb2.KeyTemplate(
+        type_url='type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key',
+        output_prefix_type=tink_pb2.RAW)
+
+    key = container.get_key(template)
+    self.assertEqual(key.status, tink_pb2.ENABLED)
+    self.assertEqual(key.key_id, 1349954765)
+    self.assertEqual(key.output_prefix_type, tink_pb2.RAW)
+    self.assertEqual(key.key_data.key_material_type, tink_pb2.KeyData.SYMMETRIC)
+    self.assertEqual(
+        key.key_data.value,
+        b'\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611'
+    )
+
+  def test_element_not_present_throws(self):
+    container = test_keys.TestKeysContainer()
+    template = tink_pb2.KeyTemplate(
+        type_url='type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key',
+        output_prefix_type=tink_pb2.RAW)
+    with self.assertRaises(KeyError):
+      container.get_key(template)
+
+  def test_wrong_format_throws(self):
+    valid_template = r"""
+      type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+      # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+      # }
+      value: ""
+      output_prefix_type: RAW"""
+    valid_key = r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+        # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+        #   version: 0
+        #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+        # }
+        value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+        key_material_type: SYMMETRIC
+      }
+      status: ENABLED
+      key_id: 1349954765
+      output_prefix_type: RAW"""
+    container = test_keys.TestKeysContainer()
+    with self.assertRaises(AssertionError):
+      container.add_key('# Comment\n' + valid_template, valid_key)
+    with self.assertRaises(AssertionError):
+      container.add_key(valid_template, '# Comment\n' + valid_key)
+
+    # To check that the above constants are valid, we insert them
+    container.add_key(valid_template, valid_key)
+
+  def test_multiple_keys_works(self):
+    container = test_keys.TestKeysContainer()
+    container.add_key(
+        template=r"""
+          type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+          # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+          # }
+          value: ""
+          output_prefix_type: RAW""",
+        key=r"""
+          key_data {
+            type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+            # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+            #   version: 0
+            #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            # }
+            value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            key_material_type: SYMMETRIC
+          }
+          status: ENABLED
+          key_id: 1349954765
+          output_prefix_type: RAW""")
+    container.add_key(
+        template=r"""
+          type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+          # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+          # }
+          value: ""
+          output_prefix_type: TINK""",
+        key=r"""
+          key_data {
+            type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+            # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+            #   version: 0
+            #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            # }
+            value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            key_material_type: SYMMETRIC
+          }
+          status: ENABLED
+          key_id: 1349954765
+          output_prefix_type: TINK""")
+    template = tink_pb2.KeyTemplate(
+        type_url='type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key',
+        output_prefix_type=tink_pb2.RAW)
+    self.assertEqual(
+        container.get_key(template).output_prefix_type, tink_pb2.RAW)
+    template.output_prefix_type = tink_pb2.TINK
+    self.assertEqual(
+        container.get_key(template).output_prefix_type, tink_pb2.TINK)
+
+  def test_insert_same_template_twice_fails(self):
+    container = test_keys.TestKeysContainer()
+    container.add_key(
+        template=r"""
+          type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+          # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+          # }
+          value: ""
+          output_prefix_type: RAW""",
+        key=r"""
+          key_data {
+            type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+            # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+            #   version: 0
+            #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            # }
+            value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+            key_material_type: SYMMETRIC
+          }
+          status: ENABLED
+          key_id: 1349954765
+          output_prefix_type: RAW""")
+    with self.assertRaises(ValueError):
+      container.add_key(
+          template=r"""
+            type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+            # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+            # }
+            value: ""
+            output_prefix_type: RAW""",
+          key=r"""
+            key_data {
+              type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+              # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+              #   version: 0
+              #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+              # }
+              value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+              key_material_type: SYMMETRIC
+            }
+            status: ENABLED
+            key_id: 1349954764
+            output_prefix_type: TINK""")
+
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/cross_language/util/test_keys/_test_keys_db.py b/testing/cross_language/util/test_keys/_test_keys_db.py
new file mode 100644
index 0000000..47c5bad
--- /dev/null
+++ b/testing/cross_language/util/test_keys/_test_keys_db.py
@@ -0,0 +1,839 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Database of precomputed Tink Keys for the cross language tests.
+
+To add a new key to this, we recommend the following process:
+1) Add a test which calls one of test_keys.new_or_stored_key or
+   test_keys.new_or_stored_keyset
+2) Change _use_stored_key in _create_test_keys.py to return true for the key
+   format you would like to add to this list.
+3) Run your test. It should fail, and give you a test command to run to produce
+   a key.
+4) Run the test command given. It should fail, and give you a new block to add
+   to this file here.
+"""
+from util.test_keys import _test_keys_container
+
+db = _test_keys_container.TestKeysContainer()
+
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+      # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+      # }
+      value: ""
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+        # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+        #   version: 0
+        #   key_value: "\372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+        # }
+        value: "\022 \372\022\371\335\313\301\314\253\r\364\376\341o\242\375\000p\317,t\326\373U\332\267\342\212\210\2160\3611"
+        key_material_type: SYMMETRIC
+      }
+      status: ENABLED
+      key_id: 1349954765
+      output_prefix_type: RAW""")
+
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+      # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305KeyFormat] {
+      # }
+      value: ""
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
+        # value: [type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key] {
+        #   version: 0
+        #   key_value: ".\361n\315k\373\266\030\234N\360~6d\304sZ\325*\005\355\010~\376#\352\221<\214@*s"
+        # }
+        value: "\022 .\361n\315k\373\266\030\234N\360~6d\304sZ\325*\005\355\010~\376#\352\221<\214@*s"
+        key_material_type: SYMMETRIC
+      }
+      status: ENABLED
+      key_id: 653548180
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.RsaSsaPkcs1KeyFormat] {
+      #   params {
+      #     hash_type: SHA256
+      #   }
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\n\002\010\003\020\200\030\032\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     params {
+        #       hash_type: SHA256
+        #     }
+        #     n: "\302\322\337R\330\321\005\223b@\037\317\354\001/\3741\326[\251\005\331\002\3032\367S\034\252h,\2251\252\234\251rU9\234P\274\365J\254Zfe>1\252\375j\227\360\021\014a\352_jNc\274y.v\3501\367N\177\013\001\316g\"\226\350\240\334\2274!\177\311F\364\206\205F\242\261\212p\271\316S\330\037\206\213\245\332S\016\302\267!Q\262\0260\250\340\r\337f\351\251\366Bg\326\214\253xH@\027\335.^\016\232\324\261y\2256\312\207\"\266\300\203\300\213\3573\020r\000\002z\010\217-\216\232|\254F\320\246\\:\257\274\013\370\361\321\215\334\225\321<]7S0\254\201\247\026\325\324\006\301\271\277\336gs\001\262\202`\363u\367\371\261\207\370\234\200\360\212\350\220!\227\276\273\361\272a\003\034\\\252\262\326\234B\027\267\201\235\332\333\205\203\307\341oP.\033\357\375n\250\361M\201B\373L\327\345\364\204\004v\270\263\323I\255\204.\247\335\231\366.\346\277\224\275W\357>]\314\272\271b9|\320\016\327\0209\250\253\300wb\005|\\\032\n!]\0011R8\244c\253(c\001\017\335\016\223W\242\035\331\201\311%\370\363\261\307\025\033u \312\376\005\033\244s\323\301 \264\377\312\205\2226|\221}\037;\242\026\032\031\310jj\2604\260\026\037\214\0163\235\2533mW\311\322\367\244:\217a\025\023[\237\221%\324\221"
+        #     e: "\001\000\001"
+        #   }
+        #   d: " \027b\254o\244\374\225b\257{\336\000\272\320W\3530\253P\013\363\313i\362l\375~\201\016`[\241\376\377\307\304\232\307\276\331(\313\001\346\"\214\357\333\337\014\022\252\272\204\323\226\025\321\350\256&\343\364a\204\267\026\367\326\014\022\316\262\0318\361`h\310\310\225J\203\303\034\031IA\230V\002\212\032\253\007\\6\200\350\356}u\320 \362\323=\235\252*\232\317\240\235\251\203U]#\177:@\206\375\214h\'\204\2219\300\177\303I\233C\361\252S\310h\032\236~W%\202\027`r\262\310\220\037\340e\217DHk\305g\363\312\027r\236Ll/qK\312\241\235w\006\t\267\311\207\000\277i\221\320\036\231\017S\244~\341M\023\317k\007\357\214ZH}\030\023\374U\234\272\232jF4f\242\222w\2725\252\324L\321\221p\242=w\363S?#}\362cw4v\307\246\351\347N\377~\264Q\224\271\t\027w>\033Z\024\362d-a\262mk1\252m\366\030\265\017wE*\030\022\364\203E\214\014\203\364\206\331\224u\347`\232;5\237\251w\340@@G\234\332\306[\261\367\346\343\274\206\247\'[\244\353\3211]\373c\262\3310\343!\204\301)\016\2747a\310\361\t\207\267\277\230,@S\353?\314\322\265\235\004\016\217\303\212\215-\366O\235\206=<\034\034\337R\263\262\243\333\2258\021\357\377\253m\n6h\331J\351J\205\r"
+        #   p: "\350J^\264\3240U\265\261\016H\024\335\\y\216\031\235\325\027\266\307\226i\220\347\374\200\022\316<Q\000\232\262{\271$\re\314\341\304\373\343\356\335\017\245P\377\232\201\367\343\033\034<\366-\303\233x\325pG2`m6\251\\\"/(\206=X\366- w\331\004\272l\033\242^\265\nM\240\253\0131\003L\r9\252\311\"\207\030\353\316\325+\266:\374$\250\245\242\22029\203\3200O&\335\243`\t\2443\322\243W]\010.\310\270<\324\307*\013\320}\004]F\331C\333\307\325\335\024\303f\372\t\216\373y\363\013K\230|k\244\360q\034\226\367\276}\3218K\220qx!\354\034h\364\362\2035)\r"
+        #   q: "\326\265\203\307\033\345\343 \177\305\272\251m\322\240\263\313Ez\233&Q\r\265\220\351\266\025\306\006t\356.\222\327b8\261\376h\240\025\022q\320\245\354\237\035&\371\226\022\303\331\215a$\211\373`u\227\340\013I\242!\323\271fZz\257\336n\310\223\372Z\317\\*Ud\2766\272n\251\345\227\306\307C\276d\026\024\002\221\340\274\203b\033\310C\034\356\261#\244\354q\311#\1779\007%\t\220\034-=o\3735\271\236\301aJi\352\266Iga\261\367B\327\025\314\203?\354\326\216\034M\273\025q\262\377\234j`3\253g\320\300\004\020:`\351+G\r\224\276,\272\322\310d\340\n\243\010\004\342,K\354\260\225"
+        #   dp: "\221b\212\247\356\356Y\226\240<j\014\365\006\375\345\\O\026\242\002\372$?\352;\350#X\241\317\312z\360W\251l\025\016u\232\247\353\234#\352\312]\022\201\001\231x\242\347w~S\001Y_F\035x\251\017F\216g\214\200\257s\215\304uPL/\267\312\314w\375\247\230&+\227\317\003\245\326[wc\263\306\223\270v\025\361\020Q\036\265\223b\nj\034\211\355+\315\365&\032-O\316\014\234,\270&Q\242}\364\345\332\266\025{\002\221\2351\'~?j\273\364QU\030\202\212:&\266zZZG@XG8\352O5\1778\320>\251f\202\030R\362\334\330g\275\223\250\021\"Z\036\345\351\371&\371\314\271"
+        #   dq: "?x\006\315[\354\327\370*m\033\341\253\312@\241\005l\263r\233\240\010*l\374\371KtoB/\212j\363\352`\022\025\277}\332\334\311\362@\2522\332\336C\227^R\260\303\277<\232\341IAY\264\354\001GA2\2422\244zq\364\221\321\017\025I\264\324l\3333]\2335\3772\217\341\016\354\246\224\350g2a\024\030X\001\212\205\345\354\030\350bJ\304\034\365\001\335Qe]<P\230^\013\355\206e\277\032\360\327\377W\214\371\312\n\346\331\225O\037^\017^\226I\326L\022TA\222\025#\270#!l\274\245i|\325\323} \002\361\324\333k4\223\214\250\022M\356\331\n\365y\376\013\333\203\366\251\260\005"
+        #   crt: " AE\311\351y\322\306\236n\274\230\351\374\355Z\261\35430\022\221\214\303\2403\234\177\320\230\n&\020Y\220\177L\365{\000.\001\3179\254@7c\0131$\277\306C\351zsae\213L\t\275\216\ra\222H]\177\007@5,\375Q\277?\345O>\256\315u\273\000YY\340\235\373\026\225\212\245\347\037u\037\204k\204~@\326\026S\236i\211}\365\227?a\305\222\211\020\331\275\355J\027\351`\346\016\000\206\254\305\204\242_\014\034H\211\377:\032\036@\345I\027\321hv\374\030\227\245\260\343\037\'\203[`-L\200D\363TL\026Eu:\024\226\354R\343\304)\363)\254[CQ\301\303\245\305{\013\256"
+        # }
+        value: "\022\214\003\022\002\010\003\032\200\003\302\322\337R\330\321\005\223b@\037\317\354\001/\3741\326[\251\005\331\002\3032\367S\034\252h,\2251\252\234\251rU9\234P\274\365J\254Zfe>1\252\375j\227\360\021\014a\352_jNc\274y.v\3501\367N\177\013\001\316g\"\226\350\240\334\2274!\177\311F\364\206\205F\242\261\212p\271\316S\330\037\206\213\245\332S\016\302\267!Q\262\0260\250\340\r\337f\351\251\366Bg\326\214\253xH@\027\335.^\016\232\324\261y\2256\312\207\"\266\300\203\300\213\3573\020r\000\002z\010\217-\216\232|\254F\320\246\\:\257\274\013\370\361\321\215\334\225\321<]7S0\254\201\247\026\325\324\006\301\271\277\336gs\001\262\202`\363u\367\371\261\207\370\234\200\360\212\350\220!\227\276\273\361\272a\003\034\\\252\262\326\234B\027\267\201\235\332\333\205\203\307\341oP.\033\357\375n\250\361M\201B\373L\327\345\364\204\004v\270\263\323I\255\204.\247\335\231\366.\346\277\224\275W\357>]\314\272\271b9|\320\016\327\0209\250\253\300wb\005|\\\032\n!]\0011R8\244c\253(c\001\017\335\016\223W\242\035\331\201\311%\370\363\261\307\025\033u \312\376\005\033\244s\323\301 \264\377\312\205\2226|\221}\037;\242\026\032\031\310jj\2604\260\026\037\214\0163\235\2533mW\311\322\367\244:\217a\025\023[\237\221%\324\221\"\003\001\000\001\032\200\003 \027b\254o\244\374\225b\257{\336\000\272\320W\3530\253P\013\363\313i\362l\375~\201\016`[\241\376\377\307\304\232\307\276\331(\313\001\346\"\214\357\333\337\014\022\252\272\204\323\226\025\321\350\256&\343\364a\204\267\026\367\326\014\022\316\262\0318\361`h\310\310\225J\203\303\034\031IA\230V\002\212\032\253\007\\6\200\350\356}u\320 \362\323=\235\252*\232\317\240\235\251\203U]#\177:@\206\375\214h\'\204\2219\300\177\303I\233C\361\252S\310h\032\236~W%\202\027`r\262\310\220\037\340e\217DHk\305g\363\312\027r\236Ll/qK\312\241\235w\006\t\267\311\207\000\277i\221\320\036\231\017S\244~\341M\023\317k\007\357\214ZH}\030\023\374U\234\272\232jF4f\242\222w\2725\252\324L\321\221p\242=w\363S?#}\362cw4v\307\246\351\347N\377~\264Q\224\271\t\027w>\033Z\024\362d-a\262mk1\252m\366\030\265\017wE*\030\022\364\203E\214\014\203\364\206\331\224u\347`\232;5\237\251w\340@@G\234\332\306[\261\367\346\343\274\206\247\'[\244\353\3211]\373c\262\3310\343!\204\301)\016\2747a\310\361\t\207\267\277\230,@S\353?\314\322\265\235\004\016\217\303\212\215-\366O\235\206=<\034\034\337R\263\262\243\333\2258\021\357\377\253m\n6h\331J\351J\205\r\"\300\001\350J^\264\3240U\265\261\016H\024\335\\y\216\031\235\325\027\266\307\226i\220\347\374\200\022\316<Q\000\232\262{\271$\re\314\341\304\373\343\356\335\017\245P\377\232\201\367\343\033\034<\366-\303\233x\325pG2`m6\251\\\"/(\206=X\366- w\331\004\272l\033\242^\265\nM\240\253\0131\003L\r9\252\311\"\207\030\353\316\325+\266:\374$\250\245\242\22029\203\3200O&\335\243`\t\2443\322\243W]\010.\310\270<\324\307*\013\320}\004]F\331C\333\307\325\335\024\303f\372\t\216\373y\363\013K\230|k\244\360q\034\226\367\276}\3218K\220qx!\354\034h\364\362\2035)\r*\300\001\326\265\203\307\033\345\343 \177\305\272\251m\322\240\263\313Ez\233&Q\r\265\220\351\266\025\306\006t\356.\222\327b8\261\376h\240\025\022q\320\245\354\237\035&\371\226\022\303\331\215a$\211\373`u\227\340\013I\242!\323\271fZz\257\336n\310\223\372Z\317\\*Ud\2766\272n\251\345\227\306\307C\276d\026\024\002\221\340\274\203b\033\310C\034\356\261#\244\354q\311#\1779\007%\t\220\034-=o\3735\271\236\301aJi\352\266Iga\261\367B\327\025\314\203?\354\326\216\034M\273\025q\262\377\234j`3\253g\320\300\004\020:`\351+G\r\224\276,\272\322\310d\340\n\243\010\004\342,K\354\260\2252\300\001\221b\212\247\356\356Y\226\240<j\014\365\006\375\345\\O\026\242\002\372$?\352;\350#X\241\317\312z\360W\251l\025\016u\232\247\353\234#\352\312]\022\201\001\231x\242\347w~S\001Y_F\035x\251\017F\216g\214\200\257s\215\304uPL/\267\312\314w\375\247\230&+\227\317\003\245\326[wc\263\306\223\270v\025\361\020Q\036\265\223b\nj\034\211\355+\315\365&\032-O\316\014\234,\270&Q\242}\364\345\332\266\025{\002\221\2351\'~?j\273\364QU\030\202\212:&\266zZZG@XG8\352O5\1778\320>\251f\202\030R\362\334\330g\275\223\250\021\"Z\036\345\351\371&\371\314\271:\300\001?x\006\315[\354\327\370*m\033\341\253\312@\241\005l\263r\233\240\010*l\374\371KtoB/\212j\363\352`\022\025\277}\332\334\311\362@\2522\332\336C\227^R\260\303\277<\232\341IAY\264\354\001GA2\2422\244zq\364\221\321\017\025I\264\324l\3333]\2335\3772\217\341\016\354\246\224\350g2a\024\030X\001\212\205\345\354\030\350bJ\304\034\365\001\335Qe]<P\230^\013\355\206e\277\032\360\327\377W\214\371\312\n\346\331\225O\037^\017^\226I\326L\022TA\222\025#\270#!l\274\245i|\325\323} \002\361\324\333k4\223\214\250\022M\356\331\n\365y\376\013\333\203\366\251\260\005B\300\001 AE\311\351y\322\306\236n\274\230\351\374\355Z\261\35430\022\221\214\303\2403\234\177\320\230\n&\020Y\220\177L\365{\000.\001\3179\254@7c\0131$\277\306C\351zsae\213L\t\275\216\ra\222H]\177\007@5,\375Q\277?\345O>\256\315u\273\000YY\340\235\373\026\225\212\245\347\037u\037\204k\204~@\326\026S\236i\211}\365\227?a\305\222\211\020\331\275\355J\027\351`\346\016\000\206\254\305\204\242_\014\034H\211\377:\032\036@\345I\027\321hv\374\030\227\245\260\343\037\'\203[`-L\200D\363TL\026Eu:\024\226\354R\343\304)\363)\254[CQ\301\303\245\305{\013\256"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 923074750
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.RsaSsaPkcs1KeyFormat] {
+      #   params {
+      #     hash_type: SHA512
+      #   }
+      #   modulus_size_in_bits: 4096
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\n\002\010\004\020\200 \032\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.RsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     params {
+        #       hash_type: SHA512
+        #     }
+        #     n: "\302\247\343\035\006\272\022/\031\020f\334\360\000\366\300\031?K\206\317\317\014h1\035\025\303\240*\3575=\'\212:\373\222\010\257\300=\022\253\247K\210\031\343\312\217\273\214\274\262\335\276\355XD\253\316I\022\375&*\331\342\341\027\033\232\275\354\236N\351R\267xH\377\\qi\240\351\217\345\235i\303\212\016\366\320*\311P\352\311kW\022\2742}\353q\272\233?\3104i<\364TT\362\267\330\tG=\213\013V-\307\361,\225A\325\320\374<\033 \274L\215\2202\272\350P\263\335\256\342\3238\021J$q\334\352\026\310\217O)T\000\026u\030\271\016\224xZ\303\267\377\000m\221\322\357v\346\360\211[\353n]\347\245[\265\031s+M\302\n\022\320>\2611\016I\031-H\017\306\371H6\256H\264\021\240\215z\372\342\nn\273\344z\377\261\222\034\235\321\304D8\220T)o\275W\213\305\013\213\300\255\267,\350(x\370\233\006\362\212\272=\366M\020On\307IO\204\360w\274}0\321\000$\337|\341\351\373\021\251\354\345GQh\370cv\322\361\230\265[\341\206{\'Z\'o\275\240\331\262\317Y\346\370(N\252\'L$b>\240\023\350\267\205d\343\033\322j\237\0064\253\031\233f\3650\326\330\205\000\t\024\365\377U\345\262\362\254w\210N\033M\321\202\034\226\260\035YM{P\200\006,\002&q\213\372\224\00349\265\210\227\330Bw3\371a\2305hl\314\344r\354\364\006\341\324W\204\301`\226]B\234%\374,\017n\267xV\255\305v\310\334\215,\322\307\032[\014\362\000\345|\236\261\323Z\214\260\373\305\231\341\r?\317B]\215\357\356=\223\202\2534\207#}\nM\240\220\253\022\301|\264\370$\305\31631\271\207[\000f\t\361Esw[\272\03061((\031\005o\301\\z\304\255v\264!l\302O\375h\225\373"
+        #     e: "\001\000\001"
+        #   }
+        #   d: "V\250\270\217\r\026\034\217T8\275\232\241\314\304\r\214\217\345\267l_\260\225\330;\253\033N\374\236G\024\345<\000{L-\312\324\252;\334(\234\023H\367e\020->\200)\237W\347As=\013\226\363\271\270\332\275\352\350+\311\361\317\230\376w\202\037}\247\262\323\340\333j\333\244\357\227\231Gc2\334\353\233\214X\330\311\326\007\020\032:\247\275\360jN\331\334\000\354\370g\027\035N>J\203\206\311\355\007\260q\332\352\327\037r\211\177?\315s\222\3365\206]E\263\230\013b\026Y\\\211\005\340\301\255\300\340\342)c A\320\313\304\346O\031-{\001\227.\247\265\327}\322\272t\377\263\365:\346\0368\312l\365\250q\261\014\304zE\356\225\262\267bwn\2328\322i?\204\003\346 \307\271\364HP\306B4\212j\023\344\362\034{!\031!|;z5O\213\321\244\313\2330\360Y\377\323\'\013>)\370Q)?H7U\372\226t\207\371\315=\346\356\036\352\356\277<#\301\301\3648\355pzMg\rc\303\264\344Y\326\324\200:\247\267\024\246&\273:.\335U\304\3556\017\335\203\323,eV\014(\026\010\273\342=\255\313-\331{\204\227\302.\330\266/\330\300\204&\255\354+\177\006\206\261\217c\252\022\256s\251G\336\366j6\336 R~n7\221\035\213\223\236.\212z\214\352q\360e\022E\022b\266\327t\205\320\027p\350\036*\226XX\365\346\341yD\271ie\235\240\024\302V\013\231\213\323\332\357\265\305;\242\245&T\213(\355]\004L\303\372\211UU\215\312\334\255\273\252\310\262\214Exb\362\004\203m\024{]\271\316\341D4\222?\232\365\225C\355\330\307\332\271\264^\275\253\351\210\212v\307\270\006\345\200\233\020X\003\273\301[@\212\336A\356;K\026f\034\257\220t\300Kf\203\022\261\024\351\303\215a\377|\023>\231"
+        #   p: "\356\345\000\241\204\211\235@\255\246\205\306\032n\241\207I\026\022&\313\365dd\351\330O\211\273|\276\3157\250\325\271\027\232%\301/\0342\255\312\250\365\311\254T\374J\204y\025\360\347\002\360\274\363\031\232\372\001\366\214\360\256\034\256D2g\314F\254\207\nV\351>\t$\2437\220\272E\0317\274\327Na\330x\307\210_\353\2772\3223+\215\214!>\225\356\224\034\323\2109|\033\2219\345q\212\374\352\331\327*\272\016\272N\r\023\343\317\036\217\242c\364i\357\234\321\204\276j!^\337\013\240t+t\177\256\367\331\000\3404W\017\326\rZ7It-\345\231>U\246n\033\362L\034%\266\325\254\236\222a\217\030\205\272\357,\t\245\2348\244\310FN$G\t\316\206\347\311\007t\314\226\276\014J\303;l\262\232\232o\323\027j\001\032e\034/~\265\277v\22782A\206V+k+K\3770\017\n\310,\250\262\331"
+        #   q: "\320\227\372>\376\266\'\356\250\342O4\210_S\244\001\225u}\220@A\344\373\370\021\272\302\3353\263:\370|\024b)\223\2120,_\254d\327I\3764\342C\237\354\227\023\221=\367\217\027\354\215\250&^\372\277\250:r\330k!\356\336\315\366eA]\005\331\033\037r\267\315\221\350\034\032\373+3ZB\217\264\013\022\375w\2357Z\233\361h\205\211\226\035\333g\263z\226\306\'\255c\216\365\002F%\254\373\222Qb\360\033\301\033\334\337k\256\341\010VQ\347#\347\005\007\227AqG9\365\031\211\370\371\233#\270\342yg\000g\271I\371\020\245\327ow\320c\221*\210\275#\364F\367\276\367\026\270p\265\261\210\227\215\205\014-\031>U\rG\264\231\311\236@|[\330\356\367z\245\254\211\310\322\274u\367\330\372e7\376RG\016s*\337\200h\364\365\212\344d\340\306$\035\360g\003\324\257\241_\374\016\335\366\"\363"
+        #   dp: "\211\326\314Te\232\331\207\026*\375\010\336\373rl\022\271\217c\237OI\314\0109m;5X\266\346\305\364\276\314{W\317\354\365w\271\256\031?\000\002\213R\305\367a\024T\232\231PS\003R\3515H\207\225p\240\366uI\377\303m\254\252\"\246\020K1m?\355I\327\247\3042Vh\230\247Jl\236p\242\000\210\010\010\\\024x\275\352\200.6d`2I\247\253\364/-\340\317\235\031\264nV;\2670\312\005\361mS\222da\177j\264\362#vtp\361[<[\024B\037D \"\247h\"\274\033\371\263\315\251\245\212W\241\200\252a\321\215!\353\027\322g\243$\260\333EjebH\245~$\230b7\3272e s\323\375\302\346c\332v\210\036\232q\260\217\207\243\326\340+l\000?A\254h)Xn\251G\333<#\264\257\\\240)\321\375e\'a\247\371\021\003 d\355\006\242\376*\344q\020-\221"
+        #   dq: "8O\216\302\312\310)\347\207\267\360a\342\036\271\331\240\203\324\177\335\035;\254\261H\272\240\214Rb\316\223\260\330\3567\301\271\216\250\212\222\031[\357\262\215\351\200\333x\244\311\364\002U\224+\020\356O\037\201\234J\\\312\361\\g\210$T\363\330\356\027O\231\351&>\3502s\344\016\353v\n\342\233:\365\266\332Ld`\360\004\006q\027\010\332\026\271U\367,\200:r}9\216\254\317e\375\005\257cxpFI\255\246\343\342\3322\307a\231\024P\263\213y\370\307\374i\243.\365\377\272\226\256\320\002+0V\355A\320\001\256\010\261\367\227<\241}\343\266\267\305\215\333v|\013sUR\2037\016E\027\355u_C\334\205\333\320\371\253\302\343\372\375\314\207g\242M\016EM>\325\230JyU\210\335:\373-\271i\177A\252jS\274V\342\320\013>JB\257\010\021#\214\331\333+6\344\025\332\203\332\001\225\221\336\014y"
+        #   crt: "\003\257\035\211N\034,\377\010\000\030\323\205/\224\206MG\372\r\007\316;,\027f+\000\014\302\244P4(D\257aR_\226\271\000\032\303 \000\001\367\304,\364H-W\337\350\254\264XW\367l\231\354\213\356\3155\375n\000\257\"+\364\357\305|\202\300\256\312\311_\363b\036\337`\024\365m\271\365\211\022\363\211\027\224M\326\335\266\005\257\222D\224\263\277^}\203\255q=\342;Ty\351\354r\217\310\244\243\'\347\330dl$14\242\013\257h\000\020Y\004\033\\e\033\343\327\013\223\263\202;\221\344\361\364\3079\313O)\277tU\351\207G1\031\t\240\342)\332\232\257V\001]\2732\225F\312N\255@4\207\004\355\3251]:i\364\356Ga\307\002\335\316\307\335\261g\240J\221\361\366|\253c\201W\263\364J\213\034\373\322\225(\266\344w\347\260\013a\002\277\335\354n~\377\342\255\331\220B\256\036\346\331\\\2750"
+        # }
+        value: "\022\214\004\022\002\010\004\032\200\004\302\247\343\035\006\272\022/\031\020f\334\360\000\366\300\031?K\206\317\317\014h1\035\025\303\240*\3575=\'\212:\373\222\010\257\300=\022\253\247K\210\031\343\312\217\273\214\274\262\335\276\355XD\253\316I\022\375&*\331\342\341\027\033\232\275\354\236N\351R\267xH\377\\qi\240\351\217\345\235i\303\212\016\366\320*\311P\352\311kW\022\2742}\353q\272\233?\3104i<\364TT\362\267\330\tG=\213\013V-\307\361,\225A\325\320\374<\033 \274L\215\2202\272\350P\263\335\256\342\3238\021J$q\334\352\026\310\217O)T\000\026u\030\271\016\224xZ\303\267\377\000m\221\322\357v\346\360\211[\353n]\347\245[\265\031s+M\302\n\022\320>\2611\016I\031-H\017\306\371H6\256H\264\021\240\215z\372\342\nn\273\344z\377\261\222\034\235\321\304D8\220T)o\275W\213\305\013\213\300\255\267,\350(x\370\233\006\362\212\272=\366M\020On\307IO\204\360w\274}0\321\000$\337|\341\351\373\021\251\354\345GQh\370cv\322\361\230\265[\341\206{\'Z\'o\275\240\331\262\317Y\346\370(N\252\'L$b>\240\023\350\267\205d\343\033\322j\237\0064\253\031\233f\3650\326\330\205\000\t\024\365\377U\345\262\362\254w\210N\033M\321\202\034\226\260\035YM{P\200\006,\002&q\213\372\224\00349\265\210\227\330Bw3\371a\2305hl\314\344r\354\364\006\341\324W\204\301`\226]B\234%\374,\017n\267xV\255\305v\310\334\215,\322\307\032[\014\362\000\345|\236\261\323Z\214\260\373\305\231\341\r?\317B]\215\357\356=\223\202\2534\207#}\nM\240\220\253\022\301|\264\370$\305\31631\271\207[\000f\t\361Esw[\272\03061((\031\005o\301\\z\304\255v\264!l\302O\375h\225\373\"\003\001\000\001\032\200\004V\250\270\217\r\026\034\217T8\275\232\241\314\304\r\214\217\345\267l_\260\225\330;\253\033N\374\236G\024\345<\000{L-\312\324\252;\334(\234\023H\367e\020->\200)\237W\347As=\013\226\363\271\270\332\275\352\350+\311\361\317\230\376w\202\037}\247\262\323\340\333j\333\244\357\227\231Gc2\334\353\233\214X\330\311\326\007\020\032:\247\275\360jN\331\334\000\354\370g\027\035N>J\203\206\311\355\007\260q\332\352\327\037r\211\177?\315s\222\3365\206]E\263\230\013b\026Y\\\211\005\340\301\255\300\340\342)c A\320\313\304\346O\031-{\001\227.\247\265\327}\322\272t\377\263\365:\346\0368\312l\365\250q\261\014\304zE\356\225\262\267bwn\2328\322i?\204\003\346 \307\271\364HP\306B4\212j\023\344\362\034{!\031!|;z5O\213\321\244\313\2330\360Y\377\323\'\013>)\370Q)?H7U\372\226t\207\371\315=\346\356\036\352\356\277<#\301\301\3648\355pzMg\rc\303\264\344Y\326\324\200:\247\267\024\246&\273:.\335U\304\3556\017\335\203\323,eV\014(\026\010\273\342=\255\313-\331{\204\227\302.\330\266/\330\300\204&\255\354+\177\006\206\261\217c\252\022\256s\251G\336\366j6\336 R~n7\221\035\213\223\236.\212z\214\352q\360e\022E\022b\266\327t\205\320\027p\350\036*\226XX\365\346\341yD\271ie\235\240\024\302V\013\231\213\323\332\357\265\305;\242\245&T\213(\355]\004L\303\372\211UU\215\312\334\255\273\252\310\262\214Exb\362\004\203m\024{]\271\316\341D4\222?\232\365\225C\355\330\307\332\271\264^\275\253\351\210\212v\307\270\006\345\200\233\020X\003\273\301[@\212\336A\356;K\026f\034\257\220t\300Kf\203\022\261\024\351\303\215a\377|\023>\231\"\200\002\356\345\000\241\204\211\235@\255\246\205\306\032n\241\207I\026\022&\313\365dd\351\330O\211\273|\276\3157\250\325\271\027\232%\301/\0342\255\312\250\365\311\254T\374J\204y\025\360\347\002\360\274\363\031\232\372\001\366\214\360\256\034\256D2g\314F\254\207\nV\351>\t$\2437\220\272E\0317\274\327Na\330x\307\210_\353\2772\3223+\215\214!>\225\356\224\034\323\2109|\033\2219\345q\212\374\352\331\327*\272\016\272N\r\023\343\317\036\217\242c\364i\357\234\321\204\276j!^\337\013\240t+t\177\256\367\331\000\3404W\017\326\rZ7It-\345\231>U\246n\033\362L\034%\266\325\254\236\222a\217\030\205\272\357,\t\245\2348\244\310FN$G\t\316\206\347\311\007t\314\226\276\014J\303;l\262\232\232o\323\027j\001\032e\034/~\265\277v\22782A\206V+k+K\3770\017\n\310,\250\262\331*\200\002\320\227\372>\376\266\'\356\250\342O4\210_S\244\001\225u}\220@A\344\373\370\021\272\302\3353\263:\370|\024b)\223\2120,_\254d\327I\3764\342C\237\354\227\023\221=\367\217\027\354\215\250&^\372\277\250:r\330k!\356\336\315\366eA]\005\331\033\037r\267\315\221\350\034\032\373+3ZB\217\264\013\022\375w\2357Z\233\361h\205\211\226\035\333g\263z\226\306\'\255c\216\365\002F%\254\373\222Qb\360\033\301\033\334\337k\256\341\010VQ\347#\347\005\007\227AqG9\365\031\211\370\371\233#\270\342yg\000g\271I\371\020\245\327ow\320c\221*\210\275#\364F\367\276\367\026\270p\265\261\210\227\215\205\014-\031>U\rG\264\231\311\236@|[\330\356\367z\245\254\211\310\322\274u\367\330\372e7\376RG\016s*\337\200h\364\365\212\344d\340\306$\035\360g\003\324\257\241_\374\016\335\366\"\3632\200\002\211\326\314Te\232\331\207\026*\375\010\336\373rl\022\271\217c\237OI\314\0109m;5X\266\346\305\364\276\314{W\317\354\365w\271\256\031?\000\002\213R\305\367a\024T\232\231PS\003R\3515H\207\225p\240\366uI\377\303m\254\252\"\246\020K1m?\355I\327\247\3042Vh\230\247Jl\236p\242\000\210\010\010\\\024x\275\352\200.6d`2I\247\253\364/-\340\317\235\031\264nV;\2670\312\005\361mS\222da\177j\264\362#vtp\361[<[\024B\037D \"\247h\"\274\033\371\263\315\251\245\212W\241\200\252a\321\215!\353\027\322g\243$\260\333EjebH\245~$\230b7\3272e s\323\375\302\346c\332v\210\036\232q\260\217\207\243\326\340+l\000?A\254h)Xn\251G\333<#\264\257\\\240)\321\375e\'a\247\371\021\003 d\355\006\242\376*\344q\020-\221:\200\0028O\216\302\312\310)\347\207\267\360a\342\036\271\331\240\203\324\177\335\035;\254\261H\272\240\214Rb\316\223\260\330\3567\301\271\216\250\212\222\031[\357\262\215\351\200\333x\244\311\364\002U\224+\020\356O\037\201\234J\\\312\361\\g\210$T\363\330\356\027O\231\351&>\3502s\344\016\353v\n\342\233:\365\266\332Ld`\360\004\006q\027\010\332\026\271U\367,\200:r}9\216\254\317e\375\005\257cxpFI\255\246\343\342\3322\307a\231\024P\263\213y\370\307\374i\243.\365\377\272\226\256\320\002+0V\355A\320\001\256\010\261\367\227<\241}\343\266\267\305\215\333v|\013sUR\2037\016E\027\355u_C\334\205\333\320\371\253\302\343\372\375\314\207g\242M\016EM>\325\230JyU\210\335:\373-\271i\177A\252jS\274V\342\320\013>JB\257\010\021#\214\331\333+6\344\025\332\203\332\001\225\221\336\014yB\200\002\003\257\035\211N\034,\377\010\000\030\323\205/\224\206MG\372\r\007\316;,\027f+\000\014\302\244P4(D\257aR_\226\271\000\032\303 \000\001\367\304,\364H-W\337\350\254\264XW\367l\231\354\213\356\3155\375n\000\257\"+\364\357\305|\202\300\256\312\311_\363b\036\337`\024\365m\271\365\211\022\363\211\027\224M\326\335\266\005\257\222D\224\263\277^}\203\255q=\342;Ty\351\354r\217\310\244\243\'\347\330dl$14\242\013\257h\000\020Y\004\033\\e\033\343\327\013\223\263\202;\221\344\361\364\3079\313O)\277tU\351\207G1\031\t\240\342)\332\232\257V\001]\2732\225F\312N\255@4\207\004\355\3251]:i\364\356Ga\307\002\335\316\307\335\261g\240J\221\361\366|\253c\201W\263\364J\213\034\373\322\225(\266\344w\347\260\013a\002\277\335\354n~\377\342\255\331\220B\256\036\346\331\\\2750"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 2006215308
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.RsaSsaPssKeyFormat] {
+      #   params {
+      #     sig_hash: SHA256
+      #     mgf1_hash: SHA256
+      #     salt_length: 32
+      #   }
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\n\006\010\003\020\003\030 \020\200\030\032\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     params {
+        #       sig_hash: SHA256
+        #       mgf1_hash: SHA256
+        #       salt_length: 32
+        #     }
+        #     n: "\255\355\244\342\\\347>\206\375|\321\023\372\211\210~\325\345\032\"\323\032\036f8D2u<.|\333\314\2177\246\3315\320S\305\331\343\325\206U\322\224\340\017$e\255\270K\275\013\221V\342/Z\254\367\342\207x!\033\204o\237\242\002\356I\372\304=Q\0133\037\376\311\322\254\322\277\2131\202^v\352Yh\334\010\310\315\347R\311t\345\003[\275N\226\315r\254\023\373\311<\333&\252\330\202\321&\260\245v3\027\361\206}\024\236\314\355\325*+\2376\374\221)\343*\367\316M[2\373fN\264\201XHG\'mD\317\250\226v\305,\364S\271`p\334\'\331G\\,\371\316\367\316\363t\"1]\034B\031\017S\364 \323\\\312X\027\305}\264\327N\020\035\213(\360\014\177\265\251\263\226\351\245\255\227\007{\250A\237\215\2316\030\017\024LPG|pVO\016\224\207\350\322F\030\366/\235K\244\352\345g\233Kt\305\321\207 &\031\332I:\353\313\201\021n{X\254\350g?\347d\202\363\354\'\377H\243d\363\352\320\360_SS\272D\024\022\\?G\233z\033h\266\264\375}V3;zj\200^\247\317\'\020\334M\361\223]p\340\000\277\202\251U\3269K\033\261O\263\300\234x\270\241\336f\220\036\262\263(\3145\206\'\006\333\357\\\276\324\034!\374\363\nCX\021\214\261\246\214>D\253\257u\365\211\325DqG"
+        #     e: "\001\000\001"
+        #   }
+        #   d: "@\302N\225\257\364\263\335\314-8\245\301/\213@z\200\242\242\033\312\312M\322\036fQ+V\275\313\223\0272\'\360\021.\337\2727\2362\247!vVS4\240\0371\267\000\220Ci\332\231=\373{\222\222hQL\266s\261\242\024\317\025\215;\016\355+\253I^\\\336%\220\216\251\235\370\221\253}\210UA\013HD\010\022\\\313@\270ro*\205\303\365|\000a2k#\273\017c&YR\351\236u\005\025\022\021\256;\r=\361\\\216ut\247\203\3607V\212\3149W9\213>\350\307\027\177\362\366\375vl\322-\000J\253\267ND&6\264;\231\332\007\006J!\"g\342\355v\303C\326\345]_C\246u\233\360_o\002\373h\\#\231\024\204\030\264\243\371\371\211\305\375\026\223\017\354\232\235W\007B\210\212L6\014O\373>Q\214\3628[W\311\235\214:\270.R\370\230]\002\216OG\211\026!a\211\255Q\355\037\207\033Up\276Z\334\207P\221\004\034/\322\274\366\3127\274\216\2634\035\343QI\260D\2277b\231\202\315\023}Hf\343W\354\207\267zL\272\016bkV\226\n\364<\365\006\247\201\366E\206,\362\026\307\366i\035%\320l\032\366\211\276\021^\305\225\314?\225\233\247\211(\2515\326\350\335\347\363\023\025\371\010\366\005\330\324\200or\341\323M\276\027\030\262\2105\"\341\031\017a\001\360s2}\2009"
+        #   p: "\357\240\010\211\303\345p&\272\325\006\023\313\344\207\327^&8.\210\350\202\021\335I\266U?C\304\340\2036\361A\'9!w\356:\327\032\017\261\307\357\315X\341f\236\033\"\033V\351\300\'\324&\020:bi!\336\311\002\037t\377\267\377M\334n\3048\213^u\364(-\252\222kw}\217\"l\231-:\314tyn\020\267\0370Wu\251X\206\230Z\037:C\257k3\371\260\020\3245\004^\030b^\361\246Pd\20471\220\214\320\236\222\002\020\230\354tP\205&\346\341\'r\300Z\375\355\360\3431\036r\\k\343x9\251!B\222Y\311X\022\222\340\334w\257F\215-\007\206(}\300\373\361\353\272_"
+        #   q: "\271\320Q\355kj\267p\235\036\222\3329\213v\246\376\005C\325\245bS\2671\211\314\227t\343\' h<b\216\361&\306\026|\254;\246\"\225~\340j\020P\204\276\010GJ,\313\314J$3\210\034\023l\031 \306l\315:e\360=dq\363e\3155\026S\006\322\332xK-\266$K\331\214\240a.=\023\n9\273\310a\007A}\017\312\340bI\t\366\002W\307\304\263p;\025\016\316\317\340\217]\035\355t\326\032\020\253\274\320p\376)\337\304-\327\032>~\037\350\223\232\356\253\t\211(\004\n,\243i\226\311\013\202\321m\021\275T-\353\305:\377\027\344d\252\025\272\201R`-e\340\003\333E\202\031"
+        #   dp: "6\032N\3633\374\300\266\255\346]\t\3516?\t|cj\271\357\005x \252r\363-\n,\265\355\357\022_\244SC\315\357\263%+\335\341\177Eb:]\377\376XbM\360\030N\214\2511x*\237\324\nb\260Wx\030<%d\230\313.\242\377\300\270\216\352X\347\035pn\300\276\350\345\201c=\256}\241\036\0070\267\253p\352LD\357\250b3Q\335\315\034\355%jE\314\337\357\331\314\223\2463\307\266\r\324\006\355\347<T\243\306\222\226\352\312n\214\254\262em\344!\037\364\003\304G\243\n\211qT\253\203|\204\216g\216mI\007\234(\036\257\212\344$C\r\233\003S\020h\325\t\227\242\n\374+G"
+        #   dq: "\240\363\222\003\325\307\332\235\361{J9=\037P\347\325\2419\341\026 \325\343\031F\003\262-\315\225V\274\363\221k\277\367\226\036d_\314K\\)\242}Ab`\\\233?\020\201o\314\030i r\202\263\317\037\023\245\020;\256\211\247C1\352\217\312\005\244{\027\233S\215\321/\341\356\366\226\372\034\267s4\272\230\254\245n\334\371\365\022\023\350\000\205\373W\'m9kN\035\374\204q\362\273\212\330\031\270\252v>61\276CU\255m  \300\010\005\214)(\351\322\267\355k\310o\253\314\373\265\222\263\355\034=px\354,\314d\033\326\001\351\250\335Q:\211<h\331\311z\210\322\374G\323\013+\300+oJ\001"
+        #   crt: "\203\275\021\224]l\307g+H\371\214W\233*\013\374\373w\202\344B\352\243\3431\'\315mz\250U-B\032\215;\000C\005\276\014b\237F1\200\20740\032p\342^\255r\336,\010Oj\205X\013\225K\336\032\n;{\372\001\373\214\2519w(\210\310\301\301=!\237!\202^\014\256\346\252\020(\312\235\334\212_\210\267?\r\274\267\366&\365\363\262\037k\244\305|3\223MK0\344\177\362\037\332\217\316I\361\\\212\365\276\013\017\306z\026\270\213S\003h\240\272\rI\377\353\317\246M\212\311\207\211|0O{\274\313\314H@\026\235L3A\360\363\211\311\255\324\317).\2043()<~\357\203\213\320\242\375"
+        # }
+        value: "\022\220\003\022\006\010\003\020\003\030 \032\200\003\255\355\244\342\\\347>\206\375|\321\023\372\211\210~\325\345\032\"\323\032\036f8D2u<.|\333\314\2177\246\3315\320S\305\331\343\325\206U\322\224\340\017$e\255\270K\275\013\221V\342/Z\254\367\342\207x!\033\204o\237\242\002\356I\372\304=Q\0133\037\376\311\322\254\322\277\2131\202^v\352Yh\334\010\310\315\347R\311t\345\003[\275N\226\315r\254\023\373\311<\333&\252\330\202\321&\260\245v3\027\361\206}\024\236\314\355\325*+\2376\374\221)\343*\367\316M[2\373fN\264\201XHG\'mD\317\250\226v\305,\364S\271`p\334\'\331G\\,\371\316\367\316\363t\"1]\034B\031\017S\364 \323\\\312X\027\305}\264\327N\020\035\213(\360\014\177\265\251\263\226\351\245\255\227\007{\250A\237\215\2316\030\017\024LPG|pVO\016\224\207\350\322F\030\366/\235K\244\352\345g\233Kt\305\321\207 &\031\332I:\353\313\201\021n{X\254\350g?\347d\202\363\354\'\377H\243d\363\352\320\360_SS\272D\024\022\\?G\233z\033h\266\264\375}V3;zj\200^\247\317\'\020\334M\361\223]p\340\000\277\202\251U\3269K\033\261O\263\300\234x\270\241\336f\220\036\262\263(\3145\206\'\006\333\357\\\276\324\034!\374\363\nCX\021\214\261\246\214>D\253\257u\365\211\325DqG\"\003\001\000\001\032\200\003@\302N\225\257\364\263\335\314-8\245\301/\213@z\200\242\242\033\312\312M\322\036fQ+V\275\313\223\0272\'\360\021.\337\2727\2362\247!vVS4\240\0371\267\000\220Ci\332\231=\373{\222\222hQL\266s\261\242\024\317\025\215;\016\355+\253I^\\\336%\220\216\251\235\370\221\253}\210UA\013HD\010\022\\\313@\270ro*\205\303\365|\000a2k#\273\017c&YR\351\236u\005\025\022\021\256;\r=\361\\\216ut\247\203\3607V\212\3149W9\213>\350\307\027\177\362\366\375vl\322-\000J\253\267ND&6\264;\231\332\007\006J!\"g\342\355v\303C\326\345]_C\246u\233\360_o\002\373h\\#\231\024\204\030\264\243\371\371\211\305\375\026\223\017\354\232\235W\007B\210\212L6\014O\373>Q\214\3628[W\311\235\214:\270.R\370\230]\002\216OG\211\026!a\211\255Q\355\037\207\033Up\276Z\334\207P\221\004\034/\322\274\366\3127\274\216\2634\035\343QI\260D\2277b\231\202\315\023}Hf\343W\354\207\267zL\272\016bkV\226\n\364<\365\006\247\201\366E\206,\362\026\307\366i\035%\320l\032\366\211\276\021^\305\225\314?\225\233\247\211(\2515\326\350\335\347\363\023\025\371\010\366\005\330\324\200or\341\323M\276\027\030\262\2105\"\341\031\017a\001\360s2}\2009\"\300\001\357\240\010\211\303\345p&\272\325\006\023\313\344\207\327^&8.\210\350\202\021\335I\266U?C\304\340\2036\361A\'9!w\356:\327\032\017\261\307\357\315X\341f\236\033\"\033V\351\300\'\324&\020:bi!\336\311\002\037t\377\267\377M\334n\3048\213^u\364(-\252\222kw}\217\"l\231-:\314tyn\020\267\0370Wu\251X\206\230Z\037:C\257k3\371\260\020\3245\004^\030b^\361\246Pd\20471\220\214\320\236\222\002\020\230\354tP\205&\346\341\'r\300Z\375\355\360\3431\036r\\k\343x9\251!B\222Y\311X\022\222\340\334w\257F\215-\007\206(}\300\373\361\353\272_*\300\001\271\320Q\355kj\267p\235\036\222\3329\213v\246\376\005C\325\245bS\2671\211\314\227t\343\' h<b\216\361&\306\026|\254;\246\"\225~\340j\020P\204\276\010GJ,\313\314J$3\210\034\023l\031 \306l\315:e\360=dq\363e\3155\026S\006\322\332xK-\266$K\331\214\240a.=\023\n9\273\310a\007A}\017\312\340bI\t\366\002W\307\304\263p;\025\016\316\317\340\217]\035\355t\326\032\020\253\274\320p\376)\337\304-\327\032>~\037\350\223\232\356\253\t\211(\004\n,\243i\226\311\013\202\321m\021\275T-\353\305:\377\027\344d\252\025\272\201R`-e\340\003\333E\202\0312\300\0016\032N\3633\374\300\266\255\346]\t\3516?\t|cj\271\357\005x \252r\363-\n,\265\355\357\022_\244SC\315\357\263%+\335\341\177Eb:]\377\376XbM\360\030N\214\2511x*\237\324\nb\260Wx\030<%d\230\313.\242\377\300\270\216\352X\347\035pn\300\276\350\345\201c=\256}\241\036\0070\267\253p\352LD\357\250b3Q\335\315\034\355%jE\314\337\357\331\314\223\2463\307\266\r\324\006\355\347<T\243\306\222\226\352\312n\214\254\262em\344!\037\364\003\304G\243\n\211qT\253\203|\204\216g\216mI\007\234(\036\257\212\344$C\r\233\003S\020h\325\t\227\242\n\374+G:\300\001\240\363\222\003\325\307\332\235\361{J9=\037P\347\325\2419\341\026 \325\343\031F\003\262-\315\225V\274\363\221k\277\367\226\036d_\314K\\)\242}Ab`\\\233?\020\201o\314\030i r\202\263\317\037\023\245\020;\256\211\247C1\352\217\312\005\244{\027\233S\215\321/\341\356\366\226\372\034\267s4\272\230\254\245n\334\371\365\022\023\350\000\205\373W\'m9kN\035\374\204q\362\273\212\330\031\270\252v>61\276CU\255m  \300\010\005\214)(\351\322\267\355k\310o\253\314\373\265\222\263\355\034=px\354,\314d\033\326\001\351\250\335Q:\211<h\331\311z\210\322\374G\323\013+\300+oJ\001B\300\001\203\275\021\224]l\307g+H\371\214W\233*\013\374\373w\202\344B\352\243\3431\'\315mz\250U-B\032\215;\000C\005\276\014b\237F1\200\20740\032p\342^\255r\336,\010Oj\205X\013\225K\336\032\n;{\372\001\373\214\2519w(\210\310\301\301=!\237!\202^\014\256\346\252\020(\312\235\334\212_\210\267?\r\274\267\366&\365\363\262\037k\244\305|3\223MK0\344\177\362\037\332\217\316I\361\\\212\365\276\013\017\306z\026\270\213S\003h\240\272\rI\377\353\317\246M\212\311\207\211|0O{\274\313\314H@\026\235L3A\360\363\211\311\255\324\317).\2043()<~\357\203\213\320\242\375"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1142101956
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS256
+      #   modulus_size_in_bits: 2048
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\020\"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS256
+        #     n: "\302\235{\273\'\\D\014\022\357\350\031\304\363\240\375\303-&Ge\030\372\035\\X\312\257\017Y\246\350\023\315\364\277\331\026\361(\2118\241\265\2619lV\314\365\306&w\227\210F\364\337\325\\\326k\271pRH\003\364\276\016\331\300A E\364U\336Z\361\365KKlIb\316\265%B\300\321\031\371\225t\250\317\263d\243\270\213{\245+3rAf\222\031\232e\200X\263\261%\033d\303wW\351vGR#\271\370iQI\211\371\211\335\333N.\201H\016~b\027!H*]\264\013\237S\357c\335\315\0227n\365\2448\255\"\211\344Qm\303\375\361\367\370\253)\242\252P\315\327\274\3252\367%\222\014n\233\323\373\205\242\'\177\303[\200T\177\210\331\361l\303\334\306E\222\000\300\354\336x*jQ\236\355\374\373\360\222v\234\330\026\266\361\245\223\277C&-q\036(\345\202\201\312\006\020\"T\242\335\316\277?A\335"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "K\361te\025\370\365<\330\356\361]\214\363\2351)F\247\027Y&$\003\355\023a\024k#\n\364\2515\307zM\326\234\010\273ss\367\030\317U\255\221\221\236\337G\357\303\350\034\212t\022$B)<\341~\344\274N\2460\346\313\0033\t#\260\377@G\3767\322k\330\270\335\013K?>\331\252\001^b\271\232A}y\370D)\261\360\003w\355\364\2714\374\302\326\332X\030(\326kHX\357\353\215\354\232J\376\3508\007\252:tai\232~LW;+\313\017\356\365\267\211\341\340:5\234G@\330az\006\304\222\0236_\230\0235\351\274q\245\225\307\357\210i\363Nx\231:f\370\354\306;\366P\037\273\265\023-m\035\037\2655\321d\236\201]\355@T\3760\263\002\273\254H\221+d\216\320A\nw?\3737NN\212\363\321a\277\234\022\352\337V\306%=\251\362\n\260\357IC\3107}!\320\020)"
+        #   p: "\372\345\346\311W\364\036\303\306\372_\275K*\201p\310!`%i\"XbB\233\356flK\276\026\010\217\222\332R^\325+<\214fW\331p\006\024\217\3654?\330s\304\234\247\027\377\321G|B\373!\264\235\234F\206\345\260#\n\273g\264\302\360\364H\247J\021C\t\270\371n\013wgP)C\023\001yN\3234.\332`\370(*:\\\370\311\345\367%\256]\010zY\026\3171\034<\024\357\300k"
+        #   q: "\306\222\227 \333\001\007\316\317\205\323\345\210\365v3\003h\250x\nsPRM\374\312\242\000\375&\245Y\320S\032\2640\303\240\030\000\365\334l={\'\275\350\0245\002\371=GL>\037\326\274\203o\306\306\331\213\342f\\\262\366r\005\356\320W\343\205uC\261\261\313\310p\217\220\275H\203\212C\023\372\300\272\212L&<^2\272\264V\\\305\233\267\207\227\260\360\277\032\353pDrU\302\330\205\321\267\370\327"
+        #   dp: "!u\257\205\202yG\361\014\004/\350j\261\314ya\3671\177K\272\014\327m\322\245\304{\004\316\340\331[lV9M\364W\303\307e\216\251\254\312\342\313 \004K\317n\000\366sB<\027\210\325\005\211\300@\372\023\361C\3445\3156\215/\223g\274\002\227\375\327^\335\365\331\371I\267\265\300\315k\363J\001\224O\362\322\256\274\350\331\257a\222N\373\316\321 X\242\034>\0312\360[\311L\336\246\\\"\351"
+        #   dq: "\020e\034\013?i\307^@\356\030\277\263\336(c/\335\335C\004\001\337X\334M\211\341tYrnf\212\367\301\225\255\r\t\323\321\210{)\210\255\371k\t\225Y\207$R\365\347\n\236\020Y\3024ab\033\246\034\014=\215\035!\356\361w\3149\341\323\373\301\331\037-.u\374\nT\311_\212\010ED\322b\006\305\205hu\241\312\257\362\272\362\017f\'\225\031[}\372\200kvQ4d20M\223C"
+        #   crt: "\224\034J^\243&\241\235\375\010\232\216\323;\3322yR\001\003W\231+x\005\220X\377\243\215\303*\t\200-\236\006\211C\'g\315\035P\272\3354C\003kQu\377<\031\235|\273\307\013\2408T`\275\037\315\'\324SH\261\014\"\006S}\010\263c\330L\273\242\366\265*7Y\006i\016\333\027\231\323\254_E\312H&\337\320\214*\3648oeg8M\273\253\230. \270[\263\233\356@\376/\203\277"
+        # }
+        value: "\022\212\002\020\001\032\200\002\302\235{\273\'\\D\014\022\357\350\031\304\363\240\375\303-&Ge\030\372\035\\X\312\257\017Y\246\350\023\315\364\277\331\026\361(\2118\241\265\2619lV\314\365\306&w\227\210F\364\337\325\\\326k\271pRH\003\364\276\016\331\300A E\364U\336Z\361\365KKlIb\316\265%B\300\321\031\371\225t\250\317\263d\243\270\213{\245+3rAf\222\031\232e\200X\263\261%\033d\303wW\351vGR#\271\370iQI\211\371\211\335\333N.\201H\016~b\027!H*]\264\013\237S\357c\335\315\0227n\365\2448\255\"\211\344Qm\303\375\361\367\370\253)\242\252P\315\327\274\3252\367%\222\014n\233\323\373\205\242\'\177\303[\200T\177\210\331\361l\303\334\306E\222\000\300\354\336x*jQ\236\355\374\373\360\222v\234\330\026\266\361\245\223\277C&-q\036(\345\202\201\312\006\020\"T\242\335\316\277?A\335\"\003\001\000\001\032\200\002K\361te\025\370\365<\330\356\361]\214\363\2351)F\247\027Y&$\003\355\023a\024k#\n\364\2515\307zM\326\234\010\273ss\367\030\317U\255\221\221\236\337G\357\303\350\034\212t\022$B)<\341~\344\274N\2460\346\313\0033\t#\260\377@G\3767\322k\330\270\335\013K?>\331\252\001^b\271\232A}y\370D)\261\360\003w\355\364\2714\374\302\326\332X\030(\326kHX\357\353\215\354\232J\376\3508\007\252:tai\232~LW;+\313\017\356\365\267\211\341\340:5\234G@\330az\006\304\222\0236_\230\0235\351\274q\245\225\307\357\210i\363Nx\231:f\370\354\306;\366P\037\273\265\023-m\035\037\2655\321d\236\201]\355@T\3760\263\002\273\254H\221+d\216\320A\nw?\3737NN\212\363\321a\277\234\022\352\337V\306%=\251\362\n\260\357IC\3107}!\320\020)\"\200\001\372\345\346\311W\364\036\303\306\372_\275K*\201p\310!`%i\"XbB\233\356flK\276\026\010\217\222\332R^\325+<\214fW\331p\006\024\217\3654?\330s\304\234\247\027\377\321G|B\373!\264\235\234F\206\345\260#\n\273g\264\302\360\364H\247J\021C\t\270\371n\013wgP)C\023\001yN\3234.\332`\370(*:\\\370\311\345\367%\256]\010zY\026\3171\034<\024\357\300k*\200\001\306\222\227 \333\001\007\316\317\205\323\345\210\365v3\003h\250x\nsPRM\374\312\242\000\375&\245Y\320S\032\2640\303\240\030\000\365\334l={\'\275\350\0245\002\371=GL>\037\326\274\203o\306\306\331\213\342f\\\262\366r\005\356\320W\343\205uC\261\261\313\310p\217\220\275H\203\212C\023\372\300\272\212L&<^2\272\264V\\\305\233\267\207\227\260\360\277\032\353pDrU\302\330\205\321\267\370\3272\200\001!u\257\205\202yG\361\014\004/\350j\261\314ya\3671\177K\272\014\327m\322\245\304{\004\316\340\331[lV9M\364W\303\307e\216\251\254\312\342\313 \004K\317n\000\366sB<\027\210\325\005\211\300@\372\023\361C\3445\3156\215/\223g\274\002\227\375\327^\335\365\331\371I\267\265\300\315k\363J\001\224O\362\322\256\274\350\331\257a\222N\373\316\321 X\242\034>\0312\360[\311L\336\246\\\"\351:\200\001\020e\034\013?i\307^@\356\030\277\263\336(c/\335\335C\004\001\337X\334M\211\341tYrnf\212\367\301\225\255\r\t\323\321\210{)\210\255\371k\t\225Y\207$R\365\347\n\236\020Y\3024ab\033\246\034\014=\215\035!\356\361w\3149\341\323\373\301\331\037-.u\374\nT\311_\212\010ED\322b\006\305\205hu\241\312\257\362\272\362\017f\'\225\031[}\372\200kvQ4d20M\223CB\200\001\224\034J^\243&\241\235\375\010\232\216\323;\3322yR\001\003W\231+x\005\220X\377\243\215\303*\t\200-\236\006\211C\'g\315\035P\272\3354C\003kQu\377<\031\235|\273\307\013\2408T`\275\037\315\'\324SH\261\014\"\006S}\010\263c\330L\273\242\366\265*7Y\006i\016\333\027\231\323\254_E\312H&\337\320\214*\3648oeg8M\273\253\230. \270[\263\233\356@\376/\203\277"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1430391526
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS256
+      #   modulus_size_in_bits: 2048
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\020\"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS256
+        #     n: "\315\357\235`G\2443\211\355\243\312\366Io\0339\304\351\024\210?\003\233D\222[\211\334\2746J~a\312`x\332\264\343\356\2210s\324`\356JGA.\007\247\241q9;\025\266\202-\321%\3059\002X\335\225~\032\253\341hDZ\'\340kS\243\242\245mV\020v\325eTE\243&R$XPc\004\3712\200\360\244\351t\211BCR\233G\357u\232\277\257fUA\375\022A{r\275\234\t\271\374\321\3215\310\374\013\374\201W\273\242z0\332<\220\361\253\271\272\316\321BM\275\345\027\261<zD\312\377JGl\223\2014\005\r%,\272B\271\231\2711\375\333Q\226/\233=\016\005\010\276 \216)b\352\331(h\020\226\003\310\324\262\0005\301\210\256(C\003;z\2268N\351m\260\t\327\314K\224\226q\232S\211\231\0041\244uR\343q\223$\230\301_nlG$\013P\033\021\rF\336>\301\321"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "L\337uO\313np\016\274\266\267\274\330\014\261\215\017\0042\265\351O\337U\351\214b\032\023\347\360F\335\032>\210\336\233G\272vX/\344\307\212,\020\r4\261\206\321\255\002@\034\n\324\355euC\371\323\366x@\245\242/\'%T\336\222\244#lEY!\207~\351\006s\0020C\016s\357\225\301\241\300T\312\361\037P\265>\260Q\314zu\013\241\376\261W\342\326\006\315,\244\333{\266)N\276@\242\221NW<\240\0271\271k\334\356\204)\275\360G\302\260\340\2409\024\353\033\236\361\344\274%\315\224L\202\323\334\252\255\243\262*\350:\261C\244\024\234\265\t\365G\214 \376CdYy\255\330\"8\363\204UwS\335I9]\354\036P\020\222F\207\\\246B\220\272\207S\377\255Xn\t\240\316\006\246\204\t\256\240\266h;\257\020\366$\010\274\325\"\213{\032Xm^\016U\236RV\004\351\304\355\365\277\201"
+        #   p: "\354\212\273\234\324\n\t\253\376\330\335(w+u\300\347\216H\'\235\267\313\003[\331N;pX4v\363\257\243\031.k\373\273\212\035\335G/\237@\302Y2\362\2528\357`\325Lp\206i\242\237\001t\3411\272;t\226\026\347Ya\301\374\214\315\336\342\346\341\\\364#\352j\037\345\245\230\257\366\362\350\253\257\233\317\366\346+\322\307\014i\236s\317\257\221[5F\372\301\266\301]cO\342\347O\271wdQ"
+        #   q: "\336\340\\N4\335\017\263\322 )\244\036\037h\033[\307$p0\365\324X\237\266\001]\273\331\311LW\344\253\267mb\365\377|\267\303$pn\344\020\020[\363\221\310\002X\212^\"\206M\347A\262\350P\341\t\362\327r\270P\213\235\372\211\262d\372\332\325\246\2515E\205\035u\342\306\303\216_\213\301+\255\242\000\203\366e1\006\353\305\265\314:>X\277\031\323\347\275q\362\300\314\222A\242s\304]\245\201"
+        #   dp: "\314\254\356\3221\027\316n\251A\365|\002A\364\316J\216\357\030x|\204\212\017{\345\031\301\210Q=lv\331\345\374)+\325\207\031xS\024}\364\2076\257\303\022O\331\262,^\314/BP\230\315\245\331\0062c\'\352\207?\0004z\252\221\033\302u\013\332\215\230#K\2770\202\201\333\260\307:@\341\356> \241?aD-@,T\023Y\356M\024b\361\\!*\205\376\314\344_.O\351\347\311A"
+        #   dq: "P\376\'\215$\030\321\203\377p@\261\253`\256l\202\305\263\2601\232\24480\221\353\235,\216\320`5\360\233\226d\341\371\223\206\267 \3535\020\227@_1\213*\007\r\023/\365U\206\210\370\351\037\"\335\201j$\035\267J\323\272i\006\212\323\221n\267\347\274P\361R\3724\342\212.\301\022\024\026\255t9\375\rB\347_\216\231}D\263\364+\226L\231\345\300\344\016,B\223]\304(\245\350|\013\254\001"
+        #   crt: "\024\350z-\231t\212h\323\007\300\265\001\307\010\344\232\314N\2478\000CU\220`^\274\310\207F]\230\244\267\346L\026\214\035\321\235q\037\204\321\314\202\236\347\236\002}\361\005\210`\320~\377\010\232\024zU\003\205\273\370\366\2467\017]\330(\212\310\337\300\217\326Z\372\324@\321[F\254\234g\203fH\225\310\265.,\226\233Ch\212V\304\267CN\027a:g\355\007\320\317;\351\254\017\013\003\247(V\025"
+        # }
+        value: "\022\212\002\020\001\032\200\002\315\357\235`G\2443\211\355\243\312\366Io\0339\304\351\024\210?\003\233D\222[\211\334\2746J~a\312`x\332\264\343\356\2210s\324`\356JGA.\007\247\241q9;\025\266\202-\321%\3059\002X\335\225~\032\253\341hDZ\'\340kS\243\242\245mV\020v\325eTE\243&R$XPc\004\3712\200\360\244\351t\211BCR\233G\357u\232\277\257fUA\375\022A{r\275\234\t\271\374\321\3215\310\374\013\374\201W\273\242z0\332<\220\361\253\271\272\316\321BM\275\345\027\261<zD\312\377JGl\223\2014\005\r%,\272B\271\231\2711\375\333Q\226/\233=\016\005\010\276 \216)b\352\331(h\020\226\003\310\324\262\0005\301\210\256(C\003;z\2268N\351m\260\t\327\314K\224\226q\232S\211\231\0041\244uR\343q\223$\230\301_nlG$\013P\033\021\rF\336>\301\321\"\003\001\000\001\032\377\001L\337uO\313np\016\274\266\267\274\330\014\261\215\017\0042\265\351O\337U\351\214b\032\023\347\360F\335\032>\210\336\233G\272vX/\344\307\212,\020\r4\261\206\321\255\002@\034\n\324\355euC\371\323\366x@\245\242/\'%T\336\222\244#lEY!\207~\351\006s\0020C\016s\357\225\301\241\300T\312\361\037P\265>\260Q\314zu\013\241\376\261W\342\326\006\315,\244\333{\266)N\276@\242\221NW<\240\0271\271k\334\356\204)\275\360G\302\260\340\2409\024\353\033\236\361\344\274%\315\224L\202\323\334\252\255\243\262*\350:\261C\244\024\234\265\t\365G\214 \376CdYy\255\330\"8\363\204UwS\335I9]\354\036P\020\222F\207\\\246B\220\272\207S\377\255Xn\t\240\316\006\246\204\t\256\240\266h;\257\020\366$\010\274\325\"\213{\032Xm^\016U\236RV\004\351\304\355\365\277\201\"\200\001\354\212\273\234\324\n\t\253\376\330\335(w+u\300\347\216H\'\235\267\313\003[\331N;pX4v\363\257\243\031.k\373\273\212\035\335G/\237@\302Y2\362\2528\357`\325Lp\206i\242\237\001t\3411\272;t\226\026\347Ya\301\374\214\315\336\342\346\341\\\364#\352j\037\345\245\230\257\366\362\350\253\257\233\317\366\346+\322\307\014i\236s\317\257\221[5F\372\301\266\301]cO\342\347O\271wdQ*\200\001\336\340\\N4\335\017\263\322 )\244\036\037h\033[\307$p0\365\324X\237\266\001]\273\331\311LW\344\253\267mb\365\377|\267\303$pn\344\020\020[\363\221\310\002X\212^\"\206M\347A\262\350P\341\t\362\327r\270P\213\235\372\211\262d\372\332\325\246\2515E\205\035u\342\306\303\216_\213\301+\255\242\000\203\366e1\006\353\305\265\314:>X\277\031\323\347\275q\362\300\314\222A\242s\304]\245\2012\200\001\314\254\356\3221\027\316n\251A\365|\002A\364\316J\216\357\030x|\204\212\017{\345\031\301\210Q=lv\331\345\374)+\325\207\031xS\024}\364\2076\257\303\022O\331\262,^\314/BP\230\315\245\331\0062c\'\352\207?\0004z\252\221\033\302u\013\332\215\230#K\2770\202\201\333\260\307:@\341\356> \241?aD-@,T\023Y\356M\024b\361\\!*\205\376\314\344_.O\351\347\311A:\200\001P\376\'\215$\030\321\203\377p@\261\253`\256l\202\305\263\2601\232\24480\221\353\235,\216\320`5\360\233\226d\341\371\223\206\267 \3535\020\227@_1\213*\007\r\023/\365U\206\210\370\351\037\"\335\201j$\035\267J\323\272i\006\212\323\221n\267\347\274P\361R\3724\342\212.\301\022\024\026\255t9\375\rB\347_\216\231}D\263\364+\226L\231\345\300\344\016,B\223]\304(\245\350|\013\254\001B\200\001\024\350z-\231t\212h\323\007\300\265\001\307\010\344\232\314N\2478\000CU\220`^\274\310\207F]\230\244\267\346L\026\214\035\321\235q\037\204\321\314\202\236\347\236\002}\361\005\210`\320~\377\010\232\024zU\003\205\273\370\366\2467\017]\330(\212\310\337\300\217\326Z\372\324@\321[F\254\234g\203fH\225\310\265.,\226\233Ch\212V\304\267CN\027a:g\355\007\320\317;\351\254\017\013\003\247(V\025"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1361155745
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS256
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\030\"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS256
+        #     n: "\315\021\033l\244L]\313Q\\\357\275h-U\035\201\233q\000B\326*\343\\d\310\337\357\303\242O\340I\205\241\221\3746\262\010\313\315\353i\336\317\220>K\264U\311\216\306 On\310.8\314\265\257j\027[n\037\360\026j\372\246\245a(\367\343_\024\251\333\"\2202\007\024P\241\203\2678k?\330u.\224\350\000\316\021\214\334\016\036\271\374\t\024p\357T\036I\211\334\006\207\264%\213\230%4F)6\203h6\001\323*\341\257\314\230\000Y0\223w[k\311XX\363Px\351\373\020^0^\332\246\323\323\223\211\2609\250k\363w\023p\325F\375\242\017\240\240\255\2727*`V\375]\256|gUtz\314\240\251\276\n\220\027\234UoY\006\234\315I\325\300\374\311\025\340\224\270\262\361\021\305\325\r\255\257\357\271\316\317\327\343\272p\350.&\251\222\370\211<\004G\014\205$\351@\245s$\177\265h9#t1\307f\304\3438\307\305\235\326\242\344\311\222i\361\222\022\373\357U\347*\372\371\334i\'h\3668\374\303\361#\2777\021\036D\242\221(\345\313\"}\020\304V\036\017E\3706\014\237,\264\325\357\217\321\r\333uo\023\240\310\004\273\362}\217\274Q\205\327\003\225\231\275\220~(kI\022\336t\216u\253\300\374\307s\2041\215$\327AFR\343\253rq}\234\245s\353\230G\200\344\217\357\361\016\213\260\323\252\005"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\031\304\235\266\026\245)J\021\252\031\316\252\247\020\366\316\266Ot\361q\031\0108\205t\352h%\371\3665\340L\216\356\031\266nnZ\334j\311\255\277\253\235E\257\263\234\240\016\014dT.\337\256\257\3215iv\345\221\311(\234K5h\343\007zN\352\224$4\023)\2620\211\3141\337\227(\260I\273\034B3\375ns\312\033\321uF\024H\004@\035\325\362t\354X\265\236\212!\206E\013\202\255\240\313\312/\243\356\2139u%W\313\257\353\372k:\261:m(\241\004\210*6\353\370\0143\177\325\030\203\2205`\364\347\367,\037\232E\010\200O\0145\311\366\347&$\332\252b!79\221\307\334\376F\221)\255\020\354\306a\302\022\0254\313\331\345\327\205\227G\360\027\t\0023\306(\253\247b\320\217\352\3134\207`\316\000j\032\266\304\0162\304{\375\306\026\246N\326+n\234\005d\t\240^\177\340\251\301\265\246X\242\262\244]\372-j\265Nv\t\321l1\334!E\262\223+\301\247\007\222\004I\314xg\351\244\235\264\313\035\277\257\270\215wh\342d,/*\014\tE\003\317\366M\354\360\030]&j/\355\216\236\035\303f\"\306\020_\215\201\3210\264{\231iH\354\210\213\333\216X\335\033\353\204\302bU2\024\324\275x~\244\236B\371\306\025\374\344\232\016\201\000\326~\206\\\224\214l\301\342h]H\270\326@\3507\367"
+        #   p: "\377\247\212\313\247|\341\313\022\016,\371\242\252\303B\203\214\004=\250\317/\2244\324\223\346\007\321\245Kd\034y\301\246\364N\037\367\232\275;\325L\1773=\342\331\303M\333(\330F\223\264\032\236\201\022\034\210)\025\374\351\\#p+\222\351\206\315\325\222\036!\253\0071\364\212\314T\037\300\344\005\317\210\241\360!)4;O\316\272\212\250\363\3526\362{ \202&r\230\003\252\334\260HBTw]!\327Sy\033\032d\256\224g/m\235\336\311\362\025\314yg\322\035\221X\036\0343\303=yl\247\352\035\213\020R\273,1\262\302\354r4\211\337\346%\024z\037\003\335C\275G{\267=D?\340\310\260\251\236s"
+        #   q: "\315X\017\265F\255% \262c\\>\361\260\263\276\'E\305\030C}B\277\016}\372\310\036U\247@@\2220\377QN!\022\271\352Z\024a\223\215:\223d-\342\037\2019\2605L_\356\3652*\332\366\317\366\237O\007\356\331\316\\\272\274r\3375\211M\312\212#\334\207\356\033\241\265\023\353X\341\232R\302>\351Y\314L\330\037\355Gy\354\376 K\321T\321o\365\314\027\201\257tk\212\274\200q\263\257b\370|\240\006P\024\334N0\232Zd\305\226p\216\002\203\203\245\321\036N\323\177\tj\207\210\301`\227\203*\3247)\317V\t9.38\233\234g\037\304\357zW\365iE%\273\333\270\210U?\247"
+        #   dp: "44\273t!\014\260s.\211\264\207y\277SFV\344\225\315\035\226\311\211\203LQ\263l\257T\010\3245\270kb3\213l\235\274|\236&M\213\300$\010\032\035\366\335%7\035\032vq\345\"HW\211:\222\241\345\351\211\327\023\036\332\017L#+\306yk\232y\212\ry\362G\325\264\372\266\311\231\000\027Uy\264\016\365\021D\325\201\221\267\214\306\265\371\275\3562\272\264\376\202\005\017~~-\207\206\230&\'\361\270\323\251\274t\203\366\364\022\333\033\266x\250I\320\375\3507\245\334\260&\362\217\314\256S@\035\2427T\254\005l&\252\237\255\364\t\344R\215)\2369e\346\326\035\036k%o\301\360Y\233\031\237"
+        #   dq: "V6!\252.\371$\274\\\210\340\302\300\200:\206\234@k\246k7\\\335\217\375\016\\\024xK\226\353E5s\0144\254(\013\214;\263\220\337\375\014Y\263V\23483\355\377o\361\027\331\340q\346\2225\313L4\310^\201FJ\240\235\371\336\224\236\n~q\211\233\322\313\363\t\000\324`\177\217\272p+L\310R\372S\216\363\262;\321\245{\375\231\233\233Pk\372\000\215fU\031O\334\222\313\337u]FF\\#\036\240g\340\3129\275\250 Il\267c\234\013\254!\212m\210\266\371\216\275jOj\370\035P\237NL\370\270\002\007\222)\302\236\330\246d|\333V\270`z+[\276\037@\206\330+\314\366\303"
+        #   crt: "\r\357C\211\227\225C*y@}\023\343%4\201\347D\'\302\212b_-\263DT\234\270J\354\337\024\301\324\3157\330oA\275 /\371\314\310H@7\243 \237\317\313\2355\001\260\327\374b\235\221\270\3503*\245\353\364j`R\2405\006\005\265\342\247\267 \2636C\213\374W>e\231\3373\347\355\303H\333\302\005\370\326\356\013\315+\220\301\351\236\301$\326\261|(\244Z8\350\212\273,\027\243k\275l%\244\276vaH\"\267<\343%\377;#\\\327S\302\224E\200\212j\214\033:3o\302\"\3042\031\224j\2166\013R\207\320\343xO\354t\226\266\016\244\253\2443s\364\023#\377\233\243\347\016\013\006"
+        # }
+        value: "\022\212\003\020\001\032\200\003\315\021\033l\244L]\313Q\\\357\275h-U\035\201\233q\000B\326*\343\\d\310\337\357\303\242O\340I\205\241\221\3746\262\010\313\315\353i\336\317\220>K\264U\311\216\306 On\310.8\314\265\257j\027[n\037\360\026j\372\246\245a(\367\343_\024\251\333\"\2202\007\024P\241\203\2678k?\330u.\224\350\000\316\021\214\334\016\036\271\374\t\024p\357T\036I\211\334\006\207\264%\213\230%4F)6\203h6\001\323*\341\257\314\230\000Y0\223w[k\311XX\363Px\351\373\020^0^\332\246\323\323\223\211\2609\250k\363w\023p\325F\375\242\017\240\240\255\2727*`V\375]\256|gUtz\314\240\251\276\n\220\027\234UoY\006\234\315I\325\300\374\311\025\340\224\270\262\361\021\305\325\r\255\257\357\271\316\317\327\343\272p\350.&\251\222\370\211<\004G\014\205$\351@\245s$\177\265h9#t1\307f\304\3438\307\305\235\326\242\344\311\222i\361\222\022\373\357U\347*\372\371\334i\'h\3668\374\303\361#\2777\021\036D\242\221(\345\313\"}\020\304V\036\017E\3706\014\237,\264\325\357\217\321\r\333uo\023\240\310\004\273\362}\217\274Q\205\327\003\225\231\275\220~(kI\022\336t\216u\253\300\374\307s\2041\215$\327AFR\343\253rq}\234\245s\353\230G\200\344\217\357\361\016\213\260\323\252\005\"\003\001\000\001\032\200\003\031\304\235\266\026\245)J\021\252\031\316\252\247\020\366\316\266Ot\361q\031\0108\205t\352h%\371\3665\340L\216\356\031\266nnZ\334j\311\255\277\253\235E\257\263\234\240\016\014dT.\337\256\257\3215iv\345\221\311(\234K5h\343\007zN\352\224$4\023)\2620\211\3141\337\227(\260I\273\034B3\375ns\312\033\321uF\024H\004@\035\325\362t\354X\265\236\212!\206E\013\202\255\240\313\312/\243\356\2139u%W\313\257\353\372k:\261:m(\241\004\210*6\353\370\0143\177\325\030\203\2205`\364\347\367,\037\232E\010\200O\0145\311\366\347&$\332\252b!79\221\307\334\376F\221)\255\020\354\306a\302\022\0254\313\331\345\327\205\227G\360\027\t\0023\306(\253\247b\320\217\352\3134\207`\316\000j\032\266\304\0162\304{\375\306\026\246N\326+n\234\005d\t\240^\177\340\251\301\265\246X\242\262\244]\372-j\265Nv\t\321l1\334!E\262\223+\301\247\007\222\004I\314xg\351\244\235\264\313\035\277\257\270\215wh\342d,/*\014\tE\003\317\366M\354\360\030]&j/\355\216\236\035\303f\"\306\020_\215\201\3210\264{\231iH\354\210\213\333\216X\335\033\353\204\302bU2\024\324\275x~\244\236B\371\306\025\374\344\232\016\201\000\326~\206\\\224\214l\301\342h]H\270\326@\3507\367\"\300\001\377\247\212\313\247|\341\313\022\016,\371\242\252\303B\203\214\004=\250\317/\2244\324\223\346\007\321\245Kd\034y\301\246\364N\037\367\232\275;\325L\1773=\342\331\303M\333(\330F\223\264\032\236\201\022\034\210)\025\374\351\\#p+\222\351\206\315\325\222\036!\253\0071\364\212\314T\037\300\344\005\317\210\241\360!)4;O\316\272\212\250\363\3526\362{ \202&r\230\003\252\334\260HBTw]!\327Sy\033\032d\256\224g/m\235\336\311\362\025\314yg\322\035\221X\036\0343\303=yl\247\352\035\213\020R\273,1\262\302\354r4\211\337\346%\024z\037\003\335C\275G{\267=D?\340\310\260\251\236s*\300\001\315X\017\265F\255% \262c\\>\361\260\263\276\'E\305\030C}B\277\016}\372\310\036U\247@@\2220\377QN!\022\271\352Z\024a\223\215:\223d-\342\037\2019\2605L_\356\3652*\332\366\317\366\237O\007\356\331\316\\\272\274r\3375\211M\312\212#\334\207\356\033\241\265\023\353X\341\232R\302>\351Y\314L\330\037\355Gy\354\376 K\321T\321o\365\314\027\201\257tk\212\274\200q\263\257b\370|\240\006P\024\334N0\232Zd\305\226p\216\002\203\203\245\321\036N\323\177\tj\207\210\301`\227\203*\3247)\317V\t9.38\233\234g\037\304\357zW\365iE%\273\333\270\210U?\2472\300\00144\273t!\014\260s.\211\264\207y\277SFV\344\225\315\035\226\311\211\203LQ\263l\257T\010\3245\270kb3\213l\235\274|\236&M\213\300$\010\032\035\366\335%7\035\032vq\345\"HW\211:\222\241\345\351\211\327\023\036\332\017L#+\306yk\232y\212\ry\362G\325\264\372\266\311\231\000\027Uy\264\016\365\021D\325\201\221\267\214\306\265\371\275\3562\272\264\376\202\005\017~~-\207\206\230&\'\361\270\323\251\274t\203\366\364\022\333\033\266x\250I\320\375\3507\245\334\260&\362\217\314\256S@\035\2427T\254\005l&\252\237\255\364\t\344R\215)\2369e\346\326\035\036k%o\301\360Y\233\031\237:\300\001V6!\252.\371$\274\\\210\340\302\300\200:\206\234@k\246k7\\\335\217\375\016\\\024xK\226\353E5s\0144\254(\013\214;\263\220\337\375\014Y\263V\23483\355\377o\361\027\331\340q\346\2225\313L4\310^\201FJ\240\235\371\336\224\236\n~q\211\233\322\313\363\t\000\324`\177\217\272p+L\310R\372S\216\363\262;\321\245{\375\231\233\233Pk\372\000\215fU\031O\334\222\313\337u]FF\\#\036\240g\340\3129\275\250 Il\267c\234\013\254!\212m\210\266\371\216\275jOj\370\035P\237NL\370\270\002\007\222)\302\236\330\246d|\333V\270`z+[\276\037@\206\330+\314\366\303B\300\001\r\357C\211\227\225C*y@}\023\343%4\201\347D\'\302\212b_-\263DT\234\270J\354\337\024\301\324\3157\330oA\275 /\371\314\310H@7\243 \237\317\313\2355\001\260\327\374b\235\221\270\3503*\245\353\364j`R\2405\006\005\265\342\247\267 \2636C\213\374W>e\231\3373\347\355\303H\333\302\005\370\326\356\013\315+\220\301\351\236\301$\326\261|(\244Z8\350\212\273,\027\243k\275l%\244\276vaH\"\267<\343%\377;#\\\327S\302\224E\200\212j\214\033:3o\302\"\3042\031\224j\2166\013R\207\320\343xO\354t\226\266\016\244\253\2443s\364\023#\377\233\243\347\016\013\006"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 650573878
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS256
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\030\"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS256
+        #     n: "\267\322T(\000\201ohF\345\362Oy\242\0368\336\315\035!hg\007\230x\271\010>2\326_s\025i\004\031\247\027.\212x9\n\334m\372\037\233\314l\250\006\345\377h\007\265}\375\351\021\266\213F\305\221XS\314\207\030\362VR\243t@<\003\201\341^\265\241\030\020\220\225\312\nP\315\303\236\n@]\033\341;\223\306\247A\343-\3109s\214\2065P\221$\\\310G\355\254\212\235\250F\231S\255<v\207P*\341T\273\0144S\244 M\216\025\310\242h\000*\355\242\341\331\027fU\340\336G\341\031\274\0254\034\010<\014\366\371CLX\310\231\217Ite\373\336-\337?\375\370w\216\243X\352\252\343C\371\242\340{m\036\213\354Ob\243\233\005\364E<\317\013Z~\213\363\270_\266\343\351L\273\356|\rX\355[\375\312J\355\177\026\215\270\333\226\212\221\224fl\3623)J\206\001eC2A?\247z \301%\255\232\261\357\250lB\244I\323\373d\255\322\375v\002qZ\214hqn\316\237\255\370\250\215\314\024e7$\014\312\203#\257\010\025y\207\3413e\254\345b\253\342\207\2066\035\020%\01042\030\177\352\222\270\252\220@|b\023u\324L\242\356W\336l\343W\364I#\361\211\244[\2231%\032`\257\314\352\342(A\276N~=\323)\033\343\033:\314\016 _\337\036\"\305\304\272\215khp\336Q"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\004\021n.\033\363\301]\315\n\337\371\321\n\354\321\014\242<\261\255t1Cw1~\034\225F\260\002U\240\0278\203\235\204X\311h\026\304\321\324\302 \022.9\352<+\022\267\357\364\357\272y\233@\372\022\033\2019J\212\227[^\234q\324[\353\020\027N\337\350\024^zX^\316C\273\355\317\325V\253\375\273\205\030\252\320\257\311* L`\364\035\021\'\313&\211\'\332\001X\201\323\350\001\016=\354\257\205\351\350\034\366\263\371\252q\216\323f\213\225\367\315\344\334\256AI\261\354c\303\350\214\t\334\302\002\224y[\263\232\355\177c\247\324K\350(\277\202\301\376=-\213\247\277/#\364\2207\360ZjdH~;cenM\310\357MpS\324\375\017\372\324}~g\222\325\206\240]7\023^\200\225\256\211P\245\261jI\365\343\224\035G\010\355skn\030\3651<b\305\3742\365B\230\277\342\334*\000\022g\347\344\255\241\353*\336!Bf\030M\004\024Z\250\"\366\203\374V\230=\033\210C\341\200\366\266G\236E\215\331\321\017\300\2638\22635c\370r\275\330\317.\367EFVM U\031\"\2416r\361\276C\333\216a\202\033\373W\027<\300\277\251\2244\241g?&a\177R\323=UI\275\207\366\347j\245\002<\236\346\273\242#\302\250\0324\333E\242\300\344f6\250\343\374\245\306\215B5\201#d\246\361\371\2749"
+        #   p: "\372l\320bT\351>\353cz\341\236\324\222\021^\374&\235JE\215\203\016SC\343\206\316m\310 \327\241\231\374\225g>\200\244*\356\307\217>l\007\305\235\036\370\337/\230\'\211\250\261\221\257(\216Z\223\326\312\377I\242\217=\007;2\221v\304\270\211M\002\000l\376\030\210\374\332\210\232Nf\222;\241\317d!S\005r\001\221\351\004~\203\206\307AZ\306\334\241\372\212\021e\270e\323\225V\220\252D\355\360\251\032/\236r\020\301\270u\361\302\013]~\363[\364\232\026\031\017Vd\332\t\331,v\224\247\263\244o\362@\n\266\334\247\013\234c#\220\376\233{\236\205\356\226\r6\034\337\0359x\263:<[\331"
+        #   q: "\273\351\360!8\325\222\364\325\244\210\367\216e-\335\354>\36336Z*\355\362\021\330\2677\204\027\323\340\201\367\246\031\014\377_(\tQ$\375{\254S8\274\035\231gx:Sj\2607M\177:\345\353\360\237\320\322\301\274\234\276~g\201\267\2073\233H\023\213;<%\206y\337\005e\274mH\372\305=\322\010\235\306V\272 \361\324\271o\275\240#|\010v,\266\037N\307\302yl\360\341\277\177\006\177\366m\027\'\275\317;\211\3711;$\351\350\216\305\344\254\267\222W\014I\341\325\264\000\212\225\357\201\327H\2340\257$\0265\226I\300\233 \374\216\320\237\230\2011+\226\034\323\276\270\374\376\317\364i\247\3439"
+        #   dp: "9\010\273U\t\335\255t\322\315S\301f\314u\244Zm8\371\207i\271es\325Lf\221q3Y7\203\030\235\0141\335lR\367l6\213\013>sn\260`3r1V\307\372t/,\354\345\241\266Car\016>i\005\222r\251*\3252e\217Y\305\356$\306\256\000L\202\272\n\316\211\370\226izj\317u?\201G\373\317\215pv\227\342\004S\200\'6l\265\260\350Qps\320\202|\232\252\014\324\3340\307C\375$!h\335h\017M\220e\024\332\274 \263OA\031\333%P\373Y8\3450\352\030\341\344\225\031\274\326\364\336\036\250\331sj<r\220\357\003\265\330\372\335N*\no\342k\177a\353\321"
+        #   dq: "(j\330\313L~#m\245\306\r\357\261 \362\014\231\223\260\233\203EA\226\214Jjb}\376[a\247\030M;\334\207\177%\350f\230\235l\'\375\230\250\313MR\201~\312\214\306\225\300OZ\336@5\317;\220\236\304,\335I\333\306G\220A\364G\241\351\0229[fv\326\037\261\371\177\335\301\244\256g\355\225\014\332l,\374\211\007H\033P\235\361\003\354\215\376o\355\253\364+P\022\256|\337\303b\222\203^Sl\250\326\271\347\243A\203\207\035\010x,\370Rm\351{\357\230\264\212R}\013Q\324\202\010\334\005<3o\177\227g\013\325\361\207\326\312\240\364\262\310\226\332\331\036\272,{}o\021+p\316;\201"
+        #   crt: "\027Wiu\024\246u\242Z\211s\364L\016\3629\350\362\031z\005\210\320P\301\207W\003\324\025\340\222\001\375\3341\230P\257\032\315\n\\\233k\026c\312Nx\363\'\377Fn\026\220\206Bd\374n\351W\217\277\262\255/\035-\357\034\366\277v\277\373\261\201\266\331\324^\320W\231\200\267\270\201G\246B\301\260F<\352\242\301\226n\212\346C\235\'W\335e\030\373\244g\203_N\366W2SC\212\227=\371<5\271e\340\374\272\264\261]\276j\020\330\303`u\2457:\005\231~\215\324\353J\002\270\270uP\377\254\223\262\026\037\237\034jP\226\342\333=R\037QC\"\367\307\221D3b9\300\207\306}\2339Z"
+        # }
+        value: "\022\212\003\020\001\032\200\003\267\322T(\000\201ohF\345\362Oy\242\0368\336\315\035!hg\007\230x\271\010>2\326_s\025i\004\031\247\027.\212x9\n\334m\372\037\233\314l\250\006\345\377h\007\265}\375\351\021\266\213F\305\221XS\314\207\030\362VR\243t@<\003\201\341^\265\241\030\020\220\225\312\nP\315\303\236\n@]\033\341;\223\306\247A\343-\3109s\214\2065P\221$\\\310G\355\254\212\235\250F\231S\255<v\207P*\341T\273\0144S\244 M\216\025\310\242h\000*\355\242\341\331\027fU\340\336G\341\031\274\0254\034\010<\014\366\371CLX\310\231\217Ite\373\336-\337?\375\370w\216\243X\352\252\343C\371\242\340{m\036\213\354Ob\243\233\005\364E<\317\013Z~\213\363\270_\266\343\351L\273\356|\rX\355[\375\312J\355\177\026\215\270\333\226\212\221\224fl\3623)J\206\001eC2A?\247z \301%\255\232\261\357\250lB\244I\323\373d\255\322\375v\002qZ\214hqn\316\237\255\370\250\215\314\024e7$\014\312\203#\257\010\025y\207\3413e\254\345b\253\342\207\2066\035\020%\01042\030\177\352\222\270\252\220@|b\023u\324L\242\356W\336l\343W\364I#\361\211\244[\2231%\032`\257\314\352\342(A\276N~=\323)\033\343\033:\314\016 _\337\036\"\305\304\272\215khp\336Q\"\003\001\000\001\032\200\003\004\021n.\033\363\301]\315\n\337\371\321\n\354\321\014\242<\261\255t1Cw1~\034\225F\260\002U\240\0278\203\235\204X\311h\026\304\321\324\302 \022.9\352<+\022\267\357\364\357\272y\233@\372\022\033\2019J\212\227[^\234q\324[\353\020\027N\337\350\024^zX^\316C\273\355\317\325V\253\375\273\205\030\252\320\257\311* L`\364\035\021\'\313&\211\'\332\001X\201\323\350\001\016=\354\257\205\351\350\034\366\263\371\252q\216\323f\213\225\367\315\344\334\256AI\261\354c\303\350\214\t\334\302\002\224y[\263\232\355\177c\247\324K\350(\277\202\301\376=-\213\247\277/#\364\2207\360ZjdH~;cenM\310\357MpS\324\375\017\372\324}~g\222\325\206\240]7\023^\200\225\256\211P\245\261jI\365\343\224\035G\010\355skn\030\3651<b\305\3742\365B\230\277\342\334*\000\022g\347\344\255\241\353*\336!Bf\030M\004\024Z\250\"\366\203\374V\230=\033\210C\341\200\366\266G\236E\215\331\321\017\300\2638\22635c\370r\275\330\317.\367EFVM U\031\"\2416r\361\276C\333\216a\202\033\373W\027<\300\277\251\2244\241g?&a\177R\323=UI\275\207\366\347j\245\002<\236\346\273\242#\302\250\0324\333E\242\300\344f6\250\343\374\245\306\215B5\201#d\246\361\371\2749\"\300\001\372l\320bT\351>\353cz\341\236\324\222\021^\374&\235JE\215\203\016SC\343\206\316m\310 \327\241\231\374\225g>\200\244*\356\307\217>l\007\305\235\036\370\337/\230\'\211\250\261\221\257(\216Z\223\326\312\377I\242\217=\007;2\221v\304\270\211M\002\000l\376\030\210\374\332\210\232Nf\222;\241\317d!S\005r\001\221\351\004~\203\206\307AZ\306\334\241\372\212\021e\270e\323\225V\220\252D\355\360\251\032/\236r\020\301\270u\361\302\013]~\363[\364\232\026\031\017Vd\332\t\331,v\224\247\263\244o\362@\n\266\334\247\013\234c#\220\376\233{\236\205\356\226\r6\034\337\0359x\263:<[\331*\300\001\273\351\360!8\325\222\364\325\244\210\367\216e-\335\354>\36336Z*\355\362\021\330\2677\204\027\323\340\201\367\246\031\014\377_(\tQ$\375{\254S8\274\035\231gx:Sj\2607M\177:\345\353\360\237\320\322\301\274\234\276~g\201\267\2073\233H\023\213;<%\206y\337\005e\274mH\372\305=\322\010\235\306V\272 \361\324\271o\275\240#|\010v,\266\037N\307\302yl\360\341\277\177\006\177\366m\027\'\275\317;\211\3711;$\351\350\216\305\344\254\267\222W\014I\341\325\264\000\212\225\357\201\327H\2340\257$\0265\226I\300\233 \374\216\320\237\230\2011+\226\034\323\276\270\374\376\317\364i\247\34392\300\0019\010\273U\t\335\255t\322\315S\301f\314u\244Zm8\371\207i\271es\325Lf\221q3Y7\203\030\235\0141\335lR\367l6\213\013>sn\260`3r1V\307\372t/,\354\345\241\266Car\016>i\005\222r\251*\3252e\217Y\305\356$\306\256\000L\202\272\n\316\211\370\226izj\317u?\201G\373\317\215pv\227\342\004S\200\'6l\265\260\350Qps\320\202|\232\252\014\324\3340\307C\375$!h\335h\017M\220e\024\332\274 \263OA\031\333%P\373Y8\3450\352\030\341\344\225\031\274\326\364\336\036\250\331sj<r\220\357\003\265\330\372\335N*\no\342k\177a\353\321:\300\001(j\330\313L~#m\245\306\r\357\261 \362\014\231\223\260\233\203EA\226\214Jjb}\376[a\247\030M;\334\207\177%\350f\230\235l\'\375\230\250\313MR\201~\312\214\306\225\300OZ\336@5\317;\220\236\304,\335I\333\306G\220A\364G\241\351\0229[fv\326\037\261\371\177\335\301\244\256g\355\225\014\332l,\374\211\007H\033P\235\361\003\354\215\376o\355\253\364+P\022\256|\337\303b\222\203^Sl\250\326\271\347\243A\203\207\035\010x,\370Rm\351{\357\230\264\212R}\013Q\324\202\010\334\005<3o\177\227g\013\325\361\207\326\312\240\364\262\310\226\332\331\036\272,{}o\021+p\316;\201B\300\001\027Wiu\024\246u\242Z\211s\364L\016\3629\350\362\031z\005\210\320P\301\207W\003\324\025\340\222\001\375\3341\230P\257\032\315\n\\\233k\026c\312Nx\363\'\377Fn\026\220\206Bd\374n\351W\217\277\262\255/\035-\357\034\366\277v\277\373\261\201\266\331\324^\320W\231\200\267\270\201G\246B\301\260F<\352\242\301\226n\212\346C\235\'W\335e\030\373\244g\203_N\366W2SC\212\227=\371<5\271e\340\374\272\264\261]\276j\020\330\303`u\2457:\005\231~\215\324\353J\002\270\270uP\377\254\223\262\026\037\237\034jP\226\342\333=R\037QC\"\367\307\221D3b9\300\207\306}\2339Z"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 2108123916
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS384
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\002\030\200\030\"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS384
+        #     n: "\272\372\335f\"|\271\242\222^\254\r6\026\334L\351\274\235\337\027\001\274\367\024\361p\207\275M\233\275\376\226\316V\207\375O\230\324\351\341\252\277f\273\247\254\254\335\226\322\302\261\311p\237ER\017\270^\223G\213k\227\316y\303\205\300\034\021\311DZ\031\214\\\031\215\213\360\303\344-\360\302\371E@M9w\376g\207U\361\236\342E\311\301\223\334\026\207-\240ZQ\321\242P\265\2716~\270bh\346Q\201\347\030\330\017.\017\312\317\332W\037u\npFp8\214hdU\250\t\352G\037V\223\"\236\013\211w\346\262\241\246\tD8\254\023\246\\C\213\"\373r\247\207JG\234\351Y[\273J\241\355\317\031-\237\373*\243\274\276\240\257$Kb\037b;\205|uq_\264\250]\030:\265\364\005w\323\037\221\330K\033\200}\236<>\303\003K\220\243\324\377DF\032\203\246$vU\256\253\214&Jv\220\251l\221\360\255\013ov\023O7\325\255\023\207\233\207\02651\214\250\302\240\225\037\307cO\362K\034UcZ\362\260\266Z\177\'>\327\002\353\213\016`<^\304\210\307K\201\026\360\251\374\220\301\021{t\225\302\377N\350)\263\372]m02\204\005\336#\302^\250\222r\231\206\376\004ku_\243\n\000\314\244\376\206\326\253\264\341\263\n\374\335&u\373\243]\214\366k0r\254\310\216\244\024O\001\'\2473>kk\342\273"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "IG\017\205{v\215\371\334\213%\272w\2745\322\036\270@\326\261Y\211\010\37482\365\277\206\256\306\243\003\317\021\262q\337\303\361\215GWwsw\217\232\350\224_\013r)\342\3344O\"}\356\257\201\340\304\017\002\213t\304\337\242\016^\032\026\243\210\347\255 \206\214\225j\357\375\341!H7\217\237\264*\006\225\344@\026UB0?\205\221\300\353CF\352T\373C\300\355\010\212\361\204p\026\201-\342;\232\251\355\342\345\026cQZ,R\263\267\307\220\223bV\004\203m\336Q$s\203 \226\3016W\'$\312P\257q6\250y\335VF\030Z\220(\301\212\211\205G\343\020\347\222\221\224\261\007\014\377\340\3123\377/\n\3731L\376\035\340\025=\347\356j\310gR\310\252\376\360\247I/\354\221\271\356\250\347?\206\013k\361\327G\347\374\273\364dvGgl\300$^.\346\230\355b\306\221/\3212s\323\026\230P\347/\026Rz9?\317\256\320\372\005\2473\320\001f\013+\2106@4\240\005t\323\324i\301\211\314\347l\206\234~\305\257(w\212!\314\275\312\355\255D\215\343UY\236\306\271\225\207\322\254\210\212`\203\351\2443\202\350{\217k*\251l\315\233\014\001\n7~\274p\324\305\231\255\317nC\021\315IXn\03701\273hh\265Ww\361\363\306\0339\033\255\361g\355\355l4\3375\021\205\227\371UC\207a"
+        #   p: "\374+\272j\275\375Y\031\202X\027C\205\352|jLtOp\336\324\242@\305\264\300\031\177\\\336q\326\355\022\271\3365\246/\373cq\220\2644\215\224I\367\377\0306\332\275\022\317z\235\364\014\177\235\r]\035\2229\264:\232G\035:;\246\255\242\3332\256\237\r\035\030\373\357E\241;\033\036\013P\365\211\351[tB\373%\022\315\313pQ+\202\007\274X\037*\333\357[\376&l@\003\346?\351\216\323\\z\266\2423\316\340\316\310.s\224\024\341[\332\346K)\2540#\206q:\21326*l\372h\n\270\257\376\304q\2630\004Z\247^c\337\372\002\371\232h\203y\342R#QJq\2450z \366\305"
+        #   q: "\275\321\267\322\005\005\224^r\344\004=\257==Wa\326\366\361I~\264\003\345#\217\222\257j\376\224\213>{\315\347O\376]\260\030\376\364\333X\257\212\350\035~(t\350\213\\\r&V\362o\2340\004\330\341@\"\272\346\347\264J\265\206\223\352L\367%\326\306\254Y\220\214\203\022\2363\206\320\347\004rC?\347\274\244-Z\320NjZ\325\3339\303\341P\317\026\2171\274\345\341\360\266%\020QwN\351\3221\026\037u\251\216\030\214\025-D \225\025\200M\333\255\r\365\357W\306n\353w\302\372g\350Pz\236\266j,\030x%t\346\367\177$\322\3757%\rd\305\026h\374\237\355\030\214Ae\220\013\013\177"
+        #   dp: "\276\255\005\371~\215\232\030\312\321wM\314m\341\003\263\251\343\003\322\312o\241\220\235;\000L\250\331^\265\010p7q\t\232\017\000\004R\263\373?\035\252\372?vyE\311\"\\\235\333\366\241\271\355\250\300\231\037*\031M\351^\014\325\321\331\227\020\006k\276\250\001\2065\361UV\270\214\320\227\221\2416\347c\017\273\342n_^\312\030(sV=\022\306l\251)p\013\373\262\320\273\026\361T\301~\244I&\022\360\2136\234\374\032J\341]\026q\353S\370S\315\375\031\226\244\223\310-\307\371\306\316\344G\241\024\230\276\313\375Ug/\371\347\340\360[T#\340\364\354\355\317\207R\333J\006\200\212L\342~\204jv\215"
+        #   dq: "\223\204\214\213\027\204\002\216tShHX\014\013\304\0011\202\260z!\221\335\005\022\341\335i\236\211C\000\004\013K5\335!\035\306af\242\374d\253\350\024\003\367\253\221O\270\036M\311\342\000\177d\256\234\325\300\030\213\345\034\326-sr\'\250\204\212S\370\2005\336s\254\266\306\226\215\204\025\335\373\353\244\261F\324\335\361\351\235\216\r\242\361\375\tf\227(`\030\320\273p8\021\237\311\346\204_n0~\217\220\336\275\006\255\366\233\213\3202\024\377\301\"\233\227\036\352\376N\2776\306\314\\\037z\357x\223\220$\033g\204\364vX}\346\3767\021Y*Q\036\240\340\226\314\035\257\020\340c\231k^t\344mQh\031"
+        #   crt: "\324R\330\274\313t\3167J\364.jr!\345Z\361\tY\253\276\315&\320w_\321\"\203\206\r\272\247O\026\356\370\322:?|\226s\002&\375QA\325H\244TN\320w<9\006\030Zo\227;\211\370P\371\362\2747I\301J\335\236@\026@ct\222\374\324[\266\2126\274f\235\236\017\205w\230\315\316\251\334\002\201C2\204\344*6\r\232\1775\256\313\374\317?\235\254\022\345\215K(\314l\373^n>\311\304?\334}y\342\262W5\331\"\312\021-\357\330\n\036k\017\271\205\222\001W\240r\211O\305J\036\017\366\377\376ec\377RF\tV\315\356\321\356w\314\256\336\322\030qQ\302\233\256S\026\345Q"
+        # }
+        value: "\022\212\003\020\002\032\200\003\272\372\335f\"|\271\242\222^\254\r6\026\334L\351\274\235\337\027\001\274\367\024\361p\207\275M\233\275\376\226\316V\207\375O\230\324\351\341\252\277f\273\247\254\254\335\226\322\302\261\311p\237ER\017\270^\223G\213k\227\316y\303\205\300\034\021\311DZ\031\214\\\031\215\213\360\303\344-\360\302\371E@M9w\376g\207U\361\236\342E\311\301\223\334\026\207-\240ZQ\321\242P\265\2716~\270bh\346Q\201\347\030\330\017.\017\312\317\332W\037u\npFp8\214hdU\250\t\352G\037V\223\"\236\013\211w\346\262\241\246\tD8\254\023\246\\C\213\"\373r\247\207JG\234\351Y[\273J\241\355\317\031-\237\373*\243\274\276\240\257$Kb\037b;\205|uq_\264\250]\030:\265\364\005w\323\037\221\330K\033\200}\236<>\303\003K\220\243\324\377DF\032\203\246$vU\256\253\214&Jv\220\251l\221\360\255\013ov\023O7\325\255\023\207\233\207\02651\214\250\302\240\225\037\307cO\362K\034UcZ\362\260\266Z\177\'>\327\002\353\213\016`<^\304\210\307K\201\026\360\251\374\220\301\021{t\225\302\377N\350)\263\372]m02\204\005\336#\302^\250\222r\231\206\376\004ku_\243\n\000\314\244\376\206\326\253\264\341\263\n\374\335&u\373\243]\214\366k0r\254\310\216\244\024O\001\'\2473>kk\342\273\"\003\001\000\001\032\200\003IG\017\205{v\215\371\334\213%\272w\2745\322\036\270@\326\261Y\211\010\37482\365\277\206\256\306\243\003\317\021\262q\337\303\361\215GWwsw\217\232\350\224_\013r)\342\3344O\"}\356\257\201\340\304\017\002\213t\304\337\242\016^\032\026\243\210\347\255 \206\214\225j\357\375\341!H7\217\237\264*\006\225\344@\026UB0?\205\221\300\353CF\352T\373C\300\355\010\212\361\204p\026\201-\342;\232\251\355\342\345\026cQZ,R\263\267\307\220\223bV\004\203m\336Q$s\203 \226\3016W\'$\312P\257q6\250y\335VF\030Z\220(\301\212\211\205G\343\020\347\222\221\224\261\007\014\377\340\3123\377/\n\3731L\376\035\340\025=\347\356j\310gR\310\252\376\360\247I/\354\221\271\356\250\347?\206\013k\361\327G\347\374\273\364dvGgl\300$^.\346\230\355b\306\221/\3212s\323\026\230P\347/\026Rz9?\317\256\320\372\005\2473\320\001f\013+\2106@4\240\005t\323\324i\301\211\314\347l\206\234~\305\257(w\212!\314\275\312\355\255D\215\343UY\236\306\271\225\207\322\254\210\212`\203\351\2443\202\350{\217k*\251l\315\233\014\001\n7~\274p\324\305\231\255\317nC\021\315IXn\03701\273hh\265Ww\361\363\306\0339\033\255\361g\355\355l4\3375\021\205\227\371UC\207a\"\300\001\374+\272j\275\375Y\031\202X\027C\205\352|jLtOp\336\324\242@\305\264\300\031\177\\\336q\326\355\022\271\3365\246/\373cq\220\2644\215\224I\367\377\0306\332\275\022\317z\235\364\014\177\235\r]\035\2229\264:\232G\035:;\246\255\242\3332\256\237\r\035\030\373\357E\241;\033\036\013P\365\211\351[tB\373%\022\315\313pQ+\202\007\274X\037*\333\357[\376&l@\003\346?\351\216\323\\z\266\2423\316\340\316\310.s\224\024\341[\332\346K)\2540#\206q:\21326*l\372h\n\270\257\376\304q\2630\004Z\247^c\337\372\002\371\232h\203y\342R#QJq\2450z \366\305*\300\001\275\321\267\322\005\005\224^r\344\004=\257==Wa\326\366\361I~\264\003\345#\217\222\257j\376\224\213>{\315\347O\376]\260\030\376\364\333X\257\212\350\035~(t\350\213\\\r&V\362o\2340\004\330\341@\"\272\346\347\264J\265\206\223\352L\367%\326\306\254Y\220\214\203\022\2363\206\320\347\004rC?\347\274\244-Z\320NjZ\325\3339\303\341P\317\026\2171\274\345\341\360\266%\020QwN\351\3221\026\037u\251\216\030\214\025-D \225\025\200M\333\255\r\365\357W\306n\353w\302\372g\350Pz\236\266j,\030x%t\346\367\177$\322\3757%\rd\305\026h\374\237\355\030\214Ae\220\013\013\1772\300\001\276\255\005\371~\215\232\030\312\321wM\314m\341\003\263\251\343\003\322\312o\241\220\235;\000L\250\331^\265\010p7q\t\232\017\000\004R\263\373?\035\252\372?vyE\311\"\\\235\333\366\241\271\355\250\300\231\037*\031M\351^\014\325\321\331\227\020\006k\276\250\001\2065\361UV\270\214\320\227\221\2416\347c\017\273\342n_^\312\030(sV=\022\306l\251)p\013\373\262\320\273\026\361T\301~\244I&\022\360\2136\234\374\032J\341]\026q\353S\370S\315\375\031\226\244\223\310-\307\371\306\316\344G\241\024\230\276\313\375Ug/\371\347\340\360[T#\340\364\354\355\317\207R\333J\006\200\212L\342~\204jv\215:\300\001\223\204\214\213\027\204\002\216tShHX\014\013\304\0011\202\260z!\221\335\005\022\341\335i\236\211C\000\004\013K5\335!\035\306af\242\374d\253\350\024\003\367\253\221O\270\036M\311\342\000\177d\256\234\325\300\030\213\345\034\326-sr\'\250\204\212S\370\2005\336s\254\266\306\226\215\204\025\335\373\353\244\261F\324\335\361\351\235\216\r\242\361\375\tf\227(`\030\320\273p8\021\237\311\346\204_n0~\217\220\336\275\006\255\366\233\213\3202\024\377\301\"\233\227\036\352\376N\2776\306\314\\\037z\357x\223\220$\033g\204\364vX}\346\3767\021Y*Q\036\240\340\226\314\035\257\020\340c\231k^t\344mQh\031B\300\001\324R\330\274\313t\3167J\364.jr!\345Z\361\tY\253\276\315&\320w_\321\"\203\206\r\272\247O\026\356\370\322:?|\226s\002&\375QA\325H\244TN\320w<9\006\030Zo\227;\211\370P\371\362\2747I\301J\335\236@\026@ct\222\374\324[\266\2126\274f\235\236\017\205w\230\315\316\251\334\002\201C2\204\344*6\r\232\1775\256\313\374\317?\235\254\022\345\215K(\314l\373^n>\311\304?\334}y\342\262W5\331\"\312\021-\357\330\n\036k\017\271\205\222\001W\240r\211O\305J\036\017\366\377\376ec\377RF\tV\315\356\321\356w\314\256\336\322\030qQ\302\233\256S\026\345Q"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1191860486
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS384
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\002\030\200\030\"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS384
+        #     n: "\310\000\263\224\023^\356K\273\352\261N5?\016JD\2248(\376\022\300\220\245J\364\275`\024A\377R \362\354E\243\271\256$\267\210\323\374/%\022\0023\3056\017\201\257\265\'\241[5;\351\236,\002\351\033W\030\327\331H{L\257\n\031\177g\351\003\037\006\375\277?\360\260\225\331,\030B\033\336\005\364t\027{`\266r\241Mn\2009\256\236U\326\007? o\3552\363w\232\365_\207\344\202\005w\222/\236\227)\234*\177)\350D\230go\211\343\372I{\037Vn\222\366\301\313\271@\240\024\013\017:9@\324o\007a\226\010\260j\036\233\331I\002c\244:\232g\003\237\262C\265\036\226\265\220m\227\200\263\363V\271\242S\201%}hL\002\010\344)\272\306\234\362\010)\337DCn\201\\\205a`\252\320\352\023\242\233\331_`\2356\035{\211BH\273\223q\202\221@\307d\\>>Rae\313\022\343X\263\342\220N\307\273\271\325\335P\364\033Q?\270=\033Q\334\274.\201\360\024ILz\327e-\356s@x\376;\252h\276\273\306\211\235\035\031L\347\250\212slU\212a\324\344\001is\003\364\326\017\240\030\263\205\'\222\222\310\260\276\271\322\3645\266\t__\263\350X\272Z\250\356\350Ht\005$\3213(\020\316\241V\256\202\305\000G\356\221\263\332\231\370\353\252\r\014\007KKo\366\211}\t\373\372\260\351"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\r\343\001\352X\016N\230\225j;\202\342d\022\203\204\304\372,E\315\345Y\313\021\241\\\315!\030I\255\316j\007\216\215\365\027\032\274\265DV\035\200\343\001\367\243\026cBh\3278\307\365\226Y\346\256\226\225\214\321p=L\356\3024\260\310\363\215|\200\351lzx\363a@\330\337\003G7\270\374x\032_\237\371\242\220\027f\346I(`G3oo\200\321g\241\347]\226\"\344E\226M\272\r\301\262<q\033\260\360\352C\327\233\032\034\023\013\226=\275\000s\230H\206\003\021\231\203\261\361\355I\260\326\034\220\323\004m~\300\233\357\357\203\224n\264\003\2249\317HZD\362\323\271\3320\317\254\031\377\344wyY)2K\325h\2263\t\276\230\272\360\325\rF\014\306\232\327?\375\255\3449&6&\351\307\251\022\306\243\246z\'\013\262\027\024\341\327\320\235\323\322\\\324\272\227\355\255\3643\273{Q1\356\030JR\241I\0306\365\302\221\311\302.\005\"\31771Xm\321\235\351\242c\364m\226\226\211\373x\227!e\260\016}#\310n\232\006-\346@\373\235\216\361\305\324\177\370fO\244*G?\202\177\342|\005\372\231\353\023j\000\276\037\321+\346\255o\354IjM\265a\001\215\023\3556\276\305\360\2270D&\206\373\006p`\361\373\251\024\037\306\363Q\304\343\017\205\235\006s\243*J\325\214\251\202%\325<\270\355\316\366\320\250w\335"
+        #   p: "\373r\230\300\303\324\212\237\347\033\211G\312\025s\303\344\276\230\223\203\244\372\200\371\340 \t]\314\3106\332e\367/\256\002\216N\346E\315\231j\267\000A\230>\257\276\235\347q\311\014\323\036qX\213\'\023\253\356\211cb\232h\0322\006\032wq\274\237\177\245\0310\312B\344\024\214W\214P\256iD9yz\216\177\247t\213yQO\345\276\223lD\3447\304\247\032\204\3531\0328p\350\375\n\265\233\203\351\212\247\320\030\267\\b\320>P\003\0318S\211T\330HJ\021\272\355y\025\272D cti\245p=-\225\351\264\340\210\206\010\342<W#b\371-\322\312\222\254V\331\030\305\274qf\346>\275S\315"
+        #   q: "\313\237\253RB\3162\032\235c\013\\\333})w<\241\2672\037G$\220Q\240\177\320\025\306\004\037;!;\016\265\367\327\216\247a|\247\216\270D\035\262\355\253jf\353\302R\303W\2072\026\371\361\013\023\201SK3\t[h2\207\271Y\262\017\312\305\271\032\250X\305j\304\322#\264\302j^\035\313d#\271k\261\023g\004\001\205%/\245\246\370\030\364\237R\024_\030\254WJ.\203\021\200))Q|\354\223u\351\346\305d\261u\242^>\030m\031\212+\302\013\241\037rN\226\271\345\211!\232\'\271D\326\nQ\277\0268\274\r\367\"\231\032\032\251|A\341\354\002\204,\233\273\213-I\361:\036Z\255\215"
+        #   dp: "\204\342\224=\323\200\240\336N\331\353\234\'\375\\W\333\254\252\237\323\003\276k\251\"\274k\226\311\341\311\263t\r\305\201\365\321\325z\266\313(X\020l\257=\321k9\226\343\270\353\242\276t)<\275\025\242\274\313l\212<\223\023\217<~\243@\032\270\257\246Z\317LL\250;7l\317\333\324\313\307\320\t{\334\2740T\2040-\246>NQ\200A\310\312\277zKA\245\233R0\344A/\260\203\027e\037\272\255\317p\027\217\327\340T\343\211\217\211\245\215\350W\021\222j\252\242\032\\R\222D\354\305\002\301\316o\331\265\034\264\r&X\240US\222\306\016j\200\373\251\231\353\027\3571\376x\211\325x\247\027\343 \305"
+        #   dq: "\207\035A\022I\260\007\0010\024K\262\347\025\353P\352\223\250;\003\0243\007\354\222\"\235\222\005\0361\027\014Z\342\315\016S#\236\237\212\226\250\226N\344\242\031!\030n\210\334\036\014\356\344\217\361=\022\2345\352\'\272\013x\227\005\322\031\313|\3401\250l\201\002VX\351N\212\241\241\375\314\317\305\371\341?f\221 \013\225\003S^*\332\\\376\177\207\273\017\1774x\276\rb6]\313\365>\304\233\013\206\275\355A\200\t\240\016\265]\'\340\200\306\225p\267t\276\312x\353\0061\324B!\302T\267NR3\226\240\254\242G\270\330\026\270\361\227,\236l\270\257\362\013\226\216\250\343\354\306\212x\227\313\020\264\231\230\365"
+        #   crt: "\206\336\350\2751\273\225JA~\311\214\017X\003\345\242GS\254\242g\354\230\013\007g\177\313\302n<\362?F<!\252\234\003N\024\327\030)\350\371\024\251\023\261c\035Q\246\230\264[a\327\301^F\307\2655I\377\333}\242\203\244\370\037\275\324/\272\'to\013\305\335\005?\211@\242\242/R\3242\3430w\302\321\023\025,\"e(Iau\014U\023\277\212\363\000\366\336\232q$Z\331\205$\021\352\225\027\211+\215\230;\377T\010\241\222\026\233\365\275J\322M\000\332\246\3231\226\375\312\207\223a\021\255o\273\231\272\2624\260>c\271\3260\342!(\230\261o?\202\r\273\355\253\252\353\370\002\305\217]vh"
+        # }
+        value: "\022\212\003\020\002\032\200\003\310\000\263\224\023^\356K\273\352\261N5?\016JD\2248(\376\022\300\220\245J\364\275`\024A\377R \362\354E\243\271\256$\267\210\323\374/%\022\0023\3056\017\201\257\265\'\241[5;\351\236,\002\351\033W\030\327\331H{L\257\n\031\177g\351\003\037\006\375\277?\360\260\225\331,\030B\033\336\005\364t\027{`\266r\241Mn\2009\256\236U\326\007? o\3552\363w\232\365_\207\344\202\005w\222/\236\227)\234*\177)\350D\230go\211\343\372I{\037Vn\222\366\301\313\271@\240\024\013\017:9@\324o\007a\226\010\260j\036\233\331I\002c\244:\232g\003\237\262C\265\036\226\265\220m\227\200\263\363V\271\242S\201%}hL\002\010\344)\272\306\234\362\010)\337DCn\201\\\205a`\252\320\352\023\242\233\331_`\2356\035{\211BH\273\223q\202\221@\307d\\>>Rae\313\022\343X\263\342\220N\307\273\271\325\335P\364\033Q?\270=\033Q\334\274.\201\360\024ILz\327e-\356s@x\376;\252h\276\273\306\211\235\035\031L\347\250\212slU\212a\324\344\001is\003\364\326\017\240\030\263\205\'\222\222\310\260\276\271\322\3645\266\t__\263\350X\272Z\250\356\350Ht\005$\3213(\020\316\241V\256\202\305\000G\356\221\263\332\231\370\353\252\r\014\007KKo\366\211}\t\373\372\260\351\"\003\001\000\001\032\200\003\r\343\001\352X\016N\230\225j;\202\342d\022\203\204\304\372,E\315\345Y\313\021\241\\\315!\030I\255\316j\007\216\215\365\027\032\274\265DV\035\200\343\001\367\243\026cBh\3278\307\365\226Y\346\256\226\225\214\321p=L\356\3024\260\310\363\215|\200\351lzx\363a@\330\337\003G7\270\374x\032_\237\371\242\220\027f\346I(`G3oo\200\321g\241\347]\226\"\344E\226M\272\r\301\262<q\033\260\360\352C\327\233\032\034\023\013\226=\275\000s\230H\206\003\021\231\203\261\361\355I\260\326\034\220\323\004m~\300\233\357\357\203\224n\264\003\2249\317HZD\362\323\271\3320\317\254\031\377\344wyY)2K\325h\2263\t\276\230\272\360\325\rF\014\306\232\327?\375\255\3449&6&\351\307\251\022\306\243\246z\'\013\262\027\024\341\327\320\235\323\322\\\324\272\227\355\255\3643\273{Q1\356\030JR\241I\0306\365\302\221\311\302.\005\"\31771Xm\321\235\351\242c\364m\226\226\211\373x\227!e\260\016}#\310n\232\006-\346@\373\235\216\361\305\324\177\370fO\244*G?\202\177\342|\005\372\231\353\023j\000\276\037\321+\346\255o\354IjM\265a\001\215\023\3556\276\305\360\2270D&\206\373\006p`\361\373\251\024\037\306\363Q\304\343\017\205\235\006s\243*J\325\214\251\202%\325<\270\355\316\366\320\250w\335\"\300\001\373r\230\300\303\324\212\237\347\033\211G\312\025s\303\344\276\230\223\203\244\372\200\371\340 \t]\314\3106\332e\367/\256\002\216N\346E\315\231j\267\000A\230>\257\276\235\347q\311\014\323\036qX\213\'\023\253\356\211cb\232h\0322\006\032wq\274\237\177\245\0310\312B\344\024\214W\214P\256iD9yz\216\177\247t\213yQO\345\276\223lD\3447\304\247\032\204\3531\0328p\350\375\n\265\233\203\351\212\247\320\030\267\\b\320>P\003\0318S\211T\330HJ\021\272\355y\025\272D cti\245p=-\225\351\264\340\210\206\010\342<W#b\371-\322\312\222\254V\331\030\305\274qf\346>\275S\315*\300\001\313\237\253RB\3162\032\235c\013\\\333})w<\241\2672\037G$\220Q\240\177\320\025\306\004\037;!;\016\265\367\327\216\247a|\247\216\270D\035\262\355\253jf\353\302R\303W\2072\026\371\361\013\023\201SK3\t[h2\207\271Y\262\017\312\305\271\032\250X\305j\304\322#\264\302j^\035\313d#\271k\261\023g\004\001\205%/\245\246\370\030\364\237R\024_\030\254WJ.\203\021\200))Q|\354\223u\351\346\305d\261u\242^>\030m\031\212+\302\013\241\037rN\226\271\345\211!\232\'\271D\326\nQ\277\0268\274\r\367\"\231\032\032\251|A\341\354\002\204,\233\273\213-I\361:\036Z\255\2152\300\001\204\342\224=\323\200\240\336N\331\353\234\'\375\\W\333\254\252\237\323\003\276k\251\"\274k\226\311\341\311\263t\r\305\201\365\321\325z\266\313(X\020l\257=\321k9\226\343\270\353\242\276t)<\275\025\242\274\313l\212<\223\023\217<~\243@\032\270\257\246Z\317LL\250;7l\317\333\324\313\307\320\t{\334\2740T\2040-\246>NQ\200A\310\312\277zKA\245\233R0\344A/\260\203\027e\037\272\255\317p\027\217\327\340T\343\211\217\211\245\215\350W\021\222j\252\242\032\\R\222D\354\305\002\301\316o\331\265\034\264\r&X\240US\222\306\016j\200\373\251\231\353\027\3571\376x\211\325x\247\027\343 \305:\300\001\207\035A\022I\260\007\0010\024K\262\347\025\353P\352\223\250;\003\0243\007\354\222\"\235\222\005\0361\027\014Z\342\315\016S#\236\237\212\226\250\226N\344\242\031!\030n\210\334\036\014\356\344\217\361=\022\2345\352\'\272\013x\227\005\322\031\313|\3401\250l\201\002VX\351N\212\241\241\375\314\317\305\371\341?f\221 \013\225\003S^*\332\\\376\177\207\273\017\1774x\276\rb6]\313\365>\304\233\013\206\275\355A\200\t\240\016\265]\'\340\200\306\225p\267t\276\312x\353\0061\324B!\302T\267NR3\226\240\254\242G\270\330\026\270\361\227,\236l\270\257\362\013\226\216\250\343\354\306\212x\227\313\020\264\231\230\365B\300\001\206\336\350\2751\273\225JA~\311\214\017X\003\345\242GS\254\242g\354\230\013\007g\177\313\302n<\362?F<!\252\234\003N\024\327\030)\350\371\024\251\023\261c\035Q\246\230\264[a\327\301^F\307\2655I\377\333}\242\203\244\370\037\275\324/\272\'to\013\305\335\005?\211@\242\242/R\3242\3430w\302\321\023\025,\"e(Iau\014U\023\277\212\363\000\366\336\232q$Z\331\205$\021\352\225\027\211+\215\230;\377T\010\241\222\026\233\365\275J\322M\000\332\246\3231\226\375\312\207\223a\021\255o\273\231\272\2624\260>c\271\3260\342!(\230\261o?\202\r\273\355\253\252\353\370\002\305\217]vh"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1186006926
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS512
+      #   modulus_size_in_bits: 4096
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\003\030\200 \"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS512
+        #     n: "\365\374\250\3220\252\262nj\032\311M\236\n\034\2153\027\364\317\232\006p,\021\377\363\341\246Km\343vM\351\006\037(\037#\376\257\230\356\302r\316\r\262\275\237v|iU\021\336\257$\243\177&\360>\031\030\217\374\317|\375\006,8\333y\300\304\343\345\274~\253!\376\330\244J\023yC\270\010\311\030\214]\275\322\n\317\004-_\'\344\371^gm\004\212\247d\336d\220\035\221\271\257[4\254f\344O^\0323\316\025\177\257\212\212mHH\226\354\245\234\275P\177R6\\\257\335\211.\347#:\036qO@s\321\371\323\355\217\361I\303`\212\220U\301~\007`\225v?u\360\360\035P\313\255\370T\024n\357r\252\350%\007a\343\325\245\355\203\330>q\226\\;\210\"\274\210\364\274\257Z\207\022\350b\3430\340\262\333\n5\246p\232U\353u\220\323\266\274\327\034\021\207r\250\367\267\365<D\274r\343q\350t\211\027\037C*\264\373\375t\357H\204\005#\331\357\023l\226\220\276\235\245|\276\206F\250c\352^\246+\205\351\377[\030\267\001E=P&\251!\013\'\353h8\372SC\341&\376\010\"\371\206\306S\231)\255\261F\204\000N\022G>x\033\337y\325\324\2455\205\030\365\315\354q]\371\264\264qM$e\200\350\324\263\373{k]\207\275\275\267\220\360\347\317n\261i4\n\342s](Z\027zn\363\216\360:\314\270\370\215?\301c\036\r\316\242/\274\025\265_\307S\247\364C9\037\337\310\035\372;\330)U\3234\023\013U1\360~\213`8\240_|\346W\213\007\263/\363\220\256VN+\031\345U\256+\320i\255\370j\034\340\331\3721\026bM\233o\030\t.]O\275R\243i6\316+\331\035U|\363\313)\221\251;+T\016\315r\013\314\370\270\302\201\210\244%7\231\330\007\305\315\356\244r>\247\230&\311w"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "y\002\202Jd\206\224\240\234\261\360\265\2569k2K M\336\222\366\367\327\266C#\306\255\370\rg\010\233\3536\207\275E\024\200\006\002E\362\202g\273ub\263?\246\224j\265\017\3079N\365\016B\301I\267;\2362\264Z\004\337\\Y\033\334(\377\263\324\266\273\231M\265e\234\313W\254h\254i\301\031\314C\305@k\366\262\033M\224\346\327\372)7B\276vp\261\'\364c\353\344\310\tq\206lj^\360\244\353\214\307\363\243C\325\336\351V\325\263\343\027~\262#\317ziI\242b\340r\003\365U\350\226\334c?sr:\215\210\254\004\242%\277\n \345\240-\346\255\361gE\270&7\014\256M.\200$kT\210?/\022\210\362a^&\256\n\211\366^\t+?k`4\205]\376:\324\242]\203FXC\244]\261f\337\236\r\006x\037w\007>~\323\340\013\261\354Mp\301\303\224L\340w\343agq\322w\026\372Vb\260\231\240s\014\346H\002\262\004e)T\3246%\321B\026\206t\372\034\313\353\316\177\214\002\217e\t\314FJo\247\231E\255-\331\216\246\037\013\0239\341i\276\215\225D\277\334\373\275\324T\342\327\226^A\360\223fdebC\241K\031\376\245\000(\210\314S\241t\241\266!\241\342\356W\272\000@\362\001zZ\252\227M\317\302\341pG\336\376n|\010\247e\321\r\205lj\270j\354\037 s\225\312\025s\034 \3513\327W\373\372\341\235<\336\014\333\275\223\272\'\277\031\241\367\014/\2566\235_,\355I\211\330\017>\003\2339FN\247B=1\256|\033j\334\320\271^\256P\302\2253\263\027\262]z~\346\005\222\257y\226\370V\345\270\270\364\312\266d%\321\360\310I\005\205\035\233ku_\022\210\350\353\301#\223\246\246\375f\240W\235B\312JM:\306+\266\202\205\365\006\250\304\204e-\201"
+        #   p: "\374\020$\301\347A\360dY\260;\354\311\334\230\024\336\361m9\371e#\244\037\324\230\322LP\310I\323\342\006\020\326!\241\312G\020\205\337\327\003t\212\236,l\305H\262jj\200S\220\253\300`\310\006\257L\006\266\303\260\271f\326\314P\217Q\367\230\277\333\341x\010\203\216\021\373S0\254\023^B3\311\335\231+\225\370\246\327\001+\252\013\336cfs\342\200\236\235\252\242R\212\021\303\235#6\314G7a\317*\263\205\334\235$=E\230]\346\320p\336\353,\201\036\322\024\243\337\031J\334\202\222\265\231\342\243 \213\265@}\335\017>\277\232?+\276a\311\200\352\262\354\310x x\217\250\270\220\004\003\007D`\013\334\247\276\256u\245\031A\302\300\330\245w\326>x_\3305\262\020S@N\350\301\004\304\365p\027\224\304\367|?yP\244]>\273\020R\300\334o^(&PR|#\367\200eV\260+.\250\313"
+        #   q: "\371\3248\221\334\\\\\240#\250\362#\034\215\367\206\220T\303\377\211<\333\346\037;6PM\023\305\006Rh\203L\275\236\202\213l\274\177\317h\271ib\026h,\031\226\205\231\234\n\375\340\345\207\003\277\305\275\224t,Rk\233\023\321\020\233\223\261\"n\026\357\232\276H\310\265\235\212.\002k`\324#\327a\366\367>\277\322\311\303ip\0037\003\030[\271\377,\214{;Y\243/\344\2015\250\005+M\005\241\240\007\n\273\223\3631a\016\257w#\010\267\006I\254\306\002\0011\242\351\250-\266\230\260\002\236;]6\2145\225\273cU\260\321\303gs\303\211f\346\342\310D\326ge%N\210\032 7\276h\026\022o\240\234\006O\031\270_\364\010\221\020\2577\"M\355\315\037OH\224\214r\026\303F\016\246c\233%<z\230\345?\351\216\000)\201M\014o\305\213~\275\372\364\326L\200\r\332=\275\245n\236PH\205"
+        #   dp: "\332\002pxQ7C\336\327\177\310\344\255W!&t\t&*\215\\\377\255\225f@c+dwF\373~\2636^\256*\014\024\330\342\241N\213\213\201\324\372\241}$\272C\266\'\225\237<\035\240\327\200\210\237]\3637`#\213!\337\007\022^\227\242\302\326?\265\234\317\203\322c\250y!k\203\014\tT\340W\371M\266\t?N\240\340kq\206Qw\213\201)/?\0054\024|\366\316sE\222\373I\230L!\'\262p\264mB\235n\002Q\177\031\020\205!1\360aM>\312\330JZ\\\210\335\216\342i\252\335\212\377\343\234\215\361\374\260AP\351R\024\201n\375 4\347IV]\022U\307!\341(\250\347\325z\234\217\034\277]\363q`+\201u\344\'&R\340\272\324\376T\206\\\311\243 F\252\277\211\232\253\256\214K\'o\032\343~N\242f\244\334p\013\326\311-\022-\265w:;\017\353!e\307\356o"
+        #   dq: "$\300t\205\370\235\331\276q \204fg\347\366\226\344\032\244.\256X\266\234\266\321v\037\000\007\275\177=\226Db\200\256\225\323\276\356G\373\022\023\334\215\342o\366\264\330\360\324u!$\321\365\322\023\332\035S\323G\367\3214%?\030\rRe\272\264.\357\316\325\273\322g<Xm\240q\030\254\310\350p\341\347{\335\316HK\010%5\2463l\220#\306K\344\230\321\364\034.\246w\376\016\017\246\233\005\034\243\367/\262\003\2003Q{N\266\277V\367\014=\257\216\354\036\025`\340\357p\2370\362kJ\'N\342Z\210\353\305\030(\367\315(>\217\264v\372z\231\322\230\325\217\230\354G=\263E\354\241f\260W6\345\027\002|\347\034\010\204\215\030\311\206c\320?\212\251E\370\252(*:2\372hE\241\030\027%\026\250;\374\314\024\307\335\354\346.0Py\014\335\205\212\233%su\023\215v\"\211\010\222*U\363%"
+        #   crt: "\205\350N4\326\371\353]aq\2032\317{G\264\032\220\251\251\234\027\377\351\214\216\205\243\347\277s\375\340N\016\370\333\273\2625\273\013#=>@=m\n\227v\0140w,x\370\215!g\366\243\"\240\212\306\222\205`\227\216\020\0020H\230\023\023C\222ud.e\313\337\272\201\025\220\224\343\333\311\307\351\315\337\314\243D\301\343\363\352w\200\347\374\222y-Z\333\302\223\350\022L\240T\r\335\347_\353\210\266k\342\023\014\345\300\270\226Vc\027\010\334\314\313I\223\262\322\356\026\351\026\246\031\021\236\322\243q\327/\343\027k\236\372\037\305}\271\340\372\036=\273{\031\230\234\215xG\273\025\2271i\343\226!\205o\332 \212K\223\036\203#\003\\V^9D\'9g\341\207\227\2379,\302\\+\037\014~\255\336[\274bjzt\020\307\t\017]\2460^N~\246P\003L\254p\261\303\306e\261\235\233:\320p\377U"
+        # }
+        value: "\022\212\004\020\003\032\200\004\365\374\250\3220\252\262nj\032\311M\236\n\034\2153\027\364\317\232\006p,\021\377\363\341\246Km\343vM\351\006\037(\037#\376\257\230\356\302r\316\r\262\275\237v|iU\021\336\257$\243\177&\360>\031\030\217\374\317|\375\006,8\333y\300\304\343\345\274~\253!\376\330\244J\023yC\270\010\311\030\214]\275\322\n\317\004-_\'\344\371^gm\004\212\247d\336d\220\035\221\271\257[4\254f\344O^\0323\316\025\177\257\212\212mHH\226\354\245\234\275P\177R6\\\257\335\211.\347#:\036qO@s\321\371\323\355\217\361I\303`\212\220U\301~\007`\225v?u\360\360\035P\313\255\370T\024n\357r\252\350%\007a\343\325\245\355\203\330>q\226\\;\210\"\274\210\364\274\257Z\207\022\350b\3430\340\262\333\n5\246p\232U\353u\220\323\266\274\327\034\021\207r\250\367\267\365<D\274r\343q\350t\211\027\037C*\264\373\375t\357H\204\005#\331\357\023l\226\220\276\235\245|\276\206F\250c\352^\246+\205\351\377[\030\267\001E=P&\251!\013\'\353h8\372SC\341&\376\010\"\371\206\306S\231)\255\261F\204\000N\022G>x\033\337y\325\324\2455\205\030\365\315\354q]\371\264\264qM$e\200\350\324\263\373{k]\207\275\275\267\220\360\347\317n\261i4\n\342s](Z\027zn\363\216\360:\314\270\370\215?\301c\036\r\316\242/\274\025\265_\307S\247\364C9\037\337\310\035\372;\330)U\3234\023\013U1\360~\213`8\240_|\346W\213\007\263/\363\220\256VN+\031\345U\256+\320i\255\370j\034\340\331\3721\026bM\233o\030\t.]O\275R\243i6\316+\331\035U|\363\313)\221\251;+T\016\315r\013\314\370\270\302\201\210\244%7\231\330\007\305\315\356\244r>\247\230&\311w\"\003\001\000\001\032\200\004y\002\202Jd\206\224\240\234\261\360\265\2569k2K M\336\222\366\367\327\266C#\306\255\370\rg\010\233\3536\207\275E\024\200\006\002E\362\202g\273ub\263?\246\224j\265\017\3079N\365\016B\301I\267;\2362\264Z\004\337\\Y\033\334(\377\263\324\266\273\231M\265e\234\313W\254h\254i\301\031\314C\305@k\366\262\033M\224\346\327\372)7B\276vp\261\'\364c\353\344\310\tq\206lj^\360\244\353\214\307\363\243C\325\336\351V\325\263\343\027~\262#\317ziI\242b\340r\003\365U\350\226\334c?sr:\215\210\254\004\242%\277\n \345\240-\346\255\361gE\270&7\014\256M.\200$kT\210?/\022\210\362a^&\256\n\211\366^\t+?k`4\205]\376:\324\242]\203FXC\244]\261f\337\236\r\006x\037w\007>~\323\340\013\261\354Mp\301\303\224L\340w\343agq\322w\026\372Vb\260\231\240s\014\346H\002\262\004e)T\3246%\321B\026\206t\372\034\313\353\316\177\214\002\217e\t\314FJo\247\231E\255-\331\216\246\037\013\0239\341i\276\215\225D\277\334\373\275\324T\342\327\226^A\360\223fdebC\241K\031\376\245\000(\210\314S\241t\241\266!\241\342\356W\272\000@\362\001zZ\252\227M\317\302\341pG\336\376n|\010\247e\321\r\205lj\270j\354\037 s\225\312\025s\034 \3513\327W\373\372\341\235<\336\014\333\275\223\272\'\277\031\241\367\014/\2566\235_,\355I\211\330\017>\003\2339FN\247B=1\256|\033j\334\320\271^\256P\302\2253\263\027\262]z~\346\005\222\257y\226\370V\345\270\270\364\312\266d%\321\360\310I\005\205\035\233ku_\022\210\350\353\301#\223\246\246\375f\240W\235B\312JM:\306+\266\202\205\365\006\250\304\204e-\201\"\200\002\374\020$\301\347A\360dY\260;\354\311\334\230\024\336\361m9\371e#\244\037\324\230\322LP\310I\323\342\006\020\326!\241\312G\020\205\337\327\003t\212\236,l\305H\262jj\200S\220\253\300`\310\006\257L\006\266\303\260\271f\326\314P\217Q\367\230\277\333\341x\010\203\216\021\373S0\254\023^B3\311\335\231+\225\370\246\327\001+\252\013\336cfs\342\200\236\235\252\242R\212\021\303\235#6\314G7a\317*\263\205\334\235$=E\230]\346\320p\336\353,\201\036\322\024\243\337\031J\334\202\222\265\231\342\243 \213\265@}\335\017>\277\232?+\276a\311\200\352\262\354\310x x\217\250\270\220\004\003\007D`\013\334\247\276\256u\245\031A\302\300\330\245w\326>x_\3305\262\020S@N\350\301\004\304\365p\027\224\304\367|?yP\244]>\273\020R\300\334o^(&PR|#\367\200eV\260+.\250\313*\200\002\371\3248\221\334\\\\\240#\250\362#\034\215\367\206\220T\303\377\211<\333\346\037;6PM\023\305\006Rh\203L\275\236\202\213l\274\177\317h\271ib\026h,\031\226\205\231\234\n\375\340\345\207\003\277\305\275\224t,Rk\233\023\321\020\233\223\261\"n\026\357\232\276H\310\265\235\212.\002k`\324#\327a\366\367>\277\322\311\303ip\0037\003\030[\271\377,\214{;Y\243/\344\2015\250\005+M\005\241\240\007\n\273\223\3631a\016\257w#\010\267\006I\254\306\002\0011\242\351\250-\266\230\260\002\236;]6\2145\225\273cU\260\321\303gs\303\211f\346\342\310D\326ge%N\210\032 7\276h\026\022o\240\234\006O\031\270_\364\010\221\020\2577\"M\355\315\037OH\224\214r\026\303F\016\246c\233%<z\230\345?\351\216\000)\201M\014o\305\213~\275\372\364\326L\200\r\332=\275\245n\236PH\2052\200\002\332\002pxQ7C\336\327\177\310\344\255W!&t\t&*\215\\\377\255\225f@c+dwF\373~\2636^\256*\014\024\330\342\241N\213\213\201\324\372\241}$\272C\266\'\225\237<\035\240\327\200\210\237]\3637`#\213!\337\007\022^\227\242\302\326?\265\234\317\203\322c\250y!k\203\014\tT\340W\371M\266\t?N\240\340kq\206Qw\213\201)/?\0054\024|\366\316sE\222\373I\230L!\'\262p\264mB\235n\002Q\177\031\020\205!1\360aM>\312\330JZ\\\210\335\216\342i\252\335\212\377\343\234\215\361\374\260AP\351R\024\201n\375 4\347IV]\022U\307!\341(\250\347\325z\234\217\034\277]\363q`+\201u\344\'&R\340\272\324\376T\206\\\311\243 F\252\277\211\232\253\256\214K\'o\032\343~N\242f\244\334p\013\326\311-\022-\265w:;\017\353!e\307\356o:\200\002$\300t\205\370\235\331\276q \204fg\347\366\226\344\032\244.\256X\266\234\266\321v\037\000\007\275\177=\226Db\200\256\225\323\276\356G\373\022\023\334\215\342o\366\264\330\360\324u!$\321\365\322\023\332\035S\323G\367\3214%?\030\rRe\272\264.\357\316\325\273\322g<Xm\240q\030\254\310\350p\341\347{\335\316HK\010%5\2463l\220#\306K\344\230\321\364\034.\246w\376\016\017\246\233\005\034\243\367/\262\003\2003Q{N\266\277V\367\014=\257\216\354\036\025`\340\357p\2370\362kJ\'N\342Z\210\353\305\030(\367\315(>\217\264v\372z\231\322\230\325\217\230\354G=\263E\354\241f\260W6\345\027\002|\347\034\010\204\215\030\311\206c\320?\212\251E\370\252(*:2\372hE\241\030\027%\026\250;\374\314\024\307\335\354\346.0Py\014\335\205\212\233%su\023\215v\"\211\010\222*U\363%B\200\002\205\350N4\326\371\353]aq\2032\317{G\264\032\220\251\251\234\027\377\351\214\216\205\243\347\277s\375\340N\016\370\333\273\2625\273\013#=>@=m\n\227v\0140w,x\370\215!g\366\243\"\240\212\306\222\205`\227\216\020\0020H\230\023\023C\222ud.e\313\337\272\201\025\220\224\343\333\311\307\351\315\337\314\243D\301\343\363\352w\200\347\374\222y-Z\333\302\223\350\022L\240T\r\335\347_\353\210\266k\342\023\014\345\300\270\226Vc\027\010\334\314\313I\223\262\322\356\026\351\026\246\031\021\236\322\243q\327/\343\027k\236\372\037\305}\271\340\372\036=\273{\031\230\234\215xG\273\025\2271i\343\226!\205o\332 \212K\223\036\203#\003\\V^9D\'9g\341\207\227\2379,\302\\+\037\014~\255\336[\274bjzt\020\307\t\017]\2460^N~\246P\003L\254p\261\303\306e\261\235\233:\320p\377U"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1780653566
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1KeyFormat] {
+      #   version: 0
+      #   algorithm: RS512
+      #   modulus_size_in_bits: 4096
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\003\030\200 \"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: RS512
+        #     n: "\305\206\231\254\317\273`A\322DhfX7\346z\324^A\373C\\V\347\211\354\325\243\250\354z\026C\250\330\263\335v\226\006\276\312\310\205\221\340\355\304i\221\220Y4t6\375\321\321\374CK\325V\2520\311\370\313l\362\266i\032\257\255d\020\226\322V\204X\203\210\276\307\263\304\022J!\250\325<DM\225\353\256\257\234g[|#\344\2066\313\325t\300\252\221\303\242\373gX\322\\&%\037\202\215\272{N+r;\334E!\177J\2174\023\304\277-K\031z1\366Y\217\276\025@I\360\034\236#\351\305\220\201\031\230\307\321\363\363\213\374,\335\207%\177\227\341x\014\334\320\250\032\275$\264\224\033K\227i\235\r\351\353\325-\275\346\340\335{\216\333\245L@\320P\315\034\234\325\354\335y\212\371\375\375\361.\271\343N2\226\374\316`\225\341\214J?\022\315A\004\302(|\214M\313\341\332\272X\275\273\221;c\263\007\362+\273g]N\210\214\202=7\033*p\005Y\262\237\302\207\217\233\344\236\324\304ARt\274\364\251\370\256\024\3304>Uy\352\257\246\201@\t\253,\245d\201\245dKO\307#\014W\365\373\2411\321@\244J\214\343\237\210\3668\n\0039\231\013\327\254\341\302\246M k\305\243\031\246{\322\324su\357r\255\364b\364GN\207\305![GZnT\246\235\271a\335\346\260\226\030\247\332{%\313\\\025`\217\327\016_\025-n\354\373\363.\220]\356n\255l\346E\326\324YR7i\301Ja\333\335`\223l4\253\271\221\326\000\031\n\246*\342\355|\267R\3359+_s\023b\313\340(\223\333\337\221Lx\302S\220\225\314\025\332\020\223\336]\373\2276\030>\244&\247\217\264\301\t\307\260*w\344\356\035\037^\342V\"\003I\344\266\337Vw\351\320o\211\346[\022\301%\247\017R<\"V\260X\367\256\231\361\307"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\022\215J{\013\372b\002\014\236V\3355\357\357zs3\311\322\260HH\236\232\217\264\236m\313\342\310\360\2137\351\347zY\326A\256\210\302\335~\256\026B\201d\316\006\272\206=\n\000\232F\337\317\321\027O\004s\352L\243G5QG\252\300\271;{\324\233\022\342\202l\303h\356Cv\324N}(\231\351\262>^\221)4\036\235\221\224\034tM\024\273\373)\337b\353\3432C\264\270\351\231\226\377`\274a\024ht9\214h\257 \020\325S\270\324\356w\307>\232\001}\367\317y\nD\230-0x<\220\232N[\207\027C\022\231\302]z\316\216\235@\342^V(\346\\^\371\365\243\237\202C\264/B\006\003`>\200\343A\201=6\272\326\250\260\354\207\212\3540\212\235\343Zq\250#\212\360\013\361\001>\270@\'$\267\352\377\364\206_An\225\031\370\3715\2574\nC\365\347\356K=\206\251\205\375\020\002?0B\002\317\376S\216\313 Z|Ck\370<\223\320-)\013\365\262\347v@=\307\373\206&HI\200\023@\017\374\340\332[s`\321\351O\354\037\035\002\233`f1\231\331\240\026\245}\243\347B\301Y`\267\362\315 \374:B\024k\010\003i\216\002\007\020&\206\203\373\316\251x\013Y@\221,n:\312\024nG\213\242\314/I\345].\215\366\023\234\215N\025]\356\227F\"\037\0331\242/\251=\323,2+\216\336\272\252Pq\256\021 +\340\301\r}\003X_\301\370\355\302\r\313\246\335\340\273\026\247>K\373-\232\303mlw\2604\305O\373\367\272\314\2147\345\006M\214h\001\037\003\343a\243\344\023\025WN\2029\216\3734\034A\266\n\310d\026\207\266\234[\327\201[_\302d\301\340\332J!A)\342\267\221i\314o`\256\351\355\362\240\350\354 D\367m\304\254\033!\005=#\255\001\341>0\361I\375q"
+        #   p: "\374_\324A\253M\227f7A;\357\366K\210\202\2479\024\226\340\375,mH^{G\256G(Q8\245*\225\013W-\2458j<c\273\231\355\003\321\377\334\010\302\3604>Ud\233\031\236\372\325\007\325\344\020\376\303\322\255\025\224\367\252\303o\317\024\nc\335%\316\035i\020\277\226\034\302\255\007B}\2673\327KNs`\203\343v\002\360H\023k\323\353\355\224\334\376\003\310\250\374m.\231D\277\342\374\262h\212#\2279\370M\230|Dj%\367\267f!}\234\345\033X\350\025\r\307\007\3764\261\250|\241\317+w4\233_\273\311\212\210\372%\323\235\032\346\225\275B\321SD\242v\276\367\017\207\322X0\345\366NF;\220\017v\037\233\364\026pe\262\324/\376\240\343\375?\320\022\337\301BAa\027\000n\233\346\204B\243`0\330C\222\346\310\204\313\317\006@\204\322\036;2F\203\\\371z\033%\352\034$;"
+        #   q: "\310]\r9X-\242\367\335\360\001\320?.\305\254\234*\232\3165\257\000\240\273\033\214\362\214\332\220\333\244\342}l\205J\312\337\333\344\001^\371\311\271_\330\327W\226\316N\"P\354\207\340\2276GO]AuJV\262\235\322]\352H\217v!6\311A~O\372\220%\317S\326\330\"\254a0f\342\221\346\000\335*\006\337\177\001\020\265\245\234\335\230>v\301\361H/\256c\236^\014\226z\237\005w\203\336\375\207S?\t\223f\2473\227D\316\220\213 \"\222>\341\376;=\366\2257\024\263\207b\235\322\354\226\311v\314o)\016\257\333\357\332\211\355\372b\'\234\024\354i\276\301Y\354dT\261\242\334\323\312\212<U@R\033J4W\222\177\221\302j\016=_A\275q\323<8\344\302\3574\200\345\224\014\244G\230@\211-\330\370\251]~\223:m\234O+\204p01\341!\276\350\236\3730\025M\226\255\013\345"
+        #   dp: "\013\276\301\370\024j\326\002\361\313]h\273\203\244d\3007\215\327\277\274\310\226\205i\260?\023\303H\003\017\251\320#\203\225\231\0209a\350Q\223\347\203\372K\211\246\311\331\230\2505\306o\2320E\017\323^;\374\0236h\022\243\357\1771O\207,\352\3549\327\217\3614xu\355K\235F\325`\214\027\362\025\326\231\314mh=3\246 U\336\254\334\n\206\240R<=T\255\004g\250\347\303\345j\'\253\376\252\246\346\326\3450\323wy\003?\203\331$\200*\206\240{\356\305\251\013\223\377B&\003\024\234\270\004\244\333)`\260\330\022J\201\004\253\177H\307bf`\030\323\350\n)\337\345\327\264\347v\227\221\230}\036\026\252i\346W\274\000yT\036\265\364\241\'\003m\000b\250y\236\300\246@7\224G\312\246\361d4i3\372\215\363\352\022\006\203\r\256\203O\240\340\337\233\316\323\215Y\267\242aoR\331t\230B\233\005"
+        #   dq: "\225J\300O\272$G\t\351\224\253\305?N\332{\202\341;\367\352\210;/\210;?\n{i`\006I\025y\337\"rX7\364\340/%Uy\271fql\003\027\233!\034\360\257M\217K\034f\205\3746\211\356\232*\374\355\202\214\317\004\026A)QL;\221\334a\013\313/\355%\236\260R\272}\356\324GL\335~3 \324\3016r\"\264\177h\261\"K\340<\251J\207o\271$\'\373\266\001\234\313\345\233\202\371\304>\236V\036\277\2226\362\263@\366\360\313\207\217\\\237G\3143\342\226\312.V\214\302]\006\375\226G=\214\232$\356\374\t\032<\370\023\252\357\341\247F#\234n$u\235\245\276tLZ\336%\226`\026\315\276\274\301\203\247\n4\222Q\211\3510R\270=l\341\367\222H*\026\340\371/r8\346y/\275\271\255\216\204\333\213X=\323\372)\262\304\345$E[\237\2444\321uBjM\233#\001"
+        #   crt: "o\342\301V\036>g\265\3503\366\214a\013\362\205\3117\360S\221\235\001\237\363\305\2162\344\3760\333\375\211\251\'\016\255\213AdS\235\353\216E7\3004\'\266\302\316\002^U\234d\256\372\030\202\261v\333\236\363Q\363\367~\030?\352\t\276$\322z+Y\326.\254\030Jt\373`\350,\261\260\377<U\327,\362\201\222hUxf\020\001\000\333\333\242vU)m\376\234-[\035o\375\354@y\322B\016\205\000:xB\376\300\203\3219\266x\317\207\013\272\367)\004\026#\022\223\347\367y&\221<\342mv\263\321\334\001T\32544.\263\006\243cj\3108\033CE=J\202\311\261\312\312\345@k!^\013E\212\345\202\255v\370\336\223E+FAY\241e\315k\376\3110!Mb\325\317\324\371\315\325\350y\310\256\363\022\342\277\r\271\263\n\255\242/\361\344\314\340\350H\245\206nU\025\\\326/T\372a\306\036"
+        # }
+        value: "\022\212\004\020\003\032\200\004\305\206\231\254\317\273`A\322DhfX7\346z\324^A\373C\\V\347\211\354\325\243\250\354z\026C\250\330\263\335v\226\006\276\312\310\205\221\340\355\304i\221\220Y4t6\375\321\321\374CK\325V\2520\311\370\313l\362\266i\032\257\255d\020\226\322V\204X\203\210\276\307\263\304\022J!\250\325<DM\225\353\256\257\234g[|#\344\2066\313\325t\300\252\221\303\242\373gX\322\\&%\037\202\215\272{N+r;\334E!\177J\2174\023\304\277-K\031z1\366Y\217\276\025@I\360\034\236#\351\305\220\201\031\230\307\321\363\363\213\374,\335\207%\177\227\341x\014\334\320\250\032\275$\264\224\033K\227i\235\r\351\353\325-\275\346\340\335{\216\333\245L@\320P\315\034\234\325\354\335y\212\371\375\375\361.\271\343N2\226\374\316`\225\341\214J?\022\315A\004\302(|\214M\313\341\332\272X\275\273\221;c\263\007\362+\273g]N\210\214\202=7\033*p\005Y\262\237\302\207\217\233\344\236\324\304ARt\274\364\251\370\256\024\3304>Uy\352\257\246\201@\t\253,\245d\201\245dKO\307#\014W\365\373\2411\321@\244J\214\343\237\210\3668\n\0039\231\013\327\254\341\302\246M k\305\243\031\246{\322\324su\357r\255\364b\364GN\207\305![GZnT\246\235\271a\335\346\260\226\030\247\332{%\313\\\025`\217\327\016_\025-n\354\373\363.\220]\356n\255l\346E\326\324YR7i\301Ja\333\335`\223l4\253\271\221\326\000\031\n\246*\342\355|\267R\3359+_s\023b\313\340(\223\333\337\221Lx\302S\220\225\314\025\332\020\223\336]\373\2276\030>\244&\247\217\264\301\t\307\260*w\344\356\035\037^\342V\"\003I\344\266\337Vw\351\320o\211\346[\022\301%\247\017R<\"V\260X\367\256\231\361\307\"\003\001\000\001\032\200\004\022\215J{\013\372b\002\014\236V\3355\357\357zs3\311\322\260HH\236\232\217\264\236m\313\342\310\360\2137\351\347zY\326A\256\210\302\335~\256\026B\201d\316\006\272\206=\n\000\232F\337\317\321\027O\004s\352L\243G5QG\252\300\271;{\324\233\022\342\202l\303h\356Cv\324N}(\231\351\262>^\221)4\036\235\221\224\034tM\024\273\373)\337b\353\3432C\264\270\351\231\226\377`\274a\024ht9\214h\257 \020\325S\270\324\356w\307>\232\001}\367\317y\nD\230-0x<\220\232N[\207\027C\022\231\302]z\316\216\235@\342^V(\346\\^\371\365\243\237\202C\264/B\006\003`>\200\343A\201=6\272\326\250\260\354\207\212\3540\212\235\343Zq\250#\212\360\013\361\001>\270@\'$\267\352\377\364\206_An\225\031\370\3715\2574\nC\365\347\356K=\206\251\205\375\020\002?0B\002\317\376S\216\313 Z|Ck\370<\223\320-)\013\365\262\347v@=\307\373\206&HI\200\023@\017\374\340\332[s`\321\351O\354\037\035\002\233`f1\231\331\240\026\245}\243\347B\301Y`\267\362\315 \374:B\024k\010\003i\216\002\007\020&\206\203\373\316\251x\013Y@\221,n:\312\024nG\213\242\314/I\345].\215\366\023\234\215N\025]\356\227F\"\037\0331\242/\251=\323,2+\216\336\272\252Pq\256\021 +\340\301\r}\003X_\301\370\355\302\r\313\246\335\340\273\026\247>K\373-\232\303mlw\2604\305O\373\367\272\314\2147\345\006M\214h\001\037\003\343a\243\344\023\025WN\2029\216\3734\034A\266\n\310d\026\207\266\234[\327\201[_\302d\301\340\332J!A)\342\267\221i\314o`\256\351\355\362\240\350\354 D\367m\304\254\033!\005=#\255\001\341>0\361I\375q\"\200\002\374_\324A\253M\227f7A;\357\366K\210\202\2479\024\226\340\375,mH^{G\256G(Q8\245*\225\013W-\2458j<c\273\231\355\003\321\377\334\010\302\3604>Ud\233\031\236\372\325\007\325\344\020\376\303\322\255\025\224\367\252\303o\317\024\nc\335%\316\035i\020\277\226\034\302\255\007B}\2673\327KNs`\203\343v\002\360H\023k\323\353\355\224\334\376\003\310\250\374m.\231D\277\342\374\262h\212#\2279\370M\230|Dj%\367\267f!}\234\345\033X\350\025\r\307\007\3764\261\250|\241\317+w4\233_\273\311\212\210\372%\323\235\032\346\225\275B\321SD\242v\276\367\017\207\322X0\345\366NF;\220\017v\037\233\364\026pe\262\324/\376\240\343\375?\320\022\337\301BAa\027\000n\233\346\204B\243`0\330C\222\346\310\204\313\317\006@\204\322\036;2F\203\\\371z\033%\352\034$;*\200\002\310]\r9X-\242\367\335\360\001\320?.\305\254\234*\232\3165\257\000\240\273\033\214\362\214\332\220\333\244\342}l\205J\312\337\333\344\001^\371\311\271_\330\327W\226\316N\"P\354\207\340\2276GO]AuJV\262\235\322]\352H\217v!6\311A~O\372\220%\317S\326\330\"\254a0f\342\221\346\000\335*\006\337\177\001\020\265\245\234\335\230>v\301\361H/\256c\236^\014\226z\237\005w\203\336\375\207S?\t\223f\2473\227D\316\220\213 \"\222>\341\376;=\366\2257\024\263\207b\235\322\354\226\311v\314o)\016\257\333\357\332\211\355\372b\'\234\024\354i\276\301Y\354dT\261\242\334\323\312\212<U@R\033J4W\222\177\221\302j\016=_A\275q\323<8\344\302\3574\200\345\224\014\244G\230@\211-\330\370\251]~\223:m\234O+\204p01\341!\276\350\236\3730\025M\226\255\013\3452\200\002\013\276\301\370\024j\326\002\361\313]h\273\203\244d\3007\215\327\277\274\310\226\205i\260?\023\303H\003\017\251\320#\203\225\231\0209a\350Q\223\347\203\372K\211\246\311\331\230\2505\306o\2320E\017\323^;\374\0236h\022\243\357\1771O\207,\352\3549\327\217\3614xu\355K\235F\325`\214\027\362\025\326\231\314mh=3\246 U\336\254\334\n\206\240R<=T\255\004g\250\347\303\345j\'\253\376\252\246\346\326\3450\323wy\003?\203\331$\200*\206\240{\356\305\251\013\223\377B&\003\024\234\270\004\244\333)`\260\330\022J\201\004\253\177H\307bf`\030\323\350\n)\337\345\327\264\347v\227\221\230}\036\026\252i\346W\274\000yT\036\265\364\241\'\003m\000b\250y\236\300\246@7\224G\312\246\361d4i3\372\215\363\352\022\006\203\r\256\203O\240\340\337\233\316\323\215Y\267\242aoR\331t\230B\233\005:\200\002\225J\300O\272$G\t\351\224\253\305?N\332{\202\341;\367\352\210;/\210;?\n{i`\006I\025y\337\"rX7\364\340/%Uy\271fql\003\027\233!\034\360\257M\217K\034f\205\3746\211\356\232*\374\355\202\214\317\004\026A)QL;\221\334a\013\313/\355%\236\260R\272}\356\324GL\335~3 \324\3016r\"\264\177h\261\"K\340<\251J\207o\271$\'\373\266\001\234\313\345\233\202\371\304>\236V\036\277\2226\362\263@\366\360\313\207\217\\\237G\3143\342\226\312.V\214\302]\006\375\226G=\214\232$\356\374\t\032<\370\023\252\357\341\247F#\234n$u\235\245\276tLZ\336%\226`\026\315\276\274\301\203\247\n4\222Q\211\3510R\270=l\341\367\222H*\026\340\371/r8\346y/\275\271\255\216\204\333\213X=\323\372)\262\304\345$E[\237\2444\321uBjM\233#\001B\200\002o\342\301V\036>g\265\3503\366\214a\013\362\205\3117\360S\221\235\001\237\363\305\2162\344\3760\333\375\211\251\'\016\255\213AdS\235\353\216E7\3004\'\266\302\316\002^U\234d\256\372\030\202\261v\333\236\363Q\363\367~\030?\352\t\276$\322z+Y\326.\254\030Jt\373`\350,\261\260\377<U\327,\362\201\222hUxf\020\001\000\333\333\242vU)m\376\234-[\035o\375\354@y\322B\016\205\000:xB\376\300\203\3219\266x\317\207\013\272\367)\004\026#\022\223\347\367y&\221<\342mv\263\321\334\001T\32544.\263\006\243cj\3108\033CE=J\202\311\261\312\312\345@k!^\013E\212\345\202\255v\370\336\223E+FAY\241e\315k\376\3110!Mb\325\317\324\371\315\325\350y\310\256\363\022\342\277\r\271\263\n\255\242/\361\344\314\340\350H\245\206nU\025\\\326/T\372a\306\036"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1893553212
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.RsaSsaPssKeyFormat] {
+      #   params {
+      #     sig_hash: SHA512
+      #     mgf1_hash: SHA512
+      #     salt_length: 64
+      #   }
+      #   modulus_size_in_bits: 4096
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\n\006\010\004\020\004\030@\020\200 \032\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.RsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     params {
+        #       sig_hash: SHA512
+        #       mgf1_hash: SHA512
+        #       salt_length: 64
+        #     }
+        #     n: "\263\000\264\317<\033L\222(\223\223\323:\\^\035~T\256\265\251O\"\346\275\307k\224<J\013\002\317\374-`A:R\001\345\266\200>\010\334\237\374=\260\207,\306\316N\\X|\021\001\343\314\215\377\334\3650\315F&\267}\254j\313U\250\252\036]\376\205\022<\271\3436\t\210\331\364W\335\320\031\257\277\206TT\365\000\024\355\023\305\205Y\371,i.\374\032-\241\321\016\001\007z\325e\323\026\'P\354\323\022\341\013_Gy\336$\177n\206\032|\325;5\"\276+\214[\352o_QQ\241f\334$\327\223\217z\320\327\332r\252\002>\244\335\330\250\357\004\223lc\210\363N\"\254\375\031\324w5\220\377\234N\311=\341\355S\371J\203\025}\217\376\217\362U\2112\346\361\342Q\306\025\245_u\030*s\220\245\304rz\276\003\372\323e\375\373=\370\315\262k#S\304\261.\205w\t(\214\342\317\006\024\225,F\005\365\326dP@<\321L\240M\355\220YF\264{\030=\366\335\256m+\3071\027\326\260\323n\276\236\367V\177~\351|7\323a\014\007\027\256\331\271\300\311\006ZC\277J\301\377~\267\207U\020\023\246x\002Q\240b:u\216Xf\316\251}K\313M\t\217\212X\335\200\3712\237N.\224Y\316\242\372\204\004\202\251)n`\237\220\264\306tH\240\273Q\026\372\362\'\375M>\357\243\344\257\250+]\361M`\356Yd;Ow\360\202\361\200\311\323C\267\242xn\255\3467\326CQ\311\347\353c\\\2123\302p+w#\322?\260\317\321\177i\302G\000\237I\236\200\002\264\0256\241P\2249T\001\3219(\366\003\336\207\303\350\004\245=\3554\034\220\221#\224\2237\246\0201l\000\345\366\241\200\212\210\2400\357\311\356\241\360\322\230I\3260-\311\351\315m\"D\34010B/\362\301Z\374\255\346\331\312\335\315q\017"
+        #     e: "\001\000\001"
+        #   }
+        #   d: "H\270\000\274/\256\347ly\017z\376T\2671\204\311\315\235,/\233\366\221\306\207=,\373Q\031(\013Vg\030\263f\375\351\307h\\\260\326\350\312\025\256\372\007\360(\323\247\001\334\360\211\370:\222\337\035\022\246\211,\333R\3378.\3233\307\230\275\203\230\253\315c\233G\351\006\n\331\215\261\266\000f\212\010\333\305\266|\315\013;u\024\223)\231c\240\247\032oP39\211\264&\266\343/\316-\362\322\006\016/\261{\273\"\371\346\210XKl\233\016\'\250xe\035\270#\370\341^[\340](\314h\375\005\372\215\367\273\355\033\303\242\321n\030\004\342\033\"\205\367U\354\262\202\360T\315\301y\021\005?h\203\177T\000\255\300v\007\364\025R/\356\213\023\364V\206\0349\240t1\317\277h\271bue\034l\"\206\240\321-\260\257\200\356d\363\n;\006\362\014\236n\347\353\037\374\332\351]r[\006\341\0359[#>\330\204\035\031NcGl\254\354\300\207F\247\240NT\327LA|\332\210\022\275\027\236@r^lk\271\306\'&\374A\021\324$\3059\246\360n\352\"@\317\232\357:\277\214\214\"\256G\257\272\355?\004\333x\370v\234\353\252\255\037\347X\261\311\224I\372b\370y\207%\331\t6\342\200v\304C\3043\347\277\271\260\344\001K\341\255\224\333\227\2121\322Z\303:E(e\231\313B&\005\234\222]Y\241\332%%\0223pN\216\342\350\211\237J\031ti\006\330\242v\360\030\326\325\333\007\225\026\227\014\355<\276m\374\337\363\022\215\017\317GX&2\236\035\245^\325r`\t\006<Ri\032)\321Q\202\035*\034\320\357\030\\\264\303\223$\214\373w\n\\\232\355\350_o\021\014\226\342\236\007\"\003\216oB\357\367\010\376f\345?\353\313?\251\025KQ4\247\027\023\'B\242gk\2311\347\344N\260)\211\002\343\364m\201"
+        #   p: "\364n\250\312\307\223c\333\002\365,\035\222\2451\345\306<\305\237y\371\256\3260\332\027\364\203\371\214|U6Qc*\267>\205P\265\024\356\005\016,\314\314\227\261\251\200\320\340\003\317\272\253\037\310*\207\236q\004\363\337fjf\322Y\277\357\010\244\314\233\026lF\3513\336\367\246\372\303kH\370\255\246V\300&\240\205\256\277\251\370\306nA\373\340\324\250\rhy\013\366\256\013\351R>\215\312\345(\035c\274bn\270\266\246\252\373f\006\303qD\275\243_v\244O\247\260\241\025\303\371\216\260\207E<\333b\223\227\301\317%S}\346\037\326-\277%mv\335M\370\302aZ\021\020\370.\225Uh\243ST\244\020LSM\201\035&\214\361\252.\004e\203\007a\214\266 \3629/i\214\224\317\343\200\037\367\214 i\005\236\231\316\0001|XWB\231X\241a\265L\027\t\347\273\370\334\314\323\350\360\267=\314m\311\r)"
+        #   q: "\273y[U\337\331\235\377F\'\216\r\024\031\031\325\272\261b\267\036\274N\346i\'b/j\265z\201\316\371\321\023\361\231k\361\t&\000\212\027\351 4yv\341g\262>\353\205\002os\303\224V\351P\251\337\374QD\370o?}~\0048\311\212\037\307\370\263\265\304\021\306\264\337\361.\2132\371\306\244\366Y\330\2400\033F\356\027\320\312m\220\032\001\026Ra\350j\327\002\367\302\'\324\243\262\023\301+\374OVH\022\270\246\001\260\336\230\212\304\017\333\003\237]d\350\021\202\326\203\331\3521\221\211\315.E\373\347x\\s\013\023vDI\321/\234,\307\216q\304\t\263\324G\026X{*6>\250;\231\2208\214^\233\\~\256\001=\035\215\346K\236\rk\"\031\213\252\327+X\322xn\344H?\316\0175\215\023Z\274G\2436\3526\223\002\223\032J@\372\352\211\372\327I\237\260\321\212\222\225@i\267\"\355\033w"
+        #   dp: "\231-\227\335\254\0003\275\2266\352\264\205\247\274\203P\035\331\202\275?\271OEB\007m\026\037\226\350{\306\024n\336\340\002\360,\311\256\032\023T\364\302K\034m\033\332<\013\206\227\361U8\017\212\207~\3058\343S\206\254\373N\253+\332\262B\004n\3248z\237\275\377\346\226\337\343\003\"\261\244\260\357\000o\264|\322,\016jv\177\266\036\020\241s\307#Gh5\350\220RaV\016)\256=\333]\033;\\C\251B\3757\232\222v|O%\300r\034\271\217\251x\311\032\010\274%\255~\267\347\021\\\317=u\031I<\357R\375t\215*\222[\354\367v\333\203\204\332\232\203U\344ku \243\351\236\313I\343j\370\321\006\341\227\306\002m\340\027ukt\270g\222q\371`:\366\374r\343\020\361\367\201\372\365\225\255\364\230\342\n \360\203\216\232\346\256\013\273?\271\330]>\261\020=\3246\320S%\t-k\2471"
+        #   dq: "\250g\014\352\210\330<\232\360s\333\240W\353lO\227\022|\260-\305\315\302\037\267&\365\235\017C\352\222\367\311\035\367*o\321\346\373\342\004*\022Y\301\211R\206\213\325\213\273\230\221exA\271`\220\330\312\237\317QYy(\305\211~\307`\023z%\256E\363xU\225\372>\303\343,\202\021\332X\334\032\006\377\026u\340*\310\371O\256f\330\310\343-\037\367\237\005I\372\372\177\307\263\265\017\343\272\344>\023.X3\250\366F\022\221\001\311\264\360\3415\321\323\335.\261|,!\350\177\374\342\325< R\361C-\325-\352G#7\230ov*\375\366\2148\031v\365\270\273\332\220\304\242Xs\324?%dm\307y\274iK\312\026\206\305\243\217\271aR\200g\3636\210\322\033\267\377\347\373\004\310\371\267\251\345\332\302\006\001\227\000?\366\007\353\352\246%9\212AZ\251sP,ll2\356\035Z@\2605\270\003\361e"
+        #   crt: "A\360\346@\373\262\243\024\347\206W\246\374\037nfB\376\373\2629\027+\304F<\327\337\231\236\017\315v\307X\273\376\300\035\317\340\262\276\024\377\304\233\031\342&h\240k\342\237D\226\357\242&\330\330\360\'\335\367\332\262O\324\355u\340\263\243\376)\024\317~$\210\321\\\023\270\262\025,\323\243#C\222\330=\226\201P\025\306*c\307\275\313\005\337tp\245\273\014B\326\305\236r\237b\177\247\004\264\332\026b\024$\272\\TdY\275\377\252/\302~_\035\250\027\324\331\227\203X\257b\326\331(N\221\003Z>\2231\302\370<\206\363[1\340\355\232\323\256\024\036\2661\002\213yi\324\317\032\341:6\r\333\273\027\340\304\211|\026,\250\027\ny\342\311\270Z\326T\356hq^\017i>\206\267\267\267\216\210\337\360\374|\000@\3422a3\263\020N\n\302\274QU\201\\\006\264\030\312\300OR2\352\266;I\016\275\245\310"
+        # }
+        value: "\022\220\004\022\006\010\004\020\004\030@\032\200\004\263\000\264\317<\033L\222(\223\223\323:\\^\035~T\256\265\251O\"\346\275\307k\224<J\013\002\317\374-`A:R\001\345\266\200>\010\334\237\374=\260\207,\306\316N\\X|\021\001\343\314\215\377\334\3650\315F&\267}\254j\313U\250\252\036]\376\205\022<\271\3436\t\210\331\364W\335\320\031\257\277\206TT\365\000\024\355\023\305\205Y\371,i.\374\032-\241\321\016\001\007z\325e\323\026\'P\354\323\022\341\013_Gy\336$\177n\206\032|\325;5\"\276+\214[\352o_QQ\241f\334$\327\223\217z\320\327\332r\252\002>\244\335\330\250\357\004\223lc\210\363N\"\254\375\031\324w5\220\377\234N\311=\341\355S\371J\203\025}\217\376\217\362U\2112\346\361\342Q\306\025\245_u\030*s\220\245\304rz\276\003\372\323e\375\373=\370\315\262k#S\304\261.\205w\t(\214\342\317\006\024\225,F\005\365\326dP@<\321L\240M\355\220YF\264{\030=\366\335\256m+\3071\027\326\260\323n\276\236\367V\177~\351|7\323a\014\007\027\256\331\271\300\311\006ZC\277J\301\377~\267\207U\020\023\246x\002Q\240b:u\216Xf\316\251}K\313M\t\217\212X\335\200\3712\237N.\224Y\316\242\372\204\004\202\251)n`\237\220\264\306tH\240\273Q\026\372\362\'\375M>\357\243\344\257\250+]\361M`\356Yd;Ow\360\202\361\200\311\323C\267\242xn\255\3467\326CQ\311\347\353c\\\2123\302p+w#\322?\260\317\321\177i\302G\000\237I\236\200\002\264\0256\241P\2249T\001\3219(\366\003\336\207\303\350\004\245=\3554\034\220\221#\224\2237\246\0201l\000\345\366\241\200\212\210\2400\357\311\356\241\360\322\230I\3260-\311\351\315m\"D\34010B/\362\301Z\374\255\346\331\312\335\315q\017\"\003\001\000\001\032\377\003H\270\000\274/\256\347ly\017z\376T\2671\204\311\315\235,/\233\366\221\306\207=,\373Q\031(\013Vg\030\263f\375\351\307h\\\260\326\350\312\025\256\372\007\360(\323\247\001\334\360\211\370:\222\337\035\022\246\211,\333R\3378.\3233\307\230\275\203\230\253\315c\233G\351\006\n\331\215\261\266\000f\212\010\333\305\266|\315\013;u\024\223)\231c\240\247\032oP39\211\264&\266\343/\316-\362\322\006\016/\261{\273\"\371\346\210XKl\233\016\'\250xe\035\270#\370\341^[\340](\314h\375\005\372\215\367\273\355\033\303\242\321n\030\004\342\033\"\205\367U\354\262\202\360T\315\301y\021\005?h\203\177T\000\255\300v\007\364\025R/\356\213\023\364V\206\0349\240t1\317\277h\271bue\034l\"\206\240\321-\260\257\200\356d\363\n;\006\362\014\236n\347\353\037\374\332\351]r[\006\341\0359[#>\330\204\035\031NcGl\254\354\300\207F\247\240NT\327LA|\332\210\022\275\027\236@r^lk\271\306\'&\374A\021\324$\3059\246\360n\352\"@\317\232\357:\277\214\214\"\256G\257\272\355?\004\333x\370v\234\353\252\255\037\347X\261\311\224I\372b\370y\207%\331\t6\342\200v\304C\3043\347\277\271\260\344\001K\341\255\224\333\227\2121\322Z\303:E(e\231\313B&\005\234\222]Y\241\332%%\0223pN\216\342\350\211\237J\031ti\006\330\242v\360\030\326\325\333\007\225\026\227\014\355<\276m\374\337\363\022\215\017\317GX&2\236\035\245^\325r`\t\006<Ri\032)\321Q\202\035*\034\320\357\030\\\264\303\223$\214\373w\n\\\232\355\350_o\021\014\226\342\236\007\"\003\216oB\357\367\010\376f\345?\353\313?\251\025KQ4\247\027\023\'B\242gk\2311\347\344N\260)\211\002\343\364m\201\"\200\002\364n\250\312\307\223c\333\002\365,\035\222\2451\345\306<\305\237y\371\256\3260\332\027\364\203\371\214|U6Qc*\267>\205P\265\024\356\005\016,\314\314\227\261\251\200\320\340\003\317\272\253\037\310*\207\236q\004\363\337fjf\322Y\277\357\010\244\314\233\026lF\3513\336\367\246\372\303kH\370\255\246V\300&\240\205\256\277\251\370\306nA\373\340\324\250\rhy\013\366\256\013\351R>\215\312\345(\035c\274bn\270\266\246\252\373f\006\303qD\275\243_v\244O\247\260\241\025\303\371\216\260\207E<\333b\223\227\301\317%S}\346\037\326-\277%mv\335M\370\302aZ\021\020\370.\225Uh\243ST\244\020LSM\201\035&\214\361\252.\004e\203\007a\214\266 \3629/i\214\224\317\343\200\037\367\214 i\005\236\231\316\0001|XWB\231X\241a\265L\027\t\347\273\370\334\314\323\350\360\267=\314m\311\r)*\200\002\273y[U\337\331\235\377F\'\216\r\024\031\031\325\272\261b\267\036\274N\346i\'b/j\265z\201\316\371\321\023\361\231k\361\t&\000\212\027\351 4yv\341g\262>\353\205\002os\303\224V\351P\251\337\374QD\370o?}~\0048\311\212\037\307\370\263\265\304\021\306\264\337\361.\2132\371\306\244\366Y\330\2400\033F\356\027\320\312m\220\032\001\026Ra\350j\327\002\367\302\'\324\243\262\023\301+\374OVH\022\270\246\001\260\336\230\212\304\017\333\003\237]d\350\021\202\326\203\331\3521\221\211\315.E\373\347x\\s\013\023vDI\321/\234,\307\216q\304\t\263\324G\026X{*6>\250;\231\2208\214^\233\\~\256\001=\035\215\346K\236\rk\"\031\213\252\327+X\322xn\344H?\316\0175\215\023Z\274G\2436\3526\223\002\223\032J@\372\352\211\372\327I\237\260\321\212\222\225@i\267\"\355\033w2\200\002\231-\227\335\254\0003\275\2266\352\264\205\247\274\203P\035\331\202\275?\271OEB\007m\026\037\226\350{\306\024n\336\340\002\360,\311\256\032\023T\364\302K\034m\033\332<\013\206\227\361U8\017\212\207~\3058\343S\206\254\373N\253+\332\262B\004n\3248z\237\275\377\346\226\337\343\003\"\261\244\260\357\000o\264|\322,\016jv\177\266\036\020\241s\307#Gh5\350\220RaV\016)\256=\333]\033;\\C\251B\3757\232\222v|O%\300r\034\271\217\251x\311\032\010\274%\255~\267\347\021\\\317=u\031I<\357R\375t\215*\222[\354\367v\333\203\204\332\232\203U\344ku \243\351\236\313I\343j\370\321\006\341\227\306\002m\340\027ukt\270g\222q\371`:\366\374r\343\020\361\367\201\372\365\225\255\364\230\342\n \360\203\216\232\346\256\013\273?\271\330]>\261\020=\3246\320S%\t-k\2471:\200\002\250g\014\352\210\330<\232\360s\333\240W\353lO\227\022|\260-\305\315\302\037\267&\365\235\017C\352\222\367\311\035\367*o\321\346\373\342\004*\022Y\301\211R\206\213\325\213\273\230\221exA\271`\220\330\312\237\317QYy(\305\211~\307`\023z%\256E\363xU\225\372>\303\343,\202\021\332X\334\032\006\377\026u\340*\310\371O\256f\330\310\343-\037\367\237\005I\372\372\177\307\263\265\017\343\272\344>\023.X3\250\366F\022\221\001\311\264\360\3415\321\323\335.\261|,!\350\177\374\342\325< R\361C-\325-\352G#7\230ov*\375\366\2148\031v\365\270\273\332\220\304\242Xs\324?%dm\307y\274iK\312\026\206\305\243\217\271aR\200g\3636\210\322\033\267\377\347\373\004\310\371\267\251\345\332\302\006\001\227\000?\366\007\353\352\246%9\212AZ\251sP,ll2\356\035Z@\2605\270\003\361eB\200\002A\360\346@\373\262\243\024\347\206W\246\374\037nfB\376\373\2629\027+\304F<\327\337\231\236\017\315v\307X\273\376\300\035\317\340\262\276\024\377\304\233\031\342&h\240k\342\237D\226\357\242&\330\330\360\'\335\367\332\262O\324\355u\340\263\243\376)\024\317~$\210\321\\\023\270\262\025,\323\243#C\222\330=\226\201P\025\306*c\307\275\313\005\337tp\245\273\014B\326\305\236r\237b\177\247\004\264\332\026b\024$\272\\TdY\275\377\252/\302~_\035\250\027\324\331\227\203X\257b\326\331(N\221\003Z>\2231\302\370<\206\363[1\340\355\232\323\256\024\036\2661\002\213yi\324\317\032\341:6\r\333\273\027\340\304\211|\026,\250\027\ny\342\311\270Z\326T\356hq^\017i>\206\267\267\267\216\210\337\360\374|\000@\3422a3\263\020N\n\302\274QU\201\\\006\264\030\312\300OR2\352\266;I\016\275\245\310"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1236523652
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS256
+      #   modulus_size_in_bits: 2048
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\020\"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS256
+        #     n: "\242y\"\263\355\375\357\305\331,\305\356TY~\261\361\215\245\277\2069\227\022\352j\224o\250t\313\246\271\236(\311\343e$\226l\021\261\376\340\302\320\030Zb\310\321\367\022\307e\304h\200\311\037\265\310\005T\324t\n\035B\324\337\033\220u\240\320]\206o\033\332z\313\211\367\217\353\354&\222\261\2236\274\236\005 \211\207\275\261v\037#\270\254\016x\036\265\316\241^U\367\006\014mS\350\323hBOy\352}\341\274\000p\372^\006\377\207\002\245\251\373\030\276\r\305\344\272e\341\301\376Q\236d\352O(oF#D\364\237<\342\327\023\370QA{\377\256\207\006\364\311\202\002\2606Fe\022\217\301\\\323\255\2063\375\354\032)\211H \t\255inA\267cG#\002\265\347\246.M\334l8a\266,C\027\021\\\302=\253\004\007BM4\002\263\326^\301\300\036\"+O%Cw\306R\213\216\360$\342\305\205\357H\261"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\017\335\304\205\315\267\376\262S\203\013\321:k\202\363\335\242\367\214\274\216\3431\365\262r\314t\246\363<T\344\035\027\232\035Fuj\030PI\223\251r\250\356*\267q\311\257\013\311\344dP/S\351l\237\030\251\246\253V00\231\231\027\202e1\363\360,\351\213lrt\006\271\007Q\010sJ\344\025tU\372*3D\016BFF\3046D\227%#\332\235S$\343@<V\372V7fm\310\013\372\333\353GBy%a\235\244\230j\362\316Uu\340\214\363\271Oc\222\252,rg\"\350\277\226\354\n\365\304\017\336A\330\202\014\"\\V\360$\'\357\276J%\343\201\241\003\3347\245\224\265c&v\275\200s\335\212Q\315\177\016]Pqx\216\245\313\366\316\010{\352[qH\275o\204#\343\277\023\263L\344\246\343\316\335\376\016\025\n\212\027\204\177}\322\316\250\266\307\327\347[\t\"\005\007\010\005\336\220\231A\233\324\001"
+        #   p: "\324e\237S\270\226\177\301\301S\362F\223A\204\036\367<\205\206\2149\363\337\237w/;&\250j\2040\034\206<\316\222\366\020U>j\261\366\377$\350\257\\\370\321\201~\363Fa\032\027u\024\031\003N\316\362\336NMf\267\247X\246}\002H\337\375\tF(k\177\355>\300\201\356\366\254\247GyB^\245\307\362D\210]M\335\265\232%\260\251)\345\272\222\224\302\207 -\\\246\232\262\322\2517\200\031k"
+        #   q: "\303\323\315\231\240`\341=&\203s\016\301\327\034\366<\234\317\253\225\221#C\0206\227@Wv%B\2620\3755\256\365\334I\210P\020W)3\245\270[\037\274\331\262f\027\264\020BQ\"G\333\333\001\033B\200P\347\253I\300\225\345F3\365\205\345\223\220\027\271;\336]\233\223\222\241\036\337\240\216Z\014\366\210\363\326qE\261\225\014\254\305\"\202\2477\360&2N\\Ik8\240#C\025\216\3070\341S"
+        #   dp: "b\240M\273\262\233/\367\350\255\362r\223.y\013h\002\322~\007\337a?\025\355b!5r?\003\342\323\\\002 j]\230w\374\343\316\177\303:\200Q\355\372\034N\345\204Q\312\0053\237J\025\323Se\005\241\364\372<\030K\274\376\222M\335I%V\211\035{\315\213|\370\']\257m\274\277\223\257\036\004\0354\202^O\3658^{\225\304\322\310\201\304\201\014R\310,H\344\034\370\370\320\231\203\014\030\301"
+        #   dq: "\235\035A\026\341\362\370\243\334S\230\253\235`\036\257|\377\377*VL\3452\270>di\316\002Op\314\220\273\207\'\301\300\245\n\205\332&d;8\331\311\332\223\345A\331\341h\251\246\333{\232\225\315\207|\274\205@\344.\254t\244\217\030\0355\376\340\253\334\356\373\302\t\355\246\225\347\r\370<I\220\014\265\333\323\023w\330\327\246\333\304\030\225\355\323O\240t\277\340\232\2706{\235)\366@\253z+\\\217["
+        #   crt: "Z%\361\2568\034\377\340V=\210\024\023\223\363\227\206\rQ5\205Ww-\351i\234(\022\005rg\004\255BK\2222&\366\207\361\241\344p\313,\343\355\r\376\252>\202\213\347~\2067\245*\226y6\014\271*\036\240\331PX=\3568\2779ht^k\016\346Gr\236\034\274\n_\037\243\231\'\272q\320\275\010\240\264\346\020}c \242\347\305\024\310Ug1\346\006\005\331\215\2625\271s<%8\253\363"
+        # }
+        value: "\022\212\002\020\001\032\200\002\242y\"\263\355\375\357\305\331,\305\356TY~\261\361\215\245\277\2069\227\022\352j\224o\250t\313\246\271\236(\311\343e$\226l\021\261\376\340\302\320\030Zb\310\321\367\022\307e\304h\200\311\037\265\310\005T\324t\n\035B\324\337\033\220u\240\320]\206o\033\332z\313\211\367\217\353\354&\222\261\2236\274\236\005 \211\207\275\261v\037#\270\254\016x\036\265\316\241^U\367\006\014mS\350\323hBOy\352}\341\274\000p\372^\006\377\207\002\245\251\373\030\276\r\305\344\272e\341\301\376Q\236d\352O(oF#D\364\237<\342\327\023\370QA{\377\256\207\006\364\311\202\002\2606Fe\022\217\301\\\323\255\2063\375\354\032)\211H \t\255inA\267cG#\002\265\347\246.M\334l8a\266,C\027\021\\\302=\253\004\007BM4\002\263\326^\301\300\036\"+O%Cw\306R\213\216\360$\342\305\205\357H\261\"\003\001\000\001\032\200\002\017\335\304\205\315\267\376\262S\203\013\321:k\202\363\335\242\367\214\274\216\3431\365\262r\314t\246\363<T\344\035\027\232\035Fuj\030PI\223\251r\250\356*\267q\311\257\013\311\344dP/S\351l\237\030\251\246\253V00\231\231\027\202e1\363\360,\351\213lrt\006\271\007Q\010sJ\344\025tU\372*3D\016BFF\3046D\227%#\332\235S$\343@<V\372V7fm\310\013\372\333\353GBy%a\235\244\230j\362\316Uu\340\214\363\271Oc\222\252,rg\"\350\277\226\354\n\365\304\017\336A\330\202\014\"\\V\360$\'\357\276J%\343\201\241\003\3347\245\224\265c&v\275\200s\335\212Q\315\177\016]Pqx\216\245\313\366\316\010{\352[qH\275o\204#\343\277\023\263L\344\246\343\316\335\376\016\025\n\212\027\204\177}\322\316\250\266\307\327\347[\t\"\005\007\010\005\336\220\231A\233\324\001\"\200\001\324e\237S\270\226\177\301\301S\362F\223A\204\036\367<\205\206\2149\363\337\237w/;&\250j\2040\034\206<\316\222\366\020U>j\261\366\377$\350\257\\\370\321\201~\363Fa\032\027u\024\031\003N\316\362\336NMf\267\247X\246}\002H\337\375\tF(k\177\355>\300\201\356\366\254\247GyB^\245\307\362D\210]M\335\265\232%\260\251)\345\272\222\224\302\207 -\\\246\232\262\322\2517\200\031k*\200\001\303\323\315\231\240`\341=&\203s\016\301\327\034\366<\234\317\253\225\221#C\0206\227@Wv%B\2620\3755\256\365\334I\210P\020W)3\245\270[\037\274\331\262f\027\264\020BQ\"G\333\333\001\033B\200P\347\253I\300\225\345F3\365\205\345\223\220\027\271;\336]\233\223\222\241\036\337\240\216Z\014\366\210\363\326qE\261\225\014\254\305\"\202\2477\360&2N\\Ik8\240#C\025\216\3070\341S2\200\001b\240M\273\262\233/\367\350\255\362r\223.y\013h\002\322~\007\337a?\025\355b!5r?\003\342\323\\\002 j]\230w\374\343\316\177\303:\200Q\355\372\034N\345\204Q\312\0053\237J\025\323Se\005\241\364\372<\030K\274\376\222M\335I%V\211\035{\315\213|\370\']\257m\274\277\223\257\036\004\0354\202^O\3658^{\225\304\322\310\201\304\201\014R\310,H\344\034\370\370\320\231\203\014\030\301:\200\001\235\035A\026\341\362\370\243\334S\230\253\235`\036\257|\377\377*VL\3452\270>di\316\002Op\314\220\273\207\'\301\300\245\n\205\332&d;8\331\311\332\223\345A\331\341h\251\246\333{\232\225\315\207|\274\205@\344.\254t\244\217\030\0355\376\340\253\334\356\373\302\t\355\246\225\347\r\370<I\220\014\265\333\323\023w\330\327\246\333\304\030\225\355\323O\240t\277\340\232\2706{\235)\366@\253z+\\\217[B\200\001Z%\361\2568\034\377\340V=\210\024\023\223\363\227\206\rQ5\205Ww-\351i\234(\022\005rg\004\255BK\2222&\366\207\361\241\344p\313,\343\355\r\376\252>\202\213\347~\2067\245*\226y6\014\271*\036\240\331PX=\3568\2779ht^k\016\346Gr\236\034\274\n_\037\243\231\'\272q\320\275\010\240\264\346\020}c \242\347\305\024\310Ug1\346\006\005\331\215\2625\271s<%8\253\363"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1833366781
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS256
+      #   modulus_size_in_bits: 2048
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\020\"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS256
+        #     n: "\267\270\242\002K\227t\002\237G4Y\347\365\036\377\350\271\264\017iC\276\237\346#\221T\000\022\230\305`]b\366\344\026\2668+\020E\305\026\215\207\241t\242^L\002\323\377\234\304\257\205\304\343\226?\021Q\305\366g\013@\317\363\303\263jG\274Zo~\336=\223r\3462K\216\301\177?\264d\010u\324\367\2061#\204-\237\177\364\2105(6\'si\017Z\353*d\017kS\224\305\027\350\354\320F;\246J\347D\343Q\225h \322\200\032\214\252+\277\276ly\375\250\002c\270\270\336v\273\375@Q\306\372>\31635\342\240\177\356\220mv\264\333\020\354n\312\202\200\022\037\2240\335f(C\313\354wz\325\2764%N\316\263_N\216\356(\202`\2053\252\214A\254\250\020\311^\242A\267\025p\002D\255\233`\006a\215<I\030:`\0130J\265\277\261(\207\2459\336\313\351\037\325E)j\364Z\346\277"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\003.K*q\254\231\234B\003\n\353\250\347\0346J\r7\264\334\231\263\251\336c\201\354\241\016\\\tn\313\004t\364\200\252\324P\312\216\000\004\342\233\277\3413\276v5\033\2512\236\372z\375\027\003\244A9\327\342@>xL\314\022M\377\225\021\t\305!\276\204\237\274\351\207b\272,\254\313T:\341\317\363\036\245\372\313\321J\010\354\3525\226\326\004\272\002!\20613s\311\305\n\222\2517e\261>\263\014n\341\304\310\212\262\307\3359lO\256\353\370&\304\366\227T\371\315\005\342\246\305\232\244G\333\273W\362i%\234dK=\307\257\210\013\032\317r\254\221\257!\3332\254\265[fk\204E\320W\217.a\317\250\263\316\n\253>\245\364\253y\027@\021\232\031\026\315\'\336\325\'\007\345\324\2338\321\363L\246\235z\007\306[s\321\240\307-\033S\272\354C\262#\277\334\325\244\000\017\334O\211\265\317\311*\247\244\302\276\001"
+        #   p: "\341\343]N\335[\354F\275~\006\212\231\310r\274\367\025\354Q]\304\357\310\026E\216\271p-\330\241\022\250\027[+\333\314\226Q/\002\322/\260p\331\327\202s\n\220\203\n1\350\000F\305\316s3\'Y\342RLe\274zS\240\207\006/\242\037A\337:\333qm\221\357b\342\3152P\242i\242A\302\361\271$\333\002\235\210\215}\007w\361\265\233r\250\315\273:\023\220>i\365\367ti\3345\2412\377"
+        #   q: "\3206H\314\251\241m\340\367b\311[\320\222Z\330\345\321\242\224\230\207\001\221\333\241\020\353DXxL\264\264\020\337d\203\371\223\226\036[\336F6\271\277\t\306mN\371\375\245\367pK\263E\346c%\255?\372O\016u\342\330\302\310\312\223\331\264\007MZ&\227\352Lb\362[\267\254\311H\024\274~\214\266;l\241\316\347\261\224[\237[\225\251o\005\376\257\337\337\343\202\223F\245\367J\244\224\016l\367\014A"
+        #   dp: "3\245PH\037\304L\231?\351\370\204\347\3644\207\217\326\253YO\037\037\323\227\203\377\305\326(%Gn\204\220\022\200L\230\322%m\242/|G\323\005{\210\217\324\362\205\203g5\254\032\202OYwp\237\324g\014\240\226\037A\317\212\017\230\004q1\026`\326\233py\325I\276\241;\307\303\332s\006\246\023\256I\006\351\352*\266n\307\256\\\3150\345^B\353s\263\325^\357\313\024\360\300\343`?\242\361"
+        #   dq: "N*\323\021\342\3553\357-p\354\337\207b*\261\0336\244\256\334\304r\226k\272\000<\023 \223\251X}\361\207\254\230\317m\325\001\370\303m\340\305\303.\314K\345F}/}\301\256\351\322\3507x\301\236/>\326\322\363#%\235tP%\177,\356\241U\263\332\306j\257J7\236\252\302`\346\223,\010\216.\256`\233#S\353A\334\204Gc\343\000F\316\351\242\3446\236cX\177\261a\3248\234W\001"
+        #   crt: "U\226\242*\023\200\200\221,\006e!\024Z\341*\032\254\250\027\000\016E\275w\206\007\302i\224S\240r\370\211\302\310\264\376\207\200\357\203\302\265P\214\301[-w\347\275\030\032q\204cD\232\273\271x\025?\216\377\327%\274\027I\020[o\305\212 \2609C5\373\334R\353\001T\222\t\324\347B1\307\007\357S\267Gt<\222N\327\200\022\243\347^\337K\ts\323\375\302M\357t\373\227X\030\271\240\031\240"
+        # }
+        value: "\022\212\002\020\001\032\200\002\267\270\242\002K\227t\002\237G4Y\347\365\036\377\350\271\264\017iC\276\237\346#\221T\000\022\230\305`]b\366\344\026\2668+\020E\305\026\215\207\241t\242^L\002\323\377\234\304\257\205\304\343\226?\021Q\305\366g\013@\317\363\303\263jG\274Zo~\336=\223r\3462K\216\301\177?\264d\010u\324\367\2061#\204-\237\177\364\2105(6\'si\017Z\353*d\017kS\224\305\027\350\354\320F;\246J\347D\343Q\225h \322\200\032\214\252+\277\276ly\375\250\002c\270\270\336v\273\375@Q\306\372>\31635\342\240\177\356\220mv\264\333\020\354n\312\202\200\022\037\2240\335f(C\313\354wz\325\2764%N\316\263_N\216\356(\202`\2053\252\214A\254\250\020\311^\242A\267\025p\002D\255\233`\006a\215<I\030:`\0130J\265\277\261(\207\2459\336\313\351\037\325E)j\364Z\346\277\"\003\001\000\001\032\200\002\003.K*q\254\231\234B\003\n\353\250\347\0346J\r7\264\334\231\263\251\336c\201\354\241\016\\\tn\313\004t\364\200\252\324P\312\216\000\004\342\233\277\3413\276v5\033\2512\236\372z\375\027\003\244A9\327\342@>xL\314\022M\377\225\021\t\305!\276\204\237\274\351\207b\272,\254\313T:\341\317\363\036\245\372\313\321J\010\354\3525\226\326\004\272\002!\20613s\311\305\n\222\2517e\261>\263\014n\341\304\310\212\262\307\3359lO\256\353\370&\304\366\227T\371\315\005\342\246\305\232\244G\333\273W\362i%\234dK=\307\257\210\013\032\317r\254\221\257!\3332\254\265[fk\204E\320W\217.a\317\250\263\316\n\253>\245\364\253y\027@\021\232\031\026\315\'\336\325\'\007\345\324\2338\321\363L\246\235z\007\306[s\321\240\307-\033S\272\354C\262#\277\334\325\244\000\017\334O\211\265\317\311*\247\244\302\276\001\"\200\001\341\343]N\335[\354F\275~\006\212\231\310r\274\367\025\354Q]\304\357\310\026E\216\271p-\330\241\022\250\027[+\333\314\226Q/\002\322/\260p\331\327\202s\n\220\203\n1\350\000F\305\316s3\'Y\342RLe\274zS\240\207\006/\242\037A\337:\333qm\221\357b\342\3152P\242i\242A\302\361\271$\333\002\235\210\215}\007w\361\265\233r\250\315\273:\023\220>i\365\367ti\3345\2412\377*\200\001\3206H\314\251\241m\340\367b\311[\320\222Z\330\345\321\242\224\230\207\001\221\333\241\020\353DXxL\264\264\020\337d\203\371\223\226\036[\336F6\271\277\t\306mN\371\375\245\367pK\263E\346c%\255?\372O\016u\342\330\302\310\312\223\331\264\007MZ&\227\352Lb\362[\267\254\311H\024\274~\214\266;l\241\316\347\261\224[\237[\225\251o\005\376\257\337\337\343\202\223F\245\367J\244\224\016l\367\014A2\200\0013\245PH\037\304L\231?\351\370\204\347\3644\207\217\326\253YO\037\037\323\227\203\377\305\326(%Gn\204\220\022\200L\230\322%m\242/|G\323\005{\210\217\324\362\205\203g5\254\032\202OYwp\237\324g\014\240\226\037A\317\212\017\230\004q1\026`\326\233py\325I\276\241;\307\303\332s\006\246\023\256I\006\351\352*\266n\307\256\\\3150\345^B\353s\263\325^\357\313\024\360\300\343`?\242\361:\200\001N*\323\021\342\3553\357-p\354\337\207b*\261\0336\244\256\334\304r\226k\272\000<\023 \223\251X}\361\207\254\230\317m\325\001\370\303m\340\305\303.\314K\345F}/}\301\256\351\322\3507x\301\236/>\326\322\363#%\235tP%\177,\356\241U\263\332\306j\257J7\236\252\302`\346\223,\010\216.\256`\233#S\353A\334\204Gc\343\000F\316\351\242\3446\236cX\177\261a\3248\234W\001B\200\001U\226\242*\023\200\200\221,\006e!\024Z\341*\032\254\250\027\000\016E\275w\206\007\302i\224S\240r\370\211\302\310\264\376\207\200\357\203\302\265P\214\301[-w\347\275\030\032q\204cD\232\273\271x\025?\216\377\327%\274\027I\020[o\305\212 \2609C5\373\334R\353\001T\222\t\324\347B1\307\007\357S\267Gt<\222N\327\200\022\243\347^\337K\ts\323\375\302M\357t\373\227X\030\271\240\031\240"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 3431262
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS256
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\030\"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS256
+        #     n: "\331\306\021\001\225\226I\356\321\232\371\332\232\311\310\303\033\233\270\330\245;\213m\177\226\343S\215\032\343\327\273\213\370\213\352\277\367[\361\374\355\330\214\267d\'\\\350\025.;B\322!\326\344`\320)F\253\\\'\211\337pXbI\262\352C\270\313\300j1px\037Z\247\245\033O\202\032\201\272\367\311\375\020\344\330\230?5\010\260r\027\231\0051\265\346\355\257\261\271\341\341\262\240a\203T\017Ruo~\317\236Tf\016|7\335\340\267\345[\340\376\2672\374E\212\330\356\035\324\024\364L\301G\206^B\306\210Z\006\022\021\263\330\273\313\033\n\342\177n\224\330a-\262uj\033\"\026\375\325\256\364)\224\007\345Y\344\247K\262\177\000`_\2233\310[\354v\201Q\013B\305\361\241\311\242\375i\215p]\316\005Z\274\270\255\257\007A1`\373\344\222\257\377\031r\336\263\203\205}\004\353[D\215}2GV.\016\024\343\362\017\246i\244pf\t\227h\t\034\376\236\363\003\250\343\017\355\346\027\247`bZ^\360\203\251\272\201^\025`\302%.\031FBM\013\003\006K\356\250\277f\320M\203\003\326\371\235j\023Ub\250]\304\032\330\366\306%tOm\212\354wG\207\351]Np\376\326Z\336m\010E\331Y[\221P^?:\201\327q\321\256eG#Q\356\355\314\264\322\022\200\351j\351gX\331\0251\366.l\023\322l\327S\027\315"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "%\222\260\017\365*e.\244p\347\357`\331\364>\277vn]\310\242]\243\302\211\372\306IW\354\234%\322XKq\260\365\301b\036\270\275\256G\234\352\322\263\203&\003\030\374\016\325\027\371\241(\241\366\002T\033\014WB\224\220\261#\014\224\247h\327\031\231\025L\231\224G\237\016\350|`\312,\242\347\274\000\263\343\016\262\274\201N\352\005\337x\314p\337\254\372\315\276\254uh\210\362\202\360\022\322\312\256\027-\262\323\231\347\262.\340\254\361\320X\2411\344\004\325\r\307\360Kj\262C\3253Z\277\022\347\323\377\246\002\232\236\375\307\344\334\275\331r\2538]\313xv\315-\307e\017\214fv\265\365\017\360g\255YEc\013>\224I\003\347\305\250\001\004b\224;`\227\362\2028\2171Y\313\342\262\377\\-\003\343pi\335Q\031\r\005z\256\316|\233\266\372y\272\200\361\314\306\301\304z\305@K\227\230\224\350\202T\216n3\245QUh\320AMz\342Y]\353\2203\363DZ\264\330\237\031\201\377\022p\221\272\311\216\2079\016sLSB\227t17\000:\332\037\262\276\034\007\362\225\240\007f\003\231-\243\247\351\374s:3c\t\314\001\214\026\014+\215\350\277\355\324\306\221j\'@r\021\372O\317\213\247\211i\227C%\256\352\376\370.[\223Z\226\256\304\324Q\027\337\215Q\022H\251|\026\260\336m\362\022\332\317\252\326\335\026\232Q"
+        #   p: "\373\270\202\004>\333\265\346\203\320\246\364{\246\344.J\362\356&~\254BM@c{\225\244\337\363\274c\357\242\276\250\247\003\341\375\013\3478)\014\021S\337Wo\321\260\2023\220\332\233\027\321\257o\271\002\365\356A\361\313:\014\001\361A\216?p\361\007bSk\240\346i\272*\247_\200\252\031/Q\322\017m\330\217H\362\357\252c\374\034g(\215\000\014dJ\355\343\376\002\322\265g\276\217H\246\244\235\357\325\232Bl\006W\307\301\374kK\267&\315\363\3205\005\372{\252\355\260j$ \300W\336\255\223\263\276{\334\242\206\312\n\342DA\243\t4\305I\031\024\033\311\215\371\344U\214\206\220\256\377a\236\334K\361"
+        #   q: "\335y\322\021g\"\037\355\024\375\221\260\243\273nc\234<-\207|\247~\013\215\215U\220\237\364\350\212\r\341\245\267\t\316\303\210\t\276\324\230s\317\342\333\304\244\004\353\013\237\213\270]\345G\336\225\246=\247\267\373g\262\320\266\371tv]\0250\320\265A\322\324\262\016~\230\"\210\254\200\242\306\342\261T\020]\t\3128\277\274\306\342\273z\222\001\265\033\353u\235\025\200\022\323\334J\217z\340\263\205\240\000\260l\241x\301\343\r>\345\210:\323\236W\316\024\235\315x\217\2535K\035\3358\330\273*\312\323\315\267\005\327/\303\014\371CF\337\360\233IAG\342\373(w\213\336jT\'_\213\246&w\375\337|\231\325\235"
+        #   dp: "v\303\252\014h\277\357\005\360\301\\TS\226\375g\336\022\332L-N\007\006=\317\362:;\2152\211{9P6\326\211\336j\034\243&\027\270>*Z\3010\226\340\177\200\036\346DN\022\241\353=24tfR\224\313\206\372\255[\357\3136\241\212\007*\334#\026\312?\256\007{\352\262a\016\372\345\206\317\242\313\037g\222\277\203L#b\037E\341\250\331/\344\256r\370\240\312[\256\264\031\024u\023\2144\341:$\006\245\303z\313J\3260\346\034\027\345\346!nM\255U\213I\305\372\177\275`w\376\273\206}\344 \310J\002?\272[\354\000\tR\355\226<\217\341@j\2151\257/[:\036f\375\337\037\037q"
+        #   dq: "j\315xT\024\034\014\261S\322\004\023\336\241`V\200\3354G\206\001os:\313\321[q\007\265\253\375\215\267PQVxL~\006\234\226\370\"d\017\376\377\351\257?g\255\0057\242}\312\004\370\251\372\333jMS\231\372\342\311\032\333@\200\315\344\332\335D\334\250\310\273\254\313\244\004Tz\374\216\357/\004\356\327\252\016,\206\354\207\344O\314\252\375\317\312c\372\036L\366\005\341\236\327n\314\243\277\311\025K\r\203\"\\\317\342h\346\r\030G\337\247\355\241;\230\314\017\220\037\255C%J\276\327\240\000\013\320\032\354\356\207m\0267/R\206\301\033\316sP.6\021\220M\255\220\357{\317\350K\334\311|\240\245\276\261"
+        #   crt: "\350!\227\361\260\212O\210\276\240Lf\375fDY\036w\225\031\235<\3411d\235\304\005S%i\030\273\001\033\361?\320\240\362\032\333\035x\377\232#t\347\324\2375\257\3424e\026\233$\254\'\331\372\022/N\240\305_\021\033\203V\335\177#\326bh{\316\234\333\366(\036\266\030J\237\362\222Y\'`P}\233\364\313\025C\325\351\001Q\002O\215\217\025c0\350S\335\001\032\351>+h\021Q$t\240r\314\023\304\215j*\001\360\002\233E\334\323[\230\315\000\323\004X\014~/\260\316\371\364n\022z\262L\005^\331\373}\245eu\274\303\324\315B\336.\360\365u!\020\312\202YG\204\302\024\tDrBS"
+        # }
+        value: "\022\212\003\020\001\032\200\003\331\306\021\001\225\226I\356\321\232\371\332\232\311\310\303\033\233\270\330\245;\213m\177\226\343S\215\032\343\327\273\213\370\213\352\277\367[\361\374\355\330\214\267d\'\\\350\025.;B\322!\326\344`\320)F\253\\\'\211\337pXbI\262\352C\270\313\300j1px\037Z\247\245\033O\202\032\201\272\367\311\375\020\344\330\230?5\010\260r\027\231\0051\265\346\355\257\261\271\341\341\262\240a\203T\017Ruo~\317\236Tf\016|7\335\340\267\345[\340\376\2672\374E\212\330\356\035\324\024\364L\301G\206^B\306\210Z\006\022\021\263\330\273\313\033\n\342\177n\224\330a-\262uj\033\"\026\375\325\256\364)\224\007\345Y\344\247K\262\177\000`_\2233\310[\354v\201Q\013B\305\361\241\311\242\375i\215p]\316\005Z\274\270\255\257\007A1`\373\344\222\257\377\031r\336\263\203\205}\004\353[D\215}2GV.\016\024\343\362\017\246i\244pf\t\227h\t\034\376\236\363\003\250\343\017\355\346\027\247`bZ^\360\203\251\272\201^\025`\302%.\031FBM\013\003\006K\356\250\277f\320M\203\003\326\371\235j\023Ub\250]\304\032\330\366\306%tOm\212\354wG\207\351]Np\376\326Z\336m\010E\331Y[\221P^?:\201\327q\321\256eG#Q\356\355\314\264\322\022\200\351j\351gX\331\0251\366.l\023\322l\327S\027\315\"\003\001\000\001\032\200\003%\222\260\017\365*e.\244p\347\357`\331\364>\277vn]\310\242]\243\302\211\372\306IW\354\234%\322XKq\260\365\301b\036\270\275\256G\234\352\322\263\203&\003\030\374\016\325\027\371\241(\241\366\002T\033\014WB\224\220\261#\014\224\247h\327\031\231\025L\231\224G\237\016\350|`\312,\242\347\274\000\263\343\016\262\274\201N\352\005\337x\314p\337\254\372\315\276\254uh\210\362\202\360\022\322\312\256\027-\262\323\231\347\262.\340\254\361\320X\2411\344\004\325\r\307\360Kj\262C\3253Z\277\022\347\323\377\246\002\232\236\375\307\344\334\275\331r\2538]\313xv\315-\307e\017\214fv\265\365\017\360g\255YEc\013>\224I\003\347\305\250\001\004b\224;`\227\362\2028\2171Y\313\342\262\377\\-\003\343pi\335Q\031\r\005z\256\316|\233\266\372y\272\200\361\314\306\301\304z\305@K\227\230\224\350\202T\216n3\245QUh\320AMz\342Y]\353\2203\363DZ\264\330\237\031\201\377\022p\221\272\311\216\2079\016sLSB\227t17\000:\332\037\262\276\034\007\362\225\240\007f\003\231-\243\247\351\374s:3c\t\314\001\214\026\014+\215\350\277\355\324\306\221j\'@r\021\372O\317\213\247\211i\227C%\256\352\376\370.[\223Z\226\256\304\324Q\027\337\215Q\022H\251|\026\260\336m\362\022\332\317\252\326\335\026\232Q\"\300\001\373\270\202\004>\333\265\346\203\320\246\364{\246\344.J\362\356&~\254BM@c{\225\244\337\363\274c\357\242\276\250\247\003\341\375\013\3478)\014\021S\337Wo\321\260\2023\220\332\233\027\321\257o\271\002\365\356A\361\313:\014\001\361A\216?p\361\007bSk\240\346i\272*\247_\200\252\031/Q\322\017m\330\217H\362\357\252c\374\034g(\215\000\014dJ\355\343\376\002\322\265g\276\217H\246\244\235\357\325\232Bl\006W\307\301\374kK\267&\315\363\3205\005\372{\252\355\260j$ \300W\336\255\223\263\276{\334\242\206\312\n\342DA\243\t4\305I\031\024\033\311\215\371\344U\214\206\220\256\377a\236\334K\361*\300\001\335y\322\021g\"\037\355\024\375\221\260\243\273nc\234<-\207|\247~\013\215\215U\220\237\364\350\212\r\341\245\267\t\316\303\210\t\276\324\230s\317\342\333\304\244\004\353\013\237\213\270]\345G\336\225\246=\247\267\373g\262\320\266\371tv]\0250\320\265A\322\324\262\016~\230\"\210\254\200\242\306\342\261T\020]\t\3128\277\274\306\342\273z\222\001\265\033\353u\235\025\200\022\323\334J\217z\340\263\205\240\000\260l\241x\301\343\r>\345\210:\323\236W\316\024\235\315x\217\2535K\035\3358\330\273*\312\323\315\267\005\327/\303\014\371CF\337\360\233IAG\342\373(w\213\336jT\'_\213\246&w\375\337|\231\325\2352\300\001v\303\252\014h\277\357\005\360\301\\TS\226\375g\336\022\332L-N\007\006=\317\362:;\2152\211{9P6\326\211\336j\034\243&\027\270>*Z\3010\226\340\177\200\036\346DN\022\241\353=24tfR\224\313\206\372\255[\357\3136\241\212\007*\334#\026\312?\256\007{\352\262a\016\372\345\206\317\242\313\037g\222\277\203L#b\037E\341\250\331/\344\256r\370\240\312[\256\264\031\024u\023\2144\341:$\006\245\303z\313J\3260\346\034\027\345\346!nM\255U\213I\305\372\177\275`w\376\273\206}\344 \310J\002?\272[\354\000\tR\355\226<\217\341@j\2151\257/[:\036f\375\337\037\037q:\300\001j\315xT\024\034\014\261S\322\004\023\336\241`V\200\3354G\206\001os:\313\321[q\007\265\253\375\215\267PQVxL~\006\234\226\370\"d\017\376\377\351\257?g\255\0057\242}\312\004\370\251\372\333jMS\231\372\342\311\032\333@\200\315\344\332\335D\334\250\310\273\254\313\244\004Tz\374\216\357/\004\356\327\252\016,\206\354\207\344O\314\252\375\317\312c\372\036L\366\005\341\236\327n\314\243\277\311\025K\r\203\"\\\317\342h\346\r\030G\337\247\355\241;\230\314\017\220\037\255C%J\276\327\240\000\013\320\032\354\356\207m\0267/R\206\301\033\316sP.6\021\220M\255\220\357{\317\350K\334\311|\240\245\276\261B\300\001\350!\227\361\260\212O\210\276\240Lf\375fDY\036w\225\031\235<\3411d\235\304\005S%i\030\273\001\033\361?\320\240\362\032\333\035x\377\232#t\347\324\2375\257\3424e\026\233$\254\'\331\372\022/N\240\305_\021\033\203V\335\177#\326bh{\316\234\333\366(\036\266\030J\237\362\222Y\'`P}\233\364\313\025C\325\351\001Q\002O\215\217\025c0\350S\335\001\032\351>+h\021Q$t\240r\314\023\304\215j*\001\360\002\233E\334\323[\230\315\000\323\004X\014~/\260\316\371\364n\022z\262L\005^\331\373}\245eu\274\303\324\315B\336.\360\365u!\020\312\202YG\204\302\024\tDrBS"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 856779207
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS256
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\001\030\200\030\"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS256
+        #     n: "\250b\240\274\320=\325\001\"\203.c\036\353\226\350{\346\350\225\257\227tG\001\371\253\300\336m3\020>@{\272\270x\206\274\206!\374t\004\033L\373/\344\035M*\345i\237K \000\\A\372\370\211\324\274\005\004\351\333\321\350\225\366\014:F$\367\'\270\022Q\026\2740\355\254\033\000&#\263\017t\326\377J\265]\207\032\024\335\370\211\262z8\003\362$\216\236d\210\026j\003\036\300\266O{M\277\025\023K\005\204\277\337=\t\326\357\303:\367\335\034\017z\035\275\206\017\260\346AoPB\215aG)-\317\270C\342\377\315\\@&\3345\245\310\232g\246\310\217\300j\233\022\220\020\232\351l\231\251\331\001\330\374<\3758O\343/\361\222\334s\000\237\"\200\245\232)j?\275h\265\266\272H\235\350\235\246\364\000\320I\247\262\311<\274\300\225B\023\214\303\252\356\221\375\260\323q\300\'\t\367\330\223\"7m\302\335\255\355R0\\\346\335v\355\005R\273\336\013\307\307g\330\353\357\023\r\326xI\202\224\357\032\250\353\342\006J\260\347\006l\226\2740\245\034\007\306?\351+\323W\215T\215v\267\377\031\247\017\266\331BK+t\375\244\\\234!\3607U\372\273\323$\0366\3363P\271+C\034\230V\037\217\004\345\373l\353X\3062\3618\r\023\233\342\211\276\303\000\246\306\016\36235\332\265\005\262\262\333\020\007\014\234\335\261\347\003\274\371"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "$h\222\375\320\177\026\037\3650\355&D\210\307,\004\242dv\375-\037\330\274\256\031\373\215|\212\026\200\207[\331\177J\234C\20632\ry\327\327X\326\275\231\262VL\235~ru|nv[\310B\261+h`l\032z42\274\255_*L\222\336U8,\255`J\252\021U\023>\253\350k6\245\3104\204\253\260\353,H\344\225\rp\314\314\331\236\032\362W\245oGC\336q\347\331\236\275,Lg\314\010h\360=\332JV\335\247\326\207\302\230\357%\305\351uc\316)\263KX\314\323\032\016\023r\322\337\262Z\022Zz\220jv\241\350\363K\243\216\325\375\363*y\326U-)\371\277\340\'\360\002\254\302_\271\027k\331\027th\220\223\026\204\251\211\333,\035\311\005\352\t\355K\2635\014GY\320\246\244\023x\020\251.\350\274\nK[x*i\205y,B\307\272\336(-\257\257V\316\257X\177\202@\246\235O<\034\346\017\3427\354\325\266\320KS\033o}\261\257H\222\347\322\305\323\317\326\254\323\327{\360\033\312\356\317ddb\226\300\3548\234\223\003\177?\253nwD\371w\350\221\331\256l\231\t\232)\177z\003P\223\336v\032\250\307\321*\351+\244\343\235\340\224\263ku\313\240+\314\351\316\350VF\302\033,\334\303\002\363\',vb\326\333pY\217\216+\231\242\334\203d\276\260\347r\211=\315\235\227\301\025\203"
+        #   p: "\321\343\030\273\3316\036\372\246\334\271\250\347u\214\215\205\365V\001\324\364\"_\323\277vh\302`\200\272\262\271W|\020p\016\234\371E\235\326\325\365\007\025R\246\022\253\204\300\r\216\320\240\\Q\3225\322\2021\031V\313:#M\276a\334\270GZ9\200p_\027\006\007\036,\035\314^\324B\000K\325@\264\021\027\367\365\275\213-\330\325\204\034O\230\271\261\267+\021 hz\007\022`\374\201\001\252\251DGG\260q\245\205\002\235!\\V\372\3422FjU\234\3442\207L\351\372<\307\333\340\325\226\207\244\260c\323\357\242\003\257 c\003\220\021\244XUC\033RZ\217\375\210G\013/b{\354@;\232\001\355\327"
+        #   q: "\315aL\262\315\373\265\343H\032\002\266\321\252qJ\261\350\037\2300xV\277\317E\0335\244\177xl(il\273\314\302\222\310\002\021X\374G\271~\3274\244:I\371\312\333\321P\272\tsF/S=l\376\217|D-\266\372u\016CA\316A[\230y\2558\262X\216\035\352\305?\251E\245\211lisA\306\340\021+\031\327-\265\240\322\203z\0106\276\001\244\rgt\277\324\021\004\362\262?\246\242\347\032\264\010\000\"\353\315%\331\372\207\363\313\371&b\340\313\254\304\206\"\341\200\270X\372\243\230\225y\\Sy%\210\214\032\325\261-X\247i\343Y+\207\222\356\'\031:\347\315\223X],\025j\2641\257"
+        #   dp: "\254\27718K\n\324l\220\261\322\315E\323\346\037\005Fk\307\2162q\250Uk\234U\224\306@\255i4J\340\375\326|\360\215\351\322\203d\016\014\nl\277\353U\350(\371\377\265\216\206\355\201\ns|\\T\362\256\311\347\014\261\311r\024\352\005D\226\205\346}\330\244;\035,Ym#^?H*\370\332;\307rz{\312\365\255%\357Z-\331\021\335GX\326\373\311\201\306\030Fc\014\373\006/\322\326\250\026}32Jxq`\302\3463\016\026\016\246\360>\221\247\362\222\0217\275w\'\222$\026\370a\205\210\206\016\336\341`\322\235\270\022\r%\366\347\010\261\273\200\017\267\376a\"Z\370\213\366B\027\373\306y"
+        #   dq: "Gt/\254\021\320\036\"\372\005\332\245\342bKR2}P\350\261\014]\032\024\261\265Z\277e}\357\033L:\201H&\365\314R3\330\342\304\315{\234\301\372\3250\301\220[T\002\243\007\000\255\rp\007q\361J\330p\033\373\212=\311}\212\332\210\220\246\206\306\306o\3761K\245O\204\353\216\210\236<2\237\337\345\24256\352\241\233\214\222lKC\334\261\266\377y%\024\266`\273\026D\303oR\207\326+Sv\tF\212\337\301\033\0070\213\266=c\354\200\0007\274_\017O\371f\300<l\317\030\032\001\004\'\"\201\365t\321-A\234F8\320\265Ac\314\242H\353\037s\010\014\324\323\254n\326\360\255\312\217"
+        #   crt: "\204\002\272\202x\003_\303\2373\270x\257\330\253,\331\276B\245\236\351\024\375\007\336\364Wm\241\234\224\226\027\322\345\203-\2409\272#\340\2301\321\023\351E\344tE\305\3122\271z\355K\031\363U\253\3749\024\364F&\253\276\347li\366?\231\325\255_2B\250\267\021}/\306\302\016\240\3252\224\343\200P+ss\203\022U9\3461j\270\362jb\233\302\246^\352| \036j\367\033B\004\200#??1\233\243\275!\031\252 \252\340\2161;\377\367p\034Z\305\307\017(\000\261\352\005\257\013+\216\2576\313R\237v\337Y\222\354\303\247^\034\240\352\234x\244\221\002\010\016.R\332\336\'\3272\0179\251\214"
+        # }
+        value: "\022\212\003\020\001\032\200\003\250b\240\274\320=\325\001\"\203.c\036\353\226\350{\346\350\225\257\227tG\001\371\253\300\336m3\020>@{\272\270x\206\274\206!\374t\004\033L\373/\344\035M*\345i\237K \000\\A\372\370\211\324\274\005\004\351\333\321\350\225\366\014:F$\367\'\270\022Q\026\2740\355\254\033\000&#\263\017t\326\377J\265]\207\032\024\335\370\211\262z8\003\362$\216\236d\210\026j\003\036\300\266O{M\277\025\023K\005\204\277\337=\t\326\357\303:\367\335\034\017z\035\275\206\017\260\346AoPB\215aG)-\317\270C\342\377\315\\@&\3345\245\310\232g\246\310\217\300j\233\022\220\020\232\351l\231\251\331\001\330\374<\3758O\343/\361\222\334s\000\237\"\200\245\232)j?\275h\265\266\272H\235\350\235\246\364\000\320I\247\262\311<\274\300\225B\023\214\303\252\356\221\375\260\323q\300\'\t\367\330\223\"7m\302\335\255\355R0\\\346\335v\355\005R\273\336\013\307\307g\330\353\357\023\r\326xI\202\224\357\032\250\353\342\006J\260\347\006l\226\2740\245\034\007\306?\351+\323W\215T\215v\267\377\031\247\017\266\331BK+t\375\244\\\234!\3607U\372\273\323$\0366\3363P\271+C\034\230V\037\217\004\345\373l\353X\3062\3618\r\023\233\342\211\276\303\000\246\306\016\36235\332\265\005\262\262\333\020\007\014\234\335\261\347\003\274\371\"\003\001\000\001\032\200\003$h\222\375\320\177\026\037\3650\355&D\210\307,\004\242dv\375-\037\330\274\256\031\373\215|\212\026\200\207[\331\177J\234C\20632\ry\327\327X\326\275\231\262VL\235~ru|nv[\310B\261+h`l\032z42\274\255_*L\222\336U8,\255`J\252\021U\023>\253\350k6\245\3104\204\253\260\353,H\344\225\rp\314\314\331\236\032\362W\245oGC\336q\347\331\236\275,Lg\314\010h\360=\332JV\335\247\326\207\302\230\357%\305\351uc\316)\263KX\314\323\032\016\023r\322\337\262Z\022Zz\220jv\241\350\363K\243\216\325\375\363*y\326U-)\371\277\340\'\360\002\254\302_\271\027k\331\027th\220\223\026\204\251\211\333,\035\311\005\352\t\355K\2635\014GY\320\246\244\023x\020\251.\350\274\nK[x*i\205y,B\307\272\336(-\257\257V\316\257X\177\202@\246\235O<\034\346\017\3427\354\325\266\320KS\033o}\261\257H\222\347\322\305\323\317\326\254\323\327{\360\033\312\356\317ddb\226\300\3548\234\223\003\177?\253nwD\371w\350\221\331\256l\231\t\232)\177z\003P\223\336v\032\250\307\321*\351+\244\343\235\340\224\263ku\313\240+\314\351\316\350VF\302\033,\334\303\002\363\',vb\326\333pY\217\216+\231\242\334\203d\276\260\347r\211=\315\235\227\301\025\203\"\300\001\321\343\030\273\3316\036\372\246\334\271\250\347u\214\215\205\365V\001\324\364\"_\323\277vh\302`\200\272\262\271W|\020p\016\234\371E\235\326\325\365\007\025R\246\022\253\204\300\r\216\320\240\\Q\3225\322\2021\031V\313:#M\276a\334\270GZ9\200p_\027\006\007\036,\035\314^\324B\000K\325@\264\021\027\367\365\275\213-\330\325\204\034O\230\271\261\267+\021 hz\007\022`\374\201\001\252\251DGG\260q\245\205\002\235!\\V\372\3422FjU\234\3442\207L\351\372<\307\333\340\325\226\207\244\260c\323\357\242\003\257 c\003\220\021\244XUC\033RZ\217\375\210G\013/b{\354@;\232\001\355\327*\300\001\315aL\262\315\373\265\343H\032\002\266\321\252qJ\261\350\037\2300xV\277\317E\0335\244\177xl(il\273\314\302\222\310\002\021X\374G\271~\3274\244:I\371\312\333\321P\272\tsF/S=l\376\217|D-\266\372u\016CA\316A[\230y\2558\262X\216\035\352\305?\251E\245\211lisA\306\340\021+\031\327-\265\240\322\203z\0106\276\001\244\rgt\277\324\021\004\362\262?\246\242\347\032\264\010\000\"\353\315%\331\372\207\363\313\371&b\340\313\254\304\206\"\341\200\270X\372\243\230\225y\\Sy%\210\214\032\325\261-X\247i\343Y+\207\222\356\'\031:\347\315\223X],\025j\2641\2572\300\001\254\27718K\n\324l\220\261\322\315E\323\346\037\005Fk\307\2162q\250Uk\234U\224\306@\255i4J\340\375\326|\360\215\351\322\203d\016\014\nl\277\353U\350(\371\377\265\216\206\355\201\ns|\\T\362\256\311\347\014\261\311r\024\352\005D\226\205\346}\330\244;\035,Ym#^?H*\370\332;\307rz{\312\365\255%\357Z-\331\021\335GX\326\373\311\201\306\030Fc\014\373\006/\322\326\250\026}32Jxq`\302\3463\016\026\016\246\360>\221\247\362\222\0217\275w\'\222$\026\370a\205\210\206\016\336\341`\322\235\270\022\r%\366\347\010\261\273\200\017\267\376a\"Z\370\213\366B\027\373\306y:\300\001Gt/\254\021\320\036\"\372\005\332\245\342bKR2}P\350\261\014]\032\024\261\265Z\277e}\357\033L:\201H&\365\314R3\330\342\304\315{\234\301\372\3250\301\220[T\002\243\007\000\255\rp\007q\361J\330p\033\373\212=\311}\212\332\210\220\246\206\306\306o\3761K\245O\204\353\216\210\236<2\237\337\345\24256\352\241\233\214\222lKC\334\261\266\377y%\024\266`\273\026D\303oR\207\326+Sv\tF\212\337\301\033\0070\213\266=c\354\200\0007\274_\017O\371f\300<l\317\030\032\001\004\'\"\201\365t\321-A\234F8\320\265Ac\314\242H\353\037s\010\014\324\323\254n\326\360\255\312\217B\300\001\204\002\272\202x\003_\303\2373\270x\257\330\253,\331\276B\245\236\351\024\375\007\336\364Wm\241\234\224\226\027\322\345\203-\2409\272#\340\2301\321\023\351E\344tE\305\3122\271z\355K\031\363U\253\3749\024\364F&\253\276\347li\366?\231\325\255_2B\250\267\021}/\306\302\016\240\3252\224\343\200P+ss\203\022U9\3461j\270\362jb\233\302\246^\352| \036j\367\033B\004\200#??1\233\243\275!\031\252 \252\340\2161;\377\367p\034Z\305\307\017(\000\261\352\005\257\013+\216\2576\313R\237v\337Y\222\354\303\247^\034\240\352\234x\244\221\002\010\016.R\332\336\'\3272\0179\251\214"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1300581528
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS384
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\002\030\200\030\"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS384
+        #     n: "\304\016^W\367\033\006Ku}F\213\347f>\3427A\262\307\010f\034\334\031]6\303\231\034\014\352\364Pb\336\232\254\355Q\334>\235\225\251\201\243\254\302\304I=0\3254 \320\022i\270\277\314\213\305\220\335\275\365\003qN+\3466\314\342\007\247\340\r\221\236\265\317U\320>\340\322\223U\004\343\"\205p\311\206\016\354\336^8\017s\324\2024\326\314\016\236\337\001M\245kt\202\010\2713\263\312V\337\216\316`-?\246\'\344i^J\230N(\3775\301\n<\221.\0330\270\000\037\037t*\273\332\347&\364\377\213\021L\322\242\215E\207\003\235\247`\322\207*\220\373\320\275\254\263\324\3302#x\376t\362Z\036\350\255\032p\367F\210J\367\023\202\344\211\021\036\026\233\310\205\n!C\266ce\311g\022\355\316\003\251W\340\220\2345\027\232\210sK}\234h\213;\n\227B\027<\2778v\233Z\033\030F\'\264\357}\201?\270\352?\243)\250!\232\006f\365\375\362\255Y\237D\232\013\000\232H\256\276=\027X\235X\203u\025\3245\014 \344\232\266\031\\\242\000\2652\217O\315BZ\\h\327J`\0163.\263CZ\336\305\322\001UV\274\301U\267\320Q\236\204}\307#\217e\341e)/\332\347\356{J\330&\247.\337\333\361<\367\026~\246\0339\326\026\266\004r\323\036#:\217\361\277\022_2B\262\313\364$7\255u"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\031\003QG\215C\362#e\357\257\364\260\366\323>\206\270\267\317\233Su\312\241\344\311\324\036 ]H\213\227\206\351\252\027\266fk\200D\230\037\312e\240\250\257\244\275m\021\302g\016L\317\026\256G\261,iMU\013!\237\216\316\334}\360\331D\213G3K\371\020\356\215\036\366\252\265\312\354\275\245!\326\036\373\343\001\333\370\t\322\302\253\3578\226w\262\014\212y\336\237r\243\2317n\327\374\017\353\330v\010\260\355E\320\231H\375#\361N\370\213\376@\2031\347\365\256\273\020f\200\217)iX\247\214\024\203I\255\327\275}\035MAF_P\270\246 @\342\3232[\321x\301\376:\352D\223\357\001$\213\264\220\311(\016\251#S\002\332\347\253S\260\247\240\241\306?\341@\361\007\265\2212\370\267\010\265\031\234\336\233e\344\261\376\362\2536\315\340\335\231#\264\227\317\213Cr\020g!\263\276#n,O\371j\223d\007G\354\370xGw\313\307_\220i\274\016\350\325\265\246\253\322:P\353\254Z=\347=\"D\306\270\036=|~\245&X\242\001\225a\337\361f\334\232!r\322,sb\017Q\352*\216{t\300?\254*\225\366\024~W\n\\Z\251\255\356\336u\321lV\003;A\277\222:\362\357H\207\206\301X\026\373\354$\260\313\274\024\221\201\350\336)\002~\036%\025K\363dG\250\036m[c\016\324\3004A9\242\310\311"
+        #   p: "\344\202\377i\322TP\3131\320vx\375\227|j6\352(?\326i\325TE\224\310\235\351\346\306|\353\321\327\336C\236|\233\007\342W\200\203\362\371\301\225\364\302\241Aw\337?\363}%\36248\332\265b\205x\0228\346\006V\366\2107`\304\016\313\026/\217\372\240yg\026\327\013v\300\375\343@[\360\323\274\375\316m\355>\310\340\000!\341\352( u\336\314X\236k\344\263J\234\237S\237E\2740\303\3713b9\272L\360m\316\274\303\363\010\254m\305@m\216\312\376\246h\362\274\261\354|Ve\0379\246\350F\n\307\302,\367\365\206\310\363\\4\334=,\265\n\214\314@\334/\251X\267-\255\271\324\331"
+        #   q: "\333\243\347E\004\200b=\203\32665\370\211\224F\010=\264@~\343byTp\366:;\017\276\312\333W\007)\331t^M\223\007\217\322\346\206\360G\210w\245<4\272\032Jd\241F\342S\374\326hc\r\265q\320\274\307\3320\022\3637\235g\307\265?,l\306R\271\336\373p\227G\375\307\331\216\014]\232\030\0315]Q\257\266\347\234\3664K\316\337e\n\005\035\013L\257j\233%\327\364\006\024\371\374H\260\303\311W\362\345m\033\320\315\222\003v\255s\017\020|.l<\242[~>\262\324Hv2\337\003\023\312\246\r\235\026$M\315\335Z\027\257\002\262\360\033\021>\231\325\243\333!\241\005\205\263\343\013\375"
+        #   dp: "Q\212\225M\323\252\026\262\213r\021\364\226\271i\302\025\032u\206b\"\235\347Vu\003)\246\177\2350\222\215\354\363\3719\"\311\307\204s(\376\354\303F\333\025\336%\340\320\277\257xY\236\"\321\223\300\'\235\242v\3709\251\213u\230\277~\323^\342\320\201x3n\201\271\020\232K\336a_\370\271\021\014=\341\260\274\370\336\255\034\014^2m\344\311\303\313\341\203\301s\226\004v5\254X;Z\201\251F\3008\016B\321\270;\314\234\266\362`\325\243q\342\345\300\317~\310\307\223\033\032\264\023\265\201\347\324\204}!\373wC\0271\265\016\212\344\350U\350\321lcr\310\363Irx\250di\365\212\034\303\206\014\233\361"
+        #   dq: "\322{\367D\337\364|\261\000\347\307\031\337\255P\347\360;\331\302\271\027\005\246\312rZ\265!&\214\253\360\231-\'@\332Q\t<n\321\270\305\354\350\325T\205|V\377\034\235\327O*s8\260N+\326\353\236\026\225\024\371\344\370F{\322\223(\210m\rW\226&C\304\301\225\\\026\341\023?\264F\333\347#\310iq\266\377P)\3719j\345R\346\227@\344\236>\232?\332\344|`dZ\330\342\37756=\037\020\361\242\024\007#\221\265\177[\177p:\366y\250M\251[V\234,\334\006\017\360y\344\264\t\235\364X\216\333\232\377\276\030\305\301k\366(d\210\300\252\337\322\303/\306\220X`\354^\336.&\255"
+        #   crt: "{~S %\216=}\001\331\350\355\305AWa\033\025\374t\367\246T\201\314\252\313p\006j\234\372\251\325#\212\014]\260\334\342\372\2559l\241\336\037pnA\331)\336\'\235E\263?\020\255\271\003\016\\w\340\220\372\374V\202\226i5\313\3361\367$\306\033{L?~\241\034\333J\245\2379\206{\037\370\263R\304\024\231\tU5\\a\325\215\350\031Y\320\372!\211\344\343\004\267LB(\344\r\306\264\265\366\036\302\333\317\230\003+\324\300;a\345\336&\001\"\304\347\362t2\r\034n\227q62A^\372\275\313\341(\023\010z\307\r\316\361c.o\254\014\344\020\254\321\303`dLQ\356d\263;\267\2739"
+        # }
+        value: "\022\212\003\020\002\032\200\003\304\016^W\367\033\006Ku}F\213\347f>\3427A\262\307\010f\034\334\031]6\303\231\034\014\352\364Pb\336\232\254\355Q\334>\235\225\251\201\243\254\302\304I=0\3254 \320\022i\270\277\314\213\305\220\335\275\365\003qN+\3466\314\342\007\247\340\r\221\236\265\317U\320>\340\322\223U\004\343\"\205p\311\206\016\354\336^8\017s\324\2024\326\314\016\236\337\001M\245kt\202\010\2713\263\312V\337\216\316`-?\246\'\344i^J\230N(\3775\301\n<\221.\0330\270\000\037\037t*\273\332\347&\364\377\213\021L\322\242\215E\207\003\235\247`\322\207*\220\373\320\275\254\263\324\3302#x\376t\362Z\036\350\255\032p\367F\210J\367\023\202\344\211\021\036\026\233\310\205\n!C\266ce\311g\022\355\316\003\251W\340\220\2345\027\232\210sK}\234h\213;\n\227B\027<\2778v\233Z\033\030F\'\264\357}\201?\270\352?\243)\250!\232\006f\365\375\362\255Y\237D\232\013\000\232H\256\276=\027X\235X\203u\025\3245\014 \344\232\266\031\\\242\000\2652\217O\315BZ\\h\327J`\0163.\263CZ\336\305\322\001UV\274\301U\267\320Q\236\204}\307#\217e\341e)/\332\347\356{J\330&\247.\337\333\361<\367\026~\246\0339\326\026\266\004r\323\036#:\217\361\277\022_2B\262\313\364$7\255u\"\003\001\000\001\032\200\003\031\003QG\215C\362#e\357\257\364\260\366\323>\206\270\267\317\233Su\312\241\344\311\324\036 ]H\213\227\206\351\252\027\266fk\200D\230\037\312e\240\250\257\244\275m\021\302g\016L\317\026\256G\261,iMU\013!\237\216\316\334}\360\331D\213G3K\371\020\356\215\036\366\252\265\312\354\275\245!\326\036\373\343\001\333\370\t\322\302\253\3578\226w\262\014\212y\336\237r\243\2317n\327\374\017\353\330v\010\260\355E\320\231H\375#\361N\370\213\376@\2031\347\365\256\273\020f\200\217)iX\247\214\024\203I\255\327\275}\035MAF_P\270\246 @\342\3232[\321x\301\376:\352D\223\357\001$\213\264\220\311(\016\251#S\002\332\347\253S\260\247\240\241\306?\341@\361\007\265\2212\370\267\010\265\031\234\336\233e\344\261\376\362\2536\315\340\335\231#\264\227\317\213Cr\020g!\263\276#n,O\371j\223d\007G\354\370xGw\313\307_\220i\274\016\350\325\265\246\253\322:P\353\254Z=\347=\"D\306\270\036=|~\245&X\242\001\225a\337\361f\334\232!r\322,sb\017Q\352*\216{t\300?\254*\225\366\024~W\n\\Z\251\255\356\336u\321lV\003;A\277\222:\362\357H\207\206\301X\026\373\354$\260\313\274\024\221\201\350\336)\002~\036%\025K\363dG\250\036m[c\016\324\3004A9\242\310\311\"\300\001\344\202\377i\322TP\3131\320vx\375\227|j6\352(?\326i\325TE\224\310\235\351\346\306|\353\321\327\336C\236|\233\007\342W\200\203\362\371\301\225\364\302\241Aw\337?\363}%\36248\332\265b\205x\0228\346\006V\366\2107`\304\016\313\026/\217\372\240yg\026\327\013v\300\375\343@[\360\323\274\375\316m\355>\310\340\000!\341\352( u\336\314X\236k\344\263J\234\237S\237E\2740\303\3713b9\272L\360m\316\274\303\363\010\254m\305@m\216\312\376\246h\362\274\261\354|Ve\0379\246\350F\n\307\302,\367\365\206\310\363\\4\334=,\265\n\214\314@\334/\251X\267-\255\271\324\331*\300\001\333\243\347E\004\200b=\203\32665\370\211\224F\010=\264@~\343byTp\366:;\017\276\312\333W\007)\331t^M\223\007\217\322\346\206\360G\210w\245<4\272\032Jd\241F\342S\374\326hc\r\265q\320\274\307\3320\022\3637\235g\307\265?,l\306R\271\336\373p\227G\375\307\331\216\014]\232\030\0315]Q\257\266\347\234\3664K\316\337e\n\005\035\013L\257j\233%\327\364\006\024\371\374H\260\303\311W\362\345m\033\320\315\222\003v\255s\017\020|.l<\242[~>\262\324Hv2\337\003\023\312\246\r\235\026$M\315\335Z\027\257\002\262\360\033\021>\231\325\243\333!\241\005\205\263\343\013\3752\300\001Q\212\225M\323\252\026\262\213r\021\364\226\271i\302\025\032u\206b\"\235\347Vu\003)\246\177\2350\222\215\354\363\3719\"\311\307\204s(\376\354\303F\333\025\336%\340\320\277\257xY\236\"\321\223\300\'\235\242v\3709\251\213u\230\277~\323^\342\320\201x3n\201\271\020\232K\336a_\370\271\021\014=\341\260\274\370\336\255\034\014^2m\344\311\303\313\341\203\301s\226\004v5\254X;Z\201\251F\3008\016B\321\270;\314\234\266\362`\325\243q\342\345\300\317~\310\307\223\033\032\264\023\265\201\347\324\204}!\373wC\0271\265\016\212\344\350U\350\321lcr\310\363Irx\250di\365\212\034\303\206\014\233\361:\300\001\322{\367D\337\364|\261\000\347\307\031\337\255P\347\360;\331\302\271\027\005\246\312rZ\265!&\214\253\360\231-\'@\332Q\t<n\321\270\305\354\350\325T\205|V\377\034\235\327O*s8\260N+\326\353\236\026\225\024\371\344\370F{\322\223(\210m\rW\226&C\304\301\225\\\026\341\023?\264F\333\347#\310iq\266\377P)\3719j\345R\346\227@\344\236>\232?\332\344|`dZ\330\342\37756=\037\020\361\242\024\007#\221\265\177[\177p:\366y\250M\251[V\234,\334\006\017\360y\344\264\t\235\364X\216\333\232\377\276\030\305\301k\366(d\210\300\252\337\322\303/\306\220X`\354^\336.&\255B\300\001{~S %\216=}\001\331\350\355\305AWa\033\025\374t\367\246T\201\314\252\313p\006j\234\372\251\325#\212\014]\260\334\342\372\2559l\241\336\037pnA\331)\336\'\235E\263?\020\255\271\003\016\\w\340\220\372\374V\202\226i5\313\3361\367$\306\033{L?~\241\034\333J\245\2379\206{\037\370\263R\304\024\231\tU5\\a\325\215\350\031Y\320\372!\211\344\343\004\267LB(\344\r\306\264\265\366\036\302\333\317\230\003+\324\300;a\345\336&\001\"\304\347\362t2\r\034n\227q62A^\372\275\313\341(\023\010z\307\r\316\361c.o\254\014\344\020\254\321\303`dLQ\356d\263;\267\2739"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 1989890488
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS384
+      #   modulus_size_in_bits: 3072
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\002\030\200\030\"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS384
+        #     n: "\317\0221\030T\254z\032\2474\177\360\343\264\234#h\305F\016\357\262xj\243\344\377/;\005d\177\340\374n\267\\f\034o\036m0\217f!6W\033E\306\301=\227m\221\323o\r\225\312\004\241\221E\033\327,\027\030\006\360\255\376\023l\227\257M\204\202\001m\3761v\224\217\2632a\342!]L8]\t$\317\366\025\355\3762PCG\267\204\214V^\317\000\260e\034A\\?h\271?N\r\327\006\201A\000\032\334\251*\002\237B\375l\025\276\001\373&\306\216\0104\020\301&\211\221\021\\\230\232#+\030q\234\3129\202\304\036z.c\203\000\355\3254\313l\265\022\t\252\251i\263Ys\236r\252\304 J\320\367qW\005\301\276g\033 \227\311\340.@Qb\370JP\223\253z\246\347\337\2620\021a\201\312[\'d\301\006\360\351\nh\207N\260\203)\267\212\373\374\317\317s\204\313\033\242\'\327\206\360L!\252\266\351\254H\206V\323\355{\377!\025\232\275a\023\2779K\023b\222Ru\307&{wq\343\244\331[\001\236\354\260\243\n\334\2659\267M\267-\356}m\340\264/O\201l\270\331\265G\373\006\341\365\372_\347\016\240U\237\224r\241\226\247\237\376\234n\310\2165\211\330\250\370\035\317\010K\2738p\2160\306\014PZ\305\270\373\301\331\\Z\257\206\253\214.\314\344l\204\226Fp\2010\222\215\035\r\010J\335"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\005\344\271g}\037t\2216!\221#\240\303v\357.\017\361L\303\0074\024\252\351\375VQ\212w\200\t=\376\003\263zG\204e\034\013\366\004\334\334\240Q\213$\233B\336\300\264V\rr\330H\311\345tN@z\331\013&8\252Xuc\005\243\362\216\024\377\243=Go\016v/y\324\355\002\303\332\325,\304\177\032`]\333el\346\3570_\005\352\334\230\247\265\242\n#2\220\354\250tYSc,\023*\340\372a\337\241\330\241\264\265\3460\362\306\322\243\331KR\372\337be\370\255Y\\z\337E\013\234\005!\272\270\020\354^\233\250\327f\343\027\253*\331\266\3314T\231\371\022\365\243\347\302\033\277\216yl@\006\355|2\315H+\027V\266\257\206\025\214e%\023n\366X\003\256\035\327\254j\367\316\274\326H\037kt\220\024n\263PA\276\023\363cZ\374{M\177\256\337T\331eRA\017\227\327m\253\365\275*\340\246B\210\3421\365\300\362}\303\225\363U\202\370Q\'\311\265\326\007,\031+\225\241\203\342\222\310.\265\244\213n\241\313\321\004\262\231\257Z\307\313c\"\000Lv3\025\220L\300\371\237\277\322\245\344@\000\3312[\rk\240\200\017\207\202\205\270\352Va\337\372\227\345!\367i\244\332.\263\247\273\304B3\251\267\372\311\255\267~\347W\306\3638N\265l\030\335\034\205#?\375\240i\370\001/\346Ay%\220!"
+        #   p: "\352>\275\200\207\332\010\033)/D\203Jw1yAn/\3367\246\222\nK\\\254\326@\275\361\250\005\321\355f]\245\031\337uI\032x;\346_\343\010\201\220fo\345\343\205\242*\247\376\345\233y\365\337K\034i\272\375\006\"\342\221\020\345\217{i\325\006\025\275\0052\271e{!\300\245\233\263\266\002\224#\230%\030\365\013\262}\255&\346\325\304}\035\350\010\310qdo\324\"GG\264\212\251\246\277\242NT\301\240L\373\200\244\020\212\200\256}\354>\337\360I\317\331\204\321\325\213\243\250[\214E\231O\343\2516\374E;W[\206l\362j\314\361\366@\227R\341\335\356\345\027\327\025k\330\260\313O\232F\351\375"
+        #   q: "\342Ma0/\216}\346a\3518+\213\227\367\316m\271\227d\332\263s\330IV\316\330\260\221+V4\253S\335\2146\267J\227v\270\n\227\241\377\231mA\305\324\372`\370D\221\253\203X\035\033(\345+\375eF\221\363#\301\203R\255\303\376&k\345\313\263Q)\016\253\370\254\260\034s\002\213g\307\276\216\255\364\2155\312:I\030O\006V\022\226\212\361\373\036|\247\t\007&\003\206@\033\306\214&\337nc2oFg\336\361\003\005\251\363R\240\273\324vxr\221m\177\356d\006\246y[\226\262\377\231\261\200\246\235\263\263\3359\037\277P[#\364\004d\312)\030n\331\361\231+2\001\253jA\244(\312a"
+        #   dp: "\3133\352\010Y\321V>\331\227[\203\331QT\345\257P\372\315v\321T\327\001Q?\316\r\023\271R\334\331\243k\255\310\317\214\375\235\221\014\304I\260\334\256@W\027\305\362SQU\300SC\327\302\354%\322\262\301\317&\232\340{\332\267\245D\277\027|S\203\227\320\005v_F\035n\306C\343\262\036.V|\032;\364N~\3733\362\305Pr\030\245\301\017\314\256\265O?\203r\311\n\367\037b\324xa@:^M\304\372X9\372\357ky\363Z\301\240Z>m\304pg\347+\337\250\350\341\342n\200\323)\263\275\\\001\202b\257VKeB\254\223\306\254\177\005\331\024\250?\352!w\020\337-?\274\301\317\261"
+        #   dq: "\006\n\024\236\351\225\376\354(\016\306M\3732\272\rKFY\260\372Y1\255B\377\313\023cZ\354r\'\203\257\000I\236\2503\230i5\256*\257\313\027x\037\372<\235\325\372\2521\315\006\373R\003\210\212\335*\255\n>\030\344\ne\013\316\321]5\377I)\257\206i\367\027E\322\255\374\204a<\0010\006x\336Q\231w\'\007y-\351\345\374d\020\232r\254\305\372\203\246\255\007)\256\352\205\023>hG,5\377\201\222\272p\353\263\255\362\004)_Z\303\347\311}\035>s\334\223\324\315\232\215\204/\020\2754L\177\245\323\306\350\017\1776f\331\227\323\nN\372J0y\274\335B\2761^t\t\250\350\315\241\341"
+        #   crt: "s\022fO<\322\347\334\366rY\252r\025\324\026\254s_\231\n\375\317\345\277`#\240\307,\314\307\002p\355\340\347A\303\250\332R\320 k\232\226vl\031-3|\365\260\001Q\325x8\003\357\242n\323\225 \346v\264p\360\352&\332iD\352\016\356\021N\275(_\215\263\234fA\3738\326?\257n\312|\256\211\232\223\023\'\361:\306\361\244\274\206\346$\016\037\274\370\310&\277J\201O\352\203P\023\315\262H\254N\313V\277 sO\230\010>!h\254\236L\334q\375B\037\224.\371\007\312\227t\357\3623\341\276\341\235\001\317\335\325\330\223\342\305\376j\335P\007\302\345j\217I\342\220\"\252(s\367\227k"
+        # }
+        value: "\022\212\003\020\002\032\200\003\317\0221\030T\254z\032\2474\177\360\343\264\234#h\305F\016\357\262xj\243\344\377/;\005d\177\340\374n\267\\f\034o\036m0\217f!6W\033E\306\301=\227m\221\323o\r\225\312\004\241\221E\033\327,\027\030\006\360\255\376\023l\227\257M\204\202\001m\3761v\224\217\2632a\342!]L8]\t$\317\366\025\355\3762PCG\267\204\214V^\317\000\260e\034A\\?h\271?N\r\327\006\201A\000\032\334\251*\002\237B\375l\025\276\001\373&\306\216\0104\020\301&\211\221\021\\\230\232#+\030q\234\3129\202\304\036z.c\203\000\355\3254\313l\265\022\t\252\251i\263Ys\236r\252\304 J\320\367qW\005\301\276g\033 \227\311\340.@Qb\370JP\223\253z\246\347\337\2620\021a\201\312[\'d\301\006\360\351\nh\207N\260\203)\267\212\373\374\317\317s\204\313\033\242\'\327\206\360L!\252\266\351\254H\206V\323\355{\377!\025\232\275a\023\2779K\023b\222Ru\307&{wq\343\244\331[\001\236\354\260\243\n\334\2659\267M\267-\356}m\340\264/O\201l\270\331\265G\373\006\341\365\372_\347\016\240U\237\224r\241\226\247\237\376\234n\310\2165\211\330\250\370\035\317\010K\2738p\2160\306\014PZ\305\270\373\301\331\\Z\257\206\253\214.\314\344l\204\226Fp\2010\222\215\035\r\010J\335\"\003\001\000\001\032\200\003\005\344\271g}\037t\2216!\221#\240\303v\357.\017\361L\303\0074\024\252\351\375VQ\212w\200\t=\376\003\263zG\204e\034\013\366\004\334\334\240Q\213$\233B\336\300\264V\rr\330H\311\345tN@z\331\013&8\252Xuc\005\243\362\216\024\377\243=Go\016v/y\324\355\002\303\332\325,\304\177\032`]\333el\346\3570_\005\352\334\230\247\265\242\n#2\220\354\250tYSc,\023*\340\372a\337\241\330\241\264\265\3460\362\306\322\243\331KR\372\337be\370\255Y\\z\337E\013\234\005!\272\270\020\354^\233\250\327f\343\027\253*\331\266\3314T\231\371\022\365\243\347\302\033\277\216yl@\006\355|2\315H+\027V\266\257\206\025\214e%\023n\366X\003\256\035\327\254j\367\316\274\326H\037kt\220\024n\263PA\276\023\363cZ\374{M\177\256\337T\331eRA\017\227\327m\253\365\275*\340\246B\210\3421\365\300\362}\303\225\363U\202\370Q\'\311\265\326\007,\031+\225\241\203\342\222\310.\265\244\213n\241\313\321\004\262\231\257Z\307\313c\"\000Lv3\025\220L\300\371\237\277\322\245\344@\000\3312[\rk\240\200\017\207\202\205\270\352Va\337\372\227\345!\367i\244\332.\263\247\273\304B3\251\267\372\311\255\267~\347W\306\3638N\265l\030\335\034\205#?\375\240i\370\001/\346Ay%\220!\"\300\001\352>\275\200\207\332\010\033)/D\203Jw1yAn/\3367\246\222\nK\\\254\326@\275\361\250\005\321\355f]\245\031\337uI\032x;\346_\343\010\201\220fo\345\343\205\242*\247\376\345\233y\365\337K\034i\272\375\006\"\342\221\020\345\217{i\325\006\025\275\0052\271e{!\300\245\233\263\266\002\224#\230%\030\365\013\262}\255&\346\325\304}\035\350\010\310qdo\324\"GG\264\212\251\246\277\242NT\301\240L\373\200\244\020\212\200\256}\354>\337\360I\317\331\204\321\325\213\243\250[\214E\231O\343\2516\374E;W[\206l\362j\314\361\366@\227R\341\335\356\345\027\327\025k\330\260\313O\232F\351\375*\300\001\342Ma0/\216}\346a\3518+\213\227\367\316m\271\227d\332\263s\330IV\316\330\260\221+V4\253S\335\2146\267J\227v\270\n\227\241\377\231mA\305\324\372`\370D\221\253\203X\035\033(\345+\375eF\221\363#\301\203R\255\303\376&k\345\313\263Q)\016\253\370\254\260\034s\002\213g\307\276\216\255\364\2155\312:I\030O\006V\022\226\212\361\373\036|\247\t\007&\003\206@\033\306\214&\337nc2oFg\336\361\003\005\251\363R\240\273\324vxr\221m\177\356d\006\246y[\226\262\377\231\261\200\246\235\263\263\3359\037\277P[#\364\004d\312)\030n\331\361\231+2\001\253jA\244(\312a2\300\001\3133\352\010Y\321V>\331\227[\203\331QT\345\257P\372\315v\321T\327\001Q?\316\r\023\271R\334\331\243k\255\310\317\214\375\235\221\014\304I\260\334\256@W\027\305\362SQU\300SC\327\302\354%\322\262\301\317&\232\340{\332\267\245D\277\027|S\203\227\320\005v_F\035n\306C\343\262\036.V|\032;\364N~\3733\362\305Pr\030\245\301\017\314\256\265O?\203r\311\n\367\037b\324xa@:^M\304\372X9\372\357ky\363Z\301\240Z>m\304pg\347+\337\250\350\341\342n\200\323)\263\275\\\001\202b\257VKeB\254\223\306\254\177\005\331\024\250?\352!w\020\337-?\274\301\317\261:\300\001\006\n\024\236\351\225\376\354(\016\306M\3732\272\rKFY\260\372Y1\255B\377\313\023cZ\354r\'\203\257\000I\236\2503\230i5\256*\257\313\027x\037\372<\235\325\372\2521\315\006\373R\003\210\212\335*\255\n>\030\344\ne\013\316\321]5\377I)\257\206i\367\027E\322\255\374\204a<\0010\006x\336Q\231w\'\007y-\351\345\374d\020\232r\254\305\372\203\246\255\007)\256\352\205\023>hG,5\377\201\222\272p\353\263\255\362\004)_Z\303\347\311}\035>s\334\223\324\315\232\215\204/\020\2754L\177\245\323\306\350\017\1776f\331\227\323\nN\372J0y\274\335B\2761^t\t\250\350\315\241\341B\300\001s\022fO<\322\347\334\366rY\252r\025\324\026\254s_\231\n\375\317\345\277`#\240\307,\314\307\002p\355\340\347A\303\250\332R\320 k\232\226vl\031-3|\365\260\001Q\325x8\003\357\242n\323\225 \346v\264p\360\352&\332iD\352\016\356\021N\275(_\215\263\234fA\3738\326?\257n\312|\256\211\232\223\023\'\361:\306\361\244\274\206\346$\016\037\274\370\310&\277J\201O\352\203P\023\315\262H\254N\313V\277 sO\230\010>!h\254\236L\334q\375B\037\224.\371\007\312\227t\357\3623\341\276\341\235\001\317\335\325\330\223\342\305\376j\335P\007\302\345j\217I\342\220\"\252(s\367\227k"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 142783821
+      output_prefix_type: RAW""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS512
+      #   modulus_size_in_bits: 4096
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\003\030\200 \"\003\001\000\001"
+      output_prefix_type: TINK""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS512
+        #     n: "\266_\007\360G\251s\021\312\205N\240\037\217\240,E\205;\252\030wVj!\\g\373=\320\332Ut\362$\217\250\277@DC\363\033R\036K\377\033^\016~\245\224\370d|GT\204\333V\254\037\216l\253&,\226$\216\261\017\375Pa\305\030\3425N\337}V\231^R\312\356\203\227\367\232\250Z\303\306\353x\246\347\325m\232K\003\223s\271\373L\037`3\332\022\342\010\014\320\010a_\000\326\202\036\211\201\341\254\177{\332\2679qq\350\271S\274VU\005\343W\013\364<\030>%< \035\357M\206\256\262k\232\231x\214qa|\264p\241s\267\262\277\002\264\t\0034\002\320\324C\001L\360Y\272\315\321\314\330\315\377\234\255l\211u\362T\341\\\213\242y:\322\207E\340\203\346\273\0372\216\255P\260\r\341\350+\205\337\360\260\\@\224\260\320\271y\367\0268\300\356\204\366,?\200:\371n\212D\237\264-\351\\\226gJE34\007\266\261\n7;\344q\367[qNZ:\245\340/L=\000\314Y\317Z\341{\370\305\023\213\220|*\th\373Y\334#\264E0\024\221\302\355\373\315\231\212\322p.\357mF\314r\261\241F\034\311\247u\036\260.Xp\267^\317i\234m>X\275~\354\234\335\205\205\001)iS\013\200\3322\246(\007D\372d\226[C\374sD\347\037\021\233&\"\262\251)\"\203r]HnYx\\o\226\347\023:)\35220\035\362\204\264\231\372\277`|\030\214\205hN\033\231x,\266\216~\020F\271,\241}\304:\245\230.\316c\203Y\2323(_\014\260\\\021\002\350\205u\240\202:@\331\303\005\366\342\2013\375\036\213\266\340\266\362\354m\322\207U\215 \004\010\202e{\345q\014\255&\264~\203\t\276\256t\025Z\260Sr=\200]\220]@\224\306VIqw\"=lc\211/\372:\242\343"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\020R\026\332\310\377\000\036Bz\205\233\314\327D\245Q\237g\304\204&\001\344\303\221\212x\260\202\205W\212|\265\n\3526\033a@\227|\005pR\252\304\3378\372\332\204\247j\016\271/v(N\354^r\227\205\005\210\215\363=\225\330\361y\205\026gp\257\006\234\367\221\215n$\210\246\251\276*\331{a\214\r\274:\362\354\342k\311\230\026\374\326)y\264\311F\t\275\345\\\267\r\266|Y\202p\007\302cK9\005b3\222x\363V\277\261J\373r\343\362\361\356\361\366sJk*\\=f\321\024\374\272zh\035\355\205K`\277h\322)\237\330H\212\233\220\362\211\"e\200\024\342\177\237\317SIxd\377S\266\007\330\013/i\355\027>\202\000\252\331\3647F\326\004\216\234\256\2036*\252\352R\322\363\355\271\243\264h\333\244Bs>\3052\276[\310\202 \311\035\002\3759\033K\020\220\252@\034\334h\241U\215\201b/Kr\026C\026\013\225\271\003\0054EH\004z\354\360\377\244:\310\313\223\310\250Y@\261\r\224\276[\006\\$\364K\033\024\3469:\377\004\201\352\001NO\002\217\253\373\252\330\257<\302\333j\210\334.\021\207\310edg\225\324\357\001\252\031\022A\250\301\346\037\247\013A\257E<\31741\353\350\313\033\226i\314\260\016\307\230;M?\275=\331\205[\303\254\351\343\030\2224\304\321\026\353\250\251\035\227\354g\3429x#2\037\246\216[\226j\003\371\00155\327\323\312\255\324\315\314\360\"s\351~\n+L!\261\010\310\233;\345\272\\OZ3\340\004p6r.X>\212\203\276\024V\217P\207\362\207c\000=\231\272J\010\225\333xN\177)0\313\266]Z\021\371\251Q\202\302wa\321\023\215\257\220y\353sOU\314F\026\335\034\342\344\350{\322\320b\321\036:\265\321\311\n\017q\216\374\376+\214\310\371U\360\"\021"
+        #   p: "\330\371\017\205\376\237\2539\313j\002d\025I\004!\240\030\244\313\274~\316\240\374\366\305\376l\2654!b\033Y%m\201\212\244\257~,\376V\2269^p\264\017\370o\306\264\201\375u\250$\367\021\223\262ECiC\217\354\036\345\305\234\242@c\255%\307\371!\267%\320\200\216C\036\352\215\276_I\272x\353|\305GCTs\030,6*T\356%\263\344\n\221\003\274]L\250x\2616\345\255\244\310\261\177\t\236\3122\344n\271+K\223\204\274{\270\377\320\304\310\342$D\034\266\n0]\313\346\345\000\001\233t\177\266USa\275\257\016M\206:\252\210\023\365\255\030\013\316\240\n\210cA\216\276\3710\256\353},\207n#\200\014/8\033hLum?bIW\003\325\030\273\000J\320\030\272\337\366\366\236\257\000\200\304\327\304B\006\036K&\223\216\007\017#z\274\335\373\326AS\037!\370\372FL\246\'\032\206\253"
+        #   q: "\327,\25317\'\246%~\377\000,o[h\036\220.W[$\253\353l\335M7\250\361<\244\214\204\257\225@v\251\251t\341\340\240WD\t\361\250\370\213\006\315{#\037FW\013=\313\252\220ei\033h\373\361\016H\361V\206:\nw\371\267\035k.\226\333\201\274\317\322R\234\227 \032Q\307\014i\211\272\225[}\301\334\247\226k\242\256\360\377m>\325\250q\022\031\302\210\364\361H?\261\273V\017\302&\001\224\001\316\253\277\314\205R\"\343\340\360\254\252i$\t \004\345\355)\245\370\273\036l\356\256\341]\227\364\373}\331\227*\211\223t}\211\372\023\213\003\245p\233\r\247\003\202\224\002;\361<\205\226\326\0213\215\022ub\216\226\313\031(C\014\r0c\035<\270l\033\224\325[\220)\272\367l\211bH\347\307~<|\000\016\034l\253\336\177\353@\331\310Z\026S#\303\314 W\240\245Oz\007\0274\251"
+        #   dp: "}V\353k\311\232\255LV\300\347\236\257Y\035\304\332\372A\200\334d\030\263\221Nq47\3229\312+%\207K\002\306y\264\007\031\t\236\347\240\"\205\366\357]\346s\223\303D\360\211^W\364N8\027\034\302\003v\037\027\025\006A0\351\335\356v\340\303s\234\321sK\316g\340Hd\320\373/\212\244\351#\210Qb\210w\251\201FQ\010\005F\t\213\260\351\225\272%\277t\000B\260\204\371\013e\214\366T_\327V\021.:\364G\031\202\237\204N\250\241G\025\326\302$\227 J\334\371\361\305O\235\354r\001>\274d5HT\307A\360.B\243sM\346\321\336kl\256\261oD8\252on\314\377\035\2356\023Z,\372\337\006\252\344k\230\305\312t\020\320\223sk\350W?\367\274@\030y#\326C\303\211n\355\364H|\274sR^\261k=\371\277\372\013\366<\3725\\IKK\321\313\252~\256\341\032\006\277"
+        #   dq: "\211d\355^\374\237\345\2305\262/\363/\233\307-\024\325rT1cQmo\025\232?\326\341\351\024\014\271l\354\377\2300+\000\374\004\206\024\\\341!\n\253\003I\373\241\236\233\264\202\352\017}6\274zS\325BbV\303\276\316\207\340\321\304p\000;\331\257\243\006@\236Fy>\344\334\036\324\217y0\364\306\314\014\010\242\221B\013\004\242o\373\222\311\026\201\263\275\335\2158C\236^\001\037(\004\235\341C(\256I\375\256\365\277w5\351\277[\245\364\200\331O\352\330\346\266\272I\362\230\320p\265R\245-\367\036\322;pc\221\245y\277\340\236\366\030\037\035>\223Z]\256<c\210LVy\231\277\232\267\021z\021\360c\341er\354\336\255l\274\000\230g>\230\262\000\376?\250P(E\3404z`\367\022\'\333I@.\202\355\256#\376\336\251\211I\006\227:\016\225\276\350Xh\022\342\351\227\352m\004\005\2407r\031"
+        #   crt: "\327\364\334\331\233zO\270\320\343\002f\3443cRR\31154\365\326\215L\345\032\331\372\220;\330\361\341R(P\017\3759\327\233\006\230\207\006\010\265\273\264h\234\342\253\271\270\023\224S\034\336\255\317\234\264iDJq\316\306\364y\250\244\033\274Q\312v^dUZzS5S{\220x\370\t\317o+\325\352t\375\335\334\206\362\002\205\262\033\000\260\000\037f\257q\272\306\003\214\201^ %\336\333\026:\035\014@Si\202\370\347r\330\257\322f~4{5C9\234\302-\311,gG\013\270\256\224\256Y\300M\372\336\030\363\251.\352\2639\313\314\232\327;\003\332x\333F12\036\333\351\034o\351\272\304\325\311\023\031\255\210\257\273\370\376\227\366\253+\331XS\256\342u\020\250%rQ\035~\243\372\256n\021\327\270_i\217\032\365\247s\344\375\007[\013q\346f\324\036~\000xb\327\310\331\323\256\243\220\327\020\364i\020"
+        # }
+        value: "\022\212\004\020\003\032\200\004\266_\007\360G\251s\021\312\205N\240\037\217\240,E\205;\252\030wVj!\\g\373=\320\332Ut\362$\217\250\277@DC\363\033R\036K\377\033^\016~\245\224\370d|GT\204\333V\254\037\216l\253&,\226$\216\261\017\375Pa\305\030\3425N\337}V\231^R\312\356\203\227\367\232\250Z\303\306\353x\246\347\325m\232K\003\223s\271\373L\037`3\332\022\342\010\014\320\010a_\000\326\202\036\211\201\341\254\177{\332\2679qq\350\271S\274VU\005\343W\013\364<\030>%< \035\357M\206\256\262k\232\231x\214qa|\264p\241s\267\262\277\002\264\t\0034\002\320\324C\001L\360Y\272\315\321\314\330\315\377\234\255l\211u\362T\341\\\213\242y:\322\207E\340\203\346\273\0372\216\255P\260\r\341\350+\205\337\360\260\\@\224\260\320\271y\367\0268\300\356\204\366,?\200:\371n\212D\237\264-\351\\\226gJE34\007\266\261\n7;\344q\367[qNZ:\245\340/L=\000\314Y\317Z\341{\370\305\023\213\220|*\th\373Y\334#\264E0\024\221\302\355\373\315\231\212\322p.\357mF\314r\261\241F\034\311\247u\036\260.Xp\267^\317i\234m>X\275~\354\234\335\205\205\001)iS\013\200\3322\246(\007D\372d\226[C\374sD\347\037\021\233&\"\262\251)\"\203r]HnYx\\o\226\347\023:)\35220\035\362\204\264\231\372\277`|\030\214\205hN\033\231x,\266\216~\020F\271,\241}\304:\245\230.\316c\203Y\2323(_\014\260\\\021\002\350\205u\240\202:@\331\303\005\366\342\2013\375\036\213\266\340\266\362\354m\322\207U\215 \004\010\202e{\345q\014\255&\264~\203\t\276\256t\025Z\260Sr=\200]\220]@\224\306VIqw\"=lc\211/\372:\242\343\"\003\001\000\001\032\200\004\020R\026\332\310\377\000\036Bz\205\233\314\327D\245Q\237g\304\204&\001\344\303\221\212x\260\202\205W\212|\265\n\3526\033a@\227|\005pR\252\304\3378\372\332\204\247j\016\271/v(N\354^r\227\205\005\210\215\363=\225\330\361y\205\026gp\257\006\234\367\221\215n$\210\246\251\276*\331{a\214\r\274:\362\354\342k\311\230\026\374\326)y\264\311F\t\275\345\\\267\r\266|Y\202p\007\302cK9\005b3\222x\363V\277\261J\373r\343\362\361\356\361\366sJk*\\=f\321\024\374\272zh\035\355\205K`\277h\322)\237\330H\212\233\220\362\211\"e\200\024\342\177\237\317SIxd\377S\266\007\330\013/i\355\027>\202\000\252\331\3647F\326\004\216\234\256\2036*\252\352R\322\363\355\271\243\264h\333\244Bs>\3052\276[\310\202 \311\035\002\3759\033K\020\220\252@\034\334h\241U\215\201b/Kr\026C\026\013\225\271\003\0054EH\004z\354\360\377\244:\310\313\223\310\250Y@\261\r\224\276[\006\\$\364K\033\024\3469:\377\004\201\352\001NO\002\217\253\373\252\330\257<\302\333j\210\334.\021\207\310edg\225\324\357\001\252\031\022A\250\301\346\037\247\013A\257E<\31741\353\350\313\033\226i\314\260\016\307\230;M?\275=\331\205[\303\254\351\343\030\2224\304\321\026\353\250\251\035\227\354g\3429x#2\037\246\216[\226j\003\371\00155\327\323\312\255\324\315\314\360\"s\351~\n+L!\261\010\310\233;\345\272\\OZ3\340\004p6r.X>\212\203\276\024V\217P\207\362\207c\000=\231\272J\010\225\333xN\177)0\313\266]Z\021\371\251Q\202\302wa\321\023\215\257\220y\353sOU\314F\026\335\034\342\344\350{\322\320b\321\036:\265\321\311\n\017q\216\374\376+\214\310\371U\360\"\021\"\200\002\330\371\017\205\376\237\2539\313j\002d\025I\004!\240\030\244\313\274~\316\240\374\366\305\376l\2654!b\033Y%m\201\212\244\257~,\376V\2269^p\264\017\370o\306\264\201\375u\250$\367\021\223\262ECiC\217\354\036\345\305\234\242@c\255%\307\371!\267%\320\200\216C\036\352\215\276_I\272x\353|\305GCTs\030,6*T\356%\263\344\n\221\003\274]L\250x\2616\345\255\244\310\261\177\t\236\3122\344n\271+K\223\204\274{\270\377\320\304\310\342$D\034\266\n0]\313\346\345\000\001\233t\177\266USa\275\257\016M\206:\252\210\023\365\255\030\013\316\240\n\210cA\216\276\3710\256\353},\207n#\200\014/8\033hLum?bIW\003\325\030\273\000J\320\030\272\337\366\366\236\257\000\200\304\327\304B\006\036K&\223\216\007\017#z\274\335\373\326AS\037!\370\372FL\246\'\032\206\253*\200\002\327,\25317\'\246%~\377\000,o[h\036\220.W[$\253\353l\335M7\250\361<\244\214\204\257\225@v\251\251t\341\340\240WD\t\361\250\370\213\006\315{#\037FW\013=\313\252\220ei\033h\373\361\016H\361V\206:\nw\371\267\035k.\226\333\201\274\317\322R\234\227 \032Q\307\014i\211\272\225[}\301\334\247\226k\242\256\360\377m>\325\250q\022\031\302\210\364\361H?\261\273V\017\302&\001\224\001\316\253\277\314\205R\"\343\340\360\254\252i$\t \004\345\355)\245\370\273\036l\356\256\341]\227\364\373}\331\227*\211\223t}\211\372\023\213\003\245p\233\r\247\003\202\224\002;\361<\205\226\326\0213\215\022ub\216\226\313\031(C\014\r0c\035<\270l\033\224\325[\220)\272\367l\211bH\347\307~<|\000\016\034l\253\336\177\353@\331\310Z\026S#\303\314 W\240\245Oz\007\0274\2512\200\002}V\353k\311\232\255LV\300\347\236\257Y\035\304\332\372A\200\334d\030\263\221Nq47\3229\312+%\207K\002\306y\264\007\031\t\236\347\240\"\205\366\357]\346s\223\303D\360\211^W\364N8\027\034\302\003v\037\027\025\006A0\351\335\356v\340\303s\234\321sK\316g\340Hd\320\373/\212\244\351#\210Qb\210w\251\201FQ\010\005F\t\213\260\351\225\272%\277t\000B\260\204\371\013e\214\366T_\327V\021.:\364G\031\202\237\204N\250\241G\025\326\302$\227 J\334\371\361\305O\235\354r\001>\274d5HT\307A\360.B\243sM\346\321\336kl\256\261oD8\252on\314\377\035\2356\023Z,\372\337\006\252\344k\230\305\312t\020\320\223sk\350W?\367\274@\030y#\326C\303\211n\355\364H|\274sR^\261k=\371\277\372\013\366<\3725\\IKK\321\313\252~\256\341\032\006\277:\200\002\211d\355^\374\237\345\2305\262/\363/\233\307-\024\325rT1cQmo\025\232?\326\341\351\024\014\271l\354\377\2300+\000\374\004\206\024\\\341!\n\253\003I\373\241\236\233\264\202\352\017}6\274zS\325BbV\303\276\316\207\340\321\304p\000;\331\257\243\006@\236Fy>\344\334\036\324\217y0\364\306\314\014\010\242\221B\013\004\242o\373\222\311\026\201\263\275\335\2158C\236^\001\037(\004\235\341C(\256I\375\256\365\277w5\351\277[\245\364\200\331O\352\330\346\266\272I\362\230\320p\265R\245-\367\036\322;pc\221\245y\277\340\236\366\030\037\035>\223Z]\256<c\210LVy\231\277\232\267\021z\021\360c\341er\354\336\255l\274\000\230g>\230\262\000\376?\250P(E\3404z`\367\022\'\333I@.\202\355\256#\376\336\251\211I\006\227:\016\225\276\350Xh\022\342\351\227\352m\004\005\2407r\031B\200\002\327\364\334\331\233zO\270\320\343\002f\3443cRR\31154\365\326\215L\345\032\331\372\220;\330\361\341R(P\017\3759\327\233\006\230\207\006\010\265\273\264h\234\342\253\271\270\023\224S\034\336\255\317\234\264iDJq\316\306\364y\250\244\033\274Q\312v^dUZzS5S{\220x\370\t\317o+\325\352t\375\335\334\206\362\002\205\262\033\000\260\000\037f\257q\272\306\003\214\201^ %\336\333\026:\035\014@Si\202\370\347r\330\257\322f~4{5C9\234\302-\311,gG\013\270\256\224\256Y\300M\372\336\030\363\251.\352\2639\313\314\232\327;\003\332x\333F12\036\333\351\034o\351\272\304\325\311\023\031\255\210\257\273\370\376\227\366\253+\331XS\256\342u\020\250%rQ\035~\243\372\256n\021\327\270_i\217\032\365\247s\344\375\007[\013q\346f\324\036~\000xb\327\310\331\323\256\243\220\327\020\364i\020"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 957278677
+      output_prefix_type: TINK""")
+db.add_key(
+    template=r"""
+      type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+      # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssKeyFormat] {
+      #   version: 0
+      #   algorithm: PS512
+      #   modulus_size_in_bits: 4096
+      #   public_exponent: "\001\000\001"
+      # }
+      value: "\020\003\030\200 \"\003\001\000\001"
+      output_prefix_type: RAW""",
+    key=r"""
+      key_data {
+        type_url: "type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey"
+        # value: [type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPrivateKey] {
+        #   version: 0
+        #   public_key {
+        #     version: 0
+        #     algorithm: PS512
+        #     n: "\255\357\364\353\321\247\023\027(\340\2505Jt\356\006;5\261*\264\271\277PMc<\320\311\331\\7\203\325\n%]\217\332\027\274B\200#\031\200\0316\303\250_\377\354\356\352\366\371\351Sz4\201\220\354U\215\n\246\203\023\347;\007\360\313lk\255\307|J\337;\n\033\231\324G\t#h\005I\247X\237l\016\251\351\277)\217^O\215\236\031\233\231K\305\023\3700\353\3661\304u\370]\227o\327\3730\354\247N1\340\261FgW%\341\246?f\177\207{\000\244\330\217\305\317\316\321\024\027\022\246+!\222\2235\177S\007\371\247\207[\2236\307\230\017OA\344\372\372\n\017CW\205\320Dl\244.\210\340\267\3560*P\247\206\224\260\302\361\020\357\331\033\340v%;]sa\306\332\213#=\303$\210\360\214\377\217\332\332Eyf\257\353\364*\344\300\327>\366C\223s._\310\226\234A\334\350\030;\223H\264Z\347\216\352K\020<b\270\306\260#\203\257=\037|\320m\271\375\244/.^:\263-\356y\004z\306\355\032\031Jg\217\356$\325\261\257T&/\357\\C!\227>x\265\313\231\355\235o\345\0221$\213\352w\324R\342\327\213\256\251h\251\237f\036\224\314XG\266\016l~\277\200\030(\335:\327#\013<?.\303\223f\352\223Y\364\246o\246\351\243}\320\344T#\207\007k\242\203ws\343@\005\246\261\005l`,s.\201\330\375L\210\273\005\'\r=\272J\253\204\013\036|f\351\263\270\243\315.\002\242B\202\032s\374\223\270\363t\035\255\376E\362P\n;Aa\"F\220&%\375\321Y\214\227\245|{\010\241s\023\326\352\035\227bb6\224E\004\270;\246S].\021s+\343\352\315k\335\"\360\326\223P\221\303\261\347_&\321\371R$\n:\204R\214\\d\256&w\034\2163^\"\211\023(\021dp\242\201"
+        #     e: "\001\000\001"
+        #     custom_kid {
+        #       value: ""
+        #     }
+        #   }
+        #   d: "\005\254f\\l\037;S\330e(\342\026\306\244\363\226j)\230:\002\341\006\371/;\316\354\366\221\r\"\203]l[A>G\215\352\314\013\331\013k\234t\257VJI\026\200\232\331\013O\237i\322\\\026\311\\\353_\003\266\332&Q\033\247\354Q\025\177\004\227\333F-c9\300\364|6\'\306\034a@\001\03363;\363\234\243\027\206\302\303s\024a\340\255\321\371C|\22405a\365\010K%\351\033\354\021\234\014\304\220d\002\221\213\257)\242\274>\001\372:Q\340\016\241\237=\323\002&/Fz\310g_\232;\241{\344\331\010\324\337\306\"\200J)\035\255\2649W\1777\326;|E\217%\315^}\337\316\225d\256\314\250\257\216\304\013\347\304d\251\021A`\271*\326\341*A\2419C\362\002\036{\364\377_\n\243/,\032\364\240-\010n\324g\374\210\n7\251d\225fQ\300O\342z\026{\366\210,S\355\257\030\035h\354\214\246;\002N\317m\356\231\263\226\216\\\2733\231\r\3337\317&\3565\367\254\360\363\226\374s\0371\207\270\213\006Z\255\200a\226Z\373}\363\305\247\324Y\267h\267\247\tW\352\270T\033\207\374Up\355w[\271b\022h\340\264\310\377Ul\t\0141\344\355{\263\305\265{U\256\033\340\374\223u\337\270`\200\271X\337\030\261\0235\221\006\317\314k\370r\227Q\247\367\022V\225cz\365\325\037^\253\230\330\233qDy\200E\304\n\300\356\002\356\223\200\217\225\177\302-\337\346\247\022\303\242\375\216|\214\340E\021\023|\314\3074)\033\271\0343\300\006\255\344~5\353\244\226\'\362;P<\202;\221n\377\303\017pnE\320Z\254)fj\217\025\205p\313bK\214}\001\366\314\027j\014%90\034\345\033\034\272\002\250\217\235F\"M\020-0\221\2640zp\362T>\345>,\232\035:\345;\361\253\243}"
+        #   p: "\331\331\236\331q\031C\374\354\025K\234LF\225\205E\025S\364\206N\024\356#O\010\243\010\341\037,\316o\210M\364l}zB\3635\021\333d\313a+\037\234p\330 SN\210ZC\200Jk7B\364+v\267m\243J\2364\246\324\237\255=\264\337/q\036\"\244\330\212\202D\037\204=\372>\211\231\204k\226\265\371>\032.\310\221W\361\373\324\226\330\005\260\215\250f\333\262\022>\030\27180 P?\204\242\303L\251x\230\213\214\337\215\341[\001\335\013S]G\010\323\212\301S\377\333\331\3002\370V\237\203\315u\271\013\010\264\251\r\312N\031zl\037 \"E\261+C\275fg\375{\021\036\347<\300j\373\376\332-Oe\222?\315\211\274Z\301\326\325\311\340A\202.\301%\274\377\3135\357\026P9>\304+-\021\361E:\333\370\307\034\260\265\014-\261\327>\003#C7j\370\257\334Z\232g\177\336\262G"
+        #   q: "\314e\262\001\3256\223Wkw\216U\317\027\335\\\027\002\351r+\211\276\004\177\346K\205\340aX\217;\001\006\025m\034\246@qm<\014\306\365\342\342JX\020\232\373G\033v1\350\014\237\366\227\247\351\376\326\223h\223=\260E\375\026\250g\312b\223\371\321%h\336l\221\225\003)<\230\233 \330#\327\363\003\260.\tW\234\343\372Y\227\006(<\007\220\006\317p[\204\037\265\306i\377s\217O\325\200\3628|\260]\331/R.\221\267\023\326n0L\027\026\364\032\007h<;\241\253\301\352\376\332v\356\242\321\3212\326\237Z\314\301\203\035\265\010\035{\367\273\312|TX\002|\316t\006\335\210NI\301\265\002\037\325\306\327q%\372\213\336\r\332\036Q\006\330\373 \\2\350x-\323RW_\364l&\016\267b\312\3662}\333$\250G\"\350\216\336\262Bv.\346*s2\307X\205\252\373\00010\324\210`\367"
+        #   dp: "\277\216\177\002^S\304\027\263\023\352\237m\346\246\365?\320\3108\374\233\316\005\264 {\314\2703\023\361\22785W\256\264s\005=\267\223\223\267\"\003?\326\346}\356/\355\004\263\312k\354l\363\203!]\213Lh\240T\024\300\271*=\004;\277\336\301\233z\240\361\311\256\300\005\323\023\311\316\303\234\266\276\321\261\301\325\r\0003\336\\\002\332\342\305\022\026U\351o\337V@\234\351S\304|\273\312\252 \217Y`\360\345\031\353\340\034\375\255\277\332\227lpT\314$xRM\362\361\350Q\244\346\027Pz\223\227\020\274D\35664\327\001\371\261\251A\231\271\007\217\3512\235P@\300uY&\242\014\036\331\350\035\226\252\357\247\323\351\316\353\300\271\177\306\340\3651\254\276\226\320c\223\326\277\373\360\216\'2m\243e0w\225\274r\346\267\237:\253\315\337\014^*\367\227\324\332K\024\3517i\224bHx\202\277\\\203\343\311Sc"
+        #   dq: "6\032\030\362\371?\267\342d\324\301}\037\022\337\t\330!r/A,\365y\365l\274\302\323\360\220ca\'\3257h\2513\254M\031\257\322e\373\201\261\212\"#K\022\205\360\323\351C\325\313\245\226\340\340D\017\001\367\2459\247\345\356\246\307\256\251XLx\314\216Mb\312t\002pR\302\362U\345l\260\230!\177\361kN\027\275\264nx\266K\357\354k\310\010\265q\265\027\224\331%\212\273\016\030y\3666jf\337BC\245\020,}\235\036K\206\357\245\243\016]\212\0015\302\266f\306US?\343sj\025\242MMEZ\237\325\205\353\026\314\2175u\260q\223hv^P\351_\245\271\372s:\370\025\367\312\364\243\376\216\342I\325\326\252\234\375\'\245\354\246\226Zjg]&AaG\325<\301nN\016\010\311+\3023F\214\037\254eJg\010\r\201\016\310\036\306-0AK\310\216:\333V\022\347\362W\337=c"
+        #   crt: "\324\221\356^!\357\316Y\255\353\233\211gDj\260%\377\357{U1\251\260\336\306|\212\316\234\340\226<H\200jS\036\304\217\342\010\377\023\232\027\320\235\317(|H\030ut\354\211\364Zx\270\374\326\326\334O\016Y\2363\274<\244~m\303D\356\364<\034\317\226\273\236\331\035ci-\365N\025\246\251\020\263\211Q=\376>\326RK;\230hz\300\320\302ib\272\316l\356\001Q$\372\263Va\240l\222\313\341F\326\004\211\207\340\350\030/V\371\374S&\232\265^\3627\312\"e\007\034\215\230\225\2042oH\002\337\r~5.\000\204\t\241\034(c\2359\251\003U\256\241\221s$\266\345\263\234\006\310\240h\n\033\256\233\225\203\374I v}\237\251\277\241(\201\301\245*A\227gR\325\027[\340\377\253\362H\314\036\301\033\216\200P\353\221P=(\023,\303L\224K|\343h\235\354\354K\302D?\367\351\027a"
+        # }
+        value: "\022\212\004\020\003\032\200\004\255\357\364\353\321\247\023\027(\340\2505Jt\356\006;5\261*\264\271\277PMc<\320\311\331\\7\203\325\n%]\217\332\027\274B\200#\031\200\0316\303\250_\377\354\356\352\366\371\351Sz4\201\220\354U\215\n\246\203\023\347;\007\360\313lk\255\307|J\337;\n\033\231\324G\t#h\005I\247X\237l\016\251\351\277)\217^O\215\236\031\233\231K\305\023\3700\353\3661\304u\370]\227o\327\3730\354\247N1\340\261FgW%\341\246?f\177\207{\000\244\330\217\305\317\316\321\024\027\022\246+!\222\2235\177S\007\371\247\207[\2236\307\230\017OA\344\372\372\n\017CW\205\320Dl\244.\210\340\267\3560*P\247\206\224\260\302\361\020\357\331\033\340v%;]sa\306\332\213#=\303$\210\360\214\377\217\332\332Eyf\257\353\364*\344\300\327>\366C\223s._\310\226\234A\334\350\030;\223H\264Z\347\216\352K\020<b\270\306\260#\203\257=\037|\320m\271\375\244/.^:\263-\356y\004z\306\355\032\031Jg\217\356$\325\261\257T&/\357\\C!\227>x\265\313\231\355\235o\345\0221$\213\352w\324R\342\327\213\256\251h\251\237f\036\224\314XG\266\016l~\277\200\030(\335:\327#\013<?.\303\223f\352\223Y\364\246o\246\351\243}\320\344T#\207\007k\242\203ws\343@\005\246\261\005l`,s.\201\330\375L\210\273\005\'\r=\272J\253\204\013\036|f\351\263\270\243\315.\002\242B\202\032s\374\223\270\363t\035\255\376E\362P\n;Aa\"F\220&%\375\321Y\214\227\245|{\010\241s\023\326\352\035\227bb6\224E\004\270;\246S].\021s+\343\352\315k\335\"\360\326\223P\221\303\261\347_&\321\371R$\n:\204R\214\\d\256&w\034\2163^\"\211\023(\021dp\242\201\"\003\001\000\001\032\200\004\005\254f\\l\037;S\330e(\342\026\306\244\363\226j)\230:\002\341\006\371/;\316\354\366\221\r\"\203]l[A>G\215\352\314\013\331\013k\234t\257VJI\026\200\232\331\013O\237i\322\\\026\311\\\353_\003\266\332&Q\033\247\354Q\025\177\004\227\333F-c9\300\364|6\'\306\034a@\001\03363;\363\234\243\027\206\302\303s\024a\340\255\321\371C|\22405a\365\010K%\351\033\354\021\234\014\304\220d\002\221\213\257)\242\274>\001\372:Q\340\016\241\237=\323\002&/Fz\310g_\232;\241{\344\331\010\324\337\306\"\200J)\035\255\2649W\1777\326;|E\217%\315^}\337\316\225d\256\314\250\257\216\304\013\347\304d\251\021A`\271*\326\341*A\2419C\362\002\036{\364\377_\n\243/,\032\364\240-\010n\324g\374\210\n7\251d\225fQ\300O\342z\026{\366\210,S\355\257\030\035h\354\214\246;\002N\317m\356\231\263\226\216\\\2733\231\r\3337\317&\3565\367\254\360\363\226\374s\0371\207\270\213\006Z\255\200a\226Z\373}\363\305\247\324Y\267h\267\247\tW\352\270T\033\207\374Up\355w[\271b\022h\340\264\310\377Ul\t\0141\344\355{\263\305\265{U\256\033\340\374\223u\337\270`\200\271X\337\030\261\0235\221\006\317\314k\370r\227Q\247\367\022V\225cz\365\325\037^\253\230\330\233qDy\200E\304\n\300\356\002\356\223\200\217\225\177\302-\337\346\247\022\303\242\375\216|\214\340E\021\023|\314\3074)\033\271\0343\300\006\255\344~5\353\244\226\'\362;P<\202;\221n\377\303\017pnE\320Z\254)fj\217\025\205p\313bK\214}\001\366\314\027j\014%90\034\345\033\034\272\002\250\217\235F\"M\020-0\221\2640zp\362T>\345>,\232\035:\345;\361\253\243}\"\200\002\331\331\236\331q\031C\374\354\025K\234LF\225\205E\025S\364\206N\024\356#O\010\243\010\341\037,\316o\210M\364l}zB\3635\021\333d\313a+\037\234p\330 SN\210ZC\200Jk7B\364+v\267m\243J\2364\246\324\237\255=\264\337/q\036\"\244\330\212\202D\037\204=\372>\211\231\204k\226\265\371>\032.\310\221W\361\373\324\226\330\005\260\215\250f\333\262\022>\030\27180 P?\204\242\303L\251x\230\213\214\337\215\341[\001\335\013S]G\010\323\212\301S\377\333\331\3002\370V\237\203\315u\271\013\010\264\251\r\312N\031zl\037 \"E\261+C\275fg\375{\021\036\347<\300j\373\376\332-Oe\222?\315\211\274Z\301\326\325\311\340A\202.\301%\274\377\3135\357\026P9>\304+-\021\361E:\333\370\307\034\260\265\014-\261\327>\003#C7j\370\257\334Z\232g\177\336\262G*\200\002\314e\262\001\3256\223Wkw\216U\317\027\335\\\027\002\351r+\211\276\004\177\346K\205\340aX\217;\001\006\025m\034\246@qm<\014\306\365\342\342JX\020\232\373G\033v1\350\014\237\366\227\247\351\376\326\223h\223=\260E\375\026\250g\312b\223\371\321%h\336l\221\225\003)<\230\233 \330#\327\363\003\260.\tW\234\343\372Y\227\006(<\007\220\006\317p[\204\037\265\306i\377s\217O\325\200\3628|\260]\331/R.\221\267\023\326n0L\027\026\364\032\007h<;\241\253\301\352\376\332v\356\242\321\3212\326\237Z\314\301\203\035\265\010\035{\367\273\312|TX\002|\316t\006\335\210NI\301\265\002\037\325\306\327q%\372\213\336\r\332\036Q\006\330\373 \\2\350x-\323RW_\364l&\016\267b\312\3662}\333$\250G\"\350\216\336\262Bv.\346*s2\307X\205\252\373\00010\324\210`\3672\200\002\277\216\177\002^S\304\027\263\023\352\237m\346\246\365?\320\3108\374\233\316\005\264 {\314\2703\023\361\22785W\256\264s\005=\267\223\223\267\"\003?\326\346}\356/\355\004\263\312k\354l\363\203!]\213Lh\240T\024\300\271*=\004;\277\336\301\233z\240\361\311\256\300\005\323\023\311\316\303\234\266\276\321\261\301\325\r\0003\336\\\002\332\342\305\022\026U\351o\337V@\234\351S\304|\273\312\252 \217Y`\360\345\031\353\340\034\375\255\277\332\227lpT\314$xRM\362\361\350Q\244\346\027Pz\223\227\020\274D\35664\327\001\371\261\251A\231\271\007\217\3512\235P@\300uY&\242\014\036\331\350\035\226\252\357\247\323\351\316\353\300\271\177\306\340\3651\254\276\226\320c\223\326\277\373\360\216\'2m\243e0w\225\274r\346\267\237:\253\315\337\014^*\367\227\324\332K\024\3517i\224bHx\202\277\\\203\343\311Sc:\200\0026\032\030\362\371?\267\342d\324\301}\037\022\337\t\330!r/A,\365y\365l\274\302\323\360\220ca\'\3257h\2513\254M\031\257\322e\373\201\261\212\"#K\022\205\360\323\351C\325\313\245\226\340\340D\017\001\367\2459\247\345\356\246\307\256\251XLx\314\216Mb\312t\002pR\302\362U\345l\260\230!\177\361kN\027\275\264nx\266K\357\354k\310\010\265q\265\027\224\331%\212\273\016\030y\3666jf\337BC\245\020,}\235\036K\206\357\245\243\016]\212\0015\302\266f\306US?\343sj\025\242MMEZ\237\325\205\353\026\314\2175u\260q\223hv^P\351_\245\271\372s:\370\025\367\312\364\243\376\216\342I\325\326\252\234\375\'\245\354\246\226Zjg]&AaG\325<\301nN\016\010\311+\3023F\214\037\254eJg\010\r\201\016\310\036\306-0AK\310\216:\333V\022\347\362W\337=cB\200\002\324\221\356^!\357\316Y\255\353\233\211gDj\260%\377\357{U1\251\260\336\306|\212\316\234\340\226<H\200jS\036\304\217\342\010\377\023\232\027\320\235\317(|H\030ut\354\211\364Zx\270\374\326\326\334O\016Y\2363\274<\244~m\303D\356\364<\034\317\226\273\236\331\035ci-\365N\025\246\251\020\263\211Q=\376>\326RK;\230hz\300\320\302ib\272\316l\356\001Q$\372\263Va\240l\222\313\341F\326\004\211\207\340\350\030/V\371\374S&\232\265^\3627\312\"e\007\034\215\230\225\2042oH\002\337\r~5.\000\204\t\241\034(c\2359\251\003U\256\241\221s$\266\345\263\234\006\310\240h\n\033\256\233\225\203\374I v}\237\251\277\241(\201\301\245*A\227gR\325\027[\340\377\253\362H\314\036\301\033\216\200P\353\221P=(\023,\303L\224K|\343h\235\354\354K\302D?\367\351\027a"
+        key_material_type: ASYMMETRIC_PRIVATE
+      }
+      status: ENABLED
+      key_id: 2143045930
+      output_prefix_type: RAW""")
diff --git a/testing/cross_language/util/testing_servers.py b/testing/cross_language/util/testing_servers.py
index c1ca0c1..a64ce30 100644
--- a/testing/cross_language/util/testing_servers.py
+++ b/testing/cross_language/util/testing_servers.py
@@ -17,15 +17,18 @@
 import subprocess
 import time
 
-from typing import List, Optional
+from typing import List, Optional, Type, TypeVar
 from absl import logging
 import grpc
 import portpicker
+import tink
 
 from tink.proto import tink_pb2
 from util import _primitives
-from proto import testing_api_pb2
-from proto import testing_api_pb2_grpc
+from protos import testing_api_pb2
+from protos import testing_api_pb2_grpc
+
+P = TypeVar('P')
 
 # Server paths are relative to a root folder where all the server are located.
 # It can be set manually as follows:
@@ -76,6 +79,22 @@
     'jwt': ['cc', 'java', 'go', 'python'],
 }
 
+# Needed in golang, because there key URIs are not optional.
+GCP_KEY_URI_PREFIX = (
+    'gcp-kms://projects/tink-test-infrastructure/locations/global/'
+    'keyRings/unit-and-integration-testing/cryptoKeys/')
+AWS_KEY_URI_PREFIX = 'aws-kms://arn:aws:kms:us-east-2:235739564943:'
+
+GCP_CREDENTIALS_PATH = os.path.join(
+    os.environ['TEST_SRCDIR'] if 'TEST_SRCDIR' in os.environ else '',
+    'cross_language_test/testdata/gcp/credential.json')
+AWS_CREDENTIALS_INI_PATH = os.path.join(
+    os.environ['TEST_SRCDIR'] if 'TEST_SRCDIR' in os.environ else '',
+    'cross_language_test/testdata/aws/credentials.ini')
+AWS_CREDENTIALS_CRED_PATH = os.path.join(
+    os.environ['TEST_SRCDIR'] if 'TEST_SRCDIR' in os.environ else '',
+    'cross_language_test/testdata/aws/credentials.cred')
+
 _RELATIVE_ROOT_PATH = 'tink_base/testing'
 
 
@@ -123,12 +142,37 @@
 
 
 def _server_cmd(lang: str, port: int) -> List[str]:
+  """Returns the server command."""
+  if lang == 'java':
+    # Java expects a .cred file. Others a .ini file.
+    aws_credentials_path = AWS_CREDENTIALS_CRED_PATH
+  else:
+    aws_credentials_path = AWS_CREDENTIALS_INI_PATH
+
   server_path = _server_path(lang)
+  # TODO(b/249015767): Refactor KMS integration to pass credentials via gRPC.
+  server_args = [
+      '--port',
+      '%d' % port, '--gcp_credentials_path', GCP_CREDENTIALS_PATH,
+      '--aws_credentials_path', aws_credentials_path
+  ]
+  if lang == 'go':
+    # in all languages except go, the key URI parameters are optional.
+    # in go, they are required, but can be a prefix.
+    server_args.extend([
+        '--gcp_key_uri', GCP_KEY_URI_PREFIX,
+        '--aws_key_uri', AWS_KEY_URI_PREFIX])
+
   if lang == 'java' and server_path.endswith('.jar'):
     java_path = os.path.join(_root_path(), _JAVA_PATH)
-    return [java_path, '-jar', server_path, '--port', '%d' % port]
+    return [java_path, '-jar', server_path] + server_args
   else:
-    return [server_path, '--port', '%d' % port]
+    return [server_path] + server_args
+
+
+def _get_file_content(filename: str) -> str:
+  with open(filename, 'r') as f:
+    return f.read()
 
 
 class _TestingServers():
@@ -148,18 +192,13 @@
     self._signature_stub = {}
     self._prf_stub = {}
     self._jwt_stub = {}
+    self._test_name = test_name
+
     for lang in LANGUAGES:
       port = portpicker.pick_unused_port()
       cmd = _server_cmd(lang, port)
       logging.info('cmd = %s', cmd)
-      try:
-        output_dir = os.environ['TEST_UNDECLARED_OUTPUTS_DIR']
-      except KeyError as e:
-        raise RuntimeError(
-            'Could not start %s server, TEST_UNDECLARED_OUTPUTS_DIR environment'
-            'variable must be set') from e
-      output_file = '%s-%s-%s' % (test_name, lang, 'server.log')
-      output_path = os.path.join(output_dir, output_file)
+      output_path = self._get_output_path(lang)
       logging.info('writing server output to %s', output_path)
       try:
         self._output_file[lang] = open(output_path, 'w+')
@@ -168,8 +207,9 @@
         raise RuntimeError('Could not start %s server' % lang) from e
       self._server[lang] = subprocess.Popen(
           cmd, stdout=self._output_file[lang], stderr=subprocess.STDOUT)
-      logging.info('%s server started on port %d with pid: %d.',
-                   lang, port, self._server[lang].pid)
+      logging.info('%s server started on port %d with pid: %d. Log output: %s',
+                   lang, port, self._server[lang].pid,
+                   self._output_file[lang].name)
       self._channel[lang] = grpc.secure_channel(
           '[::]:%d' % port, grpc.local_channel_credentials())
     for lang in LANGUAGES:
@@ -178,9 +218,10 @@
       except Exception as e:
         logging.info('Timeout while connecting to server %s', lang)
         self._server[lang].kill()
-        out, err = self._server[lang].communicate()
-        raise RuntimeError('Could not start %s server, output=%s, err=%s' %
-                           (lang, out, err)) from e
+        _, _ = self._server[lang].communicate()
+        raise RuntimeError(
+            'Could not start %s server, output=%s' %
+            (lang, _get_file_content(self._output_file[lang].name))) from e
       self._metadata_stub[lang] = testing_api_pb2_grpc.MetadataStub(
           self._channel[lang])
       self._keyset_stub[lang] = testing_api_pb2_grpc.KeysetStub(
@@ -191,6 +232,16 @@
         getattr(self, stub_name)[lang] = _PRIMITIVE_STUBS[primitive](
             self._channel[lang])
 
+  def _get_output_path(self, lang) -> str:
+    try:
+      output_dir = os.environ['TEST_UNDECLARED_OUTPUTS_DIR']
+    except KeyError as e:
+      raise RuntimeError(
+          'Could not start %s server, TEST_UNDECLARED_OUTPUTS_DIR environment'
+          'variable must be set') from e
+    output_file = '%s-%s-%s' % (self._test_name, lang, 'server.log')
+    return os.path.join(output_dir, output_file)
+
   def keyset_stub(self, lang) -> testing_api_pb2_grpc.KeysetStub:
     return self._keyset_stub[lang]
 
@@ -237,6 +288,20 @@
       self._output_file[lang].close()
     logging.info('All servers stopped.')
 
+    print()
+    print()
+    for lang in LANGUAGES:
+      total_reps = 1 + 100 // len(lang + ' ')
+      length = total_reps * len(lang + ' ') - 1
+      print('=' * length)
+      print((lang + ' ') * total_reps)
+      print('v' * length)
+      with open(self._get_output_path(lang)) as f:
+        print(f.read())
+      print('^' * length)
+      print((lang + ' ') * total_reps)
+      print('=' * length)
+      print()
 
 _ts = None
 
@@ -258,45 +323,34 @@
     else:
       logging.warning('server in lang %s has no tink version.', lang)
   unique_versions = list(set(versions.values()))
-  if not unique_versions:
-    raise ValueError('tink version unknown')
-  if len(unique_versions) > 1:
-    raise ValueError('tink_version in testing servers are inconsistent: %s' %
-                     versions)
   logging.info('Tink version: %s', unique_versions[0])
 
 
 def stop() -> None:
   """Stops all servers."""
-  global _ts
   _ts.stop()
 
 
 def key_template(lang: str, template_name: str) -> tink_pb2.KeyTemplate:
   """Returns the key template of template_name, implemented in lang."""
-  global _ts
   return _primitives.key_template(_ts.keyset_stub(lang), template_name)
 
 
 def new_keyset(lang: str, template: tink_pb2.KeyTemplate) -> bytes:
   """Returns a new KeysetHandle, implemented in lang."""
-  global _ts
   return _primitives.new_keyset(_ts.keyset_stub(lang), template)
 
 
 def public_keyset(lang: str, private_keyset: bytes) -> bytes:
   """Returns a public keyset handle, implemented in lang."""
-  global _ts
   return _primitives.public_keyset(_ts.keyset_stub(lang), private_keyset)
 
 
 def keyset_to_json(lang: str, keyset: bytes) -> str:
-  global _ts
   return _primitives.keyset_to_json(_ts.keyset_stub(lang), keyset)
 
 
 def keyset_from_json(lang: str, json_keyset: str) -> bytes:
-  global _ts
   return _primitives.keyset_from_json(_ts.keyset_stub(lang), json_keyset)
 
 
@@ -304,7 +358,6 @@
                           master_keyset: bytes,
                           associated_data: Optional[bytes],
                           keyset_reader_type: str) -> bytes:
-  global _ts
   return _primitives.keyset_read_encrypted(
       _ts.keyset_stub(lang), encrypted_keyset, master_keyset, associated_data,
       keyset_reader_type)
@@ -313,95 +366,65 @@
 def keyset_write_encrypted(lang: str, keyset: bytes, master_keyset: bytes,
                            associated_data: Optional[bytes],
                            keyset_writer_type: str) -> bytes:
-  global _ts
   return _primitives.keyset_write_encrypted(
       _ts.keyset_stub(lang), keyset, master_keyset, associated_data,
       keyset_writer_type)
 
 
 def jwk_set_to_keyset(lang: str, jwk_set: str) -> bytes:
-  global _ts
   return _primitives.jwk_set_to_keyset(_ts.jwt_stub(lang), jwk_set)
 
 
 def jwk_set_from_keyset(lang: str, keyset: bytes) -> str:
-  global _ts
   return _primitives.jwk_set_from_keyset(_ts.jwt_stub(lang), keyset)
 
 
-def aead(lang: str, keyset: bytes) -> _primitives.Aead:
-  """Returns an AEAD primitive, implemented in lang."""
-  global _ts
-  return _primitives.Aead(lang, _ts.aead_stub(lang), keyset)
+def remote_primitive(lang: str, keyset: bytes, primitive_class: Type[P]) -> P:
+  """Creates a primitive from a keyset backed by the given language.
 
+  Internally, this does an RPC to the server specified by 'lang' in order to
+  try to 'Create' the primitive. If the RPC returns with an error, a TinkError
+  is returned. Otherwise, an instance of the primitive is returned which
+  forwards calls to the service implemented in the language.
 
-def deterministic_aead(lang: str,
-                       keyset: bytes) -> _primitives.DeterministicAead:
-  """Returns a DeterministicAEAD primitive, implemented in lang."""
-  global _ts
-  return _primitives.DeterministicAead(lang, _ts.daead_stub(lang), keyset)
+  Args:
+    lang: specification of the language to use
+    keyset: the serialized keyset
+    primitive_class: the type of the primitive
 
+  Returns:
+    A primitive to be used.
 
-def streaming_aead(lang: str, key_handle: bytes) -> _primitives.StreamingAead:
-  """Returns a StreamingAEAD primitive, implemented in lang."""
-  global _ts
-  return _primitives.StreamingAead(
-      lang, _ts.streaming_aead_stub(lang), key_handle)
+  Raises:
+    TinkError if creation fails.
+  """
 
-
-def hybrid_encrypt(lang: str, pub_keyset: bytes) -> _primitives.HybridEncrypt:
-  """Returns a HybridEncrypt  primitive, implemented in lang."""
-  global _ts
-  return _primitives.HybridEncrypt(lang, _ts.hybrid_stub(lang), pub_keyset)
-
-
-def hybrid_decrypt(lang: str, priv_keyset: bytes) -> _primitives.HybridDecrypt:
-  """Returns a HybridDecrypt primitive, implemented in lang."""
-  global _ts
-  return _primitives.HybridDecrypt(lang, _ts.hybrid_stub(lang), priv_keyset)
-
-
-def mac(lang: str, keyset: bytes) -> _primitives.Mac:
-  """Returns a MAC primitive, implemented in lang."""
-  global _ts
-  return _primitives.Mac(lang, _ts.mac_stub(lang), keyset)
-
-
-def public_key_sign(lang: str,
-                    priv_keyset: bytes) -> _primitives.PublicKeySign:
-  """Returns an PublicKeySign primitive, implemented in lang."""
-  global _ts
-  return _primitives.PublicKeySign(lang, _ts.signature_stub(lang), priv_keyset)
-
-
-def public_key_verify(lang: str,
-                      pub_keyset: bytes) -> _primitives.PublicKeyVerify:
-  """Returns an PublicKeyVerify primitive, implemented in lang."""
-  global _ts
-  return _primitives.PublicKeyVerify(lang, _ts.signature_stub(lang), pub_keyset)
-
-
-def prf_set(lang: str, keyset: bytes) -> _primitives.PrfSet:
-  """Returns an PrfSet primitive, implemented in lang."""
-  global _ts
-  return _primitives.PrfSet(lang, _ts.prf_stub(lang), keyset)
-
-
-def jwt_mac(lang: str, keyset: bytes) -> _primitives.JwtMac:
-  """Returns a JwtMac primitive, implemented in lang."""
-  global _ts
-  return _primitives.JwtMac(lang, _ts.jwt_stub(lang), keyset)
-
-
-def jwt_public_key_sign(lang: str,
-                        keyset: bytes) -> _primitives.JwtPublicKeySign:
-  """Returns a JwtPublicKeySign primitive, implemented in lang."""
-  global _ts
-  return _primitives.JwtPublicKeySign(lang, _ts.jwt_stub(lang), keyset)
-
-
-def jwt_public_key_verify(lang: str,
-                          keyset: bytes) -> _primitives.JwtPublicKeyVerify:
-  """Returns a JwtPublicKeyVerify primitive, implemented in lang."""
-  global _ts
-  return _primitives.JwtPublicKeyVerify(lang, _ts.jwt_stub(lang), keyset)
+  if primitive_class == tink.aead.Aead:
+    return _primitives.Aead(lang, _ts.aead_stub(lang), keyset, None)
+  if primitive_class == tink.daead.DeterministicAead:
+    return _primitives.DeterministicAead(lang, _ts.daead_stub(lang), keyset,
+                                         None)
+  if primitive_class == tink.streaming_aead.StreamingAead:
+    return _primitives.StreamingAead(lang, _ts.streaming_aead_stub(lang),
+                                     keyset)
+  if primitive_class == tink.hybrid.HybridDecrypt:
+    return _primitives.HybridDecrypt(lang, _ts.hybrid_stub(lang), keyset, None)
+  if primitive_class == tink.hybrid.HybridEncrypt:
+    return _primitives.HybridEncrypt(lang, _ts.hybrid_stub(lang), keyset, None)
+  if primitive_class == tink.mac.Mac:
+    return _primitives.Mac(lang, _ts.mac_stub(lang), keyset, None)
+  if primitive_class == tink.signature.PublicKeySign:
+    return _primitives.PublicKeySign(lang, _ts.signature_stub(lang), keyset,
+                                     None)
+  if primitive_class == tink.signature.PublicKeyVerify:
+    return _primitives.PublicKeyVerify(lang, _ts.signature_stub(lang), keyset,
+                                       None)
+  if primitive_class == tink.prf.PrfSet:
+    return _primitives.PrfSet(lang, _ts.prf_stub(lang), keyset, None)
+  if primitive_class == tink.jwt.JwtMac:
+    return _primitives.JwtMac(lang, _ts.jwt_stub(lang), keyset)
+  if primitive_class == tink.jwt.JwtPublicKeySign:
+    return _primitives.JwtPublicKeySign(lang, _ts.jwt_stub(lang), keyset)
+  if primitive_class == tink.jwt.JwtPublicKeyVerify:
+    return _primitives.JwtPublicKeyVerify(lang, _ts.jwt_stub(lang), keyset)
+  raise ValueError('Unsupported P in remote_primitive: ' + str(primitive_class))
diff --git a/testing/cross_language/util/testing_servers_test.py b/testing/cross_language/util/testing_servers_test.py
index 751d90c..f97f69e 100644
--- a/testing/cross_language/util/testing_servers_test.py
+++ b/testing/cross_language/util/testing_servers_test.py
@@ -15,12 +15,12 @@
 
 import datetime
 import io
-
+import textwrap
 from typing import Iterable, Tuple
 
+from absl import flags
 from absl.testing import absltest
 from absl.testing import parameterized
-
 import tink
 from tink import aead
 from tink import daead
@@ -31,10 +31,46 @@
 from tink import signature
 from tink import streaming_aead
 
+from tink.proto import tink_pb2
+from util import key_util
+from util import test_keys
 from util import testing_servers
 
 _SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE
 
+_HEX_TEMPLATE = flags.DEFINE_string(
+    'hex_template',
+    aead.aead_key_templates.AES256_GCM.SerializeToString().hex(),
+    'The template in hex format to use in the create_keyset test.'
+)
+
+_FORCE_FAILURE_FOR_ADDING_KEY_TO_DB = flags.DEFINE_boolean(
+    'force_failure_for_adding_key_to_db', False,
+    'Set to force a message which helps to add a new key to the DB.')
+
+_MESSAGE_TEMPLATE = '''
+Please add the following to _test_keys_db.py:
+COPY PASTE START ===============================================================
+db.add_key(
+    template=r"""
+{template_text_format}""",
+    key=r"""
+{key_text_format}""")
+COPY PASTE END =================================================================
+'''
+
+
+def setUpModule():
+  aead.register()
+  daead.register()
+  hybrid.register()
+  jwt.register_jwt_mac()
+  jwt.register_jwt_signature()
+  mac.register()
+  prf.register()
+  signature.register()
+  streaming_aead.register()
+
 
 class TestingServersConfigTest(absltest.TestCase):
 
@@ -84,6 +120,51 @@
     self.assertEqual(template.type_url,
                      'type.googleapis.com/google.crypto.tink.AesGcmKey')
 
+  @parameterized.parameters(testing_servers.LANGUAGES)
+  def test_new_keyset(self, lang):
+    """Tests that we can create a new keyset in each language.
+
+    This test also serves to add new keys to the _test_keys_db -- see the
+    comments there.
+
+    Args:
+      lang: language to use for the test
+    """
+    template = tink_pb2.KeyTemplate().FromString(
+        bytes.fromhex(_HEX_TEMPLATE.value))
+    keyset = testing_servers.new_keyset(lang, template)
+    parsed_keyset = tink_pb2.Keyset.FromString(keyset)
+    self.assertLen(parsed_keyset.key, 1)
+    if _FORCE_FAILURE_FOR_ADDING_KEY_TO_DB.value:
+      self.fail(
+          _MESSAGE_TEMPLATE.format(
+              template_text_format=textwrap.indent(
+                  key_util.text_format(template), ' ' * 6),
+              key_text_format=textwrap.indent(
+                  key_util.text_format(parsed_keyset.key[0]), ' ' * 6)))
+
+  @parameterized.parameters([
+      aead.Aead, daead.DeterministicAead, streaming_aead.StreamingAead,
+      hybrid.HybridDecrypt, hybrid.HybridEncrypt, mac.Mac,
+      signature.PublicKeySign, signature.PublicKeyVerify, prf.PrfSet,
+      jwt.JwtMac, jwt.JwtPublicKeySign, jwt.JwtPublicKeyVerify
+  ])
+  def test_create_with_correct_keyset(self, primitive):
+    keyset = test_keys.some_keyset_for_primitive(primitive)
+    _ = testing_servers.remote_primitive('python', keyset, primitive)
+
+  @parameterized.parameters([
+      aead.Aead, daead.DeterministicAead, streaming_aead.StreamingAead,
+      hybrid.HybridDecrypt, hybrid.HybridEncrypt, mac.Mac,
+      signature.PublicKeySign, signature.PublicKeyVerify, prf.PrfSet,
+      jwt.JwtMac, jwt.JwtPublicKeySign, jwt.JwtPublicKeyVerify
+  ])
+  def test_create_with_incorrect_keyset(self, primitive):
+    wrong_primitive = aead.Aead if primitive == mac.Mac else mac.Mac
+    keyset = test_keys.some_keyset_for_primitive(wrong_primitive)
+    with self.assertRaises(tink.TinkError):
+      testing_servers.remote_primitive('python', keyset, primitive)
+
   @parameterized.parameters(encrypted_keyset_test_cases())
   def test_read_write_encrypted_keyset(self, lang, keyset_reader_type,
                                        keyset_writer_type):
@@ -124,7 +205,7 @@
                                         aead.aead_key_templates.AES128_GCM)
     plaintext = b'The quick brown fox jumps over the lazy dog'
     associated_data = b'associated_data'
-    aead_primitive = testing_servers.aead(lang, keyset)
+    aead_primitive = testing_servers.remote_primitive(lang, keyset, aead.Aead)
     ciphertext = aead_primitive.encrypt(plaintext, associated_data)
     output = aead_primitive.decrypt(ciphertext, associated_data)
     self.assertEqual(output, plaintext)
@@ -138,7 +219,8 @@
         lang, daead.deterministic_aead_key_templates.AES256_SIV)
     plaintext = b'The quick brown fox jumps over the lazy dog'
     associated_data = b'associated_data'
-    daead_primitive = testing_servers.deterministic_aead(lang, keyset)
+    daead_primitive = testing_servers.remote_primitive(lang, keyset,
+                                                       daead.DeterministicAead)
     ciphertext = daead_primitive.encrypt_deterministically(
         plaintext, associated_data)
     output = daead_primitive.decrypt_deterministically(
@@ -155,7 +237,8 @@
     plaintext = b'The quick brown fox jumps over the lazy dog'
     plaintext_stream = io.BytesIO(plaintext)
     associated_data = b'associated_data'
-    streaming_aead_primitive = testing_servers.streaming_aead(lang, keyset)
+    streaming_aead_primitive = testing_servers.remote_primitive(
+        lang, keyset, streaming_aead.StreamingAead)
     ciphertext_stream = streaming_aead_primitive.new_encrypting_stream(
         plaintext_stream, associated_data)
     output_stream = streaming_aead_primitive.new_decrypting_stream(
@@ -171,7 +254,7 @@
     keyset = testing_servers.new_keyset(
         lang, mac.mac_key_templates.HMAC_SHA256_128BITTAG)
     data = b'The quick brown fox jumps over the lazy dog'
-    mac_primitive = testing_servers.mac(lang, keyset)
+    mac_primitive = testing_servers.remote_primitive(lang, keyset, mac.Mac)
     mac_value = mac_primitive.compute_mac(data)
     mac_primitive.verify_mac(mac_value, data)
 
@@ -184,11 +267,13 @@
         lang,
         hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM)
     public_handle = testing_servers.public_keyset(lang, private_handle)
-    enc_primitive = testing_servers.hybrid_encrypt(lang, public_handle)
+    enc_primitive = testing_servers.remote_primitive(lang, public_handle,
+                                                     hybrid.HybridEncrypt)
     data = b'The quick brown fox jumps over the lazy dog'
     context_info = b'context'
     ciphertext = enc_primitive.encrypt(data, context_info)
-    dec_primitive = testing_servers.hybrid_decrypt(lang, private_handle)
+    dec_primitive = testing_servers.remote_primitive(lang, private_handle,
+                                                     hybrid.HybridDecrypt)
     output = dec_primitive.decrypt(ciphertext, context_info)
     self.assertEqual(output, data)
 
@@ -200,10 +285,12 @@
     private_handle = testing_servers.new_keyset(
         lang, signature.signature_key_templates.ED25519)
     public_handle = testing_servers.public_keyset(lang, private_handle)
-    sign_primitive = testing_servers.public_key_sign(lang, private_handle)
+    sign_primitive = testing_servers.remote_primitive(lang, private_handle,
+                                                      signature.PublicKeySign)
     data = b'The quick brown fox jumps over the lazy dog'
     signature_value = sign_primitive.sign(data)
-    verify_primitive = testing_servers.public_key_verify(lang, public_handle)
+    verify_primitive = testing_servers.remote_primitive(
+        lang, public_handle, signature.PublicKeyVerify)
     verify_primitive.verify(signature_value, data)
 
     with self.assertRaises(tink.TinkError):
@@ -214,7 +301,8 @@
     keyset = testing_servers.new_keyset(lang,
                                         prf.prf_key_templates.HMAC_SHA256)
     input_data = b'The quick brown fox jumps over the lazy dog'
-    prf_set_primitive = testing_servers.prf_set(lang, keyset)
+    prf_set_primitive = testing_servers.remote_primitive(
+        lang, keyset, prf.PrfSet)
     output = prf_set_primitive.primary().compute(input_data, output_length=15)
     self.assertLen(output, 15)
 
@@ -225,7 +313,8 @@
   def test_jwt_mac(self, lang):
     keyset = testing_servers.new_keyset(lang, jwt.jwt_hs256_template())
 
-    jwt_mac_primitive = testing_servers.jwt_mac(lang, keyset)
+    jwt_mac_primitive = testing_servers.remote_primitive(
+        lang, keyset, jwt.JwtMac)
 
     now = datetime.datetime.now(tz=datetime.timezone.utc)
     token = jwt.new_raw_jwt(
@@ -257,8 +346,10 @@
     private_keyset = testing_servers.new_keyset(lang, jwt.jwt_es256_template())
     public_keyset = testing_servers.public_keyset(lang, private_keyset)
 
-    signer = testing_servers.jwt_public_key_sign(lang, private_keyset)
-    verifier = testing_servers.jwt_public_key_verify(lang, public_keyset)
+    signer = testing_servers.remote_primitive(lang, private_keyset,
+                                              jwt.JwtPublicKeySign)
+    verifier = testing_servers.remote_primitive(lang, public_keyset,
+                                                jwt.JwtPublicKeyVerify)
 
     now = datetime.datetime.now(tz=datetime.timezone.utc)
     token = jwt.new_raw_jwt(
@@ -291,7 +382,8 @@
     public_keyset = testing_servers.public_keyset(lang, private_keyset)
 
     # sign and export public key
-    signer = testing_servers.jwt_public_key_sign(lang, private_keyset)
+    signer = testing_servers.remote_primitive(lang, private_keyset,
+                                              jwt.JwtPublicKeySign)
     now = datetime.datetime.now(tz=datetime.timezone.utc)
     token = jwt.new_raw_jwt(
         jwt_id='jwt_id', expiration=now + datetime.timedelta(seconds=100))
@@ -302,8 +394,8 @@
     imported_public_keyset = testing_servers.jwk_set_to_keyset(
         lang, public_jwk_set)
 
-    verifier = testing_servers.jwt_public_key_verify(lang,
-                                                     imported_public_keyset)
+    verifier = testing_servers.remote_primitive(lang, imported_public_keyset,
+                                                jwt.JwtPublicKeyVerify)
     validator = jwt.new_validator(fixed_now=now)
     verified_jwt = verifier.verify_and_decode(compact, validator)
     self.assertEqual(verified_jwt.jwt_id(), 'jwt_id')
diff --git a/testing/cross_language/util/utilities.py b/testing/cross_language/util/utilities.py
new file mode 100644
index 0000000..3cc097e
--- /dev/null
+++ b/testing/cross_language/util/utilities.py
@@ -0,0 +1,366 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Various utility functions for the cross language tests.
+"""
+
+from typing import Any, Iterable, List
+
+from tink import aead
+from tink import daead
+from tink import hybrid
+from tink import jwt
+from tink import mac
+from tink import prf
+from tink import signature
+from tink import streaming_aead
+
+from tink.proto import tink_pb2
+import tink_config
+
+# All languages supported by cross-language tests.
+ALL_LANGUAGES = ['cc', 'java', 'go', 'python']
+
+
+# For each KeyType, a list of Tinkey KeyTemplate names.
+# TODO(juerg): Add missing key template names, and remove deprecated names.
+# TODO(tholenst): Change this to a function
+KEY_TEMPLATE_NAMES = {
+    'AesEaxKey': [
+        'AES128_EAX', 'AES128_EAX_RAW', 'AES256_EAX', 'AES256_EAX_RAW'
+    ],
+    'AesGcmKey': [
+        'AES128_GCM', 'AES128_GCM_RAW', 'AES256_GCM', 'AES256_GCM_RAW'
+    ],
+    'AesGcmSivKey': [
+        'AES128_GCM_SIV', 'AES128_GCM_SIV_RAW', 'AES256_GCM_SIV',
+        'AES256_GCM_SIV_RAW'
+    ],
+    'AesCtrHmacAeadKey': [
+        'AES128_CTR_HMAC_SHA256', 'AES128_CTR_HMAC_SHA256_RAW',
+        'AES256_CTR_HMAC_SHA256', 'AES256_CTR_HMAC_SHA256_RAW'
+    ],
+    'ChaCha20Poly1305Key': ['CHACHA20_POLY1305', 'CHACHA20_POLY1305_RAW'],
+    'XChaCha20Poly1305Key': ['XCHACHA20_POLY1305', 'XCHACHA20_POLY1305_RAW'],
+    'KmsAeadKey': [],
+    'KmsEnvelopeAeadKey': [],
+    'AesSivKey': ['AES256_SIV'],
+    'AesCtrHmacStreamingKey': [
+        'AES128_CTR_HMAC_SHA256_4KB',
+        'AES128_CTR_HMAC_SHA256_1MB',
+        'AES256_CTR_HMAC_SHA256_4KB',
+        'AES256_CTR_HMAC_SHA256_1MB',
+    ],
+    'AesGcmHkdfStreamingKey': [
+        'AES128_GCM_HKDF_4KB',
+        'AES128_GCM_HKDF_1MB',
+        'AES256_GCM_HKDF_4KB',
+        'AES256_GCM_HKDF_1MB',
+    ],
+    'EciesAeadHkdfPrivateKey': [
+        'ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM',
+        'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM',
+        'ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256',
+        'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256',
+    ],
+    'HpkePrivateKey': [
+        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM',
+        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW',
+        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM',
+        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW',
+        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305',
+        'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW',
+    ],
+    'AesCmacKey': ['AES_CMAC'],
+    'HmacKey': [
+        'HMAC_SHA256_128BITTAG', 'HMAC_SHA256_256BITTAG',
+        'HMAC_SHA512_256BITTAG', 'HMAC_SHA512_512BITTAG'
+    ],
+    'EcdsaPrivateKey': [
+        'ECDSA_P256', 'ECDSA_P256_RAW', 'ECDSA_P384', 'ECDSA_P384_SHA384',
+        'ECDSA_P384_SHA512', 'ECDSA_P521', 'ECDSA_P256_IEEE_P1363',
+        'ECDSA_P384_IEEE_P1363', 'ECDSA_P384_SHA384_IEEE_P1363',
+        'ECDSA_P521_IEEE_P1363'
+    ],
+    'Ed25519PrivateKey': ['ED25519'],
+    'RsaSsaPkcs1PrivateKey': [
+        'RSA_SSA_PKCS1_3072_SHA256_F4', 'RSA_SSA_PKCS1_4096_SHA512_F4'
+    ],
+    'RsaSsaPssPrivateKey': [
+        'RSA_SSA_PSS_3072_SHA256_SHA256_32_F4',
+        'RSA_SSA_PSS_4096_SHA512_SHA512_64_F4'
+    ],
+    'AesCmacPrfKey': ['AES_CMAC_PRF'],
+    'HmacPrfKey': ['HMAC_SHA256_PRF', 'HMAC_SHA512_PRF'],
+    'HkdfPrfKey': ['HKDF_SHA256'],
+    'JwtHmacKey': [
+        'JWT_HS256', 'JWT_HS256_RAW', 'JWT_HS384', 'JWT_HS384_RAW', 'JWT_HS512',
+        'JWT_HS512_RAW'
+    ],
+    'JwtEcdsaPrivateKey': [
+        'JWT_ES256', 'JWT_ES256_RAW', 'JWT_ES384', 'JWT_ES384_RAW', 'JWT_ES512',
+        'JWT_ES512_RAW'
+    ],
+    'JwtRsaSsaPkcs1PrivateKey': [
+        'JWT_RS256_2048_F4', 'JWT_RS256_2048_F4_RAW', 'JWT_RS256_3072_F4',
+        'JWT_RS256_3072_F4_RAW', 'JWT_RS384_3072_F4', 'JWT_RS384_3072_F4_RAW',
+        'JWT_RS512_4096_F4', 'JWT_RS512_4096_F4_RAW'
+    ],
+    'JwtRsaSsaPssPrivateKey': [
+        'JWT_PS256_2048_F4', 'JWT_PS256_2048_F4_RAW', 'JWT_PS256_3072_F4',
+        'JWT_PS256_3072_F4_RAW', 'JWT_PS384_3072_F4', 'JWT_PS384_3072_F4_RAW',
+        'JWT_PS512_4096_F4', 'JWT_PS512_4096_F4_RAW'
+    ],
+}
+
+# KeyTemplate (as Protobuf) for each KeyTemplate name.
+KEY_TEMPLATE = {
+    'AES128_EAX':
+        aead.aead_key_templates.AES128_EAX,
+    'AES128_EAX_RAW':
+        aead.aead_key_templates.AES128_EAX_RAW,
+    'AES256_EAX':
+        aead.aead_key_templates.AES256_EAX,
+    'AES256_EAX_RAW':
+        aead.aead_key_templates.AES256_EAX_RAW,
+    'AES128_GCM':
+        aead.aead_key_templates.AES128_GCM,
+    'AES128_GCM_RAW':
+        aead.aead_key_templates.AES128_GCM_RAW,
+    'AES256_GCM':
+        aead.aead_key_templates.AES256_GCM,
+    'AES256_GCM_RAW':
+        aead.aead_key_templates.AES256_GCM_RAW,
+    'AES128_GCM_SIV':
+        aead.aead_key_templates.AES128_GCM_SIV,
+    'AES128_GCM_SIV_RAW':
+        aead.aead_key_templates.AES128_GCM_SIV_RAW,
+    'AES256_GCM_SIV':
+        aead.aead_key_templates.AES256_GCM_SIV,
+    'AES256_GCM_SIV_RAW':
+        aead.aead_key_templates.AES256_GCM_SIV_RAW,
+    'AES128_CTR_HMAC_SHA256':
+        aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
+    'AES128_CTR_HMAC_SHA256_RAW':
+        aead.aead_key_templates.AES128_CTR_HMAC_SHA256_RAW,
+    'AES256_CTR_HMAC_SHA256':
+        aead.aead_key_templates.AES256_CTR_HMAC_SHA256,
+    'AES256_CTR_HMAC_SHA256_RAW':
+        aead.aead_key_templates.AES256_CTR_HMAC_SHA256_RAW,
+    'CHACHA20_POLY1305':
+        tink_pb2.KeyTemplate(
+            type_url=('type.googleapis.com/google.crypto.tink.' +
+                      'ChaCha20Poly1305Key'),
+            output_prefix_type=tink_pb2.TINK),
+    'CHACHA20_POLY1305_RAW':
+        tink_pb2.KeyTemplate(
+            type_url=('type.googleapis.com/google.crypto.tink.' +
+                      'ChaCha20Poly1305Key'),
+            output_prefix_type=tink_pb2.RAW),
+    'XCHACHA20_POLY1305':
+        aead.aead_key_templates.XCHACHA20_POLY1305,
+    'XCHACHA20_POLY1305_RAW':
+        aead.aead_key_templates.XCHACHA20_POLY1305_RAW,
+    'AES256_SIV':
+        daead.deterministic_aead_key_templates.AES256_SIV,
+    'AES128_CTR_HMAC_SHA256_4KB':
+        streaming_aead.streaming_aead_key_templates.AES128_CTR_HMAC_SHA256_4KB,
+    'AES128_CTR_HMAC_SHA256_1MB':
+        streaming_aead.streaming_aead_key_templates.AES128_CTR_HMAC_SHA256_1MB,
+    'AES256_CTR_HMAC_SHA256_4KB':
+        streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_4KB,
+    'AES256_CTR_HMAC_SHA256_1MB':
+        streaming_aead.streaming_aead_key_templates.AES256_CTR_HMAC_SHA256_1MB,
+    'AES128_GCM_HKDF_4KB':
+        streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_4KB,
+    'AES128_GCM_HKDF_1MB':
+        streaming_aead.streaming_aead_key_templates.AES128_GCM_HKDF_1MB,
+    'AES256_GCM_HKDF_4KB':
+        streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_4KB,
+    'AES256_GCM_HKDF_1MB':
+        streaming_aead.streaming_aead_key_templates.AES256_GCM_HKDF_1MB,
+    'ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM':
+        hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM,
+    'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM':
+        hybrid.hybrid_key_templates
+        .ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_GCM,
+    'ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256':
+        hybrid.hybrid_key_templates
+        .ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256,
+    'ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256':
+        hybrid.hybrid_key_templates
+        .ECIES_P256_COMPRESSED_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256,
+    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM':
+        hybrid.hybrid_key_templates
+        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM,
+    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW':
+        hybrid.hybrid_key_templates
+        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW,
+    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM':
+        hybrid.hybrid_key_templates
+        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM,
+    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW':
+        hybrid.hybrid_key_templates
+        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW,
+    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305':
+        hybrid.hybrid_key_templates
+        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305,
+    'DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW':
+        hybrid.hybrid_key_templates
+        .DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW,
+    'AES_CMAC':
+        mac.mac_key_templates.AES_CMAC,
+    'HMAC_SHA256_128BITTAG':
+        mac.mac_key_templates.HMAC_SHA256_128BITTAG,
+    'HMAC_SHA256_256BITTAG':
+        mac.mac_key_templates.HMAC_SHA256_256BITTAG,
+    'HMAC_SHA512_256BITTAG':
+        mac.mac_key_templates.HMAC_SHA512_256BITTAG,
+    'HMAC_SHA512_512BITTAG':
+        mac.mac_key_templates.HMAC_SHA512_512BITTAG,
+    'ECDSA_P256':
+        signature.signature_key_templates.ECDSA_P256,
+    'ECDSA_P256_RAW':
+        signature.signature_key_templates.ECDSA_P256_RAW,
+    'ECDSA_P384':
+        signature.signature_key_templates.ECDSA_P384,
+    'ECDSA_P384_SHA384':
+        signature.signature_key_templates.ECDSA_P384_SHA384,
+    'ECDSA_P384_SHA512':
+        signature.signature_key_templates.ECDSA_P384_SHA512,
+    'ECDSA_P521':
+        signature.signature_key_templates.ECDSA_P521,
+    'ECDSA_P256_IEEE_P1363':
+        signature.signature_key_templates.ECDSA_P256_IEEE_P1363,
+    'ECDSA_P384_IEEE_P1363':
+        signature.signature_key_templates.ECDSA_P384_IEEE_P1363,
+    'ECDSA_P384_SHA384_IEEE_P1363':
+        signature.signature_key_templates.ECDSA_P384_SHA384_IEEE_P1363,
+    'ECDSA_P521_IEEE_P1363':
+        signature.signature_key_templates.ECDSA_P521_IEEE_P1363,
+    'ED25519':
+        signature.signature_key_templates.ED25519,
+    'RSA_SSA_PKCS1_3072_SHA256_F4':
+        signature.signature_key_templates.RSA_SSA_PKCS1_3072_SHA256_F4,
+    'RSA_SSA_PKCS1_4096_SHA512_F4':
+        signature.signature_key_templates.RSA_SSA_PKCS1_4096_SHA512_F4,
+    'RSA_SSA_PSS_3072_SHA256_SHA256_32_F4':
+        signature.signature_key_templates.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4,
+    'RSA_SSA_PSS_4096_SHA512_SHA512_64_F4':
+        signature.signature_key_templates.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4,
+    'AES_CMAC_PRF':
+        prf.prf_key_templates.AES_CMAC,
+    'HMAC_SHA256_PRF':
+        prf.prf_key_templates.HMAC_SHA256,
+    'HMAC_SHA512_PRF':
+        prf.prf_key_templates.HMAC_SHA512,
+    'HKDF_SHA256':
+        prf.prf_key_templates.HKDF_SHA256,
+    'JWT_HS256':
+        jwt.jwt_hs256_template(),
+    'JWT_HS256_RAW':
+        jwt.raw_jwt_hs256_template(),
+    'JWT_HS384':
+        jwt.jwt_hs384_template(),
+    'JWT_HS384_RAW':
+        jwt.raw_jwt_hs384_template(),
+    'JWT_HS512':
+        jwt.jwt_hs512_template(),
+    'JWT_HS512_RAW':
+        jwt.raw_jwt_hs512_template(),
+    'JWT_ES256':
+        jwt.jwt_es256_template(),
+    'JWT_ES256_RAW':
+        jwt.raw_jwt_es256_template(),
+    'JWT_ES384':
+        jwt.jwt_es384_template(),
+    'JWT_ES384_RAW':
+        jwt.raw_jwt_es384_template(),
+    'JWT_ES512':
+        jwt.jwt_es512_template(),
+    'JWT_ES512_RAW':
+        jwt.raw_jwt_es512_template(),
+    'JWT_RS256_2048_F4':
+        jwt.jwt_rs256_2048_f4_template(),
+    'JWT_RS256_2048_F4_RAW':
+        jwt.raw_jwt_rs256_2048_f4_template(),
+    'JWT_RS256_3072_F4':
+        jwt.jwt_rs256_3072_f4_template(),
+    'JWT_RS256_3072_F4_RAW':
+        jwt.raw_jwt_rs256_3072_f4_template(),
+    'JWT_RS384_3072_F4':
+        jwt.jwt_rs384_3072_f4_template(),
+    'JWT_RS384_3072_F4_RAW':
+        jwt.raw_jwt_rs384_3072_f4_template(),
+    'JWT_RS512_4096_F4':
+        jwt.jwt_rs512_4096_f4_template(),
+    'JWT_RS512_4096_F4_RAW':
+        jwt.raw_jwt_rs512_4096_f4_template(),
+    'JWT_PS256_2048_F4':
+        jwt.jwt_ps256_2048_f4_template(),
+    'JWT_PS256_2048_F4_RAW':
+        jwt.raw_jwt_ps256_2048_f4_template(),
+    'JWT_PS256_3072_F4':
+        jwt.jwt_ps256_3072_f4_template(),
+    'JWT_PS256_3072_F4_RAW':
+        jwt.raw_jwt_ps256_3072_f4_template(),
+    'JWT_PS384_3072_F4':
+        jwt.jwt_ps384_3072_f4_template(),
+    'JWT_PS384_3072_F4_RAW':
+        jwt.raw_jwt_ps384_3072_f4_template(),
+    'JWT_PS512_4096_F4':
+        jwt.jwt_ps512_4096_f4_template(),
+    'JWT_PS512_4096_F4_RAW':
+        jwt.raw_jwt_ps512_4096_f4_template(),
+}
+
+
+# Key template names for which the list of supported languages is different from
+# the list of supported languages of the whole key type.
+_CUSTOM_SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME = {
+    # currently empty.
+}
+
+
+def _supported_languages_by_template(
+    template_name: str, key_type: str) -> List[str]:
+  if template_name in _CUSTOM_SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME:
+    return _CUSTOM_SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[template_name]
+  return tink_config.supported_languages_for_key_type(key_type)
+
+
+def _all_key_template_names_with_key_type():
+  for key_type, template_names in KEY_TEMPLATE_NAMES.items():
+    for template_name in template_names:
+      yield (template_name, key_type)
+
+
+def tinkey_template_names_for(primitive_class: Any) -> Iterable[str]:
+  """Returns all the key template names for the given primitive type."""
+  for key_type in tink_config.key_types_for_primitive(primitive_class):
+    for template_name in KEY_TEMPLATE_NAMES[key_type]:
+      yield template_name
+
+
+SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME = {
+    name: _supported_languages_by_template(name, template)
+    for name, template in _all_key_template_names_with_key_type()
+}
+
+
+def key_types_in_keyset(keyset: bytes) -> List[str]:
+  """Returns a list containing all key types in a keyset, in order."""
+  parsed_keyset = tink_pb2.Keyset.FromString(keyset)
+  type_urls = [k.key_data.type_url for k in parsed_keyset.key]
+  return [tink_config.key_type_from_type_url(t) for t in type_urls]
diff --git a/testing/cross_language/util/utilities_test.py b/testing/cross_language/util/utilities_test.py
new file mode 100644
index 0000000..b6fffc6
--- /dev/null
+++ b/testing/cross_language/util/utilities_test.py
@@ -0,0 +1,84 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for tink.testing.cross_language.supported_key_types."""
+
+
+from absl.testing import absltest
+from tink import aead
+from tink import mac
+
+from tink.proto import tink_pb2
+import tink_config
+from util import test_keys
+from util import utilities
+
+
+def all_key_template_names():
+  for _, names in utilities.KEY_TEMPLATE_NAMES.items():
+    for name in names:
+      yield name
+
+
+def setUpModule():
+  aead.register()
+
+
+class SupportedKeyTypesTest(absltest.TestCase):
+
+  def test_template_types_subset(self):
+    """Tests that all key types which have a template are in all_key_types()."""
+    self.assertContainsSubset(
+        set(utilities.KEY_TEMPLATE_NAMES.keys()),
+        set(tink_config.all_key_types()))
+
+  def test_all_key_templates_present(self):
+    self.assertEqual(
+        list(all_key_template_names()),
+        list(utilities.KEY_TEMPLATE.keys()))
+
+  def test_supported_lang_by_template_name_all_present(self):
+    self.assertEqual(
+        list(all_key_template_names()),
+        list(utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME.keys()))
+
+  def test_supported_langauges_by_template_name(self):
+    self.assertEqual(
+        utilities.SUPPORTED_LANGUAGES_BY_TEMPLATE_NAME[
+            'ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM'],
+        ['cc', 'java', 'go', 'python'])
+
+  def test_tinkey_template_names_for(self):
+    self.assertEqual(
+        list(utilities.tinkey_template_names_for(mac.Mac)), [
+            'AES_CMAC', 'HMAC_SHA256_128BITTAG', 'HMAC_SHA256_256BITTAG',
+            'HMAC_SHA512_256BITTAG', 'HMAC_SHA512_512BITTAG'
+        ])
+
+  def test_key_types_in_keyset_single_key(self):
+    aes_gcm_keyset = test_keys.new_or_stored_keyset(
+        aead.aead_key_templates.AES128_GCM)
+    self.assertEqual(
+        utilities.key_types_in_keyset(aes_gcm_keyset), ['AesGcmKey'])
+
+  def test_key_types_in_keyset_multiple_keys(self):
+    key1 = test_keys.new_or_stored_key(aead.aead_key_templates.AES128_GCM)
+    key2 = test_keys.new_or_stored_key(aead.aead_key_templates.AES256_GCM)
+    key3 = test_keys.new_or_stored_key(aead.aead_key_templates.AES128_EAX)
+    keyset = tink_pb2.Keyset(key=[key1, key2, key3], primary_key_id=key1.key_id)
+    self.assertEqual(
+        utilities.key_types_in_keyset(keyset.SerializeToString()),
+        ['AesGcmKey', 'AesGcmKey', 'AesEaxKey'])
+
+if __name__ == '__main__':
+  absltest.main()
diff --git a/testing/go/.bazelversion b/testing/go/.bazelversion
index ac14c3d..09b254e 100644
--- a/testing/go/.bazelversion
+++ b/testing/go/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/testing/go/BUILD.bazel b/testing/go/BUILD.bazel
index d53365d..ae6e7f8 100644
--- a/testing/go/BUILD.bazel
+++ b/testing/go/BUILD.bazel
@@ -10,14 +10,15 @@
 
 go_grpc_library(
     name = "testing_api_go_grpc",
-    importpath = "github.com/google/tink/testing/go/proto/testing_api_go_grpc",
-    protos = ["//proto:testing_api_proto"],
+    importpath = "github.com/google/tink/testing/go/protos/testing_api_go_grpc",
+    protos = ["//protos:testing_api_proto"],
 )
 
 go_library(
     name = "services",
     srcs = [
         "aead_service.go",
+        "annotated_keyset.go",
         "daead_service.go",
         "hybrid_service.go",
         "jwt_service.go",
@@ -31,21 +32,23 @@
     importpath = "github.com/google/tink/testing/go/services",
     deps = [
         ":testing_api_go_grpc",
-        "@org_golang_google_protobuf//proto:go_default_library",
-        "@org_golang_google_protobuf//types/known/structpb:go_default_library",
-        "@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
-        "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library",
-        "@tink_go//aead:go_default_library",
-        "@tink_go//daead:go_default_library",
-        "@tink_go//hybrid:go_default_library",
-        "@tink_go//jwt:go_default_library",
-        "@tink_go//keyset:go_default_library",
-        "@tink_go//mac:go_default_library",
-        "@tink_go//prf:go_default_library",
-        "@tink_go//proto/tink_go_proto:go_default_library",
-        "@tink_go//signature:go_default_library",
-        "@tink_go//streamingaead:go_default_library",
-        "@tink_go//testkeyset:go_default_library",
+        "@org_golang_google_protobuf//proto",
+        "@org_golang_google_protobuf//types/known/structpb",
+        "@org_golang_google_protobuf//types/known/timestamppb",
+        "@org_golang_google_protobuf//types/known/wrapperspb",
+        "@tink_go//aead",
+        "@tink_go//aead/internal/testing/kmsaead",
+        "@tink_go//core/registry",
+        "@tink_go//daead",
+        "@tink_go//hybrid",
+        "@tink_go//insecurecleartextkeyset",
+        "@tink_go//jwt",
+        "@tink_go//keyset",
+        "@tink_go//mac",
+        "@tink_go//prf",
+        "@tink_go//proto/tink_go_proto",
+        "@tink_go//signature",
+        "@tink_go//streamingaead",
     ],
 )
 
@@ -59,22 +62,22 @@
     deps = [
         ":services",
         ":testing_api_go_grpc",
-        "@com_github_google_go_cmp//cmp:go_default_library",
-        "@org_golang_google_protobuf//proto:go_default_library",
-        "@org_golang_google_protobuf//testing/protocmp:go_default_library",
-        "@org_golang_google_protobuf//types/known/durationpb:go_default_library",
-        "@org_golang_google_protobuf//types/known/structpb:go_default_library",
-        "@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
-        "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library",
-        "@tink_go//aead:go_default_library",
-        "@tink_go//daead:go_default_library",
-        "@tink_go//hybrid:go_default_library",
-        "@tink_go//jwt:go_default_library",
-        "@tink_go//keyset:go_default_library",
-        "@tink_go//mac:go_default_library",
-        "@tink_go//prf:go_default_library",
-        "@tink_go//signature:go_default_library",
-        "@tink_go//streamingaead:go_default_library",
+        "@com_github_google_go_cmp//cmp",
+        "@org_golang_google_protobuf//proto",
+        "@org_golang_google_protobuf//testing/protocmp",
+        "@org_golang_google_protobuf//types/known/durationpb",
+        "@org_golang_google_protobuf//types/known/structpb",
+        "@org_golang_google_protobuf//types/known/timestamppb",
+        "@org_golang_google_protobuf//types/known/wrapperspb",
+        "@tink_go//aead",
+        "@tink_go//daead",
+        "@tink_go//hybrid",
+        "@tink_go//jwt",
+        "@tink_go//keyset",
+        "@tink_go//mac",
+        "@tink_go//prf",
+        "@tink_go//signature",
+        "@tink_go//streamingaead",
     ],
 )
 
@@ -86,8 +89,11 @@
     deps = [
         ":services",
         ":testing_api_go_grpc",
-        "@org_golang_google_grpc//:go_default_library",
-        "@tink_go//core/registry:go_default_library",
-        "@tink_go//testing/fakekms:go_default_library",
+        "@org_golang_google_api//option",
+        "@org_golang_google_grpc//:grpc",
+        "@tink_go//core/registry",
+        "@tink_go//integration/awskms",
+        "@tink_go//integration/gcpkms",
+        "@tink_go//testing/fakekms",
     ],
 )
diff --git a/testing/go/WORKSPACE b/testing/go/WORKSPACE
index a9c6763..f1f18b8 100644
--- a/testing/go/WORKSPACE
+++ b/testing/go/WORKSPACE
@@ -7,46 +7,26 @@
     path = "../../go",
 )
 
-# Release from 2022-03-21
+# Release from 2023-04-20
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "f2dcd210c7095febe54b804bb1cd3a58fe8435a909db2ec04e31542631cf715c",
+    sha256 = "6dc2da7ab4cf5d7bfc7c949776b1b7c733f05e56edc4bcd9022bb249d2e2a996",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.31.0/rules_go-v0.31.0.zip",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.31.0/rules_go-v0.31.0.zip",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip",
     ],
 )
 
-# Release from 2021-10-11
+# Release from 2023-01-14
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
+    sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
     ],
 )
 
-load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
-load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
-load("@tink_go//:deps.bzl", "go_dependencies")
-
-go_dependencies()
-
-# TODO(b/213404399): Remove after Gazelle issue is fixed.
-go_repository(
-    name = "com_google_cloud_go_compute",
-    importpath = "cloud.google.com/go/compute",
-    sum = "h1:rSUBvAyVwNJ5uQCKNJFMwPtTvJkfN38b6Pvb9zZoqJ8=",
-    version = "v0.1.0",
-)
-
-go_rules_dependencies()
-
-go_register_toolchains(version = "1.17.6")
-
-gazelle_dependencies()
-
 # Release from 2021-09-15
 http_archive(
     name = "rules_proto",
@@ -58,12 +38,6 @@
     ],
 )
 
-load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
-
-rules_proto_dependencies()
-
-rules_proto_toolchains()
-
 # Release from 2021-12-12
 http_archive(
     name = "rules_proto_grpc",
@@ -72,12 +46,32 @@
     urls = ["https://github.com/rules-proto-grpc/rules_proto_grpc/archive/4.1.0.tar.gz"],
 )
 
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
+
+load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
+
+load("@tink_go//:deps.bzl", tink_go_dependencies="go_dependencies")
+
+load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
+
 load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_repos", "rules_proto_grpc_toolchains")
 
+load("@rules_proto_grpc//go:repositories.bzl", rules_proto_grpc_go_repos = "go_repos")
+
+tink_go_dependencies()
+
+go_rules_dependencies()
+
+go_register_toolchains(version = "1.19.9")
+
+rules_proto_dependencies()
+
+rules_proto_toolchains()
+
 rules_proto_grpc_toolchains()
 
 rules_proto_grpc_repos()
 
-load("@rules_proto_grpc//go:repositories.bzl", rules_proto_grpc_go_repos = "go_repos")
-
 rules_proto_grpc_go_repos()
+
+gazelle_dependencies()
diff --git a/testing/go/aead_service.go b/testing/go/aead_service.go
index b81a6ca..8306cbb 100644
--- a/testing/go/aead_service.go
+++ b/testing/go/aead_service.go
@@ -17,13 +17,13 @@
 package services
 
 import (
-	"bytes"
 	"context"
+	"fmt"
 
 	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	"github.com/google/tink/go/aead/internal/testing/kmsaead"
+	"github.com/google/tink/go/core/registry"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // AEADService implements the Aead testing service.
@@ -31,17 +31,26 @@
 	pb.AeadServer
 }
 
-func (s *AEADService) Encrypt(ctx context.Context, req *pb.AeadEncryptRequest) (*pb.AeadEncryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+func (s *AEADService) Create(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
-		return &pb.AeadEncryptResponse{
-			Result: &pb.AeadEncryptResponse_Err{err.Error()}}, nil
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = aead.New(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
+func (s *AEADService) Encrypt(ctx context.Context, req *pb.AeadEncryptRequest) (*pb.AeadEncryptResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return nil, err
 	}
 	cipher, err := aead.New(handle)
 	if err != nil {
-		return &pb.AeadEncryptResponse{
-			Result: &pb.AeadEncryptResponse_Err{err.Error()}}, nil
+		return nil, err
 	}
 	ciphertext, err := cipher.Encrypt(req.Plaintext, req.AssociatedData)
 	if err != nil {
@@ -53,16 +62,13 @@
 }
 
 func (s *AEADService) Decrypt(ctx context.Context, req *pb.AeadDecryptRequest) (*pb.AeadDecryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
-		return &pb.AeadDecryptResponse{
-			Result: &pb.AeadDecryptResponse_Err{err.Error()}}, nil
+		return nil, err
 	}
 	cipher, err := aead.New(handle)
 	if err != nil {
-		return &pb.AeadDecryptResponse{
-			Result: &pb.AeadDecryptResponse_Err{err.Error()}}, nil
+		return nil, err
 	}
 	plaintext, err := cipher.Decrypt(req.Ciphertext, req.AssociatedData)
 	if err != nil {
@@ -72,3 +78,9 @@
 	return &pb.AeadDecryptResponse{
 		Result: &pb.AeadDecryptResponse_Plaintext{plaintext}}, nil
 }
+
+func init() {
+	if err := registry.RegisterKeyManager(kmsaead.NewKeyManager()); err != nil {
+		panic(fmt.Sprintf("registry.RegisterKeyManager(kmsaead.NewKeyManager()) failed: %v", err))
+	}
+}
diff --git a/testing/go/annotated_keyset.go b/testing/go/annotated_keyset.go
new file mode 100644
index 0000000..31c5459
--- /dev/null
+++ b/testing/go/annotated_keyset.go
@@ -0,0 +1,31 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+package services
+
+import (
+	"bytes"
+
+	"github.com/google/tink/go/insecurecleartextkeyset"
+	"github.com/google/tink/go/keyset"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
+)
+
+func toKeysetHandle(annotatedKeyset *pb.AnnotatedKeyset) (*keyset.Handle, error) {
+	reader := keyset.NewBinaryReader(bytes.NewReader(annotatedKeyset.GetSerializedKeyset()))
+	a := annotatedKeyset.GetAnnotations()
+	return insecurecleartextkeyset.Read(reader, keyset.WithAnnotations(a))
+}
diff --git a/testing/go/daead_service.go b/testing/go/daead_service.go
index 8c5c87f..a8da121 100644
--- a/testing/go/daead_service.go
+++ b/testing/go/daead_service.go
@@ -17,13 +17,10 @@
 package services
 
 import (
-	"bytes"
 	"context"
 
 	"github.com/google/tink/go/daead"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // DeterministicAEADService implements the DeterministicAead testing service.
@@ -31,17 +28,26 @@
 	pb.DeterministicAeadServer
 }
 
-func (s *DeterministicAEADService) EncryptDeterministically(ctx context.Context, req *pb.DeterministicAeadEncryptRequest) (*pb.DeterministicAeadEncryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+func (s *DeterministicAEADService) Create(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
-		return &pb.DeterministicAeadEncryptResponse{
-			Result: &pb.DeterministicAeadEncryptResponse_Err{err.Error()}}, nil
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = daead.New(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
+func (s *DeterministicAEADService) EncryptDeterministically(ctx context.Context, req *pb.DeterministicAeadEncryptRequest) (*pb.DeterministicAeadEncryptResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return nil, err
 	}
 	cipher, err := daead.New(handle)
 	if err != nil {
-		return &pb.DeterministicAeadEncryptResponse{
-			Result: &pb.DeterministicAeadEncryptResponse_Err{err.Error()}}, nil
+		return nil, err
 	}
 	ciphertext, err := cipher.EncryptDeterministically(req.Plaintext, req.AssociatedData)
 	if err != nil {
@@ -53,16 +59,13 @@
 }
 
 func (s *DeterministicAEADService) DecryptDeterministically(ctx context.Context, req *pb.DeterministicAeadDecryptRequest) (*pb.DeterministicAeadDecryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
-		return &pb.DeterministicAeadDecryptResponse{
-			Result: &pb.DeterministicAeadDecryptResponse_Err{err.Error()}}, nil
+		return nil, err
 	}
 	cipher, err := daead.New(handle)
 	if err != nil {
-		return &pb.DeterministicAeadDecryptResponse{
-			Result: &pb.DeterministicAeadDecryptResponse_Err{err.Error()}}, nil
+		return nil, err
 	}
 	plaintext, err := cipher.DecryptDeterministically(req.Ciphertext, req.AssociatedData)
 	if err != nil {
diff --git a/testing/go/hybrid_service.go b/testing/go/hybrid_service.go
index 64fb3cf..7cf5f72 100644
--- a/testing/go/hybrid_service.go
+++ b/testing/go/hybrid_service.go
@@ -17,13 +17,10 @@
 package services
 
 import (
-	"bytes"
 	"context"
 
 	"github.com/google/tink/go/hybrid"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // HybridService implements the Hybrid encryption and decryption testing service.
@@ -31,9 +28,32 @@
 	pb.HybridServer
 }
 
+func (s *HybridService) CreateHybridEncrypt(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = hybrid.NewHybridEncrypt(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
+func (s *HybridService) CreateHybridDecrypt(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = hybrid.NewHybridDecrypt(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
 func (s *HybridService) Encrypt(ctx context.Context, req *pb.HybridEncryptRequest) (*pb.HybridEncryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.PublicKeyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetPublicAnnotatedKeyset())
 	if err != nil {
 		return &pb.HybridEncryptResponse{
 			Result: &pb.HybridEncryptResponse_Err{err.Error()}}, nil
@@ -53,8 +73,7 @@
 }
 
 func (s *HybridService) Decrypt(ctx context.Context, req *pb.HybridDecryptRequest) (*pb.HybridDecryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.PrivateKeyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetPrivateAnnotatedKeyset())
 	if err != nil {
 		return &pb.HybridDecryptResponse{
 			Result: &pb.HybridDecryptResponse_Err{err.Error()}}, nil
diff --git a/testing/go/jwt_service.go b/testing/go/jwt_service.go
index 664afa9..fc23734 100644
--- a/testing/go/jwt_service.go
+++ b/testing/go/jwt_service.go
@@ -25,10 +25,10 @@
 	spb "google.golang.org/protobuf/types/known/structpb"
 	tpb "google.golang.org/protobuf/types/known/timestamppb"
 	wpb "google.golang.org/protobuf/types/known/wrapperspb"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/jwt"
 	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // JWTService implements the JWT testing service.
@@ -36,6 +36,42 @@
 	pb.JwtServer
 }
 
+func (s *JWTService) CreateJwtMac(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = jwt.NewMAC(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
+func (s *JWTService) CreateJwtPublicKeySign(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = jwt.NewSigner(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
+func (s *JWTService) CreateJwtPublicKeyVerify(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = jwt.NewVerifier(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
 func refString(s *wpb.StringValue) *string {
 	if s == nil {
 		return nil
@@ -308,8 +344,7 @@
 }
 
 func (s *JWTService) ComputeMacAndEncode(ctx context.Context, req *pb.JwtSignRequest) (*pb.JwtSignResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return jwtSignResponseError(err), nil
 	}
@@ -331,8 +366,7 @@
 }
 
 func (s *JWTService) VerifyMacAndDecode(ctx context.Context, req *pb.JwtVerifyRequest) (*pb.JwtVerifyResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return jwtVerifyResponseError(err), nil
 	}
@@ -358,8 +392,7 @@
 }
 
 func (s *JWTService) PublicKeySignAndEncode(ctx context.Context, req *pb.JwtSignRequest) (*pb.JwtSignResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return jwtSignResponseError(err), nil
 	}
@@ -381,8 +414,8 @@
 }
 
 func (s *JWTService) PublicKeyVerifyAndDecode(ctx context.Context, req *pb.JwtVerifyRequest) (*pb.JwtVerifyResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return jwtVerifyResponseError(err), nil
 	}
@@ -431,7 +464,7 @@
 		return jwtFromJwkSetResponseError(err), nil
 	}
 	b := &bytes.Buffer{}
-	if err := testkeyset.Write(handle, keyset.NewBinaryWriter(b)); err != nil {
+	if err := insecurecleartextkeyset.Write(handle, keyset.NewBinaryWriter(b)); err != nil {
 		return jwtFromJwkSetResponseError(err), nil
 	}
 	return &pb.JwtFromJwkSetResponse{
diff --git a/testing/go/jwt_service_test.go b/testing/go/jwt_service_test.go
index 3640ff2..5a66978 100644
--- a/testing/go/jwt_service_test.go
+++ b/testing/go/jwt_service_test.go
@@ -31,8 +31,9 @@
 	"google.golang.org/protobuf/testing/protocmp"
 	"github.com/google/tink/go/aead"
 	"github.com/google/tink/go/jwt"
+	"github.com/google/tink/go/signature"
 	"github.com/google/tink/testing/go/services"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 func verifiedJWTFromResponse(response *pb.JwtVerifyResponse) (*pb.JwtToken, error) {
@@ -120,7 +121,7 @@
 			if err != nil {
 				t.Fatalf("genKeyset failed: %v", err)
 			}
-			signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{Keyset: keyset, RawJwt: tc.rawJWT})
+			signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, RawJwt: tc.rawJWT})
 			if err != nil {
 				t.Fatalf("jwtService.ComputeMacAndEncode() err = %v, want nil", err)
 			}
@@ -131,6 +132,55 @@
 	}
 }
 
+func TestSuccessfulJwtMacCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	jwtService := &services.JWTService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(jwt.HS256Template())
+	if err != nil {
+		t.Fatalf("proto.Marshal(jwt.HS256Template()) failed: %v, want nil", err)
+	}
+
+	keyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := jwtService.CreateJwtMac(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}})
+	if err != nil {
+		t.Fatalf("CreateJwtMac with good keyset failed with gRPC error: %v, want nil", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateJwtMac with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingJwtMacCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	jwtService := &services.JWTService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a JwtMac
+	template, err := proto.Marshal(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	badKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := jwtService.CreateJwtMac(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: badKeyset}})
+	if err != nil {
+		t.Fatalf("CreateJwtMac with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("result.GetErr() of bad keyset after CreateJwtMac is empty, want not empty")
+	}
+}
+
 func TestJWTComputeMACWithInvalidKeysetFails(t *testing.T) {
 	keysetService := &services.KeysetService{}
 	jwtService := &services.JWTService{}
@@ -147,7 +197,7 @@
 		TypeHeader: &wpb.StringValue{Value: "JWT"},
 		Issuer:     &wpb.StringValue{Value: "issuer"},
 	}
-	signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{Keyset: keyset, RawJwt: rawJWT})
+	signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, RawJwt: rawJWT})
 	if err != nil {
 		t.Fatalf("jwtService.ComputeMacAndEncode() err = %v, want nil", err)
 	}
@@ -234,7 +284,7 @@
 				t.Fatalf("genKeyset failed: %v", err)
 			}
 
-			signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{Keyset: keyset, RawJwt: tc.rawJWT})
+			signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, RawJwt: tc.rawJWT})
 			if err != nil {
 				t.Fatalf("jwtService.ComputeMacAndEncode() err = %v, want nil", err)
 			}
@@ -242,7 +292,7 @@
 			if err != nil {
 				t.Fatalf("JwtSignResponse_Err: %v", err)
 			}
-			verifyResponse, err := jwtService.VerifyMacAndDecode(ctx, &pb.JwtVerifyRequest{Keyset: keyset, SignedCompactJwt: compact, Validator: tc.validator})
+			verifyResponse, err := jwtService.VerifyMacAndDecode(ctx, &pb.JwtVerifyRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, SignedCompactJwt: compact, Validator: tc.validator})
 			if err != nil {
 				t.Fatalf("jwtService.VerifyMacAndDecode() err = %v, want nil", err)
 			}
@@ -275,7 +325,7 @@
 		NotBefore:  &tpb.Timestamp{Seconds: 12345},
 		IssuedAt:   &tpb.Timestamp{Seconds: 1234},
 	}
-	signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{Keyset: keyset, RawJwt: rawJWT})
+	signResponse, err := jwtService.ComputeMacAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, RawJwt: rawJWT})
 	if err != nil {
 		t.Fatalf("jwtService.ComputeMacAndEncode() err = %v, want nil", err)
 	}
@@ -287,7 +337,7 @@
 		ExpectedTypeHeader: &wpb.StringValue{Value: "JWT"},
 		Now:                &tpb.Timestamp{Seconds: 12345},
 	}
-	verifyResponse, err := jwtService.VerifyMacAndDecode(ctx, &pb.JwtVerifyRequest{Keyset: keyset, SignedCompactJwt: compact, Validator: validator})
+	verifyResponse, err := jwtService.VerifyMacAndDecode(ctx, &pb.JwtVerifyRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, SignedCompactJwt: compact, Validator: validator})
 	if err != nil {
 		t.Fatalf("jwtService.VerifyMacAndDecode() err = %v, want nil", err)
 	}
@@ -319,7 +369,7 @@
 		},
 	} {
 		t.Run(tc.tag, func(t *testing.T) {
-			verifyResponse, err := jwtService.VerifyMacAndDecode(ctx, &pb.JwtVerifyRequest{Keyset: keyset, SignedCompactJwt: compact, Validator: tc.validator})
+			verifyResponse, err := jwtService.VerifyMacAndDecode(ctx, &pb.JwtVerifyRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}, SignedCompactJwt: compact, Validator: tc.validator})
 			if err != nil {
 				t.Fatalf("jwtService.VerifyMacAndDecode() err = %v, want nil", err)
 			}
@@ -330,6 +380,112 @@
 	}
 }
 
+func TestSuccessfulJwtSignVerifyCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	jwtService := &services.JWTService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(jwt.ES256Template())
+	if err != nil {
+		t.Fatalf("proto.Marshal(hybrid.ES256Template()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := jwtService.CreateJwtPublicKeySign(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("CreateJwtPublicKeySign with good keyset failed with gRPC error: %v, want nil", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateJwtPublicKeySign with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestSuccessfulJwtVerifyCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	jwtService := &services.JWTService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(jwt.ES256Template())
+	if err != nil {
+		t.Fatalf("proto.Marshal(hybrid.ES256Template()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+	publicKeyset, err := pubKeyset(ctx, keysetService, privateKeyset)
+	if err != nil {
+		t.Fatalf("pubKeyset failed: %v", err)
+	}
+
+	result, err := jwtService.CreateJwtPublicKeyVerify(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}})
+	if err != nil {
+		t.Fatalf("CreateJwtPublicKeyVerify with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateJwtPublicKeyVerify with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingJwtSignCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	jwtService := &services.JWTService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a hybrid encrypt
+	template, err := proto.Marshal(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := jwtService.CreateJwtPublicKeySign(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("CreateJwtPublicKeySign with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreateJwtPublicKeySign with bad keyset succeeded")
+	}
+}
+
+func TestFailingJwtVerifyCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	jwtService := &services.JWTService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a hybrid encrypt
+	template, err := proto.Marshal(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+	publicKeyset, err := pubKeyset(ctx, keysetService, privateKeyset)
+	if err != nil {
+		t.Fatalf("pubKeyset failed: %v", err)
+	}
+
+	result, err := jwtService.CreateJwtPublicKeyVerify(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}})
+	if err != nil {
+		t.Fatalf("CreateJwtPublicKeyVerify with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreateJwtPublicKeyVerify with bad keyset succeeded")
+	}
+}
+
 func TestJWTPublicKeySignWithInvalidKeysetFails(t *testing.T) {
 	keysetService := &services.KeysetService{}
 	jwtService := &services.JWTService{}
@@ -346,7 +502,7 @@
 	rawJWT := &pb.JwtToken{
 		Subject: &wpb.StringValue{Value: "tink-subject"},
 	}
-	signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{Keyset: privateKeyset, RawJwt: rawJWT})
+	signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}, RawJwt: rawJWT})
 	if err != nil {
 		t.Fatalf("jwtService.PublicKeySignAndEncode() err = %v", err)
 	}
@@ -391,7 +547,7 @@
 		},
 	} {
 		t.Run(tc.tag, func(t *testing.T) {
-			signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{Keyset: privateKeyset, RawJwt: tc.rawJWT})
+			signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}, RawJwt: tc.rawJWT})
 			if err != nil {
 				t.Fatalf("jwtService.PublicKeySignAndEncode() err = %v", err)
 			}
@@ -422,7 +578,7 @@
 	rawJWT := &pb.JwtToken{
 		Subject: &wpb.StringValue{Value: "tink-subject"},
 	}
-	signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{Keyset: privateKeyset, RawJwt: rawJWT})
+	signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}, RawJwt: rawJWT})
 	if err != nil {
 		t.Fatalf("jwtService.PublicKeySignAndEncode() err = %v", err)
 	}
@@ -433,7 +589,7 @@
 	validator := &pb.JwtValidator{
 		ExpectedTypeHeader: &wpb.StringValue{Value: "JWT"},
 	}
-	verifyResponse, err := jwtService.PublicKeyVerifyAndDecode(ctx, &pb.JwtVerifyRequest{Keyset: publicKeyset, SignedCompactJwt: compact, Validator: validator})
+	verifyResponse, err := jwtService.PublicKeyVerifyAndDecode(ctx, &pb.JwtVerifyRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}, SignedCompactJwt: compact, Validator: validator})
 	if err != nil {
 		t.Fatalf("jwtVerifySignature failed: %v", err)
 	}
@@ -462,7 +618,7 @@
 	rawJWT := &pb.JwtToken{
 		Subject: &wpb.StringValue{Value: "tink-subject"},
 	}
-	signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{Keyset: privateKeyset, RawJwt: rawJWT})
+	signResponse, err := jwtService.PublicKeySignAndEncode(ctx, &pb.JwtSignRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}, RawJwt: rawJWT})
 	if err != nil {
 		t.Fatalf("jwtService.PublicKeySignAndEncode() err = %v", err)
 	}
@@ -473,7 +629,7 @@
 	validator := &pb.JwtValidator{
 		AllowMissingExpiration: true,
 	}
-	verifyResponse, err := jwtService.PublicKeyVerifyAndDecode(ctx, &pb.JwtVerifyRequest{Keyset: publicKeyset, SignedCompactJwt: compact, Validator: validator})
+	verifyResponse, err := jwtService.PublicKeyVerifyAndDecode(ctx, &pb.JwtVerifyRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}, SignedCompactJwt: compact, Validator: validator})
 	if err != nil {
 		t.Fatalf("jwtVerifySignature failed: %v", err)
 	}
diff --git a/testing/go/keyset_service.go b/testing/go/keyset_service.go
index 7808d00..497e1ad 100644
--- a/testing/go/keyset_service.go
+++ b/testing/go/keyset_service.go
@@ -25,15 +25,15 @@
 	"github.com/google/tink/go/aead"
 	"github.com/google/tink/go/daead"
 	"github.com/google/tink/go/hybrid"
+	"github.com/google/tink/go/insecurecleartextkeyset"
 	"github.com/google/tink/go/jwt"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
 	"github.com/google/tink/go/prf"
 	"github.com/google/tink/go/signature"
 	"github.com/google/tink/go/streamingaead"
-	"github.com/google/tink/go/testkeyset"
 	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 
 	"google.golang.org/protobuf/proto"
 )
@@ -47,23 +47,26 @@
 func (s *KeysetService) GetTemplate(ctx context.Context, req *pb.KeysetTemplateRequest) (*pb.KeysetTemplateResponse, error) {
 	if s.Templates == nil {
 		s.Templates = map[string]*tinkpb.KeyTemplate{
-			"AES128_GCM":                                                 aead.AES128GCMKeyTemplate(),
-			"AES256_GCM":                                                 aead.AES256GCMKeyTemplate(),
-			"AES256_GCM_RAW":                                             aead.AES256GCMNoPrefixKeyTemplate(),
-			"AES128_CTR_HMAC_SHA256":                                     aead.AES128CTRHMACSHA256KeyTemplate(),
-			"AES256_CTR_HMAC_SHA256":                                     aead.AES256CTRHMACSHA256KeyTemplate(),
-			"CHACHA20_POLY1305":                                          aead.ChaCha20Poly1305KeyTemplate(),
-			"XCHACHA20_POLY1305":                                         aead.XChaCha20Poly1305KeyTemplate(),
-			"AES256_SIV":                                                 daead.AESSIVKeyTemplate(),
-			"AES128_CTR_HMAC_SHA256_4KB":                                 streamingaead.AES128CTRHMACSHA256Segment4KBKeyTemplate(),
-			"AES128_CTR_HMAC_SHA256_1MB":                                 streamingaead.AES128CTRHMACSHA256Segment1MBKeyTemplate(),
-			"AES256_CTR_HMAC_SHA256_4KB":                                 streamingaead.AES256CTRHMACSHA256Segment4KBKeyTemplate(),
-			"AES256_CTR_HMAC_SHA256_1MB":                                 streamingaead.AES256CTRHMACSHA256Segment1MBKeyTemplate(),
-			"AES128_GCM_HKDF_4KB":                                        streamingaead.AES128GCMHKDF4KBKeyTemplate(),
-			"AES128_GCM_HKDF_1MB":                                        streamingaead.AES128GCMHKDF1MBKeyTemplate(),
-			"AES256_GCM_HKDF_4KB":                                        streamingaead.AES256GCMHKDF4KBKeyTemplate(),
-			"AES256_GCM_HKDF_1MB":                                        streamingaead.AES256GCMHKDF1MBKeyTemplate(),
-			"ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM":                     hybrid.ECIESHKDFAES128GCMKeyTemplate(),
+			"AES128_GCM":                             aead.AES128GCMKeyTemplate(),
+			"AES256_GCM":                             aead.AES256GCMKeyTemplate(),
+			"AES256_GCM_RAW":                         aead.AES256GCMNoPrefixKeyTemplate(),
+			"AES128_GCM_SIV":                         aead.AES128GCMSIVKeyTemplate(),
+			"AES256_GCM_SIV":                         aead.AES256GCMSIVKeyTemplate(),
+			"AES256_GCM_SIV_RAW":                     aead.AES256GCMSIVNoPrefixKeyTemplate(),
+			"AES128_CTR_HMAC_SHA256":                 aead.AES128CTRHMACSHA256KeyTemplate(),
+			"AES256_CTR_HMAC_SHA256":                 aead.AES256CTRHMACSHA256KeyTemplate(),
+			"CHACHA20_POLY1305":                      aead.ChaCha20Poly1305KeyTemplate(),
+			"XCHACHA20_POLY1305":                     aead.XChaCha20Poly1305KeyTemplate(),
+			"AES256_SIV":                             daead.AESSIVKeyTemplate(),
+			"AES128_CTR_HMAC_SHA256_4KB":             streamingaead.AES128CTRHMACSHA256Segment4KBKeyTemplate(),
+			"AES128_CTR_HMAC_SHA256_1MB":             streamingaead.AES128CTRHMACSHA256Segment1MBKeyTemplate(),
+			"AES256_CTR_HMAC_SHA256_4KB":             streamingaead.AES256CTRHMACSHA256Segment4KBKeyTemplate(),
+			"AES256_CTR_HMAC_SHA256_1MB":             streamingaead.AES256CTRHMACSHA256Segment1MBKeyTemplate(),
+			"AES128_GCM_HKDF_4KB":                    streamingaead.AES128GCMHKDF4KBKeyTemplate(),
+			"AES128_GCM_HKDF_1MB":                    streamingaead.AES128GCMHKDF1MBKeyTemplate(),
+			"AES256_GCM_HKDF_4KB":                    streamingaead.AES256GCMHKDF4KBKeyTemplate(),
+			"AES256_GCM_HKDF_1MB":                    streamingaead.AES256GCMHKDF1MBKeyTemplate(),
+			"ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM": hybrid.ECIESHKDFAES128GCMKeyTemplate(),
 			"ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256":         hybrid.ECIESHKDFAES128CTRHMACSHA256KeyTemplate(),
 			"DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM":           hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Key_Template(),
 			"DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_RAW":       hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_128_GCM_Raw_Key_Template(),
@@ -71,36 +74,53 @@
 			"DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_RAW":       hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM_Raw_Key_Template(),
 			"DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305":     hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Key_Template(),
 			"DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_RAW": hybrid.DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_CHACHA20_POLY1305_Raw_Key_Template(),
-			"AES_CMAC":                     mac.AESCMACTag128KeyTemplate(),
-			"HMAC_SHA256_128BITTAG":        mac.HMACSHA256Tag128KeyTemplate(),
-			"HMAC_SHA256_256BITTAG":        mac.HMACSHA256Tag256KeyTemplate(),
-			"HMAC_SHA512_256BITTAG":        mac.HMACSHA512Tag256KeyTemplate(),
-			"HMAC_SHA512_512BITTAG":        mac.HMACSHA512Tag512KeyTemplate(),
-			"ECDSA_P256":                   signature.ECDSAP256KeyTemplate(),
-			"ECDSA_P256_RAW":               signature.ECDSAP256RawKeyTemplate(),
-			"ECDSA_P384":                   signature.ECDSAP384KeyTemplate(),
-			"ECDSA_P384_SHA384":            signature.ECDSAP384SHA384KeyTemplate(),
-			"ECDSA_P384_SHA512":            signature.ECDSAP384SHA512KeyTemplate(),
-			"ECDSA_P521":                   signature.ECDSAP521KeyTemplate(),
-			"ED25519":                      signature.ED25519KeyTemplate(),
-			"RSA_SSA_PKCS1_3072_SHA256_F4": signature.RSA_SSA_PKCS1_3072_SHA256_F4_Key_Template(),
-			"RSA_SSA_PKCS1_4096_SHA512_F4": signature.RSA_SSA_PKCS1_4096_SHA512_F4_Key_Template(),
-			"AES_CMAC_PRF":                 prf.AESCMACPRFKeyTemplate(),
-			"HMAC_SHA256_PRF":              prf.HMACSHA256PRFKeyTemplate(),
-			"HMAC_SHA512_PRF":              prf.HMACSHA512PRFKeyTemplate(),
-			"HKDF_SHA256":                  prf.HKDFSHA256PRFKeyTemplate(),
-			"JWT_HS256":                    jwt.HS256Template(),
-			"JWT_HS256_RAW":                jwt.RawHS256Template(),
-			"JWT_HS384":                    jwt.HS384Template(),
-			"JWT_HS384_RAW":                jwt.RawHS384Template(),
-			"JWT_HS512":                    jwt.HS512Template(),
-			"JWT_HS512_RAW":                jwt.RawHS512Template(),
-			"JWT_ES256":                    jwt.ES256Template(),
-			"JWT_ES256_RAW":                jwt.RawES256Template(),
-			"JWT_ES384":                    jwt.ES384Template(),
-			"JWT_ES384_RAW":                jwt.RawES384Template(),
-			"JWT_ES512":                    jwt.ES512Template(),
-			"JWT_ES512_RAW":                jwt.RawES512Template(),
+			"AES_CMAC":                             mac.AESCMACTag128KeyTemplate(),
+			"HMAC_SHA256_128BITTAG":                mac.HMACSHA256Tag128KeyTemplate(),
+			"HMAC_SHA256_256BITTAG":                mac.HMACSHA256Tag256KeyTemplate(),
+			"HMAC_SHA512_256BITTAG":                mac.HMACSHA512Tag256KeyTemplate(),
+			"HMAC_SHA512_512BITTAG":                mac.HMACSHA512Tag512KeyTemplate(),
+			"ECDSA_P256":                           signature.ECDSAP256KeyTemplate(),
+			"ECDSA_P256_RAW":                       signature.ECDSAP256RawKeyTemplate(),
+			"ECDSA_P384_SHA384":                    signature.ECDSAP384SHA384KeyTemplate(),
+			"ECDSA_P384_SHA512":                    signature.ECDSAP384SHA512KeyTemplate(),
+			"ECDSA_P521":                           signature.ECDSAP521KeyTemplate(),
+			"ED25519":                              signature.ED25519KeyTemplate(),
+			"RSA_SSA_PKCS1_3072_SHA256_F4":         signature.RSA_SSA_PKCS1_3072_SHA256_F4_Key_Template(),
+			"RSA_SSA_PKCS1_4096_SHA512_F4":         signature.RSA_SSA_PKCS1_4096_SHA512_F4_Key_Template(),
+			"RSA_SSA_PSS_3072_SHA256_SHA256_32_F4": signature.RSA_SSA_PSS_3072_SHA256_32_F4_Key_Template(),
+			"RSA_SSA_PSS_4096_SHA512_SHA512_64_F4": signature.RSA_SSA_PSS_4096_SHA512_64_F4_Key_Template(),
+			"AES_CMAC_PRF":                         prf.AESCMACPRFKeyTemplate(),
+			"HMAC_SHA256_PRF":                      prf.HMACSHA256PRFKeyTemplate(),
+			"HMAC_SHA512_PRF":                      prf.HMACSHA512PRFKeyTemplate(),
+			"HKDF_SHA256":                          prf.HKDFSHA256PRFKeyTemplate(),
+			"JWT_HS256":                            jwt.HS256Template(),
+			"JWT_HS256_RAW":                        jwt.RawHS256Template(),
+			"JWT_HS384":                            jwt.HS384Template(),
+			"JWT_HS384_RAW":                        jwt.RawHS384Template(),
+			"JWT_HS512":                            jwt.HS512Template(),
+			"JWT_HS512_RAW":                        jwt.RawHS512Template(),
+			"JWT_ES256":                            jwt.ES256Template(),
+			"JWT_ES256_RAW":                        jwt.RawES256Template(),
+			"JWT_ES384":                            jwt.ES384Template(),
+			"JWT_ES384_RAW":                        jwt.RawES384Template(),
+			"JWT_ES512":                            jwt.ES512Template(),
+			"JWT_ES512_RAW":                        jwt.RawES512Template(),
+			"JWT_RS256_2048_F4":                    jwt.RS256_2048_F4_Key_Template(),
+			"JWT_RS256_2048_F4_RAW":                jwt.RawRS256_2048_F4_Key_Template(),
+			"JWT_RS256_3072_F4":                    jwt.RS256_3072_F4_Key_Template(),
+			"JWT_RS256_3072_F4_RAW":                jwt.RawRS256_3072_F4_Key_Template(),
+			"JWT_RS384_3072_F4":                    jwt.RS384_3072_F4_Key_Template(),
+			"JWT_RS384_3072_F4_RAW":                jwt.RawRS384_3072_F4_Key_Template(),
+			"JWT_RS512_4096_F4":                    jwt.RS512_4096_F4_Key_Template(),
+			"JWT_RS512_4096_F4_RAW":                jwt.RawRS512_4096_F4_Key_Template(),
+			"JWT_PS256_2048_F4":                    jwt.PS256_2048_F4_Key_Template(),
+			"JWT_PS256_2048_F4_RAW":                jwt.RawPS256_2048_F4_Key_Template(),
+			"JWT_PS256_3072_F4":                    jwt.PS256_3072_F4_Key_Template(),
+			"JWT_PS256_3072_F4_RAW":                jwt.RawPS256_3072_F4_Key_Template(),
+			"JWT_PS384_3072_F4":                    jwt.PS384_3072_F4_Key_Template(),
+			"JWT_PS384_3072_F4_RAW":                jwt.RawPS384_3072_F4_Key_Template(),
+			"JWT_PS512_4096_F4":                    jwt.PS512_4096_F4_Key_Template(),
+			"JWT_PS512_4096_F4_RAW":                jwt.RawPS512_4096_F4_Key_Template(),
 		}
 	}
 	template, success := s.Templates[req.GetTemplateName()]
@@ -131,7 +151,7 @@
 	}
 	buf := new(bytes.Buffer)
 	writer := keyset.NewBinaryWriter(buf)
-	err = testkeyset.Write(handle, writer)
+	err = insecurecleartextkeyset.Write(handle, writer)
 	if err != nil {
 		return &pb.KeysetGenerateResponse{
 			Result: &pb.KeysetGenerateResponse_Err{err.Error()}}, nil
@@ -142,7 +162,7 @@
 
 func (s *KeysetService) Public(ctx context.Context, req *pb.KeysetPublicRequest) (*pb.KeysetPublicResponse, error) {
 	reader := keyset.NewBinaryReader(bytes.NewReader(req.PrivateKeyset))
-	privateHandle, err := testkeyset.Read(reader)
+	privateHandle, err := insecurecleartextkeyset.Read(reader)
 	if err != nil {
 		return &pb.KeysetPublicResponse{
 			Result: &pb.KeysetPublicResponse_Err{err.Error()}}, nil
@@ -154,7 +174,7 @@
 	}
 	buf := new(bytes.Buffer)
 	writer := keyset.NewBinaryWriter(buf)
-	err = testkeyset.Write(publicHandle, writer)
+	err = insecurecleartextkeyset.Write(publicHandle, writer)
 	if err != nil {
 		return &pb.KeysetPublicResponse{
 			Result: &pb.KeysetPublicResponse_Err{err.Error()}}, nil
@@ -165,14 +185,14 @@
 
 func (s *KeysetService) ToJson(ctx context.Context, req *pb.KeysetToJsonRequest) (*pb.KeysetToJsonResponse, error) {
 	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := insecurecleartextkeyset.Read(reader)
 	if err != nil {
 		return &pb.KeysetToJsonResponse{
 			Result: &pb.KeysetToJsonResponse_Err{err.Error()}}, nil
 	}
 	buf := new(bytes.Buffer)
 	writer := keyset.NewJSONWriter(buf)
-	if err := testkeyset.Write(handle, writer); err != nil {
+	if err := insecurecleartextkeyset.Write(handle, writer); err != nil {
 		return &pb.KeysetToJsonResponse{
 			Result: &pb.KeysetToJsonResponse_Err{err.Error()}}, nil
 	}
@@ -182,14 +202,14 @@
 
 func (s *KeysetService) FromJson(ctx context.Context, req *pb.KeysetFromJsonRequest) (*pb.KeysetFromJsonResponse, error) {
 	reader := keyset.NewJSONReader(bytes.NewBufferString(req.JsonKeyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := insecurecleartextkeyset.Read(reader)
 	if err != nil {
 		return &pb.KeysetFromJsonResponse{
 			Result: &pb.KeysetFromJsonResponse_Err{err.Error()}}, nil
 	}
 	buf := new(bytes.Buffer)
 	writer := keyset.NewBinaryWriter(buf)
-	if err := testkeyset.Write(handle, writer); err != nil {
+	if err := insecurecleartextkeyset.Write(handle, writer); err != nil {
 		return &pb.KeysetFromJsonResponse{
 			Result: &pb.KeysetFromJsonResponse_Err{err.Error()}}, nil
 	}
@@ -199,7 +219,7 @@
 
 func (s *KeysetService) WriteEncrypted(ctx context.Context, req *pb.KeysetWriteEncryptedRequest) (*pb.KeysetWriteEncryptedResponse, error) {
 	masterReader := keyset.NewBinaryReader(bytes.NewReader(req.GetMasterKeyset()))
-	masterHandle, err := testkeyset.Read(masterReader)
+	masterHandle, err := insecurecleartextkeyset.Read(masterReader)
 	if err != nil {
 		return &pb.KeysetWriteEncryptedResponse{
 			Result: &pb.KeysetWriteEncryptedResponse_Err{err.Error()}}, nil
@@ -211,7 +231,7 @@
 	}
 
 	reader := keyset.NewBinaryReader(bytes.NewReader(req.GetKeyset()))
-	handle, err := testkeyset.Read(reader)
+	handle, err := insecurecleartextkeyset.Read(reader)
 	if err != nil {
 		return &pb.KeysetWriteEncryptedResponse{
 			Result: &pb.KeysetWriteEncryptedResponse_Err{err.Error()}}, nil
@@ -241,7 +261,7 @@
 
 func (s *KeysetService) ReadEncrypted(ctx context.Context, req *pb.KeysetReadEncryptedRequest) (*pb.KeysetReadEncryptedResponse, error) {
 	masterReader := keyset.NewBinaryReader(bytes.NewReader(req.GetMasterKeyset()))
-	masterHandle, err := testkeyset.Read(masterReader)
+	masterHandle, err := insecurecleartextkeyset.Read(masterReader)
 	if err != nil {
 		return &pb.KeysetReadEncryptedResponse{
 			Result: &pb.KeysetReadEncryptedResponse_Err{err.Error()}}, nil
@@ -273,7 +293,7 @@
 
 	buf := new(bytes.Buffer)
 	writer := keyset.NewBinaryWriter(buf)
-	if err := testkeyset.Write(handle, writer); err != nil {
+	if err := insecurecleartextkeyset.Write(handle, writer); err != nil {
 		return &pb.KeysetReadEncryptedResponse{
 			Result: &pb.KeysetReadEncryptedResponse_Err{err.Error()}}, nil
 	}
diff --git a/testing/go/mac_service.go b/testing/go/mac_service.go
index d1e36c1..e1831bf 100644
--- a/testing/go/mac_service.go
+++ b/testing/go/mac_service.go
@@ -17,13 +17,10 @@
 package services
 
 import (
-	"bytes"
 	"context"
 
-	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // MacService implements the MAC testing service.
@@ -31,9 +28,21 @@
 	pb.MacServer
 }
 
+func (s *MacService) Create(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = mac.New(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
 func (s *MacService) ComputeMac(ctx context.Context, req *pb.ComputeMacRequest) (*pb.ComputeMacResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return &pb.ComputeMacResponse{
 			Result: &pb.ComputeMacResponse_Err{err.Error()}}, nil
@@ -53,8 +62,8 @@
 }
 
 func (s *MacService) VerifyMac(ctx context.Context, req *pb.VerifyMacRequest) (*pb.VerifyMacResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return &pb.VerifyMacResponse{Err: err.Error()}, nil
 	}
diff --git a/testing/go/metadata_service.go b/testing/go/metadata_service.go
index c40a460..4189474 100644
--- a/testing/go/metadata_service.go
+++ b/testing/go/metadata_service.go
@@ -19,7 +19,7 @@
 import (
 	"context"
 
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // MetadataService implements the Keyset testing service.
diff --git a/testing/go/prf_set_service.go b/testing/go/prf_set_service.go
index 419ab2a..c0b48c8 100644
--- a/testing/go/prf_set_service.go
+++ b/testing/go/prf_set_service.go
@@ -17,13 +17,10 @@
 package services
 
 import (
-	"bytes"
 	"context"
 
-	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/prf"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // PrfSetService implements the PrfSet testing service.
@@ -31,9 +28,20 @@
 	pb.PrfSetServer
 }
 
+func (s *PrfSetService) Create(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = prf.NewPRFSet(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
 func (s *PrfSetService) KeyIds(ctx context.Context, req *pb.PrfSetKeyIdsRequest) (*pb.PrfSetKeyIdsResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return &pb.PrfSetKeyIdsResponse{
 			Result: &pb.PrfSetKeyIdsResponse_Err{err.Error()}}, nil
@@ -53,8 +61,7 @@
 }
 
 func (s *PrfSetService) Compute(ctx context.Context, req *pb.PrfSetComputeRequest) (*pb.PrfSetComputeResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return &pb.PrfSetComputeResponse{
 			Result: &pb.PrfSetComputeResponse_Err{err.Error()}}, nil
diff --git a/testing/go/proto/testing_api.proto b/testing/go/proto/testing_api.proto
deleted file mode 100644
index 2fb0e9e..0000000
--- a/testing/go/proto/testing_api.proto
+++ /dev/null
@@ -1,503 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-syntax = "proto3";
-
-package tink_testing_api;
-
-import "google/protobuf/duration.proto";
-import "google/protobuf/timestamp.proto";
-import "google/protobuf/wrappers.proto";
-
-option java_package = "com.google.crypto.tink.testing.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
-// Placeholder for java_stubby_library
-
-// Service providing metadata about the server.
-service Metadata {
-  // Returns some server information. A test may use this information to verify
-  // that it is talking to the right server.
-  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
-}
-
-message ServerInfoRequest {}
-
-message ServerInfoResponse {
-  string tink_version = 1;  // For example '1.4'
-  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
-}
-
-// Service for Keyset operations.
-service Keyset {
-  // Generates a key template from a key template name.
-  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
-  // Generates a new keyset from a template.
-  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
-  // Generates a public-key keyset from a private-key keyset.
-  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
-  // Converts a Keyset from Binary to Json Format
-  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
-  // Converts a Keyset from Json to Binary Format
-  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
-  // Reads an encrypted keyset using KeysetHandle.read() or
-  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
-  rpc ReadEncrypted(KeysetReadEncryptedRequest)
-      returns (KeysetReadEncryptedResponse) {}
-  // Writes an encrypted keyset using KeysetHandle.write() or
-  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
-  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
-      returns (KeysetWriteEncryptedResponse) {}
-}
-
-message KeysetTemplateRequest {
-  string template_name = 1;  // template name used by Tinkey
-}
-
-message KeysetTemplateResponse {
-  oneof result {
-    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
-    string err = 2;
-  }
-}
-
-message KeysetGenerateRequest {
-  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
-}
-
-message KeysetGenerateResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetPublicRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetPublicResponse {
-  oneof result {
-    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetToJsonRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetToJsonResponse {
-  oneof result {
-    string json_keyset = 1;
-    string err = 2;
-  }
-}
-
-message KeysetFromJsonRequest {
-  string json_keyset = 1;
-}
-
-message KeysetFromJsonResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-// Copy of google.protobuf.BytesValue
-message BytesValue {
-  // The bytes value.
-  bytes value = 1;
-}
-
-enum KeysetReaderType {
-  KEYSET_READER_UNKNOWN = 0;
-  KEYSET_READER_BINARY = 1;
-  KEYSET_READER_JSON = 2;
-}
-
-message KeysetReadEncryptedRequest {
-  bytes encrypted_keyset = 1;
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetReaderType keyset_reader_type = 4;
-}
-
-message KeysetReadEncryptedResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-enum KeysetWriterType {
-  KEYSET_WRITER_UNKNOWN = 0;
-  KEYSET_WRITER_BINARY = 1;
-  KEYSET_WRITER_JSON = 2;
-}
-
-message KeysetWriteEncryptedRequest {
-  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetWriterType keyset_writer_type = 4;
-}
-
-message KeysetWriteEncryptedResponse {
-  oneof result {
-    bytes encrypted_keyset = 1;
-    string err = 2;
-  }
-}
-
-// Service for AEAD encryption and decryption
-service Aead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
-}
-
-message AeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message AeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Deterministic AEAD encryption and decryption
-service DeterministicAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
-      returns (DeterministicAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
-      returns (DeterministicAeadDecryptResponse) {}
-}
-
-message DeterministicAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message DeterministicAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Streaming AEAD encryption and decryption
-service StreamingAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(StreamingAeadEncryptRequest)
-      returns (StreamingAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(StreamingAeadDecryptRequest)
-      returns (StreamingAeadDecryptResponse) {}
-}
-
-message StreamingAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message StreamingAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to compute and verify MACs
-service Mac {
-  // Computes a MAC for given data
-  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
-  // Verifies the validity of the MAC value, no error means success
-  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
-}
-
-message ComputeMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message ComputeMacResponse {
-  oneof result {
-    bytes mac_value = 1;
-    string err = 2;
-  }
-}
-
-message VerifyMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes mac_value = 2;
-  bytes data = 3;
-}
-
-message VerifyMacResponse {
-  string err = 1;
-}
-
-// Service to hybrid encrypt and decrypt
-service Hybrid {
-  // Encrypts plaintext binding context_info to the resulting ciphertext
-  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
-  // Decrypts ciphertext verifying the integrity of context_info
-  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
-}
-
-message HybridEncryptRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes context_info = 3;
-}
-
-message HybridEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message HybridDecryptRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes context_info = 3;
-}
-
-message HybridDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to sign and verify signatures.
-service Signature {
-  // Computes the signature for data
-  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
-  // Verifies that signature is a digital signature for data
-  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
-}
-
-message SignatureSignRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message SignatureSignResponse {
-  oneof result {
-    bytes signature = 1;
-    string err = 2;
-  }
-}
-
-message SignatureVerifyRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes signature = 2;
-  bytes data = 3;
-}
-
-message SignatureVerifyResponse {
-  string err = 1;
-}
-
-// Service for PrfSet computation
-service PrfSet {
-  // Returns the key ids and the primary key id in the keyset.
-  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
-  // Computes the output of the PRF with the given key_id in the PrfSet.
-  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
-}
-
-message PrfSetKeyIdsRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message PrfSetKeyIdsResponse {
-  message Output {
-    uint32 primary_key_id = 1;
-    repeated uint32 key_id = 2;
-  }
-  oneof result {
-    Output output = 1;
-    string err = 2;
-  }
-}
-
-message PrfSetComputeRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  uint32 key_id = 2;
-  bytes input_data = 3;
-  int32 output_length = 4;
-}
-
-message PrfSetComputeResponse {
-  oneof result {
-    bytes output = 1;
-    string err = 2;
-  }
-}
-
-// Service for JSON Web Tokens (JWT)
-service Jwt {
-  // Computes a signed compact JWT token.
-  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Computes a signed compact JWT token.
-  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Converts a Keyset from Tink Binary to JWK Set Format
-  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
-  // Converts a Keyset from JWK Set to Tink Binary Format
-  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
-}
-
-//  Used to represent the JSON null value.
-enum NullValue {
-  NULL_VALUE = 0;
-}
-
-message JwtClaimValue {
-  oneof kind {
-    NullValue null_value = 2;
-    double number_value = 3;
-    string string_value = 4;
-    bool bool_value = 5;
-    string json_object_value = 6;
-    string json_array_value = 7;
-  }
-}
-
-message JwtToken {
-  google.protobuf.StringValue issuer = 1;
-  google.protobuf.StringValue subject = 2;
-  repeated string audiences = 3;
-  google.protobuf.StringValue jwt_id = 4;
-  google.protobuf.Timestamp expiration = 5;
-  google.protobuf.Timestamp not_before = 6;
-  google.protobuf.Timestamp issued_at = 7;
-  map<string, JwtClaimValue> custom_claims = 8;
-  google.protobuf.StringValue type_header = 9;
-}
-
-message JwtValidator {
-  google.protobuf.StringValue expected_type_header = 7;
-  google.protobuf.StringValue expected_issuer = 1;
-  google.protobuf.StringValue expected_audience = 3;
-  bool ignore_type_header = 8;
-  bool ignore_issuer = 9;
-  bool ignore_audience = 11;
-  bool allow_missing_expiration = 12;
-  bool expect_issued_in_the_past = 13;
-  google.protobuf.Timestamp now = 5;
-  google.protobuf.Duration clock_skew = 6;
-}
-
-message JwtSignRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  JwtToken raw_jwt = 2;
-}
-
-message JwtSignResponse {
-  oneof result {
-    string signed_compact_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtVerifyRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  string signed_compact_jwt = 2;
-  JwtValidator validator = 3;
-}
-
-message JwtVerifyResponse {
-  oneof result {
-    JwtToken verified_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtToJwkSetRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message JwtToJwkSetResponse {
-  oneof result {
-    string jwk_set = 1;
-    string err = 2;
-  }
-}
-
-message JwtFromJwkSetRequest {
-  string jwk_set = 1;
-}
-
-message JwtFromJwkSetResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
diff --git a/testing/go/proto/BUILD.bazel b/testing/go/protos/BUILD.bazel
similarity index 100%
rename from testing/go/proto/BUILD.bazel
rename to testing/go/protos/BUILD.bazel
diff --git a/testing/go/protos/testing_api.proto b/testing/go/protos/testing_api.proto
new file mode 100644
index 0000000..7a02517
--- /dev/null
+++ b/testing/go/protos/testing_api.proto
@@ -0,0 +1,566 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+syntax = "proto3";
+
+package tink_testing_api;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+option java_package = "com.google.crypto.tink.testing.proto";
+option java_multiple_files = true;
+option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
+// Placeholder for java_stubby_library
+
+// Service providing metadata about the server.
+service Metadata {
+  // Returns some server information. A test may use this information to verify
+  // that it is talking to the right server.
+  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
+}
+
+message ServerInfoRequest {}
+
+message ServerInfoResponse {
+  string tink_version = 1;  // For example '1.4'
+  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
+}
+
+// Service for Keyset operations.
+service Keyset {
+  // Generates a key template from a key template name.
+  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
+  // Generates a new keyset from a template.
+  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
+  // Generates a public-key keyset from a private-key keyset.
+  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
+  // Converts a Keyset from Binary to Json Format
+  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
+  // Converts a Keyset from Json to Binary Format
+  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
+  // Reads an encrypted keyset using KeysetHandle.read() or
+  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
+  rpc ReadEncrypted(KeysetReadEncryptedRequest)
+      returns (KeysetReadEncryptedResponse) {}
+  // Writes an encrypted keyset using KeysetHandle.write() or
+  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
+  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
+      returns (KeysetWriteEncryptedResponse) {}
+}
+
+message KeysetTemplateRequest {
+  string template_name = 1;  // template name used by Tinkey
+}
+
+message KeysetTemplateResponse {
+  oneof result {
+    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
+    string err = 2;
+  }
+}
+
+message KeysetGenerateRequest {
+  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
+}
+
+message KeysetGenerateResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetPublicRequest {
+  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetPublicResponse {
+  oneof result {
+    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetToJsonRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetToJsonResponse {
+  oneof result {
+    string json_keyset = 1;
+    string err = 2;
+  }
+}
+
+message KeysetFromJsonRequest {
+  string json_keyset = 1;
+}
+
+message KeysetFromJsonResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+// Copy of google.protobuf.BytesValue
+message BytesValue {
+  // The bytes value.
+  bytes value = 1;
+}
+
+enum KeysetReaderType {
+  KEYSET_READER_UNKNOWN = 0;
+  KEYSET_READER_BINARY = 1;
+  KEYSET_READER_JSON = 2;
+}
+
+message KeysetReadEncryptedRequest {
+  bytes encrypted_keyset = 1;
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetReaderType keyset_reader_type = 4;
+}
+
+message KeysetReadEncryptedResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+enum KeysetWriterType {
+  KEYSET_WRITER_UNKNOWN = 0;
+  KEYSET_WRITER_BINARY = 1;
+  KEYSET_WRITER_JSON = 2;
+}
+
+message KeysetWriteEncryptedRequest {
+  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetWriterType keyset_writer_type = 4;
+}
+
+message KeysetWriteEncryptedResponse {
+  oneof result {
+    bytes encrypted_keyset = 1;
+    string err = 2;
+  }
+}
+
+message AnnotatedKeyset {
+  bytes serialized_keyset = 1;  // serialized google.crypto.tink.Keyset.
+  map<string, string> annotations = 2;
+}
+
+message CreationRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message CreationResponse {
+  // Empty means no error
+  string err = 1;
+}
+
+// Service for AEAD encryption and decryption
+service Aead {
+  // Creates an Aead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
+}
+
+message AeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message AeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Deterministic AEAD encryption and decryption
+service DeterministicAead {
+  // Creates a Deterministic AEAD object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
+      returns (DeterministicAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
+      returns (DeterministicAeadDecryptResponse) {}
+}
+
+message DeterministicAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message DeterministicAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Streaming AEAD encryption and decryption
+service StreamingAead {
+  // Creates a StreamingAead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(StreamingAeadEncryptRequest)
+      returns (StreamingAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(StreamingAeadDecryptRequest)
+      returns (StreamingAeadDecryptResponse) {}
+}
+
+message StreamingAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message StreamingAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to compute and verify MACs
+service Mac {
+  // Creates a Mac object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Computes a MAC for given data. The client must call "Create" first to see
+  // if creation succeeds before calling this.
+  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
+  // Verifies the validity of the MAC value, no error means success. The client
+  // must call "Create" first to see if creation succeeds before calling this.
+  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
+}
+
+message ComputeMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message ComputeMacResponse {
+  oneof result {
+    bytes mac_value = 1;
+    string err = 2;
+  }
+}
+
+message VerifyMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes mac_value = 2;
+  bytes data = 3;
+}
+
+message VerifyMacResponse {
+  string err = 1;
+}
+
+// Service to hybrid encrypt and decrypt
+service Hybrid {
+  // Creates a HybridEncrypt object without using it.
+  rpc CreateHybridEncrypt(CreationRequest) returns (CreationResponse) {}
+  // Creates a HybridDecrypt object without using it.
+  rpc CreateHybridDecrypt(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts plaintext binding context_info to the resulting ciphertext. The
+  // client must call "CreateHybridEncrypt" first to see if creation succeeds
+  // before calling this.
+  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
+  // Decrypts ciphertext verifying the integrity of context_info. The client
+  // must call "CreateHybridDecrypt" first to see if creation succeeds before
+  // calling this.
+  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
+}
+
+message HybridEncryptRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes context_info = 3;
+}
+
+message HybridEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message HybridDecryptRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes context_info = 3;
+}
+
+message HybridDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to sign and verify signatures.
+service Signature {
+  // Creates a PublicKeySign object without using it.
+  rpc CreatePublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a PublicKeyVerify object without using it.
+  rpc CreatePublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes the signature for data. The client must call "CreatePublicKeySign"
+  // first to see if creation succeeds before calling this.
+  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
+  // Verifies that signature is a digital signature for data. The client must
+  // call "CreatePublicKeyVerify" first to see if creation succeeds before
+  // calling this.
+  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
+}
+
+message SignatureSignRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message SignatureSignResponse {
+  oneof result {
+    bytes signature = 1;
+    string err = 2;
+  }
+}
+
+message SignatureVerifyRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes signature = 2;
+  bytes data = 3;
+}
+
+message SignatureVerifyResponse {
+  string err = 1;
+}
+
+// Service for PrfSet computation
+service PrfSet {
+  // Creates a PrfSet object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Returns the key ids and the primary key id in the keyset.The client must
+  // call "Create" first to see if creation succeeds before calling this.
+  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
+  // Computes the output of the PRF with the given key_id in the PrfSet.The
+  // client must call "Create" first to see if creation succeeds before calling
+  // this.
+  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
+}
+
+message PrfSetKeyIdsRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message PrfSetKeyIdsResponse {
+  message Output {
+    uint32 primary_key_id = 1;
+    repeated uint32 key_id = 2;
+  }
+  oneof result {
+    Output output = 1;
+    string err = 2;
+  }
+}
+
+message PrfSetComputeRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  uint32 key_id = 2;
+  bytes input_data = 3;
+  int32 output_length = 4;
+}
+
+message PrfSetComputeResponse {
+  oneof result {
+    bytes output = 1;
+    string err = 2;
+  }
+}
+
+// Service for JSON Web Tokens (JWT)
+service Jwt {
+  // Creates a JwtMac object without using it.
+  rpc CreateJwtMac(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeySign object without using it.
+  rpc CreateJwtPublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeyVerify object without using it.
+  rpc CreateJwtPublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes a signed compact JWT token.
+  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Computes a signed compact JWT token.
+  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Converts a Keyset from Tink Binary to JWK Set Format
+  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
+  // Converts a Keyset from JWK Set to Tink Binary Format
+  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
+}
+
+//  Used to represent the JSON null value.
+enum NullValue {
+  NULL_VALUE = 0;
+}
+
+message JwtClaimValue {
+  oneof kind {
+    NullValue null_value = 2;
+    double number_value = 3;
+    string string_value = 4;
+    bool bool_value = 5;
+    string json_object_value = 6;
+    string json_array_value = 7;
+  }
+}
+
+message JwtToken {
+  google.protobuf.StringValue issuer = 1;
+  google.protobuf.StringValue subject = 2;
+  repeated string audiences = 3;
+  google.protobuf.StringValue jwt_id = 4;
+  google.protobuf.Timestamp expiration = 5;
+  google.protobuf.Timestamp not_before = 6;
+  google.protobuf.Timestamp issued_at = 7;
+  map<string, JwtClaimValue> custom_claims = 8;
+  google.protobuf.StringValue type_header = 9;
+}
+
+message JwtValidator {
+  google.protobuf.StringValue expected_type_header = 7;
+  google.protobuf.StringValue expected_issuer = 1;
+  google.protobuf.StringValue expected_audience = 3;
+  bool ignore_type_header = 8;
+  bool ignore_issuer = 9;
+  bool ignore_audience = 11;
+  bool allow_missing_expiration = 12;
+  bool expect_issued_in_the_past = 13;
+  google.protobuf.Timestamp now = 5;
+  google.protobuf.Duration clock_skew = 6;
+}
+
+message JwtSignRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  JwtToken raw_jwt = 2;
+}
+
+message JwtSignResponse {
+  oneof result {
+    string signed_compact_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtVerifyRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  string signed_compact_jwt = 2;
+  JwtValidator validator = 3;
+}
+
+message JwtVerifyResponse {
+  oneof result {
+    JwtToken verified_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtToJwkSetRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message JwtToJwkSetResponse {
+  oneof result {
+    string jwk_set = 1;
+    string err = 2;
+  }
+}
+
+message JwtFromJwkSetRequest {
+  string jwk_set = 1;
+}
+
+message JwtFromJwkSetResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
diff --git a/testing/go/services_test.go b/testing/go/services_test.go
index 022e304..9f5f421 100644
--- a/testing/go/services_test.go
+++ b/testing/go/services_test.go
@@ -34,7 +34,7 @@
 	"github.com/google/tink/go/signature"
 	"github.com/google/tink/go/streamingaead"
 	"github.com/google/tink/testing/go/services"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 func genKeyset(ctx context.Context, keysetService *services.KeysetService, template []byte) ([]byte, error) {
@@ -330,11 +330,49 @@
 	}
 }
 
+func TestSuccessfulAeadCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	aeadService := &services.AEADService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(aead.AES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	keyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := aeadService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}})
+	if err != nil {
+		t.Fatalf("CreateAead with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateAead with good keyset failed with creation error: %v", result.GetErr())
+	}
+}
+
+func TestFailingAeadCreation(t *testing.T) {
+	aeadService := &services.AEADService{}
+	ctx := context.Background()
+
+	result, err := aeadService.Create(ctx, &pb.CreationRequest{
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: []byte{0x80}}})
+	if err != nil {
+		t.Fatalf("CreateAead with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreateAead with bad keyset succeeded instead of failing")
+	}
+}
+
 func aeadEncrypt(ctx context.Context, aeadService *services.AEADService, keyset []byte, plaintext []byte, associatedData []byte) ([]byte, error) {
 	encRequest := &pb.AeadEncryptRequest{
-		Keyset:         keyset,
-		Plaintext:      plaintext,
-		AssociatedData: associatedData,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Plaintext:       plaintext,
+		AssociatedData:  associatedData,
 	}
 	encResponse, err := aeadService.Encrypt(ctx, encRequest)
 	if err != nil {
@@ -352,9 +390,9 @@
 
 func aeadDecrypt(ctx context.Context, aeadService *services.AEADService, keyset []byte, ciphertext []byte, associatedData []byte) ([]byte, error) {
 	decRequest := &pb.AeadDecryptRequest{
-		Keyset:         keyset,
-		Ciphertext:     ciphertext,
-		AssociatedData: associatedData,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Ciphertext:      ciphertext,
+		AssociatedData:  associatedData,
 	}
 	decResponse, err := aeadService.Decrypt(ctx, decRequest)
 	if err != nil {
@@ -410,11 +448,48 @@
 	}
 }
 
+func TestSuccessfulDaeadCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	daeadService := &services.DeterministicAEADService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(daead.AESSIVKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(daead.AESSIVKeyTemplate()) failed: %v", err)
+	}
+
+	keyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := daeadService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset}})
+	if err != nil {
+		t.Fatalf("CreateDeterministicAead with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateDeterministicAead with good keyset failed with creation error: %v", result.GetErr())
+	}
+}
+
+func TestFailingDaeadCreation(t *testing.T) {
+	daeadService := &services.DeterministicAEADService{}
+	ctx := context.Background()
+
+	result, err := daeadService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: []byte{0x80}}})
+	if err != nil {
+		t.Fatalf("CreateAead with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreateAead with bad keyset succeeded instead of failing")
+	}
+}
+
 func daeadEncrypt(ctx context.Context, daeadService *services.DeterministicAEADService, keyset []byte, plaintext []byte, associatedData []byte) ([]byte, error) {
 	encRequest := &pb.DeterministicAeadEncryptRequest{
-		Keyset:         keyset,
-		Plaintext:      plaintext,
-		AssociatedData: associatedData,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Plaintext:       plaintext,
+		AssociatedData:  associatedData,
 	}
 	encResponse, err := daeadService.EncryptDeterministically(ctx, encRequest)
 	if err != nil {
@@ -432,9 +507,9 @@
 
 func daeadDecrypt(ctx context.Context, daeadService *services.DeterministicAEADService, keyset []byte, ciphertext []byte, associatedData []byte) ([]byte, error) {
 	decRequest := &pb.DeterministicAeadDecryptRequest{
-		Keyset:         keyset,
-		Ciphertext:     ciphertext,
-		AssociatedData: associatedData,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Ciphertext:      ciphertext,
+		AssociatedData:  associatedData,
 	}
 	decResponse, err := daeadService.DecryptDeterministically(ctx, decRequest)
 	if err != nil {
@@ -490,11 +565,59 @@
 	}
 }
 
+func TestSuccessfulStreamingAEADCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	streamingAEADService := &services.StreamingAEADService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(streamingaead.AES128GCMHKDF4KBKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(streamingaead.AES128GCMHKDF4KBKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := streamingAEADService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("streamingAEADService.Create with good keyset failed with gRPC error: %v, want nil", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("streamingAEADService.Create with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingStreamingAEADCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	streamingAEADService := &services.StreamingAEADService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(aead.AES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := streamingAEADService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("streamingAEADService.Create with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("streamingAEADService.Create with bad keyset succeeded")
+	}
+}
+
 func streamingAEADEncrypt(ctx context.Context, streamingAEADService *services.StreamingAEADService, keyset []byte, plaintext []byte, associatedData []byte) ([]byte, error) {
 	encRequest := &pb.StreamingAeadEncryptRequest{
-		Keyset:         keyset,
-		Plaintext:      plaintext,
-		AssociatedData: associatedData,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Plaintext:       plaintext,
+		AssociatedData:  associatedData,
 	}
 	encResponse, err := streamingAEADService.Encrypt(ctx, encRequest)
 	if err != nil {
@@ -512,9 +635,9 @@
 
 func streamingAEADDecrypt(ctx context.Context, streamingAEADService *services.StreamingAEADService, keyset []byte, ciphertext []byte, associatedData []byte) ([]byte, error) {
 	decRequest := &pb.StreamingAeadDecryptRequest{
-		Keyset:         keyset,
-		Ciphertext:     ciphertext,
-		AssociatedData: associatedData,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Ciphertext:      ciphertext,
+		AssociatedData:  associatedData,
 	}
 	decResponse, err := streamingAEADService.Decrypt(ctx, decRequest)
 	if err != nil {
@@ -570,10 +693,59 @@
 	}
 }
 
+func TestSuccessfulMacCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	macService := &services.MacService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(mac.HMACSHA256Tag128KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(mac.HMACSHA256Tag128KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := macService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("macService.Create with good keyset failed with gRPC error: %v, want nil", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("macService.Create with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingMacCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	macService := &services.MacService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a hybrid encrypt
+	template, err := proto.Marshal(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(aead.AES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := macService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("macService.Create with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("macService.Create with bad keyset succeeded")
+	}
+}
+
 func computeMAC(ctx context.Context, macService *services.MacService, keyset []byte, data []byte) ([]byte, error) {
 	encRequest := &pb.ComputeMacRequest{
-		Keyset: keyset,
-		Data:   data,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		Data:            data,
 	}
 	response, err := macService.ComputeMac(ctx, encRequest)
 	if err != nil {
@@ -591,9 +763,9 @@
 
 func verifyMAC(ctx context.Context, macService *services.MacService, keyset []byte, macValue []byte, data []byte) error {
 	request := &pb.VerifyMacRequest{
-		Keyset:   keyset,
-		MacValue: macValue,
-		Data:     data,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		MacValue:        macValue,
+		Data:            data,
 	}
 	response, err := macService.VerifyMac(ctx, request)
 	if err != nil {
@@ -639,9 +811,9 @@
 
 func hybridEncrypt(ctx context.Context, hybridService *services.HybridService, publicKeyset []byte, plaintext []byte, contextInfo []byte) ([]byte, error) {
 	encRequest := &pb.HybridEncryptRequest{
-		PublicKeyset: publicKeyset,
-		Plaintext:    plaintext,
-		ContextInfo:  contextInfo,
+		PublicAnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset},
+		Plaintext:             plaintext,
+		ContextInfo:           contextInfo,
 	}
 	encResponse, err := hybridService.Encrypt(ctx, encRequest)
 	if err != nil {
@@ -659,9 +831,9 @@
 
 func hybridDecrypt(ctx context.Context, hybridService *services.HybridService, privateKeyset []byte, ciphertext []byte, contextInfo []byte) ([]byte, error) {
 	decRequest := &pb.HybridDecryptRequest{
-		PrivateKeyset: privateKeyset,
-		Ciphertext:    ciphertext,
-		ContextInfo:   contextInfo,
+		PrivateAnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset},
+		Ciphertext:             ciphertext,
+		ContextInfo:            contextInfo,
 	}
 	decResponse, err := hybridService.Decrypt(ctx, decRequest)
 	if err != nil {
@@ -677,6 +849,112 @@
 	}
 }
 
+func TestSuccessfulHybridDecryptCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	hybridService := &services.HybridService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := hybridService.CreateHybridDecrypt(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("CreateHybridDecrypt with good keyset failed with gRPC error: %v, want nil", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateHybridDecrypt with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestSuccessfulHybridEncryptCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	hybridService := &services.HybridService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+	publicKeyset, err := pubKeyset(ctx, keysetService, privateKeyset)
+	if err != nil {
+		t.Fatalf("pubKeyset failed: %v", err)
+	}
+
+	result, err := hybridService.CreateHybridEncrypt(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}})
+	if err != nil {
+		t.Fatalf("CreateHybridEncrypt with good keyset failed with gRPC error: %v, want nil", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateHybridEncrypt with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingHybridDecryptCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	hybridService := &services.HybridService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a hybrid encrypt
+	template, err := proto.Marshal(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := hybridService.CreateHybridDecrypt(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("CreateHybridDecrypt with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreateHybridDecrypt with bad keyset succeeded")
+	}
+}
+
+func TestFailingHybridEncryptCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	hybridService := &services.HybridService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a hybrid encrypt
+	template, err := proto.Marshal(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+	publicKeyset, err := pubKeyset(ctx, keysetService, privateKeyset)
+	if err != nil {
+		t.Fatalf("pubKeyset failed: %v", err)
+	}
+
+	result, err := hybridService.CreateHybridEncrypt(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}})
+	if err != nil {
+		t.Fatalf("CreateHybridEncrypt with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreateHybridEncrypt with bad keyset succeeded")
+	}
+}
+
 func TestHybridGenerateEncryptDecrypt(t *testing.T) {
 	keysetService := &services.KeysetService{}
 	hybridService := &services.HybridService{}
@@ -724,10 +1002,114 @@
 	}
 }
 
+func TestSuccessfulPublicKeySignCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	signatureService := &services.SignatureService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := signatureService.CreatePublicKeySign(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("CreateHybridDecrypt with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateHybridDecrypt good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestSuccessfulPublicKeyVerifyCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	signatureService := &services.SignatureService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(signature.ECDSAP256KeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(signature.ECDSAP256KeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+	publicKeyset, err := pubKeyset(ctx, keysetService, privateKeyset)
+	if err != nil {
+		t.Fatalf("pubKeyset failed: %v", err)
+	}
+
+	result, err := signatureService.CreatePublicKeyVerify(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}})
+	if err != nil {
+		t.Fatalf("CreateHybridEncrypt with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("CreateHybridEncrypt good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingPublicKeySignCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	signatureService := &services.SignatureService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := signatureService.CreatePublicKeySign(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("CreatePublicKeySign with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreatePublicKeySign with bad keyset succeeded")
+	}
+}
+
+func TestFailingPublicKeyVerifyCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	signatureService := &services.SignatureService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(hybrid.ECIESHKDFAES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+	publicKeyset, err := pubKeyset(ctx, keysetService, privateKeyset)
+	if err != nil {
+		t.Fatalf("pubKeyset failed: %v", err)
+	}
+
+	result, err := signatureService.CreatePublicKeyVerify(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset}})
+	if err != nil {
+		t.Fatalf("CreatePublicKeyVerify with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("CreatePublicKeyVerify with bad keyset succeeded")
+	}
+}
+
 func signatureSign(ctx context.Context, signatureService *services.SignatureService, privateKeyset []byte, data []byte) ([]byte, error) {
 	encRequest := &pb.SignatureSignRequest{
-		PrivateKeyset: privateKeyset,
-		Data:          data,
+		PrivateAnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset},
+		Data:                   data,
 	}
 	response, err := signatureService.Sign(ctx, encRequest)
 	if err != nil {
@@ -745,9 +1127,9 @@
 
 func signatureVerify(ctx context.Context, signatureService *services.SignatureService, publicKeyset []byte, signatureValue []byte, data []byte) error {
 	request := &pb.SignatureVerifyRequest{
-		PublicKeyset: publicKeyset,
-		Signature:    signatureValue,
-		Data:         data,
+		PublicAnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: publicKeyset},
+		Signature:             signatureValue,
+		Data:                  data,
 	}
 	response, err := signatureService.Verify(ctx, request)
 	if err != nil {
@@ -798,9 +1180,58 @@
 	}
 }
 
+func TestSuccessfulPrfSetCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	prfSetService := &services.PrfSetService{}
+	ctx := context.Background()
+
+	template, err := proto.Marshal(prf.HMACSHA256PRFKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(prf.HMACSHA256PRFKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := prfSetService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("macService.Create with good keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() != "" {
+		t.Fatalf("macService.Create with good keyset failed with result.GetErr() = %q, want empty string", result.GetErr())
+	}
+}
+
+func TestFailingPrfSetCreation(t *testing.T) {
+	keysetService := &services.KeysetService{}
+	prfSetService := &services.MacService{}
+	ctx := context.Background()
+
+	// We use signature keys -- then we cannot create a hybrid encrypt
+	template, err := proto.Marshal(aead.AES128GCMKeyTemplate())
+	if err != nil {
+		t.Fatalf("proto.Marshal(aead.AES128GCMKeyTemplate()) failed: %v", err)
+	}
+
+	privateKeyset, err := genKeyset(ctx, keysetService, template)
+	if err != nil {
+		t.Fatalf("genKeyset failed: %v", err)
+	}
+
+	result, err := prfSetService.Create(ctx, &pb.CreationRequest{AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: privateKeyset}})
+	if err != nil {
+		t.Fatalf("prfSetService.Create with bad keyset failed with gRPC error: %v", err)
+	}
+	if result.GetErr() == "" {
+		t.Fatalf("prfSetService.Create with bad keyset succeeded")
+	}
+}
+
 func prfSetKeyIds(ctx context.Context, prfSetService *services.PrfSetService, keyset []byte) (uint32, []uint32, error) {
 	request := &pb.PrfSetKeyIdsRequest{
-		Keyset: keyset,
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
 	}
 	response, err := prfSetService.KeyIds(ctx, request)
 	if err != nil {
@@ -818,10 +1249,10 @@
 
 func prfSetCompute(ctx context.Context, prfSetService *services.PrfSetService, keyset []byte, keyID uint32, inputData []byte, outputLength int) ([]byte, error) {
 	request := &pb.PrfSetComputeRequest{
-		Keyset:       keyset,
-		KeyId:        keyID,
-		InputData:    inputData,
-		OutputLength: int32(outputLength),
+		AnnotatedKeyset: &pb.AnnotatedKeyset{SerializedKeyset: keyset},
+		KeyId:           keyID,
+		InputData:       inputData,
+		OutputLength:    int32(outputLength),
 	}
 	response, err := prfSetService.Compute(ctx, request)
 	if err != nil {
diff --git a/testing/go/signature_service.go b/testing/go/signature_service.go
index 72d8713..2590597 100644
--- a/testing/go/signature_service.go
+++ b/testing/go/signature_service.go
@@ -17,13 +17,10 @@
 package services
 
 import (
-	"bytes"
 	"context"
 
-	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/signature"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 // SignatureService implements the Signature testing service.
@@ -31,9 +28,32 @@
 	pb.SignatureServer
 }
 
+func (s *SignatureService) CreatePublicKeySign(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = signature.NewSigner(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
+func (s *SignatureService) CreatePublicKeyVerify(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = signature.NewVerifier(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
 func (s *SignatureService) Sign(ctx context.Context, req *pb.SignatureSignRequest) (*pb.SignatureSignResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.PrivateKeyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetPrivateAnnotatedKeyset())
 	if err != nil {
 		return &pb.SignatureSignResponse{
 			Result: &pb.SignatureSignResponse_Err{err.Error()}}, nil
@@ -53,8 +73,7 @@
 }
 
 func (s *SignatureService) Verify(ctx context.Context, req *pb.SignatureVerifyRequest) (*pb.SignatureVerifyResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.PublicKeyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetPublicAnnotatedKeyset())
 	if err != nil {
 		return &pb.SignatureVerifyResponse{Err: err.Error()}, nil
 	}
diff --git a/testing/go/streaming_aead_service.go b/testing/go/streaming_aead_service.go
index 3b4822e..753e89e 100644
--- a/testing/go/streaming_aead_service.go
+++ b/testing/go/streaming_aead_service.go
@@ -20,12 +20,11 @@
 	"bytes"
 	"context"
 	"fmt"
+
 	"io"
 
-	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/streamingaead"
-	"github.com/google/tink/go/testkeyset"
-	pb "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pb "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 const (
@@ -37,9 +36,20 @@
 	pb.StreamingAeadServer
 }
 
+func (s *StreamingAEADService) Create(ctx context.Context, req *pb.CreationRequest) (*pb.CreationResponse, error) {
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	_, err = streamingaead.New(handle)
+	if err != nil {
+		return &pb.CreationResponse{Err: err.Error()}, nil
+	}
+	return &pb.CreationResponse{}, nil
+}
+
 func (s *StreamingAEADService) Encrypt(ctx context.Context, req *pb.StreamingAeadEncryptRequest) (*pb.StreamingAeadEncryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return &pb.StreamingAeadEncryptResponse{
 			Result: &pb.StreamingAeadEncryptResponse_Err{err.Error()}}, nil
@@ -77,8 +87,7 @@
 }
 
 func (s *StreamingAEADService) Decrypt(ctx context.Context, req *pb.StreamingAeadDecryptRequest) (*pb.StreamingAeadDecryptResponse, error) {
-	reader := keyset.NewBinaryReader(bytes.NewReader(req.Keyset))
-	handle, err := testkeyset.Read(reader)
+	handle, err := toKeysetHandle(req.GetAnnotatedKeyset())
 	if err != nil {
 		return &pb.StreamingAeadDecryptResponse{
 			Result: &pb.StreamingAeadDecryptResponse_Err{err.Error()}}, nil
diff --git a/testing/go/testing_server.go b/testing/go/testing_server.go
index eb261c1..adef67f 100644
--- a/testing/go/testing_server.go
+++ b/testing/go/testing_server.go
@@ -18,31 +18,51 @@
 package main
 
 import (
+	"context"
 	"fmt"
 	"log"
 	"net"
 
 	"flag"
 	// context is used to cancel outstanding requests
+	"google.golang.org/api/option"
 	"google.golang.org/grpc"
 	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/integration/awskms"
+	"github.com/google/tink/go/integration/gcpkms"
 	"github.com/google/tink/go/testing/fakekms"
 	"github.com/google/tink/testing/go/services"
-	pbgrpc "github.com/google/tink/testing/go/proto/testing_api_go_grpc"
+	pbgrpc "github.com/google/tink/testing/go/protos/testing_api_go_grpc"
 )
 
 var (
-	port = flag.Int("port", 10000, "The server port")
+	port            = flag.Int("port", 10000, "The server port")
+	gcpCredFilePath = flag.String("gcp_credentials_path", "", "Google Cloud KMS credentials path")
+	gcpKeyURI       = flag.String("gcp_key_uri", "", "Google Cloud KMS key URL of the form: gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.")
+	awsCredFilePath = flag.String("aws_credentials_path", "", "AWS KMS credentials path")
+	awsKeyURI       = flag.String("aws_key_uri", "", "AWS KMS key URL of the form: aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>.")
 )
 
 func main() {
 	flag.Parse()
 	client, err := fakekms.NewClient("fake-kms://")
 	if err != nil {
-		log.Fatalf("Failed to generate new FakeKMSClient: %v", err)
+		log.Fatalf("fakekms.NewClient failed: %v", err)
 	}
 	registry.RegisterKMSClient(client)
 
+	gcpClient, err := gcpkms.NewClientWithOptions(context.Background(), *gcpKeyURI, option.WithCredentialsFile(*gcpCredFilePath))
+	if err != nil {
+		log.Fatalf("gcpkms.NewClientWithOptions failed: %v", err)
+	}
+	registry.RegisterKMSClient(gcpClient)
+
+	awsClient, err := awskms.NewClientWithOptions(*awsKeyURI, awskms.WithCredentialPath(*awsCredFilePath))
+	if err != nil {
+		log.Fatalf("awskms.NewClientWithOptions failed: %v", err)
+	}
+	registry.RegisterKMSClient(awsClient)
+
 	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
 	if err != nil {
 		log.Fatalf("Server failed to listen: %v", err)
diff --git a/testing/java_src/.bazelversion b/testing/java_src/.bazelversion
index ac14c3d..09b254e 100644
--- a/testing/java_src/.bazelversion
+++ b/testing/java_src/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/testing/java_src/BUILD.bazel b/testing/java_src/BUILD.bazel
index 5caab18..bb477a0 100644
--- a/testing/java_src/BUILD.bazel
+++ b/testing/java_src/BUILD.bazel
@@ -7,13 +7,13 @@
 java_proto_library(
     name = "testing_api_java_proto",
     testonly = 1,
-    deps = ["//proto:testing_api_proto"],
+    deps = ["//protos:testing_api_proto"],
 )
 
 java_grpc_library(
     name = "testing_api_java_grpc",
     testonly = 1,
-    srcs = ["//proto:testing_api_proto"],
+    srcs = ["//protos:testing_api_proto"],
     deps = [":testing_api_java_proto"],
 )
 
@@ -31,36 +31,32 @@
         "java/com/google/crypto/tink/testing/PrfSetServiceImpl.java",
         "java/com/google/crypto/tink/testing/SignatureServiceImpl.java",
         "java/com/google/crypto/tink/testing/StreamingAeadServiceImpl.java",
+        "java/com/google/crypto/tink/testing/Util.java",
     ],
     deps = [
-        "@com_google_protobuf//:duration_proto",
-        "@com_google_protobuf//:timestamp_proto",
-        "@com_google_protobuf//:wrappers_proto",
         ":testing_api_java_grpc",
         ":testing_api_java_proto",
+        "@com_google_protobuf//:duration_proto",
         "@com_google_protobuf//:protobuf_java",
+        "@com_google_protobuf//:timestamp_proto",
+        "@com_google_protobuf//:wrappers_proto",
         "@io_grpc_grpc_java//api",
         "@io_grpc_grpc_java//protobuf",
         "@io_grpc_grpc_java//stub",
-        "@tink_java//proto:tink_java_proto",
         "@tink_java//src/main/java/com/google/crypto/tink:aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_writer",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
         "@tink_java//src/main/java/com/google/crypto/tink:deterministic_aead",
         "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
         "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_writer",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:key_template",
         "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_writer",
         "@tink_java//src/main/java/com/google/crypto/tink:mac",
         "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
         "@tink_java//src/main/java/com/google/crypto/tink:public_key_verify",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
         "@tink_java//src/main/java/com/google/crypto/tink:streaming_aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink:version",
         "@tink_java//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwk_set_converter",
@@ -73,6 +69,7 @@
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_validator",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:raw_jwt",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:verified_jwt",
+        "@tink_java//src/main/java/com/google/crypto/tink/monitoring:monitoring_annotations",
         "@tink_java//src/main/java/com/google/crypto/tink/prf:prf_set",
         "@tink_java//src/main/java/com/google/crypto/tink/tinkkey:secret_key_access",
     ],
@@ -91,11 +88,14 @@
     deps = [
         ":testing_services",
         "@io_grpc_grpc_java//api",
+        "@maven//:args4j_args4j",
         "@maven//:org_conscrypt_conscrypt_openjdk_uber",
         "@tink_java//src/main/java/com/google/crypto/tink:kms_clients",
         "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
         "@tink_java//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "@tink_java//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_client",
+        "@tink_java//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config",
         "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
@@ -126,9 +126,11 @@
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
         "@tink_java//proto:tink_java_proto",
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:key_template",
         "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/config:tink_config",
         "@tink_java//src/main/java/com/google/crypto/tink/daead:aes_siv_key_manager",
         "@tink_java//src/main/java/com/google/crypto/tink/internal:key_template_proto_converter",
diff --git a/testing/java_src/WORKSPACE b/testing/java_src/WORKSPACE
index 0f93fe1..cc51e80 100644
--- a/testing/java_src/WORKSPACE
+++ b/testing/java_src/WORKSPACE
@@ -11,12 +11,20 @@
 
 tink_java_deps()
 
-# Release from 2022-04-28.
+load("@tink_java//:tink_java_deps_init.bzl", "tink_java_deps_init")
+
+tink_java_deps_init()
+
+# Release from 2022-10-19.
 http_archive(
     name = "io_grpc_grpc_java",
-    sha256 = "c1b80883511ceb1e433fb2d4b2f6d85dca0c62a265a6a3e6695144610d6f65b8",
-    strip_prefix = "grpc-java-1.46.0",
-    url = "https://github.com/grpc/grpc-java/archive/v1.46.0.tar.gz",
+    sha256 = "992f757b022bb40d2db07a4924f169c0abacbbddcae8f32edb99921683fdffe9",
+    strip_prefix = "grpc-java-1.50.2",
+    url = "https://github.com/grpc/grpc-java/archive/v1.50.2.tar.gz",
+    # Replaces `protobuf_java_lite` with `protobuf_javalite`. Protobuf
+    # does not expose a `protobuf_java_lite` target.
+    patches = ["@testing_java//third_party:io_grpc_grpc_java.diff"],
+    patch_args = ["-p1"],
 )
 
 load("@rules_jvm_external//:defs.bzl", "maven_install")
@@ -24,12 +32,11 @@
 
 grpc_java_repositories()
 
-load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps")
-
-protobuf_deps()
-
 maven_install(
-    artifacts = TINK_MAVEN_ARTIFACTS + IO_GRPC_GRPC_JAVA_ARTIFACTS + PROTOBUF_MAVEN_ARTIFACTS,
+    artifacts = TINK_MAVEN_ARTIFACTS +
+      IO_GRPC_GRPC_JAVA_ARTIFACTS + [
+        "args4j:args4j:2.33",
+      ],
     generate_compat_repositories = True,
     override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS,
     repositories = [
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/AeadServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/AeadServiceImpl.java
index e5172de..bbac1b9 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/AeadServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/AeadServiceImpl.java
@@ -17,74 +17,78 @@
 package com.google.crypto.tink.testing;
 
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.testing.proto.AeadDecryptRequest;
 import com.google.crypto.tink.testing.proto.AeadDecryptResponse;
 import com.google.crypto.tink.testing.proto.AeadEncryptRequest;
 import com.google.crypto.tink.testing.proto.AeadEncryptResponse;
 import com.google.crypto.tink.testing.proto.AeadGrpc.AeadImplBase;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 /** Implements a gRPC Aead Testing service. */
 public final class AeadServiceImpl extends AeadImplBase {
 
-  public AeadServiceImpl() throws GeneralSecurityException {
+  public AeadServiceImpl() throws GeneralSecurityException {}
+
+  @Override
+  public void create(CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, Aead.class);
+  }
+
+  AeadEncryptResponse encrypt(AeadEncryptRequest request) throws GeneralSecurityException {
+    Aead aead = Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset()).getPrimitive(Aead.class);
+    try {
+      byte[] ciphertext =
+          aead.encrypt(
+              request.getPlaintext().toByteArray(), request.getAssociatedData().toByteArray());
+      return AeadEncryptResponse.newBuilder()
+          .setCiphertext(ByteString.copyFrom(ciphertext))
+          .build();
+    } catch (GeneralSecurityException e) {
+      return AeadEncryptResponse.newBuilder().setErr(e.toString()).build();
+    }
   }
 
   /** Encrypts a message. */
   @Override
   public void encrypt(
       AeadEncryptRequest request, StreamObserver<AeadEncryptResponse> responseObserver) {
-    AeadEncryptResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      Aead aead = keysetHandle.getPrimitive(Aead.class);
-      byte[] ciphertext =
-          aead.encrypt(
-              request.getPlaintext().toByteArray(), request.getAssociatedData().toByteArray());
-      response =
-          AeadEncryptResponse.newBuilder().setCiphertext(ByteString.copyFrom(ciphertext)).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = AeadEncryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      AeadEncryptResponse response = encrypt(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
+  }
+
+  AeadDecryptResponse decrypt(AeadDecryptRequest request) throws GeneralSecurityException {
+    Aead aead =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(Aead.class);
+    try {
+      byte[] plaintext =
+          aead.decrypt(
+              request.getCiphertext().toByteArray(), request.getAssociatedData().toByteArray());
+      return AeadDecryptResponse.newBuilder().setPlaintext(ByteString.copyFrom(plaintext)).build();
+    } catch (GeneralSecurityException e) {
+      return AeadDecryptResponse.newBuilder().setErr(e.toString()).build();
+    }
   }
 
   /** Decrypts a message. */
   @Override
   public void decrypt(
       AeadDecryptRequest request, StreamObserver<AeadDecryptResponse> responseObserver) {
-    AeadDecryptResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      Aead aead = keysetHandle.getPrimitive(Aead.class);
-      byte[] plaintext =
-          aead.decrypt(
-              request.getCiphertext().toByteArray(), request.getAssociatedData().toByteArray());
-      response =
-          AeadDecryptResponse.newBuilder().setPlaintext(ByteString.copyFrom(plaintext)).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = AeadDecryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      AeadDecryptResponse response = decrypt(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/DeterministicAeadServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/DeterministicAeadServiceImpl.java
index b58f085..bd6a9e0 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/DeterministicAeadServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/DeterministicAeadServiceImpl.java
@@ -16,81 +16,85 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.DeterministicAead;
-import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.DeterministicAeadDecryptRequest;
 import com.google.crypto.tink.testing.proto.DeterministicAeadDecryptResponse;
 import com.google.crypto.tink.testing.proto.DeterministicAeadEncryptRequest;
 import com.google.crypto.tink.testing.proto.DeterministicAeadEncryptResponse;
 import com.google.crypto.tink.testing.proto.DeterministicAeadGrpc.DeterministicAeadImplBase;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 /** Implements a gRPC DeterministicAead Testing service. */
 public final class DeterministicAeadServiceImpl extends DeterministicAeadImplBase {
 
-  public DeterministicAeadServiceImpl() throws GeneralSecurityException {
+  public DeterministicAeadServiceImpl() throws GeneralSecurityException {}
+
+  @Override
+  public void create(CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, DeterministicAead.class);
   }
 
-  /** Encrypts a message. */
+  private DeterministicAeadEncryptResponse encryptDeterministically(
+      DeterministicAeadEncryptRequest request) throws GeneralSecurityException {
+    DeterministicAead daead =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(DeterministicAead.class);
+    try {
+      byte[] ciphertext =
+          daead.encryptDeterministically(
+              request.getPlaintext().toByteArray(), request.getAssociatedData().toByteArray());
+      return DeterministicAeadEncryptResponse.newBuilder()
+          .setCiphertext(ByteString.copyFrom(ciphertext))
+          .build();
+    } catch (GeneralSecurityException e) {
+      return DeterministicAeadEncryptResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void encryptDeterministically(
       DeterministicAeadEncryptRequest request,
       StreamObserver<DeterministicAeadEncryptResponse> responseObserver) {
-    DeterministicAeadEncryptResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      DeterministicAead daead = keysetHandle.getPrimitive(DeterministicAead.class);
-      byte[] ciphertext =
-          daead.encryptDeterministically(
-              request.getPlaintext().toByteArray(), request.getAssociatedData().toByteArray());
-      response =
-          DeterministicAeadEncryptResponse.newBuilder()
-              .setCiphertext(ByteString.copyFrom(ciphertext))
-              .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = DeterministicAeadEncryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      DeterministicAeadEncryptResponse response = encryptDeterministically(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
-  /** Decrypts a message. */
+  private DeterministicAeadDecryptResponse decryptDeterministically(
+      DeterministicAeadDecryptRequest request) throws GeneralSecurityException {
+    DeterministicAead daead =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(DeterministicAead.class);
+    try {
+      byte[] plaintext =
+          daead.decryptDeterministically(
+              request.getCiphertext().toByteArray(), request.getAssociatedData().toByteArray());
+      return DeterministicAeadDecryptResponse.newBuilder()
+          .setPlaintext(ByteString.copyFrom(plaintext))
+          .build();
+    } catch (GeneralSecurityException e) {
+      return DeterministicAeadDecryptResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void decryptDeterministically(
       DeterministicAeadDecryptRequest request,
       StreamObserver<DeterministicAeadDecryptResponse> responseObserver) {
-    DeterministicAeadDecryptResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      DeterministicAead daead = keysetHandle.getPrimitive(DeterministicAead.class);
-      byte[] plaintext =
-          daead.decryptDeterministically(
-              request.getCiphertext().toByteArray(), request.getAssociatedData().toByteArray());
-      response =
-          DeterministicAeadDecryptResponse.newBuilder()
-              .setPlaintext(ByteString.copyFrom(plaintext))
-              .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = DeterministicAeadDecryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      DeterministicAeadDecryptResponse response = decryptDeterministically(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/HybridServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/HybridServiceImpl.java
index 114917f..953f6a7 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/HybridServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/HybridServiceImpl.java
@@ -16,76 +16,92 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.HybridDecrypt;
 import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.HybridDecryptRequest;
 import com.google.crypto.tink.testing.proto.HybridDecryptResponse;
 import com.google.crypto.tink.testing.proto.HybridEncryptRequest;
 import com.google.crypto.tink.testing.proto.HybridEncryptResponse;
 import com.google.crypto.tink.testing.proto.HybridGrpc.HybridImplBase;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 /** Implements a gRPC Hybrid Encryption Testing service. */
 public final class HybridServiceImpl extends HybridImplBase {
 
-  public HybridServiceImpl() throws GeneralSecurityException {
+  public HybridServiceImpl() throws GeneralSecurityException {}
+
+  @Override
+  public void createHybridEncrypt(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, HybridEncrypt.class);
   }
 
-  /** Encrypts a message. */
   @Override
-  public void encrypt(
-      HybridEncryptRequest request, StreamObserver<HybridEncryptResponse> responseObserver) {
-    HybridEncryptResponse response;
+  public void createHybridDecrypt(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, HybridDecrypt.class);
+  }
+
+
+  private HybridEncryptResponse encrypt(HybridEncryptRequest request)
+      throws GeneralSecurityException {
+    HybridEncrypt hybridEncrypt =
+        Util.parseBinaryProtoKeyset(request.getPublicAnnotatedKeyset())
+            .getPrimitive(HybridEncrypt.class);
     try {
-      KeysetHandle publicKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getPublicKeyset().toByteArray()));
-      HybridEncrypt hybridEncrypt = publicKeysetHandle.getPrimitive(HybridEncrypt.class);
       byte[] ciphertext =
           hybridEncrypt.encrypt(
               request.getPlaintext().toByteArray(), request.getContextInfo().toByteArray());
-      response =
-          HybridEncryptResponse.newBuilder().setCiphertext(ByteString.copyFrom(ciphertext)).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = HybridEncryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      return HybridEncryptResponse.newBuilder()
+          .setCiphertext(ByteString.copyFrom(ciphertext))
+          .build();
+    } catch (GeneralSecurityException e) {
+      return HybridEncryptResponse.newBuilder().setErr(e.toString()).build();
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
-  /** Decrypts a message. */
   @Override
-  public void decrypt(
-      HybridDecryptRequest request, StreamObserver<HybridDecryptResponse> responseObserver) {
-    HybridDecryptResponse response;
+  public void encrypt(
+      HybridEncryptRequest request, StreamObserver<HybridEncryptResponse> responseObserver) {
     try {
-      KeysetHandle privateKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getPrivateKeyset().toByteArray()));
-      HybridDecrypt hybridDecrypt = privateKeysetHandle.getPrimitive(HybridDecrypt.class);
+      HybridEncryptResponse response = encrypt(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
+  }
+
+  private HybridDecryptResponse decrypt(HybridDecryptRequest request)
+      throws GeneralSecurityException {
+    HybridDecrypt hybridDecrypt =
+        Util.parseBinaryProtoKeyset(request.getPrivateAnnotatedKeyset())
+            .getPrimitive(HybridDecrypt.class);
+    try {
       byte[] plaintext =
           hybridDecrypt.decrypt(
               request.getCiphertext().toByteArray(), request.getContextInfo().toByteArray());
-      response =
-          HybridDecryptResponse.newBuilder().setPlaintext(ByteString.copyFrom(plaintext)).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = HybridDecryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      return HybridDecryptResponse.newBuilder()
+          .setPlaintext(ByteString.copyFrom(plaintext))
+          .build();
+    } catch (GeneralSecurityException e) {
+      return HybridDecryptResponse.newBuilder().setErr(e.toString()).build();
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
+  }
+
+  @Override
+  public void decrypt(
+      HybridDecryptRequest request, StreamObserver<HybridDecryptResponse> responseObserver) {
+    try {
+      HybridDecryptResponse response = decrypt(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/JwtServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/JwtServiceImpl.java
index 3d204ad..f463fc7 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/JwtServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/JwtServiceImpl.java
@@ -16,10 +16,9 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.BinaryKeysetWriter;
-import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.jwt.JwkSetConverter;
 import com.google.crypto.tink.jwt.JwtInvalidException;
 import com.google.crypto.tink.jwt.JwtMac;
@@ -30,7 +29,8 @@
 import com.google.crypto.tink.jwt.JwtValidator;
 import com.google.crypto.tink.jwt.RawJwt;
 import com.google.crypto.tink.jwt.VerifiedJwt;
-import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.JwtClaimValue;
 import com.google.crypto.tink.testing.proto.JwtFromJwkSetRequest;
 import com.google.crypto.tink.testing.proto.JwtFromJwkSetResponse;
@@ -44,12 +44,9 @@
 import com.google.crypto.tink.testing.proto.JwtVerifyResponse;
 import com.google.crypto.tink.testing.proto.NullValue;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.StringValue;
 import com.google.protobuf.Timestamp;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.time.Clock;
@@ -66,6 +63,24 @@
     JwtSignatureConfig.register();
   }
 
+  @Override
+  public void createJwtMac(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, JwtMac.class);
+  }
+
+  @Override
+  public void createJwtPublicKeySign(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, JwtPublicKeySign.class);
+  }
+
+  @Override
+  public void createJwtPublicKeyVerify(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, JwtPublicKeyVerify.class);
+  }
+
   private Instant timestampToInstant(Timestamp t) {
     return Instant.ofEpochMilli(t.getSeconds() * 1000 + t.getNanos() / 1000000);
   }
@@ -134,50 +149,56 @@
     return rawJwtBuilder.build();
   }
 
-  /** Creates a signed compact JWT. */
+  private JwtSignResponse computeMacAndEncode(JwtSignRequest request)
+      throws GeneralSecurityException {
+    JwtMac jwtMac =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(JwtMac.class);
+    try {
+      RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt());
+      String signedCompactJwt = jwtMac.computeMacAndEncode(rawJwt);
+      return JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build();
+    } catch (GeneralSecurityException e)  {
+      return JwtSignResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void computeMacAndEncode(
       JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver) {
-    JwtSignResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt());
-      JwtMac jwtMac = keysetHandle.getPrimitive(JwtMac.class);
-      String signedCompactJwt = jwtMac.computeMacAndEncode(rawJwt);
-      response = JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = JwtSignResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      JwtSignResponse response = computeMacAndEncode(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
-  /** Creates a signed compact JWT. */
+  private JwtSignResponse publicKeySignAndEncode(JwtSignRequest request)
+      throws GeneralSecurityException {
+    JwtPublicKeySign signer =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(JwtPublicKeySign.class);
+    try {
+      RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt());
+      String signedCompactJwt = signer.signAndEncode(rawJwt);
+      return JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build();
+    } catch (GeneralSecurityException e)  {
+      return JwtSignResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void publicKeySignAndEncode(
       JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver) {
-    JwtSignResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt());
-      JwtPublicKeySign signer = keysetHandle.getPrimitive(JwtPublicKeySign.class);
-      String signedCompactJwt = signer.signAndEncode(rawJwt);
-      response = JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = JwtSignResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      JwtSignResponse response = publicKeySignAndEncode(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
   private void addCustomClaimToBuilder(VerifiedJwt token, String name, JwtToken.Builder builder)
@@ -288,54 +309,60 @@
     return validatorBuilder.build();
   }
 
-  /** Decodes and verifies a signed, compact JWT. */
+  private JwtVerifyResponse verifyMacAndDecode(JwtVerifyRequest request)
+      throws GeneralSecurityException {
+    JwtMac jwtMac =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(JwtMac.class);
+    try {
+      JwtValidator validator = convertProtoValidatorToValidator(request.getValidator());
+      VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(request.getSignedCompactJwt(), validator);
+      JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt);
+      return JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build();
+    } catch (GeneralSecurityException e) {
+      return JwtVerifyResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void verifyMacAndDecode(
       JwtVerifyRequest request,
       StreamObserver<JwtVerifyResponse> responseObserver) {
-    JwtVerifyResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      JwtValidator validator = convertProtoValidatorToValidator(request.getValidator());
-      JwtMac jwtMac = keysetHandle.getPrimitive(JwtMac.class);
-      VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(request.getSignedCompactJwt(), validator);
-      JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt);
-      response = JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = JwtVerifyResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      JwtVerifyResponse response = verifyMacAndDecode(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (Exception e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
-  /** Decodes and verifies a signed, compact JWT. */
+  private JwtVerifyResponse publicKeyVerifyAndDecode(JwtVerifyRequest request)
+      throws GeneralSecurityException {
+    JwtPublicKeyVerify verifier =
+        Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+            .getPrimitive(JwtPublicKeyVerify.class);
+    try {
+      JwtValidator validator = convertProtoValidatorToValidator(request.getValidator());
+      VerifiedJwt verifiedJwt = verifier.verifyAndDecode(request.getSignedCompactJwt(), validator);
+      JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt);
+      return JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build();
+    } catch (GeneralSecurityException e) {
+      return JwtVerifyResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void publicKeyVerifyAndDecode(
       JwtVerifyRequest request,
       StreamObserver<JwtVerifyResponse> responseObserver) {
-    JwtVerifyResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      JwtValidator validator = convertProtoValidatorToValidator(request.getValidator());
-      JwtPublicKeyVerify verifier = keysetHandle.getPrimitive(JwtPublicKeyVerify.class);
-      VerifiedJwt verifiedJwt = verifier.verifyAndDecode(request.getSignedCompactJwt(), validator);
-      JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt);
-      response = JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = JwtVerifyResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      JwtVerifyResponse response = publicKeyVerifyAndDecode(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
   /** Converts a Tink JWT Keyset to a JWK set. */
@@ -345,15 +372,12 @@
     JwtToJwkSetResponse response;
     try {
       KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
+          TinkProtoKeysetFormat.parseKeyset(
+              request.getKeyset().toByteArray(), InsecureSecretKeyAccess.get());
       String jwkSet = JwkSetConverter.fromPublicKeysetHandle(keysetHandle);
       response = JwtToJwkSetResponse.newBuilder().setJwkSet(jwkSet).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
+    } catch (GeneralSecurityException | IOException e) {
       response = JwtToJwkSetResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
     }
     responseObserver.onNext(response);
     responseObserver.onCompleted();
@@ -366,19 +390,14 @@
     JwtFromJwkSetResponse response;
     try {
       KeysetHandle keysetHandle = JwkSetConverter.toPublicKeysetHandle(request.getJwkSet());
-
-      Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
-      ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
-      BinaryKeysetWriter.withOutputStream(keysetStream).write(keyset);
-      keysetStream.close();
+      byte[] serializedKeyset =
+          TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
       response =
           JwtFromJwkSetResponse.newBuilder()
-              .setKeyset(ByteString.copyFrom(keysetStream.toByteArray()))
+              .setKeyset(ByteString.copyFrom(serializedKeyset))
               .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
+    } catch (GeneralSecurityException | IOException e) {
       response = JwtFromJwkSetResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
       return;
     }
     responseObserver.onNext(response);
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/KeysetServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/KeysetServiceImpl.java
index 31dea2b..aff2ad9 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/KeysetServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/KeysetServiceImpl.java
@@ -16,19 +16,16 @@
 
 package com.google.crypto.tink.testing;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.BinaryKeysetWriter;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
-import com.google.crypto.tink.JsonKeysetWriter;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetReader;
-import com.google.crypto.tink.KeysetWriter;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
-import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.testing.proto.KeysetFromJsonRequest;
 import com.google.crypto.tink.testing.proto.KeysetFromJsonResponse;
 import com.google.crypto.tink.testing.proto.KeysetGenerateRequest;
@@ -47,11 +44,7 @@
 import com.google.crypto.tink.testing.proto.KeysetWriteEncryptedResponse;
 import com.google.crypto.tink.testing.proto.KeysetWriterType;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 /** Implement a gRPC Keyset Testing service. */
@@ -85,19 +78,14 @@
       KeyTemplate template =
           KeyTemplateProtoConverter.fromByteArray(request.getTemplate().toByteArray());
       KeysetHandle keysetHandle = KeysetHandle.generateNew(template);
-      Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
-      ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
-      BinaryKeysetWriter.withOutputStream(keysetStream).write(keyset);
-      keysetStream.close();
+      byte[] serializedPublicKeyset =
+          TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
       response =
           KeysetGenerateResponse.newBuilder()
-              .setKeyset(ByteString.copyFrom(keysetStream.toByteArray()))
+              .setKeyset(ByteString.copyFrom(serializedPublicKeyset))
               .build();
     } catch (GeneralSecurityException e) {
       response = KeysetGenerateResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
     }
     responseObserver.onNext(response);
     responseObserver.onCompleted();
@@ -109,22 +97,17 @@
     KeysetPublicResponse response;
     try {
       KeysetHandle privateKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getPrivateKeyset().toByteArray()));
+          TinkProtoKeysetFormat.parseKeyset(
+              request.getPrivateKeyset().toByteArray(), InsecureSecretKeyAccess.get());
       KeysetHandle publicKeysetHandle = privateKeysetHandle.getPublicKeysetHandle();
-      Keyset publicKeyset = CleartextKeysetHandle.getKeyset(publicKeysetHandle);
-      ByteArrayOutputStream publicKeysetStream = new ByteArrayOutputStream();
-      BinaryKeysetWriter.withOutputStream(publicKeysetStream).write(publicKeyset);
-      publicKeysetStream.close();
+      byte[] serializedPublicKeyset =
+          TinkProtoKeysetFormat.serializeKeyset(publicKeysetHandle, InsecureSecretKeyAccess.get());
       response =
           KeysetPublicResponse.newBuilder()
-              .setPublicKeyset(ByteString.copyFrom(publicKeysetStream.toByteArray()))
+              .setPublicKeyset(ByteString.copyFrom(serializedPublicKeyset))
               .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
+    } catch (GeneralSecurityException e) {
       response = KeysetPublicResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
     }
     responseObserver.onNext(response);
     responseObserver.onCompleted();
@@ -136,21 +119,13 @@
     KeysetToJsonResponse response;
     try {
       KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
-      ByteArrayOutputStream jsonKeysetStream = new ByteArrayOutputStream();
-      JsonKeysetWriter.withOutputStream(jsonKeysetStream).write(keyset);
-      jsonKeysetStream.close();
-      response =
-          KeysetToJsonResponse.newBuilder()
-              .setJsonKeyset(jsonKeysetStream.toString("UTF-8"))
-              .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
+          TinkProtoKeysetFormat.parseKeyset(
+              request.getKeyset().toByteArray(), InsecureSecretKeyAccess.get());
+      String jsonKeyset =
+          TinkJsonProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
+      response = KeysetToJsonResponse.newBuilder().setJsonKeyset(jsonKeyset).build();
+    } catch (GeneralSecurityException e) {
       response = KeysetToJsonResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
     }
     responseObserver.onNext(response);
     responseObserver.onCompleted();
@@ -162,20 +137,16 @@
     KeysetFromJsonResponse response;
     try {
       KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(JsonKeysetReader.withString(request.getJsonKeyset()));
-      Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
-      ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
-      BinaryKeysetWriter.withOutputStream(keysetStream).write(keyset);
-      keysetStream.close();
+          TinkJsonProtoKeysetFormat.parseKeyset(
+              request.getJsonKeyset(), InsecureSecretKeyAccess.get());
+      byte[] serializeKeyset =
+          TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
       response =
           KeysetFromJsonResponse.newBuilder()
-              .setKeyset(ByteString.copyFrom(keysetStream.toByteArray()))
+              .setKeyset(ByteString.copyFrom(serializeKeyset))
               .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
+    } catch (GeneralSecurityException e) {
       response = KeysetFromJsonResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
     }
     responseObserver.onNext(response);
     responseObserver.onCompleted();
@@ -189,38 +160,32 @@
     try {
       // get masterAead
       KeysetHandle masterKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getMasterKeyset().toByteArray()));
+          TinkProtoKeysetFormat.parseKeyset(
+              request.getMasterKeyset().toByteArray(), InsecureSecretKeyAccess.get());
       Aead masterAead = masterKeysetHandle.getPrimitive(Aead.class);
 
       // read encrypted keyset to keysetHandle
-      KeysetReader reader;
+      byte[] associatedData = request.getAssociatedData().getValue().toByteArray();
+
+      KeysetHandle keysetHandle;
       if (request.getKeysetReaderType() == KeysetReaderType.KEYSET_READER_BINARY) {
-        reader = BinaryKeysetReader.withBytes(request.getEncryptedKeyset().toByteArray());
+        keysetHandle =
+            TinkProtoKeysetFormat.parseEncryptedKeyset(
+                request.getEncryptedKeyset().toByteArray(), masterAead, associatedData);
       } else if (request.getKeysetReaderType() == KeysetReaderType.KEYSET_READER_JSON) {
-        reader = JsonKeysetReader.withBytes(request.getEncryptedKeyset().toByteArray());
+        keysetHandle =
+            TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+                request.getEncryptedKeyset().toStringUtf8(), masterAead, associatedData);
       } else {
         throw new IllegalArgumentException("unknown keyset reader type");
       }
-      KeysetHandle keysetHandle;
-      if (request.hasAssociatedData()) {
-        keysetHandle =
-            KeysetHandle.readWithAssociatedData(
-                reader, masterAead, request.getAssociatedData().getValue().toByteArray());
-      } else {
-        keysetHandle = KeysetHandle.read(reader, masterAead);
-      }
 
       // get keyset from keysetHandle
-      Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
-      ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
-      BinaryKeysetWriter.withOutputStream(keysetStream).write(keyset);
-      keysetStream.close();
+      byte[] keyset =
+          TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get());
       response =
-          KeysetReadEncryptedResponse.newBuilder()
-              .setKeyset(ByteString.copyFrom(keysetStream.toByteArray()))
-              .build();
-    } catch (IOException | GeneralSecurityException e) {
+          KeysetReadEncryptedResponse.newBuilder().setKeyset(ByteString.copyFrom(keyset)).build();
+    } catch (GeneralSecurityException e) {
       response = KeysetReadEncryptedResponse.newBuilder().setErr(e.toString()).build();
     }
     responseObserver.onNext(response);
@@ -235,38 +200,35 @@
     try {
       // get masterAead
       KeysetHandle masterKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getMasterKeyset().toByteArray()));
+          TinkProtoKeysetFormat.parseKeyset(
+              request.getMasterKeyset().toByteArray(), InsecureSecretKeyAccess.get());
       Aead masterAead = masterKeysetHandle.getPrimitive(Aead.class);
 
       // get keysetHandle
       KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
+          TinkProtoKeysetFormat.parseKeyset(
+              request.getKeyset().toByteArray(), InsecureSecretKeyAccess.get());
 
       // write keysetHandle as encrypted keyset
-      ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
-      KeysetWriter writer;
+      byte[] associatedData = request.getAssociatedData().getValue().toByteArray();
+      byte[] keyset;
       if (request.getKeysetWriterType() == KeysetWriterType.KEYSET_WRITER_BINARY) {
-        writer = BinaryKeysetWriter.withOutputStream(keysetStream);
+        keyset =
+            TinkProtoKeysetFormat.serializeEncryptedKeyset(
+                keysetHandle, masterAead, associatedData);
       } else if (request.getKeysetWriterType() == KeysetWriterType.KEYSET_WRITER_JSON) {
-        writer = JsonKeysetWriter.withOutputStream(keysetStream);
+        keyset =
+            TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+                    keysetHandle, masterAead, associatedData)
+                .getBytes(UTF_8);
       } else {
         throw new IllegalArgumentException("unknown keyset writer type");
       }
-      if (request.hasAssociatedData()) {
-        keysetHandle.writeWithAssociatedData(
-            writer, masterAead, request.getAssociatedData().getValue().toByteArray());
-      } else {
-        keysetHandle.write(writer, masterAead);
-      }
-
-      keysetStream.close();
       response =
           KeysetWriteEncryptedResponse.newBuilder()
-              .setEncryptedKeyset(ByteString.copyFrom(keysetStream.toByteArray()))
+              .setEncryptedKeyset(ByteString.copyFrom(keyset))
               .build();
-    } catch (IOException | GeneralSecurityException e) {
+    } catch (GeneralSecurityException e) {
       response = KeysetWriteEncryptedResponse.newBuilder().setErr(e.toString()).build();
     }
     responseObserver.onNext(response);
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/MacServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/MacServiceImpl.java
index 0c965f9..60cadf2 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/MacServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/MacServiceImpl.java
@@ -16,20 +16,16 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.Mac;
 import com.google.crypto.tink.testing.proto.ComputeMacRequest;
 import com.google.crypto.tink.testing.proto.ComputeMacResponse;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.MacGrpc.MacImplBase;
 import com.google.crypto.tink.testing.proto.VerifyMacRequest;
 import com.google.crypto.tink.testing.proto.VerifyMacResponse;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 /** Implements a gRPC MAC Testing service. */
@@ -38,49 +34,57 @@
   public MacServiceImpl() throws GeneralSecurityException {
   }
 
-  /** Encrypts a message. */
+  @Override
+  public void create(CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, Mac.class);
+  }
+
+  private ComputeMacResponse computeMac(
+      ComputeMacRequest request) throws GeneralSecurityException {
+    try {
+      Mac mac =
+          Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+              .getPrimitive(Mac.class);
+      byte[] macValue = mac.computeMac(request.getData().toByteArray());
+      return ComputeMacResponse.newBuilder().setMacValue(ByteString.copyFrom(macValue)).build();
+    } catch (GeneralSecurityException e)  {
+      return ComputeMacResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void computeMac(
       ComputeMacRequest request,
       StreamObserver<ComputeMacResponse> responseObserver) {
-    ComputeMacResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      Mac mac = keysetHandle.getPrimitive(Mac.class);
-      byte[] macValue = mac.computeMac(request.getData().toByteArray());
-      response = ComputeMacResponse.newBuilder().setMacValue(ByteString.copyFrom(macValue)).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = ComputeMacResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      responseObserver.onNext(computeMac(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
-  /** Decrypts a message. */
+  private VerifyMacResponse verifyMac(VerifyMacRequest request) throws GeneralSecurityException {
+    try {
+      Mac mac =
+          Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+              .getPrimitive(Mac.class);
+      mac.verifyMac(request.getMacValue().toByteArray(), request.getData().toByteArray());
+      return VerifyMacResponse.getDefaultInstance();
+    } catch (GeneralSecurityException e) {
+      return VerifyMacResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void verifyMac(
       VerifyMacRequest request,
       StreamObserver<VerifyMacResponse> responseObserver) {
-    VerifyMacResponse response;
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      Mac mac = keysetHandle.getPrimitive(Mac.class);
-      mac.verifyMac(request.getMacValue().toByteArray(), request.getData().toByteArray());
-      response = VerifyMacResponse.getDefaultInstance();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = VerifyMacResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      responseObserver.onNext(verifyMac(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/PrfSetServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/PrfSetServiceImpl.java
index 8840ce1..7b02d65 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/PrfSetServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/PrfSetServiceImpl.java
@@ -16,21 +16,17 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.prf.Prf;
 import com.google.crypto.tink.prf.PrfSet;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.PrfSetComputeRequest;
 import com.google.crypto.tink.testing.proto.PrfSetComputeResponse;
 import com.google.crypto.tink.testing.proto.PrfSetGrpc.PrfSetImplBase;
 import com.google.crypto.tink.testing.proto.PrfSetKeyIdsRequest;
 import com.google.crypto.tink.testing.proto.PrfSetKeyIdsResponse;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.Map;
 
@@ -40,61 +36,70 @@
   public PrfSetServiceImpl() throws GeneralSecurityException {
   }
 
-  /** Returns the key IDs of the keyset. */
   @Override
-  public void keyIds(
-      PrfSetKeyIdsRequest request, StreamObserver<PrfSetKeyIdsResponse> responseObserver) {
-    PrfSetKeyIdsResponse response;
+  public void create(CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, PrfSet.class);
+  }
+
+  private PrfSetKeyIdsResponse keyIds(
+      PrfSetKeyIdsRequest request) throws GeneralSecurityException {
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
+      PrfSet prfSet =
+          Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+              .getPrimitive(PrfSet.class);
       PrfSetKeyIdsResponse.Output output = PrfSetKeyIdsResponse.Output.newBuilder()
           .setPrimaryKeyId(prfSet.getPrimaryId())
           .addAllKeyId(prfSet.getPrfs().keySet())
           .build();
-      response =
-          PrfSetKeyIdsResponse.newBuilder()
-          .setOutput(output)
-          .build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = PrfSetKeyIdsResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      return PrfSetKeyIdsResponse.newBuilder().setOutput(output).build();
+    } catch (GeneralSecurityException e)  {
+      return PrfSetKeyIdsResponse.newBuilder().setErr(e.toString()).build();
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
+  }
+
+  @Override
+  public void keyIds(
+      PrfSetKeyIdsRequest request,
+      StreamObserver<PrfSetKeyIdsResponse> responseObserver) {
+    try {
+      responseObserver.onNext(keyIds(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
   }
 
   /** Computes the output of one PRF. */
-  @Override
-  public void compute(
-      PrfSetComputeRequest request, StreamObserver<PrfSetComputeResponse> responseObserver) {
-    PrfSetComputeResponse response;
+  private PrfSetComputeResponse compute(PrfSetComputeRequest request)
+      throws GeneralSecurityException {
     try {
-      KeysetHandle keysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      PrfSet prfSet = keysetHandle.getPrimitive(PrfSet.class);
+      PrfSet prfSet =
+          Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+              .getPrimitive(PrfSet.class);
       Map<Integer, Prf> prfs = prfSet.getPrfs();
       if (!prfs.containsKey(request.getKeyId())) {
-        response = PrfSetComputeResponse.newBuilder().setErr("Unknown Key ID.").build();
+        return PrfSetComputeResponse.newBuilder().setErr("Unknown Key ID.").build();
       } else {
         byte[] output =
             prfs.get(request.getKeyId())
                 .compute(request.getInputData().toByteArray(), request.getOutputLength());
-        response =
+        return
             PrfSetComputeResponse.newBuilder().setOutput(ByteString.copyFrom(output)).build();
       }
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = PrfSetComputeResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+    } catch (GeneralSecurityException e) {
+      return PrfSetComputeResponse.newBuilder().setErr(e.toString()).build();
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
+  }
+
+  @Override
+  public void compute(
+      PrfSetComputeRequest request,
+      StreamObserver<PrfSetComputeResponse> responseObserver) {
+    try {
+      responseObserver.onNext(compute(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/SignatureServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/SignatureServiceImpl.java
index 4b8b46c..f11c53a 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/SignatureServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/SignatureServiceImpl.java
@@ -16,21 +16,17 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.PublicKeySign;
 import com.google.crypto.tink.PublicKeyVerify;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.SignatureGrpc.SignatureImplBase;
 import com.google.crypto.tink.testing.proto.SignatureSignRequest;
 import com.google.crypto.tink.testing.proto.SignatureSignResponse;
 import com.google.crypto.tink.testing.proto.SignatureVerifyRequest;
 import com.google.crypto.tink.testing.proto.SignatureVerifyResponse;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 /** Implements a gRPC Signature Testing service. */
@@ -39,49 +35,62 @@
   public SignatureServiceImpl() throws GeneralSecurityException {
   }
 
-  /** Signs a message. */
   @Override
-  public void sign(
-      SignatureSignRequest request,
-      StreamObserver<SignatureSignResponse> responseObserver) {
-    SignatureSignResponse response;
-    try {
-      KeysetHandle privateKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getPrivateKeyset().toByteArray()));
-      PublicKeySign signer = privateKeysetHandle.getPrimitive(PublicKeySign.class);
-      byte[] signatureValue = signer.sign(request.getData().toByteArray());
-      response = SignatureSignResponse.newBuilder().setSignature(ByteString.copyFrom(signatureValue)).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = SignatureSignResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
-    }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
+  public void createPublicKeySign(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, PublicKeySign.class);
   }
 
-  /** Verifies a signature. */
+  @Override
+  public void createPublicKeyVerify(
+      CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, PublicKeyVerify.class);
+  }
+
+  private SignatureSignResponse sign(SignatureSignRequest request) throws GeneralSecurityException {
+    try {
+      PublicKeySign signer =
+          Util.parseBinaryProtoKeyset(request.getPrivateAnnotatedKeyset())
+              .getPrimitive(PublicKeySign.class);
+      byte[] signatureValue = signer.sign(request.getData().toByteArray());
+      return SignatureSignResponse.newBuilder().setSignature(ByteString.copyFrom(signatureValue)).build();
+    } catch (GeneralSecurityException e)  {
+      return SignatureSignResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
+  @Override
+  public void sign(
+      SignatureSignRequest request, StreamObserver<SignatureSignResponse> responseObserver) {
+    try {
+      responseObserver.onNext(sign(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
+  }
+
+  private SignatureVerifyResponse verify(SignatureVerifyRequest request)
+      throws GeneralSecurityException {
+    try {
+      PublicKeyVerify verifier =
+          Util.parseBinaryProtoKeyset(request.getPublicAnnotatedKeyset())
+              .getPrimitive(PublicKeyVerify.class);
+      verifier.verify(request.getSignature().toByteArray(), request.getData().toByteArray());
+      return SignatureVerifyResponse.getDefaultInstance();
+    } catch (GeneralSecurityException e) {
+      return SignatureVerifyResponse.newBuilder().setErr(e.toString()).build();
+    }
+  }
+
   @Override
   public void verify(
-      SignatureVerifyRequest request,
-      StreamObserver<SignatureVerifyResponse> responseObserver) {
-    SignatureVerifyResponse response;
+      SignatureVerifyRequest request, StreamObserver<SignatureVerifyResponse> responseObserver) {
     try {
-      KeysetHandle publicKeysetHandle =
-          CleartextKeysetHandle.read(
-              BinaryKeysetReader.withBytes(request.getPublicKeyset().toByteArray()));
-      PublicKeyVerify verifier = publicKeysetHandle.getPrimitive(PublicKeyVerify.class);
-      verifier.verify(request.getSignature().toByteArray(), request.getData().toByteArray());
-      response = SignatureVerifyResponse.getDefaultInstance();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = SignatureVerifyResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      responseObserver.onNext(verify(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/StreamingAeadServiceImpl.java b/testing/java_src/java/com/google/crypto/tink/testing/StreamingAeadServiceImpl.java
index 0b06937..e041a7c 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/StreamingAeadServiceImpl.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/StreamingAeadServiceImpl.java
@@ -16,18 +16,15 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.StreamingAeadDecryptRequest;
 import com.google.crypto.tink.testing.proto.StreamingAeadDecryptResponse;
 import com.google.crypto.tink.testing.proto.StreamingAeadEncryptRequest;
 import com.google.crypto.tink.testing.proto.StreamingAeadEncryptResponse;
 import com.google.crypto.tink.testing.proto.StreamingAeadGrpc.StreamingAeadImplBase;
 import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -41,16 +38,17 @@
   public StreamingAeadServiceImpl() throws GeneralSecurityException {
   }
 
-  /** Encrypts a message. */
   @Override
-  public void encrypt(
-      StreamingAeadEncryptRequest request,
-      StreamObserver<StreamingAeadEncryptResponse> responseObserver) {
-    StreamingAeadEncryptResponse response;
+  public void create(CreationRequest request, StreamObserver<CreationResponse> responseObserver) {
+    Util.createPrimitiveForRpc(request, responseObserver, StreamingAead.class);
+  }
+
+  private StreamingAeadEncryptResponse encrypt(StreamingAeadEncryptRequest request)
+      throws GeneralSecurityException {
     try {
-      KeysetHandle keysetHandle = CleartextKeysetHandle.read(
-          BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
+      StreamingAead streamingAead =
+          Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+              .getPrimitive(StreamingAead.class);
 
       ByteArrayOutputStream ciphertextStream = new ByteArrayOutputStream();
       try (OutputStream encryptingStream =
@@ -58,31 +56,35 @@
               ciphertextStream, request.getAssociatedData().toByteArray())) {
         request.getPlaintext().writeTo(encryptingStream);
       }
-      response =
-          StreamingAeadEncryptResponse.newBuilder()
-              .setCiphertext(ByteString.copyFrom(ciphertextStream.toByteArray()))
-              .build();
+      return StreamingAeadEncryptResponse.newBuilder()
+          .setCiphertext(ByteString.copyFrom(ciphertextStream.toByteArray()))
+          .build();
 
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e)  {
-      response = StreamingAeadEncryptResponse.newBuilder().setErr(e.toString()).build();
+    } catch (GeneralSecurityException e)  {
+      return StreamingAeadEncryptResponse.newBuilder().setErr(e.toString()).build();
     } catch (IOException e) {
-      responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
-      return;
+      throw new GeneralSecurityException(e);
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
   }
 
-  /** Decrypts a message. */
   @Override
-  public void decrypt(
-      StreamingAeadDecryptRequest request,
-      StreamObserver<StreamingAeadDecryptResponse> responseObserver) {
-    StreamingAeadDecryptResponse response;
+  public void encrypt(
+      StreamingAeadEncryptRequest request,
+      StreamObserver<StreamingAeadEncryptResponse> responseObserver) {
     try {
-      KeysetHandle keysetHandle = CleartextKeysetHandle.read(
-          BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
-      StreamingAead streamingAead = keysetHandle.getPrimitive(StreamingAead.class);
+      responseObserver.onNext(encrypt(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
+  }
+
+  private StreamingAeadDecryptResponse decrypt(StreamingAeadDecryptRequest request)
+      throws GeneralSecurityException {
+    try {
+      StreamingAead streamingAead =
+          Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset())
+              .getPrimitive(StreamingAead.class);
 
       InputStream ciphertextStream = request.getCiphertext().newInput();
       InputStream decryptingStream = streamingAead.newDecryptingStream(
@@ -96,14 +98,22 @@
         plaintextStream.write(bytesRead);
       }
 
-      response = StreamingAeadDecryptResponse.newBuilder().setPlaintext(
+      return StreamingAeadDecryptResponse.newBuilder().setPlaintext(
           ByteString.copyFrom(plaintextStream.toByteArray())).build();
-    } catch (GeneralSecurityException | InvalidProtocolBufferException e) {
-      response = StreamingAeadDecryptResponse.newBuilder().setErr(e.toString()).build();
-    } catch (IOException e) {
-      response = StreamingAeadDecryptResponse.newBuilder().setErr(e.toString()).build();
+    } catch (GeneralSecurityException | IOException e)  {
+      return StreamingAeadDecryptResponse.newBuilder().setErr(e.toString()).build();
     }
-    responseObserver.onNext(response);
-    responseObserver.onCompleted();
+  }
+
+  @Override
+  public void decrypt(
+      StreamingAeadDecryptRequest request,
+      StreamObserver<StreamingAeadDecryptResponse> responseObserver) {
+    try {
+      responseObserver.onNext(decrypt(request));
+      responseObserver.onCompleted();
+    } catch (GeneralSecurityException e) {
+      responseObserver.onError(e);
+    }
   }
 }
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/TestingServer.java b/testing/java_src/java/com/google/crypto/tink/testing/TestingServer.java
index 69ae465..e3a9a0f 100644
--- a/testing/java_src/java/com/google/crypto/tink/testing/TestingServer.java
+++ b/testing/java_src/java/com/google/crypto/tink/testing/TestingServer.java
@@ -16,12 +16,12 @@
 
 package com.google.crypto.tink.testing;
 
-
-
 import com.google.crypto.tink.KmsClients;
 import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
 import com.google.crypto.tink.hybrid.HybridConfig;
+import com.google.crypto.tink.integration.awskms.AwsKmsClient;
+import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
 import com.google.crypto.tink.jwt.JwtMacConfig;
 import com.google.crypto.tink.jwt.JwtSignatureConfig;
 import com.google.crypto.tink.mac.MacConfig;
@@ -32,26 +32,38 @@
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.Security;
+import java.util.Optional;
 import org.conscrypt.Conscrypt;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
 
-/**
- * Starts a server with Tink testing services.
- */
+/** Starts a server with Tink testing services. */
 public final class TestingServer {
 
-  private TestingServer() {
-    // no instances
-  }
+  @Option(name = "--port", usage = "The service port")
+  private int port;
 
-  public static void main(String[] args)
-      throws InterruptedException, GeneralSecurityException, IOException {
+  @Option(name = "--gcp_credentials_path", usage = "Google Cloud KMS credentials path")
+  private String gcpCredentialsPath;
 
-    if ((args.length != 2) || !args[0].equals("--port")) {
-      System.out.println("Usage: TestingServer --port <port>");
-      System.exit(1);
-    }
-    int port = Integer.parseInt(args[1]);
+  @Option(
+      name = "--gcp_key_uri",
+      usage =
+          "Google Cloud KMS key URL of the form:"
+              + " gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.")
+  private String gcpKeyUri;
 
+  @Option(name = "--aws_credentials_path", usage = "AWS KMS credentials path")
+  private String awsCredentialsPath;
+
+  @Option(
+      name = "--aws_key_uri",
+      usage =
+          "AWS KMS key URL of the form: aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>.")
+  private String awsKeyUri;
+
+  public void run() throws InterruptedException, GeneralSecurityException, IOException {
     installConscrypt();
     AeadConfig.register();
     DeterministicAeadConfig.register();
@@ -62,6 +74,8 @@
     PrfConfig.register();
     SignatureConfig.register();
     StreamingAeadConfig.register();
+    GcpKmsClient.register(Optional.ofNullable(gcpKeyUri), Optional.of(gcpCredentialsPath));
+    AwsKmsClient.register(Optional.ofNullable(awsKeyUri), Optional.of(awsCredentialsPath));
 
     System.out.println("Start server on port " + port);
     KmsClients.add(new FakeKmsClient());
@@ -81,6 +95,20 @@
         .awaitTermination();
   }
 
+  public static void main(String[] args)
+      throws InterruptedException, GeneralSecurityException, IOException {
+
+    TestingServer server = new TestingServer();
+    CmdLineParser parser = new CmdLineParser(server);
+    try {
+      parser.parseArgument(args);
+    } catch (CmdLineException e) {
+      System.err.println("TestingServer [options...] arguments...");
+      parser.printUsage(System.err);
+    }
+    server.run();
+  }
+
   private static void installConscrypt() {
     try {
       Conscrypt.checkAvailability();
diff --git a/testing/java_src/java/com/google/crypto/tink/testing/Util.java b/testing/java_src/java/com/google/crypto/tink/testing/Util.java
new file mode 100644
index 0000000..b553d63
--- /dev/null
+++ b/testing/java_src/java/com/google/crypto/tink/testing/Util.java
@@ -0,0 +1,63 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.testing;
+
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.monitoring.MonitoringAnnotations;
+import com.google.crypto.tink.testing.proto.AnnotatedKeyset;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
+import io.grpc.stub.StreamObserver;
+import java.security.GeneralSecurityException;
+
+/**
+ * Utility functions for implementing Services.
+ */
+final class Util {
+  static KeysetHandle parseBinaryProtoKeyset(AnnotatedKeyset annotatedKeyset)
+      throws GeneralSecurityException {
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseKeyset(
+            annotatedKeyset.getSerializedKeyset().toByteArray(), InsecureSecretKeyAccess.get());
+    return KeysetHandle.newBuilder(handle)
+        .setMonitoringAnnotations(
+            MonitoringAnnotations.newBuilder().addAll(annotatedKeyset.getAnnotationsMap()).build())
+        .build();
+  }
+
+  /** Responds to a "create" request for a specific class */
+  static void createPrimitiveForRpc(
+      CreationRequest request,
+      StreamObserver<CreationResponse> responseObserver,
+      Class<?> primitiveClass) {
+    try {
+      KeysetHandle keysetHandle = parseBinaryProtoKeyset(request.getAnnotatedKeyset());
+      // We create to check if there is an exception thrown.
+      Object unused = keysetHandle.getPrimitive(primitiveClass);
+    } catch (GeneralSecurityException e) {
+      responseObserver.onNext(CreationResponse.newBuilder().setErr(e.toString()).build());
+      responseObserver.onCompleted();
+      return;
+    }
+    responseObserver.onNext(CreationResponse.getDefaultInstance());
+    responseObserver.onCompleted();
+  }
+
+  private Util() {}
+}
diff --git a/testing/java_src/javatests/com/google/crypto/tink/testing/AsymmetricTestingServicesTest.java b/testing/java_src/javatests/com/google/crypto/tink/testing/AsymmetricTestingServicesTest.java
index c65f3e3..64d7bc7 100644
--- a/testing/java_src/javatests/com/google/crypto/tink/testing/AsymmetricTestingServicesTest.java
+++ b/testing/java_src/javatests/com/google/crypto/tink/testing/AsymmetricTestingServicesTest.java
@@ -19,11 +19,15 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.config.TinkConfig;
 import com.google.crypto.tink.hybrid.EciesAeadHkdfPrivateKeyManager;
 import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
 import com.google.crypto.tink.signature.EcdsaSignKeyManager;
+import com.google.crypto.tink.testing.proto.AnnotatedKeyset;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.HybridDecryptRequest;
 import com.google.crypto.tink.testing.proto.HybridDecryptResponse;
 import com.google.crypto.tink.testing.proto.HybridEncryptRequest;
@@ -58,23 +62,19 @@
   HybridGrpc.HybridBlockingStub hybridStub;
   SignatureGrpc.SignatureBlockingStub signatureStub;
 
-
   @Before
   public void setUp() throws Exception {
     TinkConfig.register();
     String serverName = InProcessServerBuilder.generateName();
-    server = InProcessServerBuilder
-        .forName(serverName)
-        .directExecutor()
-        .addService(new KeysetServiceImpl())
-        .addService(new HybridServiceImpl())
-        .addService(new SignatureServiceImpl())
-        .build()
-        .start();
-    channel = InProcessChannelBuilder
-        .forName(serverName)
-        .directExecutor()
-        .build();
+    server =
+        InProcessServerBuilder.forName(serverName)
+            .directExecutor()
+            .addService(new KeysetServiceImpl())
+            .addService(new HybridServiceImpl())
+            .addService(new SignatureServiceImpl())
+            .build()
+            .start();
+    channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
     keysetStub = KeysetGrpc.newBlockingStub(channel);
     hybridStub = HybridGrpc.newBlockingStub(channel);
     signatureStub = SignatureGrpc.newBlockingStub(channel);
@@ -102,6 +102,72 @@
     return keysetStub.public_(request);
   }
 
+  @Test
+  public void hybridDecryptCreateKeyset_success() throws Exception {
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(
+            EciesAeadHkdfPrivateKeyManager.eciesP256HkdfHmacSha256Aes128GcmTemplate());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        hybridStub.createHybridDecrypt(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void hybridDecryptCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        hybridStub.createHybridDecrypt(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
+
+  @Test
+  public void hybridEncryptCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(
+        EciesAeadHkdfPrivateKeyManager.eciesP256HkdfHmacSha256Aes128GcmTemplate());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    byte[] privateKeyset = keysetResponse.getKeyset().toByteArray();
+
+    KeysetPublicResponse pubResponse = publicKeyset(keysetStub, privateKeyset);
+    assertThat(pubResponse.getErr()).isEmpty();
+    CreationResponse response =
+        hybridStub.createHybridEncrypt(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(pubResponse.getPublicKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void hybridEncryptCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        hybridStub.createHybridEncrypt(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
   private static HybridEncryptResponse hybridEncrypt(
       HybridGrpc.HybridBlockingStub hybridStub,
       byte[] publicKeyset,
@@ -109,7 +175,10 @@
       byte[] contextInfo) {
     HybridEncryptRequest encRequest =
         HybridEncryptRequest.newBuilder()
-            .setPublicKeyset(ByteString.copyFrom(publicKeyset))
+            .setPublicAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(publicKeyset))
+                    .build())
             .setPlaintext(ByteString.copyFrom(plaintext))
             .setContextInfo(ByteString.copyFrom(contextInfo))
             .build();
@@ -123,7 +192,10 @@
       byte[] contextInfo) {
     HybridDecryptRequest decRequest =
         HybridDecryptRequest.newBuilder()
-            .setPrivateKeyset(ByteString.copyFrom(privateKeyset))
+            .setPrivateAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(privateKeyset))
+                    .build())
             .setCiphertext(ByteString.copyFrom(ciphertext))
             .setContextInfo(ByteString.copyFrom(contextInfo))
             .build();
@@ -166,17 +238,17 @@
   }
 
   @Test
-  public void hybridEncrypt_failsOnBadKeyset() throws Exception {
+  public void hybridEncrypt_errorOnBadKeyset() throws Exception {
     byte[] badKeyset = "bad keyset".getBytes(UTF_8);
     byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
     byte[] contextInfo = "hybrid_encrypt_bad_keyset".getBytes(UTF_8);
-    HybridEncryptResponse encResponse =
-        hybridEncrypt(hybridStub, badKeyset, plaintext, contextInfo);
-    assertThat(encResponse.getErr()).isNotEmpty();
+    assertThrows(
+        io.grpc.StatusRuntimeException.class,
+        () -> hybridEncrypt(hybridStub, badKeyset, plaintext, contextInfo));
   }
 
   @Test
-  public void hybridDecrypt_failsOnBadCiphertext() throws Exception {
+  public void hybridDecrypt_errorOnBadCiphertext() throws Exception {
     byte[] template = KeyTemplateProtoConverter.toByteArray(
         EciesAeadHkdfPrivateKeyManager.eciesP256HkdfHmacSha256Aes128GcmTemplate());
     byte[] badCiphertext = "bad ciphertext".getBytes(UTF_8);
@@ -190,9 +262,9 @@
     assertThat(pubResponse.getErr()).isEmpty();
     byte[] publicKeyset = pubResponse.getPublicKeyset().toByteArray();
 
-    HybridDecryptResponse decResponse =
-        hybridDecrypt(hybridStub, publicKeyset, badCiphertext, contextInfo);
-    assertThat(decResponse.getErr()).isNotEmpty();
+    assertThrows(
+        io.grpc.StatusRuntimeException.class,
+        () -> hybridDecrypt(hybridStub, publicKeyset, badCiphertext, contextInfo));
   }
 
   @Test
@@ -216,16 +288,84 @@
     byte[] ciphertext = encResponse.getCiphertext().toByteArray();
 
     byte[] badKeyset = "bad keyset".getBytes(UTF_8);
-    HybridDecryptResponse decResponse =
-        hybridDecrypt(hybridStub, badKeyset, ciphertext, contextInfo);
-    assertThat(decResponse.getErr()).isNotEmpty();
+    assertThrows(
+        io.grpc.StatusRuntimeException.class,
+        () -> hybridDecrypt(hybridStub, badKeyset, ciphertext, contextInfo));
+  }
+
+  @Test
+  public void publicKeySignCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(
+        EcdsaSignKeyManager.ecdsaP256Template());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        signatureStub.createPublicKeySign(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void publicKeySignCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        signatureStub.createPublicKeySign(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
+
+  @Test
+  public void publicKeyVerifyCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(
+        EcdsaSignKeyManager.ecdsaP256Template());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    byte[] privateKeyset = keysetResponse.getKeyset().toByteArray();
+
+    KeysetPublicResponse pubResponse = publicKeyset(keysetStub, privateKeyset);
+    assertThat(pubResponse.getErr()).isEmpty();
+    CreationResponse response =
+        signatureStub.createPublicKeyVerify(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(pubResponse.getPublicKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void publicKeyVerifyCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        signatureStub.createPublicKeyVerify(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
   }
 
   private static SignatureSignResponse signatureSign(
       SignatureGrpc.SignatureBlockingStub signatureStub, byte[] privateKeyset, byte[] data) {
     SignatureSignRequest request =
         SignatureSignRequest.newBuilder()
-            .setPrivateKeyset(ByteString.copyFrom(privateKeyset))
+            .setPrivateAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(privateKeyset))
+                    .build())
             .setData(ByteString.copyFrom(data))
             .build();
     return signatureStub.sign(request);
@@ -238,7 +378,10 @@
       byte[] data) {
     SignatureVerifyRequest request =
         SignatureVerifyRequest.newBuilder()
-            .setPublicKeyset(ByteString.copyFrom(publicKeyset))
+            .setPublicAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(publicKeyset))
+                    .build())
             .setSignature(ByteString.copyFrom(signature))
             .setData(ByteString.copyFrom(data))
             .build();
diff --git a/testing/java_src/javatests/com/google/crypto/tink/testing/JwtServiceImplTest.java b/testing/java_src/javatests/com/google/crypto/tink/testing/JwtServiceImplTest.java
index 9d41ffb..0569ec1 100644
--- a/testing/java_src/javatests/com/google/crypto/tink/testing/JwtServiceImplTest.java
+++ b/testing/java_src/javatests/com/google/crypto/tink/testing/JwtServiceImplTest.java
@@ -19,12 +19,16 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
 
 import com.google.crypto.tink.KeyTemplates;
 import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
 import com.google.crypto.tink.jwt.JwtHmacKeyManager;
 import com.google.crypto.tink.jwt.JwtMacConfig;
 import com.google.crypto.tink.jwt.JwtSignatureConfig;
+import com.google.crypto.tink.testing.proto.AnnotatedKeyset;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.JwtClaimValue;
 import com.google.crypto.tink.testing.proto.JwtFromJwkSetRequest;
 import com.google.crypto.tink.testing.proto.JwtFromJwkSetResponse;
@@ -69,17 +73,14 @@
     JwtSignatureConfig.register();
 
     String serverName = InProcessServerBuilder.generateName();
-    server = InProcessServerBuilder
-        .forName(serverName)
-        .directExecutor()
-        .addService(new KeysetServiceImpl())
-        .addService(new JwtServiceImpl())
-        .build()
-        .start();
-    channel = InProcessChannelBuilder
-        .forName(serverName)
-        .directExecutor()
-        .build();
+    server =
+        InProcessServerBuilder.forName(serverName)
+            .directExecutor()
+            .addService(new KeysetServiceImpl())
+            .addService(new JwtServiceImpl())
+            .build()
+            .start();
+    channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
     keysetStub = KeysetGrpc.newBlockingStub(channel);
     jwtStub = JwtGrpc.newBlockingStub(channel);
   }
@@ -131,6 +132,35 @@
   }
 
   @Test
+  public void jwtMacCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("JWT_HS256"));
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        jwtStub.createJwtMac(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void jwtMacCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        jwtStub.createJwtMac(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
+  @Test
   public void jwtComputeVerifyMac_success() throws Exception {
     byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("JWT_HS256"));
     KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
@@ -142,7 +172,13 @@
     JwtToken token = generateToken("audience", expSecs, expNanos);
 
     JwtSignRequest signRequest =
-        JwtSignRequest.newBuilder().setKeyset(ByteString.copyFrom(keyset)).setRawJwt(token).build();
+        JwtSignRequest.newBuilder()
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
+            .setRawJwt(token)
+            .build();
     JwtSignResponse signResponse = jwtStub.computeMacAndEncode(signRequest);
     assertThat(signResponse.getErr()).isEmpty();
 
@@ -155,7 +191,10 @@
             .build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
@@ -176,14 +215,23 @@
     JwtToken token = JwtToken.getDefaultInstance();
 
     JwtSignRequest signRequest =
-        JwtSignRequest.newBuilder().setKeyset(ByteString.copyFrom(keyset)).setRawJwt(token).build();
+        JwtSignRequest.newBuilder()
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
+            .setRawJwt(token)
+            .build();
     JwtSignResponse signResponse = jwtStub.computeMacAndEncode(signRequest);
     assertThat(signResponse.getErr()).isEmpty();
 
     JwtValidator validator = JwtValidator.newBuilder().setAllowMissingExpiration(true).build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
@@ -194,6 +242,67 @@
   }
 
   @Test
+  public void jwtPublicKeySignCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("JWT_ES256"));
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        jwtStub.createJwtPublicKeySign(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void jwtPublicKeySignCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        jwtStub.createJwtPublicKeySign(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80})))
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
+  @Test
+  public void jwtPublicKeyVerifyCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("JWT_ES256"));
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    byte[] privateKeyset = keysetResponse.getKeyset().toByteArray();
+
+    KeysetPublicResponse pubResponse = publicKeyset(keysetStub, privateKeyset);
+    assertThat(pubResponse.getErr()).isEmpty();
+    CreationResponse response =
+        jwtStub.createJwtPublicKeyVerify(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(pubResponse.getPublicKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void jwtPublicKeyVerifyCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        jwtStub.createJwtPublicKeyVerify(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
+  @Test
   public void publicKeySignVerify_success() throws Exception {
     byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("JWT_ES256"));
     KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
@@ -210,7 +319,10 @@
 
     JwtSignRequest signRequest =
         JwtSignRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(privateKeyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(privateKeyset))
+                    .build())
             .setRawJwt(token)
             .build();
     JwtSignResponse signResponse = jwtStub.publicKeySignAndEncode(signRequest);
@@ -225,7 +337,10 @@
             .build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(publicKeyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(publicKeyset))
+                    .build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
@@ -243,11 +358,14 @@
     JwtToken token = generateToken("audience", 1234, 0);
     JwtSignRequest signRequest =
         JwtSignRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(badKeyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(badKeyset))
+                    .build())
             .setRawJwt(token)
             .build();
-    JwtSignResponse signResponse = jwtStub.computeMacAndEncode(signRequest);
-    assertThat(signResponse.getErr()).isNotEmpty();
+    assertThrows(
+        io.grpc.StatusRuntimeException.class, () -> jwtStub.computeMacAndEncode(signRequest));
   }
 
   @Test
@@ -260,7 +378,13 @@
     JwtToken token = generateToken("audience", 1234 - 10, 0);
 
     JwtSignRequest signRequest =
-        JwtSignRequest.newBuilder().setKeyset(ByteString.copyFrom(keyset)).setRawJwt(token).build();
+        JwtSignRequest.newBuilder()
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
+            .setRawJwt(token)
+            .build();
     JwtSignResponse signResponse = jwtStub.computeMacAndEncode(signRequest);
     assertThat(signResponse.getErr()).isEmpty();
 
@@ -273,7 +397,10 @@
             .build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
@@ -293,7 +420,10 @@
 
     JwtSignRequest signRequest =
         JwtSignRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setRawJwt(token)
             .build();
     JwtSignResponse signResponse = jwtStub.computeMacAndEncode(signRequest);
@@ -308,7 +438,10 @@
             .build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
@@ -329,7 +462,10 @@
 
     JwtSignRequest signRequest =
         JwtSignRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setRawJwt(token)
             .build();
     JwtSignResponse signResponse = jwtStub.computeMacAndEncode(signRequest);
@@ -348,7 +484,10 @@
             .build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(wrongKeyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(wrongKeyset))
+                    .build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
@@ -372,7 +511,10 @@
 
     JwtSignRequest signRequest =
         JwtSignRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(privateKeyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(privateKeyset))
+                    .build())
             .setRawJwt(token)
             .build();
     JwtSignResponse signResponse = jwtStub.publicKeySignAndEncode(signRequest);
@@ -400,7 +542,8 @@
             .build();
     JwtVerifyRequest verifyRequest =
         JwtVerifyRequest.newBuilder()
-            .setKeyset(fromResponse.getKeyset())
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder().setSerializedKeyset(fromResponse.getKeyset()).build())
             .setSignedCompactJwt(signResponse.getSignedCompactJwt())
             .setValidator(validator)
             .build();
diff --git a/testing/java_src/javatests/com/google/crypto/tink/testing/TestingServicesTest.java b/testing/java_src/javatests/com/google/crypto/tink/testing/TestingServicesTest.java
index 76eb754..09cb8a8 100644
--- a/testing/java_src/javatests/com/google/crypto/tink/testing/TestingServicesTest.java
+++ b/testing/java_src/javatests/com/google/crypto/tink/testing/TestingServicesTest.java
@@ -19,25 +19,30 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.BinaryKeysetReader;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.config.TinkConfig;
 import com.google.crypto.tink.daead.AesSivKeyManager;
 import com.google.crypto.tink.internal.KeyTemplateProtoConverter;
 import com.google.crypto.tink.mac.HmacKeyManager;
 import com.google.crypto.tink.prf.HmacPrfKeyManager;
-import com.google.crypto.tink.proto.Keyset;
 import com.google.crypto.tink.streamingaead.AesGcmHkdfStreamingKeyManager;
 import com.google.crypto.tink.testing.proto.AeadDecryptRequest;
 import com.google.crypto.tink.testing.proto.AeadDecryptResponse;
 import com.google.crypto.tink.testing.proto.AeadEncryptRequest;
 import com.google.crypto.tink.testing.proto.AeadEncryptResponse;
 import com.google.crypto.tink.testing.proto.AeadGrpc;
+import com.google.crypto.tink.testing.proto.AnnotatedKeyset;
 import com.google.crypto.tink.testing.proto.BytesValue;
 import com.google.crypto.tink.testing.proto.ComputeMacRequest;
 import com.google.crypto.tink.testing.proto.ComputeMacResponse;
+import com.google.crypto.tink.testing.proto.CreationRequest;
+import com.google.crypto.tink.testing.proto.CreationResponse;
 import com.google.crypto.tink.testing.proto.DeterministicAeadDecryptRequest;
 import com.google.crypto.tink.testing.proto.DeterministicAeadDecryptResponse;
 import com.google.crypto.tink.testing.proto.DeterministicAeadEncryptRequest;
@@ -77,6 +82,7 @@
 import com.google.protobuf.ByteString;
 import io.grpc.ManagedChannel;
 import io.grpc.Server;
+import io.grpc.StatusRuntimeException;
 import io.grpc.inprocess.InProcessChannelBuilder;
 import io.grpc.inprocess.InProcessServerBuilder;
 import java.util.Optional;
@@ -102,22 +108,19 @@
   public void setUp() throws Exception {
     TinkConfig.register();
     String serverName = InProcessServerBuilder.generateName();
-    server = InProcessServerBuilder
-        .forName(serverName)
-        .directExecutor()
-        .addService(new MetadataServiceImpl())
-        .addService(new KeysetServiceImpl())
-        .addService(new AeadServiceImpl())
-        .addService(new DeterministicAeadServiceImpl())
-        .addService(new StreamingAeadServiceImpl())
-        .addService(new MacServiceImpl())
-        .addService(new PrfSetServiceImpl())
-        .build()
-        .start();
-    channel = InProcessChannelBuilder
-        .forName(serverName)
-        .directExecutor()
-        .build();
+    server =
+        InProcessServerBuilder.forName(serverName)
+            .directExecutor()
+            .addService(new MetadataServiceImpl())
+            .addService(new KeysetServiceImpl())
+            .addService(new AeadServiceImpl())
+            .addService(new DeterministicAeadServiceImpl())
+            .addService(new StreamingAeadServiceImpl())
+            .addService(new MacServiceImpl())
+            .addService(new PrfSetServiceImpl())
+            .build()
+            .start();
+    channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
     metadataStub = MetadataGrpc.newBlockingStub(channel);
     keysetStub = KeysetGrpc.newBlockingStub(channel);
     aeadStub = AeadGrpc.newBlockingStub(channel);
@@ -162,7 +165,7 @@
     assertThat(response.getErr()).isEmpty();
     KeyTemplate template =
         KeyTemplateProtoConverter.fromByteArray(response.getKeyTemplate().toByteArray());
-    assertThat(template.getTypeUrl()).isEqualTo("type.googleapis.com/google.crypto.tink.AesGcmKey");
+    assertThat(template.toParameters()).isEqualTo(KeyTemplates.get("AES256_GCM").toParameters());
   }
 
   @Test
@@ -174,7 +177,7 @@
   }
 
   @Test
-  public void toJson_success() throws Exception {
+  public void fromJson_success() throws Exception {
     String jsonKeyset =
         ""
             + "{"
@@ -184,7 +187,7 @@
             + "      \"keyData\": {"
             + "        \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesGcmKey\","
             + "        \"keyMaterialType\": \"SYMMETRIC\","
-            + "        \"value\": \"AFakeTestKeyValue1234567\""
+            + "        \"value\": \"GhCC74uJ+2f4qlpaHwR4ylNQ\""
             + "      },"
             + "      \"outputPrefixType\": \"TINK\","
             + "      \"keyId\": 42,"
@@ -194,10 +197,11 @@
             + "}";
     KeysetFromJsonResponse fromResponse = keysetFromJson(keysetStub, jsonKeyset);
     assertThat(fromResponse.getErr()).isEmpty();
-    byte[] output = fromResponse.getKeyset().toByteArray();
+    byte[] serializedKeyset = fromResponse.getKeyset().toByteArray();
 
-    Keyset keyset = BinaryKeysetReader.withBytes(output).read();
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(42);
+    KeysetHandle parseKeysetHandle =
+        TinkProtoKeysetFormat.parseKeyset(serializedKeyset, InsecureSecretKeyAccess.get());
+    assertThat(parseKeysetHandle.getPrimary().getId()).isEqualTo(42);
   }
 
   @Test
@@ -237,7 +241,9 @@
   }
 
   private static KeysetWriteEncryptedResponse keysetWriteEncrypted(
-      KeysetGrpc.KeysetBlockingStub keysetStub, byte[] keyset, byte[] masterKeyset,
+      KeysetGrpc.KeysetBlockingStub keysetStub,
+      byte[] keyset,
+      byte[] masterKeyset,
       Optional<byte[]> associatedData) {
     KeysetWriteEncryptedRequest.Builder requestBuilder =
         KeysetWriteEncryptedRequest.newBuilder()
@@ -264,7 +270,8 @@
     byte[] masterKeyset = masterKeysetResponse.getKeyset().toByteArray();
 
     KeysetWriteEncryptedResponse writeResponse =
-        keysetWriteEncrypted(keysetStub, keyset, masterKeyset, /*associatedData=*/Optional.empty());
+        keysetWriteEncrypted(
+            keysetStub, keyset, masterKeyset, /*associatedData=*/ Optional.empty());
     assertThat(writeResponse.getErr()).isEmpty();
     byte[] encryptedKeyset = writeResponse.getEncryptedKeyset().toByteArray();
 
@@ -277,6 +284,24 @@
     byte[] output = readResponse.getKeyset().toByteArray();
 
     assertThat(output).isEqualTo(keyset);
+
+    // Empty associated data should be the same as no associated data.
+    KeysetReadEncryptedResponse readResponseWithEmptyAssociatedData =
+        keysetReadEncrypted(
+            keysetStub,
+            encryptedKeyset,
+            masterKeyset,
+            /*associatedData=*/ Optional.of(new byte[0]));
+    assertThat(readResponseWithEmptyAssociatedData.getErr()).isEmpty();
+    assertThat(readResponseWithEmptyAssociatedData.getKeyset().toByteArray()).isEqualTo(keyset);
+
+    KeysetReadEncryptedResponse readResponseWithInvalidAssociatedData =
+        keysetReadEncrypted(
+            keysetStub,
+            encryptedKeyset,
+            masterKeyset,
+            Optional.of("invalidAssociatedData".getBytes(UTF_8)));
+    assertThat(readResponseWithInvalidAssociatedData.getErr()).isNotEmpty();
   }
 
   @Test
@@ -300,12 +325,57 @@
     assertThat(encryptedKeyset).isNotEqualTo(keyset);
 
     KeysetReadEncryptedResponse readResponse =
-        keysetReadEncrypted(
-            keysetStub, encryptedKeyset, masterKeyset, Optional.of(associatedData));
+        keysetReadEncrypted(keysetStub, encryptedKeyset, masterKeyset, Optional.of(associatedData));
     assertThat(readResponse.getErr()).isEmpty();
     byte[] output = readResponse.getKeyset().toByteArray();
 
     assertThat(output).isEqualTo(keyset);
+
+    KeysetReadEncryptedResponse readResponseWithInvalidAssociatedData =
+        keysetReadEncrypted(
+            keysetStub,
+            encryptedKeyset,
+            masterKeyset,
+            Optional.of("invalidAssociatedData".getBytes(UTF_8)));
+    assertThat(readResponseWithInvalidAssociatedData.getErr()).isNotEmpty();
+
+    KeysetReadEncryptedResponse readResponseWithoutAssociatedData =
+        keysetReadEncrypted(
+            keysetStub, encryptedKeyset, masterKeyset, /*associatedData=*/ Optional.empty());
+    assertThat(readResponseWithoutAssociatedData.getErr()).isNotEmpty();
+  }
+
+  @Test
+  public void generateEncryptDecryptKeysetWithEmptyAssociatedData() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("AES128_GCM"));
+    byte[] emptyAssociatedData = new byte[0];
+
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    byte[] keyset = keysetResponse.getKeyset().toByteArray();
+
+    KeysetGenerateResponse masterKeysetResponse = generateKeyset(keysetStub, template);
+    assertThat(masterKeysetResponse.getErr()).isEmpty();
+    byte[] masterKeyset = masterKeysetResponse.getKeyset().toByteArray();
+
+    KeysetWriteEncryptedResponse writeResponse =
+        keysetWriteEncrypted(keysetStub, keyset, masterKeyset, Optional.of(emptyAssociatedData));
+    assertThat(writeResponse.getErr()).isEmpty();
+    byte[] encryptedKeyset = writeResponse.getEncryptedKeyset().toByteArray();
+
+    assertThat(encryptedKeyset).isNotEqualTo(keyset);
+
+    KeysetReadEncryptedResponse readResponse =
+        keysetReadEncrypted(
+            keysetStub, encryptedKeyset, masterKeyset, Optional.of(emptyAssociatedData));
+    assertThat(readResponse.getErr()).isEmpty();
+    byte[] output = readResponse.getKeyset().toByteArray();
+    assertThat(output).isEqualTo(keyset);
+
+    KeysetReadEncryptedResponse readResponseWithoutAssociatedData =
+        keysetReadEncrypted(keysetStub, encryptedKeyset, masterKeyset, Optional.empty());
+    assertThat(readResponseWithoutAssociatedData.getErr()).isEmpty();
+    assertThat(readResponseWithoutAssociatedData.getKeyset().toByteArray()).isEqualTo(keyset);
   }
 
   @Test
@@ -340,11 +410,45 @@
     assertThat(readResponse2.getErr()).isNotEmpty();
   }
 
+  // TODO(juerg): Add tests for KEYSET_WRITER_JSON.
+
+  @Test
+  public void aeadCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("AES128_GCM"));
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        aeadStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void aeadCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        aeadStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
   private static AeadEncryptResponse aeadEncrypt(
       AeadGrpc.AeadBlockingStub aeadStub, byte[] keyset, byte[] plaintext, byte[] associatedData) {
     AeadEncryptRequest encRequest =
         AeadEncryptRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setPlaintext(ByteString.copyFrom(plaintext))
             .setAssociatedData(ByteString.copyFrom(associatedData))
             .build();
@@ -355,7 +459,10 @@
       AeadGrpc.AeadBlockingStub aeadStub, byte[] keyset, byte[] ciphertext, byte[] associatedData) {
     AeadDecryptRequest decRequest =
         AeadDecryptRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setCiphertext(ByteString.copyFrom(ciphertext))
             .setAssociatedData(ByteString.copyFrom(associatedData))
             .build();
@@ -391,15 +498,6 @@
   }
 
   @Test
-  public void aeadEncrypt_failsOnBadKeyset() throws Exception {
-    byte[] badKeyset = "bad keyset".getBytes(UTF_8);
-    byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
-    byte[] associatedData = "aead_encrypt_fails_on_bad_keyset".getBytes(UTF_8);
-    AeadEncryptResponse encResponse = aeadEncrypt(aeadStub, badKeyset, plaintext, associatedData);
-    assertThat(encResponse.getErr()).isNotEmpty();
-  }
-
-  @Test
   public void aeadDecrypt_failsOnBadCiphertext() throws Exception {
     byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("AES128_GCM"));
     byte[] badCiphertext = "bad ciphertext".getBytes(UTF_8);
@@ -414,23 +512,29 @@
   }
 
   @Test
-  public void aeadDecrypt_failsOnBadKeyset() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(KeyTemplates.get("AES128_GCM"));
-    byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
-    byte[] associatedData = "generate_encrypt_decrypt".getBytes(UTF_8);
-
+  public void deterministicAeadCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(AesSivKeyManager.aes256SivTemplate());
     KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
     assertThat(keysetResponse.getErr()).isEmpty();
-    byte[] keyset = keysetResponse.getKeyset().toByteArray();
+    CreationResponse response =
+        daeadStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder().setSerializedKeyset(keysetResponse.getKeyset()))
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
 
-    AeadEncryptResponse encResponse = aeadEncrypt(aeadStub, keyset, plaintext, associatedData);
-    assertThat(encResponse.getErr()).isEmpty();
-    byte[] ciphertext = encResponse.getCiphertext().toByteArray();
-
-    byte[] badKeyset = "bad keyset".getBytes(UTF_8);
-
-    AeadDecryptResponse decResponse = aeadDecrypt(aeadStub, badKeyset, ciphertext, associatedData);
-    assertThat(decResponse.getErr()).isNotEmpty();
+  @Test
+  public void deterministicAeadCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        daeadStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80})))
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
   }
 
   private static DeterministicAeadEncryptResponse daeadEncrypt(
@@ -440,7 +544,8 @@
       byte[] associatedData) {
     DeterministicAeadEncryptRequest encRequest =
         DeterministicAeadEncryptRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder().setSerializedKeyset(ByteString.copyFrom(keyset)))
             .setPlaintext(ByteString.copyFrom(plaintext))
             .setAssociatedData(ByteString.copyFrom(associatedData))
             .build();
@@ -454,7 +559,10 @@
       byte[] associatedData) {
     DeterministicAeadDecryptRequest decRequest =
         DeterministicAeadDecryptRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setCiphertext(ByteString.copyFrom(ciphertext))
             .setAssociatedData(ByteString.copyFrom(associatedData))
             .build();
@@ -489,9 +597,9 @@
     byte[] badKeyset = "bad keyset".getBytes(UTF_8);
     byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
     byte[] associatedData = "aead_encrypt_fails_on_bad_keyset".getBytes(UTF_8);
-    DeterministicAeadEncryptResponse encResponse =
-        daeadEncrypt(daeadStub, badKeyset, plaintext, associatedData);
-    assertThat(encResponse.getErr()).isNotEmpty();
+    assertThrows(
+        StatusRuntimeException.class,
+        () -> daeadEncrypt(daeadStub, badKeyset, plaintext, associatedData));
   }
 
   @Test
@@ -525,10 +633,40 @@
     byte[] ciphertext = encResponse.getCiphertext().toByteArray();
 
     byte[] badKeyset = "bad keyset".getBytes(UTF_8);
+    assertThrows(
+        StatusRuntimeException.class,
+        () -> daeadDecrypt(daeadStub, badKeyset, ciphertext, associatedData));
+  }
 
-    DeterministicAeadDecryptResponse decResponse =
-        daeadDecrypt(daeadStub, badKeyset, ciphertext, associatedData);
-    assertThat(decResponse.getErr()).isNotEmpty();
+  @Test
+  public void streamingAeadCreateKeyset_success() throws Exception {
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(
+            AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        streamingAeadStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void streamingAeadCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        streamingAeadStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
   }
 
   private static StreamingAeadEncryptResponse streamingAeadEncrypt(
@@ -538,7 +676,10 @@
       byte[] associatedData) {
     StreamingAeadEncryptRequest encRequest =
         StreamingAeadEncryptRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setPlaintext(ByteString.copyFrom(plaintext))
             .setAssociatedData(ByteString.copyFrom(associatedData))
             .build();
@@ -552,7 +693,10 @@
       byte[] associatedData) {
     StreamingAeadDecryptRequest decRequest =
         StreamingAeadDecryptRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setCiphertext(ByteString.copyFrom(ciphertext))
             .setAssociatedData(ByteString.copyFrom(associatedData))
             .build();
@@ -561,8 +705,9 @@
 
   @Test
   public void streamingAeadGenerateEncryptDecrypt_success() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(
-        AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(
+            AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
     byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
     byte[] associatedData = "generate_encrypt_decrypt".getBytes(UTF_8);
 
@@ -570,13 +715,13 @@
     assertThat(keysetResponse.getErr()).isEmpty();
     byte[] keyset = keysetResponse.getKeyset().toByteArray();
 
-    StreamingAeadEncryptResponse encResponse = streamingAeadEncrypt(
-        streamingAeadStub, keyset, plaintext, associatedData);
+    StreamingAeadEncryptResponse encResponse =
+        streamingAeadEncrypt(streamingAeadStub, keyset, plaintext, associatedData);
     assertThat(encResponse.getErr()).isEmpty();
     byte[] ciphertext = encResponse.getCiphertext().toByteArray();
 
-    StreamingAeadDecryptResponse decResponse = streamingAeadDecrypt(
-        streamingAeadStub, keyset, ciphertext, associatedData);
+    StreamingAeadDecryptResponse decResponse =
+        streamingAeadDecrypt(streamingAeadStub, keyset, ciphertext, associatedData);
     assertThat(decResponse.getErr()).isEmpty();
     byte[] output = decResponse.getPlaintext().toByteArray();
 
@@ -588,15 +733,16 @@
     byte[] badKeyset = "bad keyset".getBytes(UTF_8);
     byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
     byte[] associatedData = "streamingAead_encrypt_fails_on_bad_keyset".getBytes(UTF_8);
-    StreamingAeadEncryptResponse encResponse = streamingAeadEncrypt(
-        streamingAeadStub, badKeyset, plaintext, associatedData);
+    StreamingAeadEncryptResponse encResponse =
+        streamingAeadEncrypt(streamingAeadStub, badKeyset, plaintext, associatedData);
     assertThat(encResponse.getErr()).isNotEmpty();
   }
 
   @Test
   public void streamingAeadDecrypt_failsOnBadCiphertext() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(
-        AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(
+            AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
     byte[] badCiphertext = "bad ciphertext".getBytes(UTF_8);
     byte[] associatedData = "streamingAead_decrypt_fails_on_bad_ciphertext".getBytes(UTF_8);
 
@@ -604,15 +750,16 @@
     assertThat(keysetResponse.getErr()).isEmpty();
     byte[] keyset = keysetResponse.getKeyset().toByteArray();
 
-    StreamingAeadDecryptResponse decResponse = streamingAeadDecrypt(
-        streamingAeadStub, keyset, badCiphertext, associatedData);
+    StreamingAeadDecryptResponse decResponse =
+        streamingAeadDecrypt(streamingAeadStub, keyset, badCiphertext, associatedData);
     assertThat(decResponse.getErr()).isNotEmpty();
   }
 
   @Test
   public void streamingAeadDecrypt_failsOnBadKeyset() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(
-        AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(
+            AesGcmHkdfStreamingKeyManager.aes128GcmHkdf4KBTemplate());
     byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
     byte[] associatedData = "generate_encrypt_decrypt".getBytes(UTF_8);
 
@@ -620,23 +767,56 @@
     assertThat(keysetResponse.getErr()).isEmpty();
     byte[] keyset = keysetResponse.getKeyset().toByteArray();
 
-    StreamingAeadEncryptResponse encResponse = streamingAeadEncrypt(
-        streamingAeadStub, keyset, plaintext, associatedData);
+    StreamingAeadEncryptResponse encResponse =
+        streamingAeadEncrypt(streamingAeadStub, keyset, plaintext, associatedData);
     assertThat(encResponse.getErr()).isEmpty();
     byte[] ciphertext = encResponse.getCiphertext().toByteArray();
 
     byte[] badKeyset = "bad keyset".getBytes(UTF_8);
 
-    StreamingAeadDecryptResponse decResponse = streamingAeadDecrypt(
-        streamingAeadStub, badKeyset, ciphertext, associatedData);
+    StreamingAeadDecryptResponse decResponse =
+        streamingAeadDecrypt(streamingAeadStub, badKeyset, ciphertext, associatedData);
     assertThat(decResponse.getErr()).isNotEmpty();
   }
 
+  @Test
+  public void macCreateKeyset_success() throws Exception {
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(HmacKeyManager.hmacSha256HalfDigestTemplate());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        macStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void macCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        macStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
   private static ComputeMacResponse computeMac(
       MacGrpc.MacBlockingStub macStub, byte[] keyset, byte[] data) {
     ComputeMacRequest request =
         ComputeMacRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setData(ByteString.copyFrom(data))
             .build();
     return macStub.computeMac(request);
@@ -646,7 +826,10 @@
       MacGrpc.MacBlockingStub macStub, byte[] keyset, byte[] macValue, byte[] data) {
     VerifyMacRequest request =
         VerifyMacRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setMacValue(ByteString.copyFrom(macValue))
             .setData(ByteString.copyFrom(data))
             .build();
@@ -655,8 +838,8 @@
 
   @Test
   public void computeVerifyMac_success() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(
-        HmacKeyManager.hmacSha256HalfDigestTemplate());
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(HmacKeyManager.hmacSha256HalfDigestTemplate());
     byte[] data = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
 
     KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
@@ -682,8 +865,8 @@
 
   @Test
   public void verifyMac_failsOnBadMacValue() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(
-        HmacKeyManager.hmacSha256HalfDigestTemplate());
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(HmacKeyManager.hmacSha256HalfDigestTemplate());
     byte[] data = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
 
     KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
@@ -697,8 +880,8 @@
 
   @Test
   public void verifyMac_failsOnBadKeyset() throws Exception {
-    byte[] template = KeyTemplateProtoConverter.toByteArray(
-        HmacKeyManager.hmacSha256HalfDigestTemplate());
+    byte[] template =
+        KeyTemplateProtoConverter.toByteArray(HmacKeyManager.hmacSha256HalfDigestTemplate());
     byte[] data = "The quick brown fox jumps over the lazy dog".getBytes(UTF_8);
 
     KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
@@ -714,11 +897,43 @@
     assertThat(verifyResponse.getErr()).isNotEmpty();
   }
 
+  @Test
+  public void prfSetCreateKeyset_success() throws Exception {
+    byte[] template = KeyTemplateProtoConverter.toByteArray(HmacPrfKeyManager.hmacSha256Template());
+    KeysetGenerateResponse keysetResponse = generateKeyset(keysetStub, template);
+    assertThat(keysetResponse.getErr()).isEmpty();
+    CreationResponse response =
+        prfSetStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(keysetResponse.getKeyset())
+                        .build())
+                .build());
+    assertThat(response.getErr()).isEmpty();
+  }
+
+  @Test
+  public void prfSetCreateKeyset_fails() throws Exception {
+    CreationResponse response =
+        prfSetStub.create(
+            CreationRequest.newBuilder()
+                .setAnnotatedKeyset(
+                    AnnotatedKeyset.newBuilder()
+                        .setSerializedKeyset(ByteString.copyFrom(new byte[] {(byte) 0x80}))
+                        .build())
+                .build());
+    assertThat(response.getErr()).isNotEmpty();
+  }
+
   private static PrfSetKeyIdsResponse keyIds(
       PrfSetGrpc.PrfSetBlockingStub prfSetStub, byte[] keyset) {
     PrfSetKeyIdsRequest request =
         PrfSetKeyIdsRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .build();
     return prfSetStub.keyIds(request);
   }
@@ -731,7 +946,10 @@
       int outputLength) {
     PrfSetComputeRequest request =
         PrfSetComputeRequest.newBuilder()
-            .setKeyset(ByteString.copyFrom(keyset))
+            .setAnnotatedKeyset(
+                AnnotatedKeyset.newBuilder()
+                    .setSerializedKeyset(ByteString.copyFrom(keyset))
+                    .build())
             .setKeyId(keyId)
             .setInputData(ByteString.copyFrom(inputData))
             .setOutputLength(outputLength)
diff --git a/testing/java_src/proto/testing_api.proto b/testing/java_src/proto/testing_api.proto
deleted file mode 100644
index 2fb0e9e..0000000
--- a/testing/java_src/proto/testing_api.proto
+++ /dev/null
@@ -1,503 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-syntax = "proto3";
-
-package tink_testing_api;
-
-import "google/protobuf/duration.proto";
-import "google/protobuf/timestamp.proto";
-import "google/protobuf/wrappers.proto";
-
-option java_package = "com.google.crypto.tink.testing.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
-// Placeholder for java_stubby_library
-
-// Service providing metadata about the server.
-service Metadata {
-  // Returns some server information. A test may use this information to verify
-  // that it is talking to the right server.
-  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
-}
-
-message ServerInfoRequest {}
-
-message ServerInfoResponse {
-  string tink_version = 1;  // For example '1.4'
-  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
-}
-
-// Service for Keyset operations.
-service Keyset {
-  // Generates a key template from a key template name.
-  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
-  // Generates a new keyset from a template.
-  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
-  // Generates a public-key keyset from a private-key keyset.
-  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
-  // Converts a Keyset from Binary to Json Format
-  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
-  // Converts a Keyset from Json to Binary Format
-  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
-  // Reads an encrypted keyset using KeysetHandle.read() or
-  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
-  rpc ReadEncrypted(KeysetReadEncryptedRequest)
-      returns (KeysetReadEncryptedResponse) {}
-  // Writes an encrypted keyset using KeysetHandle.write() or
-  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
-  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
-      returns (KeysetWriteEncryptedResponse) {}
-}
-
-message KeysetTemplateRequest {
-  string template_name = 1;  // template name used by Tinkey
-}
-
-message KeysetTemplateResponse {
-  oneof result {
-    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
-    string err = 2;
-  }
-}
-
-message KeysetGenerateRequest {
-  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
-}
-
-message KeysetGenerateResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetPublicRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetPublicResponse {
-  oneof result {
-    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetToJsonRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetToJsonResponse {
-  oneof result {
-    string json_keyset = 1;
-    string err = 2;
-  }
-}
-
-message KeysetFromJsonRequest {
-  string json_keyset = 1;
-}
-
-message KeysetFromJsonResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-// Copy of google.protobuf.BytesValue
-message BytesValue {
-  // The bytes value.
-  bytes value = 1;
-}
-
-enum KeysetReaderType {
-  KEYSET_READER_UNKNOWN = 0;
-  KEYSET_READER_BINARY = 1;
-  KEYSET_READER_JSON = 2;
-}
-
-message KeysetReadEncryptedRequest {
-  bytes encrypted_keyset = 1;
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetReaderType keyset_reader_type = 4;
-}
-
-message KeysetReadEncryptedResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-enum KeysetWriterType {
-  KEYSET_WRITER_UNKNOWN = 0;
-  KEYSET_WRITER_BINARY = 1;
-  KEYSET_WRITER_JSON = 2;
-}
-
-message KeysetWriteEncryptedRequest {
-  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetWriterType keyset_writer_type = 4;
-}
-
-message KeysetWriteEncryptedResponse {
-  oneof result {
-    bytes encrypted_keyset = 1;
-    string err = 2;
-  }
-}
-
-// Service for AEAD encryption and decryption
-service Aead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
-}
-
-message AeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message AeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Deterministic AEAD encryption and decryption
-service DeterministicAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
-      returns (DeterministicAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
-      returns (DeterministicAeadDecryptResponse) {}
-}
-
-message DeterministicAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message DeterministicAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Streaming AEAD encryption and decryption
-service StreamingAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(StreamingAeadEncryptRequest)
-      returns (StreamingAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(StreamingAeadDecryptRequest)
-      returns (StreamingAeadDecryptResponse) {}
-}
-
-message StreamingAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message StreamingAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to compute and verify MACs
-service Mac {
-  // Computes a MAC for given data
-  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
-  // Verifies the validity of the MAC value, no error means success
-  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
-}
-
-message ComputeMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message ComputeMacResponse {
-  oneof result {
-    bytes mac_value = 1;
-    string err = 2;
-  }
-}
-
-message VerifyMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes mac_value = 2;
-  bytes data = 3;
-}
-
-message VerifyMacResponse {
-  string err = 1;
-}
-
-// Service to hybrid encrypt and decrypt
-service Hybrid {
-  // Encrypts plaintext binding context_info to the resulting ciphertext
-  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
-  // Decrypts ciphertext verifying the integrity of context_info
-  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
-}
-
-message HybridEncryptRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes context_info = 3;
-}
-
-message HybridEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message HybridDecryptRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes context_info = 3;
-}
-
-message HybridDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to sign and verify signatures.
-service Signature {
-  // Computes the signature for data
-  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
-  // Verifies that signature is a digital signature for data
-  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
-}
-
-message SignatureSignRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message SignatureSignResponse {
-  oneof result {
-    bytes signature = 1;
-    string err = 2;
-  }
-}
-
-message SignatureVerifyRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes signature = 2;
-  bytes data = 3;
-}
-
-message SignatureVerifyResponse {
-  string err = 1;
-}
-
-// Service for PrfSet computation
-service PrfSet {
-  // Returns the key ids and the primary key id in the keyset.
-  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
-  // Computes the output of the PRF with the given key_id in the PrfSet.
-  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
-}
-
-message PrfSetKeyIdsRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message PrfSetKeyIdsResponse {
-  message Output {
-    uint32 primary_key_id = 1;
-    repeated uint32 key_id = 2;
-  }
-  oneof result {
-    Output output = 1;
-    string err = 2;
-  }
-}
-
-message PrfSetComputeRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  uint32 key_id = 2;
-  bytes input_data = 3;
-  int32 output_length = 4;
-}
-
-message PrfSetComputeResponse {
-  oneof result {
-    bytes output = 1;
-    string err = 2;
-  }
-}
-
-// Service for JSON Web Tokens (JWT)
-service Jwt {
-  // Computes a signed compact JWT token.
-  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Computes a signed compact JWT token.
-  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Converts a Keyset from Tink Binary to JWK Set Format
-  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
-  // Converts a Keyset from JWK Set to Tink Binary Format
-  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
-}
-
-//  Used to represent the JSON null value.
-enum NullValue {
-  NULL_VALUE = 0;
-}
-
-message JwtClaimValue {
-  oneof kind {
-    NullValue null_value = 2;
-    double number_value = 3;
-    string string_value = 4;
-    bool bool_value = 5;
-    string json_object_value = 6;
-    string json_array_value = 7;
-  }
-}
-
-message JwtToken {
-  google.protobuf.StringValue issuer = 1;
-  google.protobuf.StringValue subject = 2;
-  repeated string audiences = 3;
-  google.protobuf.StringValue jwt_id = 4;
-  google.protobuf.Timestamp expiration = 5;
-  google.protobuf.Timestamp not_before = 6;
-  google.protobuf.Timestamp issued_at = 7;
-  map<string, JwtClaimValue> custom_claims = 8;
-  google.protobuf.StringValue type_header = 9;
-}
-
-message JwtValidator {
-  google.protobuf.StringValue expected_type_header = 7;
-  google.protobuf.StringValue expected_issuer = 1;
-  google.protobuf.StringValue expected_audience = 3;
-  bool ignore_type_header = 8;
-  bool ignore_issuer = 9;
-  bool ignore_audience = 11;
-  bool allow_missing_expiration = 12;
-  bool expect_issued_in_the_past = 13;
-  google.protobuf.Timestamp now = 5;
-  google.protobuf.Duration clock_skew = 6;
-}
-
-message JwtSignRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  JwtToken raw_jwt = 2;
-}
-
-message JwtSignResponse {
-  oneof result {
-    string signed_compact_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtVerifyRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  string signed_compact_jwt = 2;
-  JwtValidator validator = 3;
-}
-
-message JwtVerifyResponse {
-  oneof result {
-    JwtToken verified_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtToJwkSetRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message JwtToJwkSetResponse {
-  oneof result {
-    string jwk_set = 1;
-    string err = 2;
-  }
-}
-
-message JwtFromJwkSetRequest {
-  string jwk_set = 1;
-}
-
-message JwtFromJwkSetResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
diff --git a/testing/java_src/proto/BUILD.bazel b/testing/java_src/protos/BUILD.bazel
similarity index 100%
rename from testing/java_src/proto/BUILD.bazel
rename to testing/java_src/protos/BUILD.bazel
diff --git a/testing/java_src/protos/testing_api.proto b/testing/java_src/protos/testing_api.proto
new file mode 100644
index 0000000..7a02517
--- /dev/null
+++ b/testing/java_src/protos/testing_api.proto
@@ -0,0 +1,566 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+syntax = "proto3";
+
+package tink_testing_api;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+option java_package = "com.google.crypto.tink.testing.proto";
+option java_multiple_files = true;
+option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
+// Placeholder for java_stubby_library
+
+// Service providing metadata about the server.
+service Metadata {
+  // Returns some server information. A test may use this information to verify
+  // that it is talking to the right server.
+  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
+}
+
+message ServerInfoRequest {}
+
+message ServerInfoResponse {
+  string tink_version = 1;  // For example '1.4'
+  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
+}
+
+// Service for Keyset operations.
+service Keyset {
+  // Generates a key template from a key template name.
+  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
+  // Generates a new keyset from a template.
+  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
+  // Generates a public-key keyset from a private-key keyset.
+  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
+  // Converts a Keyset from Binary to Json Format
+  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
+  // Converts a Keyset from Json to Binary Format
+  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
+  // Reads an encrypted keyset using KeysetHandle.read() or
+  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
+  rpc ReadEncrypted(KeysetReadEncryptedRequest)
+      returns (KeysetReadEncryptedResponse) {}
+  // Writes an encrypted keyset using KeysetHandle.write() or
+  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
+  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
+      returns (KeysetWriteEncryptedResponse) {}
+}
+
+message KeysetTemplateRequest {
+  string template_name = 1;  // template name used by Tinkey
+}
+
+message KeysetTemplateResponse {
+  oneof result {
+    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
+    string err = 2;
+  }
+}
+
+message KeysetGenerateRequest {
+  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
+}
+
+message KeysetGenerateResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetPublicRequest {
+  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetPublicResponse {
+  oneof result {
+    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetToJsonRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetToJsonResponse {
+  oneof result {
+    string json_keyset = 1;
+    string err = 2;
+  }
+}
+
+message KeysetFromJsonRequest {
+  string json_keyset = 1;
+}
+
+message KeysetFromJsonResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+// Copy of google.protobuf.BytesValue
+message BytesValue {
+  // The bytes value.
+  bytes value = 1;
+}
+
+enum KeysetReaderType {
+  KEYSET_READER_UNKNOWN = 0;
+  KEYSET_READER_BINARY = 1;
+  KEYSET_READER_JSON = 2;
+}
+
+message KeysetReadEncryptedRequest {
+  bytes encrypted_keyset = 1;
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetReaderType keyset_reader_type = 4;
+}
+
+message KeysetReadEncryptedResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+enum KeysetWriterType {
+  KEYSET_WRITER_UNKNOWN = 0;
+  KEYSET_WRITER_BINARY = 1;
+  KEYSET_WRITER_JSON = 2;
+}
+
+message KeysetWriteEncryptedRequest {
+  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetWriterType keyset_writer_type = 4;
+}
+
+message KeysetWriteEncryptedResponse {
+  oneof result {
+    bytes encrypted_keyset = 1;
+    string err = 2;
+  }
+}
+
+message AnnotatedKeyset {
+  bytes serialized_keyset = 1;  // serialized google.crypto.tink.Keyset.
+  map<string, string> annotations = 2;
+}
+
+message CreationRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message CreationResponse {
+  // Empty means no error
+  string err = 1;
+}
+
+// Service for AEAD encryption and decryption
+service Aead {
+  // Creates an Aead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
+}
+
+message AeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message AeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Deterministic AEAD encryption and decryption
+service DeterministicAead {
+  // Creates a Deterministic AEAD object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
+      returns (DeterministicAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
+      returns (DeterministicAeadDecryptResponse) {}
+}
+
+message DeterministicAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message DeterministicAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Streaming AEAD encryption and decryption
+service StreamingAead {
+  // Creates a StreamingAead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(StreamingAeadEncryptRequest)
+      returns (StreamingAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(StreamingAeadDecryptRequest)
+      returns (StreamingAeadDecryptResponse) {}
+}
+
+message StreamingAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message StreamingAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to compute and verify MACs
+service Mac {
+  // Creates a Mac object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Computes a MAC for given data. The client must call "Create" first to see
+  // if creation succeeds before calling this.
+  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
+  // Verifies the validity of the MAC value, no error means success. The client
+  // must call "Create" first to see if creation succeeds before calling this.
+  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
+}
+
+message ComputeMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message ComputeMacResponse {
+  oneof result {
+    bytes mac_value = 1;
+    string err = 2;
+  }
+}
+
+message VerifyMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes mac_value = 2;
+  bytes data = 3;
+}
+
+message VerifyMacResponse {
+  string err = 1;
+}
+
+// Service to hybrid encrypt and decrypt
+service Hybrid {
+  // Creates a HybridEncrypt object without using it.
+  rpc CreateHybridEncrypt(CreationRequest) returns (CreationResponse) {}
+  // Creates a HybridDecrypt object without using it.
+  rpc CreateHybridDecrypt(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts plaintext binding context_info to the resulting ciphertext. The
+  // client must call "CreateHybridEncrypt" first to see if creation succeeds
+  // before calling this.
+  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
+  // Decrypts ciphertext verifying the integrity of context_info. The client
+  // must call "CreateHybridDecrypt" first to see if creation succeeds before
+  // calling this.
+  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
+}
+
+message HybridEncryptRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes context_info = 3;
+}
+
+message HybridEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message HybridDecryptRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes context_info = 3;
+}
+
+message HybridDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to sign and verify signatures.
+service Signature {
+  // Creates a PublicKeySign object without using it.
+  rpc CreatePublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a PublicKeyVerify object without using it.
+  rpc CreatePublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes the signature for data. The client must call "CreatePublicKeySign"
+  // first to see if creation succeeds before calling this.
+  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
+  // Verifies that signature is a digital signature for data. The client must
+  // call "CreatePublicKeyVerify" first to see if creation succeeds before
+  // calling this.
+  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
+}
+
+message SignatureSignRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message SignatureSignResponse {
+  oneof result {
+    bytes signature = 1;
+    string err = 2;
+  }
+}
+
+message SignatureVerifyRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes signature = 2;
+  bytes data = 3;
+}
+
+message SignatureVerifyResponse {
+  string err = 1;
+}
+
+// Service for PrfSet computation
+service PrfSet {
+  // Creates a PrfSet object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Returns the key ids and the primary key id in the keyset.The client must
+  // call "Create" first to see if creation succeeds before calling this.
+  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
+  // Computes the output of the PRF with the given key_id in the PrfSet.The
+  // client must call "Create" first to see if creation succeeds before calling
+  // this.
+  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
+}
+
+message PrfSetKeyIdsRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message PrfSetKeyIdsResponse {
+  message Output {
+    uint32 primary_key_id = 1;
+    repeated uint32 key_id = 2;
+  }
+  oneof result {
+    Output output = 1;
+    string err = 2;
+  }
+}
+
+message PrfSetComputeRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  uint32 key_id = 2;
+  bytes input_data = 3;
+  int32 output_length = 4;
+}
+
+message PrfSetComputeResponse {
+  oneof result {
+    bytes output = 1;
+    string err = 2;
+  }
+}
+
+// Service for JSON Web Tokens (JWT)
+service Jwt {
+  // Creates a JwtMac object without using it.
+  rpc CreateJwtMac(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeySign object without using it.
+  rpc CreateJwtPublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeyVerify object without using it.
+  rpc CreateJwtPublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes a signed compact JWT token.
+  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Computes a signed compact JWT token.
+  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Converts a Keyset from Tink Binary to JWK Set Format
+  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
+  // Converts a Keyset from JWK Set to Tink Binary Format
+  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
+}
+
+//  Used to represent the JSON null value.
+enum NullValue {
+  NULL_VALUE = 0;
+}
+
+message JwtClaimValue {
+  oneof kind {
+    NullValue null_value = 2;
+    double number_value = 3;
+    string string_value = 4;
+    bool bool_value = 5;
+    string json_object_value = 6;
+    string json_array_value = 7;
+  }
+}
+
+message JwtToken {
+  google.protobuf.StringValue issuer = 1;
+  google.protobuf.StringValue subject = 2;
+  repeated string audiences = 3;
+  google.protobuf.StringValue jwt_id = 4;
+  google.protobuf.Timestamp expiration = 5;
+  google.protobuf.Timestamp not_before = 6;
+  google.protobuf.Timestamp issued_at = 7;
+  map<string, JwtClaimValue> custom_claims = 8;
+  google.protobuf.StringValue type_header = 9;
+}
+
+message JwtValidator {
+  google.protobuf.StringValue expected_type_header = 7;
+  google.protobuf.StringValue expected_issuer = 1;
+  google.protobuf.StringValue expected_audience = 3;
+  bool ignore_type_header = 8;
+  bool ignore_issuer = 9;
+  bool ignore_audience = 11;
+  bool allow_missing_expiration = 12;
+  bool expect_issued_in_the_past = 13;
+  google.protobuf.Timestamp now = 5;
+  google.protobuf.Duration clock_skew = 6;
+}
+
+message JwtSignRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  JwtToken raw_jwt = 2;
+}
+
+message JwtSignResponse {
+  oneof result {
+    string signed_compact_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtVerifyRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  string signed_compact_jwt = 2;
+  JwtValidator validator = 3;
+}
+
+message JwtVerifyResponse {
+  oneof result {
+    JwtToken verified_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtToJwkSetRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message JwtToJwkSetResponse {
+  oneof result {
+    string jwk_set = 1;
+    string err = 2;
+  }
+}
+
+message JwtFromJwkSetRequest {
+  string jwk_set = 1;
+}
+
+message JwtFromJwkSetResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
diff --git a/testing/java_src/third_party/BUILD.bazel b/testing/java_src/third_party/BUILD.bazel
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/java_src/third_party/BUILD.bazel
diff --git a/testing/java_src/third_party/io_grpc_grpc_java.diff b/testing/java_src/third_party/io_grpc_grpc_java.diff
new file mode 100644
index 0000000..5c693ad
--- /dev/null
+++ b/testing/java_src/third_party/io_grpc_grpc_java.diff
@@ -0,0 +1,13 @@
+diff --git a/repositories.bzl b/repositories.bzl
+index 6e0817545..6b5274c3d 100644
+--- a/repositories.bzl
++++ b/repositories.bzl
+@@ -66,7 +66,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
+ IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS = {
+     "com.google.protobuf:protobuf-java": "@com_google_protobuf//:protobuf_java",
+     "com.google.protobuf:protobuf-java-util": "@com_google_protobuf//:protobuf_java_util",
+-    "com.google.protobuf:protobuf-javalite": "@com_google_protobuf_javalite//:protobuf_java_lite",
++    "com.google.protobuf:protobuf-javalite": "@com_google_protobuf_javalite//:protobuf_javalite",
+     "io.grpc:grpc-alts": "@io_grpc_grpc_java//alts",
+     "io.grpc:grpc-api": "@io_grpc_grpc_java//api",
+     "io.grpc:grpc-auth": "@io_grpc_grpc_java//auth",
diff --git a/testing/python/.bazelrc b/testing/python/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/testing/python/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/testing/python/.bazelversion b/testing/python/.bazelversion
index ac14c3d..09b254e 100644
--- a/testing/python/.bazelversion
+++ b/testing/python/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/testing/python/BUILD.bazel b/testing/python/BUILD.bazel
index f7d151e..ca67c0a 100644
--- a/testing/python/BUILD.bazel
+++ b/testing/python/BUILD.bazel
@@ -11,7 +11,7 @@
 
 python_grpc_library(
     name = "testing_api_python_library",
-    protos = ["//proto:testing_api_proto"],
+    protos = ["//protos:testing_api_proto"],
 )
 
 py_library(
@@ -98,6 +98,8 @@
         "@tink_py//tink/aead",
         "@tink_py//tink/daead",
         "@tink_py//tink/hybrid",
+        "@tink_py//tink/integration/awskms",
+        "@tink_py//tink/integration/gcpkms",
         "@tink_py//tink/jwt",
         "@tink_py//tink/mac",
         "@tink_py//tink/signature",
diff --git a/testing/python/jwt_service.py b/testing/python/jwt_service.py
index d7ad1a3..e2e08a2 100644
--- a/testing/python/jwt_service.py
+++ b/testing/python/jwt_service.py
@@ -28,8 +28,8 @@
 from google.protobuf import duration_pb2
 from google.protobuf import timestamp_pb2
 
-from proto import testing_api_pb2
-from proto import testing_api_pb2_grpc
+from protos import testing_api_pb2
+from protos import testing_api_pb2_grpc
 
 
 def _to_timestamp_tuple(t: datetime.datetime) -> Tuple[int, int]:
@@ -186,13 +186,49 @@
 class JwtServicer(testing_api_pb2_grpc.JwtServicer):
   """A service for signing and verifying JWTs."""
 
+  def CreateJwtMac(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a JwtMac without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(jwt.JwtMac)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
+  def CreateJwtPublicKeySign(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a JwtPublicKeySign without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(jwt.JwtPublicKeySign)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
+  def CreateJwtPublicKeyVerify(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a JwtPublicKeyVerify without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(jwt.JwtPublicKeyVerify)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def ComputeMacAndEncode(
       self, request: testing_api_pb2.JwtSignRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.JwtSignResponse:
     """Computes a MACed compact JWT."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(jwt.JwtMac)
       raw_jwt = raw_jwt_from_proto(request.raw_jwt)
       signed_compact_jwt = p.compute_mac_and_encode(raw_jwt)
@@ -207,7 +243,7 @@
     """Verifies a MAC value."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       validator = validator_from_proto(request.validator)
       p = keyset_handle.primitive(jwt.JwtMac)
       verified_jwt = p.verify_mac_and_decode(request.signed_compact_jwt,
@@ -223,7 +259,7 @@
     """Computes a signed compact JWT token."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(jwt.JwtPublicKeySign)
       raw_jwt = raw_jwt_from_proto(request.raw_jwt)
       signed_compact_jwt = p.sign_and_encode(raw_jwt)
@@ -238,7 +274,7 @@
     """Verifies the validity of the signed compact JWT token."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       validator = validator_from_proto(request.validator)
       p = keyset_handle.primitive(jwt.JwtPublicKeyVerify)
       verified_jwt = p.verify_and_decode(request.signed_compact_jwt, validator)
diff --git a/testing/python/jwt_service_test.py b/testing/python/jwt_service_test.py
index 40f2118..45eb8e4 100644
--- a/testing/python/jwt_service_test.py
+++ b/testing/python/jwt_service_test.py
@@ -18,7 +18,7 @@
 
 from tink import jwt
 
-from proto import testing_api_pb2
+from protos import testing_api_pb2
 import jwt_service
 import services
 
@@ -87,6 +87,31 @@
     jwt.register_jwt_mac()
     jwt.register_jwt_signature()
 
+  def test_create_jwt_mac(self):
+    keyset_servicer = services.KeysetServicer()
+    jwt_servicer = jwt_service.JwtServicer()
+
+    template = jwt.jwt_hs256_template().SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = jwt_servicer.CreateJwtMac(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_jwt_mac_broken_keyset(self):
+    jwt_servicer = jwt_service.JwtServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = jwt_servicer.CreateJwtMac(creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_generate_compute_verify_mac(self):
     keyset_servicer = services.KeysetServicer()
     jwt_servicer = jwt_service.JwtServicer()
@@ -97,7 +122,9 @@
     self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
     keyset = gen_response.keyset
 
-    comp_request = testing_api_pb2.JwtSignRequest(keyset=keyset)
+    comp_request = testing_api_pb2.JwtSignRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset))
     comp_request.raw_jwt.issuer.value = 'issuer'
     comp_request.raw_jwt.subject.value = 'subject'
     comp_request.raw_jwt.custom_claims['myclaim'].bool_value = True
@@ -108,7 +135,9 @@
     self.assertEqual(comp_response.WhichOneof('result'), 'signed_compact_jwt')
     signed_compact_jwt = comp_response.signed_compact_jwt
     verify_request = testing_api_pb2.JwtVerifyRequest(
-        keyset=keyset, signed_compact_jwt=signed_compact_jwt)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        signed_compact_jwt=signed_compact_jwt)
     verify_request.validator.expected_issuer.value = 'issuer'
     verify_request.validator.now.seconds = 1234
     verify_response = jwt_servicer.VerifyMacAndDecode(verify_request, self._ctx)
@@ -128,14 +157,18 @@
     self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
     keyset = gen_response.keyset
 
-    comp_request = testing_api_pb2.JwtSignRequest(keyset=keyset)
+    comp_request = testing_api_pb2.JwtSignRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset))
     comp_request.raw_jwt.issuer.value = 'issuer'
 
     comp_response = jwt_servicer.ComputeMacAndEncode(comp_request, self._ctx)
     self.assertEqual(comp_response.WhichOneof('result'), 'signed_compact_jwt')
     signed_compact_jwt = comp_response.signed_compact_jwt
     verify_request = testing_api_pb2.JwtVerifyRequest(
-        keyset=keyset, signed_compact_jwt=signed_compact_jwt)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        signed_compact_jwt=signed_compact_jwt)
     verify_request.validator.expected_issuer.value = 'issuer'
     verify_request.validator.allow_missing_expiration = True
     verify_response = jwt_servicer.VerifyMacAndDecode(verify_request, self._ctx)
@@ -143,6 +176,62 @@
     self.assertEqual(verify_response.WhichOneof('result'), 'verified_jwt')
     self.assertEqual(verify_response.verified_jwt.issuer.value, 'issuer')
 
+  def test_create_public_key_sign(self):
+    keyset_servicer = services.KeysetServicer()
+    jwt_servicer = jwt_service.JwtServicer()
+
+    template = jwt.jwt_es256_template().SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = jwt_servicer.CreateJwtPublicKeySign(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_public_key_sign_bad_keyset(self):
+    jwt_servicer = jwt_service.JwtServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = jwt_servicer.CreateJwtPublicKeySign(
+        creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
+  def test_create_public_key_verify(self):
+    keyset_servicer = services.KeysetServicer()
+    jwt_servicer = jwt_service.JwtServicer()
+
+    template = jwt.jwt_es256_template().SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+    pub_request = testing_api_pb2.KeysetPublicRequest(
+        private_keyset=gen_response.keyset)
+    pub_response = keyset_servicer.Public(pub_request, self._ctx)
+    self.assertEqual(pub_response.WhichOneof('result'), 'public_keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=pub_response.public_keyset))
+    creation_response = jwt_servicer.CreateJwtPublicKeyVerify(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_public_key_verify_bad_keyset(self):
+    jwt_servicer = jwt_service.JwtServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = jwt_servicer.CreateJwtPublicKeyVerify(
+        creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_generate_sign_export_import_verify_signature(self):
     keyset_servicer = services.KeysetServicer()
     jwt_servicer = jwt_service.JwtServicer()
@@ -153,7 +242,9 @@
     self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
     private_keyset = gen_response.keyset
 
-    comp_request = testing_api_pb2.JwtSignRequest(keyset=private_keyset)
+    comp_request = testing_api_pb2.JwtSignRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=private_keyset))
     comp_request.raw_jwt.issuer.value = 'issuer'
     comp_request.raw_jwt.subject.value = 'subject'
     comp_request.raw_jwt.custom_claims['myclaim'].bool_value = True
@@ -180,7 +271,8 @@
     self.assertEqual(from_jwkset_response.WhichOneof('result'), 'keyset')
 
     verify_request = testing_api_pb2.JwtVerifyRequest(
-        keyset=from_jwkset_response.keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=from_jwkset_response.keyset),
         signed_compact_jwt=signed_compact_jwt)
     verify_request.validator.expected_issuer.value = 'issuer'
     verify_request.validator.allow_missing_expiration = True
diff --git a/testing/python/proto/testing_api.proto b/testing/python/proto/testing_api.proto
deleted file mode 100644
index 2fb0e9e..0000000
--- a/testing/python/proto/testing_api.proto
+++ /dev/null
@@ -1,503 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-syntax = "proto3";
-
-package tink_testing_api;
-
-import "google/protobuf/duration.proto";
-import "google/protobuf/timestamp.proto";
-import "google/protobuf/wrappers.proto";
-
-option java_package = "com.google.crypto.tink.testing.proto";
-option java_multiple_files = true;
-option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
-// Placeholder for java_stubby_library
-
-// Service providing metadata about the server.
-service Metadata {
-  // Returns some server information. A test may use this information to verify
-  // that it is talking to the right server.
-  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
-}
-
-message ServerInfoRequest {}
-
-message ServerInfoResponse {
-  string tink_version = 1;  // For example '1.4'
-  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
-}
-
-// Service for Keyset operations.
-service Keyset {
-  // Generates a key template from a key template name.
-  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
-  // Generates a new keyset from a template.
-  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
-  // Generates a public-key keyset from a private-key keyset.
-  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
-  // Converts a Keyset from Binary to Json Format
-  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
-  // Converts a Keyset from Json to Binary Format
-  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
-  // Reads an encrypted keyset using KeysetHandle.read() or
-  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
-  rpc ReadEncrypted(KeysetReadEncryptedRequest)
-      returns (KeysetReadEncryptedResponse) {}
-  // Writes an encrypted keyset using KeysetHandle.write() or
-  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
-  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
-      returns (KeysetWriteEncryptedResponse) {}
-}
-
-message KeysetTemplateRequest {
-  string template_name = 1;  // template name used by Tinkey
-}
-
-message KeysetTemplateResponse {
-  oneof result {
-    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
-    string err = 2;
-  }
-}
-
-message KeysetGenerateRequest {
-  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
-}
-
-message KeysetGenerateResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetPublicRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetPublicResponse {
-  oneof result {
-    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-message KeysetToJsonRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message KeysetToJsonResponse {
-  oneof result {
-    string json_keyset = 1;
-    string err = 2;
-  }
-}
-
-message KeysetFromJsonRequest {
-  string json_keyset = 1;
-}
-
-message KeysetFromJsonResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-// Copy of google.protobuf.BytesValue
-message BytesValue {
-  // The bytes value.
-  bytes value = 1;
-}
-
-enum KeysetReaderType {
-  KEYSET_READER_UNKNOWN = 0;
-  KEYSET_READER_BINARY = 1;
-  KEYSET_READER_JSON = 2;
-}
-
-message KeysetReadEncryptedRequest {
-  bytes encrypted_keyset = 1;
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetReaderType keyset_reader_type = 4;
-}
-
-message KeysetReadEncryptedResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
-
-enum KeysetWriterType {
-  KEYSET_WRITER_UNKNOWN = 0;
-  KEYSET_WRITER_BINARY = 1;
-  KEYSET_WRITER_JSON = 2;
-}
-
-message KeysetWriteEncryptedRequest {
-  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
-  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
-  BytesValue associated_data = 3;
-  KeysetWriterType keyset_writer_type = 4;
-}
-
-message KeysetWriteEncryptedResponse {
-  oneof result {
-    bytes encrypted_keyset = 1;
-    string err = 2;
-  }
-}
-
-// Service for AEAD encryption and decryption
-service Aead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
-}
-
-message AeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message AeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message AeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Deterministic AEAD encryption and decryption
-service DeterministicAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
-      returns (DeterministicAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
-      returns (DeterministicAeadDecryptResponse) {}
-}
-
-message DeterministicAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message DeterministicAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message DeterministicAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service for Streaming AEAD encryption and decryption
-service StreamingAead {
-  // Encrypts a plaintext with the provided keyset
-  rpc Encrypt(StreamingAeadEncryptRequest)
-      returns (StreamingAeadEncryptResponse) {}
-  // Decrypts a ciphertext with the provided keyset
-  rpc Decrypt(StreamingAeadDecryptRequest)
-      returns (StreamingAeadDecryptResponse) {}
-}
-
-message StreamingAeadEncryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message StreamingAeadDecryptRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes associated_data = 3;
-}
-
-message StreamingAeadDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to compute and verify MACs
-service Mac {
-  // Computes a MAC for given data
-  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
-  // Verifies the validity of the MAC value, no error means success
-  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
-}
-
-message ComputeMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message ComputeMacResponse {
-  oneof result {
-    bytes mac_value = 1;
-    string err = 2;
-  }
-}
-
-message VerifyMacRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes mac_value = 2;
-  bytes data = 3;
-}
-
-message VerifyMacResponse {
-  string err = 1;
-}
-
-// Service to hybrid encrypt and decrypt
-service Hybrid {
-  // Encrypts plaintext binding context_info to the resulting ciphertext
-  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
-  // Decrypts ciphertext verifying the integrity of context_info
-  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
-}
-
-message HybridEncryptRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes plaintext = 2;
-  bytes context_info = 3;
-}
-
-message HybridEncryptResponse {
-  oneof result {
-    bytes ciphertext = 1;
-    string err = 2;
-  }
-}
-
-message HybridDecryptRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes ciphertext = 2;
-  bytes context_info = 3;
-}
-
-message HybridDecryptResponse {
-  oneof result {
-    bytes plaintext = 1;
-    string err = 2;
-  }
-}
-
-// Service to sign and verify signatures.
-service Signature {
-  // Computes the signature for data
-  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
-  // Verifies that signature is a digital signature for data
-  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
-}
-
-message SignatureSignRequest {
-  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes data = 2;
-}
-
-message SignatureSignResponse {
-  oneof result {
-    bytes signature = 1;
-    string err = 2;
-  }
-}
-
-message SignatureVerifyRequest {
-  bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
-  bytes signature = 2;
-  bytes data = 3;
-}
-
-message SignatureVerifyResponse {
-  string err = 1;
-}
-
-// Service for PrfSet computation
-service PrfSet {
-  // Returns the key ids and the primary key id in the keyset.
-  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
-  // Computes the output of the PRF with the given key_id in the PrfSet.
-  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
-}
-
-message PrfSetKeyIdsRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message PrfSetKeyIdsResponse {
-  message Output {
-    uint32 primary_key_id = 1;
-    repeated uint32 key_id = 2;
-  }
-  oneof result {
-    Output output = 1;
-    string err = 2;
-  }
-}
-
-message PrfSetComputeRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-  uint32 key_id = 2;
-  bytes input_data = 3;
-  int32 output_length = 4;
-}
-
-message PrfSetComputeResponse {
-  oneof result {
-    bytes output = 1;
-    string err = 2;
-  }
-}
-
-// Service for JSON Web Tokens (JWT)
-service Jwt {
-  // Computes a signed compact JWT token.
-  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Computes a signed compact JWT token.
-  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
-  // Verifies the validity of the signed compact JWT token
-  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
-  // Converts a Keyset from Tink Binary to JWK Set Format
-  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
-  // Converts a Keyset from JWK Set to Tink Binary Format
-  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
-}
-
-//  Used to represent the JSON null value.
-enum NullValue {
-  NULL_VALUE = 0;
-}
-
-message JwtClaimValue {
-  oneof kind {
-    NullValue null_value = 2;
-    double number_value = 3;
-    string string_value = 4;
-    bool bool_value = 5;
-    string json_object_value = 6;
-    string json_array_value = 7;
-  }
-}
-
-message JwtToken {
-  google.protobuf.StringValue issuer = 1;
-  google.protobuf.StringValue subject = 2;
-  repeated string audiences = 3;
-  google.protobuf.StringValue jwt_id = 4;
-  google.protobuf.Timestamp expiration = 5;
-  google.protobuf.Timestamp not_before = 6;
-  google.protobuf.Timestamp issued_at = 7;
-  map<string, JwtClaimValue> custom_claims = 8;
-  google.protobuf.StringValue type_header = 9;
-}
-
-message JwtValidator {
-  google.protobuf.StringValue expected_type_header = 7;
-  google.protobuf.StringValue expected_issuer = 1;
-  google.protobuf.StringValue expected_audience = 3;
-  bool ignore_type_header = 8;
-  bool ignore_issuer = 9;
-  bool ignore_audience = 11;
-  bool allow_missing_expiration = 12;
-  bool expect_issued_in_the_past = 13;
-  google.protobuf.Timestamp now = 5;
-  google.protobuf.Duration clock_skew = 6;
-}
-
-message JwtSignRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  JwtToken raw_jwt = 2;
-}
-
-message JwtSignResponse {
-  oneof result {
-    string signed_compact_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtVerifyRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset
-  string signed_compact_jwt = 2;
-  JwtValidator validator = 3;
-}
-
-message JwtVerifyResponse {
-  oneof result {
-    JwtToken verified_jwt = 1;
-    string err = 2;
-  }
-}
-
-message JwtToJwkSetRequest {
-  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-}
-
-message JwtToJwkSetResponse {
-  oneof result {
-    string jwk_set = 1;
-    string err = 2;
-  }
-}
-
-message JwtFromJwkSetRequest {
-  string jwk_set = 1;
-}
-
-message JwtFromJwkSetResponse {
-  oneof result {
-    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
-    string err = 2;
-  }
-}
diff --git a/testing/python/proto/BUILD.bazel b/testing/python/protos/BUILD.bazel
similarity index 100%
rename from testing/python/proto/BUILD.bazel
rename to testing/python/protos/BUILD.bazel
diff --git a/testing/python/protos/testing_api.proto b/testing/python/protos/testing_api.proto
new file mode 100644
index 0000000..7a02517
--- /dev/null
+++ b/testing/python/protos/testing_api.proto
@@ -0,0 +1,566 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+syntax = "proto3";
+
+package tink_testing_api;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+option java_package = "com.google.crypto.tink.testing.proto";
+option java_multiple_files = true;
+option go_package = "github.com/google/tink/testing/go/proto/testing_api_go_proto";
+// Placeholder for java_stubby_library
+
+// Service providing metadata about the server.
+service Metadata {
+  // Returns some server information. A test may use this information to verify
+  // that it is talking to the right server.
+  rpc GetServerInfo(ServerInfoRequest) returns (ServerInfoResponse) {}
+}
+
+message ServerInfoRequest {}
+
+message ServerInfoResponse {
+  string tink_version = 1;  // For example '1.4'
+  string language = 2;      // For example 'cc', 'java', 'go' or 'python'.
+}
+
+// Service for Keyset operations.
+service Keyset {
+  // Generates a key template from a key template name.
+  rpc GetTemplate(KeysetTemplateRequest) returns (KeysetTemplateResponse) {}
+  // Generates a new keyset from a template.
+  rpc Generate(KeysetGenerateRequest) returns (KeysetGenerateResponse) {}
+  // Generates a public-key keyset from a private-key keyset.
+  rpc Public(KeysetPublicRequest) returns (KeysetPublicResponse) {}
+  // Converts a Keyset from Binary to Json Format
+  rpc ToJson(KeysetToJsonRequest) returns (KeysetToJsonResponse) {}
+  // Converts a Keyset from Json to Binary Format
+  rpc FromJson(KeysetFromJsonRequest) returns (KeysetFromJsonResponse) {}
+  // Reads an encrypted keyset using KeysetHandle.read() or
+  // KeysetHandle.readWithAssociatedData() and the BinaryKeysetReader.
+  rpc ReadEncrypted(KeysetReadEncryptedRequest)
+      returns (KeysetReadEncryptedResponse) {}
+  // Writes an encrypted keyset using KeysetHandle.write() or
+  // KeysetHandle.writeWithAssociatedData() and the BinaryKeysetWriter.
+  rpc WriteEncrypted(KeysetWriteEncryptedRequest)
+      returns (KeysetWriteEncryptedResponse) {}
+}
+
+message KeysetTemplateRequest {
+  string template_name = 1;  // template name used by Tinkey
+}
+
+message KeysetTemplateResponse {
+  oneof result {
+    bytes key_template = 1;  // serialized google.crypto.tink.KeyTemplate.
+    string err = 2;
+  }
+}
+
+message KeysetGenerateRequest {
+  bytes template = 1;  // serialized google.crypto.tink.KeyTemplate.
+}
+
+message KeysetGenerateResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetPublicRequest {
+  bytes private_keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetPublicResponse {
+  oneof result {
+    bytes public_keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+message KeysetToJsonRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message KeysetToJsonResponse {
+  oneof result {
+    string json_keyset = 1;
+    string err = 2;
+  }
+}
+
+message KeysetFromJsonRequest {
+  string json_keyset = 1;
+}
+
+message KeysetFromJsonResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+// Copy of google.protobuf.BytesValue
+message BytesValue {
+  // The bytes value.
+  bytes value = 1;
+}
+
+enum KeysetReaderType {
+  KEYSET_READER_UNKNOWN = 0;
+  KEYSET_READER_BINARY = 1;
+  KEYSET_READER_JSON = 2;
+}
+
+message KeysetReadEncryptedRequest {
+  bytes encrypted_keyset = 1;
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetReaderType keyset_reader_type = 4;
+}
+
+message KeysetReadEncryptedResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
+
+enum KeysetWriterType {
+  KEYSET_WRITER_UNKNOWN = 0;
+  KEYSET_WRITER_BINARY = 1;
+  KEYSET_WRITER_JSON = 2;
+}
+
+message KeysetWriteEncryptedRequest {
+  bytes keyset = 1;         // serialized google.crypto.tink.Keyset.
+  bytes master_keyset = 2;  // serialized google.crypto.tink.Keyset.
+  BytesValue associated_data = 3;
+  KeysetWriterType keyset_writer_type = 4;
+}
+
+message KeysetWriteEncryptedResponse {
+  oneof result {
+    bytes encrypted_keyset = 1;
+    string err = 2;
+  }
+}
+
+message AnnotatedKeyset {
+  bytes serialized_keyset = 1;  // serialized google.crypto.tink.Keyset.
+  map<string, string> annotations = 2;
+}
+
+message CreationRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message CreationResponse {
+  // Empty means no error
+  string err = 1;
+}
+
+// Service for AEAD encryption and decryption
+service Aead {
+  // Creates an Aead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(AeadEncryptRequest) returns (AeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(AeadDecryptRequest) returns (AeadDecryptResponse) {}
+}
+
+message AeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message AeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message AeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Deterministic AEAD encryption and decryption
+service DeterministicAead {
+  // Creates a Deterministic AEAD object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc EncryptDeterministically(DeterministicAeadEncryptRequest)
+      returns (DeterministicAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling
+  // this.
+  rpc DecryptDeterministically(DeterministicAeadDecryptRequest)
+      returns (DeterministicAeadDecryptResponse) {}
+}
+
+message DeterministicAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message DeterministicAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message DeterministicAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service for Streaming AEAD encryption and decryption
+service StreamingAead {
+  // Creates a StreamingAead object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts a plaintext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Encrypt(StreamingAeadEncryptRequest)
+      returns (StreamingAeadEncryptResponse) {}
+  // Decrypts a ciphertext with the provided keyset. The client must call
+  // "Create" first to see if creation succeeds before calling this.
+  rpc Decrypt(StreamingAeadDecryptRequest)
+      returns (StreamingAeadDecryptResponse) {}
+}
+
+message StreamingAeadEncryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message StreamingAeadDecryptRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes associated_data = 3;
+}
+
+message StreamingAeadDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to compute and verify MACs
+service Mac {
+  // Creates a Mac object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+  // Computes a MAC for given data. The client must call "Create" first to see
+  // if creation succeeds before calling this.
+  rpc ComputeMac(ComputeMacRequest) returns (ComputeMacResponse) {}
+  // Verifies the validity of the MAC value, no error means success. The client
+  // must call "Create" first to see if creation succeeds before calling this.
+  rpc VerifyMac(VerifyMacRequest) returns (VerifyMacResponse) {}
+}
+
+message ComputeMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message ComputeMacResponse {
+  oneof result {
+    bytes mac_value = 1;
+    string err = 2;
+  }
+}
+
+message VerifyMacRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  bytes mac_value = 2;
+  bytes data = 3;
+}
+
+message VerifyMacResponse {
+  string err = 1;
+}
+
+// Service to hybrid encrypt and decrypt
+service Hybrid {
+  // Creates a HybridEncrypt object without using it.
+  rpc CreateHybridEncrypt(CreationRequest) returns (CreationResponse) {}
+  // Creates a HybridDecrypt object without using it.
+  rpc CreateHybridDecrypt(CreationRequest) returns (CreationResponse) {}
+
+  // Encrypts plaintext binding context_info to the resulting ciphertext. The
+  // client must call "CreateHybridEncrypt" first to see if creation succeeds
+  // before calling this.
+  rpc Encrypt(HybridEncryptRequest) returns (HybridEncryptResponse) {}
+  // Decrypts ciphertext verifying the integrity of context_info. The client
+  // must call "CreateHybridDecrypt" first to see if creation succeeds before
+  // calling this.
+  rpc Decrypt(HybridDecryptRequest) returns (HybridDecryptResponse) {}
+}
+
+message HybridEncryptRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes plaintext = 2;
+  bytes context_info = 3;
+}
+
+message HybridEncryptResponse {
+  oneof result {
+    bytes ciphertext = 1;
+    string err = 2;
+  }
+}
+
+message HybridDecryptRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes ciphertext = 2;
+  bytes context_info = 3;
+}
+
+message HybridDecryptResponse {
+  oneof result {
+    bytes plaintext = 1;
+    string err = 2;
+  }
+}
+
+// Service to sign and verify signatures.
+service Signature {
+  // Creates a PublicKeySign object without using it.
+  rpc CreatePublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a PublicKeyVerify object without using it.
+  rpc CreatePublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes the signature for data. The client must call "CreatePublicKeySign"
+  // first to see if creation succeeds before calling this.
+  rpc Sign(SignatureSignRequest) returns (SignatureSignResponse) {}
+  // Verifies that signature is a digital signature for data. The client must
+  // call "CreatePublicKeyVerify" first to see if creation succeeds before
+  // calling this.
+  rpc Verify(SignatureVerifyRequest) returns (SignatureVerifyResponse) {}
+}
+
+message SignatureSignRequest {
+  AnnotatedKeyset private_annotated_keyset = 1;
+  bytes data = 2;
+}
+
+message SignatureSignResponse {
+  oneof result {
+    bytes signature = 1;
+    string err = 2;
+  }
+}
+
+message SignatureVerifyRequest {
+  AnnotatedKeyset public_annotated_keyset = 1;
+  bytes signature = 2;
+  bytes data = 3;
+}
+
+message SignatureVerifyResponse {
+  string err = 1;
+}
+
+// Service for PrfSet computation
+service PrfSet {
+  // Creates a PrfSet object without using it.
+  rpc Create(CreationRequest) returns (CreationResponse) {}
+
+  // Returns the key ids and the primary key id in the keyset.The client must
+  // call "Create" first to see if creation succeeds before calling this.
+  rpc KeyIds(PrfSetKeyIdsRequest) returns (PrfSetKeyIdsResponse) {}
+  // Computes the output of the PRF with the given key_id in the PrfSet.The
+  // client must call "Create" first to see if creation succeeds before calling
+  // this.
+  rpc Compute(PrfSetComputeRequest) returns (PrfSetComputeResponse) {}
+}
+
+message PrfSetKeyIdsRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+}
+
+message PrfSetKeyIdsResponse {
+  message Output {
+    uint32 primary_key_id = 1;
+    repeated uint32 key_id = 2;
+  }
+  oneof result {
+    Output output = 1;
+    string err = 2;
+  }
+}
+
+message PrfSetComputeRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  uint32 key_id = 2;
+  bytes input_data = 3;
+  int32 output_length = 4;
+}
+
+message PrfSetComputeResponse {
+  oneof result {
+    bytes output = 1;
+    string err = 2;
+  }
+}
+
+// Service for JSON Web Tokens (JWT)
+service Jwt {
+  // Creates a JwtMac object without using it.
+  rpc CreateJwtMac(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeySign object without using it.
+  rpc CreateJwtPublicKeySign(CreationRequest) returns (CreationResponse) {}
+  // Creates a JwtPublicKeyVerify object without using it.
+  rpc CreateJwtPublicKeyVerify(CreationRequest) returns (CreationResponse) {}
+
+  // Computes a signed compact JWT token.
+  rpc ComputeMacAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc VerifyMacAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Computes a signed compact JWT token.
+  rpc PublicKeySignAndEncode(JwtSignRequest) returns (JwtSignResponse) {}
+  // Verifies the validity of the signed compact JWT token
+  rpc PublicKeyVerifyAndDecode(JwtVerifyRequest) returns (JwtVerifyResponse) {}
+  // Converts a Keyset from Tink Binary to JWK Set Format
+  rpc ToJwkSet(JwtToJwkSetRequest) returns (JwtToJwkSetResponse) {}
+  // Converts a Keyset from JWK Set to Tink Binary Format
+  rpc FromJwkSet(JwtFromJwkSetRequest) returns (JwtFromJwkSetResponse) {}
+}
+
+//  Used to represent the JSON null value.
+enum NullValue {
+  NULL_VALUE = 0;
+}
+
+message JwtClaimValue {
+  oneof kind {
+    NullValue null_value = 2;
+    double number_value = 3;
+    string string_value = 4;
+    bool bool_value = 5;
+    string json_object_value = 6;
+    string json_array_value = 7;
+  }
+}
+
+message JwtToken {
+  google.protobuf.StringValue issuer = 1;
+  google.protobuf.StringValue subject = 2;
+  repeated string audiences = 3;
+  google.protobuf.StringValue jwt_id = 4;
+  google.protobuf.Timestamp expiration = 5;
+  google.protobuf.Timestamp not_before = 6;
+  google.protobuf.Timestamp issued_at = 7;
+  map<string, JwtClaimValue> custom_claims = 8;
+  google.protobuf.StringValue type_header = 9;
+}
+
+message JwtValidator {
+  google.protobuf.StringValue expected_type_header = 7;
+  google.protobuf.StringValue expected_issuer = 1;
+  google.protobuf.StringValue expected_audience = 3;
+  bool ignore_type_header = 8;
+  bool ignore_issuer = 9;
+  bool ignore_audience = 11;
+  bool allow_missing_expiration = 12;
+  bool expect_issued_in_the_past = 13;
+  google.protobuf.Timestamp now = 5;
+  google.protobuf.Duration clock_skew = 6;
+}
+
+message JwtSignRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  JwtToken raw_jwt = 2;
+}
+
+message JwtSignResponse {
+  oneof result {
+    string signed_compact_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtVerifyRequest {
+  AnnotatedKeyset annotated_keyset = 1;
+  string signed_compact_jwt = 2;
+  JwtValidator validator = 3;
+}
+
+message JwtVerifyResponse {
+  oneof result {
+    JwtToken verified_jwt = 1;
+    string err = 2;
+  }
+}
+
+message JwtToJwkSetRequest {
+  bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+}
+
+message JwtToJwkSetResponse {
+  oneof result {
+    string jwk_set = 1;
+    string err = 2;
+  }
+}
+
+message JwtFromJwkSetRequest {
+  string jwk_set = 1;
+}
+
+message JwtFromJwkSetResponse {
+  oneof result {
+    bytes keyset = 1;  // serialized google.crypto.tink.Keyset.
+    string err = 2;
+  }
+}
diff --git a/testing/python/services.py b/testing/python/services.py
index cb5081a..3f757e2 100644
--- a/testing/python/services.py
+++ b/testing/python/services.py
@@ -28,8 +28,8 @@
 from tink import streaming_aead
 from tink.proto import tink_pb2
 from tink.testing import bytes_io
-from proto import testing_api_pb2
-from proto import testing_api_pb2_grpc
+from protos import testing_api_pb2
+from protos import testing_api_pb2_grpc
 
 
 # All KeyTemplate (as Protobuf) defined in the Python API.
@@ -378,14 +378,25 @@
 class AeadServicer(testing_api_pb2_grpc.AeadServicer):
   """A service for testing AEAD encryption."""
 
+  def Create(self, request: testing_api_pb2.CreationRequest,
+             context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates an AEAD without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(aead.Aead)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def Encrypt(
       self, request: testing_api_pb2.AeadEncryptRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.AeadEncryptResponse:
     """Encrypts a message."""
+    keyset_handle = cleartext_keyset_handle.read(
+        tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+    p = keyset_handle.primitive(aead.Aead)
     try:
-      keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
-      p = keyset_handle.primitive(aead.Aead)
       ciphertext = p.encrypt(request.plaintext, request.associated_data)
       return testing_api_pb2.AeadEncryptResponse(ciphertext=ciphertext)
     except tink.TinkError as e:
@@ -395,10 +406,10 @@
       self, request: testing_api_pb2.AeadDecryptRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.AeadDecryptResponse:
     """Decrypts a message."""
+    keyset_handle = cleartext_keyset_handle.read(
+        tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+    p = keyset_handle.primitive(aead.Aead)
     try:
-      keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
-      p = keyset_handle.primitive(aead.Aead)
       plaintext = p.decrypt(request.ciphertext, request.associated_data)
       return testing_api_pb2.AeadDecryptResponse(plaintext=plaintext)
     except tink.TinkError as e:
@@ -408,6 +419,17 @@
 class StreamingAeadServicer(testing_api_pb2_grpc.StreamingAeadServicer):
   """A service for testing StreamingAEAD encryption."""
 
+  def Create(self, request: testing_api_pb2.CreationRequest,
+             context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a Streaming Aead without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(streaming_aead.StreamingAead)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def Encrypt(
       self, request: testing_api_pb2.StreamingAeadEncryptRequest,
       context: grpc.ServicerContext
@@ -415,7 +437,7 @@
     """Encrypts a message."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(streaming_aead.StreamingAead)
       ciphertext_destination = bytes_io.BytesIOWithValueAfterClose()
       with p.new_encrypting_stream(ciphertext_destination,
@@ -433,7 +455,7 @@
     """Decrypts a message."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(streaming_aead.StreamingAead)
       stream = io.BytesIO(request.ciphertext)
       with p.new_decrypting_stream(stream, request.associated_data) as s:
@@ -446,15 +468,26 @@
 class DeterministicAeadServicer(testing_api_pb2_grpc.DeterministicAeadServicer):
   """A service for testing Deterministic AEAD encryption."""
 
+  def Create(self, request: testing_api_pb2.CreationRequest,
+             context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a Deterministic AEAD without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(daead.DeterministicAead)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def EncryptDeterministically(
       self, request: testing_api_pb2.DeterministicAeadEncryptRequest,
       context: grpc.ServicerContext
   ) -> testing_api_pb2.DeterministicAeadEncryptResponse:
     """Encrypts a message."""
+    keyset_handle = cleartext_keyset_handle.read(
+        tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+    p = keyset_handle.primitive(daead.DeterministicAead)
     try:
-      keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
-      p = keyset_handle.primitive(daead.DeterministicAead)
       ciphertext = p.encrypt_deterministically(request.plaintext,
                                                request.associated_data)
       return testing_api_pb2.DeterministicAeadEncryptResponse(
@@ -467,10 +500,10 @@
       context: grpc.ServicerContext
   ) -> testing_api_pb2.DeterministicAeadDecryptResponse:
     """Decrypts a message."""
+    keyset_handle = cleartext_keyset_handle.read(
+        tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+    p = keyset_handle.primitive(daead.DeterministicAead)
     try:
-      keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
-      p = keyset_handle.primitive(daead.DeterministicAead)
       plaintext = p.decrypt_deterministically(request.ciphertext,
                                               request.associated_data)
       return testing_api_pb2.DeterministicAeadDecryptResponse(
@@ -482,13 +515,24 @@
 class MacServicer(testing_api_pb2_grpc.MacServicer):
   """A service for testing MACs."""
 
+  def Create(self, request: testing_api_pb2.CreationRequest,
+             context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a MAC without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(mac.Mac)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def ComputeMac(
       self, request: testing_api_pb2.ComputeMacRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.ComputeMacResponse:
     """Computes a MAC."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(mac.Mac)
       mac_value = p.compute_mac(request.data)
       return testing_api_pb2.ComputeMacResponse(mac_value=mac_value)
@@ -501,7 +545,7 @@
     """Verifies a MAC value."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(mac.Mac)
       p.verify_mac(request.mac_value, request.data)
       return testing_api_pb2.VerifyMacResponse()
@@ -512,13 +556,38 @@
 class HybridServicer(testing_api_pb2_grpc.HybridServicer):
   """A service for testing hybrid encryption and decryption."""
 
+  def CreateHybridEncrypt(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a HybridEncrypt without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(hybrid.HybridEncrypt)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
+  def CreateHybridDecrypt(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a HybridDecrypt without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(hybrid.HybridDecrypt)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def Encrypt(
       self, request: testing_api_pb2.HybridEncryptRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.HybridEncryptResponse:
     """Encrypts a message."""
     try:
       public_keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.public_keyset))
+          tink.BinaryKeysetReader(
+              request.public_annotated_keyset.serialized_keyset))
       p = public_keyset_handle.primitive(hybrid.HybridEncrypt)
       ciphertext = p.encrypt(request.plaintext, request.context_info)
       return testing_api_pb2.HybridEncryptResponse(ciphertext=ciphertext)
@@ -531,7 +600,8 @@
     """Decrypts a message."""
     try:
       private_keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.private_keyset))
+          tink.BinaryKeysetReader(
+              request.private_annotated_keyset.serialized_keyset))
       p = private_keyset_handle.primitive(hybrid.HybridDecrypt)
       plaintext = p.decrypt(request.ciphertext, request.context_info)
       return testing_api_pb2.HybridDecryptResponse(plaintext=plaintext)
@@ -542,13 +612,38 @@
 class SignatureServicer(testing_api_pb2_grpc.SignatureServicer):
   """A service for testing signatures."""
 
+  def CreatePublicKeySign(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a PublicKeySign without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(signature.PublicKeySign)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
+  def CreatePublicKeyVerify(
+      self, request: testing_api_pb2.CreationRequest,
+      context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a PublicKeyVerify without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(signature.PublicKeyVerify)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def Sign(
       self, request: testing_api_pb2.SignatureSignRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.SignatureSignResponse:
     """Signs a message."""
     try:
       private_keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.private_keyset))
+          tink.BinaryKeysetReader(
+              request.private_annotated_keyset.serialized_keyset))
       p = private_keyset_handle.primitive(signature.PublicKeySign)
       signature_value = p.sign(request.data)
       return testing_api_pb2.SignatureSignResponse(signature=signature_value)
@@ -561,7 +656,8 @@
     """Verifies a signature."""
     try:
       public_keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.public_keyset))
+          tink.BinaryKeysetReader(
+              request.public_annotated_keyset.serialized_keyset))
       p = public_keyset_handle.primitive(signature.PublicKeyVerify)
       p.verify(request.signature, request.data)
       return testing_api_pb2.SignatureVerifyResponse()
@@ -572,13 +668,24 @@
 class PrfSetServicer(testing_api_pb2_grpc.PrfSetServicer):
   """A service for testing PrfSet."""
 
+  def Create(self, request: testing_api_pb2.CreationRequest,
+             context: grpc.ServicerContext) -> testing_api_pb2.CreationResponse:
+    """Creates a PrfSet without using it."""
+    try:
+      keyset_handle = cleartext_keyset_handle.read(
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
+      keyset_handle.primitive(prf.PrfSet)
+      return testing_api_pb2.CreationResponse()
+    except tink.TinkError as e:
+      return testing_api_pb2.CreationResponse(err=str(e))
+
   def KeyIds(
       self, request: testing_api_pb2.PrfSetKeyIdsRequest,
       context: grpc.ServicerContext) -> testing_api_pb2.PrfSetKeyIdsResponse:
     """Returns all key IDs and the primary key ID."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       p = keyset_handle.primitive(prf.PrfSet)
       prfs = p.all()
       response = testing_api_pb2.PrfSetKeyIdsResponse()
@@ -594,7 +701,7 @@
     """Computes the output of one PRF."""
     try:
       keyset_handle = cleartext_keyset_handle.read(
-          tink.BinaryKeysetReader(request.keyset))
+          tink.BinaryKeysetReader(request.annotated_keyset.serialized_keyset))
       f = keyset_handle.primitive(prf.PrfSet).all()[request.key_id]
       return testing_api_pb2.PrfSetComputeResponse(
           output=f.compute(request.input_data, request.output_length))
diff --git a/testing/python/services_test.py b/testing/python/services_test.py
index 3cb2191..c15bb29 100644
--- a/testing/python/services_test.py
+++ b/testing/python/services_test.py
@@ -26,7 +26,7 @@
 from tink import streaming_aead
 
 
-from proto import testing_api_pb2
+from protos import testing_api_pb2
 import services
 
 
@@ -271,6 +271,52 @@
         read_encrypted_request, self._ctx)
     self.assertEqual(read_encrypted_response.WhichOneof('result'), 'err')
 
+  def test_create_aead(self):
+    keyset_servicer = services.KeysetServicer()
+    aead_servicer = services.AeadServicer()
+
+    template = aead.aead_key_templates.AES128_GCM.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = aead_servicer.Create(creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_aead_broken_keyset(self):
+    aead_servicer = services.AeadServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = aead_servicer.Create(creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
+  def test_encrypt_decrypt_wrong_keyset(self):
+    aead_servicer = services.AeadServicer()
+    keyset_servicer = services.KeysetServicer()
+    # HMAC keysets will not allow creation of an AEAD.
+    template = mac.mac_key_templates.HMAC_SHA256_128BITTAG.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+    keyset = gen_response.keyset
+
+    with self.assertRaises(tink.TinkError):
+      aead_servicer.Encrypt(
+          testing_api_pb2.AeadEncryptRequest(
+              annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                  serialized_keyset=keyset)), self._ctx)
+
+    with self.assertRaises(tink.TinkError):
+      aead_servicer.Decrypt(
+          testing_api_pb2.AeadDecryptRequest(
+              annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                  serialized_keyset=keyset)), self._ctx)
+
   def test_generate_encrypt_decrypt(self):
     keyset_servicer = services.KeysetServicer()
     aead_servicer = services.AeadServicer()
@@ -283,12 +329,18 @@
     plaintext = b'The quick brown fox jumps over the lazy dog'
     associated_data = b'associated_data'
     enc_request = testing_api_pb2.AeadEncryptRequest(
-        keyset=keyset, plaintext=plaintext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        plaintext=plaintext,
+        associated_data=associated_data)
     enc_response = aead_servicer.Encrypt(enc_request, self._ctx)
     self.assertEqual(enc_response.WhichOneof('result'), 'ciphertext')
     ciphertext = enc_response.ciphertext
     dec_request = testing_api_pb2.AeadDecryptRequest(
-        keyset=keyset, ciphertext=ciphertext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        ciphertext=ciphertext,
+        associated_data=associated_data)
     dec_response = aead_servicer.Decrypt(dec_request, self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'plaintext')
     self.assertEqual(dec_response.plaintext, plaintext)
@@ -306,7 +358,10 @@
     ciphertext = b'some invalid ciphertext'
     associated_data = b'associated_data'
     dec_request = testing_api_pb2.AeadDecryptRequest(
-        keyset=keyset, ciphertext=ciphertext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        ciphertext=ciphertext,
+        associated_data=associated_data)
     dec_response = aead_servicer.Decrypt(dec_request, self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'err')
     self.assertNotEmpty(dec_response.err)
@@ -317,6 +372,54 @@
     response = metadata_servicer.GetServerInfo(request, self._ctx)
     self.assertEqual(response.language, 'python')
 
+  def test_create_deterministic_aead(self):
+    keyset_servicer = services.KeysetServicer()
+    daead_servicer = services.DeterministicAeadServicer()
+
+    template_proto = daead.deterministic_aead_key_templates.AES256_SIV
+    template = template_proto.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = daead_servicer.Create(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_deterministic_aead_broken_keyset(self):
+    daead_servicer = services.DeterministicAeadServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = daead_servicer.Create(creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
+  def test_encrypt_decrypt_deterministic_aead_broken_keyset(self):
+    keyset_servicer = services.KeysetServicer()
+    daead_servicer = services.DeterministicAeadServicer()
+
+    # AES128_GCM keysets will not allow creation of an Deterministic AEAD.
+    template = aead.aead_key_templates.AES128_GCM.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+    keyset = gen_response.keyset
+
+    enc_request = testing_api_pb2.DeterministicAeadEncryptRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset))
+    with self.assertRaises(tink.TinkError):
+      daead_servicer.EncryptDeterministically(enc_request, self._ctx)
+    dec_request = testing_api_pb2.DeterministicAeadDecryptRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset))
+    with self.assertRaises(tink.TinkError):
+      daead_servicer.DecryptDeterministically(dec_request, self._ctx)
+
   def test_generate_encrypt_decrypt_deterministically(self):
     keyset_servicer = services.KeysetServicer()
     daead_servicer = services.DeterministicAeadServicer()
@@ -330,7 +433,10 @@
     plaintext = b'The quick brown fox jumps over the lazy dog'
     associated_data = b'associated_data'
     enc_request = testing_api_pb2.DeterministicAeadEncryptRequest(
-        keyset=keyset, plaintext=plaintext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        plaintext=plaintext,
+        associated_data=associated_data)
     enc_response = daead_servicer.EncryptDeterministically(enc_request,
                                                            self._ctx)
     self.assertEqual(enc_response.WhichOneof('result'), 'ciphertext')
@@ -340,7 +446,10 @@
     self.assertEqual(enc_response2.ciphertext, enc_response.ciphertext)
     ciphertext = enc_response.ciphertext
     dec_request = testing_api_pb2.DeterministicAeadDecryptRequest(
-        keyset=keyset, ciphertext=ciphertext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        ciphertext=ciphertext,
+        associated_data=associated_data)
     dec_response = daead_servicer.DecryptDeterministically(dec_request,
                                                            self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'plaintext')
@@ -360,12 +469,40 @@
     ciphertext = b'some invalid ciphertext'
     associated_data = b'associated_data'
     dec_request = testing_api_pb2.DeterministicAeadDecryptRequest(
-        keyset=keyset, ciphertext=ciphertext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        ciphertext=ciphertext,
+        associated_data=associated_data)
     dec_response = daead_servicer.DecryptDeterministically(dec_request,
                                                            self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'err')
     self.assertNotEmpty(dec_response.err)
 
+  def test_create_mac(self):
+    keyset_servicer = services.KeysetServicer()
+    mac_servicer = services.MacServicer()
+
+    template = mac.mac_key_templates.HMAC_SHA256_128BITTAG.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = mac_servicer.Create(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_mac_broken_keyset(self):
+    mac_servicer = services.MacServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = mac_servicer.Create(creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_generate_compute_verify_mac(self):
     keyset_servicer = services.KeysetServicer()
     mac_servicer = services.MacServicer()
@@ -376,12 +513,18 @@
     self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
     keyset = gen_response.keyset
     data = b'The quick brown fox jumps over the lazy dog'
-    comp_request = testing_api_pb2.ComputeMacRequest(keyset=keyset, data=data)
+    comp_request = testing_api_pb2.ComputeMacRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        data=data)
     comp_response = mac_servicer.ComputeMac(comp_request, self._ctx)
     self.assertEqual(comp_response.WhichOneof('result'), 'mac_value')
     mac_value = comp_response.mac_value
     verify_request = testing_api_pb2.VerifyMacRequest(
-        keyset=keyset, mac_value=mac_value, data=data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        mac_value=mac_value,
+        data=data)
     verify_response = mac_servicer.VerifyMac(verify_request, self._ctx)
     self.assertEmpty(verify_response.err)
 
@@ -396,10 +539,71 @@
     keyset = gen_response.keyset
 
     verify_request = testing_api_pb2.VerifyMacRequest(
-        keyset=keyset, mac_value=b'invalid mac_value', data=b'data')
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        mac_value=b'invalid mac_value',
+        data=b'data')
     verify_response = mac_servicer.VerifyMac(verify_request, self._ctx)
     self.assertNotEmpty(verify_response.err)
 
+  def test_create_hybrid_decrypt(self):
+    keyset_servicer = services.KeysetServicer()
+    hybrid_servicer = services.HybridServicer()
+
+    tp = hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM
+    template = tp.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = hybrid_servicer.CreateHybridDecrypt(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_hybrid_decrypt_bad_keyset(self):
+    hybrid_servicer = services.HybridServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = hybrid_servicer.CreateHybridDecrypt(
+        creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
+  def test_create_hybrid_encrypt(self):
+    keyset_servicer = services.KeysetServicer()
+    hybrid_servicer = services.HybridServicer()
+
+    tp = hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM
+    template = tp.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+    pub_request = testing_api_pb2.KeysetPublicRequest(
+        private_keyset=gen_response.keyset)
+    pub_response = keyset_servicer.Public(pub_request, self._ctx)
+    self.assertEqual(pub_response.WhichOneof('result'), 'public_keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=pub_response.public_keyset))
+    creation_response = hybrid_servicer.CreateHybridEncrypt(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_hybrid_encrypt_bad_keyset(self):
+    hybrid_servicer = services.HybridServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = hybrid_servicer.CreateHybridEncrypt(
+        creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_generate_hybrid_encrypt_decrypt(self):
     keyset_servicer = services.KeysetServicer()
     hybrid_servicer = services.HybridServicer()
@@ -420,7 +624,8 @@
     plaintext = b'The quick brown fox jumps over the lazy dog'
     context_info = b'context_info'
     enc_request = testing_api_pb2.HybridEncryptRequest(
-        public_keyset=public_keyset,
+        public_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=public_keyset),
         plaintext=plaintext,
         context_info=context_info)
     enc_response = hybrid_servicer.Encrypt(enc_request, self._ctx)
@@ -428,7 +633,8 @@
     ciphertext = enc_response.ciphertext
 
     dec_request = testing_api_pb2.HybridDecryptRequest(
-        private_keyset=private_keyset,
+        private_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=private_keyset),
         ciphertext=ciphertext,
         context_info=context_info)
     dec_response = hybrid_servicer.Decrypt(dec_request, self._ctx)
@@ -447,13 +653,70 @@
     private_keyset = gen_response.keyset
 
     dec_request = testing_api_pb2.HybridDecryptRequest(
-        private_keyset=private_keyset,
+        private_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=private_keyset),
         ciphertext=b'invalid ciphertext',
         context_info=b'context_info')
     dec_response = hybrid_servicer.Decrypt(dec_request, self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'err')
     self.assertNotEmpty(dec_response.err)
 
+  def test_create_public_key_sign(self):
+    keyset_servicer = services.KeysetServicer()
+    signature_servicer = services.SignatureServicer()
+
+    template = signature.signature_key_templates.ECDSA_P256.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = signature_servicer.CreatePublicKeySign(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_public_key_sign_bad_keyset(self):
+    signature_servicer = services.SignatureServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = signature_servicer.CreatePublicKeySign(
+        creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
+  def test_create_public_key_verify(self):
+    keyset_servicer = services.KeysetServicer()
+    signature_servicer = services.SignatureServicer()
+
+    template = signature.signature_key_templates.ECDSA_P256.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+    pub_request = testing_api_pb2.KeysetPublicRequest(
+        private_keyset=gen_response.keyset)
+    pub_response = keyset_servicer.Public(pub_request, self._ctx)
+    self.assertEqual(pub_response.WhichOneof('result'), 'public_keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=pub_response.public_keyset))
+    creation_response = signature_servicer.CreatePublicKeyVerify(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_public_key_verify_bad_keyset(self):
+    signature_servicer = services.SignatureServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = signature_servicer.CreatePublicKeyVerify(
+        creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_sign_verify(self):
     keyset_servicer = services.KeysetServicer()
     signature_servicer = services.SignatureServicer()
@@ -474,14 +737,16 @@
     data = b'The quick brown fox jumps over the lazy dog'
 
     sign_request = testing_api_pb2.SignatureSignRequest(
-        private_keyset=private_keyset,
+        private_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=private_keyset),
         data=data)
     sign_response = signature_servicer.Sign(sign_request, self._ctx)
     self.assertEqual(sign_response.WhichOneof('result'), 'signature')
     a_signature = sign_response.signature
 
     verify_request = testing_api_pb2.SignatureVerifyRequest(
-        public_keyset=public_keyset,
+        public_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=public_keyset),
         signature=a_signature,
         data=data)
     verify_response = signature_servicer.Verify(verify_request, self._ctx)
@@ -505,12 +770,37 @@
     public_keyset = pub_response.public_keyset
 
     invalid_request = testing_api_pb2.SignatureVerifyRequest(
-        public_keyset=public_keyset,
+        public_annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=public_keyset),
         signature=b'invalid signature',
         data=b'The quick brown fox jumps over the lazy dog')
     invalid_response = signature_servicer.Verify(invalid_request, self._ctx)
     self.assertNotEmpty(invalid_response.err)
 
+  def test_create_prf_set(self):
+    keyset_servicer = services.KeysetServicer()
+    prf_set_servicer = services.PrfSetServicer()
+
+    template = prf.prf_key_templates.HMAC_SHA256.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = prf_set_servicer.Create(creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_prf_set_wrong_keyset(self):
+    prf_set_servicer = services.PrfSetServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = prf_set_servicer.Create(creation_request, self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_compute_prf(self):
     keyset_servicer = services.KeysetServicer()
     prf_set_servicer = services.PrfSetServicer()
@@ -520,7 +810,9 @@
     self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
     keyset = gen_response.keyset
 
-    key_ids_request = testing_api_pb2.PrfSetKeyIdsRequest(keyset=keyset)
+    key_ids_request = testing_api_pb2.PrfSetKeyIdsRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset))
     key_ids_response = prf_set_servicer.KeyIds(key_ids_request, self._ctx)
     self.assertEqual(key_ids_response.WhichOneof('result'), 'output')
     self.assertLen(key_ids_response.output.key_id, 1)
@@ -529,7 +821,8 @@
 
     output_length = 31
     compute_request = testing_api_pb2.PrfSetComputeRequest(
-        keyset=keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
         key_id=key_ids_response.output.primary_key_id,
         input_data=b'input_data',
         output_length=output_length)
@@ -540,7 +833,9 @@
   def test_key_ids_prf_fail(self):
     prf_set_servicer = services.PrfSetServicer()
     invalid_key_ids_response = prf_set_servicer.KeyIds(
-        testing_api_pb2.PrfSetKeyIdsRequest(keyset=b'badkeyset'), self._ctx)
+        testing_api_pb2.PrfSetKeyIdsRequest(
+            annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+                serialized_keyset=b'badkeyset')), self._ctx)
     self.assertNotEmpty(invalid_key_ids_response.err)
 
   def test_compute_prf_fail(self):
@@ -551,14 +846,17 @@
     gen_response = keyset_servicer.Generate(gen_request, self._ctx)
     self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
     keyset = gen_response.keyset
-    key_ids_request = testing_api_pb2.PrfSetKeyIdsRequest(keyset=keyset)
+    key_ids_request = testing_api_pb2.PrfSetKeyIdsRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset))
     key_ids_response = prf_set_servicer.KeyIds(key_ids_request, self._ctx)
     self.assertEqual(key_ids_response.WhichOneof('result'), 'output')
     primary_key_id = key_ids_response.output.primary_key_id
 
     invalid_output_length = 123456
     invalid_compute_request = testing_api_pb2.PrfSetComputeRequest(
-        keyset=keyset,
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
         key_id=primary_key_id,
         input_data=b'input_data',
         output_length=invalid_output_length)
@@ -567,6 +865,33 @@
     self.assertEqual(invalid_compute_response.WhichOneof('result'), 'err')
     self.assertNotEmpty(invalid_compute_response.err)
 
+  def test_create_streaming_aead(self):
+    keyset_servicer = services.KeysetServicer()
+    streaming_aead_servicer = services.StreamingAeadServicer()
+
+    templates = streaming_aead.streaming_aead_key_templates
+    template = templates.AES128_CTR_HMAC_SHA256_4KB.SerializeToString()
+    gen_request = testing_api_pb2.KeysetGenerateRequest(template=template)
+    gen_response = keyset_servicer.Generate(gen_request, self._ctx)
+    self.assertEqual(gen_response.WhichOneof('result'), 'keyset')
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=gen_response.keyset))
+    creation_response = streaming_aead_servicer.Create(
+        creation_request, self._ctx)
+    self.assertEmpty(creation_response.err)
+
+  def test_create_streaming_aead_broken_keyset(self):
+    streaming_aead_servicer = services.StreamingAeadServicer()
+
+    creation_request = testing_api_pb2.CreationRequest(
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=b'\x80'))
+    creation_response = streaming_aead_servicer.Create(creation_request,
+                                                       self._ctx)
+    self.assertNotEmpty(creation_response.err)
+
   def test_generate_streaming_encrypt_decrypt(self):
     keyset_servicer = services.KeysetServicer()
     streaming_aead_servicer = services.StreamingAeadServicer()
@@ -581,13 +906,19 @@
     associated_data = b'associated_data'
 
     enc_request = testing_api_pb2.StreamingAeadEncryptRequest(
-        keyset=keyset, plaintext=plaintext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        plaintext=plaintext,
+        associated_data=associated_data)
     enc_response = streaming_aead_servicer.Encrypt(enc_request, self._ctx)
     self.assertEqual(enc_response.WhichOneof('result'), 'ciphertext')
     ciphertext = enc_response.ciphertext
 
     dec_request = testing_api_pb2.StreamingAeadDecryptRequest(
-        keyset=keyset, ciphertext=ciphertext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        ciphertext=ciphertext,
+        associated_data=associated_data)
     dec_response = streaming_aead_servicer.Decrypt(dec_request, self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'plaintext')
 
@@ -607,7 +938,10 @@
     ciphertext = b'some invalid ciphertext'
     associated_data = b'associated_data'
     dec_request = testing_api_pb2.StreamingAeadDecryptRequest(
-        keyset=keyset, ciphertext=ciphertext, associated_data=associated_data)
+        annotated_keyset=testing_api_pb2.AnnotatedKeyset(
+            serialized_keyset=keyset),
+        ciphertext=ciphertext,
+        associated_data=associated_data)
     dec_response = streaming_aead_servicer.Decrypt(dec_request, self._ctx)
     self.assertEqual(dec_response.WhichOneof('result'), 'err')
     self.assertNotEmpty(dec_response.err)
diff --git a/testing/python/testing_server.py b/testing/python/testing_server.py
index 66de125..3b44ca6 100644
--- a/testing/python/testing_server.py
+++ b/testing/python/testing_server.py
@@ -14,6 +14,7 @@
 """Tink Primitive Testing Service in Python."""
 
 from concurrent import futures
+import sys
 
 from absl import app
 from absl import flags
@@ -26,20 +27,33 @@
 from tink import prf
 from tink import signature
 from tink import streaming_aead
+from tink.integration import gcpkms
 
 from tink.testing import fake_kms
-
-from proto import testing_api_pb2_grpc
-
+from protos import testing_api_pb2_grpc
 import jwt_service
 import services
 
+from tink.integration import awskms
+
 FLAGS = flags.FLAGS
 
 flags.DEFINE_integer('port', 10000, 'The port of the server.')
+GCP_CREDENTIALS_PATH = flags.DEFINE_string(
+    'gcp_credentials_path', '', 'Google Cloud KMS credentials path.')
+GCP_KEY_URI = flags.DEFINE_string(
+    'gcp_key_uri', '', 'Google Cloud KMS key URL of the form: '
+    'gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*.')
+AWS_CREDENTIALS_PATH = flags.DEFINE_string('aws_credentials_path', '',
+                                           'AWS KMS credentials path.')
+AWS_KEY_URI = flags.DEFINE_string(
+    'aws_key_uri', '', 'AWS KMS key URL of the form: '
+    'aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>.')
 
 
-def main(unused_argv):
+def init_tink() -> None:
+  """Initializes Tink registering the required primitives."""
+
   aead.register()
   daead.register()
   hybrid.register()
@@ -50,6 +64,17 @@
   jwt.register_jwt_mac()
   jwt.register_jwt_signature()
   fake_kms.register_client()
+  awskms.AwsKmsClient.register_client(
+      key_uri=AWS_KEY_URI.value, credentials_path=AWS_CREDENTIALS_PATH.value)
+
+  gcpkms.GcpKmsClient.register_client(
+      key_uri=GCP_KEY_URI.value, credentials_path=GCP_CREDENTIALS_PATH.value
+  )
+
+
+def main(unused_argv):
+  init_tink()
+
   server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
   testing_api_pb2_grpc.add_MetadataServicer_to_server(
       services.MetadataServicer(), server)
@@ -71,9 +96,11 @@
       services.StreamingAeadServicer(), server)
   testing_api_pb2_grpc.add_JwtServicer_to_server(jwt_service.JwtServicer(),
                                                  server)
-  server.add_secure_port('[::]:%d' % FLAGS.port,
-                         grpc.local_server_credentials())
+  used_port = server.add_secure_port('[::]:%d' % FLAGS.port,
+                                     grpc.local_server_credentials())
   server.start()
+  print('Server started on port ' + str(used_port))
+  print(' (stderr) Server started on port ' + str(used_port), file=sys.stderr)
   server.wait_for_termination()
 
 
diff --git a/tink_base_deps.bzl b/tink_base_deps.bzl
deleted file mode 100644
index ecf05eb..0000000
--- a/tink_base_deps.bzl
+++ /dev/null
@@ -1,68 +0,0 @@
-"""Dependencies of Tink base."""
-
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
-
-def tink_base_deps():
-    """Loads dependencies of Tink base."""
-
-    # ----- Go
-    # Release from 2022-03-21
-    http_archive(
-        name = "io_bazel_rules_go",
-        sha256 = "f2dcd210c7095febe54b804bb1cd3a58fe8435a909db2ec04e31542631cf715c",
-        urls = [
-            "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.31.0/rules_go-v0.31.0.zip",
-            "https://github.com/bazelbuild/rules_go/releases/download/v0.31.0/rules_go-v0.31.0.zip",
-        ],
-    )
-
-    #-----------------------------------------------------------------------------
-    # Actual tink base deps.
-    #-----------------------------------------------------------------------------
-    # Basic rules we need to add to bazel.
-    if not native.existing_rule("bazel_skylib"):
-        # Release from 2021-09-27
-        http_archive(
-            name = "bazel_skylib",
-            urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
-                "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
-            ],
-            sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
-        )
-
-    # Google PKI certs for connecting to GCP KMS
-    if not native.existing_rule("google_root_pem"):
-        http_file(
-            name = "google_root_pem",
-            executable = 0,
-            urls = ["https://pki.goog/roots.pem"],
-            sha256 = "9c9b9685ad319b9747c3fe69b46a61c11a0efabdfa09ca6a8b0c3da421036d27",
-        )
-
-    # proto
-    # proto_library, cc_proto_library and java_proto_library rules implicitly depend
-    # on @com_google_protobuf//:proto, @com_google_protobuf//:cc_toolchain and
-    # @com_google_protobuf//:java_toolchain, respectively.
-    # This statement defines the @com_google_protobuf repo.
-    # Release from 2021-06-08
-    if not native.existing_rule("com_google_protobuf"):
-        http_archive(
-            name = "com_google_protobuf",
-            strip_prefix = "protobuf-3.19.3",
-            urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.3.zip"],
-            sha256 = "6b6bf5cd8d0cca442745c4c3c9f527c83ad6ef35a405f64db5215889ac779b42",
-        )
-
-    # Remote Build Execution
-    if not native.existing_rule("bazel_toolchains"):
-        # Latest bazel_toolchains package on 2021-10-13
-        http_archive(
-            name = "bazel_toolchains",
-            sha256 = "179ec02f809e86abf56356d8898c8bd74069f1bd7c56044050c2cd3d79d0e024",
-            strip_prefix = "bazel-toolchains-4.1.0",
-            urls = [
-                "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-                "https://github.com/bazelbuild/bazel-toolchains/releases/download/4.1.0/bazel-toolchains-4.1.0.tar.gz",
-            ],
-        )
diff --git a/tink_base_deps_init.bzl b/tink_base_deps_init.bzl
deleted file mode 100644
index 0ffa16c..0000000
--- a/tink_base_deps_init.bzl
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-Initalization of dependencies of Tink base.
-"""
-
-load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
-load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
-
-def tink_base_deps_init():
-    """ Initializes dependencies of Tink base.
-
-    """
-
-    # Actual base inits.
-    protobuf_deps()
-
-    # Creates a default toolchain config for RBE.
-    # Use this as is if you are using the rbe_ubuntu16_04 container,
-    # otherwise refer to RBE docs.
-    rbe_autoconfig(name = "rbe_default")
diff --git a/tinkey.rb b/tinkey.rb
index 10260b9..8aa70a7 100644
--- a/tinkey.rb
+++ b/tinkey.rb
@@ -6,8 +6,8 @@
 class Tinkey < Formula
   desc "A command line tool to generate and manipulate keysets for the Tink cryptography library"
   homepage "https://github.com/google/tink/tree/master/tools/tinkey"
-  url "https://storage.googleapis.com/tinkey/tinkey-1.6.1.tar.gz"
-  sha256 "156e902e212f55b6747a55f92da69a7e10bcbd00f8942bc1568c0e7caefff3e1"
+  url "https://storage.googleapis.com/tinkey/tinkey-1.7.0.tar.gz"
+  sha256 "2c9e69e5bc7561ce37918cecd3eeabb4571e01c915c4397bce25796ff04d92a3"
 
   def install
     bin.install "tinkey"
diff --git a/tools/.bazelrc b/tools/.bazelrc
new file mode 100644
index 0000000..0fe2c3f
--- /dev/null
+++ b/tools/.bazelrc
@@ -0,0 +1,4 @@
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/tools/.bazelversion b/tools/.bazelversion
index ac14c3d..09b254e 100644
--- a/tools/.bazelversion
+++ b/tools/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/tools/release_tinkey.sh b/tools/release_tinkey.sh
index 36a2fa5..a6059dc 100755
--- a/tools/release_tinkey.sh
+++ b/tools/release_tinkey.sh
@@ -17,9 +17,10 @@
 # This script creates a Tinkey distribution and uploads it to Google Cloud
 # Storage.
 # Prerequisites:
-#   - Google Cloud SDK: https://cloud.google.com/sdk/install
+#   - Google Cloud SDK (https://cloud.google.com/sdk/install)
 #   - Write access to the "tinkey" GCS bucket. Ping tink-dev@.
-#   - bazel: https://bazel.build/
+#   - Bazelisk (https://github.com/bazelbuild/bazelisk) or Bazel
+#     (https://bazel.build/)
 
 usage() {
   echo "Usage: $0 [-dh] <version>"
@@ -60,8 +61,12 @@
 # Set up parameters.
 
 readonly GCS_LOCATION="gs://tinkey/"
-
 readonly TMP_DIR="$(mktemp -dt tinkey.XXXXXX)"
+BAZEL_CMD="bazel"
+if command -v "bazelisk" &> /dev/null; then
+  BAZEL_CMD="bazelisk"
+fi
+readonly BAZEL_CMD
 
 do_command() {
   if ! "$@"; then
@@ -97,7 +102,7 @@
 build_tinkey() {
   print_and_do cd tools
 
-  print_and_do bazel build tinkey:tinkey_deploy.jar
+  print_and_do "${BAZEL_CMD}" build tinkey:tinkey_deploy.jar
 
   print_and_do cp bazel-bin/tinkey/tinkey_deploy.jar "${TMP_DIR}"
 
diff --git a/tools/remote_build_execution/BUILD.bazel b/tools/remote_build_execution/BUILD.bazel
deleted file mode 100644
index 5d23e87..0000000
--- a/tools/remote_build_execution/BUILD.bazel
+++ /dev/null
@@ -1,19 +0,0 @@
-package(default_visibility = ["//:__subpackages__"])
-
-licenses(["notice"])
-
-# Configuration for Remote Build Execution (RBE) builds.
-# It is used to select() RBE-specific args for tests.
-#
-# Example:
-# args = select({
-#   "//tools/remote_build_execution:rbe": ["--no_external_tests"],
-#   "//conditions:default": [],
-# }),
-#
-# This will set the --no_external_tests command line flag when a test is run
-# on RBE.
-config_setting(
-    name = "rbe",
-    values = {"define": "RBE=1"},
-)
diff --git a/tools/remote_build_execution/README.md b/tools/remote_build_execution/README.md
deleted file mode 100644
index b3eb6ba..0000000
--- a/tools/remote_build_execution/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-This directory contains bazelrc files that are needed for
-Remote Build Execution.
-
-The latest files can be found here:
-https://github.com/bazelbuild/bazel-toolchains/tree/master/bazelrc
-
-The current version is: 0.27
diff --git a/tools/remote_build_execution/bazel-rbe.bazelrc b/tools/remote_build_execution/bazel-rbe.bazelrc
deleted file mode 100644
index 731210e..0000000
--- a/tools/remote_build_execution/bazel-rbe.bazelrc
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2016 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This .bazelrc file contains all of the flags required for the provided
-# toolchain with Remote Build Execution.
-# Note your WORKSPACE must contain an rbe_autoconfig target with
-# name="rbe_default" to use these flags as-is.
-
-# Depending on how many machines are in the remote execution instance, setting
-# this higher can make builds faster by allowing more jobs to run in parallel.
-# Setting it too high can result in jobs that timeout, however, while waiting
-# for a remote machine to execute them.
-build:remote --jobs=50
-
-# Set several flags related to specifying the platform, toolchain and java
-# properties.
-# These flags should only be used as is for the rbe-ubuntu16-04 container
-# and need to be adapted to work with other toolchain containers.
-build:remote --host_javabase=@rbe_default//java:jdk
-build:remote --javabase=@rbe_default//java:jdk
-build:remote --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
-build:remote --java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
-build:remote --crosstool_top=@rbe_default//cc:toolchain
-build:remote --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
-# Platform flags:
-# The toolchain container used for execution is defined in the target indicated
-# by "extra_execution_platforms", "host_platform" and "platforms".
-# More about platforms: https://docs.bazel.build/versions/master/platforms.html
-build:remote --extra_toolchains=@rbe_default//config:cc-toolchain
-build:remote --extra_execution_platforms=@rbe_default//config:platform
-build:remote --host_platform=@rbe_default//config:platform
-build:remote --platforms=@rbe_default//config:platform
-
-# Starting with Bazel 0.27.0 strategies do not need to be explicitly
-# defined. See https://github.com/bazelbuild/bazel/issues/7480
-build:remote --define=EXECUTOR=remote
-
-# Enable remote execution so actions are performed on the remote systems.
-build:remote --remote_executor=grpcs://remotebuildexecution.googleapis.com
-
-# Set a higher timeout value, just in case.
-build:remote --remote_timeout=3600
-
-# Accept remotely cached action results.
-build:remote --remote_accept_cached=true
-
-# Do not fall back to standalone local execution if remote execution fails.
-build:remote --remote_local_fallback=false
-
-# Set custom flag RBE=1. This is used to select() custom args for tests.
-build:remote --define=RBE=1
-
-# Enable authentication. This will pick up application default credentials by
-# default. You can use --google_credentials=some_file.json to use a service
-# account credential instead.
-# DISABLE THIS! Kokoro will setup the credentials for us based on the build config.
-# build:remote --google_default_credentials=true
diff --git a/tools/testdata/README.md b/tools/testdata/README.md
deleted file mode 100644
index 9d362c7..0000000
--- a/tools/testdata/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-This folder contains AWS and GCP credentials that are used for testing Tink.
-
-# AWS
-
-For security reasons, all AWS credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html).
-The credentials are required in several formats expected by different APIs. For
-example, Java expects the credentials as a
-[properties file](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html).
-In order to cover all tests across all languages you will have to replace
-`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
-can be generated in a similar way to this
-[script](https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh).
-
-# GCP
-
-For security reasons, all GCP credentials in this folder are invalid. If you
-want to run tests that depend on them, please create your own
-[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
-credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/tools/testdata/aws/README.md b/tools/testdata/aws/README.md
new file mode 100644
index 0000000..ca15b34
--- /dev/null
+++ b/tools/testdata/aws/README.md
@@ -0,0 +1,16 @@
+This folder contains AWS credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid. If you want to
+run tests that depend on them, please create your own [AWS access
+keys][aws-access-keys].
+
+The credentials are required in several formats expected by different APIs. For
+example, Java expects the credentials as a [properties file][properties-file].
+In order to cover all tests across all languages you have to replace
+`aws/credentials.cred`, `aws/credentials.csv` and `aws/credentials.ini`. These
+can be generated in a similar way to this [credential copying
+script][copy-credentials-script].
+
+[aws-access-keys]: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+[properties-file]: https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html
+[copy-credentials-script]: https://github.com/google/tink/blob/master/kokoro/copy_credentials.sh
diff --git a/tools/testdata/gcp/README.md b/tools/testdata/gcp/README.md
new file mode 100644
index 0000000..81b5de0
--- /dev/null
+++ b/tools/testdata/gcp/README.md
@@ -0,0 +1,7 @@
+This folder contains GCP credentials that are used for testing Tink.
+
+For security reasons, all credentials in this folder are invalid.
+
+If you want to run tests that depend on them, please create your own
+[Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the
+credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`.
diff --git a/tools/testing/BUILD.bazel b/tools/testing/BUILD.bazel
index 2ba468a..12b9a51 100644
--- a/tools/testing/BUILD.bazel
+++ b/tools/testing/BUILD.bazel
@@ -9,29 +9,19 @@
         "java/com/google/crypto/tink/testing/CliUtil.java",
     ],
     deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_writer",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "@tink_java//src/main/java/com/google/crypto/tink/config:tink_config",
         "@tink_java//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "@tink_java//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config",
         "@tink_java//src/main/java/com/google/crypto/tink/prf:prf_config",
         "@tink_java//src/main/java/com/google/crypto/tink/signature:signature_config",
         "@tink_java//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
     ],
 )
 
-java_library(
-    name = "compare_keysets",
-    testonly = 1,
-    srcs = ["java/com/google/crypto/tink/testing/CompareKeysets.java"],
-    deps = [
-        "@tink_java//proto:tink_java_proto",
-        "@tink_java//src/main/java/com/google/crypto/tink:privileged_registry",
-    ],
-)
-
 java_binary(
     name = "aead_cli_java",
     testonly = 1,
@@ -44,8 +34,5 @@
         "@tink_java//src/main/java/com/google/crypto/tink:aead",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
         "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
     ],
 )
diff --git a/tools/testing/cc/BUILD.bazel b/tools/testing/cc/BUILD.bazel
index ec071c0..3481379 100644
--- a/tools/testing/cc/BUILD.bazel
+++ b/tools/testing/cc/BUILD.bazel
@@ -9,10 +9,6 @@
     name = "cli_util",
     srcs = ["cli_util.cc"],
     hdrs = ["cli_util.h"],
-    data = [
-        "//testdata/aws:credentials",
-        "//testdata/gcp:credentials",
-    ],
     deps = [
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/strings",
@@ -24,32 +20,6 @@
         "@tink_cc//:kms_clients",
         "@tink_cc//config:tink_config",
         "@tink_cc//util:status",
-        "@tink_cc_awskms//:aws_kms_client",
-        "@tink_cc_gcpkms//:gcp_kms_client",
-    ],
-)
-
-cc_binary(
-    name = "aws_kms_aead_cli",
-    srcs = ["aws_kms_aead_cli.cc"],
-    deps = [
-        ":cli_util",
-        "@aws_cpp_sdk//:aws_sdk_core",
-        "@tink_cc",
-        "@tink_cc_awskms//:aws_crypto",
-        "@tink_cc_awskms//:aws_kms_aead",
-        "@tink_cc_awskms//:aws_kms_client",
-    ],
-)
-
-cc_binary(
-    name = "gcp_kms_aead_cli",
-    srcs = ["gcp_kms_aead_cli.cc"],
-    deps = [
-        ":cli_util",
-        "@tink_cc",
-        "@tink_cc_gcpkms//:gcp_kms_aead",
-        "@tink_cc_gcpkms//:gcp_kms_client",
     ],
 )
 
@@ -61,42 +31,3 @@
         "@tink_cc",
     ],
 )
-
-sh_test(
-    name = "aws_kms_aead_test",
-    size = "medium",
-    srcs = [
-        "aws_kms_aead_test.sh",
-    ],
-    data = [
-        ":aws_kms_aead_cli",
-        "//testdata/aws:bad_credentials",
-        "//testdata/aws:credentials",
-        "//testing/cross_language:test_lib",
-    ],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
-)
-
-sh_test(
-    name = "gcp_kms_aead_test",
-    size = "medium",
-    srcs = [
-        "gcp_kms_aead_test.sh",
-    ],
-    data = [
-        ":gcp_kms_aead_cli",
-        "//testdata/gcp:bad_credentials",
-        "//testdata/gcp:credentials",
-        "//testing/cross_language:test_lib",
-        "@google_root_pem//file",
-    ],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
-)
diff --git a/tools/testing/cc/aws_kms_aead_cli.cc b/tools/testing/cc/aws_kms_aead_cli.cc
deleted file mode 100644
index dcc40f8..0000000
--- a/tools/testing/cc/aws_kms_aead_cli.cc
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-
-#include <string>
-#include <vector>
-
-#include "absl/strings/ascii.h"
-#include "aws/core/Aws.h"
-#include "aws/core/utils/crypto/Factories.h"
-#include "aws/core/utils/memory/AWSMemory.h"
-#include "aws/kms/KMSClient.h"
-#include "tink/aead.h"
-#include "tink/integration/awskms/aws_crypto.h"
-#include "tink/integration/awskms/aws_kms_aead.h"
-#include "tink/integration/awskms/aws_kms_client.h"
-#include "tink/util/status.h"
-#include "tink/util/statusor.h"
-#include "testing/cc/cli_util.h"
-
-using crypto::tink::Aead;
-using crypto::tink::integration::awskms::AwsKmsAead;
-using crypto::tink::integration::awskms::AwsKmsClient;
-
-// A command-line utility for testing AwsKmsAead.
-// It requires 6 arguments:
-//   key-arn-file:  Amazon Resource Name of AWS KMS key for encryption
-//   credentials-file: credentials file containing AWS access key
-//   operation: the actual AEAD-operation, i.e. "encrypt" or "decrypt"
-//   input-file:  name of the file with input (plaintext for encryption, or
-//                or ciphertext for decryption)
-//   associated-data:  a string to be used as associated data
-//   output-file:  name of the file for the resulting output
-int main(int argc, char** argv) {
-  if (argc != 7) {
-    std::clog << "Usage: " << argv[0]
-              << " key-arn-file credentials-file"
-              << " operation input-file associated-data output-file\n";
-    exit(1);
-  }
-  std::string key_arn_filename(argv[1]);
-  std::string credentials_filename(argv[2]);
-  std::string operation(argv[3]);
-  std::string input_filename(argv[4]);
-  std::string associated_data(argv[5]);
-  std::string output_filename(argv[6]);
-  if (!(operation == "encrypt" || operation == "decrypt")) {
-    std::clog << "Unknown operation '" << operation << "'.\n"
-              << "Expected 'encrypt' or 'decrypt'.\n";
-    exit(1);
-  }
-  std::clog << "Using key_arn from file " << key_arn_filename
-            << " and AWS credentials from file " << credentials_filename
-            << " to AEAD-" << operation
-            << " file "<< input_filename
-            << " with associated data '" << associated_data << "'.\n"
-            << "The resulting output will be written to file "
-            << output_filename << std::endl;
-
-  std::string key_arn = CliUtil::Read(key_arn_filename);
-  absl::StripAsciiWhitespace(&key_arn);
-  std::clog << "Will use key ARN " << key_arn << std::endl;
-
-  // Create AwsKmsClient.
-  auto client_result = AwsKmsClient::New("", credentials_filename);
-  if (!client_result.ok()) {
-    std::clog << "Aead creation failed: "
-              << client_result.status().message()
-              << "\n";
-    exit(1);
-  }
-  auto client = std::move(client_result.value());
-
-  // Create Aead-primitive.
-  auto aead_result = client->GetAead("aws-kms://" + key_arn);
-  if (!aead_result.ok()) {
-    std::clog << "Aead creation failed: "
-              << aead_result.status().message()
-              << "\n";
-    exit(1);
-  }
-  std::unique_ptr<Aead> aead(std::move(aead_result.value()));
-
-  // Read the input.
-  std::string input = CliUtil::Read(input_filename);
-
-  // Compute the output.
-  std::clog << operation << "ing...\n";
-  std::string output;
-  if (operation == "encrypt") {
-    auto encrypt_result = aead->Encrypt(input, associated_data);
-    if (!encrypt_result.ok()) {
-      std::clog << "Error while encrypting the input:"
-                << encrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = encrypt_result.value();
-  } else {  // operation == "decrypt"
-    auto decrypt_result = aead->Decrypt(input, associated_data);
-    if (!decrypt_result.ok()) {
-      std::clog << "Error while decrypting the input:"
-                << decrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = decrypt_result.value();
-  }
-
-  // Write the output to the output file.
-  CliUtil::Write(output, output_filename);
-
-  std::clog << "All done.\n";
-}
diff --git a/tools/testing/cc/aws_kms_aead_test.sh b/tools/testing/cc/aws_kms_aead_test.sh
deleted file mode 100755
index 5c487b3..0000000
--- a/tools/testing/cc/aws_kms_aead_test.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/bash
-# Copyright 2018 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-REPO_DIR="${TEST_SRCDIR}"
-TOOLS_DIR="${REPO_DIR}/tools"
-AWS_KMS_AEAD_CLI="${TOOLS_DIR}/testing/cc/aws_kms_aead_cli"
-TEST_UTIL="${TOOLS_DIR}/testing/cross_language/test_util.sh"
-AWS_KEY_ARN_FILE="${TOOLS_DIR}/testdata/aws/key_arn.txt"
-CREDENTIALS_AWS_CSV_FILE="${TOOLS_DIR}/testdata/aws/credentials.ini"
-BAD_AWS_KEY_ARN_FILE="${TOOLS_DIR}/testdata/aws/key_arn_bad.txt"
-BAD_CREDENTIALS_AWS_CSV_FILE="${TOOLS_DIR}/testdata/aws/credentials_bad.ini"
-associated_data="some associated data"
-
-source $TEST_UTIL || exit 1
-
-#############################################################################
-# Bad access key test.
-test_name="bad_aws_access_key"
-echo "+++ starting test $test_name ..."
-generate_plaintext $test_name
-encrypted_file="$TEST_TMPDIR/${test_name}_encrypted.bin"
-log_file="$TEST_TMPDIR/${test_name}.log"
-$AWS_KMS_AEAD_CLI $AWS_KEY_ARN_FILE $BAD_CREDENTIALS_AWS_CSV_FILE\
-  encrypt $plaintext_file "$associated_data" $encrypted_file 2> $log_file
-
-assert_file_contains $log_file "UnrecognizedClientException"
-
-#############################################################################
-# Bad key arn test.
-test_name="bad_key_arn"
-echo "+++ starting test $test_name ..."
-generate_plaintext $test_name
-encrypted_file="$TEST_TMPDIR/${test_name}_encrypted.bin"
-log_file="$TEST_TMPDIR/${test_name}.log"
-$AWS_KMS_AEAD_CLI $BAD_AWS_KEY_ARN_FILE $CREDENTIALS_AWS_CSV_FILE\
-  encrypt $plaintext_file "$associated_data" $encrypted_file 2> $log_file
-
-assert_file_contains $log_file "AccessDeniedException"
-
-#############################################################################
-# All good, encryption and decryption should work.
-test_name="good_key_arn_and_access_key"
-echo "+++ starting test $test_name ..."
-generate_plaintext $test_name
-encrypted_file="$TEST_TMPDIR/${test_name}_encrypted.bin"
-decrypted_file="$TEST_TMPDIR/${test_name}_decrypted.bin"
-log_file="$TEST_TMPDIR/${test_name}.log"
-echo "    encrypting..."
-$AWS_KMS_AEAD_CLI $AWS_KEY_ARN_FILE $CREDENTIALS_AWS_CSV_FILE\
-  encrypt $plaintext_file "$associated_data" $encrypted_file 2> $log_file
-assert_file_contains $log_file "All done"
-assert_files_different $plaintext_file $encrypted_file
-
-echo "    decrypting..."
-$AWS_KMS_AEAD_CLI $AWS_KEY_ARN_FILE $CREDENTIALS_AWS_CSV_FILE\
-  decrypt $encrypted_file "$associated_data" $decrypted_file 2> $log_file
-assert_file_contains $log_file "All done"
-
-echo "    checking decryption result..."
-assert_files_equal $plaintext_file $decrypted_file
diff --git a/tools/testing/cc/cli_util.cc b/tools/testing/cc/cli_util.cc
index aed2c97..327b306 100644
--- a/tools/testing/cc/cli_util.cc
+++ b/tools/testing/cc/cli_util.cc
@@ -27,11 +27,8 @@
 #include "tink/binary_keyset_reader.h"
 #include "tink/binary_keyset_writer.h"
 #include "tink/cleartext_keyset_handle.h"
-#include "tink/config.h"
 #include "tink/config/tink_config.h"
 #include "tink/input_stream.h"
-#include "tink/integration/awskms/aws_kms_client.h"
-#include "tink/integration/gcpkms/gcp_kms_client.h"
 #include "tink/json_keyset_reader.h"
 #include "tink/json_keyset_writer.h"
 #include "tink/keyset_handle.h"
@@ -50,11 +47,8 @@
 using crypto::tink::KeysetHandle;
 using crypto::tink::KeysetReader;
 using crypto::tink::KeysetWriter;
-using crypto::tink::KmsClients;
 using crypto::tink::OutputStream;
 using crypto::tink::TinkConfig;
-using crypto::tink::integration::awskms::AwsKmsClient;
-using crypto::tink::integration::gcpkms::GcpKmsClient;
 using crypto::tink::util::Status;
 
 namespace {
@@ -186,50 +180,6 @@
               << std::endl;
     exit(1);
   }
-
-  Status gcp_result = InitGcp();
-  if (!gcp_result.ok()) {
-    std::clog << gcp_result.message() << std::endl;
-  }
-
-  Status aws_result = InitAws();
-  if (!aws_result.ok()) {
-    std::clog << aws_result.message() << std::endl;
-  }
-}
-
-// static
-Status CliUtil::InitGcp() {
-  std::string creds_file = std::string(getenv("TEST_SRCDIR")) +
-                           "/tools/testdata/gcp/credential.json";
-  auto client_result = GcpKmsClient::New("", creds_file);
-  if (!client_result.ok()) {
-    return Status(absl::StatusCode::kInternal,
-                  "Failed to connect to GCP client.");
-  }
-  auto client_add_result = KmsClients::Add(std::move(client_result.value()));
-  if (!client_add_result.ok()) {
-    return Status(absl::StatusCode::kInternal, "Failed to add KMS client.");
-  }
-  return crypto::tink::util::OkStatus();
-}
-
-// static
-Status CliUtil::InitAws() {
-  std::string creds_file = std::string(getenv("TEST_SRCDIR")) +
-                           "/tools/testdata/aws/credentials.ini";
-  auto client_result = AwsKmsClient::New("", creds_file);
-  if (!client_result.ok()) {
-    return Status(crypto::tink::util::error::INTERNAL,
-                        "Failed to connect to AWS client.");
-  }
-  auto client_add_result =
-      KmsClients::Add(std::move(client_result.value()));
-  if (!client_add_result.ok()) {
-    return Status(crypto::tink::util::error::INTERNAL,
-                        "Failed to add KMS client.");
-  }
-  return Status::OK;
 }
 
 // static
diff --git a/tools/testing/cc/gcp_kms_aead_cli.cc b/tools/testing/cc/gcp_kms_aead_cli.cc
deleted file mode 100644
index 3b32cde..0000000
--- a/tools/testing/cc/gcp_kms_aead_cli.cc
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-
-#include <string>
-#include <vector>
-
-#include "absl/strings/ascii.h"
-#include "tink/aead.h"
-#include "tink/integration/gcpkms/gcp_kms_client.h"
-#include "tink/util/status.h"
-#include "tink/util/statusor.h"
-#include "testing/cc/cli_util.h"
-
-using crypto::tink::Aead;
-using crypto::tink::integration::gcpkms::GcpKmsClient;
-
-// A command-line utility for testing GcpKmsAead.
-// It requires 6 arguments:
-//   key-name-file:  Google Cloud KMS key to be used for encryption
-//   credentials-file: credentials file containing GCP credentials
-//   operation: the actual AEAD-operation, i.e. "encrypt" or "decrypt"
-//   input-file:  name of the file with input (plaintext for encryption, or
-//                or ciphertext for decryption)
-//   associated-data:  a string to be used as associated data
-//   output-file:  name of the file for the resulting output
-int main(int argc, char** argv) {
-  if (argc != 7) {
-    std::clog << "Usage: " << argv[0]
-              << " key-name-file credentials-file"
-              << " operation input-file associated-data output-file\n";
-    exit(1);
-  }
-  std::string key_name_filename(argv[1]);
-  std::string credentials_filename(argv[2]);
-  std::string operation(argv[3]);
-  std::string input_filename(argv[4]);
-  std::string associated_data(argv[5]);
-  std::string output_filename(argv[6]);
-  if (!(operation == "encrypt" || operation == "decrypt")) {
-    std::clog << "Unknown operation '" << operation << "'.\n"
-              << "Expected 'encrypt' or 'decrypt'.\n";
-    exit(1);
-  }
-  std::clog << "Using key_name from file " << key_name_filename
-            << " and GCP credentials from file " << credentials_filename
-            << " to AEAD-" << operation
-            << " file "<< input_filename
-            << " with associated data '" << associated_data << "'.\n"
-            << "The resulting output will be written to file "
-            << output_filename << std::endl;
-
-  std::string key_name = CliUtil::Read(key_name_filename);
-  absl::StripAsciiWhitespace(&key_name);
-  std::clog << "Will use key name " << key_name << std::endl;
-
-  // Create GcpKmsClient.
-  auto client_result = GcpKmsClient::New("", credentials_filename);
-  if (!client_result.ok()) {
-    std::clog << "Aead creation failed: "
-              << client_result.status().message()
-              << "\n";
-    exit(1);
-  }
-  auto client = std::move(client_result.value());
-
-  // Create Aead-primitive.
-  auto aead_result = client->GetAead("gcp-kms://" + key_name);
-  if (!aead_result.ok()) {
-    std::clog << "Aead creation failed: "
-              << aead_result.status().message()
-              << "\n";
-    exit(1);
-  }
-  std::unique_ptr<Aead> aead(std::move(aead_result.value()));
-
-  // Read the input.
-  std::string input = CliUtil::Read(input_filename);
-
-  // Compute the output.
-  std::clog << operation << "ing...\n";
-  std::string output;
-  if (operation == "encrypt") {
-    auto encrypt_result = aead->Encrypt(input, associated_data);
-    if (!encrypt_result.ok()) {
-      std::clog << "Error while encrypting the input:"
-                << encrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = encrypt_result.value();
-  } else {  // operation == "decrypt"
-    auto decrypt_result = aead->Decrypt(input, associated_data);
-    if (!decrypt_result.ok()) {
-      std::clog << "Error while decrypting the input:"
-                << decrypt_result.status().message() << std::endl;
-      exit(1);
-    }
-    output = decrypt_result.value();
-  }
-
-  // Write the output to the output file.
-  CliUtil::Write(output, output_filename);
-
-  std::clog << "All done.\n";
-}
diff --git a/tools/testing/cc/gcp_kms_aead_test.sh b/tools/testing/cc/gcp_kms_aead_test.sh
deleted file mode 100755
index ccc1771..0000000
--- a/tools/testing/cc/gcp_kms_aead_test.sh
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/bin/bash
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-REPO_DIR="${TEST_SRCDIR}"
-TOOLS_DIR="${REPO_DIR}/tools"
-GCP_KMS_AEAD_CLI="${TOOLS_DIR}/testing/cc/gcp_kms_aead_cli"
-TEST_UTIL="${TOOLS_DIR}/testing/cross_language/test_util.sh"
-GCP_KEY_NAME_FILE="${TOOLS_DIR}/testdata/gcp/key_name.txt"
-CREDENTIALS_GCP_JSON_FILE="${TOOLS_DIR}/testdata/gcp/credential.json"
-BAD_GCP_KEY_NAME_FILE="${TOOLS_DIR}/testdata/gcp/key_name_bad.txt"
-BAD_CREDENTIALS_GCP_JSON_FILE="${TOOLS_DIR}/testdata/gcp/credential_bad.json"
-associated_data="some associated data"
-
-# Roots for GRPC
-# (https://github.com/grpc/grpc/blob/master/doc/environment_variables.md)
-export GRPC_DEFAULT_SSL_ROOTS_FILE_PATH="$TEST_SRCDIR/google_root_pem/file/downloaded"
-
-source $TEST_UTIL || exit 1
-
-#############################################################################
-# All good, encryption and decryption should work.
-test_name="good_key_name_and_credentials"
-echo "+++ starting test $test_name ..."
-generate_plaintext $test_name
-encrypted_file="$TEST_TMPDIR/${test_name}_encrypted.bin"
-decrypted_file="$TEST_TMPDIR/${test_name}_decrypted.bin"
-log_file="$TEST_TMPDIR/${test_name}.log"
-echo "    encrypting..."
-$GCP_KMS_AEAD_CLI $GCP_KEY_NAME_FILE $CREDENTIALS_GCP_JSON_FILE\
-  encrypt $plaintext_file "$associated_data" $encrypted_file 2> $log_file
-assert_file_contains $log_file "All done"
-assert_files_different $plaintext_file $encrypted_file
-
-echo "    decrypting..."
-$GCP_KMS_AEAD_CLI $GCP_KEY_NAME_FILE $CREDENTIALS_GCP_JSON_FILE\
-  decrypt $encrypted_file "$associated_data" $decrypted_file 2> $log_file
-assert_file_contains $log_file "All done"
-
-echo "    checking decryption result..."
-assert_files_equal $plaintext_file $decrypted_file
-
-#############################################################################
-# Bad credentials test.
-test_name="bad_gcp_credentials"
-echo "+++ starting test $test_name ..."
-generate_plaintext $test_name
-encrypted_file="$TEST_TMPDIR/${test_name}_encrypted.bin"
-log_file="$TEST_TMPDIR/${test_name}.log"
-$GCP_KMS_AEAD_CLI $GCP_KEY_NAME_FILE $BAD_CREDENTIALS_GCP_JSON_FILE\
-  encrypt $plaintext_file "$associated_data" $encrypted_file 2> $log_file
-
-assert_file_contains $log_file "invalid authentication credentials"
-
-#############################################################################
-# Bad key name test.
-test_name="bad_key_name"
-echo "+++ starting test $test_name ..."
-generate_plaintext $test_name
-encrypted_file="$TEST_TMPDIR/${test_name}_encrypted.bin"
-log_file="$TEST_TMPDIR/${test_name}.log"
-$GCP_KMS_AEAD_CLI $BAD_GCP_KEY_NAME_FILE $CREDENTIALS_GCP_JSON_FILE\
-  encrypt $plaintext_file "$associated_data" $encrypted_file 2> $log_file
-
-assert_file_contains $log_file "Permission" "denied" "or it may not exist"
diff --git a/tools/testing/cross_language/BUILD.bazel b/tools/testing/cross_language/BUILD.bazel
index 12c66a4..6ad5f6f 100644
--- a/tools/testing/cross_language/BUILD.bazel
+++ b/tools/testing/cross_language/BUILD.bazel
@@ -8,24 +8,3 @@
         "test_util.sh",
     ],
 )
-
-sh_test(
-    name = "aead_envelope_test",
-    size = "medium",
-    srcs = [
-        "aead_envelope_test.sh",
-    ],
-    data = [
-        ":test_lib",
-        "//testdata/aws:credentials",
-        "//testdata/gcp:credentials",
-        "//testing:aead_cli_java",
-        "//testing/cc:aead_cli_cc",
-        "//testing/go:aead_cli_go",
-        "//testing/go:generate_envelope_keyset",
-        "//testing/python:aead_cli_python",
-        "//tinkey",
-        "@google_root_pem//file",
-    ],
-    tags = ["manual"],
-)
diff --git a/tools/testing/cross_language/aead_envelope_test.sh b/tools/testing/cross_language/aead_envelope_test.sh
deleted file mode 100755
index c9d3547..0000000
--- a/tools/testing/cross_language/aead_envelope_test.sh
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/bin/bash
-# Copyright 2020 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-readonly REPO_DIR="${TEST_SRCDIR}"
-readonly TOOLS_DIR="${REPO_DIR}/tools"
-readonly TEST_UTIL="${TOOLS_DIR}/testing/cross_language/test_util.sh"
-
-# Paths to AEAD CLI utilities.
-readonly CC_AEAD_CLI="${TOOLS_DIR}/testing/cc/aead_cli_cc"
-readonly GO_AEAD_CLI="${TOOLS_DIR}/testing/go/aead_cli_go"
-readonly JAVA_AEAD_CLI="${TOOLS_DIR}/testing/aead_cli_java"
-readonly PY_AEAD_CLI="${TOOLS_DIR}/testing/python/aead_cli_python"
-
-# Modify the following variables to alter the set of languages and key formats
-# tested.
-declare -a ENCRYPT_CLIS
-ENCRYPT_CLIS=(
-  "${GO_AEAD_CLI}"
-  "${JAVA_AEAD_CLI}"
-  "${CC_AEAD_CLI}"
-  "${PY_AEAD_CLI}"
-)
-readonly ENCRYPT_CLIS
-
-declare -a DECRYPT_CLIS
-DECRYPT_CLIS=(
-  "${GO_AEAD_CLI}"
-  "${JAVA_AEAD_CLI}"
-  "${CC_AEAD_CLI}"
-  "${PY_AEAD_CLI}"
-)
-readonly DECRYPT_CLIS
-
-declare -a KEY_TEMPLATES
-KEY_TEMPLATES=(
-  AES128_GCM
-  AES128_CTR_HMAC_SHA256
-)
-readonly KEY_TEMPLATES
-
-# Root certificates for GRPC.
-# (https://github.com/grpc/grpc/blob/master/doc/environment_variables.md)
-export GRPC_DEFAULT_SSL_ROOTS_FILE_PATH="${TEST_SRCDIR}/google_root_pem/file/downloaded"
-
-source "${TEST_UTIL}" || exit 1
-
-#######################################
-# Execute cross-language AEAD envelope encryption tests.
-# Globals:
-#   ENCRYPT_CLIS
-#   DECRYPT_CLIS
-#   KEY_TEMPLATES
-# Arguments:
-#   A name which describes the test.
-#   The name of a command/function to generate the keyset.
-#######################################
-aead_envelope_test() {
-  local test_name="$1"
-  local keyset_function="$2"
-
-  echo "############ starting test ${test_name} for the following templates:"
-  echo "${KEY_TEMPLATES}"
-  for key_template in "${KEY_TEMPLATES[@]}"
-  do
-    echo "## TEST for key template ${key_template}"
-    for encrypt_cli in "${ENCRYPT_CLIS[@]}"
-    do
-      local test_instance="${test_name}_${key_template}"
-      local encrypt_cli_name="$(basename "${encrypt_cli}")"
-
-      "${keyset_function}" \
-        "${test_instance}_ENCRYPT_${encrypt_cli_name}" \
-        "${key_template}"
-      generate_plaintext "${test_instance}"
-
-      local encrypted_prefix="${test_instance}}_ENCRYPT_${encrypt_cli_name}"
-      local encrypted_file="${TEST_TMPDIR}/${encrypted_prefix}_encrypted.bin"
-      local associated_data_file="${encrypted_prefix}_aad.bin"
-      echo "AAD for ${test_instance} using ${encrypt_cli_name} for encryption" \
-          > "${associated_data_file}"
-
-      echo "## ENCRYPTING using ${encrypt_cli_name}"
-      "${encrypt_cli}" \
-        "${aws_keyset_file}" \
-        "encrypt" \
-        "${plaintext_file}" \
-        "${associated_data_file}" \
-        "${encrypted_file}" \
-        || exit 1
-
-      assert_files_different "${plaintext_file}" "${encrypted_file}"
-
-      for decrypt_cli in "${DECRYPT_CLIS[@]}"
-      do
-        local decrypt_cli_name="$(basename "${decrypt_cli}")"
-        local decrypted_prefix="${encrypted_prefix}_DECRYPT_${decrypted_cli_name}"
-        local decrypted_file="${TEST_TMPDIR}/${decrypted_prefix}_decrypted.bin"
-
-        echo "## DECRYPTING using ${decrypt_cli_name}"
-        "${decrypt_cli}" \
-          "${aws_keyset_file}" \
-          "decrypt" \
-          "${encrypted_file}" \
-          "${associated_data_file}" \
-          "${decrypted_file}" \
-          || exit 1
-
-        assert_files_equal "${plaintext_file}" "${decrypted_file}"
-      done
-    done
-  done
-}
-
-main() {
-  aead_envelope_test "aead-envelope-aws-test" generate_aws_keyset
-  aead_envelope_test "aead-envelope-gcp-test" generate_gcp_keyset
-}
-
-main "$@"
diff --git a/tools/testing/cross_language/test_util.sh b/tools/testing/cross_language/test_util.sh
index 92eb896..e466760 100755
--- a/tools/testing/cross_language/test_util.sh
+++ b/tools/testing/cross_language/test_util.sh
@@ -18,7 +18,6 @@
 REPO_DIR="${TEST_SRCDIR}"
 TOOLS_DIR="${REPO_DIR}/tools"
 TINKEY_CLI="${TOOLS_DIR}/tinkey/tinkey"
-ENVELOPE_CLI="${TOOLS_DIR}/testing/go/generate_envelope_keyset"
 #############################################################################
 ##### Helper functions.
 
@@ -70,45 +69,6 @@
   echo "Done generating a symmetric keyset."
 }
 
-# Generates an AWS Envelope Encryption using $key_template,
-# which should be supported by Tinkey.
-# Stores the key in file $aws_keyset_file.
-generate_aws_keyset() {
-  local key_name="$1"
-  local key_template="$2"
-  local output_format="$3"
-  if [ "$output_format" == "" ]; then
-    output_format="BINARY"
-  fi
-  aws_keyset_file="$TEST_TMPDIR/${key_name}_aws_keyset.bin"
-  echo "--- Using AWS KMS and template $key_template to generate keyset"\
-       "to file $aws_keyset_file ..."
-
-  $ENVELOPE_CLI $aws_keyset_file "AWS" $key_template || exit 1
-
-  echo "Done generating an AWS KMS generated keyset."
-}
-
-# Generates an GCP Envelope Encryption using $key_template,
-# which should be supported by Tinkey.
-# Stores the key in file $gcp_keyset_file.
-generate_gcp_keyset() {
-  local key_name="$1"
-  local key_template="$2"
-  local output_format="$3"
-
-  if [ "$output_format" == "" ]; then
-    output_format="BINARY"
-  fi
-  gcp_keyset_file="$TEST_TMPDIR/${key_name}_gcp_keyset.bin"
-  echo "--- Using GCP KMS and template $key_template to generate keyset"\
-      "to file $gcp_keyset_file ..."
-  $ENVELOPE_CLI $gcp_keyset_file "GCP" $key_template || exit 1
-
-  echo "Done generating an GCP KMS generated keyset."
-
-}
-
 # Generates some example plaintext data, and stores it in $plaintext_file.
 generate_plaintext() {
   local plaintext_name="$1"
diff --git a/tools/testing/go/BUILD.bazel b/tools/testing/go/BUILD.bazel
deleted file mode 100644
index eb29c4d..0000000
--- a/tools/testing/go/BUILD.bazel
+++ /dev/null
@@ -1,66 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary")
-
-package(default_visibility = ["//:__subpackages__"])
-
-licenses(["notice"])
-
-go_binary(
-    name = "aead_cli_go",
-    testonly = 1,  # keep
-    srcs = ["aead_cli.go"],
-    out = "aead_cli_go",
-    data = ["//testdata/gcp:credentials"],
-    deps = [
-        "@tink_go//aead:go_default_library",
-        "@tink_go//core/registry:go_default_library",
-        "@tink_go//integration/awskms:go_default_library",
-        "@tink_go//integration/gcpkms:go_default_library",
-        "@tink_go//keyset:go_default_library",
-        "@tink_go//testkeyset:go_default_library",
-    ],
-)
-
-go_binary(
-    name = "public_key_sign_cli_go",
-    testonly = 1,  # keep
-    srcs = ["public_key_sign_cli.go"],
-    out = "public_key_sign_cli_go",
-    deps = [
-        "@tink_go//keyset:go_default_library",
-        "@tink_go//signature:go_default_library",
-        "@tink_go//testkeyset:go_default_library",
-    ],
-)
-
-go_binary(
-    name = "public_key_verify_cli_go",
-    testonly = 1,  # keep
-    srcs = ["public_key_verify_cli.go"],
-    out = "public_key_verify_cli_go",
-    deps = [
-        "@tink_go//keyset:go_default_library",
-        "@tink_go//signature:go_default_library",
-        "@tink_go//testkeyset:go_default_library",
-    ],
-)
-
-go_binary(
-    name = "generate_envelope_keyset",
-    testonly = 1,  # keep
-    srcs = ["generate_envelope_keyset.go"],
-    out = "generate_envelope_keyset",
-    data = [
-        "//testdata/aws:credentials",
-        "//testdata/gcp:credentials",
-    ],
-    tags = ["no_rbe"],
-    deps = [
-        "@tink_go//aead:go_default_library",
-        "@tink_go//core/registry:go_default_library",
-        "@tink_go//insecurecleartextkeyset:go_default_library",
-        "@tink_go//integration/awskms:go_default_library",
-        "@tink_go//integration/gcpkms:go_default_library",
-        "@tink_go//keyset:go_default_library",
-        "@tink_go//proto/tink_go_proto:go_default_library",
-    ],
-)
diff --git a/tools/testing/go/aead_cli.go b/tools/testing/go/aead_cli.go
deleted file mode 100644
index 4a1c53e..0000000
--- a/tools/testing/go/aead_cli.go
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2019 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for testing AEAD-primitives.
-// It requires 5 arguments:
-//
-//	keyset-file:  name of the file with the keyset to be used for encryption
-//	operation: the actual AEAD-operation, i.e. "encrypt" or "decrypt"
-//	input-file:  name of the file with input (plaintext for encryption, or
-//	             or ciphertext for decryption)
-//	associated-data-file:  name of the file containing associated data
-//	output-file:  name of the file for the resulting output
-package main
-
-import (
-	"io/ioutil"
-	"log"
-	"os"
-	"path/filepath"
-
-	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/integration/awskms"
-	"github.com/google/tink/go/integration/gcpkms"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/testkeyset"
-)
-
-var (
-	gcpURI      = "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key"
-	gcpCredFile = filepath.Join(os.Getenv("TEST_SRCDIR"), "tools/testdata/gcp/credential.json")
-	awsURI      = "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-	awsCredFile = filepath.Join(os.Getenv("TEST_SRCDIR"), "tools/testdata/aws/credentials.csv")
-)
-
-func init() {
-	gcpclient, err := gcpkms.NewClientWithCredentials(gcpURI, gcpCredFile)
-	if err != nil {
-		log.Fatal(err)
-	}
-	registry.RegisterKMSClient(gcpclient)
-
-	awsclient, err := awskms.NewClientWithCredentials(awsURI, awsCredFile)
-	if err != nil {
-		log.Fatal(err)
-	}
-	registry.RegisterKMSClient(awsclient)
-}
-
-func main() {
-	if len(os.Args) != 6 {
-		log.Fatalf("Usage: %s keyset-file operation input-file associated-data-file output-file\n", os.Args[0])
-	}
-
-	keysetFilename := os.Args[1]
-	operation := os.Args[2]
-	inputFilename := os.Args[3]
-	associatedDataFile := os.Args[4]
-	outputFilename := os.Args[5]
-
-	if !(operation == "encrypt" || operation == "decrypt") {
-		log.Fatalf("Unknown operation %q. Expected 'encrypt' or 'decrypt'", operation)
-	}
-
-	log.Printf("Using keyset from file %q to-AEAD-%s file %q with associated data from file %q.",
-		keysetFilename, operation, inputFilename, associatedDataFile)
-	log.Printf("The result will be written to %q\n", outputFilename)
-
-	// Read the keyset.
-	f, err := os.Open(keysetFilename)
-	if err != nil {
-		log.Fatalf("Opening the keyset file failed: %v\n", err)
-	}
-	reader := keyset.NewBinaryReader(f)
-	handle, err := testkeyset.Read(reader)
-	if err != nil {
-		log.Fatalf("Reading the keyset failed: %v\n", err)
-	}
-
-	// Get Primitive
-	cipher, err := aead.New(handle)
-	if err != nil {
-		log.Fatalf("Failed to create primitive: %v\n", err)
-	}
-
-	// Read input
-	content, err := ioutil.ReadFile(inputFilename)
-	if err != nil {
-		log.Fatalf("Failed to read input: %v", err)
-	}
-
-	// Read associated data
-	associatedData, err := ioutil.ReadFile(associatedDataFile)
-	if err != nil {
-		log.Fatalf("Failed to read associated data file: %v", err)
-	}
-
-	// Compute output
-	var result []byte
-	if operation == "encrypt" {
-		result, err = cipher.Encrypt(content, associatedData)
-	} else if operation == "decrypt" {
-		result, err = cipher.Decrypt(content, associatedData)
-	}
-	if err != nil {
-		log.Fatalf("Failed to %s input file. Error: %v", operation, err)
-	}
-
-	// Write to output file
-	if err := ioutil.WriteFile(outputFilename, result, 0644); err != nil {
-		log.Fatalf("Failed to write result to output file. Error: %v", err)
-	}
-}
diff --git a/tools/testing/go/generate_envelope_keyset.go b/tools/testing/go/generate_envelope_keyset.go
deleted file mode 100644
index 24f77cd..0000000
--- a/tools/testing/go/generate_envelope_keyset.go
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright 2017 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package main
-
-import (
-	"bytes"
-	"io/ioutil"
-	"log"
-	"os"
-	"path"
-	"path/filepath"
-	"strings"
-
-	"flag"
-	// context is used to cancel outstanding requests
-	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/insecurecleartextkeyset"
-	"github.com/google/tink/go/integration/awskms"
-	"github.com/google/tink/go/integration/gcpkms"
-	"github.com/google/tink/go/keyset"
-
-	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
-)
-
-var (
-	gcpURI      = "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key"
-	gcpCredFile = filepath.Join(os.Getenv("TEST_SRCDIR"), "tools/testdata/gcp/credential.json")
-	awsURI      = "aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f"
-	awsCredFile = filepath.Join(os.Getenv("TEST_SRCDIR"), "tools/testdata/aws/credentials.csv")
-)
-
-func init() {
-	certPath := path.Join(os.Getenv("TEST_SRCDIR"), "tink_base/roots.pem")
-	flag.Set("cacerts", certPath)
-	os.Setenv("SSL_CERT_FILE", certPath)
-}
-
-func main() {
-	if len(os.Args) != 4 {
-		log.Fatalf("Usage: %s keyset-file kms dek-template", os.Args[0])
-	}
-	f := os.Args[1]
-	kms := os.Args[2]
-	dek := os.Args[3]
-	var dekT *tinkpb.KeyTemplate
-	var kh *keyset.Handle
-	var b bytes.Buffer
-	switch strings.ToUpper(dek) {
-	case "AES128_GCM":
-		dekT = aead.AES128GCMKeyTemplate()
-	case "AES128_CTR_HMAC_SHA256":
-		dekT = aead.AES128CTRHMACSHA256KeyTemplate()
-	default:
-		log.Fatalf("DEK template %s, is not supported. Expecting AES128_GCM or AES128_CTR_HMAC_SHA256", dek)
-	}
-	switch strings.ToUpper(kms) {
-	case "GCP":
-		gcpclient, err := gcpkms.NewClientWithCredentials(gcpURI, gcpCredFile)
-		if err != nil {
-			log.Fatal(err)
-		}
-		registry.RegisterKMSClient(gcpclient)
-		kh, err = keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(gcpURI, dekT))
-		if err != nil {
-			log.Fatal(err)
-		}
-	case "AWS":
-		awsclient, err := awskms.NewClientWithCredentials(awsURI, awsCredFile)
-		if err != nil {
-			log.Fatal(err)
-		}
-		registry.RegisterKMSClient(awsclient)
-		kh, err = keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(awsURI, dekT))
-		if err != nil {
-			log.Fatal(err)
-		}
-	default:
-		log.Fatalf("KMS %s, is not supported. Expecting AWS or GCP", kms)
-	}
-	ks := insecurecleartextkeyset.KeysetMaterial(kh)
-	h, err := insecurecleartextkeyset.Read(&keyset.MemReaderWriter{Keyset: ks})
-	if err != nil {
-		log.Fatal(err)
-	}
-	if err := insecurecleartextkeyset.Write(h, keyset.NewBinaryWriter(&b)); err != nil {
-		log.Fatal(err)
-	}
-	if err := ioutil.WriteFile(f, b.Bytes(), 0644); err != nil {
-		log.Fatal(err)
-	}
-}
diff --git a/tools/testing/go/public_key_sign_cli.go b/tools/testing/go/public_key_sign_cli.go
deleted file mode 100644
index 9554670..0000000
--- a/tools/testing/go/public_key_sign_cli.go
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2019 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for testing PublicKeySign-primitives.
-// It requires 3 arguments:
-//   keyset-file:  name of the file with the keyset to be used for signing
-//   message-file:  name of the file that contains message to be signed
-//   output-file:  name of the output file for the resulting plaintext
-package main
-
-import (
-	"io/ioutil"
-	"log"
-	"os"
-
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/signature"
-	"github.com/google/tink/go/testkeyset"
-)
-
-func main() {
-	if len(os.Args) != 4 {
-		log.Fatalf("Usage: %s keyset-file message-file output-file", os.Args[0])
-	}
-
-	keysetFilename := os.Args[1]
-	messageFilename := os.Args[2]
-	outputFilename := os.Args[3]
-
-	log.Printf("Using keyset from file %q to sign message in file %q.\nThe signature will be written to file %q\n",
-		keysetFilename, messageFilename, outputFilename)
-
-	// Read the keyset.
-	f, err := os.Open(keysetFilename)
-	if err != nil {
-		log.Fatalf("Opening the keyset file failed: %v\n", err)
-	}
-	reader := keyset.NewBinaryReader(f)
-	handle, err := testkeyset.Read(reader)
-	if err != nil {
-		log.Fatalf("Reading the keyset failed: %v\n", err)
-	}
-
-	// Get Primitive
-	signer, err := signature.NewSigner(handle)
-	if err != nil {
-		log.Fatalf("Failed to create primitive: %v\n", err)
-	}
-
-	// Read message
-	data, err := ioutil.ReadFile(messageFilename)
-	if err != nil {
-		log.Fatalf("Failed to read message: %v", err)
-	}
-
-	log.Printf("Signing...\n")
-	var result []byte
-	if result, err = signer.Sign(data); err != nil {
-		log.Fatalf("Error while signing: %v\n", err)
-	}
-
-	// Write to output file
-	if err := ioutil.WriteFile(outputFilename, result, 0644); err != nil {
-		log.Fatalf("Failed to write result to output file. Error: %v", err)
-	}
-}
diff --git a/tools/testing/go/public_key_verify_cli.go b/tools/testing/go/public_key_verify_cli.go
deleted file mode 100644
index 999bd30..0000000
--- a/tools/testing/go/public_key_verify_cli.go
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2019 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-// A command-line utility for testing PublicKeyVerify-primitives.
-// It requires 4 arguments:
-//   keyset-file:  name of the file with the keyset to be used for verification
-//   signature-file:  name of the file that contains the signature
-//   message-file:  name of the file that contains message that was signed
-//   output-file:  name of the output file for the verification result
-//                 (valid/invalid)
-package main
-
-import (
-	"io/ioutil"
-	"log"
-	"os"
-
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/signature"
-	"github.com/google/tink/go/testkeyset"
-)
-
-func main() {
-	if len(os.Args) != 5 {
-		log.Fatalf("Usage: %s keyset-file signature-file message-file output-file", os.Args[0])
-	}
-
-	keysetFilename := os.Args[1]
-	signatureFilename := os.Args[2]
-	messageFilename := os.Args[3]
-	outputFilename := os.Args[4]
-
-	log.Printf("Using keyset from file %q to verify signature from file %q of the message from file %q.\nThe verification result will be written to file %q\n",
-		keysetFilename, signatureFilename, messageFilename, outputFilename)
-
-	// Read the keyset.
-	f, err := os.Open(keysetFilename)
-	if err != nil {
-		log.Fatalf("Opening the keyset file failed: %v\n", err)
-	}
-	reader := keyset.NewBinaryReader(f)
-	handle, err := testkeyset.Read(reader)
-	if err != nil {
-		log.Fatalf("Reading the keyset failed: %v\n", err)
-	}
-
-	// Get Primitive
-	verifier, err := signature.NewVerifier(handle)
-	if err != nil {
-		log.Fatalf("Failed to create primitive: %v\n", err)
-	}
-
-	// Read message
-	data, err := ioutil.ReadFile(messageFilename)
-	if err != nil {
-		log.Fatalf("Failed to read message: %v", err)
-	}
-
-	// Read signature
-	sig, err := ioutil.ReadFile(signatureFilename)
-	if err != nil {
-		log.Fatalf("Failed to read signature: %v", err)
-	}
-
-	log.Printf("Verifying...\n")
-	result := []byte("valid")
-	if err := verifier.Verify(sig, data); err != nil {
-		log.Printf("Error while verifying the signature: %v\n", err)
-		result = []byte("invalid")
-	}
-
-	// Write to output file
-	if err := ioutil.WriteFile(outputFilename, result, 0644); err != nil {
-		log.Fatalf("Failed to write result to output file. Error: %v", err)
-	}
-}
diff --git a/tools/testing/java/com/google/crypto/tink/testing/AeadCli.java b/tools/testing/java/com/google/crypto/tink/testing/AeadCli.java
index 304f6bc..7f81b80 100644
--- a/tools/testing/java/com/google/crypto/tink/testing/AeadCli.java
+++ b/tools/testing/java/com/google/crypto/tink/testing/AeadCli.java
@@ -19,9 +19,6 @@
 import com.google.crypto.tink.Aead;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.aead.AeadConfig;
-import com.google.crypto.tink.integration.awskms.AwsKmsClient;
-import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
-import java.util.Optional;
 
 /**
  * A command-line utility for testing Aead-primitives. It requires 5 arguments: keyset-file: name of
@@ -44,8 +41,6 @@
     String associatedDataFile = args[3];
     String outputFilename = args[4];
 
-    GcpKmsClient.register(Optional.empty(), Optional.of(TestUtil.SERVICE_ACCOUNT_FILE));
-    AwsKmsClient.register(Optional.of(TestUtil.AWS_CRYPTO_URI), Optional.of(TestUtil.AWS_CREDS));
     AeadConfig.register();
 
     if (!(operation.equals("encrypt") || operation.equals("decrypt"))) {
diff --git a/tools/testing/java/com/google/crypto/tink/testing/CliUtil.java b/tools/testing/java/com/google/crypto/tink/testing/CliUtil.java
index 3af1774..cd9e051 100644
--- a/tools/testing/java/com/google/crypto/tink/testing/CliUtil.java
+++ b/tools/testing/java/com/google/crypto/tink/testing/CliUtil.java
@@ -16,23 +16,23 @@
 
 package com.google.crypto.tink.testing;
 
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.BinaryKeysetWriter;
-import com.google.crypto.tink.CleartextKeysetHandle;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
 import com.google.crypto.tink.hybrid.HybridConfig;
+import com.google.crypto.tink.keyderivation.KeyDerivationConfig;
 import com.google.crypto.tink.prf.PrfConfig;
 import com.google.crypto.tink.signature.SignatureConfig;
 import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
 import java.io.ByteArrayOutputStream;
-import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
 
@@ -47,13 +47,17 @@
   public static KeysetHandle readKeyset(String filename)
       throws GeneralSecurityException, IOException {
     System.out.println("Reading the keyset...");
-    return CleartextKeysetHandle.read(BinaryKeysetReader.withFile(new File(filename)));
+    return TinkProtoKeysetFormat.parseKeyset(
+        Files.readAllBytes(Paths.get(filename)), InsecureSecretKeyAccess.get());
   }
 
   /** Writes a keyset to the specified file. In case of errors throws an exception. */
-  public static void writeKeyset(KeysetHandle handle, String filename) throws IOException {
+  public static void writeKeyset(KeysetHandle handle, String filename)
+      throws IOException, GeneralSecurityException {
     System.out.println("Writing the keyset...");
-    CleartextKeysetHandle.write(handle, BinaryKeysetWriter.withFile(new File(filename)));
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get());
+    Files.write(Paths.get(filename), serializedKeyset);
   }
 
   /**
@@ -66,7 +70,7 @@
     PrfConfig.register();
     SignatureConfig.register();
     StreamingAeadConfig.register();
-    // place holder for KeyderivationConfig. DO NOT EDIT.
+    KeyDerivationConfig.register();
   }
 
   /**
diff --git a/tools/testing/java/com/google/crypto/tink/testing/CompareKeysets.java b/tools/testing/java/com/google/crypto/tink/testing/CompareKeysets.java
deleted file mode 100644
index c19c8de..0000000
--- a/tools/testing/java/com/google/crypto/tink/testing/CompareKeysets.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.testing;
-
-import com.google.crypto.tink.PrivilegedRegistry;
-import com.google.crypto.tink.proto.Keyset;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Provides a function {@link compareKeysets} which can compare two keysets.
- */
-final class CompareKeysets {
-  // Compares two keys of a keyset. We parse the KeyData in each keyset in order to ensure we do not
-  // depend strongly on the serialization order of the tags; note that keysets may be serialized
-  // in different languages, and assuming that the fields are ordered in the same way when generated
-  // in each language seems relatively strong.
-  private static boolean equalKeys(Keyset.Key key1, Keyset.Key key2) throws Exception {
-    if (!key1.getStatus().equals(key2.getStatus())) {
-      return false;
-    }
-    if (key1.getKeyId() != key2.getKeyId()) {
-      return false;
-    }
-    if (!key1.getOutputPrefixType().equals(key2.getOutputPrefixType())) {
-      return false;
-    }
-    if (!key1.getKeyData().getKeyMaterialType().equals(key2.getKeyData().getKeyMaterialType())) {
-      return false;
-    }
-    if (!PrivilegedRegistry.parseKeyData(key1.getKeyData())
-        .equals(PrivilegedRegistry.parseKeyData(key2.getKeyData()))) {
-      return false;
-    }
-    return true;
-  }
-
-  /**
-   * Finds a key in {@code keyList} for which {@link equalKeys} returns true, removes it, and
-   * returns true. If no such key exists, returns false.
-   */
-  private static boolean findAndRemove(Keyset.Key key, List<Keyset.Key> keyList) throws Exception {
-    for (Keyset.Key key2 : keyList) {
-      if (equalKeys(key, key2)) {
-        keyList.remove(key2);
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private static void compareKeysetLists(List<Keyset.Key> keyList1, List<Keyset.Key> keyList2)
-      throws Exception {
-    for (Keyset.Key key1 : keyList1) {
-      if (!findAndRemove(key1, keyList2)) {
-        throw new IllegalArgumentException("Key " + key1 + " not found in second keyset.");
-      }
-    }
-  }
-
-  /**
-   * Collects all keys with a fixed key id in a {@link List}, returning the result in a map from
-   * the key-id to this list.
-   */
-  private static Map<Integer, List<Keyset.Key>> getKeyDataMap(Keyset keyset) {
-    Map<Integer, List<Keyset.Key>> result = new HashMap<>();
-    for (int i = 0; i < keyset.getKeyCount(); ++i) {
-      Keyset.Key key = keyset.getKey(i);
-      if (!result.containsKey(key.getKeyId())) {
-        result.put(key.getKeyId(), new ArrayList<Keyset.Key>());
-      }
-      result.get(key.getKeyId()).add(key);
-    }
-    return result;
-  }
-
-  /**
-   * Compares two keysets, throws some exception if the keyset are different.
-   *
-   * <p>If the keysets are different, this is guaranteed to throw an exception. If they are the same
-   * there are several possibilities why this will still throw an exception:
-   *
-   * <ul>
-   *   <li>There is no key manager registered for one of the Keys used in the keyset
-   *   <li>
-   * </ul>
-   */
-  public static void compareKeysets(Keyset keyset1, Keyset keyset2) throws Exception {
-    if (keyset1.getPrimaryKeyId() != keyset2.getPrimaryKeyId()) {
-      throw new IllegalArgumentException(
-          "Given keysets contain different key ids. \n\nKeyset 1: "
-              + keyset1
-              + "\n\nKeyset2: "
-              + keyset2);
-    }
-    if (keyset1.getKeyCount() != keyset2.getKeyCount()) {
-      throw new IllegalArgumentException(
-          "Given keysets contain different number of keys. \n\nKeyset 1: "
-              + keyset1
-              + "\n\nKeyset2: "
-              + keyset2);
-    }
-
-    // The order of the keys in the keyset is considered irrelevant.
-    Map<Integer, List<Keyset.Key>> keyset1Map = getKeyDataMap(keyset1);
-    Map<Integer, List<Keyset.Key>> keyset2Map = getKeyDataMap(keyset2);
-    for (Map.Entry<Integer, List<Keyset.Key>> idToList : keyset1Map.entrySet()) {
-      if (!keyset2Map.containsKey(idToList.getKey())) {
-        throw new IllegalArgumentException(
-            "Keysets differ; the second one contains no key with id "
-                + idToList.getKey()
-                + ", but the first one does. \n\nKeyset 1: "
-                + keyset1
-                + "\n\nKeyset 2:"
-                + keyset2);
-      }
-      compareKeysetLists(keyset2Map.get(idToList.getKey()), idToList.getValue());
-    }
-  }
-
-  private CompareKeysets() {
-  }
-}
diff --git a/tools/testing/javatests/com/google/crypto/tink/testing/BUILD.bazel b/tools/testing/javatests/com/google/crypto/tink/testing/BUILD.bazel
deleted file mode 100644
index 33adfbc..0000000
--- a/tools/testing/javatests/com/google/crypto/tink/testing/BUILD.bazel
+++ /dev/null
@@ -1,16 +0,0 @@
-licenses(["notice"])
-
-package(default_visibility = ["//:__subpackages__"])
-
-java_test(
-    name = "CompareKeysetsTest",
-    size = "small",
-    srcs = ["CompareKeysetsTest.java"],
-    deps = [
-        "//testing:compare_keysets",
-        "@maven//:junit_junit",
-        "@tink_java//proto:tink_java_proto",
-        "@tink_java//src/main/java/com/google/crypto/tink/aead:aes_gcm_key_manager",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
-    ],
-)
diff --git a/tools/testing/javatests/com/google/crypto/tink/testing/CompareKeysetsTest.java b/tools/testing/javatests/com/google/crypto/tink/testing/CompareKeysetsTest.java
deleted file mode 100644
index 895a5bb..0000000
--- a/tools/testing/javatests/com/google/crypto/tink/testing/CompareKeysetsTest.java
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.testing;
-
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.aead.AesGcmKeyManager;
-import com.google.crypto.tink.proto.KeyData;
-import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
-import com.google.crypto.tink.proto.KeyStatusType;
-import com.google.crypto.tink.proto.Keyset;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public final class CompareKeysetsTest {
-  @BeforeClass
-  public static void registerAesGcm() throws Exception {
-    AesGcmKeyManager.register(true);
-  }
-
-  private static final byte[] KEY_0 = TestUtil.hexDecode("000102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY_1 = TestUtil.hexDecode("100102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY_2 = TestUtil.hexDecode("200102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY_3 = TestUtil.hexDecode("300102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY_4 = TestUtil.hexDecode("400102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY_5 = TestUtil.hexDecode("500102030405060708090a0b0c0d0e0f");
-  private static final byte[] KEY_6 = TestUtil.hexDecode("600102030405060708090a0b0c0d0e0f");
-
-  private static Keyset.Key aesGcmKey(
-      byte[] keyValue, int keyId, KeyStatusType status, OutputPrefixType prefixType)
-      throws Exception {
-    return TestUtil.createKey(TestUtil.createAesGcmKeyData(keyValue), keyId, status, prefixType);
-  }
-
-  @Test
-  public void testCompareKeysets_emptyKeysets_equal() throws Exception {
-    CompareKeysets.compareKeysets(Keyset.getDefaultInstance(),
-        Keyset.getDefaultInstance());
-  }
-
-  @Test
-  public void testCompareKeysets_singleKey_equal() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    CompareKeysets.compareKeysets(keyset1, keyset2);
-  }
-
-  @Test
-  public void testCompareKeysets_twoKeys_equal() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    CompareKeysets.compareKeysets(keyset1, keyset2);
-  }
-
-  @Test
-  public void testCompareKeysets_twoKeysDifferentOrder_equal() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    CompareKeysets.compareKeysets(keyset1, keyset2);
-  }
-
-  @Test
-  public void testCompareKeysets_twoKeysDifferentPrimary_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(18)
-            .build();
-    try {
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-      fail();
-    } catch (Exception e) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testCompareKeysets_singleKeyDifferentStatus_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.DISABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    try {
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-      fail();
-    } catch (Exception e) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testCompareKeysets_singleKeyDifferentOutputPrefix_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.RAW))
-            .setPrimaryKeyId(17)
-            .build();
-    try {
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-      fail();
-    } catch (Exception e) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testCompareKeysets_singleKeyDifferentKeyMaterial_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset.Key key = keyset1.getKey(0);
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(
-                Keyset.Key.newBuilder(key)
-                    .setKeyData(
-                        KeyData.newBuilder(key.getKeyData())
-                            .setKeyMaterialType(KeyMaterialType.UNKNOWN_KEYMATERIAL)))
-            .setPrimaryKeyId(17)
-            .build();
-    try {
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-      fail();
-    } catch (Exception e) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testCompareKeysets_differentKeyIdButRawOutputPrefix_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 18, KeyStatusType.ENABLED, OutputPrefixType.RAW))
-            .setPrimaryKeyId(18)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.RAW))
-            .setPrimaryKeyId(17)
-            .build();
-    try {
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-      fail();
-    } catch (Exception e) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testCompareKeysets_sameKeysSameIdsDifferentOrder_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_2, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_3, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_4, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_5, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_6, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_3, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_2, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_5, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_4, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_6, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-  }
-
-  @Test
-  public void testCompareKeysets_differentKeysSameIdsSimlarOrder_throws() throws Exception {
-    Keyset keyset1 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_2, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_3, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_4, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_5, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    Keyset keyset2 =
-        Keyset.newBuilder()
-            .addKey(aesGcmKey(KEY_0, 17, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_1, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_2, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_6, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK)) // != KEY_3
-            .addKey(aesGcmKey(KEY_4, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .addKey(aesGcmKey(KEY_5, 18, KeyStatusType.ENABLED, OutputPrefixType.TINK))
-            .setPrimaryKeyId(17)
-            .build();
-    try {
-      CompareKeysets.compareKeysets(keyset1, keyset2);
-      fail();
-    } catch (Exception e) {
-      // expected.
-    }
-  }
-}
diff --git a/tools/testing/python/BUILD.bazel b/tools/testing/python/BUILD.bazel
deleted file mode 100644
index ec5aa93..0000000
--- a/tools/testing/python/BUILD.bazel
+++ /dev/null
@@ -1,23 +0,0 @@
-load("@rules_python//python:defs.bzl", "py_binary")
-load("@pip_deps//:requirements.bzl", "requirement")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-py_binary(
-    name = "aead_cli_python",
-    testonly = 1,
-    srcs = ["aead_cli.py"],
-    main = "aead_cli.py",
-    python_version = "PY3",
-    srcs_version = "PY3",
-    deps = [
-        requirement("absl-py"),
-        "@tink_py//tink:cleartext_keyset_handle",
-        "@tink_py//tink:tink_python",
-        "@tink_py//tink/aead",
-        "@tink_py//tink/integration/awskms",
-        "@tink_py//tink/integration/gcpkms",
-    ],
-)
diff --git a/tools/testing/python/aead_cli.py b/tools/testing/python/aead_cli.py
deleted file mode 100644
index 2eb8c76..0000000
--- a/tools/testing/python/aead_cli.py
+++ /dev/null
@@ -1,145 +0,0 @@
-# Copyright 2019 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS-IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""A command-line utility for testing AEAD-primitives.
-
-It requires 5 arguments:
-  keyset-file:  name of the file with the keyset to be used for encryption
-  operation: the actual AEAD-operation, i.e. "encrypt" or "decrypt"
-  input-file:  name of the file with input (plaintext for encryption, or
-               or ciphertext for decryption)
-  associated-data-file:  name of the file containing associated data
-  output-file:  name of the file for the resulting output
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import os
-# Special imports
-from absl import app
-from absl import flags
-from absl import logging
-import tink
-
-from tink import aead
-from tink import cleartext_keyset_handle
-from tink.integration import awskms
-from tink.integration import gcpkms
-
-
-FLAGS = flags.FLAGS
-AWS_CREDENTIAL_PATH = os.path.join(os.environ['TEST_SRCDIR'],
-                                   'tools/testdata/aws/credentials.ini')
-AWS_KEY_URI = 'aws-kms://arn:aws:kms:us-east-2:235739564943:key/3ee50705-5a82-4f5b-9753-05c4f473922f'
-GCP_CREDENTIAL_PATH = os.path.join(os.environ['TEST_SRCDIR'],
-                                   'tools/testdata/gcp/credential.json')
-GCP_KEY_URI = 'gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key'
-
-
-def read_keyset(keyset_filename):
-  """Load a keyset from a file.
-
-  Args:
-    keyset_filename: A path to a keyset file
-
-  Returns:
-    A KeysetHandle of the file's keyset
-  Raises:
-    TinkError: if the file is not valid
-    IOError: if the file does not exist
-  """
-  with open(keyset_filename, 'rb') as keyset_file:
-    text = keyset_file.read()
-    keyset = cleartext_keyset_handle.read(tink.BinaryKeysetReader(text))
-  return keyset
-
-
-def main(argv):
-  if len(argv) != 6:
-    raise app.UsageError(
-        'Expected 5 arguments, got %d.\n'
-        'Usage: %s keyset-file operation input-file associated-data-file' %
-        (len(argv) - 1, argv[0]))
-
-  keyset_filename = argv[1]
-  operation = argv[2]
-  input_filename = argv[3]
-  associated_data_filename = argv[4]
-  output_filename = argv[5]
-
-  logging.info(
-      'Using keyset from file %s to AEAD-%s file %s with associated data '
-      'from file %s.\nThe resulting output will be written to file %s',
-      keyset_filename, operation, input_filename, associated_data_filename,
-      output_filename)
-
-  # Initialise Tink
-  try:
-    aead.register()
-  except tink.TinkError as e:
-    logging.error('Error initialising Tink: %s', e)
-    return 1
-
-  # Initialize KMS clients
-  awskms.AwsKmsClient.register_client(AWS_KEY_URI, AWS_CREDENTIAL_PATH)
-  gcpkms.GcpKmsClient.register_client(GCP_KEY_URI, GCP_CREDENTIAL_PATH)
-
-  # Read the keyset into keyset_handle
-  try:
-    keyset_handle = read_keyset(keyset_filename)
-  except tink.TinkError as e:
-    logging.error('Error reading key: %s', e)
-    return 1
-
-  # Get the primitive
-  try:
-    cipher = keyset_handle.primitive(aead.Aead)
-  except tink.TinkError as e:
-    logging.error('Error creating primitive: %s', e)
-    return 1
-
-  # Read the input files
-  with open(input_filename, 'rb') as input_file:
-    input_data = input_file.read()
-  with open(associated_data_filename, 'rb') as associated_data_file:
-    aad = associated_data_file.read()
-
-  # Compute the output
-  if operation.lower() == 'encrypt':
-    try:
-      output_data = cipher.encrypt(input_data, aad)
-    except tink.TinkError as e:
-      logging.error('Error encrypting the input: %s', e)
-      return 1
-  elif operation.lower() == 'decrypt':
-    try:
-      output_data = cipher.decrypt(input_data, aad)
-    except tink.TinkError as e:
-      logging.error('Error decrypting the input: %s', e)
-      return 1
-  else:
-    logging.error(
-        'Did not recognise operation %s.\n'
-        'Expected either "encrypt" or "decrypt"', operation)
-    return 1
-
-  with open(output_filename, 'wb') as output_file:
-    output_file.write(output_data)
-
-  logging.info('All done.')
-
-
-if __name__ == '__main__':
-  app.run(main)
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/AddKeyCommand.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/AddKeyCommand.java
index 495eff1..e08b45c 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/AddKeyCommand.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/AddKeyCommand.java
@@ -50,7 +50,14 @@
       InputStream inputStream, String inFormat,
       String masterKeyUri, String credentialPath,
       KeyTemplate keyTemplate) throws GeneralSecurityException, IOException {
-    TinkeyUtil.createKey(TinkeyUtil.CommandType.ADD_KEY, outputStream, outFormat,
-        inputStream, inFormat, masterKeyUri, credentialPath, keyTemplate);
+    TinkeyUtil.createKey(
+        TinkeyUtil.CommandType.ADD_KEY,
+        outputStream,
+        outFormat,
+        inputStream,
+        inFormat,
+        masterKeyUri,
+        credentialPath,
+        keyTemplate.toParameters());
   }
 }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/BUILD.bazel b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/BUILD.bazel
index bcebc50..2ba7e06 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/BUILD.bazel
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/BUILD.bazel
@@ -37,12 +37,16 @@
     name = "tinkey",
     srcs = ["Tinkey.java"],
     deps = [
+        ":kms_clients_factory",
         ":tinkey_commands",
         "@maven//:args4j_args4j",
         "@tink_java//src/main/java/com/google/crypto/tink/daead:deterministic_aead_config",
         "@tink_java//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_client",
+        "@tink_java//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_mac_config",
         "@tink_java//src/main/java/com/google/crypto/tink/jwt:jwt_signature_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_config",
         "@tink_java//src/main/java/com/google/crypto/tink/prf:prf_config",
         "@tink_java//src/main/java/com/google/crypto/tink/signature:signature_config",
         "@tink_java//src/main/java/com/google/crypto/tink/streamingaead:streaming_aead_config",
@@ -62,6 +66,7 @@
         ":destroy_key_command",
         ":disable_key_command",
         ":enable_key_command",
+        ":help_command",
         ":list_key_templates_command",
         ":list_keyset_command",
         ":promote_key_command",
@@ -106,25 +111,15 @@
 java_library(
     name = "tinkey_util",
     srcs = ["TinkeyUtil.java"],
-    runtime_deps = [
-        # Tinkey automatically loads these KMS clients at runtime.
-        "@tink_java//src/main/java/com/google/crypto/tink/integration/awskms:aws_kms_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/integration/gcpkms:gcp_kms_client",
-    ],
     deps = [
-        "@com_google_protobuf//:protobuf_javalite",
-        "@tink_java//proto:tink_java_proto",
+        ":kms_clients_factory",
         "@tink_java//src/main/java/com/google/crypto/tink:aead",
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:binary_keyset_writer",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:json_keyset_writer",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
         "@tink_java//src/main/java/com/google/crypto/tink:key_template",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_writer",
-        "@tink_java//src/main/java/com/google/crypto/tink:kms_clients",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:parameters",
     ],
 )
 
@@ -166,7 +161,6 @@
         ":command",
         ":out_options",
         ":tinkey_util",
-        "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
         "@tink_java//src/main/java/com/google/crypto/tink:keyset_writer",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
     ],
@@ -297,5 +291,44 @@
     deps = [
         "@tink_java//src/main/java/com/google/crypto/tink:key_template",
         "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
+        "@tink_java//src/main/java/com/google/crypto/tink/keyderivation:key_derivation_key_templates",
+    ],
+)
+
+java_library(
+    name = "help_command",
+    srcs = ["HelpCommand.java"],
+    deps = [":command"],
+)
+
+java_library(
+    name = "tinkey_test_kms_client",
+    testonly = 1,
+    srcs = ["TinkeyTestKmsClient.java"],
+    plugins = [":auto_service_plugin"],
+    deps = [
+        "@maven//:com_google_auto_service_auto_service_annotations",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_client",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+    ],
+)
+
+java_plugin(
+    name = "auto_service_plugin",
+    processor_class = "com.google.auto.service.processor.AutoServiceProcessor",
+    deps = [
+        "@maven//:com_google_auto_auto_common",
+        "@maven//:com_google_auto_service_auto_service",
+    ],
+)
+
+java_library(
+    name = "kms_clients_factory",
+    srcs = ["KmsClientsFactory.java"],
+    deps = [
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_client",
     ],
 )
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ConvertKeysetOptions.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ConvertKeysetOptions.java
index 2733a6f..df41d42 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ConvertKeysetOptions.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ConvertKeysetOptions.java
@@ -34,14 +34,10 @@
   String newCredentialPath;
 
   @Override
-  void validate() {
+  void validate() throws IOException {
     super.validate();
     if (newCredentialPath != null) {
-      try {
-        Validators.validateExists(new File(newCredentialPath));
-      } catch (IOException e) {
-        TinkeyUtil.die(e.toString());
-      }
+      Validators.validateExists(new File(newCredentialPath));
     }
   }
 }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommand.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommand.java
index 613faa0..50bea20 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommand.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommand.java
@@ -16,9 +16,7 @@
 
 package com.google.crypto.tink.tinkey;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetWriter;
 import java.io.InputStream;
 import java.io.OutputStream;
 
@@ -44,9 +42,6 @@
       throws Exception {
     KeysetHandle handle = TinkeyUtil.getKeysetHandle(inputStream, inFormat, masterKeyUri,
         credentialPath);
-    KeysetWriter writer = TinkeyUtil.createKeysetWriter(outputStream, outFormat);
-    CleartextKeysetHandle.write(
-        handle.getPublicKeysetHandle(),
-        writer);
+    TinkeyUtil.writeKeyset(handle.getPublicKeysetHandle(), outputStream, outFormat, null, null);
   }
 }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/HelpCommand.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/HelpCommand.java
new file mode 100644
index 0000000..627d02b
--- /dev/null
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/HelpCommand.java
@@ -0,0 +1,41 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.tinkey;
+
+/** Outputs a help message. */
+public class HelpCommand implements Command {
+  @Override
+  public void run() throws Exception {
+    System.out.println("Tinkey supports the following commands:");
+    System.out.println("  help:                 Prints this help message.");
+    System.out.println("  add-key:              Generates and adds a new key to a keyset.");
+    System.out.println("  convert-keyset:       Changes format, encrypts, decrypts a keyset.");
+    System.out.println("  create-keyset:        Creates a new keyset.");
+    System.out.println("  create-public-keyset: Creates a public keyset from a private keyset.");
+    System.out.println("  list-key-templates:   Lists all supported key templates.");
+    System.out.println("  delete-key:           Deletes a specified key in a keyset.");
+    System.out.println(
+        "  destroy-key:          Destroys the key material of a specified key in a keyset.");
+    System.out.println("  disable-key:          Disables a specified key in a keyset.");
+    System.out.println("  enable-key:           Enables a specified key in a keyset.");
+    System.out.println("  list-keyset:          Lists keys in a keyset.");
+    System.out.println("  promote-key:          Promotes a specified key to primary.");
+    System.out.println(
+        "  rotate-keyset:        Deprecated. Rotate keysets in two steps using the commands"
+            + " 'add-key' and later 'promote-key'.");
+  }
+}
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/InOptions.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/InOptions.java
index 2494150..237aa4c 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/InOptions.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/InOptions.java
@@ -18,6 +18,7 @@
 
 import com.google.crypto.tink.subtle.Validators;
 import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
 import org.kohsuke.args4j.Option;
 
@@ -69,17 +70,13 @@
       )
   String credentialPath;
 
-  void validate() {
+  void validate() throws IOException {
     if (inputStream == null) {
       inputStream = System.in;
     }
-    try {
-      TinkeyUtil.validateFormat(inFormat);
-      if (credentialPath != null) {
-        Validators.validateExists(new File(credentialPath));
-      }
-    } catch (Exception e) {
-      TinkeyUtil.die(e.toString());
+    TinkeyUtil.validateFormat(inFormat);
+    if (credentialPath != null) {
+      Validators.validateExists(new File(credentialPath));
     }
   }
 }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/KmsClientsFactory.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/KmsClientsFactory.java
new file mode 100644
index 0000000..e2a4446
--- /dev/null
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/KmsClientsFactory.java
@@ -0,0 +1,65 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.tinkey;
+
+import com.google.crypto.tink.KmsClient;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Allows creating {@link KmsClient} objects.
+ *
+ * <p>Tink {@link KmsClient} which are registered have the really unfortunate property that they are
+ * modifiable. For Tinkey this should never be a problem, since one always uses the client only
+ * once. However, for testing Tinkey it is a problem. Hence, we avoid the class and simply use this
+ * one here instead.
+ */
+final class KmsClientsFactory {
+
+  private List<Supplier<KmsClient>> factories = new ArrayList<>();
+
+  /** Create a new KmsClientsFactory without any registered factory. */
+  public KmsClientsFactory() {}
+
+  private static final KmsClientsFactory globalInstance = new KmsClientsFactory();
+
+  /** A unique global instance. */
+  public static KmsClientsFactory globalInstance() {
+    return globalInstance;
+  }
+
+  /**
+   * Enumerates all registered factories, creates a new client for each, and returns one if it
+   * supports keyUri.
+   */
+  public KmsClient newClientFor(String keyUri) throws GeneralSecurityException {
+    for (Supplier<KmsClient> factory : factories) {
+      KmsClient client = factory.get();
+      if (client.doesSupport(keyUri)) {
+        return client;
+      }
+    }
+    throw new GeneralSecurityException("Unable to find factory for keyUri: " + keyUri);
+  }
+
+  /** Registers an additional factory. */
+  public void addFactory(Supplier<KmsClient> factory) {
+    factories.add(factory);
+  }
+}
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ListKeyTemplatesCommand.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ListKeyTemplatesCommand.java
index d3199bb..b4eadeb 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ListKeyTemplatesCommand.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/ListKeyTemplatesCommand.java
@@ -17,6 +17,9 @@
 package com.google.crypto.tink.tinkey;
 
 import com.google.crypto.tink.Registry;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /** Creates a new {@link com.google.crypto.tink.proto.KeyTemplate}. */
 public class ListKeyTemplatesCommand implements Command {
@@ -24,10 +27,11 @@
   @Override
   public void run() throws Exception {
     System.out.println("The following key templates are supported:");
-    for (String name : Registry.keyTemplates()) {
-      System.out.println(name);
-    }
-    for (String name : TinkeyKeyTemplates.get().keySet()) {
+    List<String> keyTemplates = new ArrayList<>();
+    keyTemplates.addAll(Registry.keyTemplates());
+    keyTemplates.addAll(TinkeyKeyTemplates.get().keySet());
+    Collections.sort(keyTemplates);
+    for (String name : keyTemplates) {
       System.out.println(name);
     }
   }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/OutOptions.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/OutOptions.java
index 16a59fd..cf7080d 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/OutOptions.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/OutOptions.java
@@ -16,6 +16,7 @@
 
 package com.google.crypto.tink.tinkey;
 
+import java.io.IOException;
 import java.io.OutputStream;
 import org.kohsuke.args4j.Option;
 
@@ -40,15 +41,11 @@
   String outFormat;
 
   @Override
-  void validate() {
+  void validate() throws IOException {
     super.validate();
     if (outputStream == null) {
       outputStream = System.out;
     }
-    try {
-      TinkeyUtil.validateFormat(outFormat);
-    } catch (Exception e) {
-      TinkeyUtil.die(e.toString());
-    }
+    TinkeyUtil.validateFormat(outFormat);
   }
 }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/RotateKeysetCommand.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/RotateKeysetCommand.java
index 4963dea..b330431 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/RotateKeysetCommand.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/RotateKeysetCommand.java
@@ -42,7 +42,14 @@
       InputStream inputStream, String inFormat,
       String masterKeyUri, String credentialPath,
       KeyTemplate keyTemplate) throws Exception {
-    TinkeyUtil.createKey(TinkeyUtil.CommandType.ROTATE_KEYSET, outputStream, outFormat,
-        inputStream, inFormat, masterKeyUri, credentialPath, keyTemplate);
+    TinkeyUtil.createKey(
+        TinkeyUtil.CommandType.ROTATE_KEYSET,
+        outputStream,
+        outFormat,
+        inputStream,
+        inFormat,
+        masterKeyUri,
+        credentialPath,
+        keyTemplate.toParameters());
   }
 }
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/Tinkey.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/Tinkey.java
index 1bc7c7f..f3fc794 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/Tinkey.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/Tinkey.java
@@ -18,8 +18,11 @@
 
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
 import com.google.crypto.tink.hybrid.HybridConfig;
+import com.google.crypto.tink.integration.awskms.AwsKmsClient;
+import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
 import com.google.crypto.tink.jwt.JwtMacConfig;
 import com.google.crypto.tink.jwt.JwtSignatureConfig;
+import com.google.crypto.tink.keyderivation.KeyDerivationConfig;
 import com.google.crypto.tink.prf.PrfConfig;
 import com.google.crypto.tink.signature.SignatureConfig;
 import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
@@ -38,8 +41,12 @@
     StreamingAeadConfig.register();
     JwtSignatureConfig.register();
     JwtMacConfig.register();
-    // place holder for KeyderivationConfig. DO NOT EDIT.
+    KeyDerivationConfig.register();
     // place holder for Internal Prps. DO NOT EDIT.
+
+    KmsClientsFactory.globalInstance().addFactory(AwsKmsClient::new);
+    KmsClientsFactory.globalInstance().addFactory(GcpKmsClient::new);
+
     TinkeyCommands commands = new TinkeyCommands();
     CmdLineParser parser = new CmdLineParser(commands);
 
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyCommands.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyCommands.java
index e2c6f1e..75ed9ed 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyCommands.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyCommands.java
@@ -26,12 +26,12 @@
  */
 public final class TinkeyCommands {
   @Argument(
-    metaVar = "command",
-    required = true,
-    handler = SubCommandHandler.class,
-    usage = "Command to run"
-  )
+      metaVar = "command",
+      required = true,
+      handler = SubCommandHandler.class,
+      usage = "Command to run")
   @SubCommands({
+    @SubCommand(name = "help", impl = HelpCommand.class),
     @SubCommand(name = "add-key", impl = AddKeyCommand.class),
     @SubCommand(name = "convert-keyset", impl = ConvertKeysetCommand.class),
     @SubCommand(name = "create-keyset", impl = CreateKeysetCommand.class),
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyKeyTemplates.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyKeyTemplates.java
index 8227ad0..248d758 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyKeyTemplates.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyKeyTemplates.java
@@ -18,22 +18,58 @@
 
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeyTemplates;
-// place-holder for PrfBasedDeriverKeyManager. DO NOT EDIT
+import com.google.crypto.tink.keyderivation.KeyDerivationKeyTemplates;
 import java.security.GeneralSecurityException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 /**
- * This class contains key templates that cannot stay in Tink, such as those that break Tink's
- * modularity because they depend on other key templates.
+ * Contains key templates that should be part of Tinkey but are not defined in the corresponding
+ * {@link KeyTypeManager#keyFormats()}.
+ *
+ * <p>For example, key template {@code HKDF_SHA256_DERIVES_AES128_GCM} requires key template {@code
+ * AES128_GCM} to exist in the {@link Registry}, which requires {@link AeadConfig#register()} to
+ * have been called. Since {@link KeyDerivationConfig} does not automatically call {@link
+ * AeadConfig#register()}, {@code HKDF_SHA256_DERIVES_AES128_GCM} cannot be defined in {@link
+ * PrfBasedDeriverKeyManager#keyFormats()}.
  */
 final class TinkeyKeyTemplates {
 
   public static final Map<String, KeyTemplate> get() throws GeneralSecurityException {
     Map<String, KeyTemplate> result = new HashMap<>();
-    // Keep synchronized with copybara/java.bara.sky
-    // place-holder for PRF-based key templates. DO NOT EDIT
+    result.put(
+        "HKDF_SHA256_DERIVES_AES128_GCM",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES128_GCM")));
+    result.put(
+        "HKDF_SHA256_DERIVES_AES256_GCM",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES256_GCM")));
+    result.put(
+        "HKDF_SHA256_DERIVES_HMAC_SHA256_128BITTAG",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("HMAC_SHA256_128BITTAG")));
+    result.put(
+        "HKDF_SHA256_DERIVES_HMAC_SHA256_PRF",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("HMAC_SHA256_PRF")));
+    result.put(
+        "HKDF_SHA256_DERIVES_AES256_GCM_HKDF_1MB",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES256_GCM_HKDF_1MB")));
+    result.put(
+        "HKDF_SHA256_DERIVES_ED25519",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("ED25519")));
+    result.put(
+        "HKDF_SHA256_DERIVES_XCHACHA20_POLY1305",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("XCHACHA20_POLY1305")));
+    result.put(
+        "HKDF_SHA256_DERIVES_AES256_SIV",
+        KeyDerivationKeyTemplates.createPrfBasedKeyTemplate(
+            KeyTemplates.get("HKDF_SHA256"), KeyTemplates.get("AES256_SIV")));
     return Collections.unmodifiableMap(result);
   }
 
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyTestKmsClient.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyTestKmsClient.java
new file mode 100644
index 0000000..53d4d99
--- /dev/null
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyTestKmsClient.java
@@ -0,0 +1,109 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package com.google.crypto.tink.tinkey;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KmsClient;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * A client for testing.
+ *
+ * <p>The client supports all key uris which start with "tinkey-test-kms-client://" followed by a
+ * JSON-encoded AEAD keyset. It will use this Aead when "getAead" is called. As credentials, it must
+ * be given a file which starts whose contents are "VALID CREDENTIALS".
+ */
+final class TinkeyTestKmsClient implements KmsClient {
+
+  TinkeyTestKmsClient() {
+    this(PREFIX);
+  }
+
+  private TinkeyTestKmsClient(String prefix) {
+    this.prefix = prefix;
+  }
+
+  static KmsClient createForPrefix(String prefix) {
+    return new TinkeyTestKmsClient(prefix);
+  }
+
+  private final String prefix;
+  private static final String PREFIX = "tinkey-test-kms-client://";
+  private static final String CREDENTIALS_FILE_CONTENTS = "VALID CREDENTIALS";
+
+  static String createKeyUri(KeysetHandle handle) throws GeneralSecurityException {
+    return PREFIX
+        + TinkJsonProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get());
+  }
+
+  static void createCredentialFile(Path path) throws IOException {
+    Files.write(path, CREDENTIALS_FILE_CONTENTS.getBytes(UTF_8));
+  }
+
+  private static String stripPrefix(String prefix, String str) throws GeneralSecurityException {
+    if (!str.startsWith(prefix)) {
+      throw new GeneralSecurityException("Invalid key uri: " + str);
+    }
+    return str.substring(prefix.length());
+  }
+
+  @Override
+  public boolean doesSupport(String keyUri) {
+    return keyUri.startsWith(prefix);
+  }
+
+  byte[] credentialFileContents = new byte[] {};
+
+  @Override
+  public KmsClient withCredentials(String credentials) throws GeneralSecurityException {
+    try {
+      credentialFileContents = Files.readAllBytes(Paths.get(credentials));
+      return this;
+    } catch (IOException e) {
+      throw new GeneralSecurityException(e);
+    }
+  }
+
+  @Override
+  public KmsClient withDefaultCredentials() throws GeneralSecurityException {
+    throw new GeneralSecurityException("TinkeyTestKmsClient has no default credentials");
+  }
+
+  private void checkCredentials() throws GeneralSecurityException {
+    if (!Arrays.equals(credentialFileContents, CREDENTIALS_FILE_CONTENTS.getBytes(UTF_8))) {
+      throw new GeneralSecurityException(
+          "Invalid credentials: " + Arrays.toString(credentialFileContents));
+    }
+  }
+
+  @Override
+  public Aead getAead(String keyUri) throws GeneralSecurityException {
+    checkCredentials();
+    String keyset = stripPrefix(prefix, keyUri);
+    return TinkJsonProtoKeysetFormat.parseKeyset(keyset, InsecureSecretKeyAccess.get())
+        .getPrimitive(Aead.class);
+  }
+}
diff --git a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java
index 27f708d..0ae09d8 100644
--- a/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java
+++ b/tools/tinkey/src/main/java/com/google/crypto/tink/tinkey/TinkeyUtil.java
@@ -16,26 +16,25 @@
 
 package com.google.crypto.tink.tinkey;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.crypto.tink.Aead;
-import com.google.crypto.tink.BinaryKeysetReader;
-import com.google.crypto.tink.BinaryKeysetWriter;
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.JsonKeysetReader;
-import com.google.crypto.tink.JsonKeysetWriter;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
 import com.google.crypto.tink.KeyTemplate;
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.KeysetManager;
-import com.google.crypto.tink.KeysetReader;
-import com.google.crypto.tink.KeysetWriter;
-import com.google.crypto.tink.KmsClients;
-import com.google.crypto.tink.proto.OutputPrefixType;
-import com.google.protobuf.ByteString;
+import com.google.crypto.tink.Parameters;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.security.GeneralSecurityException;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Locale;
 
 /** Various helpers. */
 final class TinkeyUtil {
@@ -54,22 +53,8 @@
     PROMOTE_KEY
   }
 
-  /** Creates a {@code KeysetReader} that can read the keyset in the right {@code inFormat}. */
-  public static KeysetReader createKeysetReader(InputStream inputStream, String inFormat)
-      throws IOException {
-    if (inFormat == null || inFormat.toLowerCase().equals("json")) {
-      return JsonKeysetReader.withInputStream(inputStream);
-    }
-    return BinaryKeysetReader.withInputStream(inputStream);
-  }
-
-  /** Creates a {@code KeysetWriter} that can write the keyset in the right {@code outFormat}. */
-  public static KeysetWriter createKeysetWriter(OutputStream outputStream, String outFormat)
-      throws IOException {
-    if (outFormat == null || outFormat.toLowerCase().equals("json")) {
-      return JsonKeysetWriter.withOutputStream(outputStream);
-    }
-    return BinaryKeysetWriter.withOutputStream(outputStream);
+  private static boolean isJson(String outFormat) {
+    return outFormat == null || outFormat.toLowerCase(Locale.ROOT).equals("json");
   }
 
   /**
@@ -98,9 +83,7 @@
       String credentialPath,
       KeyTemplate keyTemplate)
       throws GeneralSecurityException, IOException {
-    @SuppressWarnings("deprecation") // Need to maintain backward-compatibility
-    KeysetHandle handle =
-        KeysetManager.withEmptyKeyset().rotate(toProto(keyTemplate)).getKeysetHandle();
+    KeysetHandle handle = KeysetHandle.generateNew(keyTemplate);
 
     writeKeyset(handle, outputStream, outFormat, masterKeyUri, credentialPath);
   }
@@ -154,52 +137,24 @@
       String inFormat,
       String masterKeyUri,
       String credentialPath,
-      KeyTemplate keyTemplate)
+      Parameters parameters)
       throws GeneralSecurityException, IOException {
-    KeysetManager manager =
-        KeysetManager.withKeysetHandle(
+    KeysetHandle.Builder builder =
+        KeysetHandle.newBuilder(
             getKeysetHandle(inputStream, inFormat, masterKeyUri, credentialPath));
     switch (type) {
       case ADD_KEY:
-        manager.add(keyTemplate);
+        builder.addEntry(KeysetHandle.generateEntryFromParameters(parameters).withRandomId());
         break;
       case ROTATE_KEYSET:
-        manager.rotate(toProto(keyTemplate));
+        builder.addEntry(
+            KeysetHandle.generateEntryFromParameters(parameters).withRandomId().makePrimary());
         break;
       default:
         throw new GeneralSecurityException("invalid command");
     }
 
-    writeKeyset(manager.getKeysetHandle(), outputStream, outFormat, masterKeyUri, credentialPath);
-  }
-
-  // TODO(b/153937575): remove this once KeysetManager allows to directly work with KeyTemplate
-  // POJO.
-  private static com.google.crypto.tink.proto.KeyTemplate toProto(KeyTemplate template) {
-    OutputPrefixType prefixType;
-
-    switch (template.getOutputPrefixType()) {
-      case TINK:
-        prefixType = OutputPrefixType.TINK;
-        break;
-      case LEGACY:
-        prefixType = OutputPrefixType.LEGACY;
-        break;
-      case RAW:
-        prefixType = OutputPrefixType.RAW;
-        break;
-      case CRUNCHY:
-        prefixType = OutputPrefixType.CRUNCHY;
-        break;
-      default:
-        throw new IllegalArgumentException("Unknown output prefix type");
-    }
-
-    return com.google.crypto.tink.proto.KeyTemplate.newBuilder()
-        .setTypeUrl(template.getTypeUrl())
-        .setValue(ByteString.copyFrom(template.getValue()))
-        .setOutputPrefixType(prefixType)
-        .build();
+    writeKeyset(builder.build(), outputStream, outFormat, masterKeyUri, credentialPath);
   }
 
   /**
@@ -213,16 +168,57 @@
       String masterKeyUri,
       String credentialPath)
       throws GeneralSecurityException, IOException {
-    KeysetWriter writer = createKeysetWriter(outputStream, outFormat);
     if (masterKeyUri != null) {
       Aead masterKey =
-          KmsClients.getAutoLoaded(masterKeyUri)
+          KmsClientsFactory.globalInstance()
+              .newClientFor(masterKeyUri)
               .withCredentials(credentialPath)
               .getAead(masterKeyUri);
-      handle.write(writer, masterKey);
-    } else {
-      CleartextKeysetHandle.write(handle, writer);
+      if (isJson(outFormat)) {
+        outputStream.write(
+            TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(handle, masterKey, new byte[] {})
+                .getBytes(UTF_8));
+        return;
+      } else {
+        outputStream.write(
+            TinkProtoKeysetFormat.serializeEncryptedKeyset(handle, masterKey, new byte[] {}));
+        return;
+      }
     }
+    if (isJson(outFormat)) {
+      outputStream.write(
+          TinkJsonProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get())
+              .getBytes(UTF_8));
+      return;
+    }
+    outputStream.write(
+        TinkProtoKeysetFormat.serializeKeyset(handle, InsecureSecretKeyAccess.get()));
+    return;
+  }
+
+  private static byte[] combine(ArrayDeque<byte[]> deque) {
+    int totalSize = 0;
+    for (byte[] buf : deque) {
+      totalSize += buf.length;
+    }
+    byte[] result = new byte[totalSize];
+    int curSize = 0;
+    for (byte[] buf : deque) {
+      System.arraycopy(buf, 0, result, curSize, buf.length);
+      curSize += buf.length;
+    }
+    return result;
+  }
+
+  private static byte[] toByteArray(InputStream in) throws IOException {
+    ArrayDeque<byte[]> deque = new ArrayDeque<>();
+    byte[] buf = new byte[100];
+    int read = in.read(buf);
+    while (read != -1) {
+      deque.add(Arrays.copyOf(buf, read));
+      read = in.read(buf);
+    }
+    return combine(deque);
   }
 
   /**
@@ -232,15 +228,25 @@
   public static KeysetHandle getKeysetHandle(
       InputStream inputStream, String inFormat, String masterKeyUri, String credentialPath)
       throws IOException, GeneralSecurityException {
-    KeysetReader reader = createKeysetReader(inputStream, inFormat);
+    byte[] keyset = toByteArray(inputStream);
     if (masterKeyUri != null) {
       Aead masterKey =
-          KmsClients.getAutoLoaded(masterKeyUri)
+          KmsClientsFactory.globalInstance()
+              .newClientFor(masterKeyUri)
               .withCredentials(credentialPath)
               .getAead(masterKeyUri);
-      return KeysetHandle.read(reader, masterKey);
+      if (isJson(inFormat)) {
+        return TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            new String(keyset, UTF_8), masterKey, new byte[] {});
+      } else {
+        return TinkProtoKeysetFormat.parseEncryptedKeyset(keyset, masterKey, new byte[] {});
+      }
     }
-    return CleartextKeysetHandle.read(reader);
+    if (isJson(inFormat)) {
+      return TinkJsonProtoKeysetFormat.parseKeyset(
+          new String(keyset, UTF_8), InsecureSecretKeyAccess.get());
+    }
+    return TinkProtoKeysetFormat.parseKeyset(keyset, InsecureSecretKeyAccess.get());
   }
 
   /**
@@ -257,11 +263,5 @@
     }
   }
 
-  /** Prints an error then exits. */
-  public static void die(String error) {
-    System.err.print(String.format("Error: %s\n", error));
-    System.exit(1);
-  }
-
   private TinkeyUtil() {}
 }
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/AddKeyCommandTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/AddKeyCommandTest.java
index fb71f6e..2c6c9d1 100644
--- a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/AddKeyCommandTest.java
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/AddKeyCommandTest.java
@@ -17,116 +17,234 @@
 package com.google.crypto.tink.tinkey;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.KeyTemplate;
-import com.google.crypto.tink.KeyTemplates;
-import com.google.crypto.tink.KeysetReader;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.mac.MacConfig;
-import com.google.crypto.tink.proto.EncryptedKeyset;
-import com.google.crypto.tink.proto.Keyset;
-import com.google.crypto.tink.proto.KeysetInfo;
-import com.google.crypto.tink.testing.TestUtil;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/**
- * Tests for {@code AddKeyCommand}.
- */
+/** Tests for {@code AddKeyCommand}. */
 @RunWith(JUnit4.class)
 public class AddKeyCommandTest {
-  private static KeyTemplate existingTemplate;
-  private static KeyTemplate newTemplate;
-  private static final String OUTPUT_FORMAT = "json";
-  private static final String INPUT_FORMAT = "json";
-
   @BeforeClass
   public static void setUp() throws Exception {
+    AeadConfig.register();
     MacConfig.register();
-    existingTemplate = KeyTemplates.get("HMAC_SHA256_128BITTAG");
-    newTemplate = KeyTemplates.get("HMAC_SHA256_256BITTAG");
-  }
-
-  private KeysetReader addNewKeyToKeyset(String outFormat, InputStream inputStream,
-      String inFormat, String masterKeyUri, String credentialPath, KeyTemplate template)
-      throws Exception {
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    AddKeyCommand.add(
-        outputStream, outFormat,
-        inputStream, inFormat,
-        masterKeyUri, credentialPath,
-        template);
-    return TinkeyUtil.createKeysetReader(
-        new ByteArrayInputStream(outputStream.toByteArray()), outFormat);
+    KmsClientsFactory.globalInstance().addFactory(TinkeyTestKmsClient::new);
   }
 
   @Test
-  public void testAddCleartext_shouldAddNewKey() throws Exception {
-    // Create an input stream containing a cleartext keyset.
-    String masterKeyUri = null;
-    String credentialPath = null;
-    InputStream inputStream =
-        TinkeyUtil.createKeyset(existingTemplate, INPUT_FORMAT, masterKeyUri, credentialPath);
-    // Add a new key to the existing keyset.
-    Keyset keyset =
-        addNewKeyToKeyset(
-                OUTPUT_FORMAT, inputStream, INPUT_FORMAT, masterKeyUri, credentialPath, newTemplate)
-            .read();
+  public void testAddKey_json_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
 
-    assertThat(keyset.getKeyCount()).isEqualTo(2);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(keyset.getKey(0).getKeyId());
-    TestUtil.assertHmacKey(existingTemplate, keyset.getKey(0));
-    TestUtil.assertHmacKey(newTemplate, keyset.getKey(1));
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "add-key",
+          "--in",
+          inputFile.toString(),
+          "--out",
+          outputFile.toString(),
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+        });
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isTrue();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isFalse();
   }
 
   @Test
-  public void testAddCleartext_shouldThrowExceptionIfExistingKeysetIsEmpty() throws Exception {
-    InputStream emptyStream = new ByteArrayInputStream(new byte[0]);
-    String masterKeyUri = null; // This ensures that the keyset won't be encrypted.
-    String credentialPath = null;
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+  public void testAddKey_binary_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
 
-    try {
-      AddKeyCommand.add(
-          outputStream,
-          OUTPUT_FORMAT,
-          emptyStream,
-          INPUT_FORMAT,
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset);
+
+    Tinkey.main(
+        new String[] {
+          "add-key",
+          "--in",
+          inputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out",
+          outputFile.toString(),
+          "--out-format",
+          "binary",
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+        });
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseKeyset(
+            Files.readAllBytes(outputFile), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isTrue();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isFalse();
+  }
+
+  @Test
+  public void testAddKey_binaryEncrypted_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(inputKeyset, masterKeyAead, new byte[] {});
+    Files.write(inputFile, serializedKeyset);
+
+    Tinkey.main(
+        new String[] {
+          "add-key",
+          "--in",
+          inputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out",
+          outputFile.toString(),
+          "--out-format",
+          "binary",
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+          "--master-key-uri",
           masterKeyUri,
-          credentialPath,
-          newTemplate);
-      fail("Expected IOException");
-    } catch (IOException e) {
-      // expected
-    }
+          "--credential",
+          credentialFile.toString()
+        });
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseEncryptedKeyset(
+            Files.readAllBytes(outputFile), masterKeyAead, new byte[] {});
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isTrue();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isFalse();
   }
 
   @Test
-  public void testAddEncrypted_shouldAddNewKey() throws Exception {
-    // This test requires KMS/internet access and thus cannot run on RBE.
-    assumeFalse(TestUtil.isRemoteBuildExecution());
-    // Create an input stream containing an encrypted keyset.
-    String masterKeyUri = TestUtil.RESTRICTED_CRYPTO_KEY_URI;
-    String credentialPath = TestUtil.SERVICE_ACCOUNT_FILE;
-    InputStream inputStream =
-        TinkeyUtil.createKeyset(existingTemplate, INPUT_FORMAT, masterKeyUri, credentialPath);
-    EncryptedKeyset encryptedKeyset =
-        addNewKeyToKeyset(
-                OUTPUT_FORMAT, inputStream, INPUT_FORMAT, masterKeyUri, credentialPath, newTemplate)
-            .readEncrypted();
-    KeysetInfo keysetInfo = encryptedKeyset.getKeysetInfo();
+  public void testAddKey_jsonEncrypted_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
 
-    assertThat(keysetInfo.getKeyInfoCount()).isEqualTo(2);
-    assertThat(keysetInfo.getPrimaryKeyId()).isEqualTo(keysetInfo.getKeyInfo(0).getKeyId());
-    TestUtil.assertKeyInfo(existingTemplate, keysetInfo.getKeyInfo(0));
-    TestUtil.assertKeyInfo(newTemplate, keysetInfo.getKeyInfo(0));
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            inputKeyset, masterKeyAead, new byte[] {});
+    Files.write(inputFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "add-key",
+          "--in",
+          inputFile.toString(),
+          "--in-format",
+          "json",
+          "--out",
+          outputFile.toString(),
+          "--out-format",
+          "json",
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+          "--master-key-uri",
+          masterKeyUri,
+          "--credential",
+          credentialFile.toString()
+        });
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), masterKeyAead, new byte[] {});
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isTrue();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isFalse();
   }
 
+  @Test
+  public void testAddKey_notValidKeyset_fails() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Files.write(inputFile, new byte[] {});
+
+    assertThrows(
+        Exception.class,
+        () ->
+            Tinkey.main(
+                new String[] {
+                  "add-key",
+                  "--in",
+                  inputFile.toString(),
+                  "--in-format",
+                  "binary",
+                  "--out",
+                  outputFile.toString(),
+                  "--out-format",
+                  "binary",
+                  "--key-template",
+                  "HMAC_SHA256_256BITTAG",
+                }));
+  }
 }
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/BUILD.bazel b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/BUILD.bazel
index 5597fe2..a9bdd39 100644
--- a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/BUILD.bazel
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/BUILD.bazel
@@ -4,31 +4,27 @@
     name = "CreatePublicKeysetCommandTest",
     size = "small",
     srcs = ["CreatePublicKeysetCommandTest.java"],
-    data = ["//testdata/gcp:credentials"],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
     deps = [
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:create_public_keyset_command",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
         "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_util",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
-        "@tink_java//proto:tink_java_proto",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
         "@tink_java//src/main/java/com/google/crypto/tink:cleartext_keyset_handle",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_template",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_reader",
-        "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
-        "@tink_java//src/main/java/com/google/crypto/tink:public_key_verify",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:key",
+        "@tink_java//src/main/java/com/google/crypto/tink:private_key",
         "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "@tink_java//src/main/java/com/google/crypto/tink/hybrid:hybrid_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/signature:ed25519_parameters",
         "@tink_java//src/main/java/com/google/crypto/tink/signature:signature_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
     ],
 )
 
@@ -36,22 +32,21 @@
     name = "CreateKeysetCommandTest",
     size = "small",
     srcs = ["CreateKeysetCommandTest.java"],
-    data = ["//testdata/gcp:credentials"],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
     deps = [
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:create_keyset_command",
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_util",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
-        "@tink_java//proto:tink_java_proto",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_template",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@tink_java//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
     ],
 )
 
@@ -59,23 +54,21 @@
     name = "AddKeyCommandTest",
     size = "small",
     srcs = ["AddKeyCommandTest.java"],
-    data = ["//testdata/gcp:credentials"],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
     deps = [
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:add_key_command",
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_util",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
-        "@tink_java//proto:tink_java_proto",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_template",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@tink_java//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
     ],
 )
 
@@ -83,22 +76,72 @@
     name = "RotateKeysetCommandTest",
     size = "small",
     srcs = ["RotateKeysetCommandTest.java"],
-    data = ["//testdata/gcp:credentials"],
-    tags = [
-        "manual",
-        "no_rbe",
-        "requires-network",
-    ],
     deps = [
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:rotate_keyset_command",
-        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_util",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
-        "@tink_java//proto:tink_java_proto",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_template",
-        "@tink_java//src/main/java/com/google/crypto/tink:key_templates",
-        "@tink_java//src/main/java/com/google/crypto/tink:keyset_reader",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
         "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
+        "@tink_java//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
+    ],
+)
+
+java_test(
+    name = "ConvertKeysetCommandTest",
+    size = "small",
+    srcs = ["ConvertKeysetCommandTest.java"],
+    deps = [
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_json_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+        "@tink_java//src/main/java/com/google/crypto/tink/mac:mac_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/mac:predefined_mac_parameters",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
+    ],
+)
+
+
+java_test(
+    name = "TinkeyTestKmsClientTest",
+    size = "small",
+    srcs = ["TinkeyTestKmsClientTest.java"],
+    deps = [
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+        "@tink_java//src/main/java/com/google/crypto/tink:aead",
+        "@tink_java//src/main/java/com/google/crypto/tink:kms_client",
+        "@tink_java//src/main/java/com/google/crypto/tink:registry_cluster",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:aead_config",
+        "@tink_java//src/main/java/com/google/crypto/tink/aead:predefined_aead_parameters",
+    ],
+)
+
+java_test(
+    name = "KmsClientsFactoryTest",
+    size = "small",
+    srcs = ["KmsClientsFactoryTest.java"],
+    deps = [
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:kms_clients_factory",
+        "//tinkey/src/main/java/com/google/crypto/tink/tinkey:tinkey_test_kms_client",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
     ],
 )
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/ConvertKeysetCommandTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/ConvertKeysetCommandTest.java
new file mode 100644
index 0000000..c02608c
--- /dev/null
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/ConvertKeysetCommandTest.java
@@ -0,0 +1,217 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.tinkey;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertTrue;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ConvertKeysetCommandTest {
+  private Path tempDirectory;
+  private Path credentialFile;
+  private KeysetHandle masterKeyAeadKeyset;
+  private Aead masterKeyAead;
+  private String masterKeyUri;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AeadConfig.register();
+    MacConfig.register();
+    KmsClientsFactory.globalInstance().addFactory(TinkeyTestKmsClient::new);
+  }
+
+  @Before
+  public void setUpEncryption() throws Exception {
+    tempDirectory = Files.createTempDirectory(/* prefix= */ "");
+    credentialFile = Paths.get(tempDirectory.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    masterKeyAeadKeyset = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+  }
+
+  private static KeysetHandle createArbitraryKeyset() throws GeneralSecurityException {
+    return KeysetHandle.newBuilder()
+        .addEntry(
+            KeysetHandle.generateEntryFromParameters(PredefinedMacParameters.HMAC_SHA256_128BITTAG)
+                .withRandomId()
+                .makePrimary())
+        .addEntry(
+            KeysetHandle.generateEntryFromParameters(PredefinedMacParameters.HMAC_SHA256_128BITTAG)
+                .withRandomId())
+        .build();
+  }
+
+  @Test
+  public void testConvertKeyset_json2Binary_works() throws Exception {
+    Path inputFile = Paths.get(tempDirectory.toString(), "input");
+    Path outputFile = Paths.get(tempDirectory.toString(), "output");
+
+    KeysetHandle inputKeyset = createArbitraryKeyset();
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "convert-keyset",
+          "--in",
+          inputFile.toString(),
+          "--out",
+          outputFile.toString(),
+          "--in-format",
+          "json",
+          "--out-format",
+          "binary",
+        });
+
+    KeysetHandle outputKeyset =
+        TinkProtoKeysetFormat.parseKeyset(
+            Files.readAllBytes(outputFile), InsecureSecretKeyAccess.get());
+
+    assertThat(outputKeyset.size()).isEqualTo(inputKeyset.size());
+    for (int i = 0; i < inputKeyset.size(); i++) {
+      assertTrue(outputKeyset.getAt(i).getKey().equalsKey(inputKeyset.getAt(i).getKey()));
+    }
+  }
+
+  @Test
+  public void testConvertKeyset_binary2Json_works() throws Exception {
+    Path inputFile = Paths.get(tempDirectory.toString(), "input");
+    Path outputFile = Paths.get(tempDirectory.toString(), "output");
+
+    KeysetHandle inputKeyset = createArbitraryKeyset();
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset);
+
+    Tinkey.main(
+        new String[] {
+          "convert-keyset",
+          "--in",
+          inputFile.toString(),
+          "--out",
+          outputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out-format",
+          "json",
+        });
+
+    KeysetHandle outputKeyset =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), InsecureSecretKeyAccess.get());
+
+    assertThat(outputKeyset.size()).isEqualTo(inputKeyset.size());
+    for (int i = 0; i < inputKeyset.size(); i++) {
+      assertTrue(outputKeyset.getAt(i).getKey().equalsKey(inputKeyset.getAt(i).getKey()));
+    }
+  }
+
+  @Test
+  public void testConvertKeyset_json2encryptedBinary_works() throws Exception {
+    Path inputFile = Paths.get(tempDirectory.toString(), "input");
+    Path outputFile = Paths.get(tempDirectory.toString(), "output");
+
+    KeysetHandle inputKeyset = createArbitraryKeyset();
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset);
+    Tinkey.main(
+        new String[] {
+          "convert-keyset",
+          "--in",
+          inputFile.toString(),
+          "--out",
+          outputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out-format",
+          "json",
+          "--new-master-key-uri",
+          masterKeyUri,
+          "--new-credential",
+          credentialFile.toString(),
+        });
+
+    KeysetHandle outputKeyset =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), masterKeyAead, new byte[] {});
+
+    assertThat(outputKeyset.size()).isEqualTo(inputKeyset.size());
+    for (int i = 0; i < inputKeyset.size(); i++) {
+      assertTrue(outputKeyset.getAt(i).getKey().equalsKey(inputKeyset.getAt(i).getKey()));
+    }
+  }
+
+  @Test
+  public void testConvertKeyset_encryptedBinary2Json_works() throws Exception {
+    Path inputFile = Paths.get(tempDirectory.toString(), "input");
+    Path outputFile = Paths.get(tempDirectory.toString(), "output");
+
+    KeysetHandle inputKeyset = createArbitraryKeyset();
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(inputKeyset, masterKeyAead, new byte[] {});
+    Files.write(inputFile, serializedKeyset);
+    Tinkey.main(
+        new String[] {
+          "convert-keyset",
+          "--in",
+          inputFile.toString(),
+          "--out",
+          outputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out-format",
+          "json",
+          "--master-key-uri",
+          masterKeyUri,
+          "--credential",
+          credentialFile.toString(),
+        });
+
+    KeysetHandle outputKeyset =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), InsecureSecretKeyAccess.get());
+
+    assertThat(outputKeyset.size()).isEqualTo(inputKeyset.size());
+    for (int i = 0; i < inputKeyset.size(); i++) {
+      assertTrue(outputKeyset.getAt(i).getKey().equalsKey(inputKeyset.getAt(i).getKey()));
+    }
+  }
+}
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreateKeysetCommandTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreateKeysetCommandTest.java
index d2fcdb9..af9afb4 100644
--- a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreateKeysetCommandTest.java
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreateKeysetCommandTest.java
@@ -17,76 +17,157 @@
 package com.google.crypto.tink.tinkey;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeFalse;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.crypto.tink.KeyTemplate;
-import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.mac.MacConfig;
-import com.google.crypto.tink.proto.EncryptedKeyset;
-import com.google.crypto.tink.proto.Keyset;
-import com.google.crypto.tink.proto.KeysetInfo;
-import com.google.crypto.tink.testing.TestUtil;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/**
- * Tests for {@code CreateKeysetCommand}.
- */
+/** Tests for {@code CreateKeysetCommand}. */
 @RunWith(JUnit4.class)
 public class CreateKeysetCommandTest {
-  private static KeyTemplate template;
-
   @BeforeClass
   public static void setUp() throws Exception {
+    AeadConfig.register();
     MacConfig.register();
-    template = KeyTemplates.get("HMAC_SHA256_128BITTAG");
+    KmsClientsFactory.globalInstance().addFactory(TinkeyTestKmsClient::new);
   }
 
   @Test
   public void testCreateCleartext_shouldCreateNewKeyset() throws Exception {
-    testCreateCleartext_shouldCreateNewKeyset("json");
-    testCreateCleartext_shouldCreateNewKeyset("binary");
-  }
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path outputFile = Paths.get(path.toString(), "keyset");
 
-  private void testCreateCleartext_shouldCreateNewKeyset(String outFormat)
-      throws Exception {
-    // Create a cleartext keyset.
-    String masterKeyUri = null; // This ensures that the keyset won't be encrypted.
-    String credentialPath = null;
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    CreateKeysetCommand.create(outputStream, outFormat, masterKeyUri, credentialPath, template);
-    Keyset keyset = TinkeyUtil.createKeysetReader(
-        new ByteArrayInputStream(outputStream.toByteArray()), outFormat).read();
+    Tinkey.main(
+        new String[] {
+          "create-keyset", "--key-template", "HMAC_SHA256_128BITTAG", "--out", outputFile.toString()
+        });
 
-    assertThat(keyset.getKeyCount()).isEqualTo(1);
-    TestUtil.assertHmacKey(template, keyset.getKey(0));
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getPrimary().getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
   }
 
   @Test
-  public void testCreateEncrypted_shouldCreateNewKeyset() throws Exception {
-    testCreateEncrypted_shouldCreateNewKeyset("json");
-    testCreateEncrypted_shouldCreateNewKeyset("binary");
+  public void testCreateCleartext_explicitJson_shouldCreateNewKeyset() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path outputFile = Paths.get(path.toString(), "keyset");
+
+    String commandLine =
+        String.format(
+            "create-keyset --key-template HMAC_SHA256_128BITTAG --out-format json --out %s",
+            outputFile.toString());
+
+    Tinkey.main(commandLine.split(" "));
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getPrimary().getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
   }
 
-  private void testCreateEncrypted_shouldCreateNewKeyset(
-      String outFormat) throws Exception {
-    // This test requires KMS/internet access and thus cannot run on RBE.
-    assumeFalse(TestUtil.isRemoteBuildExecution());
-    // Create an encrypted keyset.
-    String masterKeyUri = TestUtil.RESTRICTED_CRYPTO_KEY_URI;
-    String credentialPath = TestUtil.SERVICE_ACCOUNT_FILE;
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    CreateKeysetCommand.create(outputStream, outFormat, masterKeyUri, credentialPath, template);
-    EncryptedKeyset encryptedKeyset = TinkeyUtil
-        .createKeysetReader(new ByteArrayInputStream(outputStream.toByteArray()), outFormat)
-        .readEncrypted();
-    KeysetInfo keysetInfo = encryptedKeyset.getKeysetInfo();
+  @Test
+  public void testCreateCleartext_binary_shouldCreateNewKeyset() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path outputFile = Paths.get(path.toString(), "keyset");
 
-    assertThat(keysetInfo.getKeyInfoCount()).isEqualTo(1);
-    TestUtil.assertKeyInfo(template, keysetInfo.getKeyInfo(0));
+    String commandLine =
+        String.format(
+            "create-keyset --key-template HMAC_SHA256_128BITTAG --out-format binary --out %s",
+            outputFile);
+
+    Tinkey.main(commandLine.split(" "));
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseKeyset(
+            Files.readAllBytes(outputFile), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getPrimary().getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
   }
+
+  @Test
+  public void testCreateCleartext_withMasterKey_shouldCreateNewKeyset() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path outputFile = Paths.get(path.toString(), "keyset");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    String commandLine =
+        String.format(
+            "create-keyset --key-template HMAC_SHA256_128BITTAG --out-format binary "
+                + "--master-key-uri %s "
+                + "--credential %s "
+                + "--out %s",
+            masterKeyUri, credentialFile, outputFile.toString());
+
+    Tinkey.main(commandLine.split(" "));
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseEncryptedKeyset(
+            Files.readAllBytes(outputFile), masterKeyAead, new byte[] {});
+
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getPrimary().getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+  }
+
+  @Test
+  public void testCreateCleartext_withMasterKey_jsonFormat_shouldCreateNewKeyset()
+      throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path outputFile = Paths.get(path.toString(), "keyset");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    String commandLine =
+        String.format(
+            "create-keyset --key-template HMAC_SHA256_128BITTAG --out-format json "
+                + "--master-key-uri %s "
+                + "--credential %s "
+                + "--out %s",
+            masterKeyUri, credentialFile, outputFile.toString());
+
+    Tinkey.main(commandLine.split(" "));
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), masterKeyAead, new byte[] {});
+
+    assertThat(handle.size()).isEqualTo(1);
+    assertThat(handle.getPrimary().getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+  }
+
 }
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommandTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommandTest.java
index 4cbd7c4..d589fb4 100644
--- a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommandTest.java
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/CreatePublicKeysetCommandTest.java
@@ -17,27 +17,25 @@
 package com.google.crypto.tink.tinkey;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeFalse;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertTrue;
 
-import com.google.crypto.tink.CleartextKeysetHandle;
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.KeyTemplate;
-import com.google.crypto.tink.KeyTemplates;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.crypto.tink.KeysetReader;
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.PublicKeyVerify;
+import com.google.crypto.tink.PrivateKey;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.hybrid.HybridConfig;
-import com.google.crypto.tink.proto.EncryptedKeyset;
-import com.google.crypto.tink.proto.Keyset;
+import com.google.crypto.tink.mac.MacConfig;
+import com.google.crypto.tink.signature.Ed25519Parameters;
 import com.google.crypto.tink.signature.SignatureConfig;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -48,144 +46,161 @@
  */
 @RunWith(JUnit4.class)
 public class CreatePublicKeysetCommandTest {
-  private enum KeyType {
-    HYBRID,
-    SIGNATURE,
-  };
-
-  private static final String OUTPUT_FORMAT = "json";
-  private static final String INPUT_FORMAT = "json";
 
   @BeforeClass
   public static void setUp() throws Exception {
+    AeadConfig.register();
+    MacConfig.register();
     HybridConfig.register();
     SignatureConfig.register();
+    KmsClientsFactory.globalInstance().addFactory(TinkeyTestKmsClient::new);
   }
 
   @Test
-  public void testCreate_hybrid_cleartextPrivate_shouldCreateCleartextPublic()
-      throws Exception {
-    testCreate_cleartextPrivate_shouldCreateCleartextPublic(
-        KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM"), KeyType.HYBRID);
+  public void testCreatePublicKeyset_ed25519Json_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path privateKeyFile = Paths.get(path.toString(), "privateKeyFile");
+    Path publicKeyFile = Paths.get(path.toString(), "publicKeyFile");
+
+    KeysetHandle privateKeyset =
+        KeysetHandle.generateNew(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(privateKeyset, InsecureSecretKeyAccess.get());
+    Files.write(privateKeyFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "create-public-keyset",
+          "--in",
+          privateKeyFile.toString(),
+          "--out",
+          publicKeyFile.toString()
+        });
+
+    KeysetHandle publicKeyset =
+        TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(
+            new String(Files.readAllBytes(publicKeyFile), UTF_8));
+    assertThat(publicKeyset.size()).isEqualTo(1);
+    Key expectedPublicKey = ((PrivateKey) privateKeyset.getPrimary().getKey()).getPublicKey();
+    assertTrue(publicKeyset.getPrimary().getKey().equalsKey(expectedPublicKey));
   }
 
   @Test
-  public void testCreate_hybrid_encryptedPrivate_shouldCreateCleartextPublic() throws Exception {
-    testCreate_encryptedPrivate_shouldCreateCleartextPublic(
-        KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM"), KeyType.HYBRID);
+  public void testCreatePublicKeyset_ed25519Binary_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path privateKeyFile = Paths.get(path.toString(), "privateKeyFile");
+    Path publicKeyFile = Paths.get(path.toString(), "publicKeyFile");
+
+    KeysetHandle privateKeyset =
+        KeysetHandle.generateNew(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(privateKeyset, InsecureSecretKeyAccess.get());
+    Files.write(privateKeyFile, serializedKeyset);
+
+    Tinkey.main(
+        new String[] {
+          "create-public-keyset",
+          "--in",
+          privateKeyFile.toString(),
+          "--in-format",
+          "binary",
+          "--out",
+          publicKeyFile.toString(),
+          "--out-format",
+          "binary"
+        });
+
+    KeysetHandle publicKeyset =
+        TinkProtoKeysetFormat.parseKeysetWithoutSecret(Files.readAllBytes(publicKeyFile));
+    assertThat(publicKeyset.size()).isEqualTo(1);
+    Key expectedPublicKey = ((PrivateKey) privateKeyset.getPrimary().getKey()).getPublicKey();
+    assertTrue(publicKeyset.getPrimary().getKey().equalsKey(expectedPublicKey));
   }
 
   @Test
-  public void testCreate_signature_cleartextPrivate_shouldCreateCleartextPublic()
-      throws Exception {
-    testCreate_cleartextPrivate_shouldCreateCleartextPublic(
-        KeyTemplates.get("ECDSA_P256"), KeyType.SIGNATURE);
-    testCreate_cleartextPrivate_shouldCreateCleartextPublic(
-        KeyTemplates.get("ED25519"), KeyType.SIGNATURE);
+  public void testCreatePublicKeyset_ed25519_encrypted_json_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path privateKeyFile = Paths.get(path.toString(), "privateKeyFile");
+    Path publicKeyFile = Paths.get(path.toString(), "publicKeyFile");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    KeysetHandle privateKeyset =
+        KeysetHandle.generateNew(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
+
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            privateKeyset, masterKeyAead, new byte[] {});
+
+    Files.write(privateKeyFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "create-public-keyset",
+          "--in",
+          privateKeyFile.toString(),
+          "--out",
+          publicKeyFile.toString(),
+          "--master-key-uri",
+          masterKeyUri,
+          "--credential",
+          credentialFile.toString()
+        });
+
+    KeysetHandle publicKeyset =
+        TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(
+            new String(Files.readAllBytes(publicKeyFile), UTF_8));
+    assertThat(publicKeyset.size()).isEqualTo(1);
+    Key expectedPublicKey = ((PrivateKey) privateKeyset.getPrimary().getKey()).getPublicKey();
+    assertTrue(publicKeyset.getPrimary().getKey().equalsKey(expectedPublicKey));
   }
 
   @Test
-  public void testCreate_signature_encryptedPrivate_shouldCreateCleartextPublic() throws Exception {
-    testCreate_encryptedPrivate_shouldCreateCleartextPublic(
-        KeyTemplates.get("ECDSA_P256"), KeyType.SIGNATURE);
-    testCreate_encryptedPrivate_shouldCreateCleartextPublic(
-        KeyTemplates.get("ED25519"), KeyType.SIGNATURE);
-  }
+  public void testCreatePublicKeyset_ed25519_encrypted_jsonBinaryMixed_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path privateKeyFile = Paths.get(path.toString(), "privateKeyFile");
+    Path publicKeyFile = Paths.get(path.toString(), "publicKeyFile");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
 
-  private void testCreate_cleartextPrivate_shouldCreateCleartextPublic(
-      KeyTemplate template, KeyType type) throws Exception {
-    // Create a cleartext private keyset.
-    String masterKeyUri = null;
-    String credentialPath = null;
-    InputStream inputStream1 = TinkeyUtil.createKeyset(
-        template, INPUT_FORMAT, masterKeyUri, credentialPath);
-    KeysetReader privateReader = TinkeyUtil
-        .createKeysetReader(inputStream1, INPUT_FORMAT);
-    // Create the public keyset.
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    inputStream1.mark(inputStream1.available());
-    CreatePublicKeysetCommand.create(
-        outputStream, OUTPUT_FORMAT,
-        inputStream1, INPUT_FORMAT,
-        masterKeyUri, credentialPath);
-    inputStream1.reset();
-    InputStream inputStream2 = new ByteArrayInputStream(outputStream.toByteArray());
-    KeysetReader publicReader = TinkeyUtil
-        .createKeysetReader(inputStream2, OUTPUT_FORMAT);
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
 
-    assertPublicKey(type, privateReader, publicReader);
-  }
+    KeysetHandle privateKeyset =
+        KeysetHandle.generateNew(Ed25519Parameters.create(Ed25519Parameters.Variant.TINK));
 
-  private void testCreate_encryptedPrivate_shouldCreateCleartextPublic(
-      KeyTemplate template, KeyType type) throws Exception {
-    // This test requires KMS/internet access and thus cannot run on RBE.
-    assumeFalse(TestUtil.isRemoteBuildExecution());
-    // Create an input stream containing a cleartext private keyset.
-    String masterKeyUri = TestUtil.RESTRICTED_CRYPTO_KEY_URI;
-    String credentialPath = TestUtil.SERVICE_ACCOUNT_FILE;
-    InputStream inputStream1 = TinkeyUtil.createKeyset(
-        template, INPUT_FORMAT, masterKeyUri, credentialPath);
-    inputStream1.mark(inputStream1.available());
-    final KeysetHandle privateHandle = TinkeyUtil.getKeysetHandle(
-        inputStream1, INPUT_FORMAT, masterKeyUri, credentialPath);
-    inputStream1.reset();
-    KeysetReader privateReader = new KeysetReader() {
-        @Override
-        public Keyset read() throws IOException {
-            return TestUtil.getKeyset(privateHandle);
-        }
-        @Override
-        public EncryptedKeyset readEncrypted() throws IOException {
-            throw new IOException("Not Implemented");
-        }
-    };
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    inputStream1.mark(inputStream1.available());
-    CreatePublicKeysetCommand.create(
-        outputStream, OUTPUT_FORMAT,
-        inputStream1, INPUT_FORMAT,
-        masterKeyUri, credentialPath);
-    inputStream1.reset();
-    InputStream inputStream2 = new ByteArrayInputStream(outputStream.toByteArray());
-    KeysetReader publicReader = TinkeyUtil
-        .createKeysetReader(inputStream2, OUTPUT_FORMAT);
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(privateKeyset, masterKeyAead, new byte[] {});
 
-    assertPublicKey(type, privateReader, publicReader);
-  }
+    Files.write(privateKeyFile, serializedKeyset);
 
-  private void assertHybrid(KeysetReader privateReader, KeysetReader publicReader)
-    throws Exception {
-    KeysetHandle privateHandle = CleartextKeysetHandle.read(privateReader);
-    HybridDecrypt decrypter = privateHandle.getPrimitive(HybridDecrypt.class);
-    KeysetHandle publicHandle = CleartextKeysetHandle.read(publicReader);
-    HybridEncrypt encrypter = publicHandle.getPrimitive(HybridEncrypt.class);
-    byte[] message = Random.randBytes(10);
-    byte[] contextInfo = Random.randBytes(20);
+    Tinkey.main(
+        new String[] {
+          "create-public-keyset",
+          "--in",
+          privateKeyFile.toString(),
+          "--in-format",
+          "binary",
+          "--out",
+          publicKeyFile.toString(),
+          "--master-key-uri",
+          masterKeyUri,
+          "--credential",
+          credentialFile.toString()
+        });
 
-    assertThat(decrypter.decrypt(encrypter.encrypt(message, contextInfo), contextInfo)).isEqualTo(
-        message);
-  }
-
-  private void assertSignature(KeysetReader privateReader, KeysetReader publicReader)
-    throws Exception {
-    byte[] message = Random.randBytes(10);
-    KeysetHandle privateHandle = CleartextKeysetHandle.read(privateReader);
-    PublicKeySign signer = privateHandle.getPrimitive(PublicKeySign.class);
-    KeysetHandle publicHandle = CleartextKeysetHandle.read(publicReader);
-    PublicKeyVerify verifier = publicHandle.getPrimitive(PublicKeyVerify.class);
-
-    verifier.verify(signer.sign(message), message);
-  }
-
-  private void assertPublicKey(KeyType type, KeysetReader privateReader,
-      KeysetReader publicReader) throws Exception {
-    switch (type) {
-        case HYBRID:
-            assertHybrid(privateReader, publicReader);
-            break;
-        case SIGNATURE:
-            assertSignature(privateReader, publicReader);
-            break;
-    }
+    KeysetHandle publicKeyset =
+        TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(
+            new String(Files.readAllBytes(publicKeyFile), UTF_8));
+    assertThat(publicKeyset.size()).isEqualTo(1);
+    Key expectedPublicKey = ((PrivateKey) privateKeyset.getPrimary().getKey()).getPublicKey();
+    assertTrue(publicKeyset.getPrimary().getKey().equalsKey(expectedPublicKey));
   }
 }
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/KmsClientsFactoryTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/KmsClientsFactoryTest.java
new file mode 100644
index 0000000..29bbfaa
--- /dev/null
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/KmsClientsFactoryTest.java
@@ -0,0 +1,66 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.tinkey;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.security.GeneralSecurityException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class KmsClientsFactoryTest {
+  @Test
+  public void testAddAndUse_works() throws Exception {
+    KmsClientsFactory factory = new KmsClientsFactory();
+    factory.addFactory(TinkeyTestKmsClient::new);
+
+    assertThat(factory.newClientFor("tinkey-test-kms-client://some"))
+        .isInstanceOf(TinkeyTestKmsClient.class);
+  }
+
+  @Test
+  public void test_newInstances_differ() throws Exception {
+    KmsClientsFactory factory = new KmsClientsFactory();
+    factory.addFactory(TinkeyTestKmsClient::new);
+
+    assertThat(factory.newClientFor("tinkey-test-kms-client://some"))
+        .isNotEqualTo(factory.newClientFor("tinkey-test-kms-client://some"));
+  }
+
+  @Test
+  public void test_notSupported_throws() throws Exception {
+    KmsClientsFactory factory = new KmsClientsFactory();
+    factory.addFactory(TinkeyTestKmsClient::new);
+
+    assertThrows(
+        GeneralSecurityException.class, () -> factory.newClientFor("not_supported://some"));
+  }
+
+  @Test
+  public void test_multiplePrefixes_works() throws Exception {
+    KmsClientsFactory factory = new KmsClientsFactory();
+    factory.addFactory(() -> TinkeyTestKmsClient.createForPrefix("prefix1:"));
+    factory.addFactory(() -> TinkeyTestKmsClient.createForPrefix("prefix2:"));
+
+    assertThat(factory.newClientFor("prefix1:foo")).isInstanceOf(TinkeyTestKmsClient.class);
+    assertThat(factory.newClientFor("prefix2:bar")).isInstanceOf(TinkeyTestKmsClient.class);
+    assertThrows(GeneralSecurityException.class, () -> factory.newClientFor("prefix3://some"));
+  }
+}
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/RotateKeysetCommandTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/RotateKeysetCommandTest.java
index defa599..8185bb1 100644
--- a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/RotateKeysetCommandTest.java
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/RotateKeysetCommandTest.java
@@ -17,116 +17,247 @@
 package com.google.crypto.tink.tinkey;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
 
-import com.google.crypto.tink.KeyTemplate;
-import com.google.crypto.tink.KeyTemplates;
-import com.google.crypto.tink.KeysetReader;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
 import com.google.crypto.tink.mac.MacConfig;
-import com.google.crypto.tink.proto.EncryptedKeyset;
-import com.google.crypto.tink.proto.Keyset;
-import com.google.crypto.tink.proto.KeysetInfo;
-import com.google.crypto.tink.testing.TestUtil;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import com.google.crypto.tink.mac.PredefinedMacParameters;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/**
- * Tests for {@code RotateKeysetCommand}.
-*/
+/** Tests for {@code roate-key}. */
 @RunWith(JUnit4.class)
 public class RotateKeysetCommandTest {
-  private static KeyTemplate existingTemplate;
-  private static KeyTemplate newTemplate;
-  private static final String OUTPUT_FORMAT = "json";
-  private static final String INPUT_FORMAT = "json";
-
   @BeforeClass
   public static void setUp() throws Exception {
+    AeadConfig.register();
     MacConfig.register();
-    existingTemplate = KeyTemplates.get("HMAC_SHA256_128BITTAG");
-    newTemplate = KeyTemplates.get("HMAC_SHA256_256BITTAG");
-  }
-
-  private KeysetReader addNewKeyToKeyset(String outFormat, InputStream inputStream,
-      String inFormat, String masterKeyUri, String credentialPath, KeyTemplate template)
-      throws Exception {
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    RotateKeysetCommand.rotate(
-        outputStream, outFormat,
-        inputStream, inFormat,
-        masterKeyUri, credentialPath,
-        template);
-    return TinkeyUtil.createKeysetReader(
-        new ByteArrayInputStream(outputStream.toByteArray()), outFormat);
+    KmsClientsFactory.globalInstance().addFactory(TinkeyTestKmsClient::new);
   }
 
   @Test
-  public void testRotateCleartext_shouldAddNewKey() throws Exception {
-    // Create an input stream containing a cleartext keyset.
-    String masterKeyUri = null;
-    String credentialPath = null;
-    InputStream inputStream =
-        TinkeyUtil.createKeyset(existingTemplate, INPUT_FORMAT, masterKeyUri, credentialPath);
-    // Add a new key to the existing keyset.
-    Keyset keyset =
-        addNewKeyToKeyset(
-                OUTPUT_FORMAT, inputStream, INPUT_FORMAT, masterKeyUri, credentialPath, newTemplate)
-            .read();
+  public void testRotateKey_json_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
 
-    assertThat(keyset.getKeyCount()).isEqualTo(2);
-    assertThat(keyset.getPrimaryKeyId()).isEqualTo(keyset.getKey(1).getKeyId());
-    TestUtil.assertHmacKey(existingTemplate, keyset.getKey(0));
-    TestUtil.assertHmacKey(newTemplate, keyset.getKey(1));
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "rotate-keyset",
+          "--in",
+          inputFile.toString(),
+          "--out",
+          outputFile.toString(),
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+        });
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isFalse();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isTrue();
   }
 
   @Test
-  public void testRotateCleartext_shouldThrowExceptionIfExistingKeysetIsEmpty() throws Exception {
-    InputStream emptyStream = new ByteArrayInputStream(new byte[0]);
-    String masterKeyUri = null; // This ensures that the keyset won't be encrypted.
-    String credentialPath = null;
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+  public void testRotateKey_binary_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
 
-    try {
-      RotateKeysetCommand.rotate(
-          outputStream,
-          OUTPUT_FORMAT,
-          emptyStream,
-          INPUT_FORMAT,
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeKeyset(inputKeyset, InsecureSecretKeyAccess.get());
+    Files.write(inputFile, serializedKeyset);
+
+    Tinkey.main(
+        new String[] {
+          "rotate-keyset",
+          "--in",
+          inputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out",
+          outputFile.toString(),
+          "--out-format",
+          "binary",
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+        });
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseKeyset(
+            Files.readAllBytes(outputFile), InsecureSecretKeyAccess.get());
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isFalse();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isTrue();
+  }
+
+  @Test
+  public void testRotateKey_binaryEncrypted_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    byte[] serializedKeyset =
+        TinkProtoKeysetFormat.serializeEncryptedKeyset(inputKeyset, masterKeyAead, new byte[] {});
+    Files.write(inputFile, serializedKeyset);
+
+    Tinkey.main(
+        new String[] {
+          "rotate-keyset",
+          "--in",
+          inputFile.toString(),
+          "--in-format",
+          "binary",
+          "--out",
+          outputFile.toString(),
+          "--out-format",
+          "binary",
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+          "--master-key-uri",
           masterKeyUri,
-          credentialPath,
-          newTemplate);
-      fail("Expected IOException");
-    } catch (IOException e) {
-      // expected
-    }
+          "--credential",
+          credentialFile.toString()
+        });
+
+    KeysetHandle handle =
+        TinkProtoKeysetFormat.parseEncryptedKeyset(
+            Files.readAllBytes(outputFile), masterKeyAead, new byte[] {});
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isFalse();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isTrue();
   }
 
   @Test
-  public void testRotateEncrypted_shouldAddNewKey() throws Exception {
-    // This test requires KMS/internet access and thus cannot run on RBE.
-    assumeFalse(TestUtil.isRemoteBuildExecution());
-    // Create an input stream containing an encrypted keyset.
-    String masterKeyUri = TestUtil.RESTRICTED_CRYPTO_KEY_URI;
-    String credentialPath = TestUtil.SERVICE_ACCOUNT_FILE;
-    InputStream inputStream =
-        TinkeyUtil.createKeyset(existingTemplate, INPUT_FORMAT, masterKeyUri, credentialPath);
-    EncryptedKeyset encryptedKeyset =
-        addNewKeyToKeyset(
-                OUTPUT_FORMAT, inputStream, INPUT_FORMAT, masterKeyUri, credentialPath, newTemplate)
-            .readEncrypted();
-    KeysetInfo keysetInfo = encryptedKeyset.getKeysetInfo();
+  public void testRotateKey_jsonEncrypted_works() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
 
-    assertThat(keysetInfo.getKeyInfoCount()).isEqualTo(2);
-    assertThat(keysetInfo.getPrimaryKeyId()).isEqualTo(keysetInfo.getKeyInfo(1).getKeyId());
-    TestUtil.assertKeyInfo(existingTemplate, keysetInfo.getKeyInfo(0));
-    TestUtil.assertKeyInfo(newTemplate, keysetInfo.getKeyInfo(0));
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    Aead masterKeyAead = masterKeyAeadKeyset.getPrimitive(Aead.class);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    KeysetHandle inputKeyset =
+        KeysetHandle.generateNew(PredefinedMacParameters.HMAC_SHA256_128BITTAG);
+    String serializedKeyset =
+        TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(
+            inputKeyset, masterKeyAead, new byte[] {});
+    Files.write(inputFile, serializedKeyset.getBytes(UTF_8));
+
+    Tinkey.main(
+        new String[] {
+          "rotate-keyset",
+          "--in",
+          inputFile.toString(),
+          "--in-format",
+          "json",
+          "--out",
+          outputFile.toString(),
+          "--out-format",
+          "json",
+          "--key-template",
+          "HMAC_SHA256_256BITTAG",
+          "--master-key-uri",
+          masterKeyUri,
+          "--credential",
+          credentialFile.toString()
+        });
+
+    KeysetHandle handle =
+        TinkJsonProtoKeysetFormat.parseEncryptedKeyset(
+            new String(Files.readAllBytes(outputFile), UTF_8), masterKeyAead, new byte[] {});
+
+    assertThat(handle.size()).isEqualTo(2);
+    assertThat(handle.getAt(0).getKey().equalsKey(inputKeyset.getAt(0).getKey())).isTrue();
+    assertThat(handle.getAt(0).isPrimary()).isFalse();
+    assertThat(handle.getAt(1).getKey().getParameters())
+        .isEqualTo(PredefinedMacParameters.HMAC_SHA256_256BITTAG);
+    assertThat(handle.getAt(1).isPrimary()).isTrue();
   }
 
+  @Test
+  public void testRotateKey_notValidKeyset_fails() throws Exception {
+    Path path = Files.createTempDirectory(/* prefix= */ "");
+    Path inputFile = Paths.get(path.toString(), "input");
+    Path outputFile = Paths.get(path.toString(), "output");
+    Path credentialFile = Paths.get(path.toString(), "credentials");
+    TinkeyTestKmsClient.createCredentialFile(credentialFile);
+
+    KeysetHandle masterKeyAeadKeyset =
+        KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(masterKeyAeadKeyset);
+
+    Files.write(inputFile, new byte[] {});
+
+    assertThrows(
+        Exception.class,
+        () ->
+            Tinkey.main(
+                new String[] {
+                  "rotate-keyset",
+                  "--in",
+                  inputFile.toString(),
+                  "--in-format",
+                  "binary",
+                  "--out",
+                  outputFile.toString(),
+                  "--out-format",
+                  "binary",
+                  "--key-template",
+                  "HMAC_SHA256_256BITTAG",
+                  "--master-key-uri",
+                  masterKeyUri,
+                  "--credential",
+                  credentialFile.toString()
+                }));
+  }
 }
diff --git a/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/TinkeyTestKmsClientTest.java b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/TinkeyTestKmsClientTest.java
new file mode 100644
index 0000000..67b0f98
--- /dev/null
+++ b/tools/tinkey/src/test/java/com/google/crypto/tink/tinkey/TinkeyTestKmsClientTest.java
@@ -0,0 +1,111 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+package com.google.crypto.tink.tinkey;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.KmsClient;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.aead.PredefinedAeadParameters;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class TinkeyTestKmsClientTest {
+  @BeforeClass
+  public static void setUp() throws Exception {
+    AeadConfig.register();
+    KmsClientsFactory.globalInstance().addFactory(TinkeyTestKmsClient::new);
+  }
+
+  @Test
+  public void test_clientCanBeLoadedWithCredential_works() throws Exception {
+    Path directory = Files.createTempDirectory(/* prefix= */ "");
+    Path credentialPath = Paths.get(directory.toString(), "credentials");
+    Files.write(credentialPath, "VALID CREDENTIALS".getBytes(UTF_8));
+
+    KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(handle);
+    Aead masterKey =
+        new TinkeyTestKmsClient().withCredentials(credentialPath.toString()).getAead(masterKeyUri);
+    Aead manualMasterKey = handle.getPrimitive(Aead.class);
+
+    byte[] ciphertext = manualMasterKey.encrypt(new byte[] {}, new byte[] {});
+    assertThat(masterKey.decrypt(ciphertext, new byte[] {})).isEqualTo(new byte[] {});
+  }
+
+  @Test
+  public void test_clientAllowsCorrectPrefixes_works() throws Exception {
+    assertTrue(new TinkeyTestKmsClient().doesSupport("tinkey-test-kms-client://"));
+    assertFalse(new TinkeyTestKmsClient().doesSupport("somethingelse://"));
+
+    assertTrue(TinkeyTestKmsClient.createForPrefix("a").doesSupport("a://"));
+    assertFalse(TinkeyTestKmsClient.createForPrefix("a").doesSupport("tinkey-test-kms-client://"));
+  }
+
+  @Test
+  public void test_clientCannotBeUsedWithWrongCredentials_throws() throws Exception {
+    Path directory = Files.createTempDirectory(/* prefix= */ "");
+    Path credentialPath = Paths.get(directory.toString(), "credentials");
+    Files.write(credentialPath, "these are not valid credentials".getBytes(UTF_8));
+
+    KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(handle);
+    KmsClient client = new TinkeyTestKmsClient().withCredentials(credentialPath.toString());
+    assertThrows(GeneralSecurityException.class, () -> client.getAead(masterKeyUri));
+  }
+
+  @Test
+  public void test_clientCannotBeUsedWithoutCallingWithCredential_throws() throws Exception {
+    KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(handle);
+    KmsClient client = new TinkeyTestKmsClient();
+
+    assertThrows(GeneralSecurityException.class, () -> client.getAead(masterKeyUri));
+  }
+
+  @Test
+  public void test_differentPrefix_works() throws Exception {
+    Path directory = Files.createTempDirectory(/* prefix= */ "");
+    Path credentialPath = Paths.get(directory.toString(), "credentials");
+    Files.write(credentialPath, "VALID CREDENTIALS".getBytes(UTF_8));
+
+    KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM);
+    String masterKeyUri = TinkeyTestKmsClient.createKeyUri(handle);
+    Aead masterKey =
+        KmsClientsFactory.globalInstance()
+            .newClientFor(masterKeyUri)
+            .withCredentials(credentialPath.toString())
+            .getAead(masterKeyUri);
+    Aead manualMasterKey = handle.getPrimitive(Aead.class);
+
+    byte[] ciphertext = manualMasterKey.encrypt(new byte[] {}, new byte[] {});
+    assertThat(masterKey.decrypt(ciphertext, new byte[] {})).isEqualTo(new byte[] {});
+  }
+}
OSZAR »